feat: Finish v1 of plugins
All checks were successful
Build Docker / Build Image (push) Successful in 32s
All checks were successful
Build Docker / Build Image (push) Successful in 32s
This commit is contained in:
parent
78eae529e5
commit
798bac1d2f
Binary file not shown.
110
main.py
110
main.py
@ -11,6 +11,7 @@ from flask_qrcode import QRcode
|
||||
import domainLookup
|
||||
import urllib.parse
|
||||
import importlib
|
||||
import plugin as plugins_module
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
@ -91,10 +92,17 @@ def index():
|
||||
domain_count = len(domains)
|
||||
domains = render.domains(domains)
|
||||
|
||||
plugins = ""
|
||||
dashFunctions = plugins_module.getDashboardFunctions()
|
||||
for function in dashFunctions:
|
||||
functionOutput = plugins_module.runPluginFunction(function["plugin"],function["function"],{},account_module.check_account(request.cookies.get("account")))
|
||||
plugins += render.plugin_output_dash(functionOutput,plugins_module.getPluginFunctionReturns(function["plugin"],function["function"]))
|
||||
|
||||
|
||||
|
||||
|
||||
return render_template("index.html", account=account, available=available,
|
||||
total=total, pending=pending, domains=domains,
|
||||
total=total, pending=pending, domains=domains, plugins=plugins,
|
||||
domain_count=domain_count, sync=account_module.getNodeSync(),
|
||||
sort_price=sort_price,sort_expiry=sort_expiry,
|
||||
sort_domain=sort_domain,sort_price_next=sort_price_next,
|
||||
@ -320,10 +328,20 @@ def search():
|
||||
|
||||
dns = render.dns(dns)
|
||||
txs = render.txs(txs)
|
||||
|
||||
plugins = "<div class='container-fluid'>"
|
||||
# Execute domain plugins
|
||||
searchFunctions = plugins_module.getSearchFunctions()
|
||||
for function in searchFunctions:
|
||||
functionOutput = plugins_module.runPluginFunction(function["plugin"],function["function"],{"domain":search_term},account_module.check_account(request.cookies.get("account")))
|
||||
plugins += render.plugin_output(functionOutput,plugins_module.getPluginFunctionReturns(function["plugin"],function["function"]))
|
||||
|
||||
plugins += "</div>"
|
||||
|
||||
return render_template("search.html", account=account, sync=account_module.getNodeSync(),
|
||||
search_term=search_term,domain=domain['info']['name'],
|
||||
raw=domain,state=state, next=next, owner=owner,
|
||||
dns=dns, txs=txs)
|
||||
dns=dns, txs=txs,plugins=plugins)
|
||||
|
||||
@app.route('/manage/<domain>')
|
||||
def manage(domain: str):
|
||||
@ -373,20 +391,11 @@ def manage(domain: str):
|
||||
|
||||
plugins = "<div class='container-fluid'>"
|
||||
# Execute domain plugins
|
||||
plugin_links = os.listdir("plugins")
|
||||
for plugin in plugin_links:
|
||||
if os.path.isdir("plugins/" + plugin):
|
||||
module = importlib.import_module("plugins." + plugin + ".main")
|
||||
moduleFunctions = module.listFunctions()
|
||||
for moduleFunction in moduleFunctions:
|
||||
data = moduleFunctions[moduleFunction]
|
||||
if "type" in data:
|
||||
if data["type"] == "domain":
|
||||
# Run function
|
||||
print(data)
|
||||
functionOutput = module.runFunction(moduleFunction,{"domain":domain},account_module.check_account(request.cookies.get("account")))
|
||||
print(functionOutput)
|
||||
plugins += render.plugin_output(functionOutput,data['returns'])
|
||||
domainFunctions = plugins_module.getDomainFunctions()
|
||||
for function in domainFunctions:
|
||||
functionOutput = plugins_module.runPluginFunction(function["plugin"],function["function"],{"domain":domain},account_module.check_account(request.cookies.get("account")))
|
||||
plugins += render.plugin_output(functionOutput,plugins_module.getPluginFunctionReturns(function["plugin"],function["function"]))
|
||||
|
||||
plugins += "</div>"
|
||||
|
||||
|
||||
@ -1053,21 +1062,7 @@ def plugins_index():
|
||||
if not account:
|
||||
return redirect("/logout")
|
||||
|
||||
plugin_links = os.listdir("plugins")
|
||||
plugins = []
|
||||
for plugin in plugin_links:
|
||||
if os.path.isdir("plugins/" + plugin):
|
||||
if os.path.isfile("plugins/" + plugin + "/"+plugin+".json"):
|
||||
with open("plugins/" + plugin + "/"+plugin+".json") as f:
|
||||
data = json.load(f)
|
||||
data['link'] = plugin
|
||||
if 'name' not in data:
|
||||
data['name'] = plugin
|
||||
if 'description' not in data:
|
||||
data['description'] = "No description provided"
|
||||
plugins.append(data)
|
||||
|
||||
plugins = render.plugins(plugins)
|
||||
plugins = render.plugins(plugins_module.listPlugins())
|
||||
|
||||
return render_template("plugins.html", account=account, sync=account_module.getNodeSync(),
|
||||
plugins=plugins)
|
||||
@ -1082,28 +1077,12 @@ def plugin(plugin):
|
||||
if not account:
|
||||
return redirect("/logout")
|
||||
|
||||
if not os.path.isdir("plugins/" + plugin):
|
||||
if not plugins_module.pluginExists(plugin):
|
||||
return redirect("/plugins")
|
||||
|
||||
if not os.path.isfile("plugins/" + plugin + "/"+plugin+".json"):
|
||||
return redirect("/plugins")
|
||||
data = plugins_module.getPluginData(plugin)
|
||||
|
||||
with open("plugins/" + plugin + "/"+plugin+".json") as f:
|
||||
data = json.load(f)
|
||||
data['link'] = plugin
|
||||
if 'name' not in data:
|
||||
data['name'] = plugin
|
||||
if 'description' not in data:
|
||||
data['description'] = "No description provided"
|
||||
|
||||
|
||||
functions = []
|
||||
|
||||
if os.path.isfile("plugins/" + plugin + "/main.py"):
|
||||
# Get plugin/main.py listfunctions()
|
||||
print("Loading plugin: " + plugin)
|
||||
module = importlib.import_module("plugins." + plugin + ".main")
|
||||
functions = module.listFunctions()
|
||||
functions = plugins_module.getPluginFunctions(plugin)
|
||||
functions = render.plugin_functions(functions,plugin)
|
||||
|
||||
error = request.args.get("error")
|
||||
@ -1111,8 +1090,9 @@ def plugin(plugin):
|
||||
error = ""
|
||||
|
||||
return render_template("plugin.html", account=account, sync=account_module.getNodeSync(),
|
||||
name=data['name'],description=data['description'],functions=functions,
|
||||
error=error)
|
||||
name=data['name'],description=data['description'],
|
||||
author=data['author'],version=data['version'],
|
||||
functions=functions,error=error)
|
||||
|
||||
@app.route('/plugin/<plugin>/<function>', methods=["POST"])
|
||||
def plugin_function(plugin,function):
|
||||
@ -1124,26 +1104,14 @@ def plugin_function(plugin,function):
|
||||
if not account:
|
||||
return redirect("/logout")
|
||||
|
||||
if not os.path.isdir("plugins/" + plugin):
|
||||
if not plugins_module.pluginExists(plugin):
|
||||
return redirect("/plugins")
|
||||
|
||||
if not os.path.isfile("plugins/" + plugin + "/"+plugin+".json"):
|
||||
return redirect("/plugins")
|
||||
data = plugins_module.getPluginData(plugin)
|
||||
|
||||
with open("plugins/" + plugin + "/"+plugin+".json") as f:
|
||||
data = json.load(f)
|
||||
data['link'] = plugin
|
||||
if 'name' not in data:
|
||||
data['name'] = plugin
|
||||
if 'description' not in data:
|
||||
data['description'] = "No description provided"
|
||||
|
||||
if os.path.isfile("plugins/" + plugin + "/main.py"):
|
||||
# Get plugin/main.py listfunctions()
|
||||
print("Loading plugin: " + plugin)
|
||||
module = importlib.import_module("plugins." + plugin + ".main")
|
||||
if function in module.listFunctions():
|
||||
inputs = module.listFunctions()[function]["params"]
|
||||
if function in plugins_module.getPluginFunctions(plugin):
|
||||
inputs = plugins_module.getPluginFunctionInputs(plugin,function)
|
||||
request_data = {}
|
||||
for input in inputs:
|
||||
request_data[input] = request.form.get(input)
|
||||
@ -1161,13 +1129,13 @@ def plugin_function(plugin,function):
|
||||
|
||||
|
||||
|
||||
response = module.runFunction(function,request_data,request.cookies.get("account"))
|
||||
response = plugins_module.runPluginFunction(plugin,function,request_data,request.cookies.get("account"))
|
||||
if not response:
|
||||
return redirect("/plugin/" + plugin + "?error=An error occurred")
|
||||
if 'error' in response:
|
||||
return redirect("/plugin/" + plugin + "?error=" + response['error'])
|
||||
|
||||
response = render.plugin_output(response,module.listFunctions()[function]["returns"])
|
||||
response = render.plugin_output(response,plugins_module.getPluginFunctionReturns(plugin,function))
|
||||
|
||||
return render_template("plugin-output.html", account=account, sync=account_module.getNodeSync(),
|
||||
name=data['name'],description=data['description'],output=response)
|
||||
@ -1176,8 +1144,6 @@ def plugin_function(plugin,function):
|
||||
else:
|
||||
return jsonify({"error": "Function not found"})
|
||||
|
||||
return jsonify({"error": "Plugin not found"})
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
101
plugin.py
Normal file
101
plugin.py
Normal file
@ -0,0 +1,101 @@
|
||||
import os
|
||||
import json
|
||||
import importlib
|
||||
|
||||
|
||||
|
||||
def listPlugins():
|
||||
plugins = []
|
||||
for file in os.listdir("plugins"):
|
||||
if file.endswith(".py"):
|
||||
if file != "main.py":
|
||||
plugin = importlib.import_module("plugins."+file[:-3])
|
||||
details = plugin.info
|
||||
details["link"] = file[:-3]
|
||||
plugins.append(details)
|
||||
return plugins
|
||||
|
||||
|
||||
def pluginExists(plugin: str):
|
||||
for file in os.listdir("plugins"):
|
||||
if file == plugin+".py":
|
||||
return True
|
||||
return False
|
||||
|
||||
def getPluginData(plugin: str):
|
||||
plugin = importlib.import_module("plugins."+plugin)
|
||||
return plugin.info
|
||||
|
||||
def getPluginFunctions(plugin: str):
|
||||
plugin = importlib.import_module("plugins."+plugin)
|
||||
return plugin.functions
|
||||
|
||||
def runPluginFunction(plugin: str, function: str, params: dict, authentication: str):
|
||||
plugin_module = importlib.import_module("plugins."+plugin)
|
||||
if function not in plugin_module.functions:
|
||||
return {"error": "Function not found"}
|
||||
|
||||
if not hasattr(plugin_module, function):
|
||||
return {"error": "Function not found"}
|
||||
|
||||
# Get the function object from the plugin module
|
||||
plugin_function = getattr(plugin_module, function)
|
||||
|
||||
# Call the function with provided parameters
|
||||
try:
|
||||
result = plugin_function(params, authentication)
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"Error running plugin: {e}")
|
||||
return {"error": str(e)}
|
||||
# return plugin.runFunction(function, params, authentication)
|
||||
|
||||
def getPluginFunctionInputs(plugin: str, function: str):
|
||||
plugin = importlib.import_module("plugins."+plugin)
|
||||
return plugin.functions[function]["params"]
|
||||
|
||||
def getPluginFunctionReturns(plugin: str, function: str):
|
||||
plugin = importlib.import_module("plugins."+plugin)
|
||||
return plugin.functions[function]["returns"]
|
||||
|
||||
def getDomainFunctions():
|
||||
plugins = listPlugins()
|
||||
domainFunctions = []
|
||||
for plugin in plugins:
|
||||
functions = getPluginFunctions(plugin["link"])
|
||||
for function in functions:
|
||||
if functions[function]["type"] == "domain":
|
||||
domainFunctions.append({
|
||||
"plugin": plugin["link"],
|
||||
"function": function,
|
||||
"description": functions[function]["description"]
|
||||
})
|
||||
return domainFunctions
|
||||
|
||||
def getSearchFunctions():
|
||||
plugins = listPlugins()
|
||||
searchFunctions = []
|
||||
for plugin in plugins:
|
||||
functions = getPluginFunctions(plugin["link"])
|
||||
for function in functions:
|
||||
if functions[function]["type"] == "search":
|
||||
searchFunctions.append({
|
||||
"plugin": plugin["link"],
|
||||
"function": function,
|
||||
"description": functions[function]["description"]
|
||||
})
|
||||
return searchFunctions
|
||||
|
||||
def getDashboardFunctions():
|
||||
plugins = listPlugins()
|
||||
dashboardFunctions = []
|
||||
for plugin in plugins:
|
||||
functions = getPluginFunctions(plugin["link"])
|
||||
for function in functions:
|
||||
if functions[function]["type"] == "dashboard":
|
||||
dashboardFunctions.append({
|
||||
"plugin": plugin["link"],
|
||||
"function": function,
|
||||
"description": functions[function]["description"]
|
||||
})
|
||||
return dashboardFunctions
|
19
plugins.md
19
plugins.md
@ -1,5 +1,24 @@
|
||||
# Plugins
|
||||
|
||||
## Types
|
||||
### Default
|
||||
Type: `default`
|
||||
This is the default type and is used when no type is specified.
|
||||
This type is displayed in the plugin page only.
|
||||
This is the onlu type of plugin that takes user input
|
||||
|
||||
### Manage & Search
|
||||
For manage page use type: `domain`
|
||||
For search page use type: `search`
|
||||
|
||||
This type is used for domain plugins. It shows in the manage domain page or the search page.
|
||||
It gets the `domain` paramater as the only input (in addition to authentication)
|
||||
|
||||
### Dashboard
|
||||
This type is used for dashboard plugins.
|
||||
It shows in the dashboard page. It doesn't get any inputs other than the authentication
|
||||
|
||||
|
||||
## Inputs
|
||||
|
||||
### Plain Text
|
||||
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"name":"Domains Plugin",
|
||||
"description":"This is plugin for the domain page"
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import json
|
||||
import account
|
||||
|
||||
|
||||
functions = {
|
||||
"bids": {
|
||||
"name": "Bid info",
|
||||
"type": "domain",
|
||||
"description": "Check when the domain was last updated",
|
||||
"params": {
|
||||
"domain": {
|
||||
"name":"domain to check",
|
||||
"type":"domain"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"highest":
|
||||
{
|
||||
"name": "Highest bid",
|
||||
"type": "text"
|
||||
},
|
||||
"paid":
|
||||
{
|
||||
"name": "Amount paid in auction",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def listFunctions():
|
||||
return functions
|
||||
|
||||
def runFunction(function, params, authentication):
|
||||
if function == "bids":
|
||||
return bids(params['domain'], authentication)
|
||||
else:
|
||||
return "Function not found"
|
||||
|
||||
|
||||
def bids(domain, authentication):
|
||||
wallet = authentication.split(":")[0]
|
||||
data = account.getDomain(domain)
|
||||
value = str(account.convertHNS(data['info']['value'])) + " HNS"
|
||||
highest = str(account.convertHNS(data['info']['highest'])) + " HNS"
|
||||
|
||||
|
||||
return {"highest": highest,"paid":value}
|
@ -1,27 +1,22 @@
|
||||
import json
|
||||
import account
|
||||
import requests
|
||||
|
||||
|
||||
# Plugin Data
|
||||
info = {
|
||||
"name": "Example Plugin",
|
||||
"description": "This is a plugin to be used as an example",
|
||||
"version": "1.0",
|
||||
"author": "Nathan.Woodburn/"
|
||||
}
|
||||
|
||||
|
||||
# Functions
|
||||
functions = {
|
||||
"check": {
|
||||
"name": "Domain Check",
|
||||
"description": "Check if domains in file are owned by the wallet",
|
||||
"params": {
|
||||
"domains": {
|
||||
"name":"List of domains to check",
|
||||
"type":"longText"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"domains":
|
||||
{
|
||||
"name": "List of owned domains",
|
||||
"type": "list"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search":{
|
||||
"name": "Search Owned",
|
||||
"type": "default",
|
||||
"description": "Search for owned domains containing a string",
|
||||
"params": {
|
||||
"search": {
|
||||
@ -39,6 +34,7 @@ functions = {
|
||||
},
|
||||
"transfer":{
|
||||
"name": "Bulk Transfer Domains",
|
||||
"type": "default",
|
||||
"description": "Transfer domains to another wallet",
|
||||
"params": {
|
||||
"address": {
|
||||
@ -63,6 +59,7 @@ functions = {
|
||||
},
|
||||
"dns":{
|
||||
"name": "Set DNS for Domains",
|
||||
"type": "default",
|
||||
"description": "Set DNS for domains",
|
||||
"params": {
|
||||
"domains": {
|
||||
@ -84,26 +81,50 @@ functions = {
|
||||
"type": "dns"
|
||||
}
|
||||
}
|
||||
},
|
||||
"niami": {
|
||||
"name": "Niami info",
|
||||
"type": "domain",
|
||||
"description": "Check the domains niami rating",
|
||||
"params": {},
|
||||
"returns": {
|
||||
"rating":
|
||||
{
|
||||
"name": "Niami Rating",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"niamiSearch": {
|
||||
"name": "Niami info",
|
||||
"type": "search",
|
||||
"description": "Check the domains niami rating",
|
||||
"params": {},
|
||||
"returns": {
|
||||
"rating":
|
||||
{
|
||||
"name": "Niami Rating",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"connections":{
|
||||
"name": "HSD Connections",
|
||||
"type": "dashboard",
|
||||
"description": "Show the number of connections the HSD node is connected to",
|
||||
"params": {},
|
||||
"returns": {
|
||||
"connections":
|
||||
{
|
||||
"name": "HSD Connections",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def listFunctions():
|
||||
return functions
|
||||
|
||||
def runFunction(function, params, authentication):
|
||||
if function == "check":
|
||||
return check(params['domains'], authentication)
|
||||
elif function == "search":
|
||||
return search(params['search'], authentication)
|
||||
elif function == "transfer":
|
||||
return transfer(params['address'], params['domains'], authentication)
|
||||
elif function == "dns":
|
||||
return dns(params['domains'],params['dns'],authentication)
|
||||
else:
|
||||
return "Function not found"
|
||||
|
||||
|
||||
def check(domains, authentication):
|
||||
def check(params, authentication):
|
||||
domains = params["domains"]
|
||||
domains = domains.splitlines()
|
||||
|
||||
wallet = authentication.split(":")[0]
|
||||
@ -116,7 +137,8 @@ def check(domains, authentication):
|
||||
|
||||
return {"domains": domains}
|
||||
|
||||
def search(search, authentication):
|
||||
def search(params, authentication):
|
||||
search = params["search"]
|
||||
wallet = authentication.split(":")[0]
|
||||
owned = account.getDomains(wallet)
|
||||
# Only keep owned domains ["name"]
|
||||
@ -127,8 +149,27 @@ def search(search, authentication):
|
||||
return {"domains": domains}
|
||||
|
||||
|
||||
def transfer(address, domains, authentication):
|
||||
def transfer(params, authentication):
|
||||
address = params["address"]
|
||||
return {"hash":"f921ffe1bb01884bf515a8079073ee9381cb93a56b486694eda2cce0719f27c0","address":address}
|
||||
|
||||
def dns(domains,dns,authentication):
|
||||
def dns(params,authentication):
|
||||
dns = params["dns"]
|
||||
return {"hash":"f921ffe1bb01884bf515a8079073ee9381cb93a56b486694eda2cce0719f27c0","dns":dns}
|
||||
|
||||
def niami(params, authentication):
|
||||
domain = params["domain"]
|
||||
print(domain)
|
||||
response = requests.get(f"https://api.handshake.niami.io/domain/{domain}")
|
||||
print(response.text)
|
||||
data = response.json()["data"]
|
||||
rating = str(data["rating"]["score"]) + " (" + data["rating"]["rarity"] + ")"
|
||||
return {"rating":rating}
|
||||
|
||||
def niamiSearch(params, authentication):
|
||||
return niami(params, authentication)
|
||||
|
||||
|
||||
def connections(params,authentication):
|
||||
outbound = account.hsd.getInfo()['pool']['outbound']
|
||||
return {"connections": outbound}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"name":"Example Plugin",
|
||||
"description":"This is an example plugin"
|
||||
}
|
19
render.py
19
render.py
@ -204,7 +204,7 @@ def plugin_functions(functions, pluginName):
|
||||
|
||||
returns = returns.removesuffix(', ')
|
||||
|
||||
functionType = "manual"
|
||||
functionType = "default"
|
||||
if "type" in functions[function]:
|
||||
functionType = functions[function]["type"]
|
||||
|
||||
@ -215,7 +215,7 @@ def plugin_functions(functions, pluginName):
|
||||
html += f'<h6 class="text-muted card-subtitle mb-2">{description}</h6>'
|
||||
html += f'<h6 class="text-muted card-subtitle mb-2">Function type: {functionType.capitalize()}</h6>'
|
||||
|
||||
if functionType != "manual":
|
||||
if functionType != "default":
|
||||
html += f'<p class="card-text">Returns: {returns}</p>'
|
||||
html += f'</div>'
|
||||
html += f'</div>'
|
||||
@ -287,7 +287,16 @@ def plugin_output(outputs, returns):
|
||||
|
||||
html += f'</div>'
|
||||
html += f'</div>'
|
||||
|
||||
|
||||
|
||||
return html
|
||||
|
||||
def plugin_output_dash(outputs, returns):
|
||||
|
||||
html = ''
|
||||
|
||||
for returnOutput in returns:
|
||||
html += render_template('components/dashboard-plugin.html', name=returns[returnOutput]["name"], output=outputs[returnOutput])
|
||||
|
||||
|
||||
html += f'</div>'
|
||||
html += f'</div>'
|
||||
return html
|
8
templates/components/dashboard-plugin.html
Normal file
8
templates/components/dashboard-plugin.html
Normal file
@ -0,0 +1,8 @@
|
||||
<div class="col-md-6 col-xl-3 mb-4">
|
||||
<div class="card shadow border-start-warning py-2">
|
||||
<div class="card-body">
|
||||
<div class="text-uppercase fw-bold text-xs mb-1"><span style="color: var(--bs-dark);">{{name}}</span></div>
|
||||
<div class="text-dark fw-bold h5 mb-0"><span>{{output}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -126,7 +126,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>{{plugins|safe}}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
|
@ -63,7 +63,8 @@
|
||||
<h1 class="text-center" style="color: rgb(255,0,0);">{{error}}</h1>
|
||||
<div class="container-fluid" style="margin-bottom: 20px;">
|
||||
<h3 class="text-dark mb-1">{{name}}</h3>
|
||||
<h4 class="text-dark mb-1">{{description}}</h4>{{functions|safe}}
|
||||
<h4 class="text-dark mb-1">{{description}}</h4>
|
||||
<h6 class="text-dark mb-1">Author: {{author}}<br>Version: {{version}}</h6>{{functions|safe}}
|
||||
</div>
|
||||
</div>
|
||||
<footer class="sticky-footer" style="background: var(--bs-primary-text-emphasis);">
|
||||
|
@ -68,7 +68,7 @@
|
||||
<h6 class="text-muted card-subtitle mb-2">Owner: {{owner}}</h6><a class="btn btn-primary" role="button" style="margin-right: 25px;" href="/manage/{{domain}}">Manage</a><a class="btn btn-primary" role="button" href="/auction/{{domain}}">Auction</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>{{plugins|safe}}
|
||||
<div class="container-fluid" style="margin-top: 50px;">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
|
Loading…
Reference in New Issue
Block a user