Add files via upload
We recovered Zeronet Enhanced/ZNE from here web.archive.org/https://github.com/zeronet-enhanced/ZeroNet/
This commit is contained in:
parent
9aa53f848f
commit
46583c4248
|
@ -0,0 +1,399 @@
|
|||
import re
|
||||
import time
|
||||
import copy
|
||||
import os
|
||||
|
||||
from Plugin import PluginManager
|
||||
from Translate import Translate
|
||||
from util import RateLimit
|
||||
from util import helper
|
||||
from util.Flag import flag
|
||||
from Debug import Debug
|
||||
try:
|
||||
import OptionalManager.UiWebsocketPlugin # To make optioanlFileInfo merger sites compatible
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if "merger_db" not in locals().keys(): # To keep merger_sites between module reloads
|
||||
merger_db = {} # Sites that allowed to list other sites {address: [type1, type2...]}
|
||||
merged_db = {} # Sites that allowed to be merged to other sites {address: type, ...}
|
||||
merged_to_merger = {} # {address: [site1, site2, ...]} cache
|
||||
site_manager = None # Site manager for merger sites
|
||||
|
||||
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
|
||||
if "_" not in locals():
|
||||
_ = Translate(plugin_dir + "/languages/")
|
||||
|
||||
|
||||
# Check if the site has permission to this merger site
|
||||
def checkMergerPath(address, inner_path):
|
||||
merged_match = re.match("^merged-(.*?)/([A-Za-z0-9]{26,35})/", inner_path)
|
||||
if merged_match:
|
||||
merger_type = merged_match.group(1)
|
||||
# Check if merged site is allowed to include other sites
|
||||
if merger_type in merger_db.get(address, []):
|
||||
# Check if included site allows to include
|
||||
merged_address = merged_match.group(2)
|
||||
if merged_db.get(merged_address) == merger_type:
|
||||
inner_path = re.sub("^merged-(.*?)/([A-Za-z0-9]{26,35})/", "", inner_path)
|
||||
return merged_address, inner_path
|
||||
else:
|
||||
raise Exception(
|
||||
"Merger site (%s) does not have permission for merged site: %s (%s)" %
|
||||
(merger_type, merged_address, merged_db.get(merged_address))
|
||||
)
|
||||
else:
|
||||
raise Exception("No merger (%s) permission to load: <br>%s (%s not in %s)" % (
|
||||
address, inner_path, merger_type, merger_db.get(address, []))
|
||||
)
|
||||
else:
|
||||
raise Exception("Invalid merger path: %s" % inner_path)
|
||||
|
||||
|
||||
@PluginManager.registerTo("UiWebsocket")
|
||||
class UiWebsocketPlugin(object):
|
||||
# Download new site
|
||||
def actionMergerSiteAdd(self, to, addresses):
|
||||
if type(addresses) != list:
|
||||
# Single site add
|
||||
addresses = [addresses]
|
||||
# Check if the site has merger permission
|
||||
merger_types = merger_db.get(self.site.address)
|
||||
if not merger_types:
|
||||
return self.response(to, {"error": "Not a merger site"})
|
||||
|
||||
if RateLimit.isAllowed(self.site.address + "-MergerSiteAdd", 10) and len(addresses) == 1:
|
||||
# Without confirmation if only one site address and not called in last 10 sec
|
||||
self.cbMergerSiteAdd(to, addresses)
|
||||
else:
|
||||
self.cmd(
|
||||
"confirm",
|
||||
[_["Add <b>%s</b> new site?"] % len(addresses), "Add"],
|
||||
lambda res: self.cbMergerSiteAdd(to, addresses)
|
||||
)
|
||||
self.response(to, "ok")
|
||||
|
||||
# Callback of adding new site confirmation
|
||||
def cbMergerSiteAdd(self, to, addresses):
|
||||
added = 0
|
||||
for address in addresses:
|
||||
try:
|
||||
site_manager.need(address)
|
||||
added += 1
|
||||
except Exception as err:
|
||||
self.cmd("notification", ["error", _["Adding <b>%s</b> failed: %s"] % (address, err)])
|
||||
if added:
|
||||
self.cmd("notification", ["done", _["Added <b>%s</b> new site"] % added, 5000])
|
||||
RateLimit.called(self.site.address + "-MergerSiteAdd")
|
||||
site_manager.updateMergerSites()
|
||||
|
||||
# Delete a merged site
|
||||
@flag.no_multiuser
|
||||
def actionMergerSiteDelete(self, to, address):
|
||||
site = self.server.sites.get(address)
|
||||
if not site:
|
||||
return self.response(to, {"error": "No site found: %s" % address})
|
||||
|
||||
merger_types = merger_db.get(self.site.address)
|
||||
if not merger_types:
|
||||
return self.response(to, {"error": "Not a merger site"})
|
||||
if merged_db.get(address) not in merger_types:
|
||||
return self.response(to, {"error": "Merged type (%s) not in %s" % (merged_db.get(address), merger_types)})
|
||||
|
||||
self.cmd("notification", ["done", _["Site deleted: <b>%s</b>"] % address, 5000])
|
||||
self.response(to, "ok")
|
||||
|
||||
# Lists merged sites
|
||||
def actionMergerSiteList(self, to, query_site_info=False):
|
||||
merger_types = merger_db.get(self.site.address)
|
||||
ret = {}
|
||||
if not merger_types:
|
||||
return self.response(to, {"error": "Not a merger site"})
|
||||
for address, merged_type in merged_db.items():
|
||||
if merged_type not in merger_types:
|
||||
continue # Site not for us
|
||||
if query_site_info:
|
||||
site = self.server.sites.get(address)
|
||||
ret[address] = self.formatSiteInfo(site, create_user=False)
|
||||
else:
|
||||
ret[address] = merged_type
|
||||
self.response(to, ret)
|
||||
|
||||
def hasSitePermission(self, address, *args, **kwargs):
|
||||
if super(UiWebsocketPlugin, self).hasSitePermission(address, *args, **kwargs):
|
||||
return True
|
||||
else:
|
||||
if self.site.address in [merger_site.address for merger_site in merged_to_merger.get(address, [])]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
# Add support merger sites for file commands
|
||||
def mergerFuncWrapper(self, func_name, to, inner_path, *args, **kwargs):
|
||||
if inner_path.startswith("merged-"):
|
||||
merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path)
|
||||
|
||||
# Set the same cert for merged site
|
||||
merger_cert = self.user.getSiteData(self.site.address).get("cert")
|
||||
if merger_cert and self.user.getSiteData(merged_address).get("cert") != merger_cert:
|
||||
self.user.setCert(merged_address, merger_cert)
|
||||
|
||||
req_self = copy.copy(self)
|
||||
req_self.site = self.server.sites.get(merged_address) # Change the site to the merged one
|
||||
|
||||
func = getattr(super(UiWebsocketPlugin, req_self), func_name)
|
||||
return func(to, merged_inner_path, *args, **kwargs)
|
||||
else:
|
||||
func = getattr(super(UiWebsocketPlugin, self), func_name)
|
||||
return func(to, inner_path, *args, **kwargs)
|
||||
|
||||
def actionFileList(self, to, inner_path, *args, **kwargs):
|
||||
return self.mergerFuncWrapper("actionFileList", to, inner_path, *args, **kwargs)
|
||||
|
||||
def actionDirList(self, to, inner_path, *args, **kwargs):
|
||||
return self.mergerFuncWrapper("actionDirList", to, inner_path, *args, **kwargs)
|
||||
|
||||
def actionFileGet(self, to, inner_path, *args, **kwargs):
|
||||
return self.mergerFuncWrapper("actionFileGet", to, inner_path, *args, **kwargs)
|
||||
|
||||
def actionFileWrite(self, to, inner_path, *args, **kwargs):
|
||||
return self.mergerFuncWrapper("actionFileWrite", to, inner_path, *args, **kwargs)
|
||||
|
||||
def actionFileDelete(self, to, inner_path, *args, **kwargs):
|
||||
return self.mergerFuncWrapper("actionFileDelete", to, inner_path, *args, **kwargs)
|
||||
|
||||
def actionFileRules(self, to, inner_path, *args, **kwargs):
|
||||
return self.mergerFuncWrapper("actionFileRules", to, inner_path, *args, **kwargs)
|
||||
|
||||
def actionFileNeed(self, to, inner_path, *args, **kwargs):
|
||||
return self.mergerFuncWrapper("actionFileNeed", to, inner_path, *args, **kwargs)
|
||||
|
||||
def actionOptionalFileInfo(self, to, inner_path, *args, **kwargs):
|
||||
return self.mergerFuncWrapper("actionOptionalFileInfo", to, inner_path, *args, **kwargs)
|
||||
|
||||
def actionOptionalFileDelete(self, to, inner_path, *args, **kwargs):
|
||||
return self.mergerFuncWrapper("actionOptionalFileDelete", to, inner_path, *args, **kwargs)
|
||||
|
||||
def actionBigfileUploadInit(self, to, inner_path, *args, **kwargs):
|
||||
back = self.mergerFuncWrapper("actionBigfileUploadInit", to, inner_path, *args, **kwargs)
|
||||
if inner_path.startswith("merged-"):
|
||||
merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path)
|
||||
back["inner_path"] = "merged-%s/%s/%s" % (merged_db[merged_address], merged_address, back["inner_path"])
|
||||
return back
|
||||
|
||||
# Add support merger sites for file commands with privatekey parameter
|
||||
def mergerFuncWrapperWithPrivatekey(self, func_name, to, privatekey, inner_path, *args, **kwargs):
|
||||
func = getattr(super(UiWebsocketPlugin, self), func_name)
|
||||
if inner_path.startswith("merged-"):
|
||||
merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path)
|
||||
merged_site = self.server.sites.get(merged_address)
|
||||
|
||||
# Set the same cert for merged site
|
||||
merger_cert = self.user.getSiteData(self.site.address).get("cert")
|
||||
if merger_cert:
|
||||
self.user.setCert(merged_address, merger_cert)
|
||||
|
||||
site_before = self.site # Save to be able to change it back after we ran the command
|
||||
self.site = merged_site # Change the site to the merged one
|
||||
try:
|
||||
back = func(to, privatekey, merged_inner_path, *args, **kwargs)
|
||||
finally:
|
||||
self.site = site_before # Change back to original site
|
||||
return back
|
||||
else:
|
||||
return func(to, privatekey, inner_path, *args, **kwargs)
|
||||
|
||||
def actionSiteSign(self, to, privatekey=None, inner_path="content.json", *args, **kwargs):
|
||||
return self.mergerFuncWrapperWithPrivatekey("actionSiteSign", to, privatekey, inner_path, *args, **kwargs)
|
||||
|
||||
def actionSitePublish(self, to, privatekey=None, inner_path="content.json", *args, **kwargs):
|
||||
return self.mergerFuncWrapperWithPrivatekey("actionSitePublish", to, privatekey, inner_path, *args, **kwargs)
|
||||
|
||||
def actionPermissionAdd(self, to, permission):
|
||||
super(UiWebsocketPlugin, self).actionPermissionAdd(to, permission)
|
||||
if permission.startswith("Merger"):
|
||||
self.site.storage.rebuildDb()
|
||||
|
||||
def actionPermissionDetails(self, to, permission):
|
||||
if not permission.startswith("Merger"):
|
||||
return super(UiWebsocketPlugin, self).actionPermissionDetails(to, permission)
|
||||
|
||||
merger_type = permission.replace("Merger:", "")
|
||||
if not re.match("^[A-Za-z0-9-]+$", merger_type):
|
||||
raise Exception("Invalid merger_type: %s" % merger_type)
|
||||
merged_sites = []
|
||||
for address, merged_type in merged_db.items():
|
||||
if merged_type != merger_type:
|
||||
continue
|
||||
site = self.server.sites.get(address)
|
||||
try:
|
||||
merged_sites.append(site.content_manager.contents.get("content.json").get("title", address))
|
||||
except Exception:
|
||||
merged_sites.append(address)
|
||||
|
||||
details = _["Read and write permissions to sites with merged type of <b>%s</b> "] % merger_type
|
||||
details += _["(%s sites)"] % len(merged_sites)
|
||||
details += "<div style='white-space: normal; max-width: 400px'>%s</div>" % ", ".join(merged_sites)
|
||||
self.response(to, details)
|
||||
|
||||
|
||||
@PluginManager.registerTo("UiRequest")
|
||||
class UiRequestPlugin(object):
|
||||
# Allow to load merged site files using /merged-ZeroMe/address/file.jpg
|
||||
def parsePath(self, path):
|
||||
path_parts = super(UiRequestPlugin, self).parsePath(path)
|
||||
if "merged-" not in path: # Optimization
|
||||
return path_parts
|
||||
path_parts["address"], path_parts["inner_path"] = checkMergerPath(path_parts["address"], path_parts["inner_path"])
|
||||
return path_parts
|
||||
|
||||
|
||||
@PluginManager.registerTo("SiteStorage")
|
||||
class SiteStoragePlugin(object):
|
||||
# Also rebuild from merged sites
|
||||
def getDbFiles(self):
|
||||
merger_types = merger_db.get(self.site.address)
|
||||
|
||||
# First return the site's own db files
|
||||
for item in super(SiteStoragePlugin, self).getDbFiles():
|
||||
yield item
|
||||
|
||||
# Not a merger site, that's all
|
||||
if not merger_types:
|
||||
return
|
||||
|
||||
merged_sites = [
|
||||
site_manager.sites[address]
|
||||
for address, merged_type in merged_db.items()
|
||||
if merged_type in merger_types
|
||||
]
|
||||
found = 0
|
||||
for merged_site in merged_sites:
|
||||
self.log.debug("Loading merged site: %s" % merged_site)
|
||||
merged_type = merged_db[merged_site.address]
|
||||
for content_inner_path, content in merged_site.content_manager.contents.items():
|
||||
# content.json file itself
|
||||
if merged_site.storage.isFile(content_inner_path): # Missing content.json file
|
||||
merged_inner_path = "merged-%s/%s/%s" % (merged_type, merged_site.address, content_inner_path)
|
||||
yield merged_inner_path, merged_site.storage.getPath(content_inner_path)
|
||||
else:
|
||||
merged_site.log.error("[MISSING] %s" % content_inner_path)
|
||||
# Data files in content.json
|
||||
content_inner_path_dir = helper.getDirname(content_inner_path) # Content.json dir relative to site
|
||||
for file_relative_path in list(content.get("files", {}).keys()) + list(content.get("files_optional", {}).keys()):
|
||||
if not file_relative_path.endswith(".json"):
|
||||
continue # We only interesed in json files
|
||||
file_inner_path = content_inner_path_dir + file_relative_path # File Relative to site dir
|
||||
file_inner_path = file_inner_path.strip("/") # Strip leading /
|
||||
if merged_site.storage.isFile(file_inner_path):
|
||||
merged_inner_path = "merged-%s/%s/%s" % (merged_type, merged_site.address, file_inner_path)
|
||||
yield merged_inner_path, merged_site.storage.getPath(file_inner_path)
|
||||
else:
|
||||
merged_site.log.error("[MISSING] %s" % file_inner_path)
|
||||
found += 1
|
||||
if found % 100 == 0:
|
||||
time.sleep(0.001) # Context switch to avoid UI block
|
||||
|
||||
# Also notice merger sites on a merged site file change
|
||||
def onUpdated(self, inner_path, file=None):
|
||||
super(SiteStoragePlugin, self).onUpdated(inner_path, file)
|
||||
|
||||
merged_type = merged_db.get(self.site.address)
|
||||
|
||||
for merger_site in merged_to_merger.get(self.site.address, []):
|
||||
if merger_site.address == self.site.address: # Avoid infinite loop
|
||||
continue
|
||||
virtual_path = "merged-%s/%s/%s" % (merged_type, self.site.address, inner_path)
|
||||
if inner_path.endswith(".json"):
|
||||
if file is not None:
|
||||
merger_site.storage.onUpdated(virtual_path, file=file)
|
||||
else:
|
||||
merger_site.storage.onUpdated(virtual_path, file=self.open(inner_path))
|
||||
else:
|
||||
merger_site.storage.onUpdated(virtual_path)
|
||||
|
||||
|
||||
@PluginManager.registerTo("Site")
|
||||
class SitePlugin(object):
|
||||
def fileDone(self, inner_path):
|
||||
super(SitePlugin, self).fileDone(inner_path)
|
||||
|
||||
for merger_site in merged_to_merger.get(self.address, []):
|
||||
if merger_site.address == self.address:
|
||||
continue
|
||||
for ws in merger_site.websockets:
|
||||
ws.event("siteChanged", self, {"event": ["file_done", inner_path]})
|
||||
|
||||
def fileFailed(self, inner_path):
|
||||
super(SitePlugin, self).fileFailed(inner_path)
|
||||
|
||||
for merger_site in merged_to_merger.get(self.address, []):
|
||||
if merger_site.address == self.address:
|
||||
continue
|
||||
for ws in merger_site.websockets:
|
||||
ws.event("siteChanged", self, {"event": ["file_failed", inner_path]})
|
||||
|
||||
|
||||
@PluginManager.registerTo("SiteManager")
|
||||
class SiteManagerPlugin(object):
|
||||
# Update merger site for site types
|
||||
def updateMergerSites(self):
|
||||
global merger_db, merged_db, merged_to_merger, site_manager
|
||||
s = time.time()
|
||||
merger_db_new = {}
|
||||
merged_db_new = {}
|
||||
merged_to_merger_new = {}
|
||||
site_manager = self
|
||||
if not self.sites:
|
||||
return
|
||||
for site in self.sites.values():
|
||||
# Update merged sites
|
||||
try:
|
||||
merged_type = site.content_manager.contents.get("content.json", {}).get("merged_type")
|
||||
except Exception as err:
|
||||
self.log.error("Error loading site %s: %s" % (site.address, Debug.formatException(err)))
|
||||
continue
|
||||
if merged_type:
|
||||
merged_db_new[site.address] = merged_type
|
||||
|
||||
# Update merger sites
|
||||
for permission in site.settings["permissions"]:
|
||||
if not permission.startswith("Merger:"):
|
||||
continue
|
||||
if merged_type:
|
||||
self.log.error(
|
||||
"Removing permission %s from %s: Merger and merged at the same time." %
|
||||
(permission, site.address)
|
||||
)
|
||||
site.settings["permissions"].remove(permission)
|
||||
continue
|
||||
merger_type = permission.replace("Merger:", "")
|
||||
if site.address not in merger_db_new:
|
||||
merger_db_new[site.address] = []
|
||||
merger_db_new[site.address].append(merger_type)
|
||||
site_manager.sites[site.address] = site
|
||||
|
||||
# Update merged to merger
|
||||
if merged_type:
|
||||
for merger_site in self.sites.values():
|
||||
if "Merger:" + merged_type in merger_site.settings["permissions"]:
|
||||
if site.address not in merged_to_merger_new:
|
||||
merged_to_merger_new[site.address] = []
|
||||
merged_to_merger_new[site.address].append(merger_site)
|
||||
|
||||
# Update globals
|
||||
merger_db = merger_db_new
|
||||
merged_db = merged_db_new
|
||||
merged_to_merger = merged_to_merger_new
|
||||
|
||||
self.log.debug("Updated merger sites in %.3fs" % (time.time() - s))
|
||||
|
||||
def load(self, *args, **kwags):
|
||||
super(SiteManagerPlugin, self).load(*args, **kwags)
|
||||
self.updateMergerSites()
|
||||
|
||||
def saveDelayed(self, *args, **kwags):
|
||||
super(SiteManagerPlugin, self).saveDelayed(*args, **kwags)
|
||||
self.updateMergerSites()
|
|
@ -0,0 +1 @@
|
|||
from . import MergerSitePlugin
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"Add <b>%s</b> new site?": "¿Agregar <b>%s</b> nuevo sitio?",
|
||||
"Added <b>%s</b> new site": "Sitio <b>%s</b> agregado",
|
||||
"Site deleted: <b>%s</b>": "Sitio removido: <b>%s</b>"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"Add <b>%s</b> new site?": "Ajouter le site <b>%s</b> ?",
|
||||
"Added <b>%s</b> new site": "Site <b>%s</b> ajouté",
|
||||
"Site deleted: <b>%s</b>": "Site <b>%s</b> supprimé"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"Add <b>%s</b> new site?": "Új oldal hozzáadása: <b>%s</b>?",
|
||||
"Added <b>%s</b> new site": "Új oldal hozzáadva: <b>%s</b>",
|
||||
"Site deleted: <b>%s</b>": "Oldal törölve: <b>%s</b>"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"Add <b>%s</b> new site?": "Aggiungere <b>%s</b> nuovo sito ?",
|
||||
"Added <b>%s</b> new site": "Sito <b>%s</b> aggiunto",
|
||||
"Site deleted: <b>%s</b>": "Sito <b>%s</b> eliminato"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"Add <b>%s</b> new site?": "サイト: <b>%s</b> を追加しますか?",
|
||||
"Added <b>%s</b> new site": "サイト: <b>%s</b> を追加しました",
|
||||
"Site deleted: <b>%s</b>": "サイト: <b>%s</b> を削除しました"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"Add <b>%s</b> new site?": "Adicionar <b>%s</b> novo site?",
|
||||
"Added <b>%s</b> new site": "Site <b>%s</b> adicionado",
|
||||
"Site deleted: <b>%s</b>": "Site removido: <b>%s</b>"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"Add <b>%s</b> new site?": "<b>%s</b> sitesi eklensin mi?",
|
||||
"Added <b>%s</b> new site": "<b>%s</b> sitesi eklendi",
|
||||
"Site deleted: <b>%s</b>": "<b>%s</b> sitesi silindi"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"Add <b>%s</b> new site?": "添加新網站: <b>%s</b>?",
|
||||
"Added <b>%s</b> new site": "已添加到新網站:<b>%s</b>",
|
||||
"Site deleted: <b>%s</b>": "網站已刪除:<b>%s</b>"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"Add <b>%s</b> new site?": "添加新站点: <b>%s</b>?",
|
||||
"Added <b>%s</b> new site": "已添加到新站点:<b>%s</b>",
|
||||
"Site deleted: <b>%s</b>": "站点已删除:<b>%s</b>"
|
||||
}
|
|
@ -0,0 +1,627 @@
|
|||
import time
|
||||
import html
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import itertools
|
||||
|
||||
from Plugin import PluginManager
|
||||
from Config import config
|
||||
from util import helper
|
||||
from Debug import Debug
|
||||
from Db import Db
|
||||
|
||||
|
||||
@PluginManager.registerTo("UiRequest")
|
||||
class UiRequestPlugin(object):
|
||||
|
||||
def formatTableRow(self, row, class_name=""):
|
||||
back = []
|
||||
for format, val in row:
|
||||
if val is None:
|
||||
formatted = "n/a"
|
||||
elif format == "since":
|
||||
if val:
|
||||
formatted = "%.0f" % (time.time() - val)
|
||||
else:
|
||||
formatted = "n/a"
|
||||
else:
|
||||
formatted = format % val
|
||||
back.append("<td>%s</td>" % formatted)
|
||||
return "<tr class='%s'>%s</tr>" % (class_name, "".join(back))
|
||||
|
||||
def getObjSize(self, obj, hpy=None):
|
||||
if hpy:
|
||||
return float(hpy.iso(obj).domisize) / 1024
|
||||
else:
|
||||
return 0
|
||||
|
||||
def renderHead(self):
|
||||
import main
|
||||
from Crypt import CryptConnection
|
||||
|
||||
# Memory
|
||||
yield "rev%s | " % config.rev
|
||||
yield "%s | " % main.file_server.ip_external_list
|
||||
yield "Port: %s | " % main.file_server.port
|
||||
yield "Network: %s | " % main.file_server.supported_ip_types
|
||||
yield "Opened: %s | " % main.file_server.port_opened
|
||||
yield "Crypt: %s, TLSv1.3: %s | " % (CryptConnection.manager.crypt_supported, CryptConnection.ssl.HAS_TLSv1_3)
|
||||
yield "In: %.2fMB, Out: %.2fMB | " % (
|
||||
float(main.file_server.bytes_recv) / 1024 / 1024,
|
||||
float(main.file_server.bytes_sent) / 1024 / 1024
|
||||
)
|
||||
yield "Peerid: %s | " % main.file_server.peer_id
|
||||
yield "Time: %.2fs | " % main.file_server.getTimecorrection()
|
||||
yield "Blocks: %s" % Debug.num_block
|
||||
|
||||
try:
|
||||
import psutil
|
||||
process = psutil.Process(os.getpid())
|
||||
mem = process.get_memory_info()[0] / float(2 ** 20)
|
||||
yield "Mem: %.2fMB | " % mem
|
||||
yield "Threads: %s | " % len(process.threads())
|
||||
yield "CPU: usr %.2fs sys %.2fs | " % process.cpu_times()
|
||||
yield "Files: %s | " % len(process.open_files())
|
||||
yield "Sockets: %s | " % len(process.connections())
|
||||
yield "Calc size <a href='?size=1'>on</a> <a href='?size=0'>off</a>"
|
||||
except Exception:
|
||||
pass
|
||||
yield "<br>"
|
||||
|
||||
def renderConnectionsTable(self):
|
||||
import main
|
||||
|
||||
# Connections
|
||||
yield "<b>Connections</b> (%s, total made: %s, in: %s, out: %s):<br>" % (
|
||||
len(main.file_server.connections), main.file_server.last_connection_id,
|
||||
main.file_server.num_incoming, main.file_server.num_outgoing
|
||||
)
|
||||
yield "<table class='connections'><tr> <th>id</th> <th>type</th> <th>ip</th> <th>open</th> <th>crypt</th> <th>ping</th>"
|
||||
yield "<th>buff</th> <th>bad</th> <th>idle</th> <th>open</th> <th>delay</th> <th>cpu</th> <th>out</th> <th>in</th> <th>last sent</th>"
|
||||
yield "<th>wait</th> <th>version</th> <th>time</th> <th>sites</th> </tr>"
|
||||
for connection in main.file_server.connections:
|
||||
if "cipher" in dir(connection.sock):
|
||||
cipher = connection.sock.cipher()[0]
|
||||
tls_version = connection.sock.version()
|
||||
else:
|
||||
cipher = connection.crypt
|
||||
tls_version = ""
|
||||
if "time" in connection.handshake and connection.last_ping_delay:
|
||||
time_correction = connection.handshake["time"] - connection.handshake_time - connection.last_ping_delay
|
||||
else:
|
||||
time_correction = 0.0
|
||||
yield self.formatTableRow([
|
||||
("%3d", connection.id),
|
||||
("%s", connection.type),
|
||||
("%s:%s", (connection.ip, connection.port)),
|
||||
("%s", connection.handshake.get("port_opened")),
|
||||
("<span title='%s %s'>%s</span>", (cipher, tls_version, connection.crypt)),
|
||||
("%6.3f", connection.last_ping_delay),
|
||||
("%s", connection.incomplete_buff_recv),
|
||||
("%s", connection.bad_actions),
|
||||
("since", max(connection.last_send_time, connection.last_recv_time)),
|
||||
("since", connection.start_time),
|
||||
("%.3f", max(-1, connection.last_sent_time - connection.last_send_time)),
|
||||
("%.3f", connection.cpu_time),
|
||||
("%.0fk", connection.bytes_sent / 1024),
|
||||
("%.0fk", connection.bytes_recv / 1024),
|
||||
("<span title='Recv: %s'>%s</span>", (connection.last_cmd_recv, connection.last_cmd_sent)),
|
||||
("%s", list(connection.waiting_requests.keys())),
|
||||
("%s r%s", (connection.handshake.get("version"), connection.handshake.get("rev", "?"))),
|
||||
("%.2fs", time_correction),
|
||||
("%s", connection.sites)
|
||||
])
|
||||
yield "</table>"
|
||||
|
||||
def renderTrackers(self):
|
||||
# Trackers
|
||||
yield "<br><br><b>Trackers:</b><br>"
|
||||
yield "<table class='trackers'><tr> <th>address</th> <th>request</th> <th>successive errors</th> <th>last_request</th></tr>"
|
||||
from Site import SiteAnnouncer # importing at the top of the file breaks plugins
|
||||
for tracker_address, tracker_stat in sorted(SiteAnnouncer.global_stats.items()):
|
||||
yield self.formatTableRow([
|
||||
("%s", tracker_address),
|
||||
("%s", tracker_stat["num_request"]),
|
||||
("%s", tracker_stat["num_error"]),
|
||||
("%.0f min ago", min(999, (time.time() - tracker_stat["time_request"]) / 60))
|
||||
])
|
||||
yield "</table>"
|
||||
|
||||
if "AnnounceShare" in PluginManager.plugin_manager.plugin_names:
|
||||
yield "<br><br><b>Shared trackers:</b><br>"
|
||||
yield "<table class='trackers'><tr> <th>address</th> <th>added</th> <th>found</th> <th>latency</th> <th>successive errors</th> <th>last_success</th></tr>"
|
||||
from AnnounceShare import AnnounceSharePlugin
|
||||
for tracker_address, tracker_stat in sorted(AnnounceSharePlugin.tracker_storage.getTrackers().items()):
|
||||
yield self.formatTableRow([
|
||||
("%s", tracker_address),
|
||||
("%.0f min ago", min(999, (time.time() - tracker_stat["time_added"]) / 60)),
|
||||
("%.0f min ago", min(999, (time.time() - tracker_stat.get("time_found", 0)) / 60)),
|
||||
("%.3fs", tracker_stat["latency"]),
|
||||
("%s", tracker_stat["num_error"]),
|
||||
("%.0f min ago", min(999, (time.time() - tracker_stat["time_success"]) / 60)),
|
||||
])
|
||||
yield "</table>"
|
||||
|
||||
def renderTor(self):
|
||||
import main
|
||||
yield "<br><br><b>Tor hidden services (status: %s):</b><br>" % main.file_server.tor_manager.status
|
||||
for site_address, onion in list(main.file_server.tor_manager.site_onions.items()):
|
||||
yield "- %-34s: %s<br>" % (site_address, onion)
|
||||
|
||||
def renderDbStats(self):
|
||||
yield "<br><br><b>Db</b>:<br>"
|
||||
for db in Db.opened_dbs:
|
||||
tables = [row["name"] for row in db.execute("SELECT name FROM sqlite_master WHERE type = 'table'").fetchall()]
|
||||
table_rows = {}
|
||||
for table in tables:
|
||||
table_rows[table] = db.execute("SELECT COUNT(*) AS c FROM %s" % table).fetchone()["c"]
|
||||
db_size = os.path.getsize(db.db_path) / 1024.0 / 1024.0
|
||||
yield "- %.3fs: %s %.3fMB, table rows: %s<br>" % (
|
||||
time.time() - db.last_query_time, db.db_path, db_size, json.dumps(table_rows, sort_keys=True)
|
||||
)
|
||||
|
||||
def renderSites(self):
|
||||
yield "<br><br><b>Sites</b>:"
|
||||
yield "<table>"
|
||||
yield "<tr><th>address</th> <th>connected</th> <th title='connected/good/total'>peers</th> <th>content.json</th> <th>out</th> <th>in</th> </tr>"
|
||||
for site in list(self.server.sites.values()):
|
||||
yield self.formatTableRow([
|
||||
(
|
||||
"""<a href='#' onclick='document.getElementById("peers_%s").style.display="initial"; return false'>%s</a>""",
|
||||
(site.address, site.address)
|
||||
),
|
||||
("%s", [peer.connection.id for peer in list(site.peers.values()) if peer.connection and peer.connection.connected]),
|
||||
("%s/%s/%s", (
|
||||
len([peer for peer in list(site.peers.values()) if peer.connection and peer.connection.connected]),
|
||||
len(site.getConnectablePeers(100)),
|
||||
len(site.peers)
|
||||
)),
|
||||
("%s (loaded: %s)", (
|
||||
len(site.content_manager.contents),
|
||||
len([key for key, val in dict(site.content_manager.contents).items() if val])
|
||||
)),
|
||||
("%.0fk", site.settings.get("bytes_sent", 0) / 1024),
|
||||
("%.0fk", site.settings.get("bytes_recv", 0) / 1024),
|
||||
], "serving-%s" % site.settings["serving"])
|
||||
yield "<tr><td id='peers_%s' style='display: none; white-space: pre' colspan=6>" % site.address
|
||||
for key, peer in list(site.peers.items()):
|
||||
if peer.time_found:
|
||||
time_found = int(time.time() - peer.time_found) / 60
|
||||
else:
|
||||
time_found = "--"
|
||||
if peer.connection:
|
||||
connection_id = peer.connection.id
|
||||
else:
|
||||
connection_id = None
|
||||
if site.content_manager.has_optional_files:
|
||||
yield "Optional files: %4s " % len(peer.hashfield)
|
||||
time_added = (time.time() - peer.time_added) / (60 * 60 * 24)
|
||||
yield "(#%4s, rep: %2s, err: %s, found: %.1fs min, add: %.1f day) %30s -<br>" % (connection_id, peer.reputation, peer.connection_error, time_found, time_added, key)
|
||||
yield "<br></td></tr>"
|
||||
yield "</table>"
|
||||
|
||||
def renderBigfiles(self):
|
||||
yield "<br><br><b>Big files</b>:<br>"
|
||||
for site in list(self.server.sites.values()):
|
||||
if not site.settings.get("has_bigfile"):
|
||||
continue
|
||||
bigfiles = {}
|
||||
yield """<a href="#" onclick='document.getElementById("bigfiles_%s").style.display="initial"; return false'>%s</a><br>""" % (site.address, site.address)
|
||||
for peer in list(site.peers.values()):
|
||||
if not peer.time_piecefields_updated:
|
||||
continue
|
||||
for sha512, piecefield in peer.piecefields.items():
|
||||
if sha512 not in bigfiles:
|
||||
bigfiles[sha512] = []
|
||||
bigfiles[sha512].append(peer)
|
||||
|
||||
yield "<div id='bigfiles_%s' style='display: none'>" % site.address
|
||||
for sha512, peers in bigfiles.items():
|
||||
yield "<br> - " + sha512 + " (hash id: %s)<br>" % site.content_manager.hashfield.getHashId(sha512)
|
||||
yield "<table>"
|
||||
for peer in peers:
|
||||
yield "<tr><td>" + peer.key + "</td><td>" + peer.piecefields[sha512].tostring() + "</td></tr>"
|
||||
yield "</table>"
|
||||
yield "</div>"
|
||||
|
||||
def renderRequests(self):
|
||||
import main
|
||||
yield "<div style='float: left'>"
|
||||
yield "<br><br><b>Sent commands</b>:<br>"
|
||||
yield "<table>"
|
||||
for stat_key, stat in sorted(main.file_server.stat_sent.items(), key=lambda i: i[1]["bytes"], reverse=True):
|
||||
yield "<tr><td>%s</td><td style='white-space: nowrap'>x %s =</td><td>%.0fkB</td></tr>" % (stat_key, stat["num"], stat["bytes"] / 1024)
|
||||
yield "</table>"
|
||||
yield "</div>"
|
||||
|
||||
yield "<div style='float: left; margin-left: 20%; max-width: 50%'>"
|
||||
yield "<br><br><b>Received commands</b>:<br>"
|
||||
yield "<table>"
|
||||
for stat_key, stat in sorted(main.file_server.stat_recv.items(), key=lambda i: i[1]["bytes"], reverse=True):
|
||||
yield "<tr><td>%s</td><td style='white-space: nowrap'>x %s =</td><td>%.0fkB</td></tr>" % (stat_key, stat["num"], stat["bytes"] / 1024)
|
||||
yield "</table>"
|
||||
yield "</div>"
|
||||
yield "<div style='clear: both'></div>"
|
||||
|
||||
def renderMemory(self):
|
||||
import gc
|
||||
from Ui import UiRequest
|
||||
|
||||
hpy = None
|
||||
if self.get.get("size") == "1": # Calc obj size
|
||||
try:
|
||||
import guppy
|
||||
hpy = guppy.hpy()
|
||||
except Exception:
|
||||
pass
|
||||
self.sendHeader()
|
||||
|
||||
# Object types
|
||||
|
||||
obj_count = {}
|
||||
for obj in gc.get_objects():
|
||||
obj_type = str(type(obj))
|
||||
if obj_type not in obj_count:
|
||||
obj_count[obj_type] = [0, 0]
|
||||
obj_count[obj_type][0] += 1 # Count
|
||||
obj_count[obj_type][1] += float(sys.getsizeof(obj)) / 1024 # Size
|
||||
|
||||
yield "<br><br><b>Objects in memory (types: %s, total: %s, %.2fkb):</b><br>" % (
|
||||
len(obj_count),
|
||||
sum([stat[0] for stat in list(obj_count.values())]),
|
||||
sum([stat[1] for stat in list(obj_count.values())])
|
||||
)
|
||||
|
||||
for obj, stat in sorted(list(obj_count.items()), key=lambda x: x[1][0], reverse=True): # Sorted by count
|
||||
yield " - %.1fkb = %s x <a href=\"/Listobj?type=%s\">%s</a><br>" % (stat[1], stat[0], obj, html.escape(obj))
|
||||
|
||||
# Classes
|
||||
|
||||
class_count = {}
|
||||
for obj in gc.get_objects():
|
||||
obj_type = str(type(obj))
|
||||
if obj_type != "<type 'instance'>":
|
||||
continue
|
||||
class_name = obj.__class__.__name__
|
||||
if class_name not in class_count:
|
||||
class_count[class_name] = [0, 0]
|
||||
class_count[class_name][0] += 1 # Count
|
||||
class_count[class_name][1] += float(sys.getsizeof(obj)) / 1024 # Size
|
||||
|
||||
yield "<br><br><b>Classes in memory (types: %s, total: %s, %.2fkb):</b><br>" % (
|
||||
len(class_count),
|
||||
sum([stat[0] for stat in list(class_count.values())]),
|
||||
sum([stat[1] for stat in list(class_count.values())])
|
||||
)
|
||||
|
||||
for obj, stat in sorted(list(class_count.items()), key=lambda x: x[1][0], reverse=True): # Sorted by count
|
||||
yield " - %.1fkb = %s x <a href=\"/Dumpobj?class=%s\">%s</a><br>" % (stat[1], stat[0], obj, html.escape(obj))
|
||||
|
||||
from greenlet import greenlet
|
||||
objs = [obj for obj in gc.get_objects() if isinstance(obj, greenlet)]
|
||||
yield "<br>Greenlets (%s):<br>" % len(objs)
|
||||
for obj in objs:
|
||||
yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
|
||||
|
||||
from Worker import Worker
|
||||
objs = [obj for obj in gc.get_objects() if isinstance(obj, Worker)]
|
||||
yield "<br>Workers (%s):<br>" % len(objs)
|
||||
for obj in objs:
|
||||
yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
|
||||
|
||||
from Connection import Connection
|
||||
objs = [obj for obj in gc.get_objects() if isinstance(obj, Connection)]
|
||||
yield "<br>Connections (%s):<br>" % len(objs)
|
||||
for obj in objs:
|
||||
yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
|
||||
|
||||
from socket import socket
|
||||
objs = [obj for obj in gc.get_objects() if isinstance(obj, socket)]
|
||||
yield "<br>Sockets (%s):<br>" % len(objs)
|
||||
for obj in objs:
|
||||
yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
|
||||
|
||||
from msgpack import Unpacker
|
||||
objs = [obj for obj in gc.get_objects() if isinstance(obj, Unpacker)]
|
||||
yield "<br>Msgpack unpacker (%s):<br>" % len(objs)
|
||||
for obj in objs:
|
||||
yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
|
||||
|
||||
from Site.Site import Site
|
||||
objs = [obj for obj in gc.get_objects() if isinstance(obj, Site)]
|
||||
yield "<br>Sites (%s):<br>" % len(objs)
|
||||
for obj in objs:
|
||||
yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
|
||||
|
||||
objs = [obj for obj in gc.get_objects() if isinstance(obj, self.server.log.__class__)]
|
||||
yield "<br>Loggers (%s):<br>" % len(objs)
|
||||
for obj in objs:
|
||||
yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj.name)))
|
||||
|
||||
objs = [obj for obj in gc.get_objects() if isinstance(obj, UiRequest)]
|
||||
yield "<br>UiRequests (%s):<br>" % len(objs)
|
||||
for obj in objs:
|
||||
yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
|
||||
|
||||
from Peer import Peer
|
||||
objs = [obj for obj in gc.get_objects() if isinstance(obj, Peer)]
|
||||
yield "<br>Peers (%s):<br>" % len(objs)
|
||||
for obj in objs:
|
||||
yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
|
||||
|
||||
objs = [(key, val) for key, val in sys.modules.items() if val is not None]
|
||||
objs.sort()
|
||||
yield "<br>Modules (%s):<br>" % len(objs)
|
||||
for module_name, module in objs:
|
||||
yield " - %.3fkb: %s %s<br>" % (self.getObjSize(module, hpy), module_name, html.escape(repr(module)))
|
||||
|
||||
# /Stats entry point
|
||||
@helper.encodeResponse
|
||||
def actionStats(self):
|
||||
import gc
|
||||
|
||||
self.sendHeader()
|
||||
|
||||
if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local:
|
||||
yield "This function is disabled on this proxy"
|
||||
return
|
||||
|
||||
s = time.time()
|
||||
|
||||
# Style
|
||||
yield """
|
||||
<style>
|
||||
* { font-family: monospace }
|
||||
table td, table th { text-align: right; padding: 0px 10px }
|
||||
.connections td { white-space: nowrap }
|
||||
.serving-False { opacity: 0.3 }
|
||||
</style>
|
||||
"""
|
||||
|
||||
renderers = [
|
||||
self.renderHead(),
|
||||
self.renderConnectionsTable(),
|
||||
self.renderTrackers(),
|
||||
self.renderTor(),
|
||||
self.renderDbStats(),
|
||||
self.renderSites(),
|
||||
self.renderBigfiles(),
|
||||
self.renderRequests()
|
||||
|
||||
]
|
||||
|
||||
for part in itertools.chain(*renderers):
|
||||
yield part
|
||||
|
||||
if config.debug:
|
||||
for part in self.renderMemory():
|
||||
yield part
|
||||
|
||||
gc.collect() # Implicit grabage collection
|
||||
yield "Done in %.1f" % (time.time() - s)
|
||||
|
||||
@helper.encodeResponse
|
||||
def actionDumpobj(self):
|
||||
|
||||
import gc
|
||||
import sys
|
||||
|
||||
self.sendHeader()
|
||||
|
||||
if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local:
|
||||
yield "This function is disabled on this proxy"
|
||||
return
|
||||
|
||||
# No more if not in debug mode
|
||||
if not config.debug:
|
||||
yield "Not in debug mode"
|
||||
return
|
||||
|
||||
class_filter = self.get.get("class")
|
||||
|
||||
yield """
|
||||
<style>
|
||||
* { font-family: monospace; white-space: pre }
|
||||
table * { text-align: right; padding: 0px 10px }
|
||||
</style>
|
||||
"""
|
||||
|
||||
objs = gc.get_objects()
|
||||
for obj in objs:
|
||||
obj_type = str(type(obj))
|
||||
if obj_type != "<type 'instance'>" or obj.__class__.__name__ != class_filter:
|
||||
continue
|
||||
yield "%.1fkb %s... " % (float(sys.getsizeof(obj)) / 1024, html.escape(str(obj)))
|
||||
for attr in dir(obj):
|
||||
yield "- %s: %s<br>" % (attr, html.escape(str(getattr(obj, attr))))
|
||||
yield "<br>"
|
||||
|
||||
gc.collect() # Implicit grabage collection
|
||||
|
||||
@helper.encodeResponse
|
||||
def actionListobj(self):
|
||||
|
||||
import gc
|
||||
import sys
|
||||
|
||||
self.sendHeader()
|
||||
|
||||
if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local:
|
||||
yield "This function is disabled on this proxy"
|
||||
return
|
||||
|
||||
# No more if not in debug mode
|
||||
if not config.debug:
|
||||
yield "Not in debug mode"
|
||||
return
|
||||
|
||||
type_filter = self.get.get("type")
|
||||
|
||||
yield """
|
||||
<style>
|
||||
* { font-family: monospace; white-space: pre }
|
||||
table * { text-align: right; padding: 0px 10px }
|
||||
</style>
|
||||
"""
|
||||
|
||||
yield "Listing all %s objects in memory...<br>" % html.escape(type_filter)
|
||||
|
||||
ref_count = {}
|
||||
objs = gc.get_objects()
|
||||
for obj in objs:
|
||||
obj_type = str(type(obj))
|
||||
if obj_type != type_filter:
|
||||
continue
|
||||
refs = [
|
||||
ref for ref in gc.get_referrers(obj)
|
||||
if hasattr(ref, "__class__") and
|
||||
ref.__class__.__name__ not in ["list", "dict", "function", "type", "frame", "WeakSet", "tuple"]
|
||||
]
|
||||
if not refs:
|
||||
continue
|
||||
try:
|
||||
yield "%.1fkb <span title=\"%s\">%s</span>... " % (
|
||||
float(sys.getsizeof(obj)) / 1024, html.escape(str(obj)), html.escape(str(obj)[0:100].ljust(100))
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
for ref in refs:
|
||||
yield " ["
|
||||
if "object at" in str(ref) or len(str(ref)) > 100:
|
||||
yield str(ref.__class__.__name__)
|
||||
else:
|
||||
yield str(ref.__class__.__name__) + ":" + html.escape(str(ref))
|
||||
yield "] "
|
||||
ref_type = ref.__class__.__name__
|
||||
if ref_type not in ref_count:
|
||||
ref_count[ref_type] = [0, 0]
|
||||
ref_count[ref_type][0] += 1 # Count
|
||||
ref_count[ref_type][1] += float(sys.getsizeof(obj)) / 1024 # Size
|
||||
yield "<br>"
|
||||
|
||||
yield "<br>Object referrer (total: %s, %.2fkb):<br>" % (len(ref_count), sum([stat[1] for stat in list(ref_count.values())]))
|
||||
|
||||
for obj, stat in sorted(list(ref_count.items()), key=lambda x: x[1][0], reverse=True)[0:30]: # Sorted by count
|
||||
yield " - %.1fkb = %s x %s<br>" % (stat[1], stat[0], html.escape(str(obj)))
|
||||
|
||||
gc.collect() # Implicit grabage collection
|
||||
|
||||
@helper.encodeResponse
|
||||
def actionGcCollect(self):
|
||||
import gc
|
||||
self.sendHeader()
|
||||
yield str(gc.collect())
|
||||
|
||||
# /About entry point
|
||||
@helper.encodeResponse
|
||||
def actionEnv(self):
|
||||
import main
|
||||
|
||||
self.sendHeader()
|
||||
|
||||
yield """
|
||||
<style>
|
||||
* { font-family: monospace; white-space: pre; }
|
||||
h2 { font-size: 100%; margin-bottom: 0px; }
|
||||
small { opacity: 0.5; }
|
||||
table { border-collapse: collapse; }
|
||||
td { padding-right: 10px; }
|
||||
</style>
|
||||
"""
|
||||
|
||||
if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local:
|
||||
yield "This function is disabled on this proxy"
|
||||
return
|
||||
|
||||
yield from main.actions.testEnv(format="html")
|
||||
|
||||
|
||||
@PluginManager.registerTo("Actions")
|
||||
class ActionsPlugin:
|
||||
def formatTable(self, *rows, format="text"):
|
||||
if format == "html":
|
||||
return self.formatTableHtml(*rows)
|
||||
else:
|
||||
return self.formatTableText(*rows)
|
||||
|
||||
def formatHead(self, title, format="text"):
|
||||
if format == "html":
|
||||
return "<h2>%s</h2>" % title
|
||||
else:
|
||||
return "\n* %s\n" % title
|
||||
|
||||
def formatTableHtml(self, *rows):
|
||||
yield "<table>"
|
||||
for row in rows:
|
||||
yield "<tr>"
|
||||
for col in row:
|
||||
yield "<td>%s</td>" % html.escape(str(col))
|
||||
yield "</tr>"
|
||||
yield "</table>"
|
||||
|
||||
def formatTableText(self, *rows):
|
||||
for row in rows:
|
||||
yield " "
|
||||
for col in row:
|
||||
yield " " + str(col)
|
||||
yield "\n"
|
||||
|
||||
def testEnv(self, format="text"):
|
||||
import gevent
|
||||
import msgpack
|
||||
import pkg_resources
|
||||
import importlib
|
||||
import coincurve
|
||||
import sqlite3
|
||||
from Crypt import CryptBitcoin
|
||||
|
||||
yield "\n"
|
||||
|
||||
yield from self.formatTable(
|
||||
["ZeroNet version:", "%s rev%s" % (config.version, config.rev)],
|
||||
["Python:", "%s" % sys.version],
|
||||
["Platform:", "%s" % sys.platform],
|
||||
["Crypt verify lib:", "%s" % CryptBitcoin.lib_verify_best],
|
||||
["OpenSSL:", "%s" % CryptBitcoin.sslcrypto.ecc.get_backend()],
|
||||
["Libsecp256k1:", "%s" % type(coincurve._libsecp256k1.lib).__name__],
|
||||
["SQLite:", "%s, API: %s" % (sqlite3.sqlite_version, sqlite3.version)],
|
||||
format=format
|
||||
)
|
||||
|
||||
|
||||
yield self.formatHead("Libraries:")
|
||||
rows = []
|
||||
for lib_name in ["gevent", "greenlet", "msgpack", "base58", "merkletools", "rsa", "socks", "pyasn1", "gevent_ws", "websocket", "maxminddb"]:
|
||||
try:
|
||||
module = importlib.import_module(lib_name)
|
||||
if "__version__" in dir(module):
|
||||
version = module.__version__
|
||||
elif "version" in dir(module):
|
||||
version = module.version
|
||||
else:
|
||||
version = "unknown version"
|
||||
|
||||
if type(version) is tuple:
|
||||
version = ".".join(map(str, version))
|
||||
|
||||
rows.append(["- %s:" % lib_name, version, "at " + module.__file__])
|
||||
except Exception as err:
|
||||
rows.append(["! Error importing %s:", repr(err)])
|
||||
|
||||
"""
|
||||
try:
|
||||
yield " - %s<br>" % html.escape(repr(pkg_resources.get_distribution(lib_name)))
|
||||
except Exception as err:
|
||||
yield " ! %s<br>" % html.escape(repr(err))
|
||||
"""
|
||||
|
||||
yield from self.formatTable(*rows, format=format)
|
||||
|
||||
yield self.formatHead("Library config:", format=format)
|
||||
|
||||
yield from self.formatTable(
|
||||
["- gevent:", gevent.config.loop.__module__],
|
||||
["- msgpack unpacker:", msgpack.Unpacker.__module__],
|
||||
format=format
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
from . import StatsPlugin
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Stats",
|
||||
"description": "/Stats and /Benchmark pages.",
|
||||
"default": "enabled"
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
import os
|
||||
import sys
|
||||
import atexit
|
||||
|
||||
from Plugin import PluginManager
|
||||
from Config import config
|
||||
from Translate import Translate
|
||||
|
||||
allow_reload = False # No source reload supported in this plugin
|
||||
|
||||
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
|
||||
if "_" not in locals():
|
||||
_ = Translate(plugin_dir + "/languages/")
|
||||
|
||||
|
||||
@PluginManager.registerTo("Actions")
|
||||
class ActionsPlugin(object):
|
||||
|
||||
def main(self):
|
||||
global notificationicon, winfolders
|
||||
from .lib import notificationicon, winfolders
|
||||
import gevent.threadpool
|
||||
import main
|
||||
|
||||
self.main = main
|
||||
|
||||
icon = notificationicon.NotificationIcon(
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'trayicon.ico'),
|
||||
"ZeroNet %s" % config.version
|
||||
)
|
||||
self.icon = icon
|
||||
|
||||
self.console = False
|
||||
|
||||
@atexit.register
|
||||
def hideIcon():
|
||||
try:
|
||||
icon.die()
|
||||
except Exception as err:
|
||||
print("Error removing trayicon: %s" % err)
|
||||
|
||||
ui_ip = config.ui_ip if config.ui_ip != "*" else "127.0.0.1"
|
||||
|
||||
if ":" in ui_ip:
|
||||
ui_ip = "[" + ui_ip + "]"
|
||||
|
||||
icon.items = [
|
||||
(self.titleIp, False),
|
||||
(self.titleConnections, False),
|
||||
(self.titleTransfer, False),
|
||||
(self.titleConsole, self.toggleConsole),
|
||||
(self.titleAutorun, self.toggleAutorun),
|
||||
"--",
|
||||
(_["ZeroNet Twitter"], lambda: self.opensite("https://twitter.com/HelloZeroNet")),
|
||||
(_["ZeroNet Reddit"], lambda: self.opensite("http://www.reddit.com/r/zeronet/")),
|
||||
(_["ZeroNet Github"], lambda: self.opensite("https://github.com/HelloZeroNet/ZeroNet")),
|
||||
(_["Report bug/request feature"], lambda: self.opensite("https://github.com/HelloZeroNet/ZeroNet/issues")),
|
||||
"--",
|
||||
(_["!Open ZeroNet"], lambda: self.opensite("http://%s:%s/%s" % (ui_ip, config.ui_port, config.homepage))),
|
||||
"--",
|
||||
(_["Quit"], self.quit),
|
||||
]
|
||||
|
||||
if not notificationicon.hasConsole():
|
||||
del icon.items[3]
|
||||
|
||||
icon.clicked = lambda: self.opensite("http://%s:%s/%s" % (ui_ip, config.ui_port, config.homepage))
|
||||
self.quit_servers_event = gevent.threadpool.ThreadResult(
|
||||
lambda res: gevent.spawn_later(0.1, self.quitServers), gevent.threadpool.get_hub(), lambda: True
|
||||
) # Fix gevent thread switch error
|
||||
gevent.threadpool.start_new_thread(icon._run, ()) # Start in real thread (not gevent compatible)
|
||||
super(ActionsPlugin, self).main()
|
||||
icon._die = True
|
||||
|
||||
def quit(self):
|
||||
self.icon.die()
|
||||
self.quit_servers_event.set(True)
|
||||
|
||||
def quitServers(self):
|
||||
self.main.ui_server.stop()
|
||||
self.main.file_server.stop()
|
||||
|
||||
def opensite(self, url):
|
||||
import webbrowser
|
||||
webbrowser.open(url, new=0)
|
||||
|
||||
def titleIp(self):
|
||||
title = "!IP: %s " % ", ".join(self.main.file_server.ip_external_list)
|
||||
if any(self.main.file_server.port_opened):
|
||||
title += _["(active)"]
|
||||
else:
|
||||
title += _["(passive)"]
|
||||
return title
|
||||
|
||||
def titleConnections(self):
|
||||
title = _["Connections: %s"] % len(self.main.file_server.connections)
|
||||
return title
|
||||
|
||||
def titleTransfer(self):
|
||||
title = _["Received: %.2f MB | Sent: %.2f MB"] % (
|
||||
float(self.main.file_server.bytes_recv) / 1024 / 1024,
|
||||
float(self.main.file_server.bytes_sent) / 1024 / 1024
|
||||
)
|
||||
return title
|
||||
|
||||
def titleConsole(self):
|
||||
translate = _["Show console window"]
|
||||
if self.console:
|
||||
return "+" + translate
|
||||
else:
|
||||
return translate
|
||||
|
||||
def toggleConsole(self):
|
||||
if self.console:
|
||||
notificationicon.hideConsole()
|
||||
self.console = False
|
||||
else:
|
||||
notificationicon.showConsole()
|
||||
self.console = True
|
||||
|
||||
def getAutorunPath(self):
|
||||
return "%s\\zeronet.cmd" % winfolders.get(winfolders.STARTUP)
|
||||
|
||||
def formatAutorun(self):
|
||||
args = sys.argv[:]
|
||||
|
||||
if not getattr(sys, 'frozen', False): # Not frozen
|
||||
args.insert(0, sys.executable)
|
||||
cwd = os.getcwd()
|
||||
else:
|
||||
cwd = os.path.dirname(sys.executable)
|
||||
|
||||
ignored_args = [
|
||||
"--open_browser", "default_browser",
|
||||
"--dist_type", "bundle_win64"
|
||||
]
|
||||
|
||||
if sys.platform == 'win32':
|
||||
args = ['"%s"' % arg for arg in args if arg and arg not in ignored_args]
|
||||
cmd = " ".join(args)
|
||||
|
||||
# Dont open browser on autorun
|
||||
cmd = cmd.replace("start.py", "zeronet.py").strip()
|
||||
cmd += ' --open_browser ""'
|
||||
|
||||
return "\r\n".join([
|
||||
'@echo off',
|
||||
'chcp 65001 > nul',
|
||||
'set PYTHONIOENCODING=utf-8',
|
||||
'cd /D \"%s\"' % cwd,
|
||||
'start "" %s' % cmd
|
||||
])
|
||||
|
||||
def isAutorunEnabled(self):
|
||||
path = self.getAutorunPath()
|
||||
return os.path.isfile(path) and open(path, "rb").read().decode("utf8") == self.formatAutorun()
|
||||
|
||||
def titleAutorun(self):
|
||||
translate = _["Start ZeroNet when Windows starts"]
|
||||
if self.isAutorunEnabled():
|
||||
return "+" + translate
|
||||
else:
|
||||
return translate
|
||||
|
||||
def toggleAutorun(self):
|
||||
if self.isAutorunEnabled():
|
||||
os.unlink(self.getAutorunPath())
|
||||
else:
|
||||
open(self.getAutorunPath(), "wb").write(self.formatAutorun().encode("utf8"))
|
|
@ -0,0 +1,4 @@
|
|||
import sys
|
||||
|
||||
if sys.platform == 'win32':
|
||||
from . import TrayiconPlugin
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"ZeroNet Twitter": "ZeroNet Twitter",
|
||||
"ZeroNet Reddit": "ZeroNet Reddit",
|
||||
"ZeroNet Github": "ZeroNet Github",
|
||||
"Report bug/request feature": "Reportar fallo/sugerir característica",
|
||||
"!Open ZeroNet": "!Abrir ZeroNet",
|
||||
"Quit": "Sair",
|
||||
"(active)": "(activo)",
|
||||
"(passive)": "(pasivo)",
|
||||
"Connections: %s": "Conecciones: %s",
|
||||
"Received: %.2f MB | Sent: %.2f MB": "Recibido: %.2f MB | Enviado: %.2f MB",
|
||||
"Show console window": "Mostrar consola",
|
||||
"Start ZeroNet when Windows starts": "Iniciar Zeronet cuando inicie Windows"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"ZeroNet Twitter": "ZeroNet Twitter",
|
||||
"ZeroNet Reddit": "ZeroNet Reddit",
|
||||
"ZeroNet Github": "ZeroNet Github",
|
||||
"Report bug/request feature": "Rapport d'erreur/Demander une fonctionnalité",
|
||||
"!Open ZeroNet": "!Ouvrir ZeroNet",
|
||||
"Quit": "Quitter",
|
||||
"(active)": "(actif)",
|
||||
"(passive)": "(passif)",
|
||||
"Connections: %s": "Connexions: %s",
|
||||
"Received: %.2f MB | Sent: %.2f MB": "Reçu: %.2f MB | Envoyé: %.2f MB",
|
||||
"Show console window": "Afficher la console",
|
||||
"Start ZeroNet when Windows starts": "Lancer ZeroNet au démarrage de Windows"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"ZeroNet Twitter": "ZeroNet Twitter",
|
||||
"ZeroNet Reddit": "ZeroNet Reddit",
|
||||
"ZeroNet Github": "ZeroNet Github",
|
||||
"Report bug/request feature": "Hiba bejelentés/ötletek",
|
||||
"!Open ZeroNet": "!ZeroNet megnyitása",
|
||||
"Quit": "Kilépés",
|
||||
"(active)": "(aktív)",
|
||||
"(passive)": "(passive)",
|
||||
"Connections: %s": "Kapcsolatok: %s",
|
||||
"Received: %.2f MB | Sent: %.2f MB": "Fogadott: %.2f MB | Küldött: %.2f MB",
|
||||
"Show console window": "Parancssor mutatása",
|
||||
"Start ZeroNet when Windows starts": "ZeroNet indítása a Windows-al együtt"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"ZeroNet Twitter": "ZeroNet Twitter",
|
||||
"ZeroNet Reddit": "ZeroNet Reddit",
|
||||
"ZeroNet Github": "ZeroNet Github",
|
||||
"Report bug/request feature": "Segnala bug/richiesta di una funzione",
|
||||
"!Open ZeroNet": "!Apri ZeroNet",
|
||||
"Quit": "Chiudi",
|
||||
"(active)": "(attivo)",
|
||||
"(passive)": "(passivo)",
|
||||
"Connections: %s": "Connessioni: %s",
|
||||
"Received: %.2f MB | Sent: %.2f MB": "Ricevuto: %.2f MB | Inviato: %.2f MB",
|
||||
"Show console window": "Mostra finestra console",
|
||||
"Start ZeroNet when Windows starts": "Avvia ZeroNet all'avvio di Windows"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"ZeroNet Twitter": "ZeroNet Twitter",
|
||||
"ZeroNet Reddit": "ZeroNet Reddit",
|
||||
"ZeroNet Github": "ZeroNet Github",
|
||||
"Report bug/request feature": "バグ報告/要望",
|
||||
"!Open ZeroNet": "!ZeroNetをブラウザで開く",
|
||||
"Quit": "閉じる",
|
||||
"(active)": "(アクティブ)",
|
||||
"(passive)": "(パッシブ)",
|
||||
"Connections: %s": "接続数: %s",
|
||||
"Received: %.2f MB | Sent: %.2f MB": "受信: %.2f MB | 送信: %.2f MB",
|
||||
"Show console window": "コンソールを表示",
|
||||
"Start ZeroNet when Windows starts": "Windows起動時にZeroNetも起動する"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"ZeroNet Twitter": "ZeroNet Twitter",
|
||||
"ZeroNet Reddit": "ZeroNet Reddit",
|
||||
"ZeroNet Github": "ZeroNet Github",
|
||||
"Report bug/request feature": "Zgłoś błąd / propozycję",
|
||||
"!Open ZeroNet": "!Otwórz ZeroNet",
|
||||
"Quit": "Zamknij",
|
||||
"(active)": "(aktywny)",
|
||||
"(passive)": "(pasywny)",
|
||||
"Connections: %s": "Połączenia: %s",
|
||||
"Received: %.2f MB | Sent: %.2f MB": "Odebrano: %.2f MB | Wysłano: %.2f MB",
|
||||
"Show console window": "Pokaż okno konsoli",
|
||||
"Start ZeroNet when Windows starts": "Uruchom ZeroNet podczas startu Windows"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"ZeroNet Twitter": "ZeroNet Twitter",
|
||||
"ZeroNet Reddit": "ZeroNet Reddit",
|
||||
"ZeroNet Github": "ZeroNet Github",
|
||||
"Report bug/request feature": "Reportar bug/sugerir recurso",
|
||||
"!Open ZeroNet": "!Abrir ZeroNet",
|
||||
"Quit": "Sair",
|
||||
"(active)": "(ativo)",
|
||||
"(passive)": "(passivo)",
|
||||
"Connections: %s": "Conexões: %s",
|
||||
"Received: %.2f MB | Sent: %.2f MB": "Recebido: %.2f MB | Enviado: %.2f MB",
|
||||
"Show console window": "Mostrar console",
|
||||
"Start ZeroNet when Windows starts": "Iniciar o ZeroNet quando o Windows for iniciado"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"ZeroNet Twitter": "ZeroNet Twitter",
|
||||
"ZeroNet Reddit": "ZeroNet Reddit",
|
||||
"ZeroNet Github": "ZeroNet Github",
|
||||
"Report bug/request feature": "Hata bildir/geliştirme taleb et",
|
||||
"!Open ZeroNet": "!ZeroNet'i Aç",
|
||||
"Quit": "Kapat",
|
||||
"(active)": "(aktif)",
|
||||
"(passive)": "(pasif)",
|
||||
"Connections: %s": "Bağlantı sayısı: %s",
|
||||
"Received: %.2f MB | Sent: %.2f MB": "Gelen: %.2f MB | Gönderilen: %.2f MB",
|
||||
"Show console window": "Konsolu aç",
|
||||
"Start ZeroNet when Windows starts": "ZeroNet'i açılışta otomatik başlat"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"ZeroNet Twitter": "ZeroNet Twitter",
|
||||
"ZeroNet Reddit": "ZeroNet Reddit",
|
||||
"ZeroNet Github": "ZeroNet Github",
|
||||
"Report bug/request feature": "回饋问题/請求功能",
|
||||
"!Open ZeroNet": "!開啟 ZeroNet",
|
||||
"Quit": "退出",
|
||||
"(active)": "(主動模式)",
|
||||
"(passive)": "(被動模式)",
|
||||
"Connections: %s": "連線數: %s",
|
||||
"Received: %.2f MB | Sent: %.2f MB": "已收到: %.2f MB | 已傳送: %.2f MB",
|
||||
"Show console window": "顯示控制臺窗體",
|
||||
"Start ZeroNet when Windows starts": "在 Windows 啟動時執行 ZeroNet"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"ZeroNet Twitter": "ZeroNet Twitter",
|
||||
"ZeroNet Reddit": "ZeroNet Reddit",
|
||||
"ZeroNet Github": "ZeroNet Github",
|
||||
"Report bug/request feature": "反馈问题/请求功能",
|
||||
"!Open ZeroNet": "!打开 ZeroNet",
|
||||
"Quit": "退出",
|
||||
"(active)": "(主动模式)",
|
||||
"(passive)": "(被动模式)",
|
||||
"Connections: %s": "连接数: %s",
|
||||
"Received: %.2f MB | Sent: %.2f MB": "已接收: %.2f MB | 已发送: %.2f MB",
|
||||
"Show console window": "显示控制台窗口",
|
||||
"Start ZeroNet when Windows starts": "在 Windows 启动时运行 ZeroNet"
|
||||
}
|
|
@ -0,0 +1,730 @@
|
|||
# Pure ctypes windows taskbar notification icon
|
||||
# via https://gist.github.com/jasonbot/5759510
|
||||
# Modified for ZeroNet
|
||||
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
import os
|
||||
import uuid
|
||||
import time
|
||||
import gevent
|
||||
import threading
|
||||
try:
|
||||
from queue import Empty as queue_Empty # Python 3
|
||||
except ImportError:
|
||||
from Queue import Empty as queue_Empty # Python 2
|
||||
|
||||
__all__ = ['NotificationIcon']
|
||||
|
||||
# Create popup menu
|
||||
|
||||
CreatePopupMenu = ctypes.windll.user32.CreatePopupMenu
|
||||
CreatePopupMenu.restype = ctypes.wintypes.HMENU
|
||||
CreatePopupMenu.argtypes = []
|
||||
|
||||
MF_BYCOMMAND = 0x0
|
||||
MF_BYPOSITION = 0x400
|
||||
|
||||
MF_BITMAP = 0x4
|
||||
MF_CHECKED = 0x8
|
||||
MF_DISABLED = 0x2
|
||||
MF_ENABLED = 0x0
|
||||
MF_GRAYED = 0x1
|
||||
MF_MENUBARBREAK = 0x20
|
||||
MF_MENUBREAK = 0x40
|
||||
MF_OWNERDRAW = 0x100
|
||||
MF_POPUP = 0x10
|
||||
MF_SEPARATOR = 0x800
|
||||
MF_STRING = 0x0
|
||||
MF_UNCHECKED = 0x0
|
||||
|
||||
InsertMenu = ctypes.windll.user32.InsertMenuW
|
||||
InsertMenu.restype = ctypes.wintypes.BOOL
|
||||
InsertMenu.argtypes = [ctypes.wintypes.HMENU, ctypes.wintypes.UINT, ctypes.wintypes.UINT, ctypes.wintypes.UINT, ctypes.wintypes.LPCWSTR]
|
||||
|
||||
AppendMenu = ctypes.windll.user32.AppendMenuW
|
||||
AppendMenu.restype = ctypes.wintypes.BOOL
|
||||
AppendMenu.argtypes = [ctypes.wintypes.HMENU, ctypes.wintypes.UINT, ctypes.wintypes.UINT, ctypes.wintypes.LPCWSTR]
|
||||
|
||||
SetMenuDefaultItem = ctypes.windll.user32.SetMenuDefaultItem
|
||||
SetMenuDefaultItem.restype = ctypes.wintypes.BOOL
|
||||
SetMenuDefaultItem.argtypes = [ctypes.wintypes.HMENU, ctypes.wintypes.UINT, ctypes.wintypes.UINT]
|
||||
|
||||
class POINT(ctypes.Structure):
|
||||
_fields_ = [ ('x', ctypes.wintypes.LONG),
|
||||
('y', ctypes.wintypes.LONG)]
|
||||
|
||||
GetCursorPos = ctypes.windll.user32.GetCursorPos
|
||||
GetCursorPos.argtypes = [ctypes.POINTER(POINT)]
|
||||
|
||||
SetForegroundWindow = ctypes.windll.user32.SetForegroundWindow
|
||||
SetForegroundWindow.argtypes = [ctypes.wintypes.HWND]
|
||||
|
||||
TPM_LEFTALIGN = 0x0
|
||||
TPM_CENTERALIGN = 0x4
|
||||
TPM_RIGHTALIGN = 0x8
|
||||
|
||||
TPM_TOPALIGN = 0x0
|
||||
TPM_VCENTERALIGN = 0x10
|
||||
TPM_BOTTOMALIGN = 0x20
|
||||
|
||||
TPM_NONOTIFY = 0x80
|
||||
TPM_RETURNCMD = 0x100
|
||||
|
||||
TPM_LEFTBUTTON = 0x0
|
||||
TPM_RIGHTBUTTON = 0x2
|
||||
|
||||
TPM_HORNEGANIMATION = 0x800
|
||||
TPM_HORPOSANIMATION = 0x400
|
||||
TPM_NOANIMATION = 0x4000
|
||||
TPM_VERNEGANIMATION = 0x2000
|
||||
TPM_VERPOSANIMATION = 0x1000
|
||||
|
||||
TrackPopupMenu = ctypes.windll.user32.TrackPopupMenu
|
||||
TrackPopupMenu.restype = ctypes.wintypes.BOOL
|
||||
TrackPopupMenu.argtypes = [ctypes.wintypes.HMENU, ctypes.wintypes.UINT, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.wintypes.HWND, ctypes.c_void_p]
|
||||
|
||||
PostMessage = ctypes.windll.user32.PostMessageW
|
||||
PostMessage.restype = ctypes.wintypes.BOOL
|
||||
PostMessage.argtypes = [ctypes.wintypes.HWND, ctypes.wintypes.UINT, ctypes.wintypes.WPARAM, ctypes.wintypes.LPARAM]
|
||||
|
||||
DestroyMenu = ctypes.windll.user32.DestroyMenu
|
||||
DestroyMenu.restype = ctypes.wintypes.BOOL
|
||||
DestroyMenu.argtypes = [ctypes.wintypes.HMENU]
|
||||
|
||||
# Create notification icon
|
||||
|
||||
GUID = ctypes.c_ubyte * 16
|
||||
|
||||
class TimeoutVersionUnion(ctypes.Union):
|
||||
_fields_ = [('uTimeout', ctypes.wintypes.UINT),
|
||||
('uVersion', ctypes.wintypes.UINT),]
|
||||
|
||||
NIS_HIDDEN = 0x1
|
||||
NIS_SHAREDICON = 0x2
|
||||
|
||||
class NOTIFYICONDATA(ctypes.Structure):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NOTIFYICONDATA, self).__init__(*args, **kwargs)
|
||||
self.cbSize = ctypes.sizeof(self)
|
||||
_fields_ = [
|
||||
('cbSize', ctypes.wintypes.DWORD),
|
||||
('hWnd', ctypes.wintypes.HWND),
|
||||
('uID', ctypes.wintypes.UINT),
|
||||
('uFlags', ctypes.wintypes.UINT),
|
||||
('uCallbackMessage', ctypes.wintypes.UINT),
|
||||
('hIcon', ctypes.wintypes.HICON),
|
||||
('szTip', ctypes.wintypes.WCHAR * 64),
|
||||
('dwState', ctypes.wintypes.DWORD),
|
||||
('dwStateMask', ctypes.wintypes.DWORD),
|
||||
('szInfo', ctypes.wintypes.WCHAR * 256),
|
||||
('union', TimeoutVersionUnion),
|
||||
('szInfoTitle', ctypes.wintypes.WCHAR * 64),
|
||||
('dwInfoFlags', ctypes.wintypes.DWORD),
|
||||
('guidItem', GUID),
|
||||
('hBalloonIcon', ctypes.wintypes.HICON),
|
||||
]
|
||||
|
||||
NIM_ADD = 0
|
||||
NIM_MODIFY = 1
|
||||
NIM_DELETE = 2
|
||||
NIM_SETFOCUS = 3
|
||||
NIM_SETVERSION = 4
|
||||
|
||||
NIF_MESSAGE = 1
|
||||
NIF_ICON = 2
|
||||
NIF_TIP = 4
|
||||
NIF_STATE = 8
|
||||
NIF_INFO = 16
|
||||
NIF_GUID = 32
|
||||
NIF_REALTIME = 64
|
||||
NIF_SHOWTIP = 128
|
||||
|
||||
NIIF_NONE = 0
|
||||
NIIF_INFO = 1
|
||||
NIIF_WARNING = 2
|
||||
NIIF_ERROR = 3
|
||||
NIIF_USER = 4
|
||||
|
||||
NOTIFYICON_VERSION = 3
|
||||
NOTIFYICON_VERSION_4 = 4
|
||||
|
||||
Shell_NotifyIcon = ctypes.windll.shell32.Shell_NotifyIconW
|
||||
Shell_NotifyIcon.restype = ctypes.wintypes.BOOL
|
||||
Shell_NotifyIcon.argtypes = [ctypes.wintypes.DWORD, ctypes.POINTER(NOTIFYICONDATA)]
|
||||
|
||||
# Load icon/image
|
||||
|
||||
IMAGE_BITMAP = 0
|
||||
IMAGE_ICON = 1
|
||||
IMAGE_CURSOR = 2
|
||||
|
||||
LR_CREATEDIBSECTION = 0x00002000
|
||||
LR_DEFAULTCOLOR = 0x00000000
|
||||
LR_DEFAULTSIZE = 0x00000040
|
||||
LR_LOADFROMFILE = 0x00000010
|
||||
LR_LOADMAP3DCOLORS = 0x00001000
|
||||
LR_LOADTRANSPARENT = 0x00000020
|
||||
LR_MONOCHROME = 0x00000001
|
||||
LR_SHARED = 0x00008000
|
||||
LR_VGACOLOR = 0x00000080
|
||||
|
||||
OIC_SAMPLE = 32512
|
||||
OIC_HAND = 32513
|
||||
OIC_QUES = 32514
|
||||
OIC_BANG = 32515
|
||||
OIC_NOTE = 32516
|
||||
OIC_WINLOGO = 32517
|
||||
OIC_WARNING = OIC_BANG
|
||||
OIC_ERROR = OIC_HAND
|
||||
OIC_INFORMATION = OIC_NOTE
|
||||
|
||||
LoadImage = ctypes.windll.user32.LoadImageW
|
||||
LoadImage.restype = ctypes.wintypes.HANDLE
|
||||
LoadImage.argtypes = [ctypes.wintypes.HINSTANCE, ctypes.wintypes.LPCWSTR, ctypes.wintypes.UINT, ctypes.c_int, ctypes.c_int, ctypes.wintypes.UINT]
|
||||
|
||||
# CreateWindow call
|
||||
|
||||
WNDPROC = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.wintypes.HWND, ctypes.c_uint, ctypes.wintypes.WPARAM, ctypes.wintypes.LPARAM)
|
||||
DefWindowProc = ctypes.windll.user32.DefWindowProcW
|
||||
DefWindowProc.restype = ctypes.c_int
|
||||
DefWindowProc.argtypes = [ctypes.wintypes.HWND, ctypes.c_uint, ctypes.wintypes.WPARAM, ctypes.wintypes.LPARAM]
|
||||
|
||||
WS_OVERLAPPED = 0x00000000
|
||||
WS_POPUP = 0x80000000
|
||||
WS_CHILD = 0x40000000
|
||||
WS_MINIMIZE = 0x20000000
|
||||
WS_VISIBLE = 0x10000000
|
||||
WS_DISABLED = 0x08000000
|
||||
WS_CLIPSIBLINGS = 0x04000000
|
||||
WS_CLIPCHILDREN = 0x02000000
|
||||
WS_MAXIMIZE = 0x01000000
|
||||
WS_CAPTION = 0x00C00000
|
||||
WS_BORDER = 0x00800000
|
||||
WS_DLGFRAME = 0x00400000
|
||||
WS_VSCROLL = 0x00200000
|
||||
WS_HSCROLL = 0x00100000
|
||||
WS_SYSMENU = 0x00080000
|
||||
WS_THICKFRAME = 0x00040000
|
||||
WS_GROUP = 0x00020000
|
||||
WS_TABSTOP = 0x00010000
|
||||
|
||||
WS_MINIMIZEBOX = 0x00020000
|
||||
WS_MAXIMIZEBOX = 0x00010000
|
||||
|
||||
WS_OVERLAPPEDWINDOW = (WS_OVERLAPPED |
|
||||
WS_CAPTION |
|
||||
WS_SYSMENU |
|
||||
WS_THICKFRAME |
|
||||
WS_MINIMIZEBOX |
|
||||
WS_MAXIMIZEBOX)
|
||||
|
||||
SM_XVIRTUALSCREEN = 76
|
||||
SM_YVIRTUALSCREEN = 77
|
||||
SM_CXVIRTUALSCREEN = 78
|
||||
SM_CYVIRTUALSCREEN = 79
|
||||
SM_CMONITORS = 80
|
||||
SM_SAMEDISPLAYFORMAT = 81
|
||||
|
||||
WM_NULL = 0x0000
|
||||
WM_CREATE = 0x0001
|
||||
WM_DESTROY = 0x0002
|
||||
WM_MOVE = 0x0003
|
||||
WM_SIZE = 0x0005
|
||||
WM_ACTIVATE = 0x0006
|
||||
WM_SETFOCUS = 0x0007
|
||||
WM_KILLFOCUS = 0x0008
|
||||
WM_ENABLE = 0x000A
|
||||
WM_SETREDRAW = 0x000B
|
||||
WM_SETTEXT = 0x000C
|
||||
WM_GETTEXT = 0x000D
|
||||
WM_GETTEXTLENGTH = 0x000E
|
||||
WM_PAINT = 0x000F
|
||||
WM_CLOSE = 0x0010
|
||||
WM_QUERYENDSESSION = 0x0011
|
||||
WM_QUIT = 0x0012
|
||||
WM_QUERYOPEN = 0x0013
|
||||
WM_ERASEBKGND = 0x0014
|
||||
WM_SYSCOLORCHANGE = 0x0015
|
||||
WM_ENDSESSION = 0x0016
|
||||
WM_SHOWWINDOW = 0x0018
|
||||
WM_CTLCOLOR = 0x0019
|
||||
WM_WININICHANGE = 0x001A
|
||||
WM_SETTINGCHANGE = 0x001A
|
||||
WM_DEVMODECHANGE = 0x001B
|
||||
WM_ACTIVATEAPP = 0x001C
|
||||
WM_FONTCHANGE = 0x001D
|
||||
WM_TIMECHANGE = 0x001E
|
||||
WM_CANCELMODE = 0x001F
|
||||
WM_SETCURSOR = 0x0020
|
||||
WM_MOUSEACTIVATE = 0x0021
|
||||
WM_CHILDACTIVATE = 0x0022
|
||||
WM_QUEUESYNC = 0x0023
|
||||
WM_GETMINMAXINFO = 0x0024
|
||||
WM_PAINTICON = 0x0026
|
||||
WM_ICONERASEBKGND = 0x0027
|
||||
WM_NEXTDLGCTL = 0x0028
|
||||
WM_SPOOLERSTATUS = 0x002A
|
||||
WM_DRAWITEM = 0x002B
|
||||
WM_MEASUREITEM = 0x002C
|
||||
WM_DELETEITEM = 0x002D
|
||||
WM_VKEYTOITEM = 0x002E
|
||||
WM_CHARTOITEM = 0x002F
|
||||
WM_SETFONT = 0x0030
|
||||
WM_GETFONT = 0x0031
|
||||
WM_SETHOTKEY = 0x0032
|
||||
WM_GETHOTKEY = 0x0033
|
||||
WM_QUERYDRAGICON = 0x0037
|
||||
WM_COMPAREITEM = 0x0039
|
||||
WM_GETOBJECT = 0x003D
|
||||
WM_COMPACTING = 0x0041
|
||||
WM_COMMNOTIFY = 0x0044
|
||||
WM_WINDOWPOSCHANGING = 0x0046
|
||||
WM_WINDOWPOSCHANGED = 0x0047
|
||||
WM_POWER = 0x0048
|
||||
WM_COPYDATA = 0x004A
|
||||
WM_CANCELJOURNAL = 0x004B
|
||||
WM_NOTIFY = 0x004E
|
||||
WM_INPUTLANGCHANGEREQUEST = 0x0050
|
||||
WM_INPUTLANGCHANGE = 0x0051
|
||||
WM_TCARD = 0x0052
|
||||
WM_HELP = 0x0053
|
||||
WM_USERCHANGED = 0x0054
|
||||
WM_NOTIFYFORMAT = 0x0055
|
||||
WM_CONTEXTMENU = 0x007B
|
||||
WM_STYLECHANGING = 0x007C
|
||||
WM_STYLECHANGED = 0x007D
|
||||
WM_DISPLAYCHANGE = 0x007E
|
||||
WM_GETICON = 0x007F
|
||||
WM_SETICON = 0x0080
|
||||
WM_NCCREATE = 0x0081
|
||||
WM_NCDESTROY = 0x0082
|
||||
WM_NCCALCSIZE = 0x0083
|
||||
WM_NCHITTEST = 0x0084
|
||||
WM_NCPAINT = 0x0085
|
||||
WM_NCACTIVATE = 0x0086
|
||||
WM_GETDLGCODE = 0x0087
|
||||
WM_SYNCPAINT = 0x0088
|
||||
WM_NCMOUSEMOVE = 0x00A0
|
||||
WM_NCLBUTTONDOWN = 0x00A1
|
||||
WM_NCLBUTTONUP = 0x00A2
|
||||
WM_NCLBUTTONDBLCLK = 0x00A3
|
||||
WM_NCRBUTTONDOWN = 0x00A4
|
||||
WM_NCRBUTTONUP = 0x00A5
|
||||
WM_NCRBUTTONDBLCLK = 0x00A6
|
||||
WM_NCMBUTTONDOWN = 0x00A7
|
||||
WM_NCMBUTTONUP = 0x00A8
|
||||
WM_NCMBUTTONDBLCLK = 0x00A9
|
||||
WM_KEYDOWN = 0x0100
|
||||
WM_KEYUP = 0x0101
|
||||
WM_CHAR = 0x0102
|
||||
WM_DEADCHAR = 0x0103
|
||||
WM_SYSKEYDOWN = 0x0104
|
||||
WM_SYSKEYUP = 0x0105
|
||||
WM_SYSCHAR = 0x0106
|
||||
WM_SYSDEADCHAR = 0x0107
|
||||
WM_KEYLAST = 0x0108
|
||||
WM_IME_STARTCOMPOSITION = 0x010D
|
||||
WM_IME_ENDCOMPOSITION = 0x010E
|
||||
WM_IME_COMPOSITION = 0x010F
|
||||
WM_IME_KEYLAST = 0x010F
|
||||
WM_INITDIALOG = 0x0110
|
||||
WM_COMMAND = 0x0111
|
||||
WM_SYSCOMMAND = 0x0112
|
||||
WM_TIMER = 0x0113
|
||||
WM_HSCROLL = 0x0114
|
||||
WM_VSCROLL = 0x0115
|
||||
WM_INITMENU = 0x0116
|
||||
WM_INITMENUPOPUP = 0x0117
|
||||
WM_MENUSELECT = 0x011F
|
||||
WM_MENUCHAR = 0x0120
|
||||
WM_ENTERIDLE = 0x0121
|
||||
WM_MENURBUTTONUP = 0x0122
|
||||
WM_MENUDRAG = 0x0123
|
||||
WM_MENUGETOBJECT = 0x0124
|
||||
WM_UNINITMENUPOPUP = 0x0125
|
||||
WM_MENUCOMMAND = 0x0126
|
||||
WM_CTLCOLORMSGBOX = 0x0132
|
||||
WM_CTLCOLOREDIT = 0x0133
|
||||
WM_CTLCOLORLISTBOX = 0x0134
|
||||
WM_CTLCOLORBTN = 0x0135
|
||||
WM_CTLCOLORDLG = 0x0136
|
||||
WM_CTLCOLORSCROLLBAR = 0x0137
|
||||
WM_CTLCOLORSTATIC = 0x0138
|
||||
WM_MOUSEMOVE = 0x0200
|
||||
WM_LBUTTONDOWN = 0x0201
|
||||
WM_LBUTTONUP = 0x0202
|
||||
WM_LBUTTONDBLCLK = 0x0203
|
||||
WM_RBUTTONDOWN = 0x0204
|
||||
WM_RBUTTONUP = 0x0205
|
||||
WM_RBUTTONDBLCLK = 0x0206
|
||||
WM_MBUTTONDOWN = 0x0207
|
||||
WM_MBUTTONUP = 0x0208
|
||||
WM_MBUTTONDBLCLK = 0x0209
|
||||
WM_MOUSEWHEEL = 0x020A
|
||||
WM_PARENTNOTIFY = 0x0210
|
||||
WM_ENTERMENULOOP = 0x0211
|
||||
WM_EXITMENULOOP = 0x0212
|
||||
WM_NEXTMENU = 0x0213
|
||||
WM_SIZING = 0x0214
|
||||
WM_CAPTURECHANGED = 0x0215
|
||||
WM_MOVING = 0x0216
|
||||
WM_DEVICECHANGE = 0x0219
|
||||
WM_MDICREATE = 0x0220
|
||||
WM_MDIDESTROY = 0x0221
|
||||
WM_MDIACTIVATE = 0x0222
|
||||
WM_MDIRESTORE = 0x0223
|
||||
WM_MDINEXT = 0x0224
|
||||
WM_MDIMAXIMIZE = 0x0225
|
||||
WM_MDITILE = 0x0226
|
||||
WM_MDICASCADE = 0x0227
|
||||
WM_MDIICONARRANGE = 0x0228
|
||||
WM_MDIGETACTIVE = 0x0229
|
||||
WM_MDISETMENU = 0x0230
|
||||
WM_ENTERSIZEMOVE = 0x0231
|
||||
WM_EXITSIZEMOVE = 0x0232
|
||||
WM_DROPFILES = 0x0233
|
||||
WM_MDIREFRESHMENU = 0x0234
|
||||
WM_IME_SETCONTEXT = 0x0281
|
||||
WM_IME_NOTIFY = 0x0282
|
||||
WM_IME_CONTROL = 0x0283
|
||||
WM_IME_COMPOSITIONFULL = 0x0284
|
||||
WM_IME_SELECT = 0x0285
|
||||
WM_IME_CHAR = 0x0286
|
||||
WM_IME_REQUEST = 0x0288
|
||||
WM_IME_KEYDOWN = 0x0290
|
||||
WM_IME_KEYUP = 0x0291
|
||||
WM_MOUSEHOVER = 0x02A1
|
||||
WM_MOUSELEAVE = 0x02A3
|
||||
WM_CUT = 0x0300
|
||||
WM_COPY = 0x0301
|
||||
WM_PASTE = 0x0302
|
||||
WM_CLEAR = 0x0303
|
||||
WM_UNDO = 0x0304
|
||||
WM_RENDERFORMAT = 0x0305
|
||||
WM_RENDERALLFORMATS = 0x0306
|
||||
WM_DESTROYCLIPBOARD = 0x0307
|
||||
WM_DRAWCLIPBOARD = 0x0308
|
||||
WM_PAINTCLIPBOARD = 0x0309
|
||||
WM_VSCROLLCLIPBOARD = 0x030A
|
||||
WM_SIZECLIPBOARD = 0x030B
|
||||
WM_ASKCBFORMATNAME = 0x030C
|
||||
WM_CHANGECBCHAIN = 0x030D
|
||||
WM_HSCROLLCLIPBOARD = 0x030E
|
||||
WM_QUERYNEWPALETTE = 0x030F
|
||||
WM_PALETTEISCHANGING = 0x0310
|
||||
WM_PALETTECHANGED = 0x0311
|
||||
WM_HOTKEY = 0x0312
|
||||
WM_PRINT = 0x0317
|
||||
WM_PRINTCLIENT = 0x0318
|
||||
WM_HANDHELDFIRST = 0x0358
|
||||
WM_HANDHELDLAST = 0x035F
|
||||
WM_AFXFIRST = 0x0360
|
||||
WM_AFXLAST = 0x037F
|
||||
WM_PENWINFIRST = 0x0380
|
||||
WM_PENWINLAST = 0x038F
|
||||
WM_APP = 0x8000
|
||||
WM_USER = 0x0400
|
||||
WM_REFLECT = WM_USER + 0x1c00
|
||||
|
||||
class WNDCLASSEX(ctypes.Structure):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(WNDCLASSEX, self).__init__(*args, **kwargs)
|
||||
self.cbSize = ctypes.sizeof(self)
|
||||
_fields_ = [("cbSize", ctypes.c_uint),
|
||||
("style", ctypes.c_uint),
|
||||
("lpfnWndProc", WNDPROC),
|
||||
("cbClsExtra", ctypes.c_int),
|
||||
("cbWndExtra", ctypes.c_int),
|
||||
("hInstance", ctypes.wintypes.HANDLE),
|
||||
("hIcon", ctypes.wintypes.HANDLE),
|
||||
("hCursor", ctypes.wintypes.HANDLE),
|
||||
("hBrush", ctypes.wintypes.HANDLE),
|
||||
("lpszMenuName", ctypes.wintypes.LPCWSTR),
|
||||
("lpszClassName", ctypes.wintypes.LPCWSTR),
|
||||
("hIconSm", ctypes.wintypes.HANDLE)]
|
||||
|
||||
ShowWindow = ctypes.windll.user32.ShowWindow
|
||||
ShowWindow.argtypes = [ctypes.wintypes.HWND, ctypes.c_int]
|
||||
|
||||
def GenerateDummyWindow(callback, uid):
|
||||
newclass = WNDCLASSEX()
|
||||
newclass.lpfnWndProc = callback
|
||||
newclass.lpszClassName = uid.replace("-", "")
|
||||
ATOM = ctypes.windll.user32.RegisterClassExW(ctypes.byref(newclass))
|
||||
hwnd = ctypes.windll.user32.CreateWindowExW(0, newclass.lpszClassName, None, WS_POPUP, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
return hwnd
|
||||
|
||||
# Message loop calls
|
||||
|
||||
TIMERCALLBACK = ctypes.WINFUNCTYPE(None,
|
||||
ctypes.wintypes.HWND,
|
||||
ctypes.wintypes.UINT,
|
||||
ctypes.POINTER(ctypes.wintypes.UINT),
|
||||
ctypes.wintypes.DWORD)
|
||||
|
||||
SetTimer = ctypes.windll.user32.SetTimer
|
||||
SetTimer.restype = ctypes.POINTER(ctypes.wintypes.UINT)
|
||||
SetTimer.argtypes = [ctypes.wintypes.HWND,
|
||||
ctypes.POINTER(ctypes.wintypes.UINT),
|
||||
ctypes.wintypes.UINT,
|
||||
TIMERCALLBACK]
|
||||
|
||||
KillTimer = ctypes.windll.user32.KillTimer
|
||||
KillTimer.restype = ctypes.wintypes.BOOL
|
||||
KillTimer.argtypes = [ctypes.wintypes.HWND,
|
||||
ctypes.POINTER(ctypes.wintypes.UINT)]
|
||||
|
||||
class MSG(ctypes.Structure):
|
||||
_fields_ = [ ('HWND', ctypes.wintypes.HWND),
|
||||
('message', ctypes.wintypes.UINT),
|
||||
('wParam', ctypes.wintypes.WPARAM),
|
||||
('lParam', ctypes.wintypes.LPARAM),
|
||||
('time', ctypes.wintypes.DWORD),
|
||||
('pt', POINT)]
|
||||
|
||||
GetMessage = ctypes.windll.user32.GetMessageW
|
||||
GetMessage.restype = ctypes.wintypes.BOOL
|
||||
GetMessage.argtypes = [ctypes.POINTER(MSG), ctypes.wintypes.HWND, ctypes.wintypes.UINT, ctypes.wintypes.UINT]
|
||||
|
||||
TranslateMessage = ctypes.windll.user32.TranslateMessage
|
||||
TranslateMessage.restype = ctypes.wintypes.ULONG
|
||||
TranslateMessage.argtypes = [ctypes.POINTER(MSG)]
|
||||
|
||||
DispatchMessage = ctypes.windll.user32.DispatchMessageW
|
||||
DispatchMessage.restype = ctypes.wintypes.ULONG
|
||||
DispatchMessage.argtypes = [ctypes.POINTER(MSG)]
|
||||
|
||||
def LoadIcon(iconfilename, small=False):
|
||||
return LoadImage(0,
|
||||
str(iconfilename),
|
||||
IMAGE_ICON,
|
||||
16 if small else 0,
|
||||
16 if small else 0,
|
||||
LR_LOADFROMFILE)
|
||||
|
||||
|
||||
class NotificationIcon(object):
|
||||
def __init__(self, iconfilename, tooltip=None):
|
||||
assert os.path.isfile(str(iconfilename)), "{} doesn't exist".format(iconfilename)
|
||||
self._iconfile = str(iconfilename)
|
||||
self._hicon = LoadIcon(self._iconfile, True)
|
||||
assert self._hicon, "Failed to load {}".format(iconfilename)
|
||||
#self._pumpqueue = Queue.Queue()
|
||||
self._die = False
|
||||
self._timerid = None
|
||||
self._uid = uuid.uuid4()
|
||||
self._tooltip = str(tooltip) if tooltip else ''
|
||||
#self._thread = threading.Thread(target=self._run)
|
||||
#self._thread.start()
|
||||
self._info_bubble = None
|
||||
self.items = []
|
||||
|
||||
|
||||
def _bubble(self, iconinfo):
|
||||
if self._info_bubble:
|
||||
info_bubble = self._info_bubble
|
||||
self._info_bubble = None
|
||||
message = str(self._info_bubble)
|
||||
iconinfo.uFlags |= NIF_INFO
|
||||
iconinfo.szInfo = message
|
||||
iconinfo.szInfoTitle = message
|
||||
iconinfo.dwInfoFlags = NIIF_INFO
|
||||
iconinfo.union.uTimeout = 10000
|
||||
Shell_NotifyIcon(NIM_MODIFY, ctypes.pointer(iconinfo))
|
||||
|
||||
|
||||
def _run(self):
|
||||
self.WM_TASKBARCREATED = ctypes.windll.user32.RegisterWindowMessageW('TaskbarCreated')
|
||||
|
||||
self._windowproc = WNDPROC(self._callback)
|
||||
self._hwnd = GenerateDummyWindow(self._windowproc, str(self._uid))
|
||||
|
||||
iconinfo = NOTIFYICONDATA()
|
||||
iconinfo.hWnd = self._hwnd
|
||||
iconinfo.uID = 100
|
||||
iconinfo.uFlags = NIF_ICON | NIF_SHOWTIP | NIF_MESSAGE | (NIF_TIP if self._tooltip else 0)
|
||||
iconinfo.uCallbackMessage = WM_MENUCOMMAND
|
||||
iconinfo.hIcon = self._hicon
|
||||
iconinfo.szTip = self._tooltip
|
||||
|
||||
Shell_NotifyIcon(NIM_ADD, ctypes.pointer(iconinfo))
|
||||
|
||||
self.iconinfo = iconinfo
|
||||
|
||||
PostMessage(self._hwnd, WM_NULL, 0, 0)
|
||||
|
||||
message = MSG()
|
||||
last_time = -1
|
||||
ret = None
|
||||
while not self._die:
|
||||
try:
|
||||
ret = GetMessage(ctypes.pointer(message), 0, 0, 0)
|
||||
TranslateMessage(ctypes.pointer(message))
|
||||
DispatchMessage(ctypes.pointer(message))
|
||||
except Exception as err:
|
||||
# print "NotificationIcon error", err, message
|
||||
message = MSG()
|
||||
time.sleep(0.125)
|
||||
print("Icon thread stopped, removing icon (hicon: %s, hwnd: %s)..." % (self._hicon, self._hwnd))
|
||||
|
||||
Shell_NotifyIcon(NIM_DELETE, ctypes.cast(ctypes.pointer(iconinfo), ctypes.POINTER(NOTIFYICONDATA)))
|
||||
ctypes.windll.user32.DestroyWindow(self._hwnd)
|
||||
ctypes.windll.user32.DestroyIcon.argtypes = [ctypes.wintypes.HICON]
|
||||
ctypes.windll.user32.DestroyIcon(self._hicon)
|
||||
|
||||
|
||||
def _menu(self):
|
||||
if not hasattr(self, 'items'):
|
||||
return
|
||||
|
||||
menu = CreatePopupMenu()
|
||||
func = None
|
||||
|
||||
try:
|
||||
iidx = 1000
|
||||
defaultitem = -1
|
||||
item_map = {}
|
||||
for fs in self.items:
|
||||
iidx += 1
|
||||
if isinstance(fs, str):
|
||||
if fs and not fs.strip('-_='):
|
||||
AppendMenu(menu, MF_SEPARATOR, iidx, fs)
|
||||
else:
|
||||
AppendMenu(menu, MF_STRING | MF_GRAYED, iidx, fs)
|
||||
elif isinstance(fs, tuple):
|
||||
if callable(fs[0]):
|
||||
itemstring = fs[0]()
|
||||
else:
|
||||
itemstring = str(fs[0])
|
||||
flags = MF_STRING
|
||||
if itemstring.startswith("!"):
|
||||
itemstring = itemstring[1:]
|
||||
defaultitem = iidx
|
||||
if itemstring.startswith("+"):
|
||||
itemstring = itemstring[1:]
|
||||
flags = flags | MF_CHECKED
|
||||
itemcallable = fs[1]
|
||||
item_map[iidx] = itemcallable
|
||||
if itemcallable is False:
|
||||
flags = flags | MF_DISABLED
|
||||
elif not callable(itemcallable):
|
||||
flags = flags | MF_GRAYED
|
||||
AppendMenu(menu, flags, iidx, itemstring)
|
||||
|
||||
if defaultitem != -1:
|
||||
SetMenuDefaultItem(menu, defaultitem, 0)
|
||||
|
||||
pos = POINT()
|
||||
GetCursorPos(ctypes.pointer(pos))
|
||||
|
||||
PostMessage(self._hwnd, WM_NULL, 0, 0)
|
||||
|
||||
SetForegroundWindow(self._hwnd)
|
||||
|
||||
ti = TrackPopupMenu(menu, TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_NONOTIFY, pos.x, pos.y, 0, self._hwnd, None)
|
||||
|
||||
if ti in item_map:
|
||||
func = item_map[ti]
|
||||
|
||||
PostMessage(self._hwnd, WM_NULL, 0, 0)
|
||||
finally:
|
||||
DestroyMenu(menu)
|
||||
if func: func()
|
||||
|
||||
|
||||
def clicked(self):
|
||||
self._menu()
|
||||
|
||||
|
||||
|
||||
def _callback(self, hWnd, msg, wParam, lParam):
|
||||
# Check if the main thread is still alive
|
||||
if msg == WM_TIMER:
|
||||
if not any(thread.getName() == 'MainThread' and thread.isAlive()
|
||||
for thread in threading.enumerate()):
|
||||
self._die = True
|
||||
elif msg == WM_MENUCOMMAND and lParam == WM_LBUTTONUP:
|
||||
self.clicked()
|
||||
elif msg == WM_MENUCOMMAND and lParam == WM_RBUTTONUP:
|
||||
self._menu()
|
||||
elif msg == self.WM_TASKBARCREATED: # Explorer restarted, add the icon again.
|
||||
Shell_NotifyIcon(NIM_ADD, ctypes.pointer(self.iconinfo))
|
||||
else:
|
||||
return DefWindowProc(hWnd, msg, wParam, lParam)
|
||||
return 1
|
||||
|
||||
|
||||
def die(self):
|
||||
self._die = True
|
||||
PostMessage(self._hwnd, WM_NULL, 0, 0)
|
||||
time.sleep(0.2)
|
||||
try:
|
||||
Shell_NotifyIcon(NIM_DELETE, self.iconinfo)
|
||||
except Exception as err:
|
||||
print("Icon remove error", err)
|
||||
ctypes.windll.user32.DestroyWindow(self._hwnd)
|
||||
ctypes.windll.user32.DestroyIcon(self._hicon)
|
||||
|
||||
|
||||
def pump(self):
|
||||
try:
|
||||
while not self._pumpqueue.empty():
|
||||
callable = self._pumpqueue.get(False)
|
||||
callable()
|
||||
except queue_Empty:
|
||||
pass
|
||||
|
||||
|
||||
def announce(self, text):
|
||||
self._info_bubble = text
|
||||
|
||||
|
||||
def hideConsole():
|
||||
ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0)
|
||||
|
||||
def showConsole():
|
||||
ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 1)
|
||||
|
||||
def hasConsole():
|
||||
return ctypes.windll.kernel32.GetConsoleWindow() != 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
import time
|
||||
|
||||
def greet():
|
||||
ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0)
|
||||
print("Hello")
|
||||
|
||||
def quit():
|
||||
ni._die = True
|
||||
|
||||
def announce():
|
||||
ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 1)
|
||||
ni.announce("Hello there")
|
||||
|
||||
def clicked():
|
||||
ni.announce("Hello")
|
||||
|
||||
def dynamicTitle():
|
||||
return "!The time is: %s" % time.time()
|
||||
|
||||
ni = NotificationIcon(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../trayicon.ico'), "ZeroNet 0.2.9")
|
||||
ni.items = [
|
||||
(dynamicTitle, False),
|
||||
('Hello', greet),
|
||||
('Title', False),
|
||||
('!Default', greet),
|
||||
('+Popup bubble', announce),
|
||||
'Nothing',
|
||||
'--',
|
||||
('Quit', quit)
|
||||
]
|
||||
ni.clicked = clicked
|
||||
import atexit
|
||||
|
||||
@atexit.register
|
||||
def goodbye():
|
||||
print("You are now leaving the Python sector.")
|
||||
|
||||
ni._run()
|
|
@ -0,0 +1,54 @@
|
|||
''' Get windows special folders without pythonwin
|
||||
Example:
|
||||
import specialfolders
|
||||
start_programs = specialfolders.get(specialfolders.PROGRAMS)
|
||||
|
||||
Code is public domain, do with it what you will.
|
||||
|
||||
Luke Pinner - Environment.gov.au, 2010 February 10
|
||||
'''
|
||||
|
||||
#Imports use _syntax to mask them from autocomplete IDE's
|
||||
import ctypes as _ctypes
|
||||
from ctypes import create_unicode_buffer as _cub
|
||||
from ctypes.wintypes import HWND as _HWND, HANDLE as _HANDLE,DWORD as _DWORD,LPCWSTR as _LPCWSTR,MAX_PATH as _MAX_PATH
|
||||
_SHGetFolderPath = _ctypes.windll.shell32.SHGetFolderPathW
|
||||
|
||||
#public special folder constants
|
||||
DESKTOP= 0
|
||||
PROGRAMS= 2
|
||||
MYDOCUMENTS= 5
|
||||
FAVORITES= 6
|
||||
STARTUP= 7
|
||||
RECENT= 8
|
||||
SENDTO= 9
|
||||
STARTMENU= 11
|
||||
MYMUSIC= 13
|
||||
MYVIDEOS= 14
|
||||
NETHOOD= 19
|
||||
FONTS= 20
|
||||
TEMPLATES= 21
|
||||
ALLUSERSSTARTMENU= 22
|
||||
ALLUSERSPROGRAMS= 23
|
||||
ALLUSERSSTARTUP= 24
|
||||
ALLUSERSDESKTOP= 25
|
||||
APPLICATIONDATA= 26
|
||||
PRINTHOOD= 27
|
||||
LOCALSETTINGSAPPLICATIONDATA= 28
|
||||
ALLUSERSFAVORITES= 31
|
||||
LOCALSETTINGSTEMPORARYINTERNETFILES=32
|
||||
COOKIES= 33
|
||||
LOCALSETTINGSHISTORY= 34
|
||||
ALLUSERSAPPLICATIONDATA= 35
|
||||
|
||||
def get(intFolder):
|
||||
_SHGetFolderPath.argtypes = [_HWND, _ctypes.c_int, _HANDLE, _DWORD, _LPCWSTR]
|
||||
auPathBuffer = _cub(_MAX_PATH)
|
||||
exit_code=_SHGetFolderPath(0, intFolder, 0, 0, auPathBuffer)
|
||||
return auPathBuffer.value
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
print(get(STARTUP))
|
||||
open(get(STARTUP)+"\\zeronet.cmd", "w").write("cd /D %s\r\nzeronet.py" % os.getcwd())
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Trayicon",
|
||||
"description": "Icon for system tray. (Windows only)",
|
||||
"default": "enabled"
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,221 @@
|
|||
import io
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from Plugin import PluginManager
|
||||
from Config import config
|
||||
from Debug import Debug
|
||||
from Translate import Translate
|
||||
from util.Flag import flag
|
||||
|
||||
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
|
||||
if "_" not in locals():
|
||||
_ = Translate(plugin_dir + "/languages/")
|
||||
|
||||
|
||||
# Convert non-str,int,float values to str in a dict
|
||||
def restrictDictValues(input_dict):
|
||||
allowed_types = (int, str, float)
|
||||
return {
|
||||
key: val if type(val) in allowed_types else str(val)
|
||||
for key, val in input_dict.items()
|
||||
}
|
||||
|
||||
|
||||
@PluginManager.registerTo("UiRequest")
|
||||
class UiRequestPlugin(object):
|
||||
def actionWrapper(self, path, extra_headers=None):
|
||||
if path.strip("/") != "Plugins":
|
||||
return super(UiRequestPlugin, self).actionWrapper(path, extra_headers)
|
||||
|
||||
if not extra_headers:
|
||||
extra_headers = {}
|
||||
|
||||
script_nonce = self.getScriptNonce()
|
||||
|
||||
self.sendHeader(extra_headers=extra_headers, script_nonce=script_nonce)
|
||||
site = self.server.site_manager.get(config.homepage)
|
||||
return iter([super(UiRequestPlugin, self).renderWrapper(
|
||||
site, path, "uimedia/plugins/plugin_manager/plugin_manager.html",
|
||||
"Plugin Manager", extra_headers, show_loadingscreen=False, script_nonce=script_nonce
|
||||
)])
|
||||
|
||||
def actionUiMedia(self, path, *args, **kwargs):
|
||||
if path.startswith("/uimedia/plugins/plugin_manager/"):
|
||||
file_path = path.replace("/uimedia/plugins/plugin_manager/", plugin_dir + "/media/")
|
||||
if config.debug and (file_path.endswith("all.js") or file_path.endswith("all.css")):
|
||||
# If debugging merge *.css to all.css and *.js to all.js
|
||||
from Debug import DebugMedia
|
||||
DebugMedia.merge(file_path)
|
||||
|
||||
if file_path.endswith("js"):
|
||||
data = _.translateData(open(file_path).read(), mode="js").encode("utf8")
|
||||
elif file_path.endswith("html"):
|
||||
data = _.translateData(open(file_path).read(), mode="html").encode("utf8")
|
||||
else:
|
||||
data = open(file_path, "rb").read()
|
||||
|
||||
return self.actionFile(file_path, file_obj=io.BytesIO(data), file_size=len(data))
|
||||
else:
|
||||
return super(UiRequestPlugin, self).actionUiMedia(path)
|
||||
|
||||
|
||||
@PluginManager.registerTo("UiWebsocket")
|
||||
class UiWebsocketPlugin(object):
|
||||
@flag.admin
|
||||
def actionPluginList(self, to):
|
||||
plugins = []
|
||||
for plugin in PluginManager.plugin_manager.listPlugins(list_disabled=True):
|
||||
plugin_info_path = plugin["dir_path"] + "/plugin_info.json"
|
||||
plugin_info = {}
|
||||
if os.path.isfile(plugin_info_path):
|
||||
try:
|
||||
plugin_info = json.load(open(plugin_info_path))
|
||||
except Exception as err:
|
||||
self.log.error(
|
||||
"Error loading plugin info for %s: %s" %
|
||||
(plugin["name"], Debug.formatException(err))
|
||||
)
|
||||
if plugin_info:
|
||||
plugin_info = restrictDictValues(plugin_info) # For security reasons don't allow complex values
|
||||
plugin["info"] = plugin_info
|
||||
|
||||
if plugin["source"] != "builtin":
|
||||
plugin_site = self.server.sites.get(plugin["source"])
|
||||
if plugin_site:
|
||||
try:
|
||||
plugin_site_info = plugin_site.storage.loadJson(plugin["inner_path"] + "/plugin_info.json")
|
||||
plugin_site_info = restrictDictValues(plugin_site_info)
|
||||
plugin["site_info"] = plugin_site_info
|
||||
plugin["site_title"] = plugin_site.content_manager.contents["content.json"].get("title")
|
||||
plugin_key = "%s/%s" % (plugin["source"], plugin["inner_path"])
|
||||
plugin["updated"] = plugin_key in PluginManager.plugin_manager.plugins_updated
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
plugins.append(plugin)
|
||||
|
||||
return {"plugins": plugins}
|
||||
|
||||
@flag.admin
|
||||
@flag.no_multiuser
|
||||
def actionPluginConfigSet(self, to, source, inner_path, key, value):
|
||||
plugin_manager = PluginManager.plugin_manager
|
||||
plugins = plugin_manager.listPlugins(list_disabled=True)
|
||||
plugin = None
|
||||
for item in plugins:
|
||||
if item["source"] == source and item["inner_path"] in (inner_path, "disabled-" + inner_path):
|
||||
plugin = item
|
||||
break
|
||||
|
||||
if not plugin:
|
||||
return {"error": "Plugin not found"}
|
||||
|
||||
config_source = plugin_manager.config.setdefault(source, {})
|
||||
config_plugin = config_source.setdefault(inner_path, {})
|
||||
|
||||
if key in config_plugin and value is None:
|
||||
del config_plugin[key]
|
||||
else:
|
||||
config_plugin[key] = value
|
||||
|
||||
plugin_manager.saveConfig()
|
||||
|
||||
return "ok"
|
||||
|
||||
def pluginAction(self, action, address, inner_path):
|
||||
site = self.server.sites.get(address)
|
||||
plugin_manager = PluginManager.plugin_manager
|
||||
|
||||
# Install/update path should exists
|
||||
if action in ("add", "update", "add_request"):
|
||||
if not site:
|
||||
raise Exception("Site not found")
|
||||
|
||||
if not site.storage.isDir(inner_path):
|
||||
raise Exception("Directory not found on the site")
|
||||
|
||||
try:
|
||||
plugin_info = site.storage.loadJson(inner_path + "/plugin_info.json")
|
||||
plugin_data = (plugin_info["rev"], plugin_info["description"], plugin_info["name"])
|
||||
except Exception as err:
|
||||
raise Exception("Invalid plugin_info.json: %s" % Debug.formatExceptionMessage(err))
|
||||
|
||||
source_path = site.storage.getPath(inner_path)
|
||||
|
||||
target_path = plugin_manager.path_installed_plugins + "/" + address + "/" + inner_path
|
||||
plugin_config = plugin_manager.config.setdefault(site.address, {}).setdefault(inner_path, {})
|
||||
|
||||
# Make sure plugin (not)installed
|
||||
if action in ("add", "add_request") and os.path.isdir(target_path):
|
||||
raise Exception("Plugin already installed")
|
||||
|
||||
if action in ("update", "remove") and not os.path.isdir(target_path):
|
||||
raise Exception("Plugin not installed")
|
||||
|
||||
# Do actions
|
||||
if action == "add":
|
||||
shutil.copytree(source_path, target_path)
|
||||
|
||||
plugin_config["date_added"] = int(time.time())
|
||||
plugin_config["rev"] = plugin_info["rev"]
|
||||
plugin_config["enabled"] = True
|
||||
|
||||
if action == "update":
|
||||
shutil.rmtree(target_path)
|
||||
|
||||
shutil.copytree(source_path, target_path)
|
||||
|
||||
plugin_config["rev"] = plugin_info["rev"]
|
||||
plugin_config["date_updated"] = time.time()
|
||||
|
||||
if action == "remove":
|
||||
del plugin_manager.config[address][inner_path]
|
||||
shutil.rmtree(target_path)
|
||||
|
||||
def doPluginAdd(self, to, inner_path, res):
|
||||
if not res:
|
||||
return None
|
||||
|
||||
self.pluginAction("add", self.site.address, inner_path)
|
||||
PluginManager.plugin_manager.saveConfig()
|
||||
|
||||
self.cmd(
|
||||
"confirm",
|
||||
["Plugin installed!<br>You have to restart the client to load the plugin", "Restart"],
|
||||
lambda res: self.actionServerShutdown(to, restart=True)
|
||||
)
|
||||
|
||||
self.response(to, "ok")
|
||||
|
||||
@flag.no_multiuser
|
||||
def actionPluginAddRequest(self, to, inner_path):
|
||||
self.pluginAction("add_request", self.site.address, inner_path)
|
||||
plugin_info = self.site.storage.loadJson(inner_path + "/plugin_info.json")
|
||||
warning = "<b>Warning!<br/>Plugins has the same permissions as the ZeroNet client.<br/>"
|
||||
warning += "Do not install it if you don't trust the developer.</b>"
|
||||
|
||||
self.cmd(
|
||||
"confirm",
|
||||
["Install new plugin: %s?<br>%s" % (plugin_info["name"], warning), "Trust & Install"],
|
||||
lambda res: self.doPluginAdd(to, inner_path, res)
|
||||
)
|
||||
|
||||
@flag.admin
|
||||
@flag.no_multiuser
|
||||
def actionPluginRemove(self, to, address, inner_path):
|
||||
self.pluginAction("remove", address, inner_path)
|
||||
PluginManager.plugin_manager.saveConfig()
|
||||
return "ok"
|
||||
|
||||
@flag.admin
|
||||
@flag.no_multiuser
|
||||
def actionPluginUpdate(self, to, address, inner_path):
|
||||
self.pluginAction("update", address, inner_path)
|
||||
PluginManager.plugin_manager.saveConfig()
|
||||
PluginManager.plugin_manager.plugins_updated["%s/%s" % (address, inner_path)] = True
|
||||
return "ok"
|
|
@ -0,0 +1 @@
|
|||
from . import UiPluginManagerPlugin
|
|
@ -0,0 +1,75 @@
|
|||
body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; backface-visibility: hidden; }
|
||||
h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px }
|
||||
h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; }
|
||||
h2 { margin-top: 10px; }
|
||||
h3 { font-weight: normal }
|
||||
h4 { font-size: 19px; font-weight: lighter; margin-right: 100px; margin-top: 30px; }
|
||||
a { color: #9760F9 }
|
||||
a:hover { text-decoration: none }
|
||||
|
||||
.link { background-color: transparent; outline: 5px solid transparent; transition: all 0.3s }
|
||||
.link:active { background-color: #EFEFEF; outline: 5px solid #EFEFEF; transition: none }
|
||||
|
||||
.content { max-width: 800px; margin: auto; background-color: white; padding: 60px 20px; box-sizing: border-box; padding-bottom: 150px; }
|
||||
.section { margin: 0px 10%; }
|
||||
.plugins { font-size: 19px; margin-top: 25px; margin-bottom: 75px; }
|
||||
.plugin { transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: relative; padding-bottom: 20px; padding-top: 10px; }
|
||||
.plugin.hidden { opacity: 0; height: 0px; padding: 0px; }
|
||||
.plugin .title { display: inline-block; line-height: 36px; }
|
||||
.plugin .title h3 { font-size: 20px; font-weight: lighter; margin-right: 100px; }
|
||||
.plugin .title .version { font-size: 70%; margin-left: 5px; }
|
||||
.plugin .title .version .version-latest { color: #2ecc71; font-weight: normal; }
|
||||
.plugin .title .version .version-missing { color: #ffa200; font-weight: normal; }
|
||||
.plugin .title .version .version-update { padding: 0px 15px; margin-left: 5px; line-height: 28px; }
|
||||
.plugin .description { font-size: 14px; color: #666; line-height: 24px; }
|
||||
.plugin .description .source { color: #999; font-size: 90%; }
|
||||
.plugin .description .source a { color: #666; }
|
||||
.plugin .value { display: inline-block; white-space: nowrap; }
|
||||
.plugin .value-right { right: 0px; position: absolute; }
|
||||
.plugin .value-fullwidth { width: 100% }
|
||||
.plugin .marker {
|
||||
font-weight: bold; text-decoration: none; font-size: 25px; position: absolute; padding: 2px 15px; line-height: 32px;
|
||||
opacity: 0; pointer-events: none; transition: all 0.6s; transform: scale(2); color: #9760F9;
|
||||
}
|
||||
.plugin .marker.visible { opacity: 1; pointer-events: all; transform: scale(1); }
|
||||
.plugin .marker.changed { color: #2ecc71; }
|
||||
.plugin .marker.pending { color: #ffa200; }
|
||||
|
||||
|
||||
.input-text, .input-select { padding: 8px 18px; border: 1px solid #CCC; border-radius: 3px; font-size: 17px; box-sizing: border-box; }
|
||||
.input-text:focus, .input-select:focus { border: 1px solid #3396ff; outline: none; }
|
||||
.input-textarea { overflow-x: auto; overflow-y: hidden; white-space: pre; line-height: 22px; }
|
||||
|
||||
.input-select { width: initial; font-size: 14px; padding-right: 10px; padding-left: 10px; }
|
||||
|
||||
.value-right .input-text { text-align: right; width: 100px; }
|
||||
.value-fullwidth .input-text { width: 100%; font-size: 14px; font-family: 'Segoe UI', Arial, 'Helvetica Neue'; }
|
||||
.value-fullwidth { margin-top: 10px; }
|
||||
|
||||
/* Checkbox */
|
||||
.checkbox-skin { background-color: #CCC; width: 50px; height: 24px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; }
|
||||
.checkbox-skin:before {
|
||||
content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px;
|
||||
transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86);
|
||||
}
|
||||
.checkbox { font-size: 14px; font-weight: normal; display: inline-block; cursor: pointer; margin-top: 5px; }
|
||||
.checkbox .title { display: inline; line-height: 30px; vertical-align: 4px; margin-left: 11px }
|
||||
.checkbox.checked .checkbox-skin:before { margin-left: 27px; }
|
||||
.checkbox.checked .checkbox-skin { background-color: #2ECC71 }
|
||||
|
||||
/* Bottom */
|
||||
|
||||
.bottom {
|
||||
width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
|
||||
transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: fixed; backface-visibility: hidden; box-sizing: border-box;
|
||||
}
|
||||
.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; }
|
||||
.bottom .button { float: right; }
|
||||
.bottom.visible { bottom: 0px; box-shadow: 0px 0px 35px #dcdcdc; }
|
||||
.bottom .title { padding: 10px 10px; color: #363636; float: left; text-transform: uppercase; letter-spacing: 1px; }
|
||||
.bottom .title:before { content: "•"; display: inline-block; color: #2ecc71; font-size: 31px; vertical-align: -7px; margin-right: 8px; line-height: 25px; }
|
||||
.bottom-restart .title:before { color: #ffa200; }
|
||||
|
||||
.animate { transition: all 0.3s ease-out !important; }
|
||||
.animate-back { transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; }
|
||||
.animate-inout { transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; }
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,12 @@
|
|||
/* Button */
|
||||
.button {
|
||||
background-color: #FFDC00; color: black; padding: 10px 20px; display: inline-block; background-position: left center;
|
||||
border-radius: 2px; border-bottom: 2px solid #E8BE29; transition: all 0.5s ease-out; text-decoration: none;
|
||||
}
|
||||
.button:hover { border-color: white; border-bottom: 2px solid #BD960C; transition: none ; background-color: #FDEB07 }
|
||||
.button:active { position: relative; top: 1px }
|
||||
.button.loading {
|
||||
color: rgba(0,0,0,0); background: #999 url(../img/loading.gif) no-repeat center center;
|
||||
transition: all 0.5s ease-out ; pointer-events: none; border-bottom: 2px solid #666
|
||||
}
|
||||
.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 }
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 723 B |
|
@ -0,0 +1,132 @@
|
|||
class PluginList extends Class
|
||||
constructor: (plugins) ->
|
||||
@plugins = plugins
|
||||
|
||||
savePluginStatus: (plugin, is_enabled) =>
|
||||
Page.cmd "pluginConfigSet", [plugin.source, plugin.inner_path, "enabled", is_enabled], (res) =>
|
||||
if res == "ok"
|
||||
Page.updatePlugins()
|
||||
else
|
||||
Page.cmd "wrapperNotification", ["error", res.error]
|
||||
|
||||
Page.projector.scheduleRender()
|
||||
|
||||
handleCheckboxChange: (e) =>
|
||||
node = e.currentTarget
|
||||
plugin = node["data-plugin"]
|
||||
node.classList.toggle("checked")
|
||||
value = node.classList.contains("checked")
|
||||
|
||||
@savePluginStatus(plugin, value)
|
||||
|
||||
handleResetClick: (e) =>
|
||||
node = e.currentTarget
|
||||
plugin = node["data-plugin"]
|
||||
|
||||
@savePluginStatus(plugin, null)
|
||||
|
||||
handleUpdateClick: (e) =>
|
||||
node = e.currentTarget
|
||||
plugin = node["data-plugin"]
|
||||
node.classList.add("loading")
|
||||
|
||||
Page.cmd "pluginUpdate", [plugin.source, plugin.inner_path], (res) =>
|
||||
if res == "ok"
|
||||
Page.cmd "wrapperNotification", ["done", "Plugin #{plugin.name} updated to latest version"]
|
||||
Page.updatePlugins()
|
||||
else
|
||||
Page.cmd "wrapperNotification", ["error", res.error]
|
||||
node.classList.remove("loading")
|
||||
|
||||
return false
|
||||
|
||||
handleDeleteClick: (e) =>
|
||||
node = e.currentTarget
|
||||
plugin = node["data-plugin"]
|
||||
if plugin.loaded
|
||||
Page.cmd "wrapperNotification", ["info", "You can only delete plugin that are not currently active"]
|
||||
return false
|
||||
|
||||
node.classList.add("loading")
|
||||
|
||||
Page.cmd "wrapperConfirm", ["Delete #{plugin.name} plugin?", "Delete"], (res) =>
|
||||
if not res
|
||||
node.classList.remove("loading")
|
||||
return false
|
||||
|
||||
Page.cmd "pluginRemove", [plugin.source, plugin.inner_path], (res) =>
|
||||
if res == "ok"
|
||||
Page.cmd "wrapperNotification", ["done", "Plugin #{plugin.name} deleted"]
|
||||
Page.updatePlugins()
|
||||
else
|
||||
Page.cmd "wrapperNotification", ["error", res.error]
|
||||
node.classList.remove("loading")
|
||||
|
||||
return false
|
||||
|
||||
render: ->
|
||||
h("div.plugins", @plugins.map (plugin) =>
|
||||
if not plugin.info
|
||||
return
|
||||
descr = plugin.info.description
|
||||
plugin.info.default ?= "enabled"
|
||||
if plugin.info.default
|
||||
descr += " (default: #{plugin.info.default})"
|
||||
|
||||
tag_version = ""
|
||||
tag_source = ""
|
||||
tag_delete = ""
|
||||
if plugin.source != "builtin"
|
||||
tag_update = ""
|
||||
if plugin.site_info?.rev
|
||||
if plugin.site_info.rev > plugin.info.rev
|
||||
tag_update = h("a.version-update.button",
|
||||
{href: "#Update+plugin", onclick: @handleUpdateClick, "data-plugin": plugin},
|
||||
"Update to rev#{plugin.site_info.rev}"
|
||||
)
|
||||
|
||||
else
|
||||
tag_update = h("span.version-missing", "(unable to get latest vesion: update site missing)")
|
||||
|
||||
tag_version = h("span.version",[
|
||||
"rev#{plugin.info.rev} ",
|
||||
tag_update,
|
||||
])
|
||||
|
||||
tag_source = h("div.source",[
|
||||
"Source: ",
|
||||
h("a", {"href": "/#{plugin.source}", "target": "_top"}, if plugin.site_title then plugin.site_title else plugin.source),
|
||||
" /" + plugin.inner_path
|
||||
])
|
||||
|
||||
tag_delete = h("a.delete", {"href": "#Delete+plugin", onclick: @handleDeleteClick, "data-plugin": plugin}, "Delete plugin")
|
||||
|
||||
|
||||
enabled_default = plugin.info.default == "enabled"
|
||||
if plugin.enabled != plugin.loaded or plugin.updated
|
||||
marker_title = "Change pending"
|
||||
is_pending = true
|
||||
else
|
||||
marker_title = "Changed from default status (click to reset to #{plugin.info.default})"
|
||||
is_pending = false
|
||||
|
||||
is_changed = plugin.enabled != enabled_default and plugin.owner == "builtin"
|
||||
|
||||
h("div.plugin", {key: plugin.name}, [
|
||||
h("div.title", [
|
||||
h("h3", [plugin.name, tag_version]),
|
||||
h("div.description", [descr, tag_source, tag_delete]),
|
||||
])
|
||||
h("div.value.value-right",
|
||||
h("div.checkbox", {onclick: @handleCheckboxChange, "data-plugin": plugin, classes: {checked: plugin.enabled}}, h("div.checkbox-skin"))
|
||||
h("a.marker", {
|
||||
href: "#Reset", title: marker_title,
|
||||
onclick: @handleResetClick, "data-plugin": plugin,
|
||||
classes: {visible: is_pending or is_changed, pending: is_pending}
|
||||
}, "\u2022")
|
||||
)
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
window.PluginList = PluginList
|
|
@ -0,0 +1,71 @@
|
|||
window.h = maquette.h
|
||||
|
||||
class UiPluginManager extends ZeroFrame
|
||||
init: ->
|
||||
@plugin_list_builtin = new PluginList()
|
||||
@plugin_list_custom = new PluginList()
|
||||
@plugins_changed = null
|
||||
@need_restart = null
|
||||
@
|
||||
|
||||
onOpenWebsocket: =>
|
||||
@cmd("wrapperSetTitle", "Plugin manager - ZeroNet")
|
||||
@cmd "serverInfo", {}, (server_info) =>
|
||||
@server_info = server_info
|
||||
@updatePlugins()
|
||||
|
||||
updatePlugins: (cb) =>
|
||||
@cmd "pluginList", [], (res) =>
|
||||
@plugins_changed = (item for item in res.plugins when item.enabled != item.loaded or item.updated)
|
||||
|
||||
plugins_builtin = (item for item in res.plugins when item.source == "builtin")
|
||||
@plugin_list_builtin.plugins = plugins_builtin.sort (a, b) ->
|
||||
return a.name.localeCompare(b.name)
|
||||
|
||||
plugins_custom = (item for item in res.plugins when item.source != "builtin")
|
||||
@plugin_list_custom.plugins = plugins_custom.sort (a, b) ->
|
||||
return a.name.localeCompare(b.name)
|
||||
|
||||
@projector.scheduleRender()
|
||||
cb?()
|
||||
|
||||
createProjector: =>
|
||||
@projector = maquette.createProjector()
|
||||
@projector.replace($("#content"), @render)
|
||||
@projector.replace($("#bottom-restart"), @renderBottomRestart)
|
||||
|
||||
render: =>
|
||||
if not @plugin_list_builtin.plugins
|
||||
return h("div.content")
|
||||
|
||||
h("div.content", [
|
||||
h("div.section", [
|
||||
if @plugin_list_custom.plugins?.length
|
||||
[
|
||||
h("h2", "Installed third-party plugins"),
|
||||
@plugin_list_custom.render()
|
||||
]
|
||||
h("h2", "Built-in plugins")
|
||||
@plugin_list_builtin.render()
|
||||
])
|
||||
])
|
||||
|
||||
handleRestartClick: =>
|
||||
@restart_loading = true
|
||||
setTimeout ( =>
|
||||
Page.cmd("serverShutdown", {restart: true})
|
||||
), 300
|
||||
Page.projector.scheduleRender()
|
||||
return false
|
||||
|
||||
renderBottomRestart: =>
|
||||
h("div.bottom.bottom-restart", {classes: {visible: @plugins_changed?.length}}, h("div.bottom-content", [
|
||||
h("div.title", "Some plugins status has been changed"),
|
||||
h("a.button.button-submit.button-restart",
|
||||
{href: "#Restart", classes: {loading: @restart_loading}, onclick: @handleRestartClick},
|
||||
"Restart ZeroNet client"
|
||||
)
|
||||
]))
|
||||
|
||||
window.Page = new UiPluginManager()
|
||||
window.Page.createProjector()
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,23 @@
|
|||
class Class
|
||||
trace: true
|
||||
|
||||
log: (args...) ->
|
||||
return unless @trace
|
||||
return if typeof console is 'undefined'
|
||||
args.unshift("[#{@.constructor.name}]")
|
||||
console.log(args...)
|
||||
@
|
||||
|
||||
logStart: (name, args...) ->
|
||||
return unless @trace
|
||||
@logtimers or= {}
|
||||
@logtimers[name] = +(new Date)
|
||||
@log "#{name}", args..., "(started)" if args.length > 0
|
||||
@
|
||||
|
||||
logEnd: (name, args...) ->
|
||||
ms = +(new Date)-@logtimers[name]
|
||||
@log "#{name}", args..., "(Done in #{ms}ms)"
|
||||
@
|
||||
|
||||
window.Class = Class
|
|
@ -0,0 +1,74 @@
|
|||
# From: http://dev.bizo.com/2011/12/promises-in-javascriptcoffeescript.html
|
||||
|
||||
class Promise
|
||||
@when: (tasks...) ->
|
||||
num_uncompleted = tasks.length
|
||||
args = new Array(num_uncompleted)
|
||||
promise = new Promise()
|
||||
|
||||
for task, task_id in tasks
|
||||
((task_id) ->
|
||||
task.then(() ->
|
||||
args[task_id] = Array.prototype.slice.call(arguments)
|
||||
num_uncompleted--
|
||||
promise.complete.apply(promise, args) if num_uncompleted == 0
|
||||
)
|
||||
)(task_id)
|
||||
|
||||
return promise
|
||||
|
||||
constructor: ->
|
||||
@resolved = false
|
||||
@end_promise = null
|
||||
@result = null
|
||||
@callbacks = []
|
||||
|
||||
resolve: ->
|
||||
if @resolved
|
||||
return false
|
||||
@resolved = true
|
||||
@data = arguments
|
||||
if not arguments.length
|
||||
@data = [true]
|
||||
@result = @data[0]
|
||||
for callback in @callbacks
|
||||
back = callback.apply callback, @data
|
||||
if @end_promise
|
||||
@end_promise.resolve(back)
|
||||
|
||||
fail: ->
|
||||
@resolve(false)
|
||||
|
||||
then: (callback) ->
|
||||
if @resolved == true
|
||||
callback.apply callback, @data
|
||||
return
|
||||
|
||||
@callbacks.push callback
|
||||
|
||||
@end_promise = new Promise()
|
||||
|
||||
window.Promise = Promise
|
||||
|
||||
###
|
||||
s = Date.now()
|
||||
log = (text) ->
|
||||
console.log Date.now()-s, Array.prototype.slice.call(arguments).join(", ")
|
||||
|
||||
log "Started"
|
||||
|
||||
cmd = (query) ->
|
||||
p = new Promise()
|
||||
setTimeout ( ->
|
||||
p.resolve query+" Result"
|
||||
), 100
|
||||
return p
|
||||
|
||||
back = cmd("SELECT * FROM message").then (res) ->
|
||||
log res
|
||||
return "Return from query"
|
||||
.then (res) ->
|
||||
log "Back then", res
|
||||
|
||||
log "Query started", back
|
||||
###
|
|
@ -0,0 +1,8 @@
|
|||
String::startsWith = (s) -> @[...s.length] is s
|
||||
String::endsWith = (s) -> s is '' or @[-s.length..] is s
|
||||
String::repeat = (count) -> new Array( count + 1 ).join(@)
|
||||
|
||||
window.isEmpty = (obj) ->
|
||||
for key of obj
|
||||
return false
|
||||
return true
|
|
@ -0,0 +1,770 @@
|
|||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['exports'], factory);
|
||||
} else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
|
||||
// CommonJS
|
||||
factory(exports);
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(root.maquette = {});
|
||||
}
|
||||
}(this, function (exports) {
|
||||
'use strict';
|
||||
;
|
||||
;
|
||||
;
|
||||
;
|
||||
var NAMESPACE_W3 = 'http://www.w3.org/';
|
||||
var NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg';
|
||||
var NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink';
|
||||
// Utilities
|
||||
var emptyArray = [];
|
||||
var extend = function (base, overrides) {
|
||||
var result = {};
|
||||
Object.keys(base).forEach(function (key) {
|
||||
result[key] = base[key];
|
||||
});
|
||||
if (overrides) {
|
||||
Object.keys(overrides).forEach(function (key) {
|
||||
result[key] = overrides[key];
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
// Hyperscript helper functions
|
||||
var same = function (vnode1, vnode2) {
|
||||
if (vnode1.vnodeSelector !== vnode2.vnodeSelector) {
|
||||
return false;
|
||||
}
|
||||
if (vnode1.properties && vnode2.properties) {
|
||||
if (vnode1.properties.key !== vnode2.properties.key) {
|
||||
return false;
|
||||
}
|
||||
return vnode1.properties.bind === vnode2.properties.bind;
|
||||
}
|
||||
return !vnode1.properties && !vnode2.properties;
|
||||
};
|
||||
var toTextVNode = function (data) {
|
||||
return {
|
||||
vnodeSelector: '',
|
||||
properties: undefined,
|
||||
children: undefined,
|
||||
text: data.toString(),
|
||||
domNode: null
|
||||
};
|
||||
};
|
||||
var appendChildren = function (parentSelector, insertions, main) {
|
||||
for (var i = 0; i < insertions.length; i++) {
|
||||
var item = insertions[i];
|
||||
if (Array.isArray(item)) {
|
||||
appendChildren(parentSelector, item, main);
|
||||
} else {
|
||||
if (item !== null && item !== undefined) {
|
||||
if (!item.hasOwnProperty('vnodeSelector')) {
|
||||
item = toTextVNode(item);
|
||||
}
|
||||
main.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// Render helper functions
|
||||
var missingTransition = function () {
|
||||
throw new Error('Provide a transitions object to the projectionOptions to do animations');
|
||||
};
|
||||
var DEFAULT_PROJECTION_OPTIONS = {
|
||||
namespace: undefined,
|
||||
eventHandlerInterceptor: undefined,
|
||||
styleApplyer: function (domNode, styleName, value) {
|
||||
// Provides a hook to add vendor prefixes for browsers that still need it.
|
||||
domNode.style[styleName] = value;
|
||||
},
|
||||
transitions: {
|
||||
enter: missingTransition,
|
||||
exit: missingTransition
|
||||
}
|
||||
};
|
||||
var applyDefaultProjectionOptions = function (projectorOptions) {
|
||||
return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions);
|
||||
};
|
||||
var checkStyleValue = function (styleValue) {
|
||||
if (typeof styleValue !== 'string') {
|
||||
throw new Error('Style values must be strings');
|
||||
}
|
||||
};
|
||||
var setProperties = function (domNode, properties, projectionOptions) {
|
||||
if (!properties) {
|
||||
return;
|
||||
}
|
||||
var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor;
|
||||
var propNames = Object.keys(properties);
|
||||
var propCount = propNames.length;
|
||||
for (var i = 0; i < propCount; i++) {
|
||||
var propName = propNames[i];
|
||||
/* tslint:disable:no-var-keyword: edge case */
|
||||
var propValue = properties[propName];
|
||||
/* tslint:enable:no-var-keyword */
|
||||
if (propName === 'className') {
|
||||
throw new Error('Property "className" is not supported, use "class".');
|
||||
} else if (propName === 'class') {
|
||||
if (domNode.className) {
|
||||
// May happen if classes is specified before class
|
||||
domNode.className += ' ' + propValue;
|
||||
} else {
|
||||
domNode.className = propValue;
|
||||
}
|
||||
} else if (propName === 'classes') {
|
||||
// object with string keys and boolean values
|
||||
var classNames = Object.keys(propValue);
|
||||
var classNameCount = classNames.length;
|
||||
for (var j = 0; j < classNameCount; j++) {
|
||||
var className = classNames[j];
|
||||
if (propValue[className]) {
|
||||
domNode.classList.add(className);
|
||||
}
|
||||
}
|
||||
} else if (propName === 'styles') {
|
||||
// object with string keys and string (!) values
|
||||
var styleNames = Object.keys(propValue);
|
||||
var styleCount = styleNames.length;
|
||||
for (var j = 0; j < styleCount; j++) {
|
||||
var styleName = styleNames[j];
|
||||
var styleValue = propValue[styleName];
|
||||
if (styleValue) {
|
||||
checkStyleValue(styleValue);
|
||||
projectionOptions.styleApplyer(domNode, styleName, styleValue);
|
||||
}
|
||||
}
|
||||
} else if (propName === 'key') {
|
||||
continue;
|
||||
} else if (propValue === null || propValue === undefined) {
|
||||
continue;
|
||||
} else {
|
||||
var type = typeof propValue;
|
||||
if (type === 'function') {
|
||||
if (propName.lastIndexOf('on', 0) === 0) {
|
||||
if (eventHandlerInterceptor) {
|
||||
propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); // intercept eventhandlers
|
||||
}
|
||||
if (propName === 'oninput') {
|
||||
(function () {
|
||||
// record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput
|
||||
var oldPropValue = propValue;
|
||||
propValue = function (evt) {
|
||||
evt.target['oninput-value'] = evt.target.value;
|
||||
// may be HTMLTextAreaElement as well
|
||||
oldPropValue.apply(this, [evt]);
|
||||
};
|
||||
}());
|
||||
}
|
||||
domNode[propName] = propValue;
|
||||
}
|
||||
} else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') {
|
||||
if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
|
||||
domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
|
||||
} else {
|
||||
domNode.setAttribute(propName, propValue);
|
||||
}
|
||||
} else {
|
||||
domNode[propName] = propValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
var updateProperties = function (domNode, previousProperties, properties, projectionOptions) {
|
||||
if (!properties) {
|
||||
return;
|
||||
}
|
||||
var propertiesUpdated = false;
|
||||
var propNames = Object.keys(properties);
|
||||
var propCount = propNames.length;
|
||||
for (var i = 0; i < propCount; i++) {
|
||||
var propName = propNames[i];
|
||||
// assuming that properties will be nullified instead of missing is by design
|
||||
var propValue = properties[propName];
|
||||
var previousValue = previousProperties[propName];
|
||||
if (propName === 'class') {
|
||||
if (previousValue !== propValue) {
|
||||
throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.');
|
||||
}
|
||||
} else if (propName === 'classes') {
|
||||
var classList = domNode.classList;
|
||||
var classNames = Object.keys(propValue);
|
||||
var classNameCount = classNames.length;
|
||||
for (var j = 0; j < classNameCount; j++) {
|
||||
var className = classNames[j];
|
||||
var on = !!propValue[className];
|
||||
var previousOn = !!previousValue[className];
|
||||
if (on === previousOn) {
|
||||
continue;
|
||||
}
|
||||
propertiesUpdated = true;
|
||||
if (on) {
|
||||
classList.add(className);
|
||||
} else {
|
||||
classList.remove(className);
|
||||
}
|
||||
}
|
||||
} else if (propName === 'styles') {
|
||||
var styleNames = Object.keys(propValue);
|
||||
var styleCount = styleNames.length;
|
||||
for (var j = 0; j < styleCount; j++) {
|
||||
var styleName = styleNames[j];
|
||||
var newStyleValue = propValue[styleName];
|
||||
var oldStyleValue = previousValue[styleName];
|
||||
if (newStyleValue === oldStyleValue) {
|
||||
continue;
|
||||
}
|
||||
propertiesUpdated = true;
|
||||
if (newStyleValue) {
|
||||
checkStyleValue(newStyleValue);
|
||||
projectionOptions.styleApplyer(domNode, styleName, newStyleValue);
|
||||
} else {
|
||||
projectionOptions.styleApplyer(domNode, styleName, '');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!propValue && typeof previousValue === 'string') {
|
||||
propValue = '';
|
||||
}
|
||||
if (propName === 'value') {
|
||||
if (domNode[propName] !== propValue && domNode['oninput-value'] !== propValue) {
|
||||
domNode[propName] = propValue;
|
||||
// Reset the value, even if the virtual DOM did not change
|
||||
domNode['oninput-value'] = undefined;
|
||||
}
|
||||
// else do not update the domNode, otherwise the cursor position would be changed
|
||||
if (propValue !== previousValue) {
|
||||
propertiesUpdated = true;
|
||||
}
|
||||
} else if (propValue !== previousValue) {
|
||||
var type = typeof propValue;
|
||||
if (type === 'function') {
|
||||
throw new Error('Functions may not be updated on subsequent renders (property: ' + propName + '). Hint: declare event handler functions outside the render() function.');
|
||||
}
|
||||
if (type === 'string' && propName !== 'innerHTML') {
|
||||
if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
|
||||
domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
|
||||
} else {
|
||||
domNode.setAttribute(propName, propValue);
|
||||
}
|
||||
} else {
|
||||
if (domNode[propName] !== propValue) {
|
||||
domNode[propName] = propValue;
|
||||
}
|
||||
}
|
||||
propertiesUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return propertiesUpdated;
|
||||
};
|
||||
var findIndexOfChild = function (children, sameAs, start) {
|
||||
if (sameAs.vnodeSelector !== '') {
|
||||
// Never scan for text-nodes
|
||||
for (var i = start; i < children.length; i++) {
|
||||
if (same(children[i], sameAs)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
var nodeAdded = function (vNode, transitions) {
|
||||
if (vNode.properties) {
|
||||
var enterAnimation = vNode.properties.enterAnimation;
|
||||
if (enterAnimation) {
|
||||
if (typeof enterAnimation === 'function') {
|
||||
enterAnimation(vNode.domNode, vNode.properties);
|
||||
} else {
|
||||
transitions.enter(vNode.domNode, vNode.properties, enterAnimation);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
var nodeToRemove = function (vNode, transitions) {
|
||||
var domNode = vNode.domNode;
|
||||
if (vNode.properties) {
|
||||
var exitAnimation = vNode.properties.exitAnimation;
|
||||
if (exitAnimation) {
|
||||
domNode.style.pointerEvents = 'none';
|
||||
var removeDomNode = function () {
|
||||
if (domNode.parentNode) {
|
||||
domNode.parentNode.removeChild(domNode);
|
||||
}
|
||||
};
|
||||
if (typeof exitAnimation === 'function') {
|
||||
exitAnimation(domNode, removeDomNode, vNode.properties);
|
||||
return;
|
||||
} else {
|
||||
transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (domNode.parentNode) {
|
||||
domNode.parentNode.removeChild(domNode);
|
||||
}
|
||||
};
|
||||
var checkDistinguishable = function (childNodes, indexToCheck, parentVNode, operation) {
|
||||
var childNode = childNodes[indexToCheck];
|
||||
if (childNode.vnodeSelector === '') {
|
||||
return; // Text nodes need not be distinguishable
|
||||
}
|
||||
var properties = childNode.properties;
|
||||
var key = properties ? properties.key === undefined ? properties.bind : properties.key : undefined;
|
||||
if (!key) {
|
||||
for (var i = 0; i < childNodes.length; i++) {
|
||||
if (i !== indexToCheck) {
|
||||
var node = childNodes[i];
|
||||
if (same(node, childNode)) {
|
||||
if (operation === 'added') {
|
||||
throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'added, but there is now more than one. You must add unique key properties to make them distinguishable.');
|
||||
} else {
|
||||
throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'removed, but there were more than one. You must add unique key properties to make them distinguishable.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
var createDom;
|
||||
var updateDom;
|
||||
var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) {
|
||||
if (oldChildren === newChildren) {
|
||||
return false;
|
||||
}
|
||||
oldChildren = oldChildren || emptyArray;
|
||||
newChildren = newChildren || emptyArray;
|
||||
var oldChildrenLength = oldChildren.length;
|
||||
var newChildrenLength = newChildren.length;
|
||||
var transitions = projectionOptions.transitions;
|
||||
var oldIndex = 0;
|
||||
var newIndex = 0;
|
||||
var i;
|
||||
var textUpdated = false;
|
||||
while (newIndex < newChildrenLength) {
|
||||
var oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined;
|
||||
var newChild = newChildren[newIndex];
|
||||
if (oldChild !== undefined && same(oldChild, newChild)) {
|
||||
textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated;
|
||||
oldIndex++;
|
||||
} else {
|
||||
var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1);
|
||||
if (findOldIndex >= 0) {
|
||||
// Remove preceding missing children
|
||||
for (i = oldIndex; i < findOldIndex; i++) {
|
||||
nodeToRemove(oldChildren[i], transitions);
|
||||
checkDistinguishable(oldChildren, i, vnode, 'removed');
|
||||
}
|
||||
textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated;
|
||||
oldIndex = findOldIndex + 1;
|
||||
} else {
|
||||
// New child
|
||||
createDom(newChild, domNode, oldIndex < oldChildrenLength ? oldChildren[oldIndex].domNode : undefined, projectionOptions);
|
||||
nodeAdded(newChild, transitions);
|
||||
checkDistinguishable(newChildren, newIndex, vnode, 'added');
|
||||
}
|
||||
}
|
||||
newIndex++;
|
||||
}
|
||||
if (oldChildrenLength > oldIndex) {
|
||||
// Remove child fragments
|
||||
for (i = oldIndex; i < oldChildrenLength; i++) {
|
||||
nodeToRemove(oldChildren[i], transitions);
|
||||
checkDistinguishable(oldChildren, i, vnode, 'removed');
|
||||
}
|
||||
}
|
||||
return textUpdated;
|
||||
};
|
||||
var addChildren = function (domNode, children, projectionOptions) {
|
||||
if (!children) {
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
createDom(children[i], domNode, undefined, projectionOptions);
|
||||
}
|
||||
};
|
||||
var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) {
|
||||
addChildren(domNode, vnode.children, projectionOptions);
|
||||
// children before properties, needed for value property of <select>.
|
||||
if (vnode.text) {
|
||||
domNode.textContent = vnode.text;
|
||||
}
|
||||
setProperties(domNode, vnode.properties, projectionOptions);
|
||||
if (vnode.properties && vnode.properties.afterCreate) {
|
||||
vnode.properties.afterCreate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
|
||||
}
|
||||
};
|
||||
createDom = function (vnode, parentNode, insertBefore, projectionOptions) {
|
||||
var domNode, i, c, start = 0, type, found;
|
||||
var vnodeSelector = vnode.vnodeSelector;
|
||||
if (vnodeSelector === '') {
|
||||
domNode = vnode.domNode = document.createTextNode(vnode.text);
|
||||
if (insertBefore !== undefined) {
|
||||
parentNode.insertBefore(domNode, insertBefore);
|
||||
} else {
|
||||
parentNode.appendChild(domNode);
|
||||
}
|
||||
} else {
|
||||
for (i = 0; i <= vnodeSelector.length; ++i) {
|
||||
c = vnodeSelector.charAt(i);
|
||||
if (i === vnodeSelector.length || c === '.' || c === '#') {
|
||||
type = vnodeSelector.charAt(start - 1);
|
||||
found = vnodeSelector.slice(start, i);
|
||||
if (type === '.') {
|
||||
domNode.classList.add(found);
|
||||
} else if (type === '#') {
|
||||
domNode.id = found;
|
||||
} else {
|
||||
if (found === 'svg') {
|
||||
projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
|
||||
}
|
||||
if (projectionOptions.namespace !== undefined) {
|
||||
domNode = vnode.domNode = document.createElementNS(projectionOptions.namespace, found);
|
||||
} else {
|
||||
domNode = vnode.domNode = document.createElement(found);
|
||||
}
|
||||
if (insertBefore !== undefined) {
|
||||
parentNode.insertBefore(domNode, insertBefore);
|
||||
} else {
|
||||
parentNode.appendChild(domNode);
|
||||
}
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
initPropertiesAndChildren(domNode, vnode, projectionOptions);
|
||||
}
|
||||
};
|
||||
updateDom = function (previous, vnode, projectionOptions) {
|
||||
var domNode = previous.domNode;
|
||||
var textUpdated = false;
|
||||
if (previous === vnode) {
|
||||
return false; // By contract, VNode objects may not be modified anymore after passing them to maquette
|
||||
}
|
||||
var updated = false;
|
||||
if (vnode.vnodeSelector === '') {
|
||||
if (vnode.text !== previous.text) {
|
||||
var newVNode = document.createTextNode(vnode.text);
|
||||
domNode.parentNode.replaceChild(newVNode, domNode);
|
||||
vnode.domNode = newVNode;
|
||||
textUpdated = true;
|
||||
return textUpdated;
|
||||
}
|
||||
} else {
|
||||
if (vnode.vnodeSelector.lastIndexOf('svg', 0) === 0) {
|
||||
projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
|
||||
}
|
||||
if (previous.text !== vnode.text) {
|
||||
updated = true;
|
||||
if (vnode.text === undefined) {
|
||||
domNode.removeChild(domNode.firstChild); // the only textnode presumably
|
||||
} else {
|
||||
domNode.textContent = vnode.text;
|
||||
}
|
||||
}
|
||||
updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated;
|
||||
updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated;
|
||||
if (vnode.properties && vnode.properties.afterUpdate) {
|
||||
vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
|
||||
}
|
||||
}
|
||||
if (updated && vnode.properties && vnode.properties.updateAnimation) {
|
||||
vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties);
|
||||
}
|
||||
vnode.domNode = previous.domNode;
|
||||
return textUpdated;
|
||||
};
|
||||
var createProjection = function (vnode, projectionOptions) {
|
||||
return {
|
||||
update: function (updatedVnode) {
|
||||
if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) {
|
||||
throw new Error('The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)');
|
||||
}
|
||||
updateDom(vnode, updatedVnode, projectionOptions);
|
||||
vnode = updatedVnode;
|
||||
},
|
||||
domNode: vnode.domNode
|
||||
};
|
||||
};
|
||||
;
|
||||
// The other two parameters are not added here, because the Typescript compiler creates surrogate code for desctructuring 'children'.
|
||||
exports.h = function (selector) {
|
||||
var properties = arguments[1];
|
||||
if (typeof selector !== 'string') {
|
||||
throw new Error();
|
||||
}
|
||||
var childIndex = 1;
|
||||
if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') {
|
||||
childIndex = 2;
|
||||
} else {
|
||||
// Optional properties argument was omitted
|
||||
properties = undefined;
|
||||
}
|
||||
var text = undefined;
|
||||
var children = undefined;
|
||||
var argsLength = arguments.length;
|
||||
// Recognize a common special case where there is only a single text node
|
||||
if (argsLength === childIndex + 1) {
|
||||
var onlyChild = arguments[childIndex];
|
||||
if (typeof onlyChild === 'string') {
|
||||
text = onlyChild;
|
||||
} else if (onlyChild !== undefined && onlyChild.length === 1 && typeof onlyChild[0] === 'string') {
|
||||
text = onlyChild[0];
|
||||
}
|
||||
}
|
||||
if (text === undefined) {
|
||||
children = [];
|
||||
for (; childIndex < arguments.length; childIndex++) {
|
||||
var child = arguments[childIndex];
|
||||
if (child === null || child === undefined) {
|
||||
continue;
|
||||
} else if (Array.isArray(child)) {
|
||||
appendChildren(selector, child, children);
|
||||
} else if (child.hasOwnProperty('vnodeSelector')) {
|
||||
children.push(child);
|
||||
} else {
|
||||
children.push(toTextVNode(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
vnodeSelector: selector,
|
||||
properties: properties,
|
||||
children: children,
|
||||
text: text === '' ? undefined : text,
|
||||
domNode: null
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Contains simple low-level utility functions to manipulate the real DOM.
|
||||
*/
|
||||
exports.dom = {
|
||||
/**
|
||||
* Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in
|
||||
* its [[Projection.domNode|domNode]] property.
|
||||
* This is a low-level method. Users wil typically use a [[Projector]] instead.
|
||||
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
|
||||
* objects may only be rendered once.
|
||||
* @param projectionOptions - Options to be used to create and update the projection.
|
||||
* @returns The [[Projection]] which also contains the DOM Node that was created.
|
||||
*/
|
||||
create: function (vnode, projectionOptions) {
|
||||
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
|
||||
createDom(vnode, document.createElement('div'), undefined, projectionOptions);
|
||||
return createProjection(vnode, projectionOptions);
|
||||
},
|
||||
/**
|
||||
* Appends a new childnode to the DOM which is generated from a [[VNode]].
|
||||
* This is a low-level method. Users wil typically use a [[Projector]] instead.
|
||||
* @param parentNode - The parent node for the new childNode.
|
||||
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
|
||||
* objects may only be rendered once.
|
||||
* @param projectionOptions - Options to be used to create and update the [[Projection]].
|
||||
* @returns The [[Projection]] that was created.
|
||||
*/
|
||||
append: function (parentNode, vnode, projectionOptions) {
|
||||
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
|
||||
createDom(vnode, parentNode, undefined, projectionOptions);
|
||||
return createProjection(vnode, projectionOptions);
|
||||
},
|
||||
/**
|
||||
* Inserts a new DOM node which is generated from a [[VNode]].
|
||||
* This is a low-level method. Users wil typically use a [[Projector]] instead.
|
||||
* @param beforeNode - The node that the DOM Node is inserted before.
|
||||
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function.
|
||||
* NOTE: [[VNode]] objects may only be rendered once.
|
||||
* @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
|
||||
* @returns The [[Projection]] that was created.
|
||||
*/
|
||||
insertBefore: function (beforeNode, vnode, projectionOptions) {
|
||||
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
|
||||
createDom(vnode, beforeNode.parentNode, beforeNode, projectionOptions);
|
||||
return createProjection(vnode, projectionOptions);
|
||||
},
|
||||
/**
|
||||
* Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node.
|
||||
* This means that the virtual DOM and the real DOM will have one overlapping element.
|
||||
* Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided.
|
||||
* This is a low-level method. Users wil typically use a [[Projector]] instead.
|
||||
* @param domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved.
|
||||
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects
|
||||
* may only be rendered once.
|
||||
* @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
|
||||
* @returns The [[Projection]] that was created.
|
||||
*/
|
||||
merge: function (element, vnode, projectionOptions) {
|
||||
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
|
||||
vnode.domNode = element;
|
||||
initPropertiesAndChildren(element, vnode, projectionOptions);
|
||||
return createProjection(vnode, projectionOptions);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees.
|
||||
* In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem.
|
||||
* For more information, see [[CalculationCache]].
|
||||
*
|
||||
* @param <Result> The type of the value that is cached.
|
||||
*/
|
||||
exports.createCache = function () {
|
||||
var cachedInputs = undefined;
|
||||
var cachedOutcome = undefined;
|
||||
var result = {
|
||||
invalidate: function () {
|
||||
cachedOutcome = undefined;
|
||||
cachedInputs = undefined;
|
||||
},
|
||||
result: function (inputs, calculation) {
|
||||
if (cachedInputs) {
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
if (cachedInputs[i] !== inputs[i]) {
|
||||
cachedOutcome = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!cachedOutcome) {
|
||||
cachedOutcome = calculation();
|
||||
cachedInputs = inputs;
|
||||
}
|
||||
return cachedOutcome;
|
||||
}
|
||||
};
|
||||
return result;
|
||||
};
|
||||
/**
|
||||
* Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects.
|
||||
* See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}.
|
||||
*
|
||||
* @param <Source> The type of source items. A database-record for instance.
|
||||
* @param <Target> The type of target items. A [[Component]] for instance.
|
||||
* @param getSourceKey `function(source)` that must return a key to identify each source object. The result must either be a string or a number.
|
||||
* @param createResult `function(source, index)` that must create a new result object from a given source. This function is identical
|
||||
* to the `callback` argument in `Array.map(callback)`.
|
||||
* @param updateResult `function(source, target, index)` that updates a result to an updated source.
|
||||
*/
|
||||
exports.createMapping = function (getSourceKey, createResult, updateResult) {
|
||||
var keys = [];
|
||||
var results = [];
|
||||
return {
|
||||
results: results,
|
||||
map: function (newSources) {
|
||||
var newKeys = newSources.map(getSourceKey);
|
||||
var oldTargets = results.slice();
|
||||
var oldIndex = 0;
|
||||
for (var i = 0; i < newSources.length; i++) {
|
||||
var source = newSources[i];
|
||||
var sourceKey = newKeys[i];
|
||||
if (sourceKey === keys[oldIndex]) {
|
||||
results[i] = oldTargets[oldIndex];
|
||||
updateResult(source, oldTargets[oldIndex], i);
|
||||
oldIndex++;
|
||||
} else {
|
||||
var found = false;
|
||||
for (var j = 1; j < keys.length; j++) {
|
||||
var searchIndex = (oldIndex + j) % keys.length;
|
||||
if (keys[searchIndex] === sourceKey) {
|
||||
results[i] = oldTargets[searchIndex];
|
||||
updateResult(newSources[i], oldTargets[searchIndex], i);
|
||||
oldIndex = searchIndex + 1;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
results[i] = createResult(source, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
results.length = newSources.length;
|
||||
keys = newKeys;
|
||||
}
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Creates a [[Projector]] instance using the provided projectionOptions.
|
||||
*
|
||||
* For more information, see [[Projector]].
|
||||
*
|
||||
* @param projectionOptions Options that influence how the DOM is rendered and updated.
|
||||
*/
|
||||
exports.createProjector = function (projectorOptions) {
|
||||
var projector;
|
||||
var projectionOptions = applyDefaultProjectionOptions(projectorOptions);
|
||||
projectionOptions.eventHandlerInterceptor = function (propertyName, eventHandler, domNode, properties) {
|
||||
return function () {
|
||||
// intercept function calls (event handlers) to do a render afterwards.
|
||||
projector.scheduleRender();
|
||||
return eventHandler.apply(properties.bind || this, arguments);
|
||||
};
|
||||
};
|
||||
var renderCompleted = true;
|
||||
var scheduled;
|
||||
var stopped = false;
|
||||
var projections = [];
|
||||
var renderFunctions = [];
|
||||
// matches the projections array
|
||||
var doRender = function () {
|
||||
scheduled = undefined;
|
||||
if (!renderCompleted) {
|
||||
return; // The last render threw an error, it should be logged in the browser console.
|
||||
}
|
||||
renderCompleted = false;
|
||||
for (var i = 0; i < projections.length; i++) {
|
||||
var updatedVnode = renderFunctions[i]();
|
||||
projections[i].update(updatedVnode);
|
||||
}
|
||||
renderCompleted = true;
|
||||
};
|
||||
projector = {
|
||||
scheduleRender: function () {
|
||||
if (!scheduled && !stopped) {
|
||||
scheduled = requestAnimationFrame(doRender);
|
||||
}
|
||||
},
|
||||
stop: function () {
|
||||
if (scheduled) {
|
||||
cancelAnimationFrame(scheduled);
|
||||
scheduled = undefined;
|
||||
}
|
||||
stopped = true;
|
||||
},
|
||||
resume: function () {
|
||||
stopped = false;
|
||||
renderCompleted = true;
|
||||
projector.scheduleRender();
|
||||
},
|
||||
append: function (parentNode, renderMaquetteFunction) {
|
||||
projections.push(exports.dom.append(parentNode, renderMaquetteFunction(), projectionOptions));
|
||||
renderFunctions.push(renderMaquetteFunction);
|
||||
},
|
||||
insertBefore: function (beforeNode, renderMaquetteFunction) {
|
||||
projections.push(exports.dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions));
|
||||
renderFunctions.push(renderMaquetteFunction);
|
||||
},
|
||||
merge: function (domNode, renderMaquetteFunction) {
|
||||
projections.push(exports.dom.merge(domNode, renderMaquetteFunction(), projectionOptions));
|
||||
renderFunctions.push(renderMaquetteFunction);
|
||||
},
|
||||
replace: function (domNode, renderMaquetteFunction) {
|
||||
var vnode = renderMaquetteFunction();
|
||||
createDom(vnode, domNode.parentNode, domNode, projectionOptions);
|
||||
domNode.parentNode.removeChild(domNode);
|
||||
projections.push(createProjection(vnode, projectionOptions));
|
||||
renderFunctions.push(renderMaquetteFunction);
|
||||
},
|
||||
detach: function (renderMaquetteFunction) {
|
||||
for (var i = 0; i < renderFunctions.length; i++) {
|
||||
if (renderFunctions[i] === renderMaquetteFunction) {
|
||||
renderFunctions.splice(i, 1);
|
||||
return projections.splice(i, 1)[0];
|
||||
}
|
||||
}
|
||||
throw new Error('renderMaquetteFunction was not found');
|
||||
}
|
||||
};
|
||||
return projector;
|
||||
};
|
||||
}));
|
|
@ -0,0 +1,138 @@
|
|||
class Animation
|
||||
slideDown: (elem, props) ->
|
||||
if elem.offsetTop > 2000
|
||||
return
|
||||
|
||||
h = elem.offsetHeight
|
||||
cstyle = window.getComputedStyle(elem)
|
||||
margin_top = cstyle.marginTop
|
||||
margin_bottom = cstyle.marginBottom
|
||||
padding_top = cstyle.paddingTop
|
||||
padding_bottom = cstyle.paddingBottom
|
||||
transition = cstyle.transition
|
||||
|
||||
elem.style.boxSizing = "border-box"
|
||||
elem.style.overflow = "hidden"
|
||||
elem.style.transform = "scale(0.6)"
|
||||
elem.style.opacity = "0"
|
||||
elem.style.height = "0px"
|
||||
elem.style.marginTop = "0px"
|
||||
elem.style.marginBottom = "0px"
|
||||
elem.style.paddingTop = "0px"
|
||||
elem.style.paddingBottom = "0px"
|
||||
elem.style.transition = "none"
|
||||
|
||||
setTimeout (->
|
||||
elem.className += " animate-inout"
|
||||
elem.style.height = h+"px"
|
||||
elem.style.transform = "scale(1)"
|
||||
elem.style.opacity = "1"
|
||||
elem.style.marginTop = margin_top
|
||||
elem.style.marginBottom = margin_bottom
|
||||
elem.style.paddingTop = padding_top
|
||||
elem.style.paddingBottom = padding_bottom
|
||||
), 1
|
||||
|
||||
elem.addEventListener "transitionend", ->
|
||||
elem.classList.remove("animate-inout")
|
||||
elem.style.transition = elem.style.transform = elem.style.opacity = elem.style.height = null
|
||||
elem.style.boxSizing = elem.style.marginTop = elem.style.marginBottom = null
|
||||
elem.style.paddingTop = elem.style.paddingBottom = elem.style.overflow = null
|
||||
elem.removeEventListener "transitionend", arguments.callee, false
|
||||
|
||||
|
||||
slideUp: (elem, remove_func, props) ->
|
||||
if elem.offsetTop > 1000
|
||||
return remove_func()
|
||||
|
||||
elem.className += " animate-back"
|
||||
elem.style.boxSizing = "border-box"
|
||||
elem.style.height = elem.offsetHeight+"px"
|
||||
elem.style.overflow = "hidden"
|
||||
elem.style.transform = "scale(1)"
|
||||
elem.style.opacity = "1"
|
||||
elem.style.pointerEvents = "none"
|
||||
setTimeout (->
|
||||
elem.style.height = "0px"
|
||||
elem.style.marginTop = "0px"
|
||||
elem.style.marginBottom = "0px"
|
||||
elem.style.paddingTop = "0px"
|
||||
elem.style.paddingBottom = "0px"
|
||||
elem.style.transform = "scale(0.8)"
|
||||
elem.style.borderTopWidth = "0px"
|
||||
elem.style.borderBottomWidth = "0px"
|
||||
elem.style.opacity = "0"
|
||||
), 1
|
||||
elem.addEventListener "transitionend", (e) ->
|
||||
if e.propertyName == "opacity" or e.elapsedTime >= 0.6
|
||||
elem.removeEventListener "transitionend", arguments.callee, false
|
||||
remove_func()
|
||||
|
||||
|
||||
slideUpInout: (elem, remove_func, props) ->
|
||||
elem.className += " animate-inout"
|
||||
elem.style.boxSizing = "border-box"
|
||||
elem.style.height = elem.offsetHeight+"px"
|
||||
elem.style.overflow = "hidden"
|
||||
elem.style.transform = "scale(1)"
|
||||
elem.style.opacity = "1"
|
||||
elem.style.pointerEvents = "none"
|
||||
setTimeout (->
|
||||
elem.style.height = "0px"
|
||||
elem.style.marginTop = "0px"
|
||||
elem.style.marginBottom = "0px"
|
||||
elem.style.paddingTop = "0px"
|
||||
elem.style.paddingBottom = "0px"
|
||||
elem.style.transform = "scale(0.8)"
|
||||
elem.style.borderTopWidth = "0px"
|
||||
elem.style.borderBottomWidth = "0px"
|
||||
elem.style.opacity = "0"
|
||||
), 1
|
||||
elem.addEventListener "transitionend", (e) ->
|
||||
if e.propertyName == "opacity" or e.elapsedTime >= 0.6
|
||||
elem.removeEventListener "transitionend", arguments.callee, false
|
||||
remove_func()
|
||||
|
||||
|
||||
showRight: (elem, props) ->
|
||||
elem.className += " animate"
|
||||
elem.style.opacity = 0
|
||||
elem.style.transform = "TranslateX(-20px) Scale(1.01)"
|
||||
setTimeout (->
|
||||
elem.style.opacity = 1
|
||||
elem.style.transform = "TranslateX(0px) Scale(1)"
|
||||
), 1
|
||||
elem.addEventListener "transitionend", ->
|
||||
elem.classList.remove("animate")
|
||||
elem.style.transform = elem.style.opacity = null
|
||||
|
||||
|
||||
show: (elem, props) ->
|
||||
delay = arguments[arguments.length-2]?.delay*1000 or 1
|
||||
elem.style.opacity = 0
|
||||
setTimeout (->
|
||||
elem.className += " animate"
|
||||
), 1
|
||||
setTimeout (->
|
||||
elem.style.opacity = 1
|
||||
), delay
|
||||
elem.addEventListener "transitionend", ->
|
||||
elem.classList.remove("animate")
|
||||
elem.style.opacity = null
|
||||
elem.removeEventListener "transitionend", arguments.callee, false
|
||||
|
||||
hide: (elem, remove_func, props) ->
|
||||
delay = arguments[arguments.length-2]?.delay*1000 or 1
|
||||
elem.className += " animate"
|
||||
setTimeout (->
|
||||
elem.style.opacity = 0
|
||||
), delay
|
||||
elem.addEventListener "transitionend", (e) ->
|
||||
if e.propertyName == "opacity"
|
||||
remove_func()
|
||||
|
||||
addVisibleClass: (elem, props) ->
|
||||
setTimeout ->
|
||||
elem.classList.add("visible")
|
||||
|
||||
window.Animation = new Animation()
|
|
@ -0,0 +1,3 @@
|
|||
window.$ = (selector) ->
|
||||
if selector.startsWith("#")
|
||||
return document.getElementById(selector.replace("#", ""))
|
|
@ -0,0 +1,85 @@
|
|||
class ZeroFrame extends Class
|
||||
constructor: (url) ->
|
||||
@url = url
|
||||
@waiting_cb = {}
|
||||
@wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1")
|
||||
@connect()
|
||||
@next_message_id = 1
|
||||
@history_state = {}
|
||||
@init()
|
||||
|
||||
|
||||
init: ->
|
||||
@
|
||||
|
||||
|
||||
connect: ->
|
||||
@target = window.parent
|
||||
window.addEventListener("message", @onMessage, false)
|
||||
@cmd("innerReady")
|
||||
|
||||
# Save scrollTop
|
||||
window.addEventListener "beforeunload", (e) =>
|
||||
@log "save scrollTop", window.pageYOffset
|
||||
@history_state["scrollTop"] = window.pageYOffset
|
||||
@cmd "wrapperReplaceState", [@history_state, null]
|
||||
|
||||
# Restore scrollTop
|
||||
@cmd "wrapperGetState", [], (state) =>
|
||||
@history_state = state if state?
|
||||
@log "restore scrollTop", state, window.pageYOffset
|
||||
if window.pageYOffset == 0 and state
|
||||
window.scroll(window.pageXOffset, state.scrollTop)
|
||||
|
||||
|
||||
onMessage: (e) =>
|
||||
message = e.data
|
||||
cmd = message.cmd
|
||||
if cmd == "response"
|
||||
if @waiting_cb[message.to]?
|
||||
@waiting_cb[message.to](message.result)
|
||||
else
|
||||
@log "Websocket callback not found:", message
|
||||
else if cmd == "wrapperReady" # Wrapper inited later
|
||||
@cmd("innerReady")
|
||||
else if cmd == "ping"
|
||||
@response message.id, "pong"
|
||||
else if cmd == "wrapperOpenedWebsocket"
|
||||
@onOpenWebsocket()
|
||||
else if cmd == "wrapperClosedWebsocket"
|
||||
@onCloseWebsocket()
|
||||
else
|
||||
@onRequest cmd, message.params
|
||||
|
||||
|
||||
onRequest: (cmd, message) =>
|
||||
@log "Unknown request", message
|
||||
|
||||
|
||||
response: (to, result) ->
|
||||
@send {"cmd": "response", "to": to, "result": result}
|
||||
|
||||
|
||||
cmd: (cmd, params={}, cb=null) ->
|
||||
@send {"cmd": cmd, "params": params}, cb
|
||||
|
||||
|
||||
send: (message, cb=null) ->
|
||||
message.wrapper_nonce = @wrapper_nonce
|
||||
message.id = @next_message_id
|
||||
@next_message_id += 1
|
||||
@target.postMessage(message, "*")
|
||||
if cb
|
||||
@waiting_cb[message.id] = cb
|
||||
|
||||
|
||||
onOpenWebsocket: =>
|
||||
@log "Websocket open"
|
||||
|
||||
|
||||
onCloseWebsocket: =>
|
||||
@log "Websocket close"
|
||||
|
||||
|
||||
|
||||
window.ZeroFrame = ZeroFrame
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Settings - ZeroNet</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<link rel="stylesheet" href="css/all.css?rev={rev}" />
|
||||
</head>
|
||||
|
||||
|
||||
<h1>ZeroNet plugin manager</h1>
|
||||
|
||||
<div class="content" id="content"></div>
|
||||
<div class="bottom" id="bottom-restart"></div>
|
||||
|
||||
<script type="text/javascript" src="js/all.js?rev={rev}&lang={lang}"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,55 @@
|
|||
# ZeroName
|
||||
|
||||
Zeroname plugin to connect Namecoin and register all the .bit domain name.
|
||||
|
||||
## Start
|
||||
|
||||
You can create your own Zeroname.
|
||||
|
||||
### Namecoin node
|
||||
|
||||
You need to run a namecoin node.
|
||||
|
||||
[Namecoin](https://namecoin.org/download/)
|
||||
|
||||
You will need to start it as a RPC server.
|
||||
|
||||
Example of `~/.namecoin/namecoin.conf` minimal setup:
|
||||
```
|
||||
daemon=1
|
||||
rpcuser=your-name
|
||||
rpcpassword=your-password
|
||||
rpcport=8336
|
||||
server=1
|
||||
txindex=1
|
||||
valueencoding=utf8
|
||||
```
|
||||
|
||||
Don't forget to change the `rpcuser` value and `rpcpassword` value!
|
||||
|
||||
You can start your node : `./namecoind`
|
||||
|
||||
### Create a Zeroname site
|
||||
|
||||
You will also need to create a site `python zeronet.py createSite` and regitser the info.
|
||||
|
||||
In the site you will need to create a file `./data/<your-site>/data/names.json` with this is it:
|
||||
```
|
||||
{}
|
||||
```
|
||||
|
||||
### `zeroname_config.json` file
|
||||
|
||||
In `~/.namecoin/zeroname_config.json`
|
||||
```
|
||||
{
|
||||
"lastprocessed": 223910,
|
||||
"zeronet_path": "/root/ZeroNet", # Update with your path
|
||||
"privatekey": "", # Update with your private key of your site
|
||||
"site": "" # Update with the address of your site
|
||||
}
|
||||
```
|
||||
|
||||
### Run updater
|
||||
|
||||
You can now run the script : `updater/zeroname_updater.py` and wait until it is fully sync (it might take a while).
|
|
@ -0,0 +1,69 @@
|
|||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from Config import config
|
||||
from Plugin import PluginManager
|
||||
|
||||
allow_reload = False # No reload supported
|
||||
|
||||
log = logging.getLogger("ZeronamePlugin")
|
||||
|
||||
|
||||
@PluginManager.registerTo("SiteManager")
|
||||
class SiteManagerPlugin(object):
|
||||
site_zeroname = None
|
||||
db_domains = {}
|
||||
db_domains_modified = None
|
||||
|
||||
def load(self, *args, **kwargs):
|
||||
super(SiteManagerPlugin, self).load(*args, **kwargs)
|
||||
if not self.get(config.bit_resolver):
|
||||
self.need(config.bit_resolver) # Need ZeroName site
|
||||
|
||||
# Return: True if the address is .bit domain
|
||||
def isBitDomain(self, address):
|
||||
return re.match(r"(.*?)([A-Za-z0-9_-]+\.bit)$", address)
|
||||
|
||||
# Resolve domain
|
||||
# Return: The address or None
|
||||
def resolveBitDomain(self, domain):
|
||||
domain = domain.lower()
|
||||
if not self.site_zeroname:
|
||||
self.site_zeroname = self.need(config.bit_resolver)
|
||||
|
||||
site_zeroname_modified = self.site_zeroname.content_manager.contents.get("content.json", {}).get("modified", 0)
|
||||
if not self.db_domains or self.db_domains_modified != site_zeroname_modified:
|
||||
self.site_zeroname.needFile("data/names.json", priority=10)
|
||||
s = time.time()
|
||||
try:
|
||||
self.db_domains = self.site_zeroname.storage.loadJson("data/names.json")
|
||||
except Exception as err:
|
||||
log.error("Error loading names.json: %s" % err)
|
||||
|
||||
log.debug(
|
||||
"Domain db with %s entries loaded in %.3fs (modification: %s -> %s)" %
|
||||
(len(self.db_domains), time.time() - s, self.db_domains_modified, site_zeroname_modified)
|
||||
)
|
||||
self.db_domains_modified = site_zeroname_modified
|
||||
return self.db_domains.get(domain)
|
||||
|
||||
# Turn domain into address
|
||||
def resolveDomain(self, domain):
|
||||
return self.resolveBitDomain(domain) or super(SiteManagerPlugin, self).resolveDomain(domain)
|
||||
|
||||
# Return: True if the address is domain
|
||||
def isDomain(self, address):
|
||||
return self.isBitDomain(address) or super(SiteManagerPlugin, self).isDomain(address)
|
||||
|
||||
|
||||
@PluginManager.registerTo("ConfigPlugin")
|
||||
class ConfigPlugin(object):
|
||||
def createArguments(self):
|
||||
group = self.parser.add_argument_group("Zeroname plugin")
|
||||
group.add_argument(
|
||||
"--bit_resolver", help="ZeroNet site to resolve .bit domains",
|
||||
default="1Name2NXVi1RDPDgf5617UoW7xA6YrhM9F", metavar="address"
|
||||
)
|
||||
|
||||
return super(ConfigPlugin, self).createArguments()
|
|
@ -0,0 +1 @@
|
|||
from . import SiteManagerPlugin
|
|
@ -0,0 +1,249 @@
|
|||
from __future__ import print_function
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import socket
|
||||
|
||||
from six import string_types
|
||||
|
||||
from subprocess import call
|
||||
from bitcoinrpc.authproxy import AuthServiceProxy
|
||||
|
||||
|
||||
def publish():
|
||||
print("* Signing and Publishing...")
|
||||
call(" ".join(command_sign_publish), shell=True)
|
||||
|
||||
|
||||
def processNameOp(domain, value, test=False):
|
||||
if not value.strip().startswith("{"):
|
||||
return False
|
||||
try:
|
||||
data = json.loads(value)
|
||||
except Exception as err:
|
||||
print("Json load error: %s" % err)
|
||||
return False
|
||||
if "zeronet" not in data and "map" not in data:
|
||||
# Namecoin standard use {"map": { "blog": {"zeronet": "1D..."} }}
|
||||
print("No zeronet and no map in ", data.keys())
|
||||
return False
|
||||
if "map" in data:
|
||||
# If subdomains using the Namecoin standard is present, just re-write in the Zeronet way
|
||||
# and call the function again
|
||||
data_map = data["map"]
|
||||
new_value = {}
|
||||
for subdomain in data_map:
|
||||
if "zeronet" in data_map[subdomain]:
|
||||
new_value[subdomain] = data_map[subdomain]["zeronet"]
|
||||
if "zeronet" in data and isinstance(data["zeronet"], string_types):
|
||||
# {
|
||||
# "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9",
|
||||
# ....
|
||||
# }
|
||||
new_value[""] = data["zeronet"]
|
||||
if len(new_value) > 0:
|
||||
return processNameOp(domain, json.dumps({"zeronet": new_value}), test)
|
||||
else:
|
||||
return False
|
||||
if "zeronet" in data and isinstance(data["zeronet"], string_types):
|
||||
# {
|
||||
# "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9"
|
||||
# } is valid
|
||||
return processNameOp(domain, json.dumps({"zeronet": { "": data["zeronet"]}}), test)
|
||||
if not isinstance(data["zeronet"], dict):
|
||||
print("Not dict: ", data["zeronet"])
|
||||
return False
|
||||
if not re.match("^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$", domain):
|
||||
print("Invalid domain: ", domain)
|
||||
return False
|
||||
|
||||
if test:
|
||||
return True
|
||||
|
||||
if "slave" in sys.argv:
|
||||
print("Waiting for master update arrive")
|
||||
time.sleep(30) # Wait 30 sec to allow master updater
|
||||
|
||||
# Note: Requires the file data/names.json to exist and contain "{}" to work
|
||||
names_raw = open(names_path, "rb").read()
|
||||
names = json.loads(names_raw)
|
||||
for subdomain, address in data["zeronet"].items():
|
||||
subdomain = subdomain.lower()
|
||||
address = re.sub("[^A-Za-z0-9]", "", address)
|
||||
print(subdomain, domain, "->", address)
|
||||
if subdomain:
|
||||
if re.match("^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$", subdomain):
|
||||
names["%s.%s.bit" % (subdomain, domain)] = address
|
||||
else:
|
||||
print("Invalid subdomain:", domain, subdomain)
|
||||
else:
|
||||
names["%s.bit" % domain] = address
|
||||
|
||||
new_names_raw = json.dumps(names, indent=2, sort_keys=True)
|
||||
if new_names_raw != names_raw:
|
||||
open(names_path, "wb").write(new_names_raw)
|
||||
print("-", domain, "Changed")
|
||||
return True
|
||||
else:
|
||||
print("-", domain, "Not changed")
|
||||
return False
|
||||
|
||||
|
||||
def processBlock(block_id, test=False):
|
||||
print("Processing block #%s..." % block_id)
|
||||
s = time.time()
|
||||
block_hash = rpc.getblockhash(block_id)
|
||||
block = rpc.getblock(block_hash)
|
||||
|
||||
print("Checking %s tx" % len(block["tx"]))
|
||||
updated = 0
|
||||
for tx in block["tx"]:
|
||||
try:
|
||||
transaction = rpc.getrawtransaction(tx, 1)
|
||||
for vout in transaction.get("vout", []):
|
||||
if "scriptPubKey" in vout and "nameOp" in vout["scriptPubKey"] and "name" in vout["scriptPubKey"]["nameOp"]:
|
||||
name_op = vout["scriptPubKey"]["nameOp"]
|
||||
updated += processNameOp(name_op["name"].replace("d/", ""), name_op["value"], test)
|
||||
except Exception as err:
|
||||
print("Error processing tx #%s %s" % (tx, err))
|
||||
print("Done in %.3fs (updated %s)." % (time.time() - s, updated))
|
||||
return updated
|
||||
|
||||
# Connecting to RPC
|
||||
def initRpc(config):
|
||||
"""Initialize Namecoin RPC"""
|
||||
rpc_data = {
|
||||
'connect': '127.0.0.1',
|
||||
'port': '8336',
|
||||
'user': 'PLACEHOLDER',
|
||||
'password': 'PLACEHOLDER',
|
||||
'clienttimeout': '900'
|
||||
}
|
||||
try:
|
||||
fptr = open(config, 'r')
|
||||
lines = fptr.readlines()
|
||||
fptr.close()
|
||||
except:
|
||||
return None # Or take some other appropriate action
|
||||
|
||||
for line in lines:
|
||||
if not line.startswith('rpc'):
|
||||
continue
|
||||
key_val = line.split(None, 1)[0]
|
||||
(key, val) = key_val.split('=', 1)
|
||||
if not key or not val:
|
||||
continue
|
||||
rpc_data[key[3:]] = val
|
||||
|
||||
url = 'http://%(user)s:%(password)s@%(connect)s:%(port)s' % rpc_data
|
||||
|
||||
return url, int(rpc_data['clienttimeout'])
|
||||
|
||||
# Loading config...
|
||||
|
||||
# Check whether platform is on windows or linux
|
||||
# On linux namecoin is installed under ~/.namecoin, while on on windows it is in %appdata%/Namecoin
|
||||
|
||||
if sys.platform == "win32":
|
||||
namecoin_location = os.getenv('APPDATA') + "/Namecoin/"
|
||||
else:
|
||||
namecoin_location = os.path.expanduser("~/.namecoin/")
|
||||
|
||||
config_path = namecoin_location + 'zeroname_config.json'
|
||||
if not os.path.isfile(config_path): # Create sample config
|
||||
open(config_path, "w").write(
|
||||
json.dumps({'site': 'site', 'zeronet_path': '/home/zeronet', 'privatekey': '', 'lastprocessed': 223910}, indent=2)
|
||||
)
|
||||
print("* Example config written to %s" % config_path)
|
||||
sys.exit(0)
|
||||
|
||||
config = json.load(open(config_path))
|
||||
names_path = "%s/data/%s/data/names.json" % (config["zeronet_path"], config["site"])
|
||||
os.chdir(config["zeronet_path"]) # Change working dir - tells script where Zeronet install is.
|
||||
|
||||
# Parameters to sign and publish
|
||||
command_sign_publish = [sys.executable, "zeronet.py", "siteSign", config["site"], config["privatekey"], "--publish"]
|
||||
if sys.platform == 'win32':
|
||||
command_sign_publish = ['"%s"' % param for param in command_sign_publish]
|
||||
|
||||
# Initialize rpc connection
|
||||
rpc_auth, rpc_timeout = initRpc(namecoin_location + "namecoin.conf")
|
||||
rpc = AuthServiceProxy(rpc_auth, timeout=rpc_timeout)
|
||||
|
||||
node_version = rpc.getnetworkinfo()['version']
|
||||
|
||||
while 1:
|
||||
try:
|
||||
time.sleep(1)
|
||||
if node_version < 160000 :
|
||||
last_block = int(rpc.getinfo()["blocks"])
|
||||
else:
|
||||
last_block = int(rpc.getblockchaininfo()["blocks"])
|
||||
break # Connection succeeded
|
||||
except socket.timeout: # Timeout
|
||||
print(".", end=' ')
|
||||
sys.stdout.flush()
|
||||
except Exception as err:
|
||||
print("Exception", err.__class__, err)
|
||||
time.sleep(5)
|
||||
rpc = AuthServiceProxy(rpc_auth, timeout=rpc_timeout)
|
||||
|
||||
if not config["lastprocessed"]: # First startup: Start processing from last block
|
||||
config["lastprocessed"] = last_block
|
||||
|
||||
|
||||
print("- Testing domain parsing...")
|
||||
assert processBlock(223911, test=True) # Testing zeronetwork.bit
|
||||
assert processBlock(227052, test=True) # Testing brainwallets.bit
|
||||
assert not processBlock(236824, test=True) # Utf8 domain name (invalid should skip)
|
||||
assert not processBlock(236752, test=True) # Uppercase domain (invalid should skip)
|
||||
assert processBlock(236870, test=True) # Encoded domain (should pass)
|
||||
assert processBlock(438317, test=True) # Testing namecoin standard artifaxradio.bit (should pass)
|
||||
# sys.exit(0)
|
||||
|
||||
print("- Parsing skipped blocks...")
|
||||
should_publish = False
|
||||
for block_id in range(config["lastprocessed"], last_block + 1):
|
||||
if processBlock(block_id):
|
||||
should_publish = True
|
||||
config["lastprocessed"] = last_block
|
||||
|
||||
if should_publish:
|
||||
publish()
|
||||
|
||||
while 1:
|
||||
print("- Waiting for new block")
|
||||
sys.stdout.flush()
|
||||
while 1:
|
||||
try:
|
||||
time.sleep(1)
|
||||
if node_version < 160000 :
|
||||
rpc.waitforblock()
|
||||
else:
|
||||
rpc.waitfornewblock()
|
||||
print("Found")
|
||||
break # Block found
|
||||
except socket.timeout: # Timeout
|
||||
print(".", end=' ')
|
||||
sys.stdout.flush()
|
||||
except Exception as err:
|
||||
print("Exception", err.__class__, err)
|
||||
time.sleep(5)
|
||||
rpc = AuthServiceProxy(rpc_auth, timeout=rpc_timeout)
|
||||
|
||||
if node_version < 160000 :
|
||||
last_block = int(rpc.getinfo()["blocks"])
|
||||
else:
|
||||
last_block = int(rpc.getblockchaininfo()["blocks"])
|
||||
should_publish = False
|
||||
for block_id in range(config["lastprocessed"] + 1, last_block + 1):
|
||||
if processBlock(block_id):
|
||||
should_publish = True
|
||||
|
||||
config["lastprocessed"] = last_block
|
||||
open(config_path, "w").write(json.dumps(config, indent=2))
|
||||
|
||||
if should_publish:
|
||||
publish()
|
Loading…
Reference in New Issue