removed old applescript code and files
This commit is contained in:
@@ -21,7 +21,6 @@ import objc
|
|||||||
import yaml
|
import yaml
|
||||||
from Foundation import *
|
from Foundation import *
|
||||||
|
|
||||||
# from . import _applescript
|
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
|
|
||||||
# TODO: find edited photos: see https://github.com/orangeturtle739/photos-export/blob/master/extract_photos.py
|
# TODO: find edited photos: see https://github.com/orangeturtle739/photos-export/blob/master/extract_photos.py
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
""" applescript -- Easy-to-use Python wrapper for NSAppleScript """
|
|
||||||
"""
|
|
||||||
This code is from py-applescript, a public domain package available at:
|
|
||||||
https://github.com/rdhyee/py-applescript
|
|
||||||
|
|
||||||
I've included the whole thing here for simplicity as there is more than one
|
|
||||||
applescript packge on PyPi so there's ambiguity as to which one "import applescript"
|
|
||||||
would use if user had installed another library.
|
|
||||||
|
|
||||||
This package is used instead of the others because it uses a native PyObjC
|
|
||||||
bridge and is thus much faster than others which use osascript.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from Foundation import (
|
|
||||||
NSAppleScript,
|
|
||||||
NSAppleEventDescriptor,
|
|
||||||
NSURL,
|
|
||||||
NSAppleScriptErrorMessage,
|
|
||||||
NSAppleScriptErrorBriefMessage,
|
|
||||||
NSAppleScriptErrorNumber,
|
|
||||||
NSAppleScriptErrorAppName,
|
|
||||||
NSAppleScriptErrorRange,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .aecodecs import Codecs, fourcharcode, AEType, AEEnum
|
|
||||||
from . import kae
|
|
||||||
|
|
||||||
__all__ = ["AppleScript", "ScriptError", "AEType", "AEEnum", "kMissingValue", "kae"]
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
|
||||||
|
|
||||||
|
|
||||||
class AppleScript:
|
|
||||||
""" Represents a compiled AppleScript. The script object is persistent; its handlers may be called multiple times and its top-level properties will retain current state until the script object's disposal.
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
_codecs = Codecs()
|
|
||||||
|
|
||||||
def __init__(self, source=None, path=None):
|
|
||||||
"""
|
|
||||||
source : str | None -- AppleScript source code
|
|
||||||
path : str | None -- full path to .scpt/.applescript file
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- Either the path or the source argument must be provided.
|
|
||||||
|
|
||||||
- If the script cannot be read/compiled, a ScriptError is raised.
|
|
||||||
"""
|
|
||||||
if path:
|
|
||||||
url = NSURL.fileURLWithPath_(path)
|
|
||||||
self._script, errorinfo = NSAppleScript.alloc().initWithContentsOfURL_error_(
|
|
||||||
url, None
|
|
||||||
)
|
|
||||||
if errorinfo:
|
|
||||||
raise ScriptError(errorinfo)
|
|
||||||
elif source:
|
|
||||||
self._script = NSAppleScript.alloc().initWithSource_(source)
|
|
||||||
else:
|
|
||||||
raise ValueError("Missing source or path argument.")
|
|
||||||
if not self._script.isCompiled():
|
|
||||||
errorinfo = self._script.compileAndReturnError_(None)[1]
|
|
||||||
if errorinfo:
|
|
||||||
raise ScriptError(errorinfo)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
s = self.source
|
|
||||||
return "AppleScript({})".format(
|
|
||||||
repr(s) if len(s) < 100 else "{}...{}".format(repr(s)[:80], repr(s)[-17:])
|
|
||||||
)
|
|
||||||
|
|
||||||
##
|
|
||||||
|
|
||||||
def _newevent(self, suite, code, args):
|
|
||||||
evt = NSAppleEventDescriptor.appleEventWithEventClass_eventID_targetDescriptor_returnID_transactionID_(
|
|
||||||
fourcharcode(suite),
|
|
||||||
fourcharcode(code),
|
|
||||||
NSAppleEventDescriptor.nullDescriptor(),
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
evt.setDescriptor_forKeyword_(
|
|
||||||
self._codecs.pack(args), fourcharcode(kae.keyDirectObject)
|
|
||||||
)
|
|
||||||
return evt
|
|
||||||
|
|
||||||
def _unpackresult(self, result, errorinfo):
|
|
||||||
if not result:
|
|
||||||
raise ScriptError(errorinfo)
|
|
||||||
return self._codecs.unpack(result)
|
|
||||||
|
|
||||||
##
|
|
||||||
|
|
||||||
source = property(
|
|
||||||
lambda self: str(self._script.source()), doc="str -- the script's source code"
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self, *args):
|
|
||||||
""" Run the script, optionally passing arguments to its run handler.
|
|
||||||
|
|
||||||
args : anything -- arguments to pass to script, if any; see also supported type mappings documentation
|
|
||||||
Result : anything | None -- the script's return value, if any
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- The run handler must be explicitly declared in order to pass arguments.
|
|
||||||
|
|
||||||
- AppleScript will ignore excess arguments. Passing insufficient arguments will result in an error.
|
|
||||||
|
|
||||||
- If execution fails, a ScriptError is raised.
|
|
||||||
"""
|
|
||||||
if args:
|
|
||||||
evt = self._newevent(kae.kCoreEventClass, kae.kAEOpenApplication, args)
|
|
||||||
return self._unpackresult(*self._script.executeAppleEvent_error_(evt, None))
|
|
||||||
else:
|
|
||||||
return self._unpackresult(*self._script.executeAndReturnError_(None))
|
|
||||||
|
|
||||||
def call(self, name, *args):
|
|
||||||
""" Call the specified user-defined handler.
|
|
||||||
|
|
||||||
name : str -- the handler's name (case-sensitive)
|
|
||||||
args : anything -- arguments to pass to script, if any; see documentation for supported types
|
|
||||||
Result : anything | None -- the script's return value, if any
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- The handler's name must be a user-defined identifier, not an AppleScript keyword; e.g. 'myCount' is acceptable; 'count' is not.
|
|
||||||
|
|
||||||
- AppleScript will ignore excess arguments. Passing insufficient arguments will result in an error.
|
|
||||||
|
|
||||||
- If execution fails, a ScriptError is raised.
|
|
||||||
"""
|
|
||||||
evt = self._newevent(
|
|
||||||
kae.kASAppleScriptSuite, kae.kASPrepositionalSubroutine, args
|
|
||||||
)
|
|
||||||
evt.setDescriptor_forKeyword_(
|
|
||||||
NSAppleEventDescriptor.descriptorWithString_(name),
|
|
||||||
fourcharcode(kae.keyASSubroutineName),
|
|
||||||
)
|
|
||||||
return self._unpackresult(*self._script.executeAppleEvent_error_(evt, None))
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
|
|
||||||
|
|
||||||
class ScriptError(Exception):
|
|
||||||
""" Indicates an AppleScript compilation/execution error. """
|
|
||||||
|
|
||||||
def __init__(self, errorinfo):
|
|
||||||
self._errorinfo = dict(errorinfo)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "ScriptError({})".format(self._errorinfo)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
""" str -- the error message """
|
|
||||||
msg = self._errorinfo.get(NSAppleScriptErrorMessage)
|
|
||||||
if not msg:
|
|
||||||
msg = self._errorinfo.get(NSAppleScriptErrorBriefMessage, "Script Error")
|
|
||||||
return msg
|
|
||||||
|
|
||||||
number = property(
|
|
||||||
lambda self: self._errorinfo.get(NSAppleScriptErrorNumber),
|
|
||||||
doc="int | None -- the error number, if given",
|
|
||||||
)
|
|
||||||
|
|
||||||
appname = property(
|
|
||||||
lambda self: self._errorinfo.get(NSAppleScriptErrorAppName),
|
|
||||||
doc="str | None -- the name of the application that reported the error, where relevant",
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def range(self):
|
|
||||||
""" (int, int) -- the start and end points (1-indexed) within the source code where the error occurred """
|
|
||||||
range = self._errorinfo.get(NSAppleScriptErrorRange)
|
|
||||||
if range:
|
|
||||||
start = range.rangeValue().location
|
|
||||||
end = start + range.rangeValue().length
|
|
||||||
return (start, end)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
msg = self.message
|
|
||||||
for s, v in [
|
|
||||||
(" ({})", self.number),
|
|
||||||
(" app={!r}", self.appname),
|
|
||||||
(" range={0[0]}-{0[1]}", self.range),
|
|
||||||
]:
|
|
||||||
if v is not None:
|
|
||||||
msg += s.format(v)
|
|
||||||
return (
|
|
||||||
msg.encode("ascii", "replace") if sys.version_info.major < 3 else msg
|
|
||||||
) # 2.7 compatibility
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
|
|
||||||
|
|
||||||
kMissingValue = AEType(kae.cMissingValue) # convenience constant
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
""" aecodecs -- Convert from common Python types to Apple Event Manager types and vice-versa. """
|
|
||||||
|
|
||||||
import datetime, struct, sys
|
|
||||||
|
|
||||||
from Foundation import NSAppleEventDescriptor, NSURL
|
|
||||||
|
|
||||||
from . import kae
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Codecs", "AEType", "AEEnum"]
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
|
||||||
|
|
||||||
|
|
||||||
def fourcharcode(code):
|
|
||||||
""" Convert four-char code for use in NSAppleEventDescriptor methods.
|
|
||||||
|
|
||||||
code : bytes -- four-char code, e.g. b'utxt'
|
|
||||||
Result : int -- OSType, e.g. 1970567284
|
|
||||||
"""
|
|
||||||
return struct.unpack(">I", code)[0]
|
|
||||||
|
|
||||||
|
|
||||||
#######
|
|
||||||
|
|
||||||
|
|
||||||
class Codecs:
|
|
||||||
""" Implements mappings for common Python types with direct AppleScript equivalents. Used by AppleScript class. """
|
|
||||||
|
|
||||||
kMacEpoch = datetime.datetime(1904, 1, 1)
|
|
||||||
kUSRF = fourcharcode(kae.keyASUserRecordFields)
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# Clients may add/remove/replace encoder and decoder items:
|
|
||||||
self.encoders = {
|
|
||||||
NSAppleEventDescriptor.class__(): self.packdesc,
|
|
||||||
type(None): self.packnone,
|
|
||||||
bool: self.packbool,
|
|
||||||
int: self.packint,
|
|
||||||
float: self.packfloat,
|
|
||||||
bytes: self.packbytes,
|
|
||||||
str: self.packstr,
|
|
||||||
list: self.packlist,
|
|
||||||
tuple: self.packlist,
|
|
||||||
dict: self.packdict,
|
|
||||||
datetime.datetime: self.packdatetime,
|
|
||||||
AEType: self.packtype,
|
|
||||||
AEEnum: self.packenum,
|
|
||||||
}
|
|
||||||
if sys.version_info.major < 3: # 2.7 compatibility
|
|
||||||
self.encoders[unicode] = self.packstr
|
|
||||||
|
|
||||||
self.decoders = {
|
|
||||||
fourcharcode(k): v
|
|
||||||
for k, v in {
|
|
||||||
kae.typeNull: self.unpacknull,
|
|
||||||
kae.typeBoolean: self.unpackboolean,
|
|
||||||
kae.typeFalse: self.unpackboolean,
|
|
||||||
kae.typeTrue: self.unpackboolean,
|
|
||||||
kae.typeSInt32: self.unpacksint32,
|
|
||||||
kae.typeIEEE64BitFloatingPoint: self.unpackfloat64,
|
|
||||||
kae.typeUTF8Text: self.unpackunicodetext,
|
|
||||||
kae.typeUTF16ExternalRepresentation: self.unpackunicodetext,
|
|
||||||
kae.typeUnicodeText: self.unpackunicodetext,
|
|
||||||
kae.typeLongDateTime: self.unpacklongdatetime,
|
|
||||||
kae.typeAEList: self.unpackaelist,
|
|
||||||
kae.typeAERecord: self.unpackaerecord,
|
|
||||||
kae.typeAlias: self.unpackfile,
|
|
||||||
kae.typeFSS: self.unpackfile,
|
|
||||||
kae.typeFSRef: self.unpackfile,
|
|
||||||
kae.typeFileURL: self.unpackfile,
|
|
||||||
kae.typeType: self.unpacktype,
|
|
||||||
kae.typeEnumeration: self.unpackenumeration,
|
|
||||||
}.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
def pack(self, data):
|
|
||||||
"""Pack Python data.
|
|
||||||
data : anything -- a Python value
|
|
||||||
Result : NSAppleEventDescriptor -- an AE descriptor, or error if no encoder exists for this type of data
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self.encoders[data.__class__](data) # quick lookup by type/class
|
|
||||||
except (KeyError, AttributeError) as e:
|
|
||||||
for (
|
|
||||||
type,
|
|
||||||
encoder,
|
|
||||||
) in (
|
|
||||||
self.encoders.items()
|
|
||||||
): # slower but more thorough lookup that can handle subtypes/subclasses
|
|
||||||
if isinstance(data, type):
|
|
||||||
return encoder(data)
|
|
||||||
raise TypeError(
|
|
||||||
"Can't pack data into an AEDesc (unsupported type): {!r}".format(data)
|
|
||||||
)
|
|
||||||
|
|
||||||
def unpack(self, desc):
|
|
||||||
"""Unpack an Apple event descriptor.
|
|
||||||
desc : NSAppleEventDescriptor
|
|
||||||
Result : anything -- a Python value, or the original NSAppleEventDescriptor if no decoder is found
|
|
||||||
"""
|
|
||||||
decoder = self.decoders.get(desc.descriptorType())
|
|
||||||
# unpack known type
|
|
||||||
if decoder:
|
|
||||||
return decoder(desc)
|
|
||||||
# if it's a record-like desc, unpack as dict with an extra AEType(b'pcls') key containing the desc type
|
|
||||||
rec = desc.coerceToDescriptorType_(fourcharcode(kae.typeAERecord))
|
|
||||||
if rec:
|
|
||||||
rec = self.unpackaerecord(rec)
|
|
||||||
rec[AEType(kae.pClass)] = AEType(struct.pack(">I", desc.descriptorType()))
|
|
||||||
return rec
|
|
||||||
# return as-is
|
|
||||||
return desc
|
|
||||||
|
|
||||||
##
|
|
||||||
|
|
||||||
def _packbytes(self, desctype, data):
|
|
||||||
return NSAppleEventDescriptor.descriptorWithDescriptorType_bytes_length_(
|
|
||||||
fourcharcode(desctype), data, len(data)
|
|
||||||
)
|
|
||||||
|
|
||||||
def packdesc(self, val):
|
|
||||||
return val
|
|
||||||
|
|
||||||
def packnone(self, val):
|
|
||||||
return NSAppleEventDescriptor.nullDescriptor()
|
|
||||||
|
|
||||||
def packbool(self, val):
|
|
||||||
return NSAppleEventDescriptor.descriptorWithBoolean_(int(val))
|
|
||||||
|
|
||||||
def packint(self, val):
|
|
||||||
if (-2 ** 31) <= val < (2 ** 31):
|
|
||||||
return NSAppleEventDescriptor.descriptorWithInt32_(val)
|
|
||||||
else:
|
|
||||||
return self.pack(float(val))
|
|
||||||
|
|
||||||
def packfloat(self, val):
|
|
||||||
return self._packbytes(kae.typeFloat, struct.pack("d", val))
|
|
||||||
|
|
||||||
def packbytes(self, val):
|
|
||||||
return self._packbytes(kae.typeData, val)
|
|
||||||
|
|
||||||
def packstr(self, val):
|
|
||||||
return NSAppleEventDescriptor.descriptorWithString_(val)
|
|
||||||
|
|
||||||
def packdatetime(self, val):
|
|
||||||
delta = val - self.kMacEpoch
|
|
||||||
sec = delta.days * 3600 * 24 + delta.seconds
|
|
||||||
return self._packbytes(kae.typeLongDateTime, struct.pack("q", sec))
|
|
||||||
|
|
||||||
def packlist(self, val):
|
|
||||||
lst = NSAppleEventDescriptor.listDescriptor()
|
|
||||||
for item in val:
|
|
||||||
lst.insertDescriptor_atIndex_(self.pack(item), 0)
|
|
||||||
return lst
|
|
||||||
|
|
||||||
def packdict(self, val):
|
|
||||||
record = NSAppleEventDescriptor.recordDescriptor()
|
|
||||||
usrf = desctype = None
|
|
||||||
for key, value in val.items():
|
|
||||||
if isinstance(key, AEType):
|
|
||||||
if key.code == kae.pClass and isinstance(
|
|
||||||
value, AEType
|
|
||||||
): # AS packs records that contain a 'class' property by coercing the packed record to the descriptor type specified by the property's value (assuming it's an AEType)
|
|
||||||
desctype = value
|
|
||||||
else:
|
|
||||||
record.setDescriptor_forKeyword_(
|
|
||||||
self.pack(value), fourcharcode(key.code)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if not usrf:
|
|
||||||
usrf = NSAppleEventDescriptor.listDescriptor()
|
|
||||||
usrf.insertDescriptor_atIndex_(self.pack(key), 0)
|
|
||||||
usrf.insertDescriptor_atIndex_(self.pack(value), 0)
|
|
||||||
if usrf:
|
|
||||||
record.setDescriptor_forKeyword_(usrf, self.kUSRF)
|
|
||||||
if desctype:
|
|
||||||
newrecord = record.coerceToDescriptorType_(fourcharcode(desctype.code))
|
|
||||||
if newrecord:
|
|
||||||
record = newrecord
|
|
||||||
else: # coercion failed for some reason, so pack as normal key-value pair
|
|
||||||
record.setDescriptor_forKeyword_(
|
|
||||||
self.pack(desctype), fourcharcode(key.code)
|
|
||||||
)
|
|
||||||
return record
|
|
||||||
|
|
||||||
def packtype(self, val):
|
|
||||||
return NSAppleEventDescriptor.descriptorWithTypeCode_(fourcharcode(val.code))
|
|
||||||
|
|
||||||
def packenum(self, val):
|
|
||||||
return NSAppleEventDescriptor.descriptorWithEnumCode_(fourcharcode(val.code))
|
|
||||||
|
|
||||||
#######
|
|
||||||
|
|
||||||
def unpacknull(self, desc):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def unpackboolean(self, desc):
|
|
||||||
return desc.booleanValue()
|
|
||||||
|
|
||||||
def unpacksint32(self, desc):
|
|
||||||
return desc.int32Value()
|
|
||||||
|
|
||||||
def unpackfloat64(self, desc):
|
|
||||||
return struct.unpack("d", bytes(desc.data()))[0]
|
|
||||||
|
|
||||||
def unpackunicodetext(self, desc):
|
|
||||||
return desc.stringValue()
|
|
||||||
|
|
||||||
def unpacklongdatetime(self, desc):
|
|
||||||
return self.kMacEpoch + datetime.timedelta(
|
|
||||||
seconds=struct.unpack("q", bytes(desc.data()))[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
def unpackaelist(self, desc):
|
|
||||||
return [
|
|
||||||
self.unpack(desc.descriptorAtIndex_(i + 1))
|
|
||||||
for i in range(desc.numberOfItems())
|
|
||||||
]
|
|
||||||
|
|
||||||
def unpackaerecord(self, desc):
|
|
||||||
dct = {}
|
|
||||||
for i in range(desc.numberOfItems()):
|
|
||||||
key = desc.keywordForDescriptorAtIndex_(i + 1)
|
|
||||||
value = desc.descriptorForKeyword_(key)
|
|
||||||
if key == self.kUSRF:
|
|
||||||
lst = self.unpackaelist(value)
|
|
||||||
for i in range(0, len(lst), 2):
|
|
||||||
dct[lst[i]] = lst[i + 1]
|
|
||||||
else:
|
|
||||||
dct[AEType(struct.pack(">I", key))] = self.unpack(value)
|
|
||||||
return dct
|
|
||||||
|
|
||||||
def unpacktype(self, desc):
|
|
||||||
return AEType(struct.pack(">I", desc.typeCodeValue()))
|
|
||||||
|
|
||||||
def unpackenumeration(self, desc):
|
|
||||||
return AEEnum(struct.pack(">I", desc.enumCodeValue()))
|
|
||||||
|
|
||||||
def unpackfile(self, desc):
|
|
||||||
url = bytes(
|
|
||||||
desc.coerceToDescriptorType_(fourcharcode(kae.typeFileURL)).data()
|
|
||||||
).decode("utf8")
|
|
||||||
return NSURL.URLWithString_(url).path()
|
|
||||||
|
|
||||||
|
|
||||||
#######
|
|
||||||
|
|
||||||
|
|
||||||
class AETypeBase:
|
|
||||||
""" Base class for AEType and AEEnum.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- Hashable and comparable, so may be used as keys in dictionaries that map to AE records.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, code):
|
|
||||||
"""
|
|
||||||
code : bytes -- four-char code, e.g. b'utxt'
|
|
||||||
"""
|
|
||||||
if not isinstance(code, bytes):
|
|
||||||
raise TypeError("invalid code (not a bytes object): {!r}".format(code))
|
|
||||||
elif len(code) != 4:
|
|
||||||
raise ValueError("invalid code (not four bytes long): {!r}".format(code))
|
|
||||||
self._code = code
|
|
||||||
|
|
||||||
code = property(
|
|
||||||
lambda self: self._code, doc="bytes -- four-char code, e.g. b'utxt'"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(self._code)
|
|
||||||
|
|
||||||
def __eq__(self, val):
|
|
||||||
return val.__class__ == self.__class__ and val.code == self._code
|
|
||||||
|
|
||||||
def __ne__(self, val):
|
|
||||||
return not self == val
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "{}({!r})".format(self.__class__.__name__, self._code)
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
|
|
||||||
|
|
||||||
class AEType(AETypeBase):
|
|
||||||
"""An AE type. Maps to an AppleScript type class, e.g. AEType(b'utxt') <=> 'unicode text'."""
|
|
||||||
|
|
||||||
|
|
||||||
class AEEnum(AETypeBase):
|
|
||||||
"""An AE enumeration. Maps to an AppleScript constant, e.g. AEEnum(b'yes ') <=> 'yes'."""
|
|
||||||
1703
osxphotos/kae.py
1703
osxphotos/kae.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user