mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-31 07:12:23 -07:00
27
front/plugins/adguard_import/README.md
Normal file
27
front/plugins/adguard_import/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
## Overview
|
||||
|
||||
Plugin functionality overview and links to external resources if relevant. Include use cases if available.
|
||||
|
||||
> [!TIP]
|
||||
> Some tip.
|
||||
|
||||
### Quick setup guide
|
||||
|
||||
To set up the plugin correctly, make sure...
|
||||
|
||||
#### Required Settings
|
||||
|
||||
- When to run `PREF_RUN`
|
||||
-
|
||||
|
||||
### Usage
|
||||
|
||||
- Head to **Settings** > **Plugin name** to adjust the default values.
|
||||
|
||||
### Notes
|
||||
|
||||
- Additional notes, limitations, Author info.
|
||||
|
||||
- Version: 1.0.0
|
||||
- Author: `<your github handle>`
|
||||
- Release Date: `<release date>`
|
||||
158
front/plugins/adguard_import/adguard_import.py
Normal file
158
front/plugins/adguard_import/adguard_import.py
Normal file
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
from pytz import timezone
|
||||
|
||||
# Define the installation path and extend the system path for plugin imports
|
||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from const import logPath # noqa: E402, E261
|
||||
from plugin_helper import Plugin_Objects # noqa: E402, E261
|
||||
from utils.crypto_utils import string_to_mac_hash # noqa: E402 [flake8 lint suppression]
|
||||
from logger import mylog, Logger # noqa: E402, E261
|
||||
from helper import get_setting_value # noqa: E402, E261
|
||||
import conf # noqa: E402, E261
|
||||
|
||||
# ----------------------------
|
||||
# Plugin metadata
|
||||
# ----------------------------
|
||||
pluginName = "ADGUARDIMP"
|
||||
|
||||
# Make sure the TIMEZONE for logging is correct
|
||||
conf.tz = timezone(get_setting_value("TIMEZONE"))
|
||||
|
||||
# Make sure log level is initialized correctly
|
||||
Logger(get_setting_value("LOG_LEVEL"))
|
||||
|
||||
# Define paths
|
||||
LOG_PATH = logPath + "/plugins"
|
||||
LOG_FILE = os.path.join(LOG_PATH, f"script.{pluginName}.log")
|
||||
RESULT_FILE = os.path.join(LOG_PATH, f"last_result.{pluginName}.log")
|
||||
|
||||
plugin_objects = Plugin_Objects(RESULT_FILE)
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Helpers
|
||||
# ----------------------------
|
||||
def ag_request(path, server, port, protocol, auth, timeout):
|
||||
"""Unified request handler"""
|
||||
url = f"{protocol}://{server}:{port}{path}"
|
||||
|
||||
try:
|
||||
r = requests.get(url, auth=auth, timeout=timeout, verify=False)
|
||||
if r.status_code != 200:
|
||||
mylog("none", [f"[{pluginName}] Failed request {url} -> {r.status_code}"])
|
||||
return None
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
mylog("none", [f"[{pluginName}] Exception accessing {url}: {e}"])
|
||||
return None
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# MAIN
|
||||
# ----------------------------
|
||||
def main():
|
||||
mylog("verbose", [f"[{pluginName}] In script"])
|
||||
|
||||
# Retrieve plugin settings
|
||||
server = get_setting_value("ADGUARDIMP_SERVER")
|
||||
port = get_setting_value("ADGUARDIMP_PORT")
|
||||
protocol = get_setting_value("ADGUARDIMP_PROTOCOL") or "http"
|
||||
user = get_setting_value("ADGUARDIMP_USER")
|
||||
pw = get_setting_value("ADGUARDIMP_PASS")
|
||||
fake_mac_enabled = get_setting_value("ADGUARDIMP_FAKE_MAC")
|
||||
timeout = int(get_setting_value("ADGUARDIMP_RUN_TIMEOUT") or 5)
|
||||
|
||||
auth = (user, pw) if user or pw else None
|
||||
|
||||
# -------------------------------------------
|
||||
# Fetch clients from AdGuard Home
|
||||
# -------------------------------------------
|
||||
clients_json = ag_request(
|
||||
"/control/clients",
|
||||
server, port, protocol, auth, timeout
|
||||
)
|
||||
|
||||
if not clients_json:
|
||||
mylog("none", [f"[{pluginName}] No clients returned"])
|
||||
plugin_objects.write_result_file()
|
||||
return 1
|
||||
|
||||
raw_clients = clients_json.get("auto_clients", []) or []
|
||||
|
||||
# -------------------------------------------
|
||||
# Fetch DHCP leases if DHCP enabled
|
||||
# -------------------------------------------
|
||||
dhcp_json = ag_request(
|
||||
"/control/dhcp/status",
|
||||
server, port, protocol, auth, timeout
|
||||
)
|
||||
|
||||
dhcp_leases = []
|
||||
if dhcp_json and dhcp_json.get("enabled"):
|
||||
dhcp_leases = dhcp_json.get("leases", [])
|
||||
|
||||
# Build MAC lookup table for DHCP
|
||||
dhcp_mac_map = {}
|
||||
for lease in dhcp_leases:
|
||||
ip = lease.get("ip")
|
||||
mac = lease.get("mac")
|
||||
if ip and mac:
|
||||
dhcp_mac_map[ip] = mac.upper()
|
||||
|
||||
# -------------------------------------------
|
||||
# Process devices
|
||||
# -------------------------------------------
|
||||
device_data = []
|
||||
|
||||
for cl in raw_clients:
|
||||
ip = cl.get("ip")
|
||||
hostname = cl.get("name") or ""
|
||||
dsource = cl.get("source") or ""
|
||||
|
||||
# Determine MAC
|
||||
mac = dhcp_mac_map.get(ip)
|
||||
|
||||
if not mac and fake_mac_enabled:
|
||||
mylog("verbose", [f"[{pluginName}] Generating FAKE MAC for ip: {ip}"])
|
||||
mac = string_to_mac_hash(ip)
|
||||
|
||||
if not mac:
|
||||
# Skip devices without MAC if fake MAC not allowed
|
||||
mylog("verbose", [f"[{pluginName}] Skipping device with {ip} as no MAC supplied and ADGUARDIMP_FAKE_MAC set to False"])
|
||||
continue
|
||||
|
||||
device_data.append({
|
||||
"mac_address": mac,
|
||||
"ip_address": ip,
|
||||
"hostname": hostname,
|
||||
"device_type": dsource
|
||||
})
|
||||
|
||||
# -------------------------------------------
|
||||
# Write plugin objects
|
||||
# -------------------------------------------
|
||||
for dev in device_data:
|
||||
plugin_objects.add_object(
|
||||
primaryId = dev["mac_address"],
|
||||
secondaryId = dev["ip_address"],
|
||||
watched1 = dev["hostname"],
|
||||
watched2 = dev["device_type"],
|
||||
watched3 = '',
|
||||
watched4 = '',
|
||||
extra = '',
|
||||
foreignKey = dev["mac_address"],
|
||||
)
|
||||
|
||||
mylog("verbose", [f"[{pluginName}] New entries: {len(device_data)}"])
|
||||
plugin_objects.write_result_file()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
504
front/plugins/adguard_import/config.json
Normal file
504
front/plugins/adguard_import/config.json
Normal file
@@ -0,0 +1,504 @@
|
||||
{
|
||||
"code_name": "adguard_import",
|
||||
"unique_prefix": "ADGUARDIMP",
|
||||
"plugin_type": "device_scanner",
|
||||
"execution_order" : "Layer_0",
|
||||
"enabled": true,
|
||||
"data_source": "script",
|
||||
"mapped_to_table": "CurrentScan",
|
||||
"data_filters": [
|
||||
{
|
||||
"compare_column": "Object_PrimaryID",
|
||||
"compare_operator": "==",
|
||||
"compare_field_id": "txtMacFilter",
|
||||
"compare_js_template": "'{value}'.toString()",
|
||||
"compare_use_quotes": true
|
||||
}
|
||||
],
|
||||
"show_ui": true,
|
||||
"localized": ["display_name", "description", "icon"],
|
||||
"display_name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "AdGuard (Device import)"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Plugin to ..."
|
||||
}
|
||||
],
|
||||
"icon": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "<i class=\"fa fa-search\"></i>"
|
||||
}
|
||||
],
|
||||
"params": [],
|
||||
"settings": [
|
||||
{
|
||||
"function": "RUN",
|
||||
"events": ["run"],
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{ "elementType": "select", "elementOptions": [], "transformers": [] }
|
||||
]
|
||||
},
|
||||
|
||||
"default_value": "disabled",
|
||||
"options": [
|
||||
"disabled",
|
||||
"before_name_updates",
|
||||
"on_new_device",
|
||||
"once",
|
||||
"schedule",
|
||||
"always_after_scan"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "When to run"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "When the plugin should run. A good option is <code>schedule</code> for device scanners."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "RUN_SCHD",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "span",
|
||||
"elementOptions": [
|
||||
{
|
||||
"cssClasses": "input-group-addon validityCheck"
|
||||
},
|
||||
{
|
||||
"getStringKey": "Gen_ValidIcon"
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
},
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [
|
||||
{
|
||||
"focusout": "validateRegex(this)"
|
||||
},
|
||||
{
|
||||
"base64Regex": "Xig/OlwqfCg/OlswLTldfFsxLTVdWzAtOV18WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlswLTldfDFbMC05XXwyWzAtM118WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlsxLTldfFsxMl1bMC05XXwzWzAxXXxbMC05XSstWzAtOV0rfFwqL1swLTldKykpXHMrKD86XCp8KD86WzEtOV18MVswLTJdfFswLTldKy1bMC05XSt8XCovWzAtOV0rKSlccysoPzpcKnwoPzpbMC02XXxbMC02XS1bMC02XXxcKi9bMC05XSspKSQ="
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": "*/5 * * * *",
|
||||
"options": [],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Schedule"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Only enabled if you select <code>schedule</code> in the <a href=\"#SYNC_RUN\"><code>SYNC_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "CMD",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [{ "readonly": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": "python3 /app/front/plugins/adguard_import/adguard_import.py",
|
||||
"options": [],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Command"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Command to run. This can not be changed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "RUN_TIMEOUT",
|
||||
"type": {
|
||||
"dataType": "integer",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [{ "type": "number" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": 30,
|
||||
"options": [],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Run timeout"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SERVER",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{ "elementType": "input", "elementOptions": [], "transformers": [] }
|
||||
]
|
||||
},
|
||||
"maxLength": 200,
|
||||
"default_value": "",
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "AdGuard Home Server"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Hostname or IP of your AdGuard Home server. <br>Example: <code>192.168.1.10</code> or <code>adguard.local</code>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "PORT",
|
||||
"type": {
|
||||
"dataType": "integer",
|
||||
"elements": [
|
||||
{ "elementType": "input", "elementOptions": [{ "type": "number" }], "transformers": [] }
|
||||
]
|
||||
},
|
||||
"default_value": 3000,
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{ "language_code": "en_us", "string": "Port" }
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Port used by AdGuard Home API. Default is normally <code>3000</code>."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "PROTOCOL",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{ "elementType": "select", "elementOptions": [], "transformers": [] }
|
||||
]
|
||||
},
|
||||
"default_value": "http",
|
||||
"options": ["http", "https"],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{ "language_code": "en_us", "string": "Protocol" }
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Choose whether to use HTTP or HTTPS to connect to the AdGuard Home API."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "USER",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{ "elementType": "input", "elementOptions": [], "transformers": [] }
|
||||
]
|
||||
},
|
||||
"maxLength": 200,
|
||||
"default_value": "",
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{ "language_code": "en_us", "string": "Username" }
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "API username for AdGuard Home. Leave empty if your API does not require login."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "PASS",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [{ "type": "password" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"maxLength": 200,
|
||||
"default_value": "",
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{ "language_code": "en_us", "string": "Password" }
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "API password for AdGuard Home. Leave empty if authentication is disabled."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "FAKE_MAC",
|
||||
"type": {
|
||||
"dataType": "boolean",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [{ "type": "checkbox" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": false,
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{ "language_code": "en_us", "string": "Generate Fake MACs" }
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Some devices don't have a MAC assigned. Enabling the FAKE_MAC setting generates a fake MAC address from the IP address to track devices, but it may cause inconsistencies if IPs change or devices are re-discovered with a different MAC. Static IPs are recommended. Device type and icon might not be detected correctly and some plugins might fail if they depend on a valid MAC address. When unchecked, devices with empty MAC addresses are skipped."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"database_column_definitions": [
|
||||
{
|
||||
"column": "Index",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": true,
|
||||
"type": "none",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Index"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Object_PrimaryID",
|
||||
"mapped_to_column": "cur_MAC",
|
||||
"css_classes": "col-sm-3",
|
||||
"show": true,
|
||||
"type": "device_name_mac",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "MAC (name)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Object_SecondaryID",
|
||||
"mapped_to_column": "cur_IP",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": true,
|
||||
"type": "device_ip",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "IP"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Watched_Value1",
|
||||
"mapped_to_column": "cur_Name",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": true,
|
||||
"type": "label",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Name"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Watched_Value2",
|
||||
"mapped_to_column": "cur_Type",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": true,
|
||||
"type": "label",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Device Type"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Watched_Value3",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": false,
|
||||
"type": "label",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "N/A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Watched_Value4",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": false,
|
||||
"type": "label",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "N/A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Dummy",
|
||||
"mapped_to_column": "cur_ScanMethod",
|
||||
"mapped_to_column_data": {
|
||||
"value": "Example Plugin"
|
||||
},
|
||||
"css_classes": "col-sm-2",
|
||||
"show": false,
|
||||
"type": "label",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "ADGUARDIMP"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "DateTimeCreated",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": true,
|
||||
"type": "label",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Created"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "DateTimeChanged",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": true,
|
||||
"type": "label",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Changed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Status",
|
||||
"css_classes": "col-sm-1",
|
||||
"show": true,
|
||||
"type": "replace",
|
||||
"default_value": "",
|
||||
"options": [
|
||||
{
|
||||
"equals": "watched-not-changed",
|
||||
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
|
||||
},
|
||||
{
|
||||
"equals": "watched-changed",
|
||||
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
|
||||
},
|
||||
{
|
||||
"equals": "new",
|
||||
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
|
||||
},
|
||||
{
|
||||
"equals": "missing-in-last-scan",
|
||||
"replacement": "<div style='text-align:center'><i class='fa-solid fa-question'></i></div>"
|
||||
}
|
||||
],
|
||||
"localized": ["name"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Status"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user