ALMOST done args
This commit is contained in:
parent
9c2b26bf7f
commit
e221401e1c
6
pass.py
6
pass.py
@ -7,7 +7,11 @@ def main():
|
|||||||
rawargs = vaultpass.args.parseArgs()
|
rawargs = vaultpass.args.parseArgs()
|
||||||
args = rawargs.parse_args()
|
args = rawargs.parse_args()
|
||||||
if not args.oper:
|
if not args.oper:
|
||||||
args.oper = 'show'
|
rawargs.print_help()
|
||||||
|
return(None)
|
||||||
|
if args.oper == 'help':
|
||||||
|
rawargs.print_help()
|
||||||
|
return(None)
|
||||||
import pprint
|
import pprint
|
||||||
pprint.pprint(vars(args))
|
pprint.pprint(vars(args))
|
||||||
return(None)
|
return(None)
|
||||||
|
@ -5,10 +5,13 @@ from . import constants
|
|||||||
|
|
||||||
def parseArgs():
|
def parseArgs():
|
||||||
args = argparse.ArgumentParser(description = 'VaultPass - a Vault-backed Pass replacement',
|
args = argparse.ArgumentParser(description = 'VaultPass - a Vault-backed Pass replacement',
|
||||||
prog = 'pass',
|
prog = 'VaultPass',
|
||||||
epilog = ('This program has context-specific help. Try "... cp --help". '
|
epilog = ('This program has context-specific help. Try "... cp --help". '
|
||||||
'This help output is intentionally terse; see "man 1 vaultpass" and the '
|
'This help output is intentionally terse; see "man 1 vaultpass" and the '
|
||||||
'README for more complete information, configuration, and usage.'))
|
'README for more complete information, configuration, and usage.'))
|
||||||
|
args.add_argument('-V', '--version',
|
||||||
|
action = 'version',
|
||||||
|
version = '%(prog)s {0}'.format(constants.VERSION))
|
||||||
args.add_argument('-c', '--config',
|
args.add_argument('-c', '--config',
|
||||||
default = '~/.config/vaultpass.xml',
|
default = '~/.config/vaultpass.xml',
|
||||||
help = ('The path to your configuration file. Default: ~/.config/vaultpass.xml'))
|
help = ('The path to your configuration file. Default: ~/.config/vaultpass.xml'))
|
||||||
@ -22,26 +25,56 @@ def parseArgs():
|
|||||||
metavar = 'OPERATION',
|
metavar = 'OPERATION',
|
||||||
dest = 'oper')
|
dest = 'oper')
|
||||||
cp = subparser.add_parser('cp',
|
cp = subparser.add_parser('cp',
|
||||||
|
description = ('Copy a secret from one path to another'),
|
||||||
|
help = ('Copy a secret from one path to another'),
|
||||||
aliases = ['copy'])
|
aliases = ['copy'])
|
||||||
edit = subparser.add_parser('edit')
|
edit = subparser.add_parser('edit',
|
||||||
|
description = ('Edit an existing secret or create it if it does not exist'),
|
||||||
|
help = ('Edit an existing secret or create it if it does not exist'))
|
||||||
find = subparser.add_parser('find',
|
find = subparser.add_parser('find',
|
||||||
|
description = ('Find the path to a secret given a regex of the name'),
|
||||||
|
help = ('Find the path to a secret given a regex of the name'),
|
||||||
aliases = ['search'])
|
aliases = ['search'])
|
||||||
gen = subparser.add_parser('generate')
|
gen = subparser.add_parser('generate',
|
||||||
git = subparser.add_parser('git') # Dummy opt; do nothing
|
description = ('Generate a password/passphrase'),
|
||||||
grep = subparser.add_parser('grep')
|
help = ('Generate a password/passphrase'))
|
||||||
helpme = subparser.add_parser('help')
|
# Dummy opt; do nothing
|
||||||
initvault = subparser.add_parser('init')
|
git = subparser.add_parser('git',
|
||||||
|
description = ('This operation does nothing except maintain compatibility'))
|
||||||
|
grep = subparser.add_parser('grep',
|
||||||
|
description = ('Search secret content by regex'),
|
||||||
|
help = ('Search secret content by regex'))
|
||||||
|
helpme = subparser.add_parser('help',
|
||||||
|
description = ('Show this help and exit'),
|
||||||
|
help = ('Show this help and exit'))
|
||||||
|
initvault = subparser.add_parser('init',
|
||||||
|
description = ('This operation does nothing except maintain compatibility'),
|
||||||
|
help = ('This operation does nothing except maintain compatibility'))
|
||||||
insertval = subparser.add_parser('insert',
|
insertval = subparser.add_parser('insert',
|
||||||
|
description = ('Add a new secret (or overwrite one)'),
|
||||||
|
help = ('Add a new secret (or overwrite one)'),
|
||||||
aliases = ['add'])
|
aliases = ['add'])
|
||||||
ls = subparser.add_parser('ls',
|
ls = subparser.add_parser('ls',
|
||||||
|
description = ('List names of secrets available'),
|
||||||
|
help = ('List names of secrets available'),
|
||||||
aliases = ['list'])
|
aliases = ['list'])
|
||||||
mv = subparser.add_parser('mv',
|
mv = subparser.add_parser('mv',
|
||||||
aliases = ['rename'])
|
description = ('Moves a secret to a different path'),
|
||||||
|
help = ('Moves a secret to a different path'),
|
||||||
|
aliases = ['rename', 'move'])
|
||||||
rm = subparser.add_parser('rm',
|
rm = subparser.add_parser('rm',
|
||||||
|
description = ('Delete a secret'),
|
||||||
|
help = ('Delete a secret'),
|
||||||
aliases = ['remove', 'delete'])
|
aliases = ['remove', 'delete'])
|
||||||
show = subparser.add_parser('show')
|
show = subparser.add_parser('show',
|
||||||
version = subparser.add_parser('version')
|
description = ('Print/fetch a secret'),
|
||||||
importvault = subparser.add_parser('import')
|
help = ('Print/fetch a secret'))
|
||||||
|
version = subparser.add_parser('version',
|
||||||
|
description = ('Print the VaultPass version and exit'),
|
||||||
|
help = ('Print the VaultPass version and exit'))
|
||||||
|
importvault = subparser.add_parser('import',
|
||||||
|
description = ('Import your existing Pass into Vault'),
|
||||||
|
help = ('Import your existing Pass into Vault'))
|
||||||
# CP/COPY
|
# CP/COPY
|
||||||
cp.add_argument('-f', '--force',
|
cp.add_argument('-f', '--force',
|
||||||
dest = 'force',
|
dest = 'force',
|
||||||
@ -69,6 +102,7 @@ def parseArgs():
|
|||||||
metavar = 'NAME_PATTERN',
|
metavar = 'NAME_PATTERN',
|
||||||
help = ('List secrets\' paths whose names match the regex NAME_PATTERN'))
|
help = ('List secrets\' paths whose names match the regex NAME_PATTERN'))
|
||||||
# GENERATE
|
# GENERATE
|
||||||
|
# TODO: feature parity with passgen (spaces? etc.)
|
||||||
gen.add_argument('-n', '--no-symbols',
|
gen.add_argument('-n', '--no-symbols',
|
||||||
dest = 'symbols',
|
dest = 'symbols',
|
||||||
action = 'store_false',
|
action = 'store_false',
|
||||||
@ -130,94 +164,94 @@ def parseArgs():
|
|||||||
## DUMMY OPTIONS ##
|
## DUMMY OPTIONS ##
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
grep.add_argument('-V', '--version',
|
grep.add_argument('-V', '--version',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-E', '--extended-regexp',
|
grep.add_argument('-E', '--extended-regexp',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-F', '--fixed-strings',
|
grep.add_argument('-F', '--fixed-strings',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-G', '--basic-regexp',
|
grep.add_argument('-G', '--basic-regexp',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-P', '--perl-regexp',
|
grep.add_argument('-P', '--perl-regexp',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-i', '--ignore_case',
|
grep.add_argument('-i', '--ignore_case',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('--no-ignore-case',
|
grep.add_argument('--no-ignore-case',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-v', '--invert-match',
|
grep.add_argument('-v', '--invert-match',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-w', '--word-regexp',
|
grep.add_argument('-w', '--word-regexp',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-x', '--line-regexp',
|
grep.add_argument('-x', '--line-regexp',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-y',
|
grep.add_argument('-y',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-c', '--count',
|
grep.add_argument('-c', '--count',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-L', '--files-without-match',
|
grep.add_argument('-L', '--files-without-match',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-l', '--files-with-matches',
|
grep.add_argument('-l', '--files-with-matches',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-o', '--only-matching',
|
grep.add_argument('-o', '--only-matching',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-q', '--quiet', '--silent',
|
grep.add_argument('-q', '--quiet', '--silent',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-s', '--no-messages',
|
grep.add_argument('-s', '--no-messages',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-b', '--byte-offset',
|
grep.add_argument('-b', '--byte-offset',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-H', '--with-filename',
|
grep.add_argument('-H', '--with-filename',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-n', '--line-number',
|
grep.add_argument('-n', '--line-number',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-T', '--initial-tab',
|
grep.add_argument('-T', '--initial-tab',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-u', '--unix-byte-offsets',
|
grep.add_argument('-u', '--unix-byte-offsets',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-Z', '--null',
|
grep.add_argument('-Z', '--null',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-a', '--text',
|
grep.add_argument('-a', '--text',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-I',
|
grep.add_argument('-I',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-r', '--recursive',
|
grep.add_argument('-r', '--recursive',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-R', '--dereference-recursive',
|
grep.add_argument('-R', '--dereference-recursive',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('--line-buffered',
|
grep.add_argument('--line-buffered',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-U', '--binary',
|
grep.add_argument('-U', '--binary',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-z', '--null-data',
|
grep.add_argument('-z', '--null-data',
|
||||||
action='store_true',
|
action = 'store_true',
|
||||||
help = ('(Dummy option; kept for compatibility reasons)'))
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
grep.add_argument('-e', '--regexp',
|
grep.add_argument('-e', '--regexp',
|
||||||
dest = 'dummy_0',
|
dest = 'dummy_0',
|
||||||
@ -285,5 +319,81 @@ def parseArgs():
|
|||||||
grep.add_argument('pattern',
|
grep.add_argument('pattern',
|
||||||
metavar = 'REGEX_PATTERN',
|
metavar = 'REGEX_PATTERN',
|
||||||
help = ('Regex pattern to search passwords'))
|
help = ('Regex pattern to search passwords'))
|
||||||
|
# HELP has no arguments.
|
||||||
|
# INIT
|
||||||
|
initvault.add_argument('-p', '--path',
|
||||||
|
dest = 'path',
|
||||||
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
|
initvault.add_argument('gpg_id',
|
||||||
|
dest = 'gpg_id',
|
||||||
|
help = ('(Dummy option; kept for compatibility reasons)'))
|
||||||
|
# INSERT
|
||||||
|
# TODO: if -e/--echo is specified and sys.stdin, use sys.stdin rather than prompt
|
||||||
|
insertval.add_argument('-e', '--echo',
|
||||||
|
dest = 'allow_shouldersurf',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, enable keyboard echo (show the secret as it\'s being typed) and '
|
||||||
|
'disable confirmation'))
|
||||||
|
insertval.add_argument('-m', '--multiline',
|
||||||
|
action = 'store_true',
|
||||||
|
dest = 'multiline',
|
||||||
|
help = ('If specified, keep reading stdin until EOF is reached or ctrl-d is pressed'))
|
||||||
|
insertval.add_argument('-f', '--force',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, overwrite any existing secret without prompting'))
|
||||||
|
insertval.add_argument('-n', '--no-confirm',
|
||||||
|
dest = 'confirm',
|
||||||
|
action = 'store_false',
|
||||||
|
help = ('If specified, disable password prompt confirmation. '
|
||||||
|
'Has no effect if -e/--echo is specified'))
|
||||||
|
# LS
|
||||||
|
ls.add_argument('path',
|
||||||
|
metavar = 'PATH/TO/TREE/BASE',
|
||||||
|
help = ('List names of secrets recursively, starting at PATH/TO/TREE/BASE'))
|
||||||
|
# MV
|
||||||
|
mv.add_argument('-f', '--force',
|
||||||
|
dest = 'force',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, replace NEWPATH if it exists'))
|
||||||
|
mv.add_argument('oldpath',
|
||||||
|
metavar = 'OLDPATH',
|
||||||
|
help = ('The original ("source") path for the secret'))
|
||||||
|
mv.add_argument('newpath',
|
||||||
|
metavar = 'NEWPATH',
|
||||||
|
help = ('The new ("destination") path for the secret'))
|
||||||
|
# RM
|
||||||
|
# Is this argument even sensible since it isn't a filesystem?
|
||||||
|
rm.add_argument('-r', '--recursive',
|
||||||
|
dest = 'recurse',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If PATH/TO/SECRET is a directory, delete all subentries'))
|
||||||
|
rm.add_argument('-f', '--force',
|
||||||
|
dest = 'force',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, delete all matching path(s) without prompting for confirmation'))
|
||||||
|
rm.add_argument('path',
|
||||||
|
metavar = 'PATH/TO/SECRET',
|
||||||
|
help = ('The path to the secret or subdirectory'))
|
||||||
|
# SHOW
|
||||||
|
show.add_argument('-c', '--clip',
|
||||||
|
nargs = '?',
|
||||||
|
type = int,
|
||||||
|
default = constants.SHOW_CLIP_LINENUM,
|
||||||
|
metavar = 'LINE_NUMBER',
|
||||||
|
dest = 'clip',
|
||||||
|
help = ('If specified, copy line number LINE_NUMBER (Default: {0}) from the secret to the '
|
||||||
|
'clipboard instead of printing it.').format(constants.SHOW_CLIP_LINENUM))
|
||||||
|
show.add_argument('-q', '--qrcode',
|
||||||
|
nargs = '?',
|
||||||
|
type = int,
|
||||||
|
metavar = 'LINE_NUMBER',
|
||||||
|
default = constants.SHOW_CLIP_LINENUM,
|
||||||
|
help = ('If specified, do not print the secret at but instead '))
|
||||||
|
show.add_argument('-s', '--seconds',
|
||||||
|
dest = 'seconds',
|
||||||
|
type = int,
|
||||||
|
default = constants.CLIP_TIMEOUT,
|
||||||
|
help = ('If copying to the clipboard (see -c/--clip), clear the clipboard after this many '
|
||||||
|
'seconds. Default: {0}').format(constants.CLIP_TIMEOUT))
|
||||||
|
|
||||||
return(args)
|
return(args)
|
||||||
|
@ -10,6 +10,7 @@ NUM_PASS_CHARS = string.digits
|
|||||||
ALPHANUM_PASS_CHARS = ALPHA_PASS_CHARS + NUM_PASS_CHARS
|
ALPHANUM_PASS_CHARS = ALPHA_PASS_CHARS + NUM_PASS_CHARS
|
||||||
SYMBOL_PASS_CHARS = string.punctuation
|
SYMBOL_PASS_CHARS = string.punctuation
|
||||||
ALL_PASS_CHARS = ALPHANUM_PASS_CHARS + SYMBOL_PASS_CHARS
|
ALL_PASS_CHARS = ALPHANUM_PASS_CHARS + SYMBOL_PASS_CHARS
|
||||||
|
SHOW_CLIP_LINENUM = 1
|
||||||
# These CAN be generated dynamically, see below.
|
# These CAN be generated dynamically, see below.
|
||||||
CLIP_TIMEOUT = 45
|
CLIP_TIMEOUT = 45
|
||||||
SELECTED_PASS_CHARS = ALL_PASS_CHARS
|
SELECTED_PASS_CHARS = ALL_PASS_CHARS
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import warnings
|
import warnings
|
||||||
##
|
##
|
||||||
import dpath.util # https://pypi.org/project/dpath/
|
import dpath.util # https://pypi.org/project/dpath/
|
||||||
@ -174,12 +175,12 @@ class MountHandler(object):
|
|||||||
return(None)
|
return(None)
|
||||||
|
|
||||||
def printer(self, output = None, indent = 4):
|
def printer(self, output = None, indent = 4):
|
||||||
def treePrint(obj, s = 'Password Store\n', level = 0):
|
# def treePrint(obj, s = 'Password Store\n', level = 0):
|
||||||
prefix = '├──'
|
# prefix = '├──'
|
||||||
leading_prefix = '│'
|
# leading_prefix = '│'
|
||||||
last_prefix = '└──'
|
# last_prefix = '└──'
|
||||||
pass
|
# pass
|
||||||
return(s)
|
# return(s)
|
||||||
if output:
|
if output:
|
||||||
output = output.lower()
|
output = output.lower()
|
||||||
if output and output not in ('pretty', 'yaml', 'json'):
|
if output and output not in ('pretty', 'yaml', 'json'):
|
||||||
@ -207,7 +208,7 @@ class MountHandler(object):
|
|||||||
import pprint
|
import pprint
|
||||||
if indent is None:
|
if indent is None:
|
||||||
indent = 1
|
indent = 1
|
||||||
return(pprint.pformat(self.paths, indent = indent))
|
return(pprint.pformat(self.paths, indent = indent, width = shutil.get_terminal_size((80, 20)).columns))
|
||||||
# elif output == 'tree':
|
# elif output == 'tree':
|
||||||
# import tree # TODO? Wayyy later.
|
# import tree # TODO? Wayyy later.
|
||||||
elif not output:
|
elif not output:
|
||||||
|
Reference in New Issue
Block a user