Updated JSON.lua
This commit is contained in:
parent
03e52b1d41
commit
4fbfd8556f
238
bot/JSON.lua
238
bot/JSON.lua
@ -14,13 +14,13 @@
|
|||||||
-- the web-page links above, and the 'AUTHOR_NOTE' string below are
|
-- the web-page links above, and the 'AUTHOR_NOTE' string below are
|
||||||
-- maintained. Enjoy.
|
-- maintained. Enjoy.
|
||||||
--
|
--
|
||||||
local VERSION = 20140920.13 -- version history at end of file
|
local VERSION = 20141223.14 -- version history at end of file
|
||||||
local AUTHOR_NOTE = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20140920.13 ]-"
|
local AUTHOR_NOTE = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20141223.14 ]-"
|
||||||
|
|
||||||
--
|
--
|
||||||
-- The 'AUTHOR_NOTE' variable exists so that information about the source
|
-- The 'AUTHOR_NOTE' variable exists so that information about the source
|
||||||
-- of the package is maintained even in compiled versions. It's included in
|
-- of the package is maintained even in compiled versions. It's also
|
||||||
-- OBJDEF mostly to quiet warnings about unused variables.
|
-- included in OBJDEF below mostly to quiet warnings about unused variables.
|
||||||
--
|
--
|
||||||
local OBJDEF = {
|
local OBJDEF = {
|
||||||
VERSION = VERSION,
|
VERSION = VERSION,
|
||||||
@ -33,7 +33,7 @@ local OBJDEF = {
|
|||||||
-- http://www.json.org/
|
-- http://www.json.org/
|
||||||
--
|
--
|
||||||
--
|
--
|
||||||
-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines
|
-- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines
|
||||||
--
|
--
|
||||||
-- local lua_value = JSON:decode(raw_json_text)
|
-- local lua_value = JSON:decode(raw_json_text)
|
||||||
--
|
--
|
||||||
@ -41,9 +41,11 @@ local OBJDEF = {
|
|||||||
-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability
|
-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability
|
||||||
--
|
--
|
||||||
--
|
--
|
||||||
-- DECODING
|
|
||||||
--
|
--
|
||||||
-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines
|
-- DECODING (from a JSON string to a Lua table)
|
||||||
|
--
|
||||||
|
--
|
||||||
|
-- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines
|
||||||
--
|
--
|
||||||
-- local lua_value = JSON:decode(raw_json_text)
|
-- local lua_value = JSON:decode(raw_json_text)
|
||||||
--
|
--
|
||||||
@ -58,22 +60,27 @@ local OBJDEF = {
|
|||||||
-- { "Larry", "Curly", "Moe" }
|
-- { "Larry", "Curly", "Moe" }
|
||||||
--
|
--
|
||||||
--
|
--
|
||||||
-- The encode and decode routines accept an optional second argument, "etc", which is not used
|
-- The encode and decode routines accept an optional second argument,
|
||||||
-- during encoding or decoding, but upon error is passed along to error handlers. It can be of any
|
-- "etc", which is not used during encoding or decoding, but upon error
|
||||||
-- type (including nil).
|
-- is passed along to error handlers. It can be of any type (including nil).
|
||||||
|
--
|
||||||
|
--
|
||||||
|
--
|
||||||
|
-- ERROR HANDLING
|
||||||
--
|
--
|
||||||
-- With most errors during decoding, this code calls
|
-- With most errors during decoding, this code calls
|
||||||
--
|
--
|
||||||
-- JSON:onDecodeError(message, text, location, etc)
|
-- JSON:onDecodeError(message, text, location, etc)
|
||||||
--
|
--
|
||||||
-- with a message about the error, and if known, the JSON text being parsed and the byte count
|
-- with a message about the error, and if known, the JSON text being
|
||||||
-- where the problem was discovered. You can replace the default JSON:onDecodeError() with your
|
-- parsed and the byte count where the problem was discovered. You can
|
||||||
-- own function.
|
-- replace the default JSON:onDecodeError() with your own function.
|
||||||
--
|
--
|
||||||
-- The default onDecodeError() merely augments the message with data about the text and the
|
-- The default onDecodeError() merely augments the message with data
|
||||||
-- location if known (and if a second 'etc' argument had been provided to decode(), its value is
|
-- about the text and the location if known (and if a second 'etc'
|
||||||
-- tacked onto the message as well), and then calls JSON.assert(), which itself defaults to Lua's
|
-- argument had been provided to decode(), its value is tacked onto the
|
||||||
-- built-in assert(), and can also be overridden.
|
-- message as well), and then calls JSON.assert(), which itself defaults
|
||||||
|
-- to Lua's built-in assert(), and can also be overridden.
|
||||||
--
|
--
|
||||||
-- For example, in an Adobe Lightroom plugin, you might use something like
|
-- For example, in an Adobe Lightroom plugin, you might use something like
|
||||||
--
|
--
|
||||||
@ -95,9 +102,10 @@ local OBJDEF = {
|
|||||||
--
|
--
|
||||||
-- JSON:onDecodeOfHTMLError(message, text, nil, etc)
|
-- JSON:onDecodeOfHTMLError(message, text, nil, etc)
|
||||||
--
|
--
|
||||||
-- The use of the fourth 'etc' argument allows stronger coordination between decoding and error
|
-- The use of the fourth 'etc' argument allows stronger coordination
|
||||||
-- reporting, especially when you provide your own error-handling routines. Continuing with the
|
-- between decoding and error reporting, especially when you provide your
|
||||||
-- the Adobe Lightroom plugin example:
|
-- own error-handling routines. Continuing with the the Adobe Lightroom
|
||||||
|
-- plugin example:
|
||||||
--
|
--
|
||||||
-- function JSON:onDecodeError(message, text, location, etc)
|
-- function JSON:onDecodeError(message, text, location, etc)
|
||||||
-- local note = "Internal Error: invalid JSON data"
|
-- local note = "Internal Error: invalid JSON data"
|
||||||
@ -121,42 +129,136 @@ local OBJDEF = {
|
|||||||
--
|
--
|
||||||
--
|
--
|
||||||
--
|
--
|
||||||
|
--
|
||||||
-- DECODING AND STRICT TYPES
|
-- DECODING AND STRICT TYPES
|
||||||
--
|
--
|
||||||
-- Because both JSON objects and JSON arrays are converted to Lua tables, it's not normally
|
-- Because both JSON objects and JSON arrays are converted to Lua tables,
|
||||||
-- possible to tell which a JSON type a particular Lua table was derived from, or guarantee
|
-- it's not normally possible to tell which original JSON type a
|
||||||
-- decode-encode round-trip equivalency.
|
-- particular Lua table was derived from, or guarantee decode-encode
|
||||||
|
-- round-trip equivalency.
|
||||||
--
|
--
|
||||||
-- However, if you enable strictTypes, e.g.
|
-- However, if you enable strictTypes, e.g.
|
||||||
--
|
--
|
||||||
-- JSON = (loadfile "JSON.lua")() --load the routines
|
-- JSON = assert(loadfile "JSON.lua")() --load the routines
|
||||||
-- JSON.strictTypes = true
|
-- JSON.strictTypes = true
|
||||||
--
|
--
|
||||||
-- then the Lua table resulting from the decoding of a JSON object or JSON array is marked via Lua
|
-- then the Lua table resulting from the decoding of a JSON object or
|
||||||
-- metatable, so that when re-encoded with JSON:encode() it ends up as the appropriate JSON type.
|
-- JSON array is marked via Lua metatable, so that when re-encoded with
|
||||||
|
-- JSON:encode() it ends up as the appropriate JSON type.
|
||||||
--
|
--
|
||||||
-- (This is not the default because other routines may not work well with tables that have a
|
-- (This is not the default because other routines may not work well with
|
||||||
-- metatable set, for example, Lightroom API calls.)
|
-- tables that have a metatable set, for example, Lightroom API calls.)
|
||||||
--
|
--
|
||||||
--
|
--
|
||||||
-- ENCODING
|
-- ENCODING (from a lua table to a JSON string)
|
||||||
--
|
--
|
||||||
-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines
|
-- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines
|
||||||
--
|
--
|
||||||
-- local raw_json_text = JSON:encode(lua_table_or_value)
|
-- local raw_json_text = JSON:encode(lua_table_or_value)
|
||||||
-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability
|
-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability
|
||||||
|
-- local custom_pretty = JSON:encode(lua_table_or_value, etc, { pretty = true, indent = "| ", align_keys = false })
|
||||||
|
--
|
||||||
-- On error during encoding, this code calls:
|
-- On error during encoding, this code calls:
|
||||||
--
|
--
|
||||||
-- JSON:onEncodeError(message, etc)
|
-- JSON:onEncodeError(message, etc)
|
||||||
--
|
--
|
||||||
-- which you can override in your local JSON object.
|
-- which you can override in your local JSON object.
|
||||||
--
|
--
|
||||||
-- If the Lua table contains both string and numeric keys, it fits neither JSON's
|
-- The 'etc' in the error call is the second argument to encode()
|
||||||
-- idea of an object, nor its idea of an array. To get around this, when any string
|
-- and encode_pretty(), or nil if it wasn't provided.
|
||||||
-- key exists (or when non-positive numeric keys exist), numeric keys are converted
|
--
|
||||||
-- to strings.
|
--
|
||||||
|
-- PRETTY-PRINTING
|
||||||
|
--
|
||||||
|
-- An optional third argument, a table of options, allows a bit of
|
||||||
|
-- configuration about how the encoding takes place:
|
||||||
|
--
|
||||||
|
-- pretty = JSON:encode(val, etc, {
|
||||||
|
-- pretty = true, -- if false, no other options matter
|
||||||
|
-- indent = " ", -- this provides for a three-space indent per nesting level
|
||||||
|
-- align_keys = false, -- see below
|
||||||
|
-- })
|
||||||
|
--
|
||||||
|
-- encode() and encode_pretty() are identical except that encode_pretty()
|
||||||
|
-- provides a default options table if none given in the call:
|
||||||
|
--
|
||||||
|
-- { pretty = true, align_keys = false, indent = " " }
|
||||||
|
--
|
||||||
|
-- For example, if
|
||||||
|
--
|
||||||
|
-- JSON:encode(data)
|
||||||
|
--
|
||||||
|
-- produces:
|
||||||
|
--
|
||||||
|
-- {"city":"Kyoto","climate":{"avg_temp":16,"humidity":"high","snowfall":"minimal"},"country":"Japan","wards":11}
|
||||||
|
--
|
||||||
|
-- then
|
||||||
|
--
|
||||||
|
-- JSON:encode_pretty(data)
|
||||||
|
--
|
||||||
|
-- produces:
|
||||||
|
--
|
||||||
|
-- {
|
||||||
|
-- "city": "Kyoto",
|
||||||
|
-- "climate": {
|
||||||
|
-- "avg_temp": 16,
|
||||||
|
-- "humidity": "high",
|
||||||
|
-- "snowfall": "minimal"
|
||||||
|
-- },
|
||||||
|
-- "country": "Japan",
|
||||||
|
-- "wards": 11
|
||||||
|
-- }
|
||||||
|
--
|
||||||
|
-- The following three lines return identical results:
|
||||||
|
-- JSON:encode_pretty(data)
|
||||||
|
-- JSON:encode_pretty(data, nil, { pretty = true, align_keys = false, indent = " " })
|
||||||
|
-- JSON:encode (data, nil, { pretty = true, align_keys = false, indent = " " })
|
||||||
|
--
|
||||||
|
-- An example of setting your own indent string:
|
||||||
|
--
|
||||||
|
-- JSON:encode_pretty(data, nil, { pretty = true, indent = "| " })
|
||||||
|
--
|
||||||
|
-- produces:
|
||||||
|
--
|
||||||
|
-- {
|
||||||
|
-- | "city": "Kyoto",
|
||||||
|
-- | "climate": {
|
||||||
|
-- | | "avg_temp": 16,
|
||||||
|
-- | | "humidity": "high",
|
||||||
|
-- | | "snowfall": "minimal"
|
||||||
|
-- | },
|
||||||
|
-- | "country": "Japan",
|
||||||
|
-- | "wards": 11
|
||||||
|
-- }
|
||||||
|
--
|
||||||
|
-- An example of setting align_keys to true:
|
||||||
|
--
|
||||||
|
-- JSON:encode_pretty(data, nil, { pretty = true, indent = " ", align_keys = true })
|
||||||
|
--
|
||||||
|
-- produces:
|
||||||
|
--
|
||||||
|
-- {
|
||||||
|
-- "city": "Kyoto",
|
||||||
|
-- "climate": {
|
||||||
|
-- "avg_temp": 16,
|
||||||
|
-- "humidity": "high",
|
||||||
|
-- "snowfall": "minimal"
|
||||||
|
-- },
|
||||||
|
-- "country": "Japan",
|
||||||
|
-- "wards": 11
|
||||||
|
-- }
|
||||||
|
--
|
||||||
|
-- which I must admit is kinda ugly, sorry. This was the default for
|
||||||
|
-- encode_pretty() prior to version 20141223.14.
|
||||||
|
--
|
||||||
|
--
|
||||||
|
-- AMBIGUOUS SITUATIONS DURING THE ENCODING
|
||||||
|
--
|
||||||
|
-- During the encode, if a Lua table being encoded contains both string
|
||||||
|
-- and numeric keys, it fits neither JSON's idea of an object, nor its
|
||||||
|
-- idea of an array. To get around this, when any string key exists (or
|
||||||
|
-- when non-positive numeric keys exist), numeric keys are converted to
|
||||||
|
-- strings.
|
||||||
--
|
--
|
||||||
-- For example,
|
-- For example,
|
||||||
-- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" }))
|
-- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" }))
|
||||||
@ -165,6 +267,9 @@ local OBJDEF = {
|
|||||||
--
|
--
|
||||||
-- To prohibit this conversion and instead make it an error condition, set
|
-- To prohibit this conversion and instead make it an error condition, set
|
||||||
-- JSON.noKeyConversion = true
|
-- JSON.noKeyConversion = true
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
@ -181,6 +286,9 @@ local OBJDEF = {
|
|||||||
--
|
--
|
||||||
---------------------------------------------------------------------------
|
---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local default_pretty_indent = " "
|
||||||
|
local default_pretty_options = { pretty = true, align_keys = false, indent = default_pretty_indent }
|
||||||
|
|
||||||
local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray
|
local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray
|
||||||
local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject
|
local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject
|
||||||
|
|
||||||
@ -692,8 +800,13 @@ end
|
|||||||
--
|
--
|
||||||
-- Encode
|
-- Encode
|
||||||
--
|
--
|
||||||
|
-- 'options' is nil, or a table with possible keys:
|
||||||
|
-- pretty -- if true, return a pretty-printed version
|
||||||
|
-- indent -- a string (usually of spaces) used to indent each nested level
|
||||||
|
-- align_keys -- if true, align all the keys when formatting a table
|
||||||
|
--
|
||||||
local encode_value -- must predeclare because it calls itself
|
local encode_value -- must predeclare because it calls itself
|
||||||
function encode_value(self, value, parents, etc, indent) -- non-nil indent means pretty-printing
|
function encode_value(self, value, parents, etc, options, indent)
|
||||||
|
|
||||||
if value == nil then
|
if value == nil then
|
||||||
return 'null'
|
return 'null'
|
||||||
@ -739,6 +852,13 @@ function encode_value(self, value, parents, etc, indent) -- non-nil indent means
|
|||||||
--
|
--
|
||||||
local T = value
|
local T = value
|
||||||
|
|
||||||
|
if type(options) ~= 'table' then
|
||||||
|
options = {}
|
||||||
|
end
|
||||||
|
if type(indent) ~= 'string' then
|
||||||
|
indent = ""
|
||||||
|
end
|
||||||
|
|
||||||
if parents[T] then
|
if parents[T] then
|
||||||
self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc)
|
self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc)
|
||||||
else
|
else
|
||||||
@ -754,13 +874,13 @@ function encode_value(self, value, parents, etc, indent) -- non-nil indent means
|
|||||||
--
|
--
|
||||||
local ITEMS = { }
|
local ITEMS = { }
|
||||||
for i = 1, maximum_number_key do
|
for i = 1, maximum_number_key do
|
||||||
table.insert(ITEMS, encode_value(self, T[i], parents, etc, indent))
|
table.insert(ITEMS, encode_value(self, T[i], parents, etc, options, indent))
|
||||||
end
|
end
|
||||||
|
|
||||||
if indent then
|
if options.pretty then
|
||||||
result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]"
|
result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]"
|
||||||
else
|
else
|
||||||
result_value = "[" .. table.concat(ITEMS, ",") .. "]"
|
result_value = "[" .. table.concat(ITEMS, ",") .. "]"
|
||||||
end
|
end
|
||||||
|
|
||||||
elseif object_keys then
|
elseif object_keys then
|
||||||
@ -769,22 +889,24 @@ function encode_value(self, value, parents, etc, indent) -- non-nil indent means
|
|||||||
--
|
--
|
||||||
local TT = map or T
|
local TT = map or T
|
||||||
|
|
||||||
if indent then
|
if options.pretty then
|
||||||
|
|
||||||
local KEYS = { }
|
local KEYS = { }
|
||||||
local max_key_length = 0
|
local max_key_length = 0
|
||||||
for _, key in ipairs(object_keys) do
|
for _, key in ipairs(object_keys) do
|
||||||
local encoded = encode_value(self, tostring(key), parents, etc, "")
|
local encoded = encode_value(self, tostring(key), parents, etc, options, indent)
|
||||||
max_key_length = math.max(max_key_length, #encoded)
|
if options.align_keys then
|
||||||
|
max_key_length = math.max(max_key_length, #encoded)
|
||||||
|
end
|
||||||
table.insert(KEYS, encoded)
|
table.insert(KEYS, encoded)
|
||||||
end
|
end
|
||||||
local key_indent = indent .. " "
|
local key_indent = indent .. tostring(options.indent or "")
|
||||||
local subtable_indent = indent .. string.rep(" ", max_key_length + 2 + 4)
|
local subtable_indent = key_indent .. string.rep(" ", max_key_length) .. (options.align_keys and " " or "")
|
||||||
local FORMAT = "%s%" .. string.format("%d", max_key_length) .. "s: %s"
|
local FORMAT = "%s%" .. string.format("%d", max_key_length) .. "s: %s"
|
||||||
|
|
||||||
local COMBINED_PARTS = { }
|
local COMBINED_PARTS = { }
|
||||||
for i, key in ipairs(object_keys) do
|
for i, key in ipairs(object_keys) do
|
||||||
local encoded_val = encode_value(self, TT[key], parents, etc, subtable_indent)
|
local encoded_val = encode_value(self, TT[key], parents, etc, options, subtable_indent)
|
||||||
table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val))
|
table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val))
|
||||||
end
|
end
|
||||||
result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}"
|
result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}"
|
||||||
@ -793,8 +915,8 @@ function encode_value(self, value, parents, etc, indent) -- non-nil indent means
|
|||||||
|
|
||||||
local PARTS = { }
|
local PARTS = { }
|
||||||
for _, key in ipairs(object_keys) do
|
for _, key in ipairs(object_keys) do
|
||||||
local encoded_val = encode_value(self, TT[key], parents, etc, indent)
|
local encoded_val = encode_value(self, TT[key], parents, etc, options, indent)
|
||||||
local encoded_key = encode_value(self, tostring(key), parents, etc, indent)
|
local encoded_key = encode_value(self, tostring(key), parents, etc, options, indent)
|
||||||
table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val))
|
table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val))
|
||||||
end
|
end
|
||||||
result_value = "{" .. table.concat(PARTS, ",") .. "}"
|
result_value = "{" .. table.concat(PARTS, ",") .. "}"
|
||||||
@ -813,18 +935,18 @@ function encode_value(self, value, parents, etc, indent) -- non-nil indent means
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function OBJDEF:encode(value, etc)
|
function OBJDEF:encode(value, etc, options)
|
||||||
if type(self) ~= 'table' or self.__index ~= OBJDEF then
|
if type(self) ~= 'table' or self.__index ~= OBJDEF then
|
||||||
OBJDEF:onEncodeError("JSON:encode must be called in method format", etc)
|
OBJDEF:onEncodeError("JSON:encode must be called in method format", etc)
|
||||||
end
|
end
|
||||||
return encode_value(self, value, {}, etc, nil)
|
return encode_value(self, value, {}, etc, options or nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
function OBJDEF:encode_pretty(value, etc)
|
function OBJDEF:encode_pretty(value, etc, options)
|
||||||
if type(self) ~= 'table' or self.__index ~= OBJDEF then
|
if type(self) ~= 'table' or self.__index ~= OBJDEF then
|
||||||
OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc)
|
OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc)
|
||||||
end
|
end
|
||||||
return encode_value(self, value, {}, etc, "")
|
return encode_value(self, value, {}, etc, options or default_pretty_options)
|
||||||
end
|
end
|
||||||
|
|
||||||
function OBJDEF.__tostring()
|
function OBJDEF.__tostring()
|
||||||
@ -850,6 +972,16 @@ return OBJDEF:new()
|
|||||||
--
|
--
|
||||||
-- Version history:
|
-- Version history:
|
||||||
--
|
--
|
||||||
|
-- 20141223.14 The encode_pretty() routine produced fine results for small datasets, but isn't really
|
||||||
|
-- appropriate for anything large, so with help from Alex Aulbach I've made the encode routines
|
||||||
|
-- more flexible, and changed the default encode_pretty() to be more generally useful.
|
||||||
|
--
|
||||||
|
-- Added a third 'options' argument to the encode() and encode_pretty() routines, to control
|
||||||
|
-- how the encoding takes place.
|
||||||
|
--
|
||||||
|
-- Updated docs to add assert() call to the loadfile() line, just as good practice so that
|
||||||
|
-- if there is a problem loading JSON.lua, the appropriate error message will percolate up.
|
||||||
|
--
|
||||||
-- 20140920.13 Put back (in a way that doesn't cause warnings about unused variables) the author string,
|
-- 20140920.13 Put back (in a way that doesn't cause warnings about unused variables) the author string,
|
||||||
-- so that the source of the package, and its version number, are visible in compiled copies.
|
-- so that the source of the package, and its version number, are visible in compiled copies.
|
||||||
--
|
--
|
||||||
|
Reference in New Issue
Block a user