From 34211e6b0b9e0f47a1bfaa1048e8768270eccbf3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Krzysztof=20Otr=C4=99ba?= <krzotr@gmail.com>
Date: Fri, 12 Feb 2021 15:54:06 +0100
Subject: [PATCH 001/333] Polish translation

---
 plugins/Sidebar/languages/pl.json | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/plugins/Sidebar/languages/pl.json b/plugins/Sidebar/languages/pl.json
index 93268507..740aaafd 100644
--- a/plugins/Sidebar/languages/pl.json
+++ b/plugins/Sidebar/languages/pl.json
@@ -1,9 +1,13 @@
 {
+	"Total peers": "Łącznie użytkowników równorzędnych",
+	"Connected peers": "Łącznie połączonych użytkowników równorzędnych",
 	"Peers": "Użytkownicy równorzędni",
 	"Connected": "Połączony",
-	"Connectable": "Możliwy do podłączenia",
+	"Connectable": "Do połączenia",
 	"Connectable peers": "Połączeni użytkownicy równorzędni",
 
+	"Copy to clipboard": "Kopiuj do schowka",
+
 	"Data transfer": "Transfer danych",
 	"Received": "Odebrane",
 	"Received bytes": "Odebrany bajty",
@@ -11,7 +15,7 @@
 	"Sent bytes": "Wysłane bajty",
 
 	"Files": "Pliki",
-	"Total": "Sumarycznie",
+	"Total": "Łącznie",
 	"Image": "Obraz",
 	"Other": "Inne",
 	"User data": "Dane użytkownika",
@@ -36,6 +40,10 @@
 
 	"Identity address": "Adres identyfikacyjny",
 	"Change": "Zmień",
+	"Needs to be updated": "Muszą zostać zaktualizowane",
+	"Download previous files": "Pobierz wszystkie pliki",
+	"Help distribute added optional files": "Pomóż rozpowszechniać wszystkie pliki",
+	"Auto download big file size limit": "Limit dla automatycznego pobierania dużych plików",
 
 	"Site control": "Kontrola strony",
 	"Update": "Zaktualizuj",
@@ -56,11 +64,15 @@
 	"Site title": "Tytuł strony",
 	"Site description": "Opis strony",
 	"Save site settings": "Zapisz ustawienia strony",
+	"Save as .zip": "Zapisz jako .zip",
+	"Browse files": "Przeglądaj pliki",
 
+	"Add saved private key": "Dodaj klucz prywatny",
 	"Content publishing": "Publikowanie treści",
 	"Choose": "Wybierz",
 	"Sign": "Podpisz",
 	"Publish": "Opublikuj",
+	"Sign and publish": "Zapisz i opublikuj",
 
 	"This function is disabled on this proxy": "Ta funkcja jest zablokowana w tym proxy",
 	"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Błąd ściągania bazy danych GeoLite2 City: {}!<br>Proszę ściągnąć ją recznie i wypakować do katalogu danych:<br>{}",

From 785a02ce949420494338f7ecb7ce4ced8b62bdf3 Mon Sep 17 00:00:00 2001
From: sgmoore <git-sgmoore@users.noreply.github.com>
Date: Sun, 12 Dec 2021 22:22:23 -0800
Subject: [PATCH 002/333] Minor grammar fix

Minor grammar fix at line 120
---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index d8e36a71..3d8f5e71 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/
 
 ## Why?
 
-* We believe in open, free, and uncensored network and communication.
+* We believe in open, free, and uncensored networks and communication.
 * No single point of failure: Site remains online so long as at least 1 peer is
   serving it.
 * No hosting costs: Sites are served by visitors.
@@ -117,7 +117,7 @@ There is an official image, built from source at: https://hub.docker.com/r/nofis
  * Click on **⋮** > **"Create new, empty site"** menu item on the site [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D).
  * You will be **redirected** to a completely new site that is only modifiable by you!
  * You can find and modify your site's content in **data/[yoursiteaddress]** directory
- * After the modifications open your site, drag the topright "0" button to left, then press **sign** and **publish** buttons on the bottom
+ * After the modifications open your site, drag the topright "0" button to the left, then press **sign** and **publish** buttons on the bottom
 
 Next steps: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_development/getting_started/)
 

From 987be85e42c143ea89584ff5bba1d605e45501c9 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 21 Dec 2021 08:58:46 +0000
Subject: [PATCH 003/333] codeql-analysis

let's try how this goes
---
 .github/workflows/codeql-analysis.yml | 70 +++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)
 create mode 100644 .github/workflows/codeql-analysis.yml

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 00000000..66a64b3a
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,70 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ py3 ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ py3 ]
+  schedule:
+    - cron: '37 18 * * 5'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'javascript', 'python' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+        # Learn more about CodeQL language support at https://git.io/codeql-language-support
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v2
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v1
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v1
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 https://git.io/JvXDl
+
+    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+    #    and modify them (or add more) to build your code if your project
+    #    uses a compiled language
+
+    #- run: |
+    #   make bootstrap
+    #   make release
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v1

From be00a7e8555519b307a6bf25cee0c570adb62e3c Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 21 Dec 2021 09:54:18 +0000
Subject: [PATCH 004/333] Add ed25519.py from torproject (onion-v3 support)

57364fae7269ec562c5fc8cdb073ff9463d9a0f0
https://gitweb.torproject.org/stem.git/commit/stem/util/ed25519.py?id=57364fae7269ec562c5fc8cdb073ff9463d9a0f0
---
 src/Crypt/ed25519.py | 292 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 292 insertions(+)
 create mode 100644 src/Crypt/ed25519.py

diff --git a/src/Crypt/ed25519.py b/src/Crypt/ed25519.py
new file mode 100644
index 00000000..7c0161dc
--- /dev/null
+++ b/src/Crypt/ed25519.py
@@ -0,0 +1,292 @@
+# The following is copied from...
+#
+#   https://github.com/pyca/ed25519
+#
+# This is under the CC0 license. For more information please see...
+#
+#   https://github.com/pyca/cryptography/issues/5068
+
+
+# ed25519.py - Optimized version of the reference implementation of Ed25519
+#
+# Written in 2011? by Daniel J. Bernstein <djb@cr.yp.to>
+#            2013 by Donald Stufft <donald@stufft.io>
+#            2013 by Alex Gaynor <alex.gaynor@gmail.com>
+#            2013 by Greg Price <price@mit.edu>
+#
+# To the extent possible under law, the author(s) have dedicated all copyright
+# and related and neighboring rights to this software to the public domain
+# worldwide. This software is distributed without any warranty.
+#
+# You should have received a copy of the CC0 Public Domain Dedication along
+# with this software. If not, see
+# <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+"""
+NB: This code is not safe for use with secret keys or secret data.
+The only safe use of this code is for verifying signatures on public messages.
+
+Functions for computing the public key of a secret key and for signing
+a message are included, namely publickey_unsafe and signature_unsafe,
+for testing purposes only.
+
+The root of the problem is that Python's long-integer arithmetic is
+not designed for use in cryptography.  Specifically, it may take more
+or less time to execute an operation depending on the values of the
+inputs, and its memory access patterns may also depend on the inputs.
+This opens it to timing and cache side-channel attacks which can
+disclose data to an attacker.  We rely on Python's long-integer
+arithmetic, so we cannot handle secrets without risking their disclosure.
+"""
+
+import hashlib
+import operator
+
+
+__version__ = "1.0.dev0"
+
+b = 256
+q = 2 ** 255 - 19
+l = 2 ** 252 + 27742317777372353535851937790883648493
+int2byte = operator.methodcaller("to_bytes", 1, "big")
+
+
+def H(m):
+    return hashlib.sha512(m).digest()
+
+
+def pow2(x, p):
+    """== pow(x, 2**p, q)"""
+    while p > 0:
+        x = x * x % q
+        p -= 1
+    return x
+
+
+def inv(z):
+    """$= z^{-1} \mod q$, for z != 0"""
+    # Adapted from curve25519_athlon.c in djb's Curve25519.
+    z2 = z * z % q                                # 2
+    z9 = pow2(z2, 2) * z % q                      # 9
+    z11 = z9 * z2 % q                             # 11
+    z2_5_0 = (z11 * z11) % q * z9 % q             # 31 == 2^5 - 2^0
+    z2_10_0 = pow2(z2_5_0, 5) * z2_5_0 % q        # 2^10 - 2^0
+    z2_20_0 = pow2(z2_10_0, 10) * z2_10_0 % q     # ...
+    z2_40_0 = pow2(z2_20_0, 20) * z2_20_0 % q
+    z2_50_0 = pow2(z2_40_0, 10) * z2_10_0 % q
+    z2_100_0 = pow2(z2_50_0, 50) * z2_50_0 % q
+    z2_200_0 = pow2(z2_100_0, 100) * z2_100_0 % q
+    z2_250_0 = pow2(z2_200_0, 50) * z2_50_0 % q   # 2^250 - 2^0
+    return pow2(z2_250_0, 5) * z11 % q            # 2^255 - 2^5 + 11 = q - 2
+
+
+d = -121665 * inv(121666) % q
+I = pow(2, (q - 1) // 4, q)
+
+
+def xrecover(y):
+    xx = (y * y - 1) * inv(d * y * y + 1)
+    x = pow(xx, (q + 3) // 8, q)
+
+    if (x * x - xx) % q != 0:
+        x = (x * I) % q
+
+    if x % 2 != 0:
+        x = q-x
+
+    return x
+
+
+By = 4 * inv(5)
+Bx = xrecover(By)
+B = (Bx % q, By % q, 1, (Bx * By) % q)
+ident = (0, 1, 1, 0)
+
+
+def edwards_add(P, Q):
+    # This is formula sequence 'addition-add-2008-hwcd-3' from
+    # http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
+    (x1, y1, z1, t1) = P
+    (x2, y2, z2, t2) = Q
+
+    a = (y1-x1)*(y2-x2) % q
+    b = (y1+x1)*(y2+x2) % q
+    c = t1*2*d*t2 % q
+    dd = z1*2*z2 % q
+    e = b - a
+    f = dd - c
+    g = dd + c
+    h = b + a
+    x3 = e*f
+    y3 = g*h
+    t3 = e*h
+    z3 = f*g
+
+    return (x3 % q, y3 % q, z3 % q, t3 % q)
+
+
+def edwards_double(P):
+    # This is formula sequence 'dbl-2008-hwcd' from
+    # http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
+    (x1, y1, z1, t1) = P
+
+    a = x1*x1 % q
+    b = y1*y1 % q
+    c = 2*z1*z1 % q
+    # dd = -a
+    e = ((x1+y1)*(x1+y1) - a - b) % q
+    g = -a + b  # dd + b
+    f = g - c
+    h = -a - b  # dd - b
+    x3 = e*f
+    y3 = g*h
+    t3 = e*h
+    z3 = f*g
+
+    return (x3 % q, y3 % q, z3 % q, t3 % q)
+
+
+def scalarmult(P, e):
+    if e == 0:
+        return ident
+    Q = scalarmult(P, e // 2)
+    Q = edwards_double(Q)
+    if e & 1:
+        Q = edwards_add(Q, P)
+    return Q
+
+
+# Bpow[i] == scalarmult(B, 2**i)
+Bpow = []
+
+
+def make_Bpow():
+    P = B
+    for i in range(253):
+        Bpow.append(P)
+        P = edwards_double(P)
+make_Bpow()
+
+
+def scalarmult_B(e):
+    """
+    Implements scalarmult(B, e) more efficiently.
+    """
+    # scalarmult(B, l) is the identity
+    e = e % l
+    P = ident
+    for i in range(253):
+        if e & 1:
+            P = edwards_add(P, Bpow[i])
+        e = e // 2
+    assert e == 0, e
+    return P
+
+
+def encodeint(y):
+    bits = [(y >> i) & 1 for i in range(b)]
+    return b''.join([
+        int2byte(sum([bits[i * 8 + j] << j for j in range(8)]))
+        for i in range(b//8)
+    ])
+
+
+def encodepoint(P):
+    (x, y, z, t) = P
+    zi = inv(z)
+    x = (x * zi) % q
+    y = (y * zi) % q
+    bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1]
+    return b''.join([
+        int2byte(sum([bits[i * 8 + j] << j for j in range(8)]))
+        for i in range(b // 8)
+    ])
+
+
+def bit(h, i):
+    return (operator.getitem(h, i // 8) >> (i % 8)) & 1
+
+
+def publickey_unsafe(sk):
+    """
+    Not safe to use with secret keys or secret data.
+
+    See module docstring.  This function should be used for testing only.
+    """
+    h = H(sk)
+    a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
+    A = scalarmult_B(a)
+    return encodepoint(A)
+
+
+def Hint(m):
+    h = H(m)
+    return sum(2 ** i * bit(h, i) for i in range(2 * b))
+
+
+def signature_unsafe(m, sk, pk):
+    """
+    Not safe to use with secret keys or secret data.
+
+    See module docstring.  This function should be used for testing only.
+    """
+    h = H(sk)
+    a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
+    r = Hint(
+        bytes([operator.getitem(h, j) for j in range(b // 8, b // 4)]) + m
+    )
+    R = scalarmult_B(r)
+    S = (r + Hint(encodepoint(R) + pk + m) * a) % l
+    return encodepoint(R) + encodeint(S)
+
+
+def isoncurve(P):
+    (x, y, z, t) = P
+    return (z % q != 0 and
+            x*y % q == z*t % q and
+            (y*y - x*x - z*z - d*t*t) % q == 0)
+
+
+def decodeint(s):
+    return sum(2 ** i * bit(s, i) for i in range(0, b))
+
+
+def decodepoint(s):
+    y = sum(2 ** i * bit(s, i) for i in range(0, b - 1))
+    x = xrecover(y)
+    if x & 1 != bit(s, b-1):
+        x = q - x
+    P = (x, y, 1, (x*y) % q)
+    if not isoncurve(P):
+        raise ValueError("decoding point that is not on curve")
+    return P
+
+
+class SignatureMismatch(Exception):
+    pass
+
+
+def checkvalid(s, m, pk):
+    """
+    Not safe to use when any argument is secret.
+
+    See module docstring.  This function should be used only for
+    verifying public signatures of public messages.
+    """
+    if len(s) != b // 4:
+        raise ValueError("signature length is wrong")
+
+    if len(pk) != b // 8:
+        raise ValueError("public-key length is wrong")
+
+    R = decodepoint(s[:b // 8])
+    A = decodepoint(pk)
+    S = decodeint(s[b // 8:b // 4])
+    h = Hint(encodepoint(R) + pk + m)
+
+    (x1, y1, z1, t1) = P = scalarmult_B(S)
+    (x2, y2, z2, t2) = Q = edwards_add(R, scalarmult(A, h))
+
+    if (not isoncurve(P) or not isoncurve(Q) or
+       (x1*z2 - x2*z1) % q != 0 or (y1*z2 - y2*z1) % q != 0):
+        raise SignatureMismatch("signature does not pass verification")

From acb313f4813da4a88adf21733c28ee7e711b1e81 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 22 Dec 2021 14:41:20 +0000
Subject: [PATCH 005/333] onionv3 support WIP

thanks to @anonymoose, @zeroseed and @geekless
---
 src/Crypt/CryptRsa.py | 40 +++++++++++++++++++++++++++++++---------
 src/Tor/TorManager.py |  5 +++--
 2 files changed, 34 insertions(+), 11 deletions(-)

diff --git a/src/Crypt/CryptRsa.py b/src/Crypt/CryptRsa.py
index 494c4d24..65a42f85 100644
--- a/src/Crypt/CryptRsa.py
+++ b/src/Crypt/CryptRsa.py
@@ -1,31 +1,49 @@
 import base64
 import hashlib
+import Crypt.ed25519 as ed25519
+import rsa
 
 def sign(data, privatekey):
-    import rsa
-    from rsa import pkcs1
+    # !ONION v3!
+    if len(privatekey) == 88:
+        prv_key = base64.b64decode(privatekey)
+        pub_key = ed25519.publickey_unsafe(prv_key)
+        signed = ed25519.signature_unsafe(data, prv_key, pub_key)
+        return signed
 
+    # FIXME: doesn't look good
     if "BEGIN RSA PRIVATE KEY" not in privatekey:
         privatekey = "-----BEGIN RSA PRIVATE KEY-----\n%s\n-----END RSA PRIVATE KEY-----" % privatekey
 
     priv = rsa.PrivateKey.load_pkcs1(privatekey)
-    sign = rsa.pkcs1.sign(data, priv, 'SHA-256')
-    return sign
+    signed = rsa.pkcs1.sign(data, priv, 'SHA-256')
+    return signed
 
 def verify(data, publickey, sign):
-    import rsa
-    from rsa import pkcs1
+    # !ONION v3!
+    if len(publickey) == 32:
+        try:
+            valid = CryptEd25519.checkvalid(sign, data, publickey) 
+            valid = 'SHA-256'
+        except Exception as err:
+            # TODO: traceback
+            print(err)
+            valid = False
+        return valid
 
     pub = rsa.PublicKey.load_pkcs1(publickey, format="DER")
     try:
         valid = rsa.pkcs1.verify(data, sign, pub)
-    except pkcs1.VerificationError:
+    except rsa.pkcs1.VerificationError:
         valid = False
     return valid
 
 def privatekeyToPublickey(privatekey):
-    import rsa
-    from rsa import pkcs1
+    # !ONION v3!
+    if len(privatekey) == 88:
+        prv_key = base64.b64decode(privatekey)
+        pub_key = ed25519.publickey_unsafe(prv_key)
+        return pub_key
 
     if "BEGIN RSA PRIVATE KEY" not in privatekey:
         privatekey = "-----BEGIN RSA PRIVATE KEY-----\n%s\n-----END RSA PRIVATE KEY-----" % privatekey
@@ -35,4 +53,8 @@ def privatekeyToPublickey(privatekey):
     return pub.save_pkcs1("DER")
 
 def publickeyToOnion(publickey):
+    # !ONION v3!
+    if len(publickey) == 32:
+        addr = ed25519.publickey_to_onionaddress(publickey)[:-6]
+        return addr
     return base64.b32encode(hashlib.sha1(publickey).digest()[:10]).lower().decode("ascii")
diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py
index 7e5c8bb0..2fbca8c9 100644
--- a/src/Tor/TorManager.py
+++ b/src/Tor/TorManager.py
@@ -13,6 +13,7 @@ import gevent
 
 from Config import config
 from Crypt import CryptRsa
+from Crypt import ed25519
 from Site import SiteManager
 import socks
 from gevent.lock import RLock
@@ -214,8 +215,8 @@ class TorManager(object):
             return False
 
     def makeOnionAndKey(self):
-        res = self.request("ADD_ONION NEW:RSA1024 port=%s" % self.fileserver_port)
-        match = re.search("ServiceID=([A-Za-z0-9]+).*PrivateKey=RSA1024:(.*?)[\r\n]", res, re.DOTALL)
+        res = self.request(f"ADD_ONION NEW:ED25519-V3 port={self.fileserver_port}")
+        match = re.search("ServiceID=([A-Za-z0-9]+).*PrivateKey=ED25519-V3:(.*?)[\r\n]", res, re.DOTALL)
         if match:
             onion_address, onion_privatekey = match.groups()
             return (onion_address, onion_privatekey)

From 8dc589b98ed37414cfcf36c05bee215093734d8e Mon Sep 17 00:00:00 2001
From: Vadim Ushakov <igeekless@gmail.com>
Date: Wed, 20 Oct 2021 16:28:28 +0700
Subject: [PATCH 006/333] Add default onion v3 tracker addresses

---
 src/Config.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/Config.py b/src/Config.py
index 7095975b..f66af1fc 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -80,6 +80,17 @@ class Config(object):
     # Create command line arguments
     def createArguments(self):
         trackers = [
+            # by zeroseed at http://127.0.0.1:43110/19HKdTAeBh5nRiKn791czY7TwRB1QNrf1Q/?:users/1HvNGwHKqhj3ZMEM53tz6jbdqe4LRpanEu:zn:dc17f896-bf3f-4962-bdd4-0a470040c9c5
+            "zero://k5w77dozo3hy5zualyhni6vrh73iwfkaofa64abbilwyhhd3wgenbjqd.onion:15441",
+            "zero://2kcb2fqesyaevc4lntogupa4mkdssth2ypfwczd2ov5a3zo6ytwwbayd.onion:15441",
+            "zero://my562dxpjropcd5hy3nd5pemsc4aavbiptci5amwxzbelmzgkkuxpvid.onion:15441",
+            "zero://pn4q2zzt2pw4nk7yidxvsxmydko7dfibuzxdswi6gu6ninjpofvqs2id.onion:15441",
+            "zero://6i54dd5th73oelv636ivix6sjnwfgk2qsltnyvswagwphub375t3xcad.onion:15441",
+            "zero://tl74auz4tyqv4bieeclmyoe4uwtoc2dj7fdqv4nc4gl5j2bwg2r26bqd.onion:15441",
+            "zero://wlxav3szbrdhest4j7dib2vgbrd7uj7u7rnuzg22cxbih7yxyg2hsmid.onion:15441",
+            "zero://zy7wttvjtsijt5uwmlar4yguvjc2gppzbdj4v6bujng6xwjmkdg7uvqd.onion:15441",
+
+            # ZeroNet 0.7.2 defaults:
             "zero://boot3rdez4rzn36x.onion:15441",
             "zero://zero.booth.moe#f36ca555bee6ba216b14d10f38c16f7769ff064e0e37d887603548cc2e64191d:443",  # US/NY
             "udp://tracker.coppersurfer.tk:6969",  # DE

From 1075b5884285d5db659a6b9a184188f82522792b Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 22 Dec 2021 18:14:02 +0000
Subject: [PATCH 007/333] zeronet-conservancy readme

---
 README.md | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index d8e36a71..b72a0f79 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,14 @@
-# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=py3)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/) ![tests](https://github.com/HelloZeroNet/ZeroNet/workflows/tests/badge.svg) [![Docker Pulls](https://img.shields.io/docker/pulls/nofish/zeronet)](https://hub.docker.com/r/nofish/zeronet)
+# zeronet-conservancy
 
-Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.io / [onion](http://zeronet34m3r5ngdu54uj57dcafpgdjhxsgq5kla5con4qvcmfzpvhad.onion)
+This is a minimalist conservative fork of [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) with tor onion-v3 support (and perhaps more essential/security fixes in the future)
 
+## Why fork?
 
-## Why?
+We need a fork that works with onion-v3 and doesn't depend on trust to one or two people. We need it now. This fork implements minimal changes to [ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3) branch which are easy to audit yourself.
+
+This fork is intended as temporary measure and will possibly stop being maintained after its original author established there is an alternative active trust-worthy fork.
+
+## Why 0net?
 
 * We believe in open, free, and uncensored network and communication.
 * No single point of failure: Site remains online so long as at least 1 peer is

From 3f904b80966079300a5eff3a99efca996593dbe4 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 22 Dec 2021 18:27:33 +0000
Subject: [PATCH 008/333] more zeronet-conservancy readme updates

---
 README.md | 29 +++++++++++++++++++++--------
 1 file changed, 21 insertions(+), 8 deletions(-)

diff --git a/README.md b/README.md
index b72a0f79..f8cffbd1 100644
--- a/README.md
+++ b/README.md
@@ -126,17 +126,30 @@ There is an official image, built from source at: https://hub.docker.com/r/nofis
 
 Next steps: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_development/getting_started/)
 
-## Help keep this project alive
+## Help this project stay alive
 
-- Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX
-- Paypal: https://zeronet.io/docs/help_zeronet/donate/
+### Become a maintainer
 
-### Sponsors
+We need more maintainers! Become one today! Seriously, there's not going to be
+that much new code to audit and auditing new code is the only requirement.
 
-* Better macOS/Safari compatibility made possible by [BrowserStack.com](https://www.browserstack.com)
+### Use it and spread the word
+
+Make sure to tell people why do you use 0net and this fork in particular! People
+need to know their alternatives.
+
+### Financially support maintainers
+
+Currently the only maintainer of this fork is @caryoscelus. You can see ways to
+donate to them on https://caryoscelus.github.io/donate/
+
+If you want to make sure your donation is recognized as donation for this
+project, there is a dedicated bitcoin address for that, too:
+1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6
+
+If you want to donate in a different way, feel free to contact maintainer or
+create an issue
 
 #### Thank you!
 
-* More info, help, changelog, zeronet sites: https://www.reddit.com/r/zeronet/
-* Come, chat with us: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) or on [gitter](https://gitter.im/HelloZeroNet/ZeroNet)
-* Email: hello@zeronet.io (PGP: [960F FF2D 6C14 5AA6 13E8 491B 5B63 BAE6 CB96 13AE](https://zeronet.io/files/tamas@zeronet.io_pub.asc))
+* Generic zeronet subreddit: https://www.reddit.com/r/zeronet/

From 87988a4b85ca9c0c945c73c33d651d339d07603b Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 22 Dec 2021 18:31:40 +0000
Subject: [PATCH 009/333] fix install instructions

---
 README.md | 39 ++++-----------------------------------
 1 file changed, 4 insertions(+), 35 deletions(-)

diff --git a/README.md b/README.md
index f8cffbd1..ef057b00 100644
--- a/README.md
+++ b/README.md
@@ -66,49 +66,18 @@ This fork is intended as temporary measure and will possibly stop being maintain
 
 ## How to join
 
-### Windows
-
- - Download [ZeroNet-py3-win64.zip](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist-win64/ZeroNet-py3-win64.zip) (18MB)
- - Unpack anywhere
- - Run `ZeroNet.exe`
- 
-### macOS
-
- - Download [ZeroNet-dist-mac.zip](https://github.com/HelloZeroNet/ZeroNet-dist/archive/mac/ZeroNet-dist-mac.zip) (13.2MB)
- - Unpack anywhere
- - Run `ZeroNet.app`
- 
-### Linux (x86-64bit)
- - `wget https://github.com/HelloZeroNet/ZeroNet-linux/archive/dist-linux64/ZeroNet-py3-linux64.tar.gz`
- - `tar xvpfz ZeroNet-py3-linux64.tar.gz`
- - `cd ZeroNet-linux-dist-linux64/`
- - Start with: `./ZeroNet.sh`
- - Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
- 
- __Tip:__ Start with `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address` to allow remote connections on the web interface.
- 
- ### Android (arm, arm64, x86)
- - minimum Android version supported 16 (JellyBean)
- - [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" 
-      alt="Download from Google Play" 
-      height="80">](https://play.google.com/store/apps/details?id=in.canews.zeronetmobile)
- - APK download: https://github.com/canewsin/zeronet_mobile/releases
- - XDA Labs: https://labs.xda-developers.com/store/app/in.canews.zeronet
- 
-#### Docker
-There is an official image, built from source at: https://hub.docker.com/r/nofish/zeronet/
-
 ### Install from source
 
- - `wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz`
- - `tar xvpfz ZeroNet-py3.tar.gz`
- - `cd ZeroNet-py3`
+ - clone this repo
+ - install py3 and pip if needed (following instruction are for apt-based distributives)
  - `sudo apt-get update`
  - `sudo apt-get install python3-pip`
  - `sudo python3 -m pip install -r requirements.txt`
  - Start with: `python3 zeronet.py`
  - Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
 
+It is recommended to use python environments instead of installing all dependencies globally (TODO: find/write up the detailed instructions)
+
 ## Current limitations
 
 * ~~No torrent-like file splitting for big file support~~ (big file support added)

From cbc2c67b342a9929fe1cc08f721355863426dcc4 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 24 Dec 2021 14:50:07 +0000
Subject: [PATCH 010/333] fix typo in onionv3 support

thanks @anonymoose
---
 src/Crypt/CryptRsa.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Crypt/CryptRsa.py b/src/Crypt/CryptRsa.py
index 65a42f85..1a9fffa9 100644
--- a/src/Crypt/CryptRsa.py
+++ b/src/Crypt/CryptRsa.py
@@ -23,7 +23,7 @@ def verify(data, publickey, sign):
     # !ONION v3!
     if len(publickey) == 32:
         try:
-            valid = CryptEd25519.checkvalid(sign, data, publickey) 
+            valid = ed25519.checkvalid(sign, data, publickey)
             valid = 'SHA-256'
         except Exception as err:
             # TODO: traceback

From 023ec7706f6f4959afb7a5790b5beaa41f8e3d4b Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 25 Dec 2021 18:19:07 +0000
Subject: [PATCH 011/333] onion-v3 support fix (partial)

thanks @anonymoose
---
 src/Crypt/CryptRsa.py | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/src/Crypt/CryptRsa.py b/src/Crypt/CryptRsa.py
index 1a9fffa9..16e3ee2c 100644
--- a/src/Crypt/CryptRsa.py
+++ b/src/Crypt/CryptRsa.py
@@ -52,9 +52,17 @@ def privatekeyToPublickey(privatekey):
     pub = rsa.PublicKey(priv.n, priv.e)
     return pub.save_pkcs1("DER")
 
+# adopted by @anonymoose & @caryoscelus from https://gitweb.torproject.org/stem.git/tree/stem/descriptor/hidden_service.py @ address_from_identity_key
+def publickeyToOnionV3Address(key):
+     CHECKSUM_CONSTANT = b'.onion checksum'
+     version = b'\x03' # v3
+     checksum = hashlib.sha3_256(CHECKSUM_CONSTANT + key + version).digest()[:2]
+     onion_address = base64.b32encode(key + checksum + version)
+     return onion_address.decode('utf-8', 'replace').lower()
+
 def publickeyToOnion(publickey):
-    # !ONION v3!
     if len(publickey) == 32:
-        addr = ed25519.publickey_to_onionaddress(publickey)[:-6]
-        return addr
-    return base64.b32encode(hashlib.sha1(publickey).digest()[:10]).lower().decode("ascii")
+        # !ONION v3!
+        return publickeyToOnionV3Address(publickey)
+    else:
+        return base64.b32encode(hashlib.sha1(publickey).digest()[:10]).lower().decode("ascii")

From a945b00b6de3e4f409e67321a8dc10a3102ad71e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 28 Dec 2021 17:35:32 +0000
Subject: [PATCH 012/333] improve install instruction

thanks to @mitya57
---
 README.md | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index ef057b00..dfa4be01 100644
--- a/README.md
+++ b/README.md
@@ -69,10 +69,10 @@ This fork is intended as temporary measure and will possibly stop being maintain
 ### Install from source
 
  - clone this repo
- - install py3 and pip if needed (following instruction are for apt-based distributives)
- - `sudo apt-get update`
- - `sudo apt-get install python3-pip`
- - `sudo python3 -m pip install -r requirements.txt`
+ - install python3 and pip if needed (following instruction are for apt-based distributives)
+   - `sudo apt-get update`
+   - `sudo apt-get install python3-pip`
+ - `python3 -m pip install -r requirements.txt`
  - Start with: `python3 zeronet.py`
  - Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
 

From b044139a4d225697b4ad00604f39e8a334d9a37d Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 28 Dec 2021 17:45:49 +0000
Subject: [PATCH 013/333] minor README.md update

fix grammar

Co-authored-by: Dmitry Shachnev <mitya57@users.noreply.github.com>
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index dfa4be01..d52da2d4 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@ This fork is intended as temporary measure and will possibly stop being maintain
 ### Install from source
 
  - clone this repo
- - install python3 and pip if needed (following instruction are for apt-based distributives)
+ - install python3 and pip if needed (the following instructions are for apt-based distributions)
    - `sudo apt-get update`
    - `sudo apt-get install python3-pip`
  - `python3 -m pip install -r requirements.txt`

From a08c1e1825e5368f83c361dd0c875ec72d74a5a5 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 28 Dec 2021 17:48:08 +0000
Subject: [PATCH 014/333] minor README improvement

apt-get -> apt
---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index d52da2d4..82400148 100644
--- a/README.md
+++ b/README.md
@@ -70,8 +70,8 @@ This fork is intended as temporary measure and will possibly stop being maintain
 
  - clone this repo
  - install python3 and pip if needed (the following instructions are for apt-based distributions)
-   - `sudo apt-get update`
-   - `sudo apt-get install python3-pip`
+   - `sudo apt update`
+   - `sudo apt install python3-pip`
  - `python3 -m pip install -r requirements.txt`
  - Start with: `python3 zeronet.py`
  - Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/

From 0577589197a0f50fe72875e396395c5690d200fd Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 28 Dec 2021 21:17:45 +0000
Subject: [PATCH 015/333] further README update

---
 README.md | 31 +++++++++++++------------------
 1 file changed, 13 insertions(+), 18 deletions(-)

diff --git a/README.md b/README.md
index 82400148..990dac08 100644
--- a/README.md
+++ b/README.md
@@ -21,15 +21,12 @@ This fork is intended as temporary measure and will possibly stop being maintain
 
 ## Features
  * Real-time updated sites
- * Namecoin .bit domains support
- * Easy to setup: unpack & run
  * Clone websites in one click
- * Password-less [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
-   based authorization: Your account is protected by the same cryptography as your Bitcoin wallet
- * Built-in SQL server with P2P data synchronization: Allows easier site development and faster page load times
- * Anonymity: Full Tor network support with .onion hidden services instead of IPv4 addresses
- * TLS encrypted connections
- * Automatic uPnP port opening
+ * Password-less authorization using private/public keys
+ * Built-in SQL server with P2P data synchronization: allows easier dynamic site development
+ * Anonymity: Tor network support with .onion hidden services (including onion-v3 support)
+ * TLS encrypted connections (through clearnet)
+ * Automatic uPnP port opening (if opted in)
  * Plugin for multiuser (openproxy) support
  * Works with any browser/OS
 
@@ -50,10 +47,11 @@ This fork is intended as temporary measure and will possibly stop being maintain
   signature), they download the modified files and publish the new content to
   other peers.
 
-####  [Slideshow about ZeroNet cryptography, site updates, multi-user sites »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
-####  [Frequently asked questions »](https://zeronet.io/docs/faq/)
+Following links relate to original ZeroNet:
 
-####  [ZeroNet Developer Documentation »](https://zeronet.io/docs/site_development/getting_started/)
+- [Slideshow about ZeroNet cryptography, site updates, multi-user sites »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
+- [Frequently asked questions »](https://zeronet.io/docs/faq/)
+- [ZeroNet Developer Documentation »](https://zeronet.io/docs/site_development/getting_started/)
 
 
 ## Screenshots
@@ -80,10 +78,11 @@ It is recommended to use python environments instead of installing all dependenc
 
 ## Current limitations
 
-* ~~No torrent-like file splitting for big file support~~ (big file support added)
-* ~~No more anonymous than Bittorrent~~ (built-in full Tor support added)
-* File transactions are not compressed ~~or encrypted yet~~ (TLS encryption added)
+* File transactions are not compressed
 * No private sites
+* No DHT support
+* Centralized elements like zeroid
+* No reliable spam protection
 
 
 ## How can I create a ZeroNet site?
@@ -118,7 +117,3 @@ project, there is a dedicated bitcoin address for that, too:
 
 If you want to donate in a different way, feel free to contact maintainer or
 create an issue
-
-#### Thank you!
-
-* Generic zeronet subreddit: https://www.reddit.com/r/zeronet/

From 296e0dbb57d6e8a24db3e03f12829710b17fc917 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 5 Jan 2022 17:32:41 +0000
Subject: [PATCH 016/333] partial russian readme update

refs #8
---
 README-ru.md | 158 +++++++++++++++++----------------------------------
 1 file changed, 53 insertions(+), 105 deletions(-)

diff --git a/README-ru.md b/README-ru.md
index 75abbfab..d20828ef 100644
--- a/README-ru.md
+++ b/README-ru.md
@@ -1,14 +1,29 @@
-# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=master)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/)
-
-[简体中文](./README-zh-cn.md)
 [English](./README.md)
+[简体中文](./README-zh-cn.md)
 
-Децентрализованные вебсайты использующие Bitcoin криптографию и BitTorrent сеть - https://zeronet.io
+(ОСТОРОЖНО: русскоязычная версия README может быть не полностью обновлена)
 
+# zeronet-conservancy
+
+Минималистичный форк [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) с
+поддержкой onion-v3 tor (и возможных других необходимых фиксов и закрытий
+уязвимостей)
+
+## Зачем форк?
+
+Нам нужен форк работающий с onion-v3 и не зависящий от доверия к одному или двум
+личностям. Этот форк нужен прямо сейчас. Данный форк представляет из себя
+минимальный [сет изменений по сравнению с последним коммитом
+ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/compare/py3...zeronet-conservancy:master)
+, их легко проверить самостоятельно.
+
+Этот форк является временной мерой и может закончиться, если/когда автор сего
+форка решит, что существует альтернативный, активный, заслуживающий доверия
+форк.
 
 ## Зачем?
 
-* Мы верим в открытую, свободную, и не отцензуренную сеть и коммуникацию.
+* Мы верим в открытую, свободную, и не поддающуюся цензуре сеть и коммуникацию.
 * Нет единой точки отказа: Сайт онлайн пока по крайней мере 1 пир обслуживает его.
 * Никаких затрат на хостинг: Сайты обслуживаются посетителями.
 * Невозможно отключить: Он нигде, потому что он везде.
@@ -17,15 +32,12 @@
 
 ## Особенности
  * Обновляемые в реальном времени сайты
- * Поддержка Namecoin .bit доменов
- * Лёгок в установке: распаковал & запустил
  * Клонирование вебсайтов в один клик
- * Password-less [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
-   based authorization: Ваша учетная запись защищена той же криптографией, что и ваш Bitcoin-кошелек
- * Встроенный SQL-сервер с синхронизацией данных P2P: Позволяет упростить разработку сайта и ускорить загрузку страницы
- * Анонимность: Полная поддержка сети Tor с помощью скрытых служб .onion вместо адресов IPv4
- * TLS зашифрованные связи
- * Автоматическое открытие uPnP порта
+ * Авторизация без паролей, с использованием пары публичный/приватный ключ
+ * Встроенный SQL-сервер с синхронизацией данных P2P: позволяет упростить разработку сайта
+ * Анонимность: поддержка сети Tor с помощью скрытых служб .onion (включая onion-v3)
+ * TLS зашифрованные связи (в клирнете)
+ * Автоматическое открытие uPnP порта (опционально)
  * Плагин для поддержки многопользовательской (openproxy)
  * Работает с любыми браузерами и операционными системами
 
@@ -60,95 +72,23 @@
 
 ## Как вступить
 
-* Скачайте ZeroBundle пакет:
-  * [Microsoft Windows](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist/ZeroNet-win.zip)
-  * [Apple macOS](https://github.com/HelloZeroNet/ZeroNet-mac/archive/dist/ZeroNet-mac.zip)
-  * [Linux 64-bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz)
-  * [Linux 32-bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux32.tar.gz)
-* Распакуйте где угодно
-* Запустите `ZeroNet.exe` (win), `ZeroNet(.app)` (osx), `ZeroNet.sh` (linux)
+(WIP)
 
-### Linux терминал
+### Install from source
 
-* `wget https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz`
-* `tar xvpfz ZeroBundle-linux64.tar.gz`
-* `cd ZeroBundle`
-* Запустите с помощью `./ZeroNet.sh`
-
-Он загружает последнюю версию ZeroNet, затем запускает её автоматически.
-
-#### Ручная установка для Debian Linux
-
-* `sudo apt-get update`
-* `sudo apt-get install msgpack-python python-gevent`
-* `wget https://github.com/HelloZeroNet/ZeroNet/archive/master.tar.gz`
-* `tar xvpfz master.tar.gz`
-* `cd ZeroNet-master`
-* Запустите с помощью `python2 zeronet.py`
-* Откройте http://127.0.0.1:43110/ в вашем браузере.
-
-### [Arch Linux](https://www.archlinux.org)
-
-* `git clone https://aur.archlinux.org/zeronet.git`
-* `cd zeronet`
-* `makepkg -srci`
-* `systemctl start zeronet`
-* Откройте http://127.0.0.1:43110/ в вашем браузере.
-
-Смотрите [ArchWiki](https://wiki.archlinux.org)'s [ZeroNet
-article](https://wiki.archlinux.org/index.php/ZeroNet) для дальнейшей помощи.
-
-### [Gentoo Linux](https://www.gentoo.org)
-
-* [`layman -a raiagent`](https://github.com/leycec/raiagent)
-* `echo '>=net-vpn/zeronet-0.5.4' >> /etc/portage/package.accept_keywords`
-* *(Опционально)* Включить поддержку Tor: `echo 'net-vpn/zeronet tor' >>
-  /etc/portage/package.use`
-* `emerge zeronet`
-* `rc-service zeronet start`
-* Откройте http://127.0.0.1:43110/ в вашем браузере.
-
-Смотрите `/usr/share/doc/zeronet-*/README.gentoo.bz2` для дальнейшей помощи.
-
-### [FreeBSD](https://www.freebsd.org/)
-
-* `pkg install zeronet` or `cd /usr/ports/security/zeronet/ && make install clean`
-* `sysrc zeronet_enable="YES"`
-* `service zeronet start`
-* Откройте http://127.0.0.1:43110/ в вашем браузере.
-
-### [Vagrant](https://www.vagrantup.com/)
-
-* `vagrant up`
-* Подключитесь к VM с помощью `vagrant ssh`
-* `cd /vagrant`
-* Запустите `python2 zeronet.py --ui_ip 0.0.0.0`
-* Откройте http://127.0.0.1:43110/ в вашем браузере.
-
-### [Docker](https://www.docker.com/)
-* `docker run -d -v <local_data_folder>:/root/data -p 15441:15441 -p 127.0.0.1:43110:43110 nofish/zeronet`
-* Это изображение Docker включает в себя прокси-сервер Tor, который по умолчанию отключён.
-  Остерегайтесь что некоторые хостинг-провайдеры могут не позволить вам запускать Tor на своих серверах.
-  Если вы хотите включить его,установите переменную среды `ENABLE_TOR` в` true` (по умолчанию: `false`) Например:
-
- `docker run -d -e "ENABLE_TOR=true" -v <local_data_folder>:/root/data -p 15441:15441 -p 127.0.0.1:43110:43110 nofish/zeronet`
-* Откройте http://127.0.0.1:43110/ в вашем браузере.
-
-### [Virtualenv](https://virtualenv.readthedocs.org/en/latest/)
-
-* `virtualenv env`
-* `source env/bin/activate`
-* `pip install msgpack gevent`
-* `python2 zeronet.py`
-* Откройте http://127.0.0.1:43110/ в вашем браузере.
+ - clone this repo
+ - install python3 and pip if needed (the following instructions are for apt-based distributions)
+   - `sudo apt update`
+   - `sudo apt install python3-pip`
+ - `python3 -m pip install -r requirements.txt`
+ - Start with: `python3 zeronet.py`
+ - Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
 
 ## Текущие ограничения
 
-* ~~Нет torrent-похожего файла разделения для поддержки больших файлов~~ (поддержка больших файлов добавлена)
-* ~~Не анонимнее чем Bittorrent~~ (добавлена встроенная поддержка Tor)
-* Файловые транзакции не сжаты ~~ или незашифрованы еще ~~ (добавлено шифрование TLS)
+* Файловые транзакции не сжаты
 * Нет приватных сайтов
-
+* ...
 
 ## Как я могу создать сайт в Zeronet?
 
@@ -194,18 +134,26 @@ Site:13DNDk..bhC2 Successfuly published to 3 peers
 
 * Вот и всё! Вы успешно подписали и опубликовали свои изменения.
 
+## Help this project stay alive
 
-## Поддержите проект
+### Become a maintainer
 
-- Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX
-- Paypal: https://zeronet.io/docs/help_zeronet/donate/
+We need more maintainers! Become one today! Seriously, there's not going to be
+that much new code to audit and auditing new code is the only requirement.
 
-### Спонсоры
+### Use it and spread the word
 
-* Улучшенная совместимость с MacOS / Safari стала возможной благодаря [BrowserStack.com](https://www.browserstack.com)
+Make sure to tell people why do you use 0net and this fork in particular! People
+need to know their alternatives.
 
-#### Спасибо!
+### Financially support maintainers
 
-* Больше информации, помощь, журнал изменений, zeronet сайты: https://www.reddit.com/r/zeronet/
-* Приходите, пообщайтесь с нами: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) или на [gitter](https://gitter.im/HelloZeroNet/ZeroNet)
-* Email: hello@zeronet.io (PGP: CB9613AE)
+Currently the only maintainer of this fork is @caryoscelus. You can see ways to
+donate to them on https://caryoscelus.github.io/donate/
+
+If you want to make sure your donation is recognized as donation for this
+project, there is a dedicated bitcoin address for that, too:
+1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6
+
+If you want to donate in a different way, feel free to contact maintainer or
+create an issue

From 00235a0288d5df3acc139e5cefe1c964ac6d5a4c Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 7 Jan 2022 11:23:59 +0000
Subject: [PATCH 017/333] better error message & minor comment

---
 src/Peer/Peer.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py
index 03cc1f47..bbbf9501 100644
--- a/src/Peer/Peer.py
+++ b/src/Peer/Peer.py
@@ -157,10 +157,11 @@ class Peer(object):
         for retry in range(1, 4):  # Retry 3 times
             try:
                 if not self.connection:
+                    # this is redundant, already established that self.connection is present
                     raise Exception("No connection found")
                 res = self.connection.request(cmd, params, stream_to)
                 if not res:
-                    raise Exception("Send error")
+                    raise Exception("Send error : result is empty")
                 if "error" in res:
                     self.log("%s error: %s" % (cmd, res["error"]))
                     self.onConnectionError("Response error")

From 897dae2790184279d895093887a1376c9ea2603c Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 7 Jan 2022 11:32:22 +0000
Subject: [PATCH 018/333] fix onion-v3 trackers

fixes #4
---
 plugins/AnnounceZero/AnnounceZeroPlugin.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/plugins/AnnounceZero/AnnounceZeroPlugin.py b/plugins/AnnounceZero/AnnounceZeroPlugin.py
index 623cd4b5..d57190b2 100644
--- a/plugins/AnnounceZero/AnnounceZeroPlugin.py
+++ b/plugins/AnnounceZero/AnnounceZeroPlugin.py
@@ -124,7 +124,7 @@ class SiteAnnouncerPlugin(object):
                     sign = CryptRsa.sign(res["onion_sign_this"].encode("utf8"), self.site.connection_server.tor_manager.getPrivatekey(onion))
                     request["onion_signs"][publickey] = sign
             res = tracker_peer.request("announce", request)
-            if not res or "onion_sign_this" in res:
+            if not res:
                 if full_announce:
                     time_full_announced[tracker_address] = 0
                 raise AnnounceError("Announce onion address to failed: %s" % res)

From 2cfe2fa731fd02b0d49a47a9f727e51a2a0faa54 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 7 Jan 2022 11:52:00 +0000
Subject: [PATCH 019/333] bump version v0.7.3

---
 CHANGELOG.md  | 7 +++++--
 src/Config.py | 4 ++--
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b49b9ef6..79cdb651 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,10 @@
+### zeronet-conservancy 0.7.3 (2022-01-07) Rev5000
+- forked from the latest py3 branch of ZeroNet
+- onion v3 support (thanks to @anonymoose, @zeroseed and @geekless)
+- partial readme rewrite (thanks to @mitya57)
+
 ### ZeroNet 0.7.2 (2020-09-?) Rev4206?
 
-
-
 ### ZeroNet 0.7.1 (2019-07-01) Rev4206
 ### Added
  - Built-in logging console in the web UI to see what's happening in the background. (pull down top-right 0 button to see it)
diff --git a/src/Config.py b/src/Config.py
index f66af1fc..ce3487e6 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -13,8 +13,8 @@ import time
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.2"
-        self.rev = 4555
+        self.version = "0.7.3"
+        self.rev = 5000
         self.argv = argv
         self.action = None
         self.test_parser = None

From 53d1437a654502097e81469ee8db3e674cf2af53 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 7 Jan 2022 12:45:05 +0000
Subject: [PATCH 020/333] fix orthography in error message

---
 src/Peer/Peer.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py
index bbbf9501..e525b421 100644
--- a/src/Peer/Peer.py
+++ b/src/Peer/Peer.py
@@ -161,7 +161,7 @@ class Peer(object):
                     raise Exception("No connection found")
                 res = self.connection.request(cmd, params, stream_to)
                 if not res:
-                    raise Exception("Send error : result is empty")
+                    raise Exception("Send error: result is empty")
                 if "error" in res:
                     self.log("%s error: %s" % (cmd, res["error"]))
                     self.onConnectionError("Response error")

From b785d827b2ccbe68c4aad95c8dccaffc3c36f8bc Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 7 Jan 2022 21:12:44 +0000
Subject: [PATCH 021/333] revive CHANGELOG

---
 CHANGELOG.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 64 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79cdb651..6656caad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,72 @@
 ### zeronet-conservancy 0.7.3 (2022-01-07) Rev5000
+maintainers: @caryoscelus
 - forked from the latest py3 branch of ZeroNet
 - onion v3 support (thanks to @anonymoose, @zeroseed and @geekless)
 - partial readme rewrite (thanks to @mitya57)
 
-### ZeroNet 0.7.2 (2020-09-?) Rev4206?
+### ZeroNet 0.7.2+ (latest official py3 branch)
+maintainers: shortcutme a.k.a nofish a.k.a HelloZeroNet a.k.a Tamas Kocsis
+- Update requirements.txt (#2617) (Jabba)
+- Fix Cors permission request for connecting site
+- Allow sites to request several CORS permissions at once (#2631) (Ivanq)
+- Display library versions at /Env url endpoint
+- Fix OpenSSL dll/so location find patcher
+- Fix site listing show on big site visit
+- Fix 404 error handler in UiFilePlugin (Ivanq)
+- other fixes, improvements and translation updates (see more in git log)
+
+### ZeroNet 0.7.2 2020-09-21 (Rev4528)
+maintainers: shortcutme a.k.a nofish a.k.a HelloZeroNet a.k.a Tamas Kocsis
+- Save content.json of site even if limit size is reached (#2114) (Lola Dam)
+- fix #2107; Still save the content.json received even if site size limit is reached but dont download files (Lola Dam)
+- Add multiuser admin status to server info
+- Restrict blocked site addition when using mergerSiteAdd
+- Open console with #ZeroNet:Console hash in url
+- Fix compacting large json files
+- Fix utf8 post data parsing
+- Remove UiRequestPlugin from Zeroname plugin
+- Fix shutdown errors on macOS
+- Fix OpenSSL cert generation using LibreSSL
+- Allow NOSANDBOX in local mode (#2238) (Filip Š)
+- Remove limitations for img, font, media, style src in raw mode
+- Use master seed to create new site from cli
+- No restart after update (#2242) (Lola Dam)
+- Upgrade to Python 3.8 (Christian Clauss)
+- Allow all valid filenames to be added to content.json (#2141) (Josh)
+- Fix loading screen scrolling on smaller screens
+- Fix file rendering if content.json download failed
+- Replace usage of deprecated API 'cgi.parse_qsl' (Natalia Fenclová)
+- Handle announcer thread killing properly
+- Move file writes and reads to separate thread
+- Allow images from data uris
+- Prefer connecting to non-onion peers
+- Make sure we use local peers if possible
+- Faster, async local ip discovery
+- improve db access locks
+- Fix memory leak when using sleep in threads
+- more thread safety
+- Validate json files in src and plugins dir
+- Don't add escaping iframe message for link without target=_top
+- Fix updateing deleted site in contentdb
+- Don't allow parallel sites.json loading
+- Fix incomplete loading of dbschema.json
+- Added Custom Openssl Path for Native Clients and start_dir config (canewsin)
+- Fixed "LookupError: 'hex' is not a text encoding" on /StatsBootstrapper page (#2442) (krzotr)
+- Switch from gevent-websocket to gevent-ws (#2439) (Ivanq)
+- Make ThreadPool a context manager to prevent memory leaks (Ivanq)
+- Fix sslcrypto thread safety (#2454)
+- Search for any OpenSSL version in LD_LIBRARY_PATH (Ivanq)
+- Change to GPLv3 license
+- Allow opening the sidebar while content.json is not loaded (Vadim Ushakov)
+- UiPassword fixes
+- Fix loading invalid site block list
+- Fix wrapper_nonce adding to url
+- Try to recover site privatekey from master seed when site owned switch enabled
+- Fix not downloaded site delete on startup
+- bad file download fixes
+- UiFileManager plugin
+- translation updates
+- many other fixes and improvements (see git log for details)
 
 ### ZeroNet 0.7.1 (2019-07-01) Rev4206
 ### Added

From 75421ec82f588be591107bd381e7c7dc82f9bd7b Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 8 Jan 2022 09:32:27 +0000
Subject: [PATCH 022/333] improve source install instruction

- use venv
- android/termux instructions
---
 README.md | 35 ++++++++++++++++++++++++++---------
 1 file changed, 26 insertions(+), 9 deletions(-)

diff --git a/README.md b/README.md
index 990dac08..1b0eb6e7 100644
--- a/README.md
+++ b/README.md
@@ -64,17 +64,34 @@ Following links relate to original ZeroNet:
 
 ## How to join
 
-### Install from source
+### Install from source (recommended)
 
- - clone this repo
- - install python3 and pip if needed (the following instructions are for apt-based distributions)
-   - `sudo apt update`
-   - `sudo apt install python3-pip`
- - `python3 -m pip install -r requirements.txt`
- - Start with: `python3 zeronet.py`
- - Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
+#### System dependencies
 
-It is recommended to use python environments instead of installing all dependencies globally (TODO: find/write up the detailed instructions)
+##### Generic unix-like
+
+Install autoconf and other basic development tools, python3 and pip.
+
+##### Apt-based (debian, ubuntu, etc)
+ - `sudo apt update`
+ - `sudo apt install python3-pip build-essential`
+
+##### Android/Termux
+ - install [Termux](https://termux.com/)
+ - in Termux install via `pkg install <package-names>`
+ - `pkg update`
+ - `pkg install python automake autoconf-dev git` (TODO: check fresh installation whether there are more dependencies to install)
+
+#### Building python dependencies & running
+ - clone this repo (NOTE: on Android/Termux you should clone it into "home" folder of Termux, because virtual environment cannot live in `storage/`)
+ - `python3 -m venv venv` (make python virtual environment, the last `venv` is just a name, if you use different you should replace it in later commands)
+ - `source venv/bin/activate` (activate environment)
+ - `python3 -m pip install -r requirements.txt` (install dependencies)
+ - `python3 zeronet.py` (**run zeronet-conservancy!**)
+ - open the landing page in your browser by navigating to: http://127.0.0.1:43110/
+ - to start it again from fresh terminal, you need to navigate to repo directory and:
+ - `source venv/bin/activate`
+ - `python3 zeronet.py`
 
 ## Current limitations
 

From 52bd46f3d783646abac6a9076190468317474ea2 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 8 Jan 2022 11:05:07 +0000
Subject: [PATCH 023/333] disable update.py as unsafe

users should use git to update until there's safety mechanisms in place
for upgrading via update zite
---
 update.py | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/update.py b/update.py
index cf9898f9..a7bd086a 100644
--- a/update.py
+++ b/update.py
@@ -115,6 +115,13 @@ def update():
 
 
 if __name__ == "__main__":
-    sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))  # Imports relative to src
+    print('please update zeronet-conservancy via git. usually it can be done via single commnad')
+    print('  git pull')
+    print('although it depends on your branches setup')
+    print('updating through 1update site is not considered safe at the moment')
+    print('if you really want to use it, edit this file')
 
-    update()
+    if False:
+        sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))  # Imports relative to src
+
+        update()

From 6803379ad2025a52fddbcc73de49d8ba7db9c740 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 14 Jan 2022 19:03:48 +0000
Subject: [PATCH 024/333] code improvements in UiRequest

---
 src/Ui/UiRequest.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 8f00efcb..0cc27b84 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -329,7 +329,7 @@ class UiRequest(object):
 
         def renderReplacer(m):
             if m.group(1) in kwargs:
-                return "%s" % kwargs.get(m.group(1), "")
+                return str(kwargs[m.group(1)])
             else:
                 return m.group(0)
 
@@ -545,13 +545,13 @@ class UiRequest(object):
             "src/Ui/template/wrapper.html",
             server_url=server_url,
             inner_path=inner_path,
-            file_url=re.escape(file_url),
-            file_inner_path=re.escape(file_inner_path),
+            file_url=html.escape(re.escape(file_url)),
+            file_inner_path=html.escape(re.escape(file_inner_path)),
             address=site.address,
             title=html.escape(title),
             body_style=body_style,
             meta_tags=meta_tags,
-            query_string=re.escape(inner_query_string),
+            query_string=html.escape(re.escape(inner_query_string)),
             wrapper_key=site.settings["wrapper_key"],
             ajax_key=site.settings["ajax_key"],
             wrapper_nonce=wrapper_nonce,

From 93b896917eba8a7962c76965fe76432b63fcaf70 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 14 Jan 2022 19:21:24 +0000
Subject: [PATCH 025/333] zeronet-conservancy branding

---
 zeronet.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/zeronet.py b/zeronet.py
index dacd2096..2a4194f5 100755
--- a/zeronet.py
+++ b/zeronet.py
@@ -9,7 +9,7 @@ def main():
         sys.exit(0)
 
     if "--silent" not in sys.argv:
-        print("- Starting ZeroNet...")
+        print("- Starting zeronet-conservancy...")
 
     main = None
     try:
@@ -27,7 +27,7 @@ def main():
         error_log_path = config.log_dir + "/error.log"
         traceback.print_exc(file=open(error_log_path, "w"))
         print("---")
-        print("Please report it: https://github.com/HelloZeroNet/ZeroNet/issues/new?assignees=&labels=&template=bug-report.md")
+        print("Please report it: https://github.com/zeronet-conservancy/zeronet-conservancy/issues/new?template=bug-report.md")
         if sys.platform.startswith("win") and "python.exe" not in sys.executable:
             displayErrorMessage(err, error_log_path)
 

From 6e4404d280523b6950b33d10abe61cff3726f196 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 14 Jan 2022 22:16:51 +0000
Subject: [PATCH 026/333] Update FUNDING.yml

---
 .github/FUNDING.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index db8c40a5..89c3e46e 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1,2 @@
-custom: https://zeronet.io/docs/help_zeronet/donate/
+liberapay: caryoscelus
+custom: https://caryoscelus.github.io/donate/

From 4455a6760e02c1162fdef57de54a5d8ac87292e3 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 21 Jan 2022 02:04:18 +0000
Subject: [PATCH 027/333] update changelog

v0.7.3
---
 CHANGELOG.md | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6656caad..b0545761 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,11 @@
-### zeronet-conservancy 0.7.3 (2022-01-07) Rev5000
+### zeronet-conservancy 0.7.3 (2022-01-21) Rev5000
 maintainers: @caryoscelus
 - forked from the latest py3 branch of ZeroNet
 - onion v3 support (thanks to @anonymoose, @zeroseed and @geekless)
 - partial readme rewrite (thanks to @mitya57)
+- disable updating through zite (unsafe)
+- polish translation update (by Krzysztof Otręba)
+- improved install instructions
 
 ### ZeroNet 0.7.2+ (latest official py3 branch)
 maintainers: shortcutme a.k.a nofish a.k.a HelloZeroNet a.k.a Tamas Kocsis

From 232a74d2741e299393c1d01a624a6b4107c4c964 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 24 Jan 2022 01:48:08 +0000
Subject: [PATCH 028/333] disable update more completely

---
 update.py | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/update.py b/update.py
index a7bd086a..62106469 100644
--- a/update.py
+++ b/update.py
@@ -6,6 +6,13 @@ import shutil
 
 
 def update():
+    print('please update zeronet-conservancy via git. usually it can be done via single commnad')
+    print('  git pull')
+    print('although it depends on your branches setup')
+    print('updating through 1update site is not considered safe at the moment')
+    print('if you really want to use it, edit this file')
+    return False
+
     from Config import config
     config.parse(silent=True)
 
@@ -115,13 +122,6 @@ def update():
 
 
 if __name__ == "__main__":
-    print('please update zeronet-conservancy via git. usually it can be done via single commnad')
-    print('  git pull')
-    print('although it depends on your branches setup')
-    print('updating through 1update site is not considered safe at the moment')
-    print('if you really want to use it, edit this file')
+    sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))  # Imports relative to src
 
-    if False:
-        sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))  # Imports relative to src
-
-        update()
+    update()

From 66fcc037e3e06a38475ccc9ffa7ff3b1713ddece Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 24 Jan 2022 02:05:41 +0000
Subject: [PATCH 029/333] change of ADMIN privilege management

- don't grant ADMIN permission to home or update pages
- allow granting ADMIN permission via `--admin_pages` command line option
---
 CHANGELOG.md     | 5 +++++
 src/Config.py    | 1 +
 src/Site/Site.py | 4 ++--
 3 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0545761..be61f424 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+### zeronet-conservancy 0.7.3+ (master)
+maintainers: @caryoscelus
+- don't grant ADMIN permission to home or update pages
+- allow granting ADMIN permission via `--admin_pages` command line option
+
 ### zeronet-conservancy 0.7.3 (2022-01-21) Rev5000
 maintainers: @caryoscelus
 - forked from the latest py3 branch of ZeroNet
diff --git a/src/Config.py b/src/Config.py
index ce3487e6..779e6d53 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -260,6 +260,7 @@ class Config(object):
                                  metavar='address')
         self.parser.add_argument('--updatesite', help='Source code update site', default='1uPDaT3uSyWAPdCv1WkMb5hBQjWSNNACf',
                                  metavar='address')
+        self.parser.add_argument('--admin_pages', help='Pages with admin privileges', default=[], metavar='address', nargs='*')
         self.parser.add_argument('--dist_type', help='Type of installed distribution', default='source')
 
         self.parser.add_argument('--size_limit', help='Default site size limit in MB', default=10, type=int, metavar='limit')
diff --git a/src/Site/Site.py b/src/Site/Site.py
index 354fe9c0..ea19c4a2 100644
--- a/src/Site/Site.py
+++ b/src/Site/Site.py
@@ -110,8 +110,8 @@ class Site(object):
             if config.download_optional == "auto":
                 self.settings["autodownloadoptional"] = True
 
-        # Add admin permissions to homepage
-        if self.address in (config.homepage, config.updatesite) and "ADMIN" not in self.settings["permissions"]:
+        # Add admin permissions according to user settings
+        if self.address in config.admin_pages and "ADMIN" not in self.settings["permissions"]:
             self.settings["permissions"].append("ADMIN")
 
         return

From 0556bb400d2f84299d739189288aec251e053924 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 26 Jan 2022 11:14:55 +0000
Subject: [PATCH 030/333] update travis

use only recent python versions (3.9 & 3.10)
---
 .travis.yml | 12 ++----------
 1 file changed, 2 insertions(+), 10 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index bdaafa22..75ffb9ef 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,7 @@
 language: python
 python:
-  - 3.4
-  - 3.5
-  - 3.6
-  - 3.7
-  - 3.8
+  - 3.9
+  - 3.10
 services:
  - docker
 cache: pip
@@ -40,8 +37,3 @@ after_failure:
 after_success:
   - codecov
   - coveralls --rcfile=src/Test/coverage.ini
-notifications:
-  email:
-    recipients:
-      hello@zeronet.io
-    on_success: change

From 855b23a84bec65c3dc56954bd69a46e90da805c4 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 26 Jan 2022 19:28:17 +0000
Subject: [PATCH 031/333] fix readdress loop

use better escaping in render

fixes #19
---
 src/Ui/UiRequest.py | 27 +++++++++++++++++++++++----
 1 file changed, 23 insertions(+), 4 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 0cc27b84..c7f7c922 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -541,17 +541,36 @@ class UiRequest(object):
         if show_loadingscreen is None:
             show_loadingscreen = not site.storage.isFile(file_inner_path)
 
+        def xescape(s):
+            '''combines parts from re.escape & html.escape'''
+            # https://github.com/python/cpython/blob/3.10/Lib/re.py#L267
+            # '&' is handled otherwise
+            re_chars = {i: '\\' + chr(i) for i in  b'()[]{}*+-|^$\\.~# \t\n\r\v\f'}
+            # https://github.com/python/cpython/blob/3.10/Lib/html/__init__.py#L12
+            html_chars = {
+                '<' : '&lt;',
+                '>' : '&gt;',
+                '"' : '&quot;',
+                "'" : '&#x27;',
+            }
+            # we can't replace '&' because it makes certain zites work incorrectly
+            # it should however in no way interfere with re.sub in render
+            repl = {}
+            repl.update(re_chars)
+            repl.update(html_chars)
+            return s.translate(repl)
+
         return self.render(
             "src/Ui/template/wrapper.html",
             server_url=server_url,
             inner_path=inner_path,
-            file_url=html.escape(re.escape(file_url)),
-            file_inner_path=html.escape(re.escape(file_inner_path)),
+            file_url=xescape(file_url),
+            file_inner_path=xescape(file_inner_path),
             address=site.address,
-            title=html.escape(title),
+            title=xescape(title),
             body_style=body_style,
             meta_tags=meta_tags,
-            query_string=html.escape(re.escape(inner_query_string)),
+            query_string=xescape(inner_query_string),
             wrapper_key=site.settings["wrapper_key"],
             ajax_key=site.settings["ajax_key"],
             wrapper_nonce=wrapper_nonce,

From a57ebf8d3551307eda53b7427bca3e8ee54c97a3 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 26 Jan 2022 20:09:24 +0000
Subject: [PATCH 032/333] v0.7.3.1

---
 CHANGELOG.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index be61f424..350a840d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,8 @@
-### zeronet-conservancy 0.7.3+ (master)
+### zeronet-conservancy 0.7.3.1 (2022-01-26)
 maintainers: @caryoscelus
 - don't grant ADMIN permission to home or update pages
 - allow granting ADMIN permission via `--admin_pages` command line option
+- fix infinite readdress bug (thx @mahdi-ln for reporting)
 
 ### zeronet-conservancy 0.7.3 (2022-01-21) Rev5000
 maintainers: @caryoscelus

From 5401cdff9b94b310e38d16ab76f44f1962e7863f Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 26 Jan 2022 20:12:34 +0000
Subject: [PATCH 033/333] changelog

---
 CHANGELOG.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 350a840d..cfee6048 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,6 @@
-### zeronet-conservancy 0.7.3.1 (2022-01-26)
+### zeronet-conservancy 0.7.3.1+ (master)
+
+### zeronet-conservancy 0.7.3.1 (2022-01-26) (a57ebf8)
 maintainers: @caryoscelus
 - don't grant ADMIN permission to home or update pages
 - allow granting ADMIN permission via `--admin_pages` command line option

From 1a73dd794f14db2ab90d1ec47c135e57b0f40db4 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 26 Jan 2022 20:18:32 +0000
Subject: [PATCH 034/333] v0.7.3.2

fix version number
---
 CHANGELOG.md  | 4 +++-
 src/Config.py | 5 +++--
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cfee6048..0e9ffbb8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,6 @@
-### zeronet-conservancy 0.7.3.1+ (master)
+### zeronet-conservancy 0.7.3.2 (2022-01-26)
+maintainers: @caryoscelus
+(quick fix: technical release with proper versioning info)
 
 ### zeronet-conservancy 0.7.3.1 (2022-01-26) (a57ebf8)
 maintainers: @caryoscelus
diff --git a/src/Config.py b/src/Config.py
index 779e6d53..a7f9dbc6 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -13,8 +13,9 @@ import time
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.3"
-        self.rev = 5000
+        self.version = "0.7.3.2"
+        # DEPRECATED ; replace with git-generated commit
+        self.rev = 5002
         self.argv = argv
         self.action = None
         self.test_parser = None

From 2fdb970f3c82e04b75aea14489d67008fa31f9ca Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 1 Feb 2022 19:21:52 +0000
Subject: [PATCH 035/333] Update README.md

fix apt-based distros dependency instruction
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 3f658c8d..05511c38 100644
--- a/README.md
+++ b/README.md
@@ -74,7 +74,7 @@ Install autoconf and other basic development tools, python3 and pip.
 
 ##### Apt-based (debian, ubuntu, etc)
  - `sudo apt update`
- - `sudo apt install python3-pip build-essential`
+ - `sudo apt install python3-pip python3-venv`
 
 ##### Android/Termux
  - install [Termux](https://termux.com/)

From 15f94c325a683fdc251538a03c33bbcbb93653ad Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 2 Feb 2022 12:19:47 +0000
Subject: [PATCH 036/333] use new admin page as home page for now

this is temporary until new static home page with a choice
is made
---
 src/Config.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index a7f9dbc6..3290c535 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -257,10 +257,10 @@ class Config(object):
 
         self.parser.add_argument('--open_browser', help='Open homepage in web browser automatically',
                                  nargs='?', const="default_browser", metavar='browser_name')
-        self.parser.add_argument('--homepage', help='Web interface Homepage', default='1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D',
-                                 metavar='address')
-        self.parser.add_argument('--updatesite', help='Source code update site', default='1uPDaT3uSyWAPdCv1WkMb5hBQjWSNNACf',
+        self.parser.add_argument('--homepage', help='Web interface Homepage', default='126NXcevn1AUehWFZLTBw7FrX1crEizQdr',
                                  metavar='address')
+        # self.parser.add_argument('--updatesite', help='Source code update site', default='1uPDaT3uSyWAPdCv1WkMb5hBQjWSNNACf',
+                                 # metavar='address')
         self.parser.add_argument('--admin_pages', help='Pages with admin privileges', default=[], metavar='address', nargs='*')
         self.parser.add_argument('--dist_type', help='Type of installed distribution', default='source')
 

From ba0662327bf38da7dbe5b5da693c722ee7f97e45 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 2 Feb 2022 12:26:57 +0000
Subject: [PATCH 037/333] fix uiwebsocket

fixes previous commit (usage of updatesite)
---
 src/Ui/UiWebsocket.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index 88e395d6..cf386e8a 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -315,7 +315,7 @@ class UiWebsocket(object):
             "user_settings": self.user.settings
         }
         if "ADMIN" in self.site.settings["permissions"]:
-            back["updatesite"] = config.updatesite
+            # back["updatesite"] = config.updatesite
             back["dist_type"] = config.dist_type
             back["lib_verify_best"] = CryptBitcoin.lib_verify_best
         return back

From 09b73e2222204544248e04c01e0f09f74f6f14d7 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 7 Feb 2022 15:26:19 +0000
Subject: [PATCH 038/333] fix multiuser/merger plugins interaction

---
 plugins/disabled-Multiuser/MultiuserPlugin.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py
index 799c3337..fd28ead8 100644
--- a/plugins/disabled-Multiuser/MultiuserPlugin.py
+++ b/plugins/disabled-Multiuser/MultiuserPlugin.py
@@ -27,6 +27,9 @@ class UiRequestPlugin(object):
         self.user_manager = UserManager.user_manager
         super(UiRequestPlugin, self).__init__(*args, **kwargs)
 
+    def parsePath(self, path):
+        return super(UiRequestPlugin, self).parsePath(path)
+
     # Create new user and inject user welcome message if necessary
     # Return: Html body also containing the injection
     def actionWrapper(self, path, extra_headers=None):

From f40ed9b26cfb0f665b6e2cf333ca64973166847e Mon Sep 17 00:00:00 2001
From: defder-su <85085602+defder-su@users.noreply.github.com>
Date: Wed, 23 Feb 2022 15:46:14 +0000
Subject: [PATCH 039/333] [Sidebar] "Open site directory" feature

https://github.com/zeronet-conservancy/zeronet-conservancy/issues/30
---
 plugins/Sidebar/SidebarPlugin.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index 4ecca75a..b3823046 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -175,6 +175,7 @@ class UiWebsocketPlugin(object):
               {_[Files]}
               <a href='/list/{site.address}' class='link-right link-outline' id="browse-files">{_[Browse files]}</a>
               <small class="label-right">
+               <a href='#Site+directory' id='link-directory' class='link-right'>{_[Open site directory]}</a>
                <a href='/ZeroNet-Internal/Zip?address={site.address}' id='link-zip' class='link-right' download='site.zip'>{_[Save as .zip]}</a>
               </small>
              </label>

From 06727cb51179c4f0e327f14fc9f2c48797603bd9 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 3 Mar 2022 05:15:20 +0000
Subject: [PATCH 040/333] fix UiRequest.parsePath

---
 src/Ui/UiRequest.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index c7f7c922..960da168 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -630,7 +630,7 @@ class UiRequest(object):
         if "../" in path or "./" in path:
             raise SecurityError("Invalid path")
 
-        match = re.match(r"/media/(?P<address>[A-Za-z0-9]+[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
+        match = re.match(r"/(media/)?(?P<address>[A-Za-z0-9]+[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
         if match:
             path_parts = match.groupdict()
             if self.isDomain(path_parts["address"]):

From 7658f571617f07f193f92d936fcdd50f169edbca Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 26 Mar 2022 09:24:59 +0000
Subject: [PATCH 041/333] minor code improvement

---
 src/Site/SiteStorage.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py
index c12a80b0..4cbed75d 100644
--- a/src/Site/SiteStorage.py
+++ b/src/Site/SiteStorage.py
@@ -29,7 +29,7 @@ thread_pool_fs_batch = ThreadPool.ThreadPool(1, name="FS batch")
 class SiteStorage(object):
     def __init__(self, site, allow_create=True):
         self.site = site
-        self.directory = "%s/%s" % (config.data_dir, self.site.address)  # Site data diretory
+        self.directory = f'{config.data_dir}/{self.site.address}'  # Site data diretory
         self.allowed_dir = os.path.abspath(self.directory)  # Only serve file within this dir
         self.log = site.log
         self.db = None  # Db class

From a6a36c7c09c2da7861155059e1b282460a63388a Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 12 Apr 2022 06:46:17 +0000
Subject: [PATCH 042/333] bump revision

---
 src/Config.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index 3290c535..561eb1fc 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -13,9 +13,9 @@ import time
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.3.2"
+        self.version = "0.7.3.95"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5002
+        self.rev = 5003
         self.argv = argv
         self.action = None
         self.test_parser = None

From d07a1bb2c50ddaab440725229eaef1fa99d66b31 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 12 Apr 2022 07:10:41 +0000
Subject: [PATCH 043/333] update FUNDING.yml

---
 .github/FUNDING.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 89c3e46e..d596801e 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,2 +1,3 @@
 liberapay: caryoscelus
+ko_fi: caryoscelus
 custom: https://caryoscelus.github.io/donate/

From 733b1b05b1ef4dcd6aa59200cfc03e15028538bd Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 25 Apr 2022 11:46:49 +0000
Subject: [PATCH 044/333] v0.7.4

---
 CHANGELOG.md  | 10 +++++++++-
 src/Config.py |  2 +-
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e9ffbb8..0760986a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,12 @@
-### zeronet-conservancy 0.7.3.2 (2022-01-26)
+### zeronet-conservancy 0.7.4 (2022-04-25)
+maintainers: @caryoscelus
+- fix UiRequest.parsePath & minor code improvements
+- Sidebar "Open site directory" feature (by @defder-su)
+- fix multiuser/merger plugin interaction
+- use new admin page
+- update dependency instructions
+
+### zeronet-conservancy 0.7.3.2 (2022-01-26) (1a73dd7)
 maintainers: @caryoscelus
 (quick fix: technical release with proper versioning info)
 
diff --git a/src/Config.py b/src/Config.py
index 561eb1fc..66b92be6 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -13,7 +13,7 @@ import time
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.3.95"
+        self.version = "0.7.4"
         # DEPRECATED ; replace with git-generated commit
         self.rev = 5003
         self.argv = argv

From 5b63b0298f84d05938acda5ba49de06f6348ccb2 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 3 May 2022 16:31:09 +0400
Subject: [PATCH 045/333] colourful greetings

---
 requirements.txt |  1 +
 src/Config.py    |  4 ++--
 zeronet.py       | 39 +++++++++++++++++++++++++++++++++++++--
 3 files changed, 40 insertions(+), 4 deletions(-)

diff --git a/requirements.txt b/requirements.txt
index b3df57ea..81669ff1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,3 +11,4 @@ websocket_client
 gevent-ws
 coincurve
 maxminddb
+rich
diff --git a/src/Config.py b/src/Config.py
index 66b92be6..4463460a 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -13,9 +13,9 @@ import time
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.4"
+        self.version = "0.7.4+"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5003
+        self.rev = 5004
         self.argv = argv
         self.action = None
         self.test_parser = None
diff --git a/zeronet.py b/zeronet.py
index 2a4194f5..36b2ed74 100755
--- a/zeronet.py
+++ b/zeronet.py
@@ -1,7 +1,43 @@
 #!/usr/bin/env python3
 import os
 import sys
+from src.Config import config
 
+def grad(n):
+    s = 0x08
+    r = 0xff
+    g = 0x00
+    b = 0x00
+    for i in range(n):
+        if r >= s and b < s:
+            r -= s
+            g += s
+        elif g >= s and r < s:
+            g -= s
+            b += s
+        elif b >= s and g < s:
+            b -= s
+            r += s
+    return f'#{r:02x}{g:02x}{b:02x}'
+
+def fancy_greet():
+    from rich.console import Console
+    from rich.text import Text
+    zc_msg = f'''
+|||  __.  _. . _  .   . _    _.  _|_          _.   .   . _  .--   _. . _  .  .  __.  . _    _.  .  .
+|||   /  /_| |/  / \  |/ |  /_|   |     ==   /    / \  |/ |  \   /_| |/   |  |  __|  |/ |  /     \_|
+|||  /_. \_  |   \_/  |  |  \_    |.         \__  \_/  |  | ._|  \_  |     \/  |__|  |  |  \__     |
+|||                                                                                              _/
+|||  v{config.version}
+'''
+    lns = zc_msg.split('\n')
+    console = Console()
+    for l in lns:
+        txt = Text(l)
+        txt.stylize('bold')
+        for i in range(len(l)):
+            txt.stylize(grad(i), i, i+1)
+        console.print(txt)
 
 def main():
     if sys.version_info.major < 3:
@@ -9,7 +45,7 @@ def main():
         sys.exit(0)
 
     if "--silent" not in sys.argv:
-        print("- Starting zeronet-conservancy...")
+        fancy_greet()
 
     main = None
     try:
@@ -23,7 +59,6 @@ def main():
         except Exception as log_err:
             print("Failed to log error:", log_err)
             traceback.print_exc()
-        from Config import config
         error_log_path = config.log_dir + "/error.log"
         traceback.print_exc(file=open(error_log_path, "w"))
         print("---")

From b92e03a3be8feaf57e5223da0bf10acca535e75d Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 8 May 2022 17:35:00 +0400
Subject: [PATCH 046/333] remove start.py

it's unused and potentially confusing
---
 start.py | 17 -----------------
 1 file changed, 17 deletions(-)
 delete mode 100644 start.py

diff --git a/start.py b/start.py
deleted file mode 100644
index 063d7802..00000000
--- a/start.py
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env python3
-
-
-# Included modules
-import sys
-
-# ZeroNet Modules
-import zeronet
-
-
-def main():
-    if "--open_browser" not in sys.argv:
-        sys.argv = [sys.argv[0]] + ["--open_browser", "default_browser"] + sys.argv[1:]
-    zeronet.start()
-
-if __name__ == '__main__':
-    main()

From 36c0869846db418f64b0fe832c68fc71a0c2cf52 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 8 May 2022 17:36:50 +0400
Subject: [PATCH 047/333] update gitignore

---
 .gitignore | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.gitignore b/.gitignore
index 38dd3a34..40519faf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ __pycache__/
 
 # Temporary files
 *.bak
+*~
 
 # Data dir
 data/*
@@ -21,6 +22,7 @@ data/*
 
 # Virtualenv
 env/*
+venv/*
 
 # Tor data
 tools/tor/data

From 80284fcab42200156932018f1809fcc218ec1422 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 8 May 2022 17:37:04 +0400
Subject: [PATCH 048/333] add easy to use startup script

---
 start-venv.sh | 8 ++++++++
 1 file changed, 8 insertions(+)
 create mode 100755 start-venv.sh

diff --git a/start-venv.sh b/start-venv.sh
new file mode 100755
index 00000000..be8b5888
--- /dev/null
+++ b/start-venv.sh
@@ -0,0 +1,8 @@
+#! /usr/bin/env bash
+
+if [ ! -f venv/bin/activate ] ; then
+    python3 -m venv venv
+fi
+source venv/bin/activate
+python3 -m pip install -r requirements.txt
+python3 zeronet.py

From b8b1c3deac0f4338ee22310c7e80c83f46098a2d Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 8 May 2022 21:11:29 +0400
Subject: [PATCH 049/333] add more active trackers from Syncronite

---
 src/Config.py | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/src/Config.py b/src/Config.py
index 4463460a..ca7bed05 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -81,6 +81,28 @@ class Config(object):
     # Create command line arguments
     def createArguments(self):
         trackers = [
+            # more trackers from Syncronite (TODO: check if any of the old ones still work)
+            'zero://kgsvasoakvj4gnjiy7zemu34l3hq46dn5eauqkn76jpowmilci5t2vqd.onion:15445',
+            'zero://hb6ozikfiaafeuqvgseiik4r46szbpjfu66l67wjinnyv6dtopuwhtqd.onion:15445',
+            'zero://75pmmcbp4vvo2zndmjnrkandvbg6jyptygvvpwsf2zguj7urq7t4jzyd.onion:7777',
+            'zero://dw4f4sckg2ultdj5qu7vtkf3jsfxsah3mz6pivwfd6nv3quji3vfvhyd.onion:6969',
+            'zero://5vczpwawviukvd7grfhsfxp7a6huz77hlis4fstjkym5kmf4pu7i7myd.onion:15441',
+            'zero://ow7in4ftwsix5klcbdfqvfqjvimqshbm2o75rhtpdnsderrcbx74wbad.onion:15441',
+            'zero://agufghdtniyfwty3wk55drxxwj2zxgzzo7dbrtje73gmvcpxy4ngs4ad.onion:15441',
+            'zero://qn65si4gtcwdiliq7vzrwu62qrweoxb6tx2cchwslaervj6szuje66qd.onion:26117',
+            'udp://tracker.opentrackr.org:1337/announce',
+            'udp://tracker.moeking.me:6969/announce',
+            'http://tracker.files.fm:6969/announce',
+            'http://t.overflow.biz:6969/announce',
+            'udp://fe.dealclub.de:6969/announce',
+            'udp://movies.zsw.ca:6969/announce',
+            'udp://6ahddutb1ucc3cp.ru:6969/announce',
+            'zero://145.239.95.38:15441',
+            'zero://23.184.48.134:15441',
+            'zero://95.110.227.231:15441',
+            'zero://159.65.50.3:26117',
+            'zero://2a03:b0c0:1:d0::f52:1:26117',
+            'zero://k5w77dozo3hy5zualyhni6vrh73iwfkaofa64abbilwyhhd3wgenbjqd.onion:15441',
             # by zeroseed at http://127.0.0.1:43110/19HKdTAeBh5nRiKn791czY7TwRB1QNrf1Q/?:users/1HvNGwHKqhj3ZMEM53tz6jbdqe4LRpanEu:zn:dc17f896-bf3f-4962-bdd4-0a470040c9c5
             "zero://k5w77dozo3hy5zualyhni6vrh73iwfkaofa64abbilwyhhd3wgenbjqd.onion:15441",
             "zero://2kcb2fqesyaevc4lntogupa4mkdssth2ypfwczd2ov5a3zo6ytwwbayd.onion:15441",

From 4d06eaf5c4e76be5cc11fb36e2a5b0f81ba79ecf Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 10 May 2022 22:47:26 +0400
Subject: [PATCH 050/333] README updates

- update mission statement
- mention new launch script
---
 README.md | 27 +++++++++++++--------------
 1 file changed, 13 insertions(+), 14 deletions(-)

diff --git a/README.md b/README.md
index 05511c38..4fa19ac5 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,17 @@
 # zeronet-conservancy
 
-This is a minimalist conservative fork of [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) with tor onion-v3 support (and perhaps more essential/security fixes in the future)
+zeronet-conservancy is a fork/continuation of [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) project
+(that has been abandoned by its creator) that is dedicated to sustaining existing p2p network and developing
+its values of decentralization and freedom, while gradually switching to a better designed network
 
 ## Why fork?
 
-We need a fork that works with onion-v3 and doesn't depend on trust to one or two people. We need it now. This fork implements minimal changes to [ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3) branch which are easy to audit yourself.
-
-This fork is intended as temporary measure and will possibly stop being maintained after its original author established there is an alternative active trust-worthy fork.
+During onion-v3 switch crisis, we needed a fork that worked with onion-v3 and didn't depend on trust to one or
+two people. This fork started from fulfilling that mission, implementing minimal changes to
+[ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3) branch which are easy to audit by anyone. While
+you can still use the early releases of the fork to get onion-v3 working, the goal of this fork has since shifted
+and we're dedicated to solving more problems and improving user experience and security all over, until the
+brand new, completely transparent and audited network is ready and this project can be put to rest
 
 ## Why 0net?
 
@@ -28,7 +33,7 @@ This fork is intended as temporary measure and will possibly stop being maintain
  * TLS encrypted connections (through clearnet)
  * Automatic uPnP port opening (if opted in)
  * Plugin for multiuser (openproxy) support
- * Works with any browser/OS
+ * Works with any modern browser/OS
 
 
 ## How does it work?
@@ -53,15 +58,6 @@ Following links relate to original ZeroNet:
 - [Frequently asked questions »](https://zeronet.io/docs/faq/)
 - [ZeroNet Developer Documentation »](https://zeronet.io/docs/site_development/getting_started/)
 
-
-## Screenshots
-
-![Screenshot](https://i.imgur.com/H60OAHY.png)
-![ZeroTalk](https://zeronet.io/docs/img/zerotalk.png)
-
-#### [More screenshots in ZeroNet docs »](https://zeronet.io/docs/using_zeronet/sample_sites/)
-
-
 ## How to join
 
 ### Install from source (recommended)
@@ -93,6 +89,9 @@ Install autoconf and other basic development tools, python3 and pip.
  - `source venv/bin/activate`
  - `python3 zeronet.py`
 
+#### alternative script
+ - after installing general dependencies and cloning repo (as above), run `start-venv.sh` which will create a virtual env for you and install python requirements
+
 ## Current limitations
 
 * File transactions are not compressed

From 961cc5361de191fa45b1670df88a168ca567f63a Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 8 May 2022 17:40:44 +0400
Subject: [PATCH 051/333] [cleanup] remove old coffeescript related stuff

---
 tools/coffee/README.md        |  19 --
 tools/coffee/coffee-script.js | 405 ----------------------------------
 tools/coffee/coffee.cmd       |   2 -
 tools/coffee/coffee.wsf       | 109 ---------
 4 files changed, 535 deletions(-)
 delete mode 100644 tools/coffee/README.md
 delete mode 100644 tools/coffee/coffee-script.js
 delete mode 100644 tools/coffee/coffee.cmd
 delete mode 100644 tools/coffee/coffee.wsf

diff --git a/tools/coffee/README.md b/tools/coffee/README.md
deleted file mode 100644
index dc589d21..00000000
--- a/tools/coffee/README.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# CoffeeScript compiler for Windows
-
-A simple command-line utilty for Windows that will compile `*.coffee` files to JavaScript `*.js` files using [CoffeeScript](http://jashkenas.github.com/coffee-script/) and the venerable Windows Script Host, ubiquitous on Windows since the 90s.
-
-## Usage
-
-To use it, invoke `coffee.cmd` like so:
-
-    coffee input.coffee output.js
-    
-If an output is not specified, it is written to `stdout`. In neither an input or output are specified then data is assumed to be on `stdin`. For example:
-
-    type input.coffee | coffee > output.js
-
-Errors are written to `stderr`.
-
-In the `test` directory there's a version of the standard CoffeeScript tests which can be kicked off using `test.cmd`. The test just attempts to compile the *.coffee files but doesn't execute them.
-
-To upgrade to the latest CoffeeScript simply replace `coffee-script.js` from the upstream https://github.com/jashkenas/coffee-script/blob/master/extras/coffee-script.js (the tests will likely need updating as well, if you want to run them).
diff --git a/tools/coffee/coffee-script.js b/tools/coffee/coffee-script.js
deleted file mode 100644
index 7fce39a6..00000000
--- a/tools/coffee/coffee-script.js
+++ /dev/null
@@ -1,405 +0,0 @@
-/**
- * CoffeeScript Compiler v1.12.6
- * http://coffeescript.org
- *
- * Copyright 2011, Jeremy Ashkenas
- * Released under the MIT License
- */
-var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.checkStringArgs=function(u,xa,va){if(null==u)throw new TypeError("The 'this' value for String.prototype."+va+" must not be null or undefined");if(xa instanceof RegExp)throw new TypeError("First argument to String.prototype."+va+" must not be a regular expression");return u+""};
-$jscomp.defineProperty="function"==typeof Object.defineProperties?Object.defineProperty:function(u,xa,va){if(va.get||va.set)throw new TypeError("ES3 does not support getters and setters.");u!=Array.prototype&&u!=Object.prototype&&(u[xa]=va.value)};$jscomp.getGlobal=function(u){return"undefined"!=typeof window&&window===u?u:"undefined"!=typeof global&&null!=global?global:u};$jscomp.global=$jscomp.getGlobal(this);
-$jscomp.polyfill=function(u,xa,va,f){if(xa){va=$jscomp.global;u=u.split(".");for(f=0;f<u.length-1;f++){var qa=u[f];qa in va||(va[qa]={});va=va[qa]}u=u[u.length-1];f=va[u];xa=xa(f);xa!=f&&null!=xa&&$jscomp.defineProperty(va,u,{configurable:!0,writable:!0,value:xa})}};
-$jscomp.polyfill("String.prototype.repeat",function(u){return u?u:function(u){var va=$jscomp.checkStringArgs(this,null,"repeat");if(0>u||1342177279<u)throw new RangeError("Invalid count value");u|=0;for(var f="";u;)if(u&1&&(f+=va),u>>>=1)va+=va;return f}},"es6-impl","es3");$jscomp.findInternal=function(u,xa,va){u instanceof String&&(u=String(u));for(var f=u.length,qa=0;qa<f;qa++){var q=u[qa];if(xa.call(va,q,qa,u))return{i:qa,v:q}}return{i:-1,v:void 0}};
-$jscomp.polyfill("Array.prototype.find",function(u){return u?u:function(u,va){return $jscomp.findInternal(this,u,va).v}},"es6-impl","es3");$jscomp.SYMBOL_PREFIX="jscomp_symbol_";$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){};$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)};$jscomp.symbolCounter_=0;$jscomp.Symbol=function(u){return $jscomp.SYMBOL_PREFIX+(u||"")+$jscomp.symbolCounter_++};
-$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var u=$jscomp.global.Symbol.iterator;u||(u=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator"));"function"!=typeof Array.prototype[u]&&$jscomp.defineProperty(Array.prototype,u,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}});$jscomp.initSymbolIterator=function(){}};
-$jscomp.arrayIterator=function(u){var xa=0;return $jscomp.iteratorPrototype(function(){return xa<u.length?{done:!1,value:u[xa++]}:{done:!0}})};$jscomp.iteratorPrototype=function(u){$jscomp.initSymbolIterator();u={next:u};u[$jscomp.global.Symbol.iterator]=function(){return this};return u};
-$jscomp.iteratorFromArray=function(u,xa){$jscomp.initSymbolIterator();u instanceof String&&(u+="");var va=0,f={next:function(){if(va<u.length){var qa=va++;return{value:xa(qa,u[qa]),done:!1}}f.next=function(){return{done:!0,value:void 0}};return f.next()}};f[Symbol.iterator]=function(){return f};return f};$jscomp.polyfill("Array.prototype.keys",function(u){return u?u:function(){return $jscomp.iteratorFromArray(this,function(u){return u})}},"es6-impl","es3");
-(function(u){var xa=function(){function u(f){return u[f]}u["../../package.json"]={name:"coffee-script",description:"Unfancy JavaScript",keywords:["javascript","language","coffeescript","compiler"],author:"Jeremy Ashkenas",version:"1.12.6",license:"MIT",engines:{node:"\x3e\x3d0.8.0"},directories:{lib:"./lib/coffee-script"},main:"./lib/coffee-script/coffee-script",bin:{coffee:"./bin/coffee",cake:"./bin/cake"},files:["bin","lib","register.js","repl.js"],scripts:{test:"node ./bin/cake test","test-harmony":"node --harmony ./bin/cake test"},
-homepage:"http://coffeescript.org",bugs:"https://github.com/jashkenas/coffeescript/issues",repository:{type:"git",url:"git://github.com/jashkenas/coffeescript.git"},devDependencies:{docco:"~0.7.0","google-closure-compiler-js":"^20170423.0.0","highlight.js":"~9.11.0",jison:"\x3e\x3d0.4.17","markdown-it":"^8.3.1",underscore:"~1.8.3"}};u["./helpers"]=function(){var f={};(function(){var u,q,y;f.starts=function(a,h,r){return h===a.substr(r,h.length)};f.ends=function(a,h,r){var g=h.length;return h===a.substr(a.length-
-g-(r||0),g)};f.repeat=y=function(a,h){var g;for(g="";0<h;)h&1&&(g+=a),h>>>=1,a+=a;return g};f.compact=function(a){var g,b;var n=[];var y=0;for(b=a.length;y<b;y++)(g=a[y])&&n.push(g);return n};f.count=function(a,h){var g;var b=g=0;if(!h.length)return 1/0;for(;g=1+a.indexOf(h,g);)b++;return b};f.merge=function(g,h){return a(a({},g),h)};var a=f.extend=function(a,h){var g;for(g in h){var b=h[g];a[g]=b}return a};f.flatten=u=function(a){var g;var b=[];var y=0;for(g=a.length;y<g;y++){var f=a[y];"[object Array]"===
-Object.prototype.toString.call(f)?b=b.concat(u(f)):b.push(f)}return b};f.del=function(a,h){var g=a[h];delete a[h];return g};f.some=null!=(q=Array.prototype.some)?q:function(a){var g;var b=0;for(g=this.length;b<g;b++){var y=this[b];if(a(y))return!0}return!1};f.invertLiterate=function(a){var g=!0;var b;var y=a.split("\n");var f=[];var H=0;for(b=y.length;H<b;H++)a=y[H],g&&/^([ ]{4}|[ ]{0,3}\t)/.test(a)?f.push(a):(g=/^\s*$/.test(a))?f.push(a):f.push("# "+a);return f.join("\n")};var b=function(a,b){return b?
-{first_line:a.first_line,first_column:a.first_column,last_line:b.last_line,last_column:b.last_column}:a};f.addLocationDataFn=function(a,h){return function(g){"object"===typeof g&&g.updateLocationDataIfMissing&&g.updateLocationDataIfMissing(b(a,h));return g}};f.locationDataToString=function(a){var g;"2"in a&&"first_line"in a[2]?g=a[2]:"first_line"in a&&(g=a);return g?g.first_line+1+":"+(g.first_column+1)+"-"+(g.last_line+1+":"+(g.last_column+1)):"No location data"};f.baseFileName=function(a,b,y){null==
-b&&(b=!1);null==y&&(y=!1);a=a.split(y?/\\|\//:/\//);a=a[a.length-1];if(!(b&&0<=a.indexOf(".")))return a;a=a.split(".");a.pop();"coffee"===a[a.length-1]&&1<a.length&&a.pop();return a.join(".")};f.isCoffee=function(a){return/\.((lit)?coffee|coffee\.md)$/.test(a)};f.isLiterate=function(a){return/\.(litcoffee|coffee\.md)$/.test(a)};f.throwSyntaxError=function(a,b){a=new SyntaxError(a);a.location=b;a.toString=ya;a.stack=a.toString();throw a;};f.updateSyntaxError=function(a,b,y){a.toString===ya&&(a.code||
-(a.code=b),a.filename||(a.filename=y),a.stack=a.toString());return a};var ya=function(){var a,b,f;if(!this.code||!this.location)return Error.prototype.toString.call(this);var n=this.location;var B=n.first_line;var H=n.first_column;var I=n.last_line;var F=n.last_column;null==I&&(I=B);null==F&&(F=H);var u=this.filename||"[stdin]";n=this.code.split("\n")[B];I=B===I?F+1:n.length;F=n.slice(0,H).replace(/[^\s]/g," ")+y("^",I-H);if("undefined"!==typeof process&&null!==process)var x=(null!=(a=process.stdout)?
-a.isTTY:void 0)&&!(null!=(b=process.env)&&b.NODE_DISABLE_COLORS);if(null!=(f=this.colorful)?f:x)x=function(a){return"\u001b[1;31m"+a+"\u001b[0m"},n=n.slice(0,H)+x(n.slice(H,I))+n.slice(I),F=x(F);return u+":"+(B+1)+":"+(H+1)+": error: "+this.message+"\n"+n+"\n"+F};f.nameWhitespaceCharacter=function(a){switch(a){case " ":return"space";case "\n":return"newline";case "\r":return"carriage return";case "\t":return"tab";default:return a}}}).call(this);return f}();u["./rewriter"]=function(){var f={};(function(){var u,
-q,y=[].indexOf||function(a){for(var c=0,b=this.length;c<b;c++)if(c in this&&this[c]===a)return c;return-1},a=[].slice;var b=function(a,c,b){a=[a,c];a.generated=!0;b&&(a.origin=b);return a};f.Rewriter=function(){function l(){}l.prototype.rewrite=function(a){this.tokens=a;this.removeLeadingNewlines();this.closeOpenCalls();this.closeOpenIndexes();this.normalizeLines();this.tagPostfixConditionals();this.addImplicitBracesAndParens();this.addLocationDataToGeneratedTokens();this.fixOutdentLocationData();
-return this.tokens};l.prototype.scanTokens=function(a){var c,b;var k=this.tokens;for(c=0;b=k[c];)c+=a.call(this,b,c,k);return!0};l.prototype.detectEnd=function(a,b,m){var c,w,l,L;var f=this.tokens;for(c=0;L=f[a];){if(0===c&&b.call(this,L,a))return m.call(this,L,a);if(!L||0>c)return m.call(this,L,a-1);(w=L[0],0<=y.call(g,w))?c+=1:(l=L[0],0<=y.call(h,l))&&--c;a+=1}return a-1};l.prototype.removeLeadingNewlines=function(){var a,b;var m=this.tokens;var k=a=0;for(b=m.length;a<b;k=++a){var g=m[k][0];if("TERMINATOR"!==
-g)break}if(k)return this.tokens.splice(0,k)};l.prototype.closeOpenCalls=function(){var a=function(a,c){var k;return")"===(k=a[0])||"CALL_END"===k||"OUTDENT"===a[0]&&")"===this.tag(c-1)};var b=function(a,c){return this.tokens["OUTDENT"===a[0]?c-1:c][0]="CALL_END"};return this.scanTokens(function(c,k){"CALL_START"===c[0]&&this.detectEnd(k+1,a,b);return 1})};l.prototype.closeOpenIndexes=function(){var a=function(a,c){var k;return"]"===(k=a[0])||"INDEX_END"===k};var b=function(a,c){return a[0]="INDEX_END"};
-return this.scanTokens(function(c,k){"INDEX_START"===c[0]&&this.detectEnd(k+1,a,b);return 1})};l.prototype.indexOfTag=function(){var c,b,g,k;var l=arguments[0];var h=2<=arguments.length?a.call(arguments,1):[];var f=b=c=0;for(g=h.length;0<=g?b<g:b>g;f=0<=g?++b:--b){for(;"HERECOMMENT"===this.tag(l+f+c);)c+=2;if(null!=h[f]&&("string"===typeof h[f]&&(h[f]=[h[f]]),k=this.tag(l+f+c),0>y.call(h[f],k)))return-1}return l+f+c-1};l.prototype.looksObjectish=function(a){if(-1<this.indexOfTag(a,"@",null,":")||
--1<this.indexOfTag(a,null,":"))return!0;a=this.indexOfTag(a,g);if(-1<a){var c=null;this.detectEnd(a+1,function(a){var c;return c=a[0],0<=y.call(h,c)},function(a,b){return c=b});if(":"===this.tag(c+1))return!0}return!1};l.prototype.findTagsBackwards=function(a,b){var c,k,l,w,f,n,x;for(c=[];0<=a&&(c.length||(w=this.tag(a),0>y.call(b,w))&&((f=this.tag(a),0>y.call(g,f))||this.tokens[a].generated)&&(n=this.tag(a),0>y.call(R,n)));)(k=this.tag(a),0<=y.call(h,k))&&c.push(this.tag(a)),(l=this.tag(a),0<=y.call(g,
-l))&&c.length&&c.pop(),--a;return x=this.tag(a),0<=y.call(b,x)};l.prototype.addImplicitBracesAndParens=function(){var a=[];var l=null;return this.scanTokens(function(c,k,f){var m,w,n,r;var G=c[0];var K=(m=0<k?f[k-1]:[])[0];var u=(k<f.length-1?f[k+1]:[])[0];var B=function(){return a[a.length-1]};var D=k;var A=function(a){return k-D+a};var H=function(a){var b;return null!=a?null!=(b=a[2])?b.ours:void 0:void 0};var E=function(a){return H(a)&&"{"===(null!=a?a[0]:void 0)};var J=function(a){return H(a)&&
-"("===(null!=a?a[0]:void 0)};var O=function(){return H(B())};var C=function(){return J(B())};var T=function(){return E(B())};var v=function(){var a;return O&&"CONTROL"===(null!=(a=B())?a[0]:void 0)};var Y=function(c){var g=null!=c?c:k;a.push(["(",g,{ours:!0}]);f.splice(g,0,b("CALL_START","("));if(null==c)return k+=1};var S=function(){a.pop();f.splice(k,0,b("CALL_END",")",["","end of input",c[2]]));return k+=1};var M=function(g,l){null==l&&(l=!0);var m=null!=g?g:k;a.push(["{",m,{sameLine:!0,startsLine:l,
-ours:!0}]);l=new String("{");l.generated=!0;f.splice(m,0,b("{",l,c));if(null==g)return k+=1};var q=function(g){g=null!=g?g:k;a.pop();f.splice(g,0,b("}","}",c));return k+=1};if(C()&&("IF"===G||"TRY"===G||"FINALLY"===G||"CATCH"===G||"CLASS"===G||"SWITCH"===G))return a.push(["CONTROL",k,{ours:!0}]),A(1);if("INDENT"===G&&O()){if("\x3d\x3e"!==K&&"-\x3e"!==K&&"["!==K&&"("!==K&&","!==K&&"{"!==K&&"TRY"!==K&&"ELSE"!==K&&"\x3d"!==K)for(;C();)S();v()&&a.pop();a.push([G,k]);return A(1)}if(0<=y.call(g,G))return a.push([G,
-k]),A(1);if(0<=y.call(h,G)){for(;O();)C()?S():T()?q():a.pop();l=a.pop()}if((0<=y.call(I,G)&&c.spaced||"?"===G&&0<k&&!f[k-1].spaced)&&(0<=y.call(F,u)||0<=y.call(Q,u)&&(null==(w=f[k+1])||!w.spaced)&&(null==(n=f[k+1])||!n.newLine)))return"?"===G&&(G=c[0]="FUNC_EXIST"),Y(k+1),A(2);if(0<=y.call(I,G)&&-1<this.indexOfTag(k+1,"INDENT")&&this.looksObjectish(k+2)&&!this.findTagsBackwards(k,"CLASS EXTENDS IF CATCH SWITCH LEADING_WHEN FOR WHILE UNTIL".split(" ")))return Y(k+1),a.push(["INDENT",k+2]),A(3);if(":"===
-G){for(q=function(){var a;switch(!1){case a=this.tag(k-1),0>y.call(h,a):return l[1];case "@"!==this.tag(k-2):return k-2;default:return k-1}}.call(this);"HERECOMMENT"===this.tag(q-2);)q-=2;this.insideForDeclaration="FOR"===u;m=0===q||(r=this.tag(q-1),0<=y.call(R,r))||f[q-1].newLine;if(B()&&(T=B(),r=T[0],v=T[1],("{"===r||"INDENT"===r&&"{"===this.tag(v-1))&&(m||","===this.tag(q-1)||"{"===this.tag(q-1))))return A(1);M(q,!!m);return A(2)}if(0<=y.call(R,G))for(M=a.length-1;0<=M;M+=-1)r=a[M],E(r)&&(r[2].sameLine=
-!1);M="OUTDENT"===K||m.newLine;if(0<=y.call(x,G)||0<=y.call(z,G)&&M)for(;O();)if(M=B(),r=M[0],v=M[1],m=M[2],M=m.sameLine,m=m.startsLine,C()&&","!==K)S();else if(T()&&!this.insideForDeclaration&&M&&"TERMINATOR"!==G&&":"!==K)q();else if(!T()||"TERMINATOR"!==G||","===K||m&&this.looksObjectish(k+1))break;else{if("HERECOMMENT"===u)return A(1);q()}if(!(","!==G||this.looksObjectish(k+1)||!T()||this.insideForDeclaration||"TERMINATOR"===u&&this.looksObjectish(k+2)))for(u="OUTDENT"===u?1:0;T();)q(k+u);return A(1)})};
-l.prototype.addLocationDataToGeneratedTokens=function(){return this.scanTokens(function(a,b,g){var c,l;if(a[2]||!a.generated&&!a.explicit)return 1;if("{"===a[0]&&(c=null!=(l=g[b+1])?l[2]:void 0)){var m=c.first_line;c=c.first_column}else(c=null!=(m=g[b-1])?m[2]:void 0)?(m=c.last_line,c=c.last_column):m=c=0;a[2]={first_line:m,first_column:c,last_line:m,last_column:c};return 1})};l.prototype.fixOutdentLocationData=function(){return this.scanTokens(function(a,b,g){if(!("OUTDENT"===a[0]||a.generated&&
-"CALL_END"===a[0]||a.generated&&"}"===a[0]))return 1;b=g[b-1][2];a[2]={first_line:b.last_line,first_column:b.last_column,last_line:b.last_line,last_column:b.last_column};return 1})};l.prototype.normalizeLines=function(){var b,g;var l=b=g=null;var k=function(a,b){var c,g,k,f;return";"!==a[1]&&(c=a[0],0<=y.call(O,c))&&!("TERMINATOR"===a[0]&&(g=this.tag(b+1),0<=y.call(H,g)))&&!("ELSE"===a[0]&&"THEN"!==l)&&!!("CATCH"!==(k=a[0])&&"FINALLY"!==k||"-\x3e"!==l&&"\x3d\x3e"!==l)||(f=a[0],0<=y.call(z,f))&&(this.tokens[b-
-1].newLine||"OUTDENT"===this.tokens[b-1][0])};var f=function(a,b){return this.tokens.splice(","===this.tag(b-1)?b-1:b,0,g)};return this.scanTokens(function(c,m,h){var w,n,r;c=c[0];if("TERMINATOR"===c){if("ELSE"===this.tag(m+1)&&"OUTDENT"!==this.tag(m-1))return h.splice.apply(h,[m,1].concat(a.call(this.indentation()))),1;if(w=this.tag(m+1),0<=y.call(H,w))return h.splice(m,1),0}if("CATCH"===c)for(w=n=1;2>=n;w=++n)if("OUTDENT"===(r=this.tag(m+w))||"TERMINATOR"===r||"FINALLY"===r)return h.splice.apply(h,
-[m+w,0].concat(a.call(this.indentation()))),2+w;0<=y.call(J,c)&&"INDENT"!==this.tag(m+1)&&("ELSE"!==c||"IF"!==this.tag(m+1))&&(l=c,r=this.indentation(h[m]),b=r[0],g=r[1],"THEN"===l&&(b.fromThen=!0),h.splice(m+1,0,b),this.detectEnd(m+2,k,f),"THEN"===c&&h.splice(m,1));return 1})};l.prototype.tagPostfixConditionals=function(){var a=null;var b=function(a,b){a=a[0];b=this.tokens[b-1][0];return"TERMINATOR"===a||"INDENT"===a&&0>y.call(J,b)};var g=function(b,c){if("INDENT"!==b[0]||b.generated&&!b.fromThen)return a[0]=
-"POST_"+a[0]};return this.scanTokens(function(c,l){if("IF"!==c[0])return 1;a=c;this.detectEnd(l+1,b,g);return 1})};l.prototype.indentation=function(a){var b=["INDENT",2];var c=["OUTDENT",2];a?(b.generated=c.generated=!0,b.origin=c.origin=a):b.explicit=c.explicit=!0;return[b,c]};l.prototype.generate=b;l.prototype.tag=function(a){var b;return null!=(b=this.tokens[a])?b[0]:void 0};return l}();var ya=[["(",")"],["[","]"],["{","}"],["INDENT","OUTDENT"],["CALL_START","CALL_END"],["PARAM_START","PARAM_END"],
-["INDEX_START","INDEX_END"],["STRING_START","STRING_END"],["REGEX_START","REGEX_END"]];f.INVERSES=u={};var g=[];var h=[];var r=0;for(q=ya.length;r<q;r++){var n=ya[r];var B=n[0];n=n[1];g.push(u[n]=B);h.push(u[B]=n)}var H=["CATCH","THEN","ELSE","FINALLY"].concat(h);var I="IDENTIFIER PROPERTY SUPER ) CALL_END ] INDEX_END @ THIS".split(" ");var F="IDENTIFIER PROPERTY NUMBER INFINITY NAN STRING STRING_START REGEX REGEX_START JS NEW PARAM_START CLASS IF TRY SWITCH THIS UNDEFINED NULL BOOL UNARY YIELD UNARY_MATH SUPER THROW @ -\x3e \x3d\x3e [ ( { -- ++".split(" ");
-var Q=["+","-"];var x="POST_IF FOR WHILE UNTIL WHEN BY LOOP TERMINATOR".split(" ");var J="ELSE -\x3e \x3d\x3e TRY FINALLY THEN".split(" ");var O="TERMINATOR CATCH FINALLY ELSE OUTDENT LEADING_WHEN".split(" ");var R=["TERMINATOR","INDENT","OUTDENT"];var z=[".","?.","::","?::"]}).call(this);return f}();u["./lexer"]=function(){var f={};(function(){var qa,q=[].indexOf||function(a){for(var N=0,b=this.length;N<b;N++)if(N in this&&this[N]===a)return N;return-1},y=[].slice;var a=u("./rewriter");var b=a.Rewriter;
-var ya=a.INVERSES;a=u("./helpers");var g=a.count;var h=a.repeat;var r=a.invertLiterate;var n=a.throwSyntaxError;f.Lexer=function(){function a(){}a.prototype.tokenize=function(a,c){var N,g;null==c&&(c={});this.literate=c.literate;this.outdebt=this.indebt=this.baseIndent=this.indent=0;this.indents=[];this.ends=[];this.tokens=[];this.exportSpecifierList=this.importSpecifierList=this.seenExport=this.seenImport=this.seenFor=!1;this.chunkLine=c.line||0;this.chunkColumn=c.column||0;a=this.clean(a);for(g=
-0;this.chunk=a.slice(g);){var l=this.identifierToken()||this.commentToken()||this.whitespaceToken()||this.lineToken()||this.stringToken()||this.numberToken()||this.regexToken()||this.jsToken()||this.literalToken();var k=this.getLineAndColumnFromChunk(l);this.chunkLine=k[0];this.chunkColumn=k[1];g+=l;if(c.untilBalanced&&0===this.ends.length)return{tokens:this.tokens,index:g}}this.closeIndentation();(N=this.ends.pop())&&this.error("missing "+N.tag,N.origin[2]);return!1===c.rewrite?this.tokens:(new b).rewrite(this.tokens)};
-a.prototype.clean=function(a){a.charCodeAt(0)===R&&(a=a.slice(1));a=a.replace(/\r/g,"").replace(Z,"");w.test(a)&&(a="\n"+a,this.chunkLine--);this.literate&&(a=r(a));return a};a.prototype.identifierToken=function(){var a,b,c,g,l,k,m;if(!(a=z.exec(this.chunk)))return 0;var f=a[0];var h=a[1];a=a[2];var y=h.length;var w=void 0;if("own"===h&&"FOR"===this.tag())return this.token("OWN",h),h.length;if("from"===h&&"YIELD"===this.tag())return this.token("FROM",h),h.length;if("as"===h&&this.seenImport){if("*"===
-this.value())this.tokens[this.tokens.length-1][0]="IMPORT_ALL";else if(b=this.value(),0<=q.call(F,b))this.tokens[this.tokens.length-1][0]="IDENTIFIER";if("DEFAULT"===(c=this.tag())||"IMPORT_ALL"===c||"IDENTIFIER"===c)return this.token("AS",h),h.length}if("as"===h&&this.seenExport&&("IDENTIFIER"===(g=this.tag())||"DEFAULT"===g))return this.token("AS",h),h.length;if("default"===h&&this.seenExport&&("EXPORT"===(l=this.tag())||"AS"===l))return this.token("DEFAULT",h),h.length;b=this.tokens;b=b[b.length-
-1];var n=a||null!=b&&("."===(k=b[0])||"?."===k||"::"===k||"?::"===k||!b.spaced&&"@"===b[0])?"PROPERTY":"IDENTIFIER";"IDENTIFIER"!==n||!(0<=q.call(I,h)||0<=q.call(F,h))||this.exportSpecifierList&&0<=q.call(F,h)?"IDENTIFIER"===n&&this.seenFor&&"from"===h&&H(b)&&(n="FORFROM",this.seenFor=!1):(n=h.toUpperCase(),"WHEN"===n&&(m=this.tag(),0<=q.call(ra,m))?n="LEADING_WHEN":"FOR"===n?this.seenFor=!0:"UNLESS"===n?n="IF":"IMPORT"===n?this.seenImport=!0:"EXPORT"===n?this.seenExport=!0:0<=q.call(ia,n)?n="UNARY":
-0<=q.call(pa,n)&&("INSTANCEOF"!==n&&this.seenFor?(n="FOR"+n,this.seenFor=!1):(n="RELATION","!"===this.value()&&(w=this.tokens.pop(),h="!"+h))));"IDENTIFIER"===n&&0<=q.call(J,h)&&this.error("reserved word '"+h+"'",{length:h.length});if("PROPERTY"!==n){if(0<=q.call(x,h)){var r=h;h=Q[h]}n=function(){switch(h){case "!":return"UNARY";case "\x3d\x3d":case "!\x3d":return"COMPARE";case "true":case "false":return"BOOL";case "break":case "continue":case "debugger":return"STATEMENT";case "\x26\x26":case "||":return h;
-default:return n}}()}k=this.token(n,h,0,y);r&&(k.origin=[n,r,k[2]]);w&&(r=[w[2].first_line,w[2].first_column],k[2].first_line=r[0],k[2].first_column=r[1]);a&&(r=f.lastIndexOf(":"),this.token(":",":",r,a.length));return f.length};a.prototype.numberToken=function(){var a,b;if(!(a=l.exec(this.chunk)))return 0;var c=a[0];a=c.length;switch(!1){case !/^0[BOX]/.test(c):this.error("radix prefix in '"+c+"' must be lowercase",{offset:1});break;case !/^(?!0x).*E/.test(c):this.error("exponential notation in '"+
-c+"' must be indicated with a lowercase 'e'",{offset:c.indexOf("E")});break;case !/^0\d*[89]/.test(c):this.error("decimal literal '"+c+"' must not be prefixed with '0'",{length:a});break;case !/^0\d+/.test(c):this.error("octal literal '"+c+"' must be prefixed with '0o'",{length:a})}var g=function(){switch(c.charAt(1)){case "b":return 2;case "o":return 8;case "x":return 16;default:return null}}();g=null!=g?parseInt(c.slice(2),g):parseFloat(c);if("b"===(b=c.charAt(1))||"o"===b)c="0x"+g.toString(16);
-this.token(Infinity===g?"INFINITY":"NUMBER",c,0,a);return a};a.prototype.stringToken=function(){var a,b,c,g,l;var k=(V.exec(this.chunk)||[])[0];if(!k)return 0;this.tokens.length&&"from"===this.value()&&(this.seenImport||this.seenExport)&&(this.tokens[this.tokens.length-1][0]="FROM");var h=function(){switch(k){case "'":return X;case '"':return G;case "'''":return aa;case '"""':return U}}();var m=3===k.length;h=this.matchWithInterpolations(h,k);var f=h.tokens;var n=h.index;var y=f.length-1;h=k.charAt(0);
-if(m){var w=null;for(m=function(){var a,c;var N=[];b=a=0;for(c=f.length;a<c;b=++a)l=f[b],"NEOSTRING"===l[0]&&N.push(l[1]);return N}().join("#{}");a=A.exec(m);)if(a=a[1],null===w||0<(g=a.length)&&g<w.length)w=a;w&&(c=RegExp("\\n"+w,"g"));this.mergeInterpolationTokens(f,{delimiter:h},function(a){return function(b,N){b=a.formatString(b,{delimiter:k});c&&(b=b.replace(c,"\n"));0===N&&(b=b.replace(za,""));N===y&&(b=b.replace(ma,""));return b}}(this))}else this.mergeInterpolationTokens(f,{delimiter:h},function(a){return function(b,
-N){b=a.formatString(b,{delimiter:k});return b=b.replace(D,function(a,p){return 0===N&&0===p||N===y&&p+a.length===b.length?"":" "})}}(this));return n};a.prototype.commentToken=function(){var a,b;if(!(b=this.chunk.match(m)))return 0;var c=b[0];if(a=b[1])(b=Y.exec(c))&&this.error("block comments cannot contain "+b[0],{offset:b.index,length:b[0].length}),0<=a.indexOf("\n")&&(a=a.replace(RegExp("\\n"+h(" ",this.indent),"g"),"\n")),this.token("HERECOMMENT",a,0,c.length);return c.length};a.prototype.jsToken=
-function(){var a;if("`"!==this.chunk.charAt(0)||!(a=L.exec(this.chunk)||P.exec(this.chunk)))return 0;var b=a[1].replace(/\\+(`|$)/g,function(a){return a.slice(-Math.ceil(a.length/2))});this.token("JS",b,0,a[0].length);return a[0].length};a.prototype.regexToken=function(){var a,b,c;switch(!1){case !(a=T.exec(this.chunk)):this.error("regular expressions cannot begin with "+a[2],{offset:a.index+a[1].length});break;case !(a=this.matchWithInterpolations(ca,"///")):var g=a.tokens;var k=a.index;break;case !(a=
-fc.exec(this.chunk)):var l=a[0];var h=a[1];a=a[2];this.validateEscapes(h,{isRegex:!0,offsetInChunk:1});h=this.formatRegex(h,{delimiter:"/"});k=l.length;var m=this.tokens;if(m=m[m.length-1])if(m.spaced&&(b=m[0],0<=q.call(ha,b))){if(!a||v.test(l))return 0}else if(c=m[0],0<=q.call(na,c))return 0;a||this.error("missing / (unclosed regex)");break;default:return 0}c=E.exec(this.chunk.slice(k))[0];b=k+c.length;a=this.makeToken("REGEX",null,0,b);switch(!1){case !!ba.test(c):this.error("invalid regular expression flags "+
-c,{offset:k,length:c.length});break;case !(l||1===g.length):null==h&&(h=this.formatHeregex(g[0][1]));this.token("REGEX",""+this.makeDelimitedLiteral(h,{delimiter:"/"})+c,0,b,a);break;default:this.token("REGEX_START","(",0,0,a),this.token("IDENTIFIER","RegExp",0,0),this.token("CALL_START","(",0,0),this.mergeInterpolationTokens(g,{delimiter:'"',double:!0},this.formatHeregex),c&&(this.token(",",",",k-1,0),this.token("STRING",'"'+c+'"',k-1,c.length)),this.token(")",")",b-1,0),this.token("REGEX_END",")",
-b-1,0)}return b};a.prototype.lineToken=function(){var a;if(!(a=K.exec(this.chunk)))return 0;a=a[0];this.seenFor=!1;this.importSpecifierList||(this.seenImport=!1);this.exportSpecifierList||(this.seenExport=!1);var b=a.length-1-a.lastIndexOf("\n");var c=this.unfinished();if(b-this.indebt===this.indent)return c?this.suppressNewlines():this.newlineToken(0),a.length;if(b>this.indent){if(c||"RETURN"===this.tag())return this.indebt=b-this.indent,this.suppressNewlines(),a.length;if(!this.tokens.length)return this.baseIndent=
-this.indent=b,a.length;c=b-this.indent+this.outdebt;this.token("INDENT",c,a.length-b,b);this.indents.push(c);this.ends.push({tag:"OUTDENT"});this.outdebt=this.indebt=0;this.indent=b}else b<this.baseIndent?this.error("missing indentation",{offset:a.length}):(this.indebt=0,this.outdentToken(this.indent-b,c,a.length));return a.length};a.prototype.outdentToken=function(a,b,c){var g,N,k;for(g=this.indent-a;0<a;)if(N=this.indents[this.indents.length-1])if(N===this.outdebt)a-=this.outdebt,this.outdebt=0;
-else if(N<this.outdebt)this.outdebt-=N,a-=N;else{var h=this.indents.pop()+this.outdebt;c&&(k=this.chunk[c],0<=q.call(da,k))&&(g-=h-a,a=h);this.outdebt=0;this.pair("OUTDENT");this.token("OUTDENT",a,0,c);a-=h}else a=0;h&&(this.outdebt-=a);for(;";"===this.value();)this.tokens.pop();"TERMINATOR"===this.tag()||b||this.token("TERMINATOR","\n",c,0);this.indent=g;return this};a.prototype.whitespaceToken=function(){var a;if(!(a=w.exec(this.chunk))&&"\n"!==this.chunk.charAt(0))return 0;var b=this.tokens;(b=
-b[b.length-1])&&(b[a?"spaced":"newLine"]=!0);return a?a[0].length:0};a.prototype.newlineToken=function(a){for(;";"===this.value();)this.tokens.pop();"TERMINATOR"!==this.tag()&&this.token("TERMINATOR","\n",a,0);return this};a.prototype.suppressNewlines=function(){"\\"===this.value()&&this.tokens.pop();return this};a.prototype.literalToken=function(){var a,b,g,h,l;(a=c.exec(this.chunk))?(a=a[0],k.test(a)&&this.tagParameters()):a=this.chunk.charAt(0);var m=a;var f=this.tokens;if((f=f[f.length-1])&&0<=
-q.call(["\x3d"].concat(y.call(fa)),a)){var n=!1;"\x3d"!==a||"||"!==(g=f[1])&&"\x26\x26"!==g||f.spaced||(f[0]="COMPOUND_ASSIGN",f[1]+="\x3d",f=this.tokens[this.tokens.length-2],n=!0);f&&"PROPERTY"!==f[0]&&(g=null!=(b=f.origin)?b:f,(b=B(f[1],g[1]))&&this.error(b,g[2]));if(n)return a.length}"{"===a&&this.seenImport?this.importSpecifierList=!0:this.importSpecifierList&&"}"===a?this.importSpecifierList=!1:"{"===a&&"EXPORT"===(null!=f?f[0]:void 0)?this.exportSpecifierList=!0:this.exportSpecifierList&&"}"===
-a&&(this.exportSpecifierList=!1);if(";"===a)this.seenFor=this.seenImport=this.seenExport=!1,m="TERMINATOR";else if("*"===a&&"EXPORT"===f[0])m="EXPORT_ALL";else if(0<=q.call(oa,a))m="MATH";else if(0<=q.call(la,a))m="COMPARE";else if(0<=q.call(fa,a))m="COMPOUND_ASSIGN";else if(0<=q.call(ia,a))m="UNARY";else if(0<=q.call(ga,a))m="UNARY_MATH";else if(0<=q.call(ja,a))m="SHIFT";else if("?"===a&&null!=f&&f.spaced)m="BIN?";else if(f&&!f.spaced)if("("===a&&(h=f[0],0<=q.call(ha,h)))"?"===f[0]&&(f[0]="FUNC_EXIST"),
-m="CALL_START";else if("["===a&&(l=f[0],0<=q.call(ka,l)))switch(m="INDEX_START",f[0]){case "?":f[0]="INDEX_SOAK"}h=this.makeToken(m,a);switch(a){case "(":case "{":case "[":this.ends.push({tag:ya[a],origin:h});break;case ")":case "}":case "]":this.pair(a)}this.tokens.push(h);return a.length};a.prototype.tagParameters=function(){var a;if(")"!==this.tag())return this;var b=[];var c=this.tokens;var g=c.length;for(c[--g][0]="PARAM_END";a=c[--g];)switch(a[0]){case ")":b.push(a);break;case "(":case "CALL_START":if(b.length)b.pop();
-else return"("===a[0]&&(a[0]="PARAM_START"),this}return this};a.prototype.closeIndentation=function(){return this.outdentToken(this.indent)};a.prototype.matchWithInterpolations=function(b,c){var g,h;var k=[];var l=c.length;if(this.chunk.slice(0,l)!==c)return null;for(h=this.chunk.slice(l);;){var f=b.exec(h)[0];this.validateEscapes(f,{isRegex:"/"===c.charAt(0),offsetInChunk:l});k.push(this.makeToken("NEOSTRING",f,l));h=h.slice(f.length);l+=f.length;if("#{"!==h.slice(0,2))break;var m=this.getLineAndColumnFromChunk(l+
-1);f=m[0];m=m[1];m=(new a).tokenize(h.slice(1),{line:f,column:m,untilBalanced:!0});f=m.tokens;var N=m.index;N+=1;var n=f[0];m=f[f.length-1];n[0]=n[1]="(";m[0]=m[1]=")";m.origin=["","end of interpolation",m[2]];"TERMINATOR"===(null!=(g=f[1])?g[0]:void 0)&&f.splice(1,1);k.push(["TOKENS",f]);h=h.slice(N);l+=N}h.slice(0,c.length)!==c&&this.error("missing "+c,{length:c.length});b=k[0];g=k[k.length-1];b[2].first_column-=c.length;"\n"===g[1].substr(-1)?(g[2].last_line+=1,g[2].last_column=c.length-1):g[2].last_column+=
-c.length;0===g[1].length&&--g[2].last_column;return{tokens:k,index:l+c.length}};a.prototype.mergeInterpolationTokens=function(a,b,c){var g,h,k,f;1<a.length&&(k=this.token("STRING_START","(",0,0));var l=this.tokens.length;var m=g=0;for(h=a.length;g<h;m=++g){var n=a[m];var N=n[0];var y=n[1];switch(N){case "TOKENS":if(2===y.length)continue;var w=y[0];var r=y;break;case "NEOSTRING":N=c.call(this,n[1],m);if(0===N.length)if(0===m)var Ha=this.tokens.length;else continue;2===m&&null!=Ha&&this.tokens.splice(Ha,
-2);n[0]="STRING";n[1]=this.makeDelimitedLiteral(N,b);w=n;r=[n]}this.tokens.length>l&&(m=this.token("+","+"),m[2]={first_line:w[2].first_line,first_column:w[2].first_column,last_line:w[2].first_line,last_column:w[2].first_column});(f=this.tokens).push.apply(f,r)}if(k)return a=a[a.length-1],k.origin=["STRING",null,{first_line:k[2].first_line,first_column:k[2].first_column,last_line:a[2].last_line,last_column:a[2].last_column}],k=this.token("STRING_END",")"),k[2]={first_line:a[2].last_line,first_column:a[2].last_column,
-last_line:a[2].last_line,last_column:a[2].last_column}};a.prototype.pair=function(a){var b=this.ends;b=b[b.length-1];return a!==(b=null!=b?b.tag:void 0)?("OUTDENT"!==b&&this.error("unmatched "+a),b=this.indents,b=b[b.length-1],this.outdentToken(b,!0),this.pair(a)):this.ends.pop()};a.prototype.getLineAndColumnFromChunk=function(a){if(0===a)return[this.chunkLine,this.chunkColumn];var b=a>=this.chunk.length?this.chunk:this.chunk.slice(0,+(a-1)+1||9E9);a=g(b,"\n");var c=this.chunkColumn;0<a?(c=b.split("\n"),
-c=c[c.length-1],c=c.length):c+=b.length;return[this.chunkLine+a,c]};a.prototype.makeToken=function(a,b,c,g){null==c&&(c=0);null==g&&(g=b.length);var k={};var h=this.getLineAndColumnFromChunk(c);k.first_line=h[0];k.first_column=h[1];c=this.getLineAndColumnFromChunk(c+(0<g?g-1:0));k.last_line=c[0];k.last_column=c[1];return[a,b,k]};a.prototype.token=function(a,b,c,g,k){a=this.makeToken(a,b,c,g);k&&(a.origin=k);this.tokens.push(a);return a};a.prototype.tag=function(){var a=this.tokens;a=a[a.length-1];
-return null!=a?a[0]:void 0};a.prototype.value=function(){var a=this.tokens;a=a[a.length-1];return null!=a?a[1]:void 0};a.prototype.unfinished=function(){var a;return S.test(this.chunk)||"\\"===(a=this.tag())||"."===a||"?."===a||"?::"===a||"UNARY"===a||"MATH"===a||"UNARY_MATH"===a||"+"===a||"-"===a||"**"===a||"SHIFT"===a||"RELATION"===a||"COMPARE"===a||"\x26"===a||"^"===a||"|"===a||"\x26\x26"===a||"||"===a||"BIN?"===a||"THROW"===a||"EXTENDS"===a||"DEFAULT"===a};a.prototype.formatString=function(a,
-b){return this.replaceUnicodeCodePointEscapes(a.replace(W,"$1"),b)};a.prototype.formatHeregex=function(a){return this.formatRegex(a.replace(C,"$1$2"),{delimiter:"///"})};a.prototype.formatRegex=function(a,b){return this.replaceUnicodeCodePointEscapes(a,b)};a.prototype.unicodeCodePointToUnicodeEscapes=function(a){var b=function(a){a=a.toString(16);return"\\u"+h("0",4-a.length)+a};if(65536>a)return b(a);var c=Math.floor((a-65536)/1024)+55296;a=(a-65536)%1024+56320;return""+b(c)+b(a)};a.prototype.replaceUnicodeCodePointEscapes=
-function(a,b){return a.replace(sa,function(a){return function(c,g,k,h){if(g)return g;c=parseInt(k,16);1114111<c&&a.error("unicode code point escapes greater than \\u{10ffff} are not allowed",{offset:h+b.delimiter.length,length:k.length+4});return a.unicodeCodePointToUnicodeEscapes(c)}}(this))};a.prototype.validateEscapes=function(a,b){var c,g;null==b&&(b={});if(c=(b.isRegex?va:M).exec(a)){c[0];a=c[1];var k=c[2];var h=c[3];var f=c[4];var l=c[5];h="\\"+(k||h||f||l);return this.error((k?"octal escape sequences are not allowed":
-"invalid escape sequence")+" "+h,{offset:(null!=(g=b.offsetInChunk)?g:0)+c.index+a.length,length:h.length})}};a.prototype.makeDelimitedLiteral=function(a,b){null==b&&(b={});""===a&&"/"===b.delimiter&&(a="(?:)");a=a.replace(RegExp("(\\\\\\\\)|(\\\\0(?\x3d[1-7]))|\\\\?("+b.delimiter+")|\\\\?(?:(\\n)|(\\r)|(\\u2028)|(\\u2029))|(\\\\.)","g"),function(a,c,g,k,h,f,l,m,n){switch(!1){case !c:return b.double?c+c:c;case !g:return"\\x00";case !k:return"\\"+k;case !h:return"\\n";case !f:return"\\r";case !l:return"\\u2028";
-case !m:return"\\u2029";case !n:return b.double?"\\"+n:n}});return""+b.delimiter+a+b.delimiter};a.prototype.error=function(a,b){var c,g,k,h,f;null==b&&(b={});b="first_line"in b?b:(h=this.getLineAndColumnFromChunk(null!=(k=b.offset)?k:0),g=h[0],c=h[1],h,{first_line:g,first_column:c,last_column:c+(null!=(f=b.length)?f:1)-1});return n(a,b)};return a}();var B=function(a,b){null==b&&(b=a);switch(!1){case 0>q.call(y.call(I).concat(y.call(F)),a):return"keyword '"+b+"' can't be assigned";case 0>q.call(O,
-a):return"'"+b+"' can't be assigned";case 0>q.call(J,a):return"reserved word '"+b+"' can't be assigned";default:return!1}};f.isUnassignable=B;var H=function(a){var b;return"IDENTIFIER"===a[0]?("from"===a[1]&&(a[1][0]="IDENTIFIER",!0),!0):"FOR"===a[0]?!1:"{"===(b=a[1])||"["===b||","===b||":"===b?!1:!0};var I="true false null this new delete typeof in instanceof return throw break continue debugger yield if else switch for while do try catch finally class extends super import export default".split(" ");
-var F="undefined Infinity NaN then unless until loop of by when".split(" ");var Q={and:"\x26\x26",or:"||",is:"\x3d\x3d",isnt:"!\x3d",not:"!",yes:"true",no:"false",on:"true",off:"false"};var x=function(){var a=[];for(qa in Q)a.push(qa);return a}();F=F.concat(x);var J="case function var void with const let enum native implements interface package private protected public static".split(" ");var O=["arguments","eval"];f.JS_FORBIDDEN=I.concat(J).concat(O);var R=65279;var z=/^(?!\d)((?:(?!\s)[$\w\x7f-\uffff])+)([^\n\S]*:(?!:))?/;
-var l=/^0b[01]+|^0o[0-7]+|^0x[\da-f]+|^\d*\.?\d+(?:e[+-]?\d+)?/i;var c=/^(?:[-=]>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>*\/%])\2=?|\?(\.|::)|\.{2,3})/;var w=/^[^\n\S]+/;var m=/^###([^#][\s\S]*?)(?:###[^\n\S]*|###$)|^(?:\s*#(?!##[^#]).*)+/;var k=/^[-=]>/;var K=/^(?:\n[^\n\S]*)+/;var P=/^`(?!``)((?:[^`\\]|\\[\s\S])*)`/;var L=/^```((?:[^`\\]|\\[\s\S]|`(?!``))*)```/;var V=/^(?:'''|"""|'|")/;var X=/^(?:[^\\']|\\[\s\S])*/;var G=/^(?:[^\\"#]|\\[\s\S]|\#(?!\{))*/;var aa=/^(?:[^\\']|\\[\s\S]|'(?!''))*/;
-var U=/^(?:[^\\"#]|\\[\s\S]|"(?!"")|\#(?!\{))*/;var W=/((?:\\\\)+)|\\[^\S\n]*\n\s*/g;var D=/\s*\n\s*/g;var A=/\n+([^\n\S]*)(?=\S)/g;var fc=/^\/(?!\/)((?:[^[\/\n\\]|\\[^\n]|\[(?:\\[^\n]|[^\]\n\\])*\])*)(\/)?/;var E=/^\w*/;var ba=/^(?!.*(.).*\1)[imguy]*$/;var ca=/^(?:[^\\\/#]|\\[\s\S]|\/(?!\/\/)|\#(?!\{))*/;var C=/((?:\\\\)+)|\\(\s)|\s+(?:#.*)?/g;var T=/^(\/|\/{3}\s*)(\*)/;var v=/^\/=?\s/;var Y=/\*\//;var S=/^\s*(?:,|\??\.(?![.\d])|::)/;var M=/((?:^|[^\\])(?:\\\\)*)\\(?:(0[0-7]|[1-7])|(x(?![\da-fA-F]{2}).{0,2})|(u\{(?![\da-fA-F]{1,}\})[^}]*\}?)|(u(?!\{|[\da-fA-F]{4}).{0,4}))/;
-var va=/((?:^|[^\\])(?:\\\\)*)\\(?:(0[0-7])|(x(?![\da-fA-F]{2}).{0,2})|(u\{(?![\da-fA-F]{1,}\})[^}]*\}?)|(u(?!\{|[\da-fA-F]{4}).{0,4}))/;var sa=/(\\\\)|\\u\{([\da-fA-F]+)\}/g;var za=/^[^\n\S]*\n/;var ma=/\n[^\n\S]*$/;var Z=/\s+$/;var fa="-\x3d +\x3d /\x3d *\x3d %\x3d ||\x3d \x26\x26\x3d ?\x3d \x3c\x3c\x3d \x3e\x3e\x3d \x3e\x3e\x3e\x3d \x26\x3d ^\x3d |\x3d **\x3d //\x3d %%\x3d".split(" ");var ia=["NEW","TYPEOF","DELETE","DO"];var ga=["!","~"];var ja=["\x3c\x3c","\x3e\x3e","\x3e\x3e\x3e"];var la="\x3d\x3d !\x3d \x3c \x3e \x3c\x3d \x3e\x3d".split(" ");
-var oa=["*","/","%","//","%%"];var pa=["IN","OF","INSTANCEOF"];var ha="IDENTIFIER PROPERTY ) ] ? @ THIS SUPER".split(" ");var ka=ha.concat("NUMBER INFINITY NAN STRING STRING_END REGEX REGEX_END BOOL NULL UNDEFINED } ::".split(" "));var na=ka.concat(["++","--"]);var ra=["INDENT","OUTDENT","TERMINATOR"];var da=[")","}","]"]}).call(this);return f}();u["./parser"]=function(){var f={},qa={exports:f},q=function(){function f(){this.yy={}}var a=function(a,p,t,d){t=t||{};for(d=a.length;d--;t[a[d]]=p);return t},
-b=[1,22],u=[1,25],g=[1,83],h=[1,79],r=[1,84],n=[1,85],B=[1,81],H=[1,82],I=[1,56],F=[1,58],Q=[1,59],x=[1,60],J=[1,61],O=[1,62],R=[1,49],z=[1,50],l=[1,32],c=[1,68],w=[1,69],m=[1,78],k=[1,47],K=[1,51],P=[1,52],L=[1,67],V=[1,65],X=[1,66],G=[1,64],aa=[1,42],U=[1,48],W=[1,63],D=[1,73],A=[1,74],q=[1,75],E=[1,76],ba=[1,46],ca=[1,72],C=[1,34],T=[1,35],v=[1,36],Y=[1,37],S=[1,38],M=[1,39],qa=[1,86],sa=[1,6,32,42,131],za=[1,101],ma=[1,89],Z=[1,88],fa=[1,87],ia=[1,90],ga=[1,91],ja=[1,92],la=[1,93],oa=[1,94],pa=
-[1,95],ha=[1,96],ka=[1,97],na=[1,98],ra=[1,99],da=[1,100],va=[1,104],N=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],xa=[2,166],ta=[1,110],Na=[1,111],Fa=[1,112],Ga=[1,113],Ca=[1,115],Pa=[1,116],Ia=[1,109],Ea=[1,6,32,42,131,133,135,139,156],Va=[2,27],ea=[1,123],Ya=[1,121],Ba=[1,6,31,32,40,41,42,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,
-173,174],Ha=[2,94],t=[1,6,31,32,42,46,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],p=[2,73],d=[1,128],wa=[1,133],e=[1,134],Da=[1,136],Ta=[1,6,31,32,40,41,42,55,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],ua=[2,91],Eb=[1,6,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,
-169,170,171,172,173,174],Za=[2,63],Fb=[1,166],$a=[1,178],Ua=[1,180],Gb=[1,175],Oa=[1,182],sb=[1,184],La=[1,6,31,32,40,41,42,55,65,70,73,82,83,84,85,87,89,90,94,96,113,114,115,120,122,131,133,134,135,139,140,156,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175],Hb=[2,110],Ib=[1,6,31,32,40,41,42,58,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],Jb=[1,6,31,32,40,41,42,46,58,65,70,73,82,83,84,
-85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],Kb=[40,41,114],Lb=[1,241],tb=[1,240],Ma=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156],Ja=[2,71],Mb=[1,250],Sa=[6,31,32,65,70],fb=[6,31,32,55,65,70,73],ab=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,159,160,164,166,167,168,169,170,171,172,173,174],Nb=[40,41,82,83,84,85,87,90,113,114],gb=[1,269],bb=[2,62],hb=[1,279],Wa=[1,281],ub=[1,
-286],cb=[1,288],Ob=[2,187],vb=[1,6,31,32,40,41,42,55,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,146,147,148,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],ib=[1,297],Qa=[6,31,32,70,115,120],Pb=[1,6,31,32,40,41,42,55,58,65,70,73,82,83,84,85,87,89,90,94,96,113,114,115,120,122,131,133,134,135,139,140,146,147,148,156,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175],Qb=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,140,156],Xa=[1,6,31,32,
-42,65,70,73,89,94,115,120,122,131,134,140,156],jb=[146,147,148],kb=[70,146,147,148],lb=[6,31,94],Rb=[1,311],Aa=[6,31,32,70,94],Sb=[6,31,32,58,70,94],wb=[6,31,32,55,58,70,94],Tb=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,159,160,166,167,168,169,170,171,172,173,174],Ub=[12,28,34,38,40,41,44,45,48,49,50,51,52,53,61,62,63,67,68,89,92,95,97,105,112,117,118,119,125,129,130,133,135,137,139,149,155,157,158,159,160,161,162],Vb=[2,176],Ra=[6,31,32],db=[2,72],Wb=[1,323],Xb=[1,324],
-Yb=[1,6,31,32,42,65,70,73,89,94,115,120,122,127,128,131,133,134,135,139,140,151,153,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],mb=[32,151,153],Zb=[1,6,32,42,65,70,73,89,94,115,120,122,131,134,140,156],nb=[1,350],xb=[1,356],yb=[1,6,32,42,131,156],eb=[2,86],ob=[1,367],pb=[1,368],$b=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,151,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],zb=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,135,139,140,156],ac=
-[1,381],bc=[1,382],Ab=[6,31,32,94],cc=[6,31,32,70],Bb=[1,6,31,32,42,65,70,73,89,94,115,120,122,127,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],dc=[31,70],qb=[1,408],rb=[1,409],Cb=[1,415],Db=[1,416],ec={trace:function(){},yy:{},symbols_:{error:2,Root:3,Body:4,Line:5,TERMINATOR:6,Expression:7,Statement:8,YieldReturn:9,Return:10,Comment:11,STATEMENT:12,Import:13,Export:14,Value:15,Invocation:16,Code:17,Operation:18,Assign:19,If:20,Try:21,While:22,For:23,Switch:24,
-Class:25,Throw:26,Yield:27,YIELD:28,FROM:29,Block:30,INDENT:31,OUTDENT:32,Identifier:33,IDENTIFIER:34,Property:35,PROPERTY:36,AlphaNumeric:37,NUMBER:38,String:39,STRING:40,STRING_START:41,STRING_END:42,Regex:43,REGEX:44,REGEX_START:45,REGEX_END:46,Literal:47,JS:48,UNDEFINED:49,NULL:50,BOOL:51,INFINITY:52,NAN:53,Assignable:54,"\x3d":55,AssignObj:56,ObjAssignable:57,":":58,SimpleObjAssignable:59,ThisProperty:60,RETURN:61,HERECOMMENT:62,PARAM_START:63,ParamList:64,PARAM_END:65,FuncGlyph:66,"-\x3e":67,
-"\x3d\x3e":68,OptComma:69,",":70,Param:71,ParamVar:72,"...":73,Array:74,Object:75,Splat:76,SimpleAssignable:77,Accessor:78,Parenthetical:79,Range:80,This:81,".":82,"?.":83,"::":84,"?::":85,Index:86,INDEX_START:87,IndexValue:88,INDEX_END:89,INDEX_SOAK:90,Slice:91,"{":92,AssignList:93,"}":94,CLASS:95,EXTENDS:96,IMPORT:97,ImportDefaultSpecifier:98,ImportNamespaceSpecifier:99,ImportSpecifierList:100,ImportSpecifier:101,AS:102,DEFAULT:103,IMPORT_ALL:104,EXPORT:105,ExportSpecifierList:106,EXPORT_ALL:107,
-ExportSpecifier:108,OptFuncExist:109,Arguments:110,Super:111,SUPER:112,FUNC_EXIST:113,CALL_START:114,CALL_END:115,ArgList:116,THIS:117,"@":118,"[":119,"]":120,RangeDots:121,"..":122,Arg:123,SimpleArgs:124,TRY:125,Catch:126,FINALLY:127,CATCH:128,THROW:129,"(":130,")":131,WhileSource:132,WHILE:133,WHEN:134,UNTIL:135,Loop:136,LOOP:137,ForBody:138,FOR:139,BY:140,ForStart:141,ForSource:142,ForVariables:143,OWN:144,ForValue:145,FORIN:146,FOROF:147,FORFROM:148,SWITCH:149,Whens:150,ELSE:151,When:152,LEADING_WHEN:153,
-IfBlock:154,IF:155,POST_IF:156,UNARY:157,UNARY_MATH:158,"-":159,"+":160,"--":161,"++":162,"?":163,MATH:164,"**":165,SHIFT:166,COMPARE:167,"\x26":168,"^":169,"|":170,"\x26\x26":171,"||":172,"BIN?":173,RELATION:174,COMPOUND_ASSIGN:175,$accept:0,$end:1},terminals_:{2:"error",6:"TERMINATOR",12:"STATEMENT",28:"YIELD",29:"FROM",31:"INDENT",32:"OUTDENT",34:"IDENTIFIER",36:"PROPERTY",38:"NUMBER",40:"STRING",41:"STRING_START",42:"STRING_END",44:"REGEX",45:"REGEX_START",46:"REGEX_END",48:"JS",49:"UNDEFINED",
-50:"NULL",51:"BOOL",52:"INFINITY",53:"NAN",55:"\x3d",58:":",61:"RETURN",62:"HERECOMMENT",63:"PARAM_START",65:"PARAM_END",67:"-\x3e",68:"\x3d\x3e",70:",",73:"...",82:".",83:"?.",84:"::",85:"?::",87:"INDEX_START",89:"INDEX_END",90:"INDEX_SOAK",92:"{",94:"}",95:"CLASS",96:"EXTENDS",97:"IMPORT",102:"AS",103:"DEFAULT",104:"IMPORT_ALL",105:"EXPORT",107:"EXPORT_ALL",112:"SUPER",113:"FUNC_EXIST",114:"CALL_START",115:"CALL_END",117:"THIS",118:"@",119:"[",120:"]",122:"..",125:"TRY",127:"FINALLY",128:"CATCH",
-129:"THROW",130:"(",131:")",133:"WHILE",134:"WHEN",135:"UNTIL",137:"LOOP",139:"FOR",140:"BY",144:"OWN",146:"FORIN",147:"FOROF",148:"FORFROM",149:"SWITCH",151:"ELSE",153:"LEADING_WHEN",155:"IF",156:"POST_IF",157:"UNARY",158:"UNARY_MATH",159:"-",160:"+",161:"--",162:"++",163:"?",164:"MATH",165:"**",166:"SHIFT",167:"COMPARE",168:"\x26",169:"^",170:"|",171:"\x26\x26",172:"||",173:"BIN?",174:"RELATION",175:"COMPOUND_ASSIGN"},productions_:[0,[3,0],[3,1],[4,1],[4,3],[4,2],[5,1],[5,1],[5,1],[8,1],[8,1],[8,
-1],[8,1],[8,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[27,1],[27,2],[27,3],[30,2],[30,3],[33,1],[35,1],[37,1],[37,1],[39,1],[39,3],[43,1],[43,3],[47,1],[47,1],[47,1],[47,1],[47,1],[47,1],[47,1],[47,1],[19,3],[19,4],[19,5],[56,1],[56,3],[56,5],[56,3],[56,5],[56,1],[59,1],[59,1],[59,1],[57,1],[57,1],[10,2],[10,1],[9,3],[9,2],[11,1],[17,5],[17,2],[66,1],[66,1],[69,0],[69,1],[64,0],[64,1],[64,3],[64,4],[64,6],[71,1],[71,2],[71,3],[71,1],[72,1],[72,1],[72,1],[72,
-1],[76,2],[77,1],[77,2],[77,2],[77,1],[54,1],[54,1],[54,1],[15,1],[15,1],[15,1],[15,1],[15,1],[78,2],[78,2],[78,2],[78,2],[78,1],[78,1],[86,3],[86,2],[88,1],[88,1],[75,4],[93,0],[93,1],[93,3],[93,4],[93,6],[25,1],[25,2],[25,3],[25,4],[25,2],[25,3],[25,4],[25,5],[13,2],[13,4],[13,4],[13,5],[13,7],[13,6],[13,9],[100,1],[100,3],[100,4],[100,4],[100,6],[101,1],[101,3],[101,1],[101,3],[98,1],[99,3],[14,3],[14,5],[14,2],[14,4],[14,5],[14,6],[14,3],[14,4],[14,7],[106,1],[106,3],[106,4],[106,4],[106,6],[108,
-1],[108,3],[108,3],[108,1],[108,3],[16,3],[16,3],[16,3],[16,1],[111,1],[111,2],[109,0],[109,1],[110,2],[110,4],[81,1],[81,1],[60,2],[74,2],[74,4],[121,1],[121,1],[80,5],[91,3],[91,2],[91,2],[91,1],[116,1],[116,3],[116,4],[116,4],[116,6],[123,1],[123,1],[123,1],[124,1],[124,3],[21,2],[21,3],[21,4],[21,5],[126,3],[126,3],[126,2],[26,2],[79,3],[79,5],[132,2],[132,4],[132,2],[132,4],[22,2],[22,2],[22,2],[22,1],[136,2],[136,2],[23,2],[23,2],[23,2],[138,2],[138,4],[138,2],[141,2],[141,3],[145,1],[145,1],
-[145,1],[145,1],[143,1],[143,3],[142,2],[142,2],[142,4],[142,4],[142,4],[142,6],[142,6],[142,2],[142,4],[24,5],[24,7],[24,4],[24,6],[150,1],[150,2],[152,3],[152,4],[154,3],[154,5],[20,1],[20,3],[20,3],[20,3],[18,2],[18,2],[18,2],[18,2],[18,2],[18,2],[18,2],[18,2],[18,2],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,5],[18,4],[18,3]],performAction:function(a,p,t,d,wa,b,e){a=b.length-1;switch(wa){case 1:return this.$=d.addLocationDataFn(e[a],e[a])(new d.Block);
-case 2:return this.$=b[a];case 3:this.$=d.addLocationDataFn(e[a],e[a])(d.Block.wrap([b[a]]));break;case 4:this.$=d.addLocationDataFn(e[a-2],e[a])(b[a-2].push(b[a]));break;case 5:this.$=b[a-1];break;case 6:case 7:case 8:case 9:case 10:case 12:case 13:case 14:case 15:case 16:case 17:case 18:case 19:case 20:case 21:case 22:case 23:case 24:case 25:case 26:case 35:case 40:case 42:case 56:case 57:case 58:case 59:case 60:case 61:case 71:case 72:case 82:case 83:case 84:case 85:case 90:case 91:case 94:case 98:case 104:case 163:case 187:case 188:case 190:case 220:case 221:case 239:case 245:this.$=
-b[a];break;case 11:this.$=d.addLocationDataFn(e[a],e[a])(new d.StatementLiteral(b[a]));break;case 27:this.$=d.addLocationDataFn(e[a],e[a])(new d.Op(b[a],new d.Value(new d.Literal(""))));break;case 28:case 249:case 250:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op(b[a-1],b[a]));break;case 29:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Op(b[a-2].concat(b[a-1]),b[a]));break;case 30:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Block);break;case 31:case 105:this.$=d.addLocationDataFn(e[a-2],e[a])(b[a-
-1]);break;case 32:this.$=d.addLocationDataFn(e[a],e[a])(new d.IdentifierLiteral(b[a]));break;case 33:this.$=d.addLocationDataFn(e[a],e[a])(new d.PropertyName(b[a]));break;case 34:this.$=d.addLocationDataFn(e[a],e[a])(new d.NumberLiteral(b[a]));break;case 36:this.$=d.addLocationDataFn(e[a],e[a])(new d.StringLiteral(b[a]));break;case 37:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.StringWithInterpolations(b[a-1]));break;case 38:this.$=d.addLocationDataFn(e[a],e[a])(new d.RegexLiteral(b[a]));break;
-case 39:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.RegexWithInterpolations(b[a-1].args));break;case 41:this.$=d.addLocationDataFn(e[a],e[a])(new d.PassthroughLiteral(b[a]));break;case 43:this.$=d.addLocationDataFn(e[a],e[a])(new d.UndefinedLiteral);break;case 44:this.$=d.addLocationDataFn(e[a],e[a])(new d.NullLiteral);break;case 45:this.$=d.addLocationDataFn(e[a],e[a])(new d.BooleanLiteral(b[a]));break;case 46:this.$=d.addLocationDataFn(e[a],e[a])(new d.InfinityLiteral(b[a]));break;case 47:this.$=
-d.addLocationDataFn(e[a],e[a])(new d.NaNLiteral);break;case 48:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Assign(b[a-2],b[a]));break;case 49:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Assign(b[a-3],b[a]));break;case 50:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Assign(b[a-4],b[a-1]));break;case 51:case 87:case 92:case 93:case 95:case 96:case 97:case 222:case 223:this.$=d.addLocationDataFn(e[a],e[a])(new d.Value(b[a]));break;case 52:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Assign(d.addLocationDataFn(e[a-
-2])(new d.Value(b[a-2])),b[a],"object",{operatorToken:d.addLocationDataFn(e[a-1])(new d.Literal(b[a-1]))}));break;case 53:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Assign(d.addLocationDataFn(e[a-4])(new d.Value(b[a-4])),b[a-1],"object",{operatorToken:d.addLocationDataFn(e[a-3])(new d.Literal(b[a-3]))}));break;case 54:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Assign(d.addLocationDataFn(e[a-2])(new d.Value(b[a-2])),b[a],null,{operatorToken:d.addLocationDataFn(e[a-1])(new d.Literal(b[a-1]))}));
-break;case 55:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Assign(d.addLocationDataFn(e[a-4])(new d.Value(b[a-4])),b[a-1],null,{operatorToken:d.addLocationDataFn(e[a-3])(new d.Literal(b[a-3]))}));break;case 62:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Return(b[a]));break;case 63:this.$=d.addLocationDataFn(e[a],e[a])(new d.Return);break;case 64:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.YieldReturn(b[a]));break;case 65:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.YieldReturn);break;case 66:this.$=
-d.addLocationDataFn(e[a],e[a])(new d.Comment(b[a]));break;case 67:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Code(b[a-3],b[a],b[a-1]));break;case 68:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Code([],b[a],b[a-1]));break;case 69:this.$=d.addLocationDataFn(e[a],e[a])("func");break;case 70:this.$=d.addLocationDataFn(e[a],e[a])("boundfunc");break;case 73:case 110:this.$=d.addLocationDataFn(e[a],e[a])([]);break;case 74:case 111:case 130:case 150:case 182:case 224:this.$=d.addLocationDataFn(e[a],
-e[a])([b[a]]);break;case 75:case 112:case 131:case 151:case 183:this.$=d.addLocationDataFn(e[a-2],e[a])(b[a-2].concat(b[a]));break;case 76:case 113:case 132:case 152:case 184:this.$=d.addLocationDataFn(e[a-3],e[a])(b[a-3].concat(b[a]));break;case 77:case 114:case 134:case 154:case 186:this.$=d.addLocationDataFn(e[a-5],e[a])(b[a-5].concat(b[a-2]));break;case 78:this.$=d.addLocationDataFn(e[a],e[a])(new d.Param(b[a]));break;case 79:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Param(b[a-1],null,!0));
-break;case 80:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Param(b[a-2],b[a]));break;case 81:case 189:this.$=d.addLocationDataFn(e[a],e[a])(new d.Expansion);break;case 86:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Splat(b[a-1]));break;case 88:this.$=d.addLocationDataFn(e[a-1],e[a])(b[a-1].add(b[a]));break;case 89:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Value(b[a-1],[].concat(b[a])));break;case 99:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Access(b[a]));break;case 100:this.$=d.addLocationDataFn(e[a-
-1],e[a])(new d.Access(b[a],"soak"));break;case 101:this.$=d.addLocationDataFn(e[a-1],e[a])([d.addLocationDataFn(e[a-1])(new d.Access(new d.PropertyName("prototype"))),d.addLocationDataFn(e[a])(new d.Access(b[a]))]);break;case 102:this.$=d.addLocationDataFn(e[a-1],e[a])([d.addLocationDataFn(e[a-1])(new d.Access(new d.PropertyName("prototype"),"soak")),d.addLocationDataFn(e[a])(new d.Access(b[a]))]);break;case 103:this.$=d.addLocationDataFn(e[a],e[a])(new d.Access(new d.PropertyName("prototype")));
-break;case 106:this.$=d.addLocationDataFn(e[a-1],e[a])(d.extend(b[a],{soak:!0}));break;case 107:this.$=d.addLocationDataFn(e[a],e[a])(new d.Index(b[a]));break;case 108:this.$=d.addLocationDataFn(e[a],e[a])(new d.Slice(b[a]));break;case 109:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Obj(b[a-2],b[a-3].generated));break;case 115:this.$=d.addLocationDataFn(e[a],e[a])(new d.Class);break;case 116:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Class(null,null,b[a]));break;case 117:this.$=d.addLocationDataFn(e[a-
-2],e[a])(new d.Class(null,b[a]));break;case 118:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Class(null,b[a-1],b[a]));break;case 119:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Class(b[a]));break;case 120:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Class(b[a-1],null,b[a]));break;case 121:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Class(b[a-2],b[a]));break;case 122:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Class(b[a-3],b[a-1],b[a]));break;case 123:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.ImportDeclaration(null,
-b[a]));break;case 124:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.ImportDeclaration(new d.ImportClause(b[a-2],null),b[a]));break;case 125:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.ImportDeclaration(new d.ImportClause(null,b[a-2]),b[a]));break;case 126:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.ImportDeclaration(new d.ImportClause(null,new d.ImportSpecifierList([])),b[a]));break;case 127:this.$=d.addLocationDataFn(e[a-6],e[a])(new d.ImportDeclaration(new d.ImportClause(null,new d.ImportSpecifierList(b[a-
-4])),b[a]));break;case 128:this.$=d.addLocationDataFn(e[a-5],e[a])(new d.ImportDeclaration(new d.ImportClause(b[a-4],b[a-2]),b[a]));break;case 129:this.$=d.addLocationDataFn(e[a-8],e[a])(new d.ImportDeclaration(new d.ImportClause(b[a-7],new d.ImportSpecifierList(b[a-4])),b[a]));break;case 133:case 153:case 169:case 185:this.$=d.addLocationDataFn(e[a-3],e[a])(b[a-2]);break;case 135:this.$=d.addLocationDataFn(e[a],e[a])(new d.ImportSpecifier(b[a]));break;case 136:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ImportSpecifier(b[a-
-2],b[a]));break;case 137:this.$=d.addLocationDataFn(e[a],e[a])(new d.ImportSpecifier(new d.Literal(b[a])));break;case 138:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ImportSpecifier(new d.Literal(b[a-2]),b[a]));break;case 139:this.$=d.addLocationDataFn(e[a],e[a])(new d.ImportDefaultSpecifier(b[a]));break;case 140:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ImportNamespaceSpecifier(new d.Literal(b[a-2]),b[a]));break;case 141:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ExportNamedDeclaration(new d.ExportSpecifierList([])));
-break;case 142:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.ExportNamedDeclaration(new d.ExportSpecifierList(b[a-2])));break;case 143:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.ExportNamedDeclaration(b[a]));break;case 144:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.ExportNamedDeclaration(new d.Assign(b[a-2],b[a],null,{moduleDeclaration:"export"})));break;case 145:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.ExportNamedDeclaration(new d.Assign(b[a-3],b[a],null,{moduleDeclaration:"export"})));
-break;case 146:this.$=d.addLocationDataFn(e[a-5],e[a])(new d.ExportNamedDeclaration(new d.Assign(b[a-4],b[a-1],null,{moduleDeclaration:"export"})));break;case 147:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ExportDefaultDeclaration(b[a]));break;case 148:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.ExportAllDeclaration(new d.Literal(b[a-2]),b[a]));break;case 149:this.$=d.addLocationDataFn(e[a-6],e[a])(new d.ExportNamedDeclaration(new d.ExportSpecifierList(b[a-4]),b[a]));break;case 155:this.$=d.addLocationDataFn(e[a],
-e[a])(new d.ExportSpecifier(b[a]));break;case 156:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ExportSpecifier(b[a-2],b[a]));break;case 157:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ExportSpecifier(b[a-2],new d.Literal(b[a])));break;case 158:this.$=d.addLocationDataFn(e[a],e[a])(new d.ExportSpecifier(new d.Literal(b[a])));break;case 159:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ExportSpecifier(new d.Literal(b[a-2]),b[a]));break;case 160:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.TaggedTemplateCall(b[a-
-2],b[a],b[a-1]));break;case 161:case 162:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Call(b[a-2],b[a],b[a-1]));break;case 164:this.$=d.addLocationDataFn(e[a],e[a])(new d.SuperCall);break;case 165:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.SuperCall(b[a]));break;case 166:this.$=d.addLocationDataFn(e[a],e[a])(!1);break;case 167:this.$=d.addLocationDataFn(e[a],e[a])(!0);break;case 168:this.$=d.addLocationDataFn(e[a-1],e[a])([]);break;case 170:case 171:this.$=d.addLocationDataFn(e[a],e[a])(new d.Value(new d.ThisLiteral));
-break;case 172:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Value(d.addLocationDataFn(e[a-1])(new d.ThisLiteral),[d.addLocationDataFn(e[a])(new d.Access(b[a]))],"this"));break;case 173:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Arr([]));break;case 174:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Arr(b[a-2]));break;case 175:this.$=d.addLocationDataFn(e[a],e[a])("inclusive");break;case 176:this.$=d.addLocationDataFn(e[a],e[a])("exclusive");break;case 177:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Range(b[a-
-3],b[a-1],b[a-2]));break;case 178:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Range(b[a-2],b[a],b[a-1]));break;case 179:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Range(b[a-1],null,b[a]));break;case 180:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Range(null,b[a],b[a-1]));break;case 181:this.$=d.addLocationDataFn(e[a],e[a])(new d.Range(null,null,b[a]));break;case 191:this.$=d.addLocationDataFn(e[a-2],e[a])([].concat(b[a-2],b[a]));break;case 192:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Try(b[a]));
-break;case 193:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Try(b[a-1],b[a][0],b[a][1]));break;case 194:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Try(b[a-2],null,null,b[a]));break;case 195:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Try(b[a-3],b[a-2][0],b[a-2][1],b[a]));break;case 196:this.$=d.addLocationDataFn(e[a-2],e[a])([b[a-1],b[a]]);break;case 197:this.$=d.addLocationDataFn(e[a-2],e[a])([d.addLocationDataFn(e[a-1])(new d.Value(b[a-1])),b[a]]);break;case 198:this.$=d.addLocationDataFn(e[a-
-1],e[a])([null,b[a]]);break;case 199:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Throw(b[a]));break;case 200:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Parens(b[a-1]));break;case 201:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Parens(b[a-2]));break;case 202:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.While(b[a]));break;case 203:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.While(b[a-2],{guard:b[a]}));break;case 204:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.While(b[a],{invert:!0}));break;
-case 205:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.While(b[a-2],{invert:!0,guard:b[a]}));break;case 206:this.$=d.addLocationDataFn(e[a-1],e[a])(b[a-1].addBody(b[a]));break;case 207:case 208:this.$=d.addLocationDataFn(e[a-1],e[a])(b[a].addBody(d.addLocationDataFn(e[a-1])(d.Block.wrap([b[a-1]]))));break;case 209:this.$=d.addLocationDataFn(e[a],e[a])(b[a]);break;case 210:this.$=d.addLocationDataFn(e[a-1],e[a])((new d.While(d.addLocationDataFn(e[a-1])(new d.BooleanLiteral("true")))).addBody(b[a]));
-break;case 211:this.$=d.addLocationDataFn(e[a-1],e[a])((new d.While(d.addLocationDataFn(e[a-1])(new d.BooleanLiteral("true")))).addBody(d.addLocationDataFn(e[a])(d.Block.wrap([b[a]]))));break;case 212:case 213:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.For(b[a-1],b[a]));break;case 214:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.For(b[a],b[a-1]));break;case 215:this.$=d.addLocationDataFn(e[a-1],e[a])({source:d.addLocationDataFn(e[a])(new d.Value(b[a]))});break;case 216:this.$=d.addLocationDataFn(e[a-
-3],e[a])({source:d.addLocationDataFn(e[a-2])(new d.Value(b[a-2])),step:b[a]});break;case 217:d=d.addLocationDataFn(e[a-1],e[a]);b[a].own=b[a-1].own;b[a].ownTag=b[a-1].ownTag;b[a].name=b[a-1][0];b[a].index=b[a-1][1];this.$=d(b[a]);break;case 218:this.$=d.addLocationDataFn(e[a-1],e[a])(b[a]);break;case 219:wa=d.addLocationDataFn(e[a-2],e[a]);b[a].own=!0;b[a].ownTag=d.addLocationDataFn(e[a-1])(new d.Literal(b[a-1]));this.$=wa(b[a]);break;case 225:this.$=d.addLocationDataFn(e[a-2],e[a])([b[a-2],b[a]]);
-break;case 226:this.$=d.addLocationDataFn(e[a-1],e[a])({source:b[a]});break;case 227:this.$=d.addLocationDataFn(e[a-1],e[a])({source:b[a],object:!0});break;case 228:this.$=d.addLocationDataFn(e[a-3],e[a])({source:b[a-2],guard:b[a]});break;case 229:this.$=d.addLocationDataFn(e[a-3],e[a])({source:b[a-2],guard:b[a],object:!0});break;case 230:this.$=d.addLocationDataFn(e[a-3],e[a])({source:b[a-2],step:b[a]});break;case 231:this.$=d.addLocationDataFn(e[a-5],e[a])({source:b[a-4],guard:b[a-2],step:b[a]});
-break;case 232:this.$=d.addLocationDataFn(e[a-5],e[a])({source:b[a-4],step:b[a-2],guard:b[a]});break;case 233:this.$=d.addLocationDataFn(e[a-1],e[a])({source:b[a],from:!0});break;case 234:this.$=d.addLocationDataFn(e[a-3],e[a])({source:b[a-2],guard:b[a],from:!0});break;case 235:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Switch(b[a-3],b[a-1]));break;case 236:this.$=d.addLocationDataFn(e[a-6],e[a])(new d.Switch(b[a-5],b[a-3],b[a-1]));break;case 237:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Switch(null,
-b[a-1]));break;case 238:this.$=d.addLocationDataFn(e[a-5],e[a])(new d.Switch(null,b[a-3],b[a-1]));break;case 240:this.$=d.addLocationDataFn(e[a-1],e[a])(b[a-1].concat(b[a]));break;case 241:this.$=d.addLocationDataFn(e[a-2],e[a])([[b[a-1],b[a]]]);break;case 242:this.$=d.addLocationDataFn(e[a-3],e[a])([[b[a-2],b[a-1]]]);break;case 243:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.If(b[a-1],b[a],{type:b[a-2]}));break;case 244:this.$=d.addLocationDataFn(e[a-4],e[a])(b[a-4].addElse(d.addLocationDataFn(e[a-
-2],e[a])(new d.If(b[a-1],b[a],{type:b[a-2]}))));break;case 246:this.$=d.addLocationDataFn(e[a-2],e[a])(b[a-2].addElse(b[a]));break;case 247:case 248:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.If(b[a],d.addLocationDataFn(e[a-2])(d.Block.wrap([b[a-2]])),{type:b[a-1],statement:!0}));break;case 251:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op("-",b[a]));break;case 252:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op("+",b[a]));break;case 253:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op("--",
-b[a]));break;case 254:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op("++",b[a]));break;case 255:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op("--",b[a-1],null,!0));break;case 256:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op("++",b[a-1],null,!0));break;case 257:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Existence(b[a-1]));break;case 258:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Op("+",b[a-2],b[a]));break;case 259:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Op("-",b[a-2],b[a]));break;
-case 260:case 261:case 262:case 263:case 264:case 265:case 266:case 267:case 268:case 269:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Op(b[a-1],b[a-2],b[a]));break;case 270:e=d.addLocationDataFn(e[a-2],e[a]);b="!"===b[a-1].charAt(0)?(new d.Op(b[a-1].slice(1),b[a-2],b[a])).invert():new d.Op(b[a-1],b[a-2],b[a]);this.$=e(b);break;case 271:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Assign(b[a-2],b[a],b[a-1]));break;case 272:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Assign(b[a-4],b[a-1],b[a-3]));
-break;case 273:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Assign(b[a-3],b[a],b[a-2]));break;case 274:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Extends(b[a-2],b[a]))}},table:[{1:[2,1],3:1,4:2,5:3,7:4,8:5,9:6,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,
-97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{1:[3]},{1:[2,2],6:qa},a(sa,[2,3]),a(sa,[2,6],{141:77,132:102,138:103,133:D,135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(sa,[2,7],{141:77,132:105,138:106,133:D,135:A,139:E,156:va}),a(sa,[2,8]),a(N,[2,14],{109:107,78:108,86:114,40:xa,41:xa,114:xa,82:ta,83:Na,
-84:Fa,85:Ga,87:Ca,90:Pa,113:Ia}),a(N,[2,15],{86:114,109:117,78:118,82:ta,83:Na,84:Fa,85:Ga,87:Ca,90:Pa,113:Ia,114:xa}),a(N,[2,16]),a(N,[2,17]),a(N,[2,18]),a(N,[2,19]),a(N,[2,20]),a(N,[2,21]),a(N,[2,22]),a(N,[2,23]),a(N,[2,24]),a(N,[2,25]),a(N,[2,26]),a(Ea,[2,9]),a(Ea,[2,10]),a(Ea,[2,11]),a(Ea,[2,12]),a(Ea,[2,13]),a([1,6,32,42,131,133,135,139,156,163,164,165,166,167,168,169,170,171,172,173,174],Va,{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,
-47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,7:120,8:122,12:b,28:ea,29:Ya,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,61:[1,119],62:z,63:l,67:c,68:w,92:m,95:k,97:K,105:P,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,137:q,149:ba,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}),a(Ba,Ha,{55:[1,124]}),a(Ba,[2,95]),a(Ba,[2,96]),a(Ba,[2,97]),a(Ba,[2,98]),a(t,[2,163]),a([6,31,65,70],p,{64:125,71:126,72:127,33:129,60:130,
-74:131,75:132,34:g,73:d,92:m,118:wa,119:e}),{30:135,31:Da},{7:137,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,
-158:T,159:v,160:Y,161:S,162:M},{7:138,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},
-{7:139,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:140,8:122,10:20,11:21,12:b,
-13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{15:142,16:143,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,
-44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:144,60:71,74:53,75:54,77:141,79:28,80:29,81:30,92:m,111:31,112:L,117:V,118:X,119:G,130:W},{15:142,16:143,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:144,60:71,74:53,75:54,77:145,79:28,80:29,81:30,92:m,111:31,112:L,117:V,118:X,119:G,130:W},a(Ta,ua,{96:[1,149],161:[1,146],162:[1,147],175:[1,148]}),a(N,[2,245],{151:[1,150]}),{30:151,31:Da},{30:152,31:Da},a(N,[2,209]),{30:153,31:Da},{7:154,8:122,10:20,11:21,
-12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:[1,155],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Eb,[2,115],{47:27,79:28,80:29,81:30,111:31,
-74:53,75:54,37:55,43:57,33:70,60:71,39:80,15:142,16:143,54:144,30:156,77:158,31:Da,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,92:m,96:[1,157],112:L,117:V,118:X,119:G,130:W}),{7:159,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,
-111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Ea,Za,{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,8:122,7:160,12:b,28:ea,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,61:R,62:z,63:l,67:c,68:w,
-92:m,95:k,97:K,105:P,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,137:q,149:ba,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}),a([1,6,31,32,42,70,94,131,133,135,139,156],[2,66]),{33:165,34:g,39:161,40:r,41:n,92:[1,164],98:162,99:163,104:Fb},{25:168,33:169,34:g,92:[1,167],95:k,103:[1,170],107:[1,171]},a(Ta,[2,92]),a(Ta,[2,93]),a(Ba,[2,40]),a(Ba,[2,41]),a(Ba,[2,42]),a(Ba,[2,43]),a(Ba,[2,44]),a(Ba,[2,45]),a(Ba,[2,46]),a(Ba,[2,47]),{4:172,5:3,7:4,8:5,9:6,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,
-20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,31:[1,173],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:174,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,
-23:15,24:16,25:17,26:18,27:19,28:ea,31:$a,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Ua,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,116:176,117:V,118:X,119:G,120:Gb,123:177,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Ba,[2,170]),a(Ba,[2,171],{35:181,36:Oa}),a([1,6,31,32,42,46,65,70,73,82,
-83,84,85,87,89,90,94,113,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],[2,164],{110:183,114:sb}),{31:[2,69]},{31:[2,70]},a(La,[2,87]),a(La,[2,90]),{7:185,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,
-105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:186,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,
-119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:187,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,
-133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:189,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,30:188,31:Da,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,
-137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{33:194,34:g,60:195,74:196,75:197,80:190,92:m,118:wa,119:G,143:191,144:[1,192],145:193},{142:198,146:[1,199],147:[1,200],148:[1,201]},a([6,31,70,94],Hb,{39:80,93:202,56:203,57:204,59:205,11:206,37:207,33:208,35:209,60:210,34:g,36:Oa,38:h,40:r,41:n,62:z,118:wa}),a(Ib,[2,34]),a(Ib,[2,35]),a(Ba,[2,38]),{15:142,16:211,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:144,60:71,
-74:53,75:54,77:212,79:28,80:29,81:30,92:m,111:31,112:L,117:V,118:X,119:G,130:W},a([1,6,29,31,32,40,41,42,55,58,65,70,73,82,83,84,85,87,89,90,94,96,102,113,114,115,120,122,131,133,134,135,139,140,146,147,148,156,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175],[2,32]),a(Jb,[2,36]),{4:213,5:3,7:4,8:5,9:6,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,
-50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(sa,[2,5],{7:4,8:5,9:6,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57,
-33:70,60:71,141:77,39:80,5:214,12:b,28:u,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,61:R,62:z,63:l,67:c,68:w,92:m,95:k,97:K,105:P,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,133:D,135:A,137:q,139:E,149:ba,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}),a(N,[2,257]),{7:215,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,
-61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:216,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,
-74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:217,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,
-81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:218,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,
-112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:219,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,
-129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:220,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,
-136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:221,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,
-149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:222,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,
-159:v,160:Y,161:S,162:M},{7:223,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:224,
-8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:225,8:122,10:20,11:21,12:b,13:23,
-14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:226,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,
-20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:227,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,
-25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:228,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,
-34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(N,[2,208]),a(N,[2,213]),{7:229,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,
-37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(N,[2,207]),a(N,[2,212]),{39:230,40:r,41:n,110:231,114:sb},a(La,[2,88]),a(Kb,[2,167]),{35:232,36:Oa},{35:233,36:Oa},a(La,[2,103],{35:234,36:Oa}),{35:235,36:Oa},a(La,
-[2,104]),{7:237,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Lb,74:53,75:54,77:40,79:28,80:29,81:30,88:236,91:238,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,121:239,122:tb,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,
-161:S,162:M},{86:242,87:Ca,90:Pa},{110:243,114:sb},a(La,[2,89]),a(sa,[2,65],{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,8:122,7:244,12:b,28:ea,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,61:R,62:z,63:l,67:c,68:w,92:m,95:k,97:K,105:P,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,133:Za,135:Za,139:Za,156:Za,
-137:q,149:ba,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}),a(Ma,[2,28],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{7:245,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,
-111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{132:105,133:D,135:A,138:106,139:E,141:77,156:va},a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,163,164,165,166,167,168,169,170,171,172,173,174],Va,{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,
-138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,7:120,8:122,12:b,28:ea,29:Ya,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,61:R,62:z,63:l,67:c,68:w,92:m,95:k,97:K,105:P,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,137:q,149:ba,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}),{6:[1,247],7:246,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:[1,248],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,
-49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a([6,31],Ja,{69:251,65:[1,249],70:Mb}),a(Sa,[2,74]),a(Sa,[2,78],{55:[1,253],73:[1,252]}),a(Sa,[2,81]),a(fb,[2,82]),a(fb,[2,83]),a(fb,[2,84]),a(fb,[2,85]),{35:181,36:Oa},{7:254,8:122,10:20,11:21,12:b,13:23,14:24,15:7,
-16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:$a,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Ua,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,116:176,117:V,118:X,119:G,120:Gb,123:177,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(N,[2,68]),{4:256,5:3,7:4,8:5,9:6,
-10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,32:[1,255],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a([1,6,31,32,42,65,70,73,89,94,
-115,120,122,131,133,134,135,139,140,156,159,160,164,165,166,167,168,169,170,171,172,173,174],[2,249],{141:77,132:102,138:103,163:fa}),a(ab,[2,250],{141:77,132:102,138:103,163:fa,165:ga}),a(ab,[2,251],{141:77,132:102,138:103,163:fa,165:ga}),a(ab,[2,252],{141:77,132:102,138:103,163:fa,165:ga}),a(N,[2,253],{40:ua,41:ua,82:ua,83:ua,84:ua,85:ua,87:ua,90:ua,113:ua,114:ua}),a(Kb,xa,{109:107,78:108,86:114,82:ta,83:Na,84:Fa,85:Ga,87:Ca,90:Pa,113:Ia}),{78:118,82:ta,83:Na,84:Fa,85:Ga,86:114,87:Ca,90:Pa,109:117,
-113:Ia,114:xa},a(Nb,Ha),a(N,[2,254],{40:ua,41:ua,82:ua,83:ua,84:ua,85:ua,87:ua,90:ua,113:ua,114:ua}),a(N,[2,255]),a(N,[2,256]),{6:[1,259],7:257,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:[1,258],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,
-130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:260,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,
-137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{30:261,31:Da,155:[1,262]},a(N,[2,192],{126:263,127:[1,264],128:[1,265]}),a(N,[2,206]),a(N,[2,214]),{31:[1,266],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},{150:267,152:268,153:gb},a(N,[2,116]),{7:270,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,
-33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Eb,[2,119],{30:271,31:Da,40:ua,41:ua,82:ua,83:ua,84:ua,85:ua,87:ua,90:ua,113:ua,114:ua,96:[1,272]}),a(Ma,[2,199],{141:77,132:102,138:103,159:ma,160:Z,
-163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ea,bb,{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ea,[2,123]),{29:[1,273],70:[1,274]},{29:[1,275]},{31:hb,33:280,34:g,94:[1,276],100:277,101:278,103:Wa},a([29,70],[2,139]),{102:[1,282]},{31:ub,33:287,34:g,94:[1,283],103:cb,106:284,108:285},a(Ea,[2,143]),{55:[1,289]},{7:290,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,
-20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{29:[1,291]},{6:qa,131:[1,292]},{4:293,5:3,7:4,8:5,9:6,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,
-18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a([6,31,70,120],Ob,{141:77,132:102,138:103,121:294,73:[1,295],122:tb,133:D,135:A,139:E,
-156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(vb,[2,173]),a([6,31,120],Ja,{69:296,70:ib}),a(Qa,[2,182]),{7:254,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:$a,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Ua,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,
-112:L,116:298,117:V,118:X,119:G,123:177,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Qa,[2,188]),a(Qa,[2,189]),a(Pb,[2,172]),a(Pb,[2,33]),a(t,[2,165]),{7:254,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:$a,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Ua,
-74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,115:[1,299],116:300,117:V,118:X,119:G,123:177,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{30:301,31:Da,132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},a(Qb,[2,202],{141:77,132:102,138:103,133:D,134:[1,302],135:A,139:E,159:ma,160:Z,163:fa,164:ia,
-165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Qb,[2,204],{141:77,132:102,138:103,133:D,134:[1,303],135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(N,[2,210]),a(Xa,[2,211],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,156,159,160,163,164,165,166,167,168,
-169,170,171,172,173,174],[2,215],{140:[1,304]}),a(jb,[2,218]),{33:194,34:g,60:195,74:196,75:197,92:m,118:wa,119:e,143:305,145:193},a(jb,[2,224],{70:[1,306]}),a(kb,[2,220]),a(kb,[2,221]),a(kb,[2,222]),a(kb,[2,223]),a(N,[2,217]),{7:307,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,
-79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:308,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,
-97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:309,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,
-118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(lb,Ja,{69:310,70:Rb}),a(Aa,[2,111]),a(Aa,[2,51],{58:[1,312]}),a(Sb,[2,60],{55:[1,313]}),a(Aa,[2,56]),a(Sb,[2,61]),a(wb,[2,57]),a(wb,[2,58]),a(wb,[2,59]),{46:[1,314],78:118,82:ta,83:Na,84:Fa,85:Ga,86:114,87:Ca,90:Pa,109:117,113:Ia,114:xa},a(Nb,ua),{6:qa,42:[1,315]},a(sa,[2,4]),a(Tb,[2,258],{141:77,132:102,138:103,163:fa,164:ia,165:ga}),a(Tb,[2,259],{141:77,
-132:102,138:103,163:fa,164:ia,165:ga}),a(ab,[2,260],{141:77,132:102,138:103,163:fa,165:ga}),a(ab,[2,261],{141:77,132:102,138:103,163:fa,165:ga}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,166,167,168,169,170,171,172,173,174],[2,262],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,167,168,169,170,171,172,173],[2,263],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,174:da}),
-a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,168,169,170,171,172,173],[2,264],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,169,170,171,172,173],[2,265],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,170,171,172,173],[2,266],{141:77,132:102,138:103,159:ma,160:Z,
-163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,171,172,173],[2,267],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,172,173],[2,268],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,
-135,139,140,156,173],[2,269],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,167,168,169,170,171,172,173,174],[2,270],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja}),a(Xa,[2,248],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Xa,[2,247],{141:77,132:102,
-138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(t,[2,160]),a(t,[2,161]),a(La,[2,99]),a(La,[2,100]),a(La,[2,101]),a(La,[2,102]),{89:[1,316]},{73:Lb,89:[2,107],121:317,122:tb,132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},{89:[2,108]},{7:318,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,
-24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,89:[2,181],92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Ub,[2,175]),a(Ub,Vb),a(La,[2,106]),a(t,[2,162]),a(sa,[2,64],{141:77,132:102,138:103,133:bb,135:bb,139:bb,156:bb,
-159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ma,[2,29],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ma,[2,48],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{7:319,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,
-37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:320,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,
-44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{66:321,67:c,68:w},a(Ra,db,{72:127,33:129,60:130,74:131,75:132,71:322,34:g,73:d,92:m,118:wa,119:e}),{6:Wb,31:Xb},a(Sa,[2,79]),{7:325,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,
-20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Qa,Ob,{141:77,132:102,138:103,73:[1,326],133:D,135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,
-166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Yb,[2,30]),{6:qa,32:[1,327]},a(Ma,[2,271],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{7:328,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,
-79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:329,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,
-97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Ma,[2,274],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(N,[2,246]),{7:330,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,
-48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(N,[2,193],{127:[1,331]}),{30:332,31:Da},{30:335,31:Da,33:333,34:g,75:334,92:m},{150:336,152:268,153:gb},{32:[1,337],151:[1,338],152:339,153:gb},a(mb,[2,239]),{7:341,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,
-17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,124:340,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Zb,[2,117],{141:77,132:102,138:103,30:342,31:Da,133:D,135:A,139:E,159:ma,
-160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(N,[2,120]),{7:343,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,
-139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{39:344,40:r,41:n},{92:[1,346],99:345,104:Fb},{39:347,40:r,41:n},{29:[1,348]},a(lb,Ja,{69:349,70:nb}),a(Aa,[2,130]),{31:hb,33:280,34:g,100:351,101:278,103:Wa},a(Aa,[2,135],{102:[1,352]}),a(Aa,[2,137],{102:[1,353]}),{33:354,34:g},a(Ea,[2,141]),a(lb,Ja,{69:355,70:xb}),a(Aa,[2,150]),{31:ub,33:287,34:g,103:cb,106:357,108:285},a(Aa,[2,155],{102:[1,358]}),a(Aa,[2,158],{102:[1,359]}),{6:[1,361],7:360,8:122,10:20,11:21,12:b,13:23,14:24,
-15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:[1,362],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(yb,[2,147],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,
-160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{39:363,40:r,41:n},a(Ba,[2,200]),{6:qa,32:[1,364]},{7:365,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,
-132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a([12,28,34,38,40,41,44,45,48,49,50,51,52,53,61,62,63,67,68,92,95,97,105,112,117,118,119,125,129,130,133,135,137,139,149,155,157,158,159,160,161,162],Vb,{6:eb,31:eb,70:eb,120:eb}),{6:ob,31:pb,120:[1,366]},a([6,31,32,115,120],db,{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,
-136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,8:122,76:179,7:254,123:369,12:b,28:ea,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,61:R,62:z,63:l,67:c,68:w,73:Ua,92:m,95:k,97:K,105:P,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,133:D,135:A,137:q,139:E,149:ba,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}),a(Ra,Ja,{69:370,70:ib}),a(t,[2,168]),a([6,31,115],Ja,{69:371,70:ib}),a($b,[2,243]),{7:372,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,
-23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:373,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,
-28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:374,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,
-39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(jb,[2,219]),{33:194,34:g,60:195,74:196,75:197,92:m,118:wa,119:e,145:375},a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,135,139,156],[2,226],{141:77,132:102,138:103,134:[1,
-376],140:[1,377],159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(zb,[2,227],{141:77,132:102,138:103,134:[1,378],159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(zb,[2,233],{141:77,132:102,138:103,134:[1,379],159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{6:ac,31:bc,94:[1,380]},a(Ab,db,{39:80,57:204,59:205,11:206,37:207,33:208,35:209,60:210,56:383,
-34:g,36:Oa,38:h,40:r,41:n,62:z,118:wa}),{7:384,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:[1,385],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,
-160:Y,161:S,162:M},{7:386,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:[1,387],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},
-a(Ba,[2,39]),a(Jb,[2,37]),a(La,[2,105]),{7:388,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,89:[2,179],92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,
-160:Y,161:S,162:M},{89:[2,180],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},a(Ma,[2,49],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{32:[1,389],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},{30:390,31:Da},a(Sa,[2,75]),{33:129,
-34:g,60:130,71:391,72:127,73:d,74:131,75:132,92:m,118:wa,119:e},a(cc,p,{71:126,72:127,33:129,60:130,74:131,75:132,64:392,34:g,73:d,92:m,118:wa,119:e}),a(Sa,[2,80],{141:77,132:102,138:103,133:D,135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Qa,eb),a(Yb,[2,31]),{32:[1,393],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},a(Ma,[2,273],
-{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{30:394,31:Da,132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},{30:395,31:Da},a(N,[2,194]),{30:396,31:Da},{30:397,31:Da},a(Bb,[2,198]),{32:[1,398],151:[1,399],152:339,153:gb},a(N,[2,237]),{30:400,31:Da},a(mb,[2,240]),{30:401,31:Da,70:[1,402]},a(dc,[2,190],{141:77,132:102,138:103,133:D,
-135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(N,[2,118]),a(Zb,[2,121],{141:77,132:102,138:103,30:403,31:Da,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ea,[2,124]),{29:[1,404]},{31:hb,33:280,34:g,100:405,101:278,103:Wa},a(Ea,[2,125]),{39:406,40:r,41:n},{6:qb,31:rb,94:[1,407]},a(Ab,db,{33:280,101:410,34:g,103:Wa}),a(Ra,Ja,{69:411,70:nb}),{33:412,34:g},
-{33:413,34:g},{29:[2,140]},{6:Cb,31:Db,94:[1,414]},a(Ab,db,{33:287,108:417,34:g,103:cb}),a(Ra,Ja,{69:418,70:xb}),{33:419,34:g,103:[1,420]},{33:421,34:g},a(yb,[2,144],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{7:422,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,
-51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:423,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,
-62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Ea,[2,148]),{131:[1,424]},{120:[1,425],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},a(vb,[2,174]),{7:254,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,
-18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Ua,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,123:426,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:254,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,
-20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:$a,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Ua,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,116:427,117:V,118:X,119:G,123:177,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Qa,[2,183]),{6:ob,31:pb,32:[1,428]},{6:ob,31:pb,115:[1,429]},
-a(Xa,[2,203],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Xa,[2,205],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Xa,[2,216],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(jb,[2,225]),{7:430,8:122,10:20,11:21,
-12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:431,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,
-18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:432,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,
-23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:433,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,
-28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(vb,[2,109]),{11:206,33:208,34:g,35:209,36:Oa,37:207,38:h,39:80,40:r,41:n,56:434,57:204,59:205,60:210,62:z,118:wa},a(cc,Hb,{39:80,56:203,57:204,
-59:205,11:206,37:207,33:208,35:209,60:210,93:435,34:g,36:Oa,38:h,40:r,41:n,62:z,118:wa}),a(Aa,[2,112]),a(Aa,[2,52],{141:77,132:102,138:103,133:D,135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{7:436,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,
-66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Aa,[2,54],{141:77,132:102,138:103,133:D,135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{7:437,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,
-27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{89:[2,178],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,
-173:ra,174:da},a(N,[2,50]),a(N,[2,67]),a(Sa,[2,76]),a(Ra,Ja,{69:438,70:Mb}),a(N,[2,272]),a($b,[2,244]),a(N,[2,195]),a(Bb,[2,196]),a(Bb,[2,197]),a(N,[2,235]),{30:439,31:Da},{32:[1,440]},a(mb,[2,241],{6:[1,441]}),{7:442,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,
-92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(N,[2,122]),{39:443,40:r,41:n},a(lb,Ja,{69:444,70:nb}),a(Ea,[2,126]),{29:[1,445]},{33:280,34:g,101:446,103:Wa},{31:hb,33:280,34:g,100:447,101:278,103:Wa},a(Aa,[2,131]),{6:qb,31:rb,32:[1,448]},a(Aa,[2,136]),a(Aa,[2,138]),a(Ea,[2,142],{29:[1,449]}),{33:287,34:g,103:cb,108:450},{31:ub,33:287,34:g,103:cb,106:451,108:285},
-a(Aa,[2,151]),{6:Cb,31:Db,32:[1,452]},a(Aa,[2,156]),a(Aa,[2,157]),a(Aa,[2,159]),a(yb,[2,145],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{32:[1,453],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},a(Ba,[2,201]),a(Ba,[2,177]),a(Qa,[2,184]),a(Ra,Ja,{69:454,70:ib}),a(Qa,[2,185]),a(t,[2,169]),a([1,6,31,32,42,
-65,70,73,89,94,115,120,122,131,133,134,135,139,156],[2,228],{141:77,132:102,138:103,140:[1,455],159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(zb,[2,230],{141:77,132:102,138:103,134:[1,456],159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ma,[2,229],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ma,[2,234],{141:77,132:102,
-138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Aa,[2,113]),a(Ra,Ja,{69:457,70:Rb}),{32:[1,458],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},{32:[1,459],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},{6:Wb,31:Xb,32:[1,460]},{32:[1,461]},a(N,
-[2,238]),a(mb,[2,242]),a(dc,[2,191],{141:77,132:102,138:103,133:D,135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ea,[2,128]),{6:qb,31:rb,94:[1,462]},{39:463,40:r,41:n},a(Aa,[2,132]),a(Ra,Ja,{69:464,70:nb}),a(Aa,[2,133]),{39:465,40:r,41:n},a(Aa,[2,152]),a(Ra,Ja,{69:466,70:xb}),a(Aa,[2,153]),a(Ea,[2,146]),{6:ob,31:pb,32:[1,467]},{7:468,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,
-25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:469,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,
-34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{6:ac,31:bc,32:[1,470]},a(Aa,[2,53]),a(Aa,[2,55]),a(Sa,[2,77]),a(N,[2,236]),{29:[1,471]},a(Ea,[2,127]),{6:qb,31:rb,32:[1,472]},a(Ea,[2,149]),{6:Cb,31:Db,32:[1,
-473]},a(Qa,[2,186]),a(Ma,[2,231],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ma,[2,232],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Aa,[2,114]),{39:474,40:r,41:n},a(Aa,[2,134]),a(Aa,[2,154]),a(Ea,[2,129])],defaultActions:{68:[2,69],69:[2,70],238:[2,108],354:[2,140]},parseError:function(a,d){if(d.recoverable)this.trace(a);else{var e=function(a,
-d){this.message=a;this.hash=d};e.prototype=Error;throw new e(a,d);}},parse:function(a){var d=[0],e=[null],b=[],p=this.table,t="",wa=0,c=0,g=0,Da=b.slice.call(arguments,1),k=Object.create(this.lexer),h={};for(f in this.yy)Object.prototype.hasOwnProperty.call(this.yy,f)&&(h[f]=this.yy[f]);k.setInput(a,h);h.lexer=k;h.parser=this;"undefined"==typeof k.yylloc&&(k.yylloc={});var f=k.yylloc;b.push(f);var l=k.options&&k.options.ranges;this.parseError="function"===typeof h.parseError?h.parseError:Object.getPrototypeOf(this).parseError;
-for(var m,Ta,Ha,n,ua={},y,w;;){Ha=d[d.length-1];if(this.defaultActions[Ha])n=this.defaultActions[Ha];else{if(null===m||"undefined"==typeof m)m=k.lex()||1,"number"!==typeof m&&(m=this.symbols_[m]||m);n=p[Ha]&&p[Ha][m]}if("undefined"===typeof n||!n.length||!n[0]){w=[];for(y in p[Ha])this.terminals_[y]&&2<y&&w.push("'"+this.terminals_[y]+"'");var q=k.showPosition?"Parse error on line "+(wa+1)+":\n"+k.showPosition()+"\nExpecting "+w.join(", ")+", got '"+(this.terminals_[m]||m)+"'":"Parse error on line "+
-(wa+1)+": Unexpected "+(1==m?"end of input":"'"+(this.terminals_[m]||m)+"'");this.parseError(q,{text:k.match,token:this.terminals_[m]||m,line:k.yylineno,loc:f,expected:w})}if(n[0]instanceof Array&&1<n.length)throw Error("Parse Error: multiple actions possible at state: "+Ha+", token: "+m);switch(n[0]){case 1:d.push(m);e.push(k.yytext);b.push(k.yylloc);d.push(n[1]);m=null;Ta?(m=Ta,Ta=null):(c=k.yyleng,t=k.yytext,wa=k.yylineno,f=k.yylloc,0<g&&g--);break;case 2:w=this.productions_[n[1]][1];ua.$=e[e.length-
-w];ua._$={first_line:b[b.length-(w||1)].first_line,last_line:b[b.length-1].last_line,first_column:b[b.length-(w||1)].first_column,last_column:b[b.length-1].last_column};l&&(ua._$.range=[b[b.length-(w||1)].range[0],b[b.length-1].range[1]]);Ha=this.performAction.apply(ua,[t,c,wa,h,n[1],e,b].concat(Da));if("undefined"!==typeof Ha)return Ha;w&&(d=d.slice(0,-2*w),e=e.slice(0,-1*w),b=b.slice(0,-1*w));d.push(this.productions_[n[1]][0]);e.push(ua.$);b.push(ua._$);n=p[d[d.length-2]][d[d.length-1]];d.push(n);
-break;case 3:return!0}}}};f.prototype=ec;ec.Parser=f;return new f}();"undefined"!==typeof u&&"undefined"!==typeof f&&(f.parser=q,f.Parser=q.Parser,f.parse=function(){return q.parse.apply(q,arguments)},f.main=function(y){y[1]||(console.log("Usage: "+y[0]+" FILE"),process.exit(1));var a="",b=u("fs");"undefined"!==typeof b&&null!==b&&(a=b.readFileSync(u("path").normalize(y[1]),"utf8"));return f.parser.parse(a)},"undefined"!==typeof qa&&u.main===qa&&f.main(process.argv.slice(1)));return qa.exports}();
-u["./scope"]=function(){var f={};(function(){var u=[].indexOf||function(f){for(var y=0,a=this.length;y<a;y++)if(y in this&&this[y]===f)return y;return-1};f.Scope=function(){function f(f,a,b,q){var g,h;this.parent=f;this.expressions=a;this.method=b;this.referencedVars=q;this.variables=[{name:"arguments",type:"arguments"}];this.positions={};this.parent||(this.utilities={});this.root=null!=(g=null!=(h=this.parent)?h.root:void 0)?g:this}f.prototype.add=function(f,a,b){return this.shared&&!b?this.parent.add(f,
-a,b):Object.prototype.hasOwnProperty.call(this.positions,f)?this.variables[this.positions[f]].type=a:this.positions[f]=this.variables.push({name:f,type:a})-1};f.prototype.namedMethod=function(){var f;return null!=(f=this.method)&&f.name||!this.parent?this.method:this.parent.namedMethod()};f.prototype.find=function(f,a){null==a&&(a="var");if(this.check(f))return!0;this.add(f,a);return!1};f.prototype.parameter=function(f){if(!this.shared||!this.parent.check(f,!0))return this.add(f,"param")};f.prototype.check=
-function(f){var a;return!!(this.type(f)||null!=(a=this.parent)&&a.check(f))};f.prototype.temporary=function(f,a,b){null==b&&(b=!1);return b?(b=f.charCodeAt(0),f=122-b,b=String.fromCharCode(b+a%(f+1)),a=Math.floor(a/(f+1)),""+b+(a||"")):""+f+(a||"")};f.prototype.type=function(f){var a;var b=this.variables;var q=0;for(a=b.length;q<a;q++){var g=b[q];if(g.name===f)return g.type}return null};f.prototype.freeVariable=function(f,a){var b,q;null==a&&(a={});for(b=0;;){var g=this.temporary(f,b,a.single);if(!(this.check(g)||
-0<=u.call(this.root.referencedVars,g)))break;b++}(null!=(q=a.reserve)?q:1)&&this.add(g,"var",!0);return g};f.prototype.assign=function(f,a){this.add(f,{value:a,assigned:!0},!0);return this.hasAssignments=!0};f.prototype.hasDeclarations=function(){return!!this.declaredVariables().length};f.prototype.declaredVariables=function(){var f;var a=this.variables;var b=[];var q=0;for(f=a.length;q<f;q++){var g=a[q];"var"===g.type&&b.push(g.name)}return b.sort()};f.prototype.assignedVariables=function(){var f;
-var a=this.variables;var b=[];var q=0;for(f=a.length;q<f;q++){var g=a[q];g.type.assigned&&b.push(g.name+" \x3d "+g.type.value)}return b};return f}()}).call(this);return f}();u["./nodes"]=function(){var f={};(function(){var qa,q,y,a,b,ya,g,h,r,n,B,H,I,F,Q,x,J,O,R,z,l,c,w,m,k,K,P,L,V,X,G,aa,U,W,D,A,va,E,ba,ca,C,T,v=function(a,b){function p(){this.constructor=a}for(var d in b)Y.call(b,d)&&(a[d]=b[d]);p.prototype=b.prototype;a.prototype=new p;a.__super__=b.prototype;return a},Y={}.hasOwnProperty,S=[].indexOf||
-function(a){for(var b=0,p=this.length;b<p;b++)if(b in this&&this[b]===a)return b;return-1},M=[].slice;Error.stackTraceLimit=Infinity;var xa=u("./scope").Scope;var sa=u("./lexer");var za=sa.isUnassignable;var ma=sa.JS_FORBIDDEN;var Z=u("./helpers");var fa=Z.compact;var ia=Z.flatten;var ga=Z.extend;var ja=Z.merge;var la=Z.del;sa=Z.addLocationDataFn;var oa=Z.locationDataToString;var pa=Z.throwSyntaxError;f.extend=ga;f.addLocationDataFn=sa;var ha=function(){return!0};var ka=function(){return!1};var na=
-function(){return this};var ra=function(){this.negated=!this.negated;return this};f.CodeFragment=r=function(){function a(a,b){var d;this.code=""+b;this.locationData=null!=a?a.locationData:void 0;this.type=(null!=a?null!=(d=a.constructor)?d.name:void 0:void 0)||"unknown"}a.prototype.toString=function(){return""+this.code+(this.locationData?": "+oa(this.locationData):"")};return a}();var da=function(a){var b;var p=[];var d=0;for(b=a.length;d<b;d++){var wa=a[d];p.push(wa.code)}return p.join("")};f.Base=
-sa=function(){function b(){}b.prototype.compile=function(a,b){return da(this.compileToFragments(a,b))};b.prototype.compileToFragments=function(a,b){a=ga({},a);b&&(a.level=b);b=this.unfoldSoak(a)||this;b.tab=a.indent;return a.level!==N&&b.isStatement(a)?b.compileClosure(a):b.compileNode(a)};b.prototype.compileClosure=function(b){var p,d,t;(d=this.jumps())&&d.error("cannot use a pure statement in an expression");b.sharedScope=!0;d=new h([],a.wrap([this]));var e=[];if((p=this.contains(Va))||this.contains(ea))e=
-[new E],p?(p="apply",e.push(new x("arguments"))):p="call",d=new C(d,[new qa(new L(p))]);b=(new ya(d,e)).compileNode(b);if(d.isGenerator||null!=(t=d.base)&&t.isGenerator)b.unshift(this.makeCode("(yield* ")),b.push(this.makeCode(")"));return b};b.prototype.cache=function(a,b,d){if(null!=d?d(this):this.isComplex()){d=new x(a.scope.freeVariable("ref"));var p=new y(d,this);return b?[p.compileToFragments(a,b),[this.makeCode(d.value)]]:[p,d]}d=b?this.compileToFragments(a,b):this;return[d,d]};b.prototype.cacheToCodeFragments=
-function(a){return[da(a[0]),da(a[1])]};b.prototype.makeReturn=function(a){var b=this.unwrapAll();return a?new ya(new z(a+".push"),[b]):new G(b)};b.prototype.contains=function(a){var b=void 0;this.traverseChildren(!1,function(d){if(a(d))return b=d,!1});return b};b.prototype.lastNonComment=function(a){var b;for(b=a.length;b--;)if(!(a[b]instanceof n))return a[b];return null};b.prototype.toString=function(a,b){null==a&&(a="");null==b&&(b=this.constructor.name);var d="\n"+a+b;this.soak&&(d+="?");this.eachChild(function(b){return d+=
-b.toString(a+Ca)});return d};b.prototype.eachChild=function(a){var b,d;if(!this.children)return this;var t=this.children;var e=0;for(b=t.length;e<b;e++){var c=t[e];if(this[c]){var f=ia([this[c]]);var g=0;for(d=f.length;g<d;g++)if(c=f[g],!1===a(c))return this}}return this};b.prototype.traverseChildren=function(a,b){return this.eachChild(function(d){if(!1!==b(d))return d.traverseChildren(a,b)})};b.prototype.invert=function(){return new k("!",this)};b.prototype.unwrapAll=function(){var a;for(a=this;a!==
-(a=a.unwrap()););return a};b.prototype.children=[];b.prototype.isStatement=ka;b.prototype.jumps=ka;b.prototype.isComplex=ha;b.prototype.isChainable=ka;b.prototype.isAssignable=ka;b.prototype.isNumber=ka;b.prototype.unwrap=na;b.prototype.unfoldSoak=ka;b.prototype.assigns=ka;b.prototype.updateLocationDataIfMissing=function(a){if(this.locationData)return this;this.locationData=a;return this.eachChild(function(b){return b.updateLocationDataIfMissing(a)})};b.prototype.error=function(a){return pa(a,this.locationData)};
-b.prototype.makeCode=function(a){return new r(this,a)};b.prototype.wrapInBraces=function(a){return[].concat(this.makeCode("("),a,this.makeCode(")"))};b.prototype.joinFragmentArrays=function(a,b){var d,p;var e=[];var t=d=0;for(p=a.length;d<p;t=++d){var c=a[t];t&&e.push(this.makeCode(b));e=e.concat(c)}return e};return b}();f.Block=a=function(a){function b(a){this.expressions=fa(ia(a||[]))}v(b,a);b.prototype.children=["expressions"];b.prototype.push=function(a){this.expressions.push(a);return this};
-b.prototype.pop=function(){return this.expressions.pop()};b.prototype.unshift=function(a){this.expressions.unshift(a);return this};b.prototype.unwrap=function(){return 1===this.expressions.length?this.expressions[0]:this};b.prototype.isEmpty=function(){return!this.expressions.length};b.prototype.isStatement=function(a){var d;var b=this.expressions;var e=0;for(d=b.length;e<d;e++){var p=b[e];if(p.isStatement(a))return!0}return!1};b.prototype.jumps=function(a){var d;var b=this.expressions;var e=0;for(d=
-b.length;e<d;e++){var p=b[e];if(p=p.jumps(a))return p}};b.prototype.makeReturn=function(a){var d;for(d=this.expressions.length;d--;){var b=this.expressions[d];if(!(b instanceof n)){this.expressions[d]=b.makeReturn(a);b instanceof G&&!b.expression&&this.expressions.splice(d,1);break}}return this};b.prototype.compileToFragments=function(a,d){null==a&&(a={});return a.scope?b.__super__.compileToFragments.call(this,a,d):this.compileRoot(a)};b.prototype.compileNode=function(a){var d,p;this.tab=a.indent;
-var e=a.level===N;var t=[];var c=this.expressions;var f=d=0;for(p=c.length;d<p;f=++d){var g=c[f];g=g.unwrapAll();g=g.unfoldSoak(a)||g;g instanceof b?t.push(g.compileNode(a)):e?(g.front=!0,f=g.compileToFragments(a),g.isStatement(a)||(f.unshift(this.makeCode(""+this.tab)),f.push(this.makeCode(";"))),t.push(f)):t.push(g.compileToFragments(a,ta))}if(e)return this.spaced?[].concat(this.joinFragmentArrays(t,"\n\n"),this.makeCode("\n")):this.joinFragmentArrays(t,"\n");d=t.length?this.joinFragmentArrays(t,
-", "):[this.makeCode("void 0")];return 1<t.length&&a.level>=ta?this.wrapInBraces(d):d};b.prototype.compileRoot=function(a){var d,b;a.indent=a.bare?"":Ca;a.level=N;this.spaced=!0;a.scope=new xa(null,this,null,null!=(b=a.referencedVars)?b:[]);var e=a.locals||[];b=0;for(d=e.length;b<d;b++){var p=e[b];a.scope.parameter(p)}b=[];if(!a.bare){var t=this.expressions;d=[];var c=p=0;for(e=t.length;p<e;c=++p){c=t[c];if(!(c.unwrap()instanceof n))break;d.push(c)}p=this.expressions.slice(d.length);this.expressions=
-d;d.length&&(b=this.compileNode(ja(a,{indent:""})),b.push(this.makeCode("\n")));this.expressions=p}d=this.compileWithDeclarations(a);return a.bare?d:[].concat(b,this.makeCode("(function() {\n"),d,this.makeCode("\n}).call(this);\n"))};b.prototype.compileWithDeclarations=function(a){var d,b;var e=[];var p=this.expressions;var t=b=0;for(d=p.length;b<d;t=++b){var c=p[t];c=c.unwrap();if(!(c instanceof n||c instanceof z))break}a=ja(a,{level:N});t&&(c=this.expressions.splice(t,9E9),e=[this.spaced,!1],b=
-e[0],this.spaced=e[1],b=[this.compileNode(a),b],e=b[0],this.spaced=b[1],this.expressions=c);c=this.compileNode(a);b=a.scope;b.expressions===this&&(d=a.scope.hasDeclarations(),a=b.hasAssignments,d||a?(t&&e.push(this.makeCode("\n")),e.push(this.makeCode(this.tab+"var ")),d&&e.push(this.makeCode(b.declaredVariables().join(", "))),a&&(d&&e.push(this.makeCode(",\n"+(this.tab+Ca))),e.push(this.makeCode(b.assignedVariables().join(",\n"+(this.tab+Ca))))),e.push(this.makeCode(";\n"+(this.spaced?"\n":"")))):
-e.length&&c.length&&e.push(this.makeCode("\n")));return e.concat(c)};b.wrap=function(a){return 1===a.length&&a[0]instanceof b?a[0]:new b(a)};return b}(sa);f.Literal=z=function(a){function b(a){this.value=a}v(b,a);b.prototype.isComplex=ka;b.prototype.assigns=function(a){return a===this.value};b.prototype.compileNode=function(a){return[this.makeCode(this.value)]};b.prototype.toString=function(){return" "+(this.isStatement()?b.__super__.toString.apply(this,arguments):this.constructor.name)+": "+this.value};
-return b}(sa);f.NumberLiteral=w=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(z);f.InfinityLiteral=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);b.prototype.compileNode=function(){return[this.makeCode("2e308")]};return b}(w);f.NaNLiteral=function(a){function b(){b.__super__.constructor.call(this,"NaN")}v(b,a);b.prototype.compileNode=function(a){var d=[this.makeCode("0/0")];return a.level>=Fa?this.wrapInBraces(d):
-d};return b}(w);f.StringLiteral=D=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(z);f.RegexLiteral=X=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(z);f.PassthroughLiteral=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(z);f.IdentifierLiteral=x=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);b.prototype.isAssignable=ha;
-return b}(z);f.PropertyName=L=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);b.prototype.isAssignable=ha;return b}(z);f.StatementLiteral=W=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);b.prototype.isStatement=ha;b.prototype.makeReturn=na;b.prototype.jumps=function(a){if("break"===this.value&&!(null!=a&&a.loop||null!=a&&a.block)||"continue"===this.value&&(null==a||!a.loop))return this};b.prototype.compileNode=function(a){return[this.makeCode(""+
-this.tab+this.value+";")]};return b}(z);f.ThisLiteral=E=function(a){function b(){b.__super__.constructor.call(this,"this")}v(b,a);b.prototype.compileNode=function(a){var d;a=null!=(d=a.scope.method)&&d.bound?a.scope.method.context:this.value;return[this.makeCode(a)]};return b}(z);f.UndefinedLiteral=ca=function(a){function b(){b.__super__.constructor.call(this,"undefined")}v(b,a);b.prototype.compileNode=function(a){return[this.makeCode(a.level>=Ga?"(void 0)":"void 0")]};return b}(z);f.NullLiteral=
-c=function(a){function b(){b.__super__.constructor.call(this,"null")}v(b,a);return b}(z);f.BooleanLiteral=b=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(z);f.Return=G=function(a){function b(a){this.expression=a}v(b,a);b.prototype.children=["expression"];b.prototype.isStatement=ha;b.prototype.makeReturn=na;b.prototype.jumps=na;b.prototype.compileToFragments=function(a,d){var p;var e=null!=(p=this.expression)?p.makeReturn():void 0;return!e||e instanceof
-b?b.__super__.compileToFragments.call(this,a,d):e.compileToFragments(a,d)};b.prototype.compileNode=function(a){var b=[];b.push(this.makeCode(this.tab+("return"+(this.expression?" ":""))));this.expression&&(b=b.concat(this.expression.compileToFragments(a,Ka)));b.push(this.makeCode(";"));return b};return b}(sa);f.YieldReturn=T=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);b.prototype.compileNode=function(a){null==a.scope.parent&&this.error("yield can only occur inside functions");
-return b.__super__.compileNode.apply(this,arguments)};return b}(G);f.Value=C=function(a){function t(a,b,wa){if(!b&&a instanceof t)return a;this.base=a;this.properties=b||[];wa&&(this[wa]=!0);return this}v(t,a);t.prototype.children=["base","properties"];t.prototype.add=function(a){this.properties=this.properties.concat(a);return this};t.prototype.hasProperties=function(){return!!this.properties.length};t.prototype.bareLiteral=function(a){return!this.properties.length&&this.base instanceof a};t.prototype.isArray=
-function(){return this.bareLiteral(q)};t.prototype.isRange=function(){return this.bareLiteral(V)};t.prototype.isComplex=function(){return this.hasProperties()||this.base.isComplex()};t.prototype.isAssignable=function(){return this.hasProperties()||this.base.isAssignable()};t.prototype.isNumber=function(){return this.bareLiteral(w)};t.prototype.isString=function(){return this.bareLiteral(D)};t.prototype.isRegex=function(){return this.bareLiteral(X)};t.prototype.isUndefined=function(){return this.bareLiteral(ca)};
-t.prototype.isNull=function(){return this.bareLiteral(c)};t.prototype.isBoolean=function(){return this.bareLiteral(b)};t.prototype.isAtomic=function(){var a;var b=this.properties.concat(this.base);var wa=0;for(a=b.length;wa<a;wa++){var e=b[wa];if(e.soak||e instanceof ya)return!1}return!0};t.prototype.isNotCallable=function(){return this.isNumber()||this.isString()||this.isRegex()||this.isArray()||this.isRange()||this.isSplice()||this.isObject()||this.isUndefined()||this.isNull()||this.isBoolean()};
-t.prototype.isStatement=function(a){return!this.properties.length&&this.base.isStatement(a)};t.prototype.assigns=function(a){return!this.properties.length&&this.base.assigns(a)};t.prototype.jumps=function(a){return!this.properties.length&&this.base.jumps(a)};t.prototype.isObject=function(a){return this.properties.length?!1:this.base instanceof m&&(!a||this.base.generated)};t.prototype.isSplice=function(){var a=this.properties;return a[a.length-1]instanceof aa};t.prototype.looksStatic=function(a){var b;
-return this.base.value===a&&1===this.properties.length&&"prototype"!==(null!=(b=this.properties[0].name)?b.value:void 0)};t.prototype.unwrap=function(){return this.properties.length?this:this.base};t.prototype.cacheReference=function(a){var b=this.properties;var p=b[b.length-1];if(2>this.properties.length&&!this.base.isComplex()&&(null==p||!p.isComplex()))return[this,this];b=new t(this.base,this.properties.slice(0,-1));if(b.isComplex()){var e=new x(a.scope.freeVariable("base"));b=new t(new P(new y(e,
-b)))}if(!p)return[b,e];if(p.isComplex()){var c=new x(a.scope.freeVariable("name"));p=new R(new y(c,p.index));c=new R(c)}return[b.add(p),new t(e||b.base,[c||p])]};t.prototype.compileNode=function(a){var b;this.base.front=this.front;var p=this.properties;var e=this.base.compileToFragments(a,p.length?Ga:null);p.length&&Pa.test(da(e))&&e.push(this.makeCode("."));var t=0;for(b=p.length;t<b;t++){var c=p[t];e.push.apply(e,c.compileToFragments(a))}return e};t.prototype.unfoldSoak=function(a){return null!=
-this.unfoldedSoak?this.unfoldedSoak:this.unfoldedSoak=function(b){return function(){var d,e,p;if(e=b.base.unfoldSoak(a))return(d=e.body.properties).push.apply(d,b.properties),e;var c=b.properties;e=d=0;for(p=c.length;d<p;e=++d){var f=c[e];if(f.soak)return f.soak=!1,d=new t(b.base,b.properties.slice(0,e)),p=new t(b.base,b.properties.slice(e)),d.isComplex()&&(e=new x(a.scope.freeVariable("ref")),d=new P(new y(e,d)),p.base=e),new J(new B(d),p,{soak:!0})}return!1}}(this)()};return t}(sa);f.Comment=n=
-function(a){function b(a){this.comment=a}v(b,a);b.prototype.isStatement=ha;b.prototype.makeReturn=na;b.prototype.compileNode=function(a,b){var d=this.comment.replace(/^(\s*)#(?=\s)/gm,"$1 *");d="/*"+Ea(d,this.tab)+(0<=S.call(d,"\n")?"\n"+this.tab:"")+" */";(b||a.level)===N&&(d=a.indent+d);return[this.makeCode("\n"),this.makeCode(d)]};return b}(sa);f.Call=ya=function(a){function b(a,b,c){this.variable=a;this.args=null!=b?b:[];this.soak=c;this.isNew=!1;this.variable instanceof C&&this.variable.isNotCallable()&&
-this.variable.error("literal is not a function")}v(b,a);b.prototype.children=["variable","args"];b.prototype.updateLocationDataIfMissing=function(a){var d;if(this.locationData&&this.needsUpdatedStartLocation){this.locationData.first_line=a.first_line;this.locationData.first_column=a.first_column;var p=(null!=(d=this.variable)?d.base:void 0)||this.variable;p.needsUpdatedStartLocation&&(this.variable.locationData.first_line=a.first_line,this.variable.locationData.first_column=a.first_column,p.updateLocationDataIfMissing(a));
-delete this.needsUpdatedStartLocation}return b.__super__.updateLocationDataIfMissing.apply(this,arguments)};b.prototype.newInstance=function(){var a;var d=(null!=(a=this.variable)?a.base:void 0)||this.variable;d instanceof b&&!d.isNew?d.newInstance():this.isNew=!0;this.needsUpdatedStartLocation=!0;return this};b.prototype.unfoldSoak=function(a){var d,p;if(this.soak){if(this instanceof va){var e=new z(this.superReference(a));var c=new C(e)}else{if(c=Ba(a,this,"variable"))return c;c=(new C(this.variable)).cacheReference(a);
-e=c[0];c=c[1]}c=new b(c,this.args);c.isNew=this.isNew;e=new z("typeof "+e.compile(a)+' \x3d\x3d\x3d "function"');return new J(e,new C(c),{soak:!0})}e=this;for(d=[];;)if(e.variable instanceof b)d.push(e),e=e.variable;else{if(!(e.variable instanceof C))break;d.push(e);if(!((e=e.variable.base)instanceof b))break}var t=d.reverse();d=0;for(p=t.length;d<p;d++)e=t[d],c&&(e.variable instanceof b?e.variable=c:e.variable.base=c),c=Ba(a,e,"variable");return c};b.prototype.compileNode=function(a){var b,p,e;null!=
-(b=this.variable)&&(b.front=this.front);b=U.compileSplattedArray(a,this.args,!0);if(b.length)return this.compileSplat(a,b);b=[];var c=this.args;var t=p=0;for(e=c.length;p<e;t=++p){var f=c[t];t&&b.push(this.makeCode(", "));b.push.apply(b,f.compileToFragments(a,ta))}f=[];this instanceof va?(a=this.superReference(a)+(".call("+this.superThis(a)),b.length&&(a+=", "),f.push(this.makeCode(a))):(this.isNew&&f.push(this.makeCode("new ")),f.push.apply(f,this.variable.compileToFragments(a,Ga)),f.push(this.makeCode("(")));
-f.push.apply(f,b);f.push(this.makeCode(")"));return f};b.prototype.compileSplat=function(a,b){var d;if(this instanceof va)return[].concat(this.makeCode(this.superReference(a)+".apply("+this.superThis(a)+", "),b,this.makeCode(")"));if(this.isNew){var e=this.tab+Ca;return[].concat(this.makeCode("(function(func, args, ctor) {\n"+e+"ctor.prototype \x3d func.prototype;\n"+e+"var child \x3d new ctor, result \x3d func.apply(child, args);\n"+e+"return Object(result) \x3d\x3d\x3d result ? result : child;\n"+
-this.tab+"})("),this.variable.compileToFragments(a,ta),this.makeCode(", "),b,this.makeCode(", function(){})"))}e=[];var p=new C(this.variable);if((d=p.properties.pop())&&p.isComplex()){var c=a.scope.freeVariable("ref");e=e.concat(this.makeCode("("+c+" \x3d "),p.compileToFragments(a,ta),this.makeCode(")"),d.compileToFragments(a))}else p=p.compileToFragments(a,Ga),Pa.test(da(p))&&(p=this.wrapInBraces(p)),d?(c=da(p),p.push.apply(p,d.compileToFragments(a))):c="null",e=e.concat(p);return e.concat(this.makeCode(".apply("+
-c+", "),b,this.makeCode(")"))};return b}(sa);f.SuperCall=va=function(a){function b(a){b.__super__.constructor.call(this,null,null!=a?a:[new U(new x("arguments"))]);this.isBare=null!=a}v(b,a);b.prototype.superReference=function(a){var b=a.scope.namedMethod();if(null!=b&&b.klass){var p=b.klass;var e=b.name;var c=b.variable;if(p.isComplex()){var t=new x(a.scope.parent.freeVariable("base"));var f=new C(new P(new y(t,p)));c.base=f;c.properties.splice(0,p.properties.length)}if(e.isComplex()||e instanceof
-R&&e.index.isAssignable()){var g=new x(a.scope.parent.freeVariable("name"));e=new R(new y(g,e.index));c.properties.pop();c.properties.push(e)}f=[new qa(new L("__super__"))];b["static"]&&f.push(new qa(new L("constructor")));f.push(null!=g?new R(g):e);return(new C(null!=t?t:p,f)).compile(a)}return null!=b&&b.ctor?b.name+".__super__.constructor":this.error("cannot call super outside of an instance method.")};b.prototype.superThis=function(a){return(a=a.scope.method)&&!a.klass&&a.context||"this"};return b}(ya);
-f.RegexWithInterpolations=function(a){function b(a){null==a&&(a=[]);b.__super__.constructor.call(this,new C(new x("RegExp")),a,!1)}v(b,a);return b}(ya);f.TaggedTemplateCall=function(b){function c(b,d,t){d instanceof D&&(d=new A(a.wrap([new C(d)])));c.__super__.constructor.call(this,b,[d],t)}v(c,b);c.prototype.compileNode=function(a){a.inTaggedTemplateCall=!0;return this.variable.compileToFragments(a,Ga).concat(this.args[0].compileToFragments(a,ta))};return c}(ya);f.Extends=F=function(a){function b(a,
-b){this.child=a;this.parent=b}v(b,a);b.prototype.children=["child","parent"];b.prototype.compileToFragments=function(a){return(new ya(new C(new z(Ia("extend",a))),[this.child,this.parent])).compileToFragments(a)};return b}(sa);f.Access=qa=function(a){function b(a,b){this.name=a;this.soak="soak"===b}v(b,a);b.prototype.children=["name"];b.prototype.compileToFragments=function(a){var b;a=this.name.compileToFragments(a);var p=this.name.unwrap();return p instanceof L?(b=p.value,0<=S.call(ma,b))?[this.makeCode('["')].concat(M.call(a),
-[this.makeCode('"]')]):[this.makeCode(".")].concat(M.call(a)):[this.makeCode("[")].concat(M.call(a),[this.makeCode("]")])};b.prototype.isComplex=ka;return b}(sa);f.Index=R=function(a){function b(a){this.index=a}v(b,a);b.prototype.children=["index"];b.prototype.compileToFragments=function(a){return[].concat(this.makeCode("["),this.index.compileToFragments(a,Ka),this.makeCode("]"))};b.prototype.isComplex=function(){return this.index.isComplex()};return b}(sa);f.Range=V=function(a){function b(a,b,c){this.from=
-a;this.to=b;this.equals=(this.exclusive="exclusive"===c)?"":"\x3d"}v(b,a);b.prototype.children=["from","to"];b.prototype.compileVariables=function(a){a=ja(a,{top:!0});var b=la(a,"isComplex");var p=this.cacheToCodeFragments(this.from.cache(a,ta,b));this.fromC=p[0];this.fromVar=p[1];p=this.cacheToCodeFragments(this.to.cache(a,ta,b));this.toC=p[0];this.toVar=p[1];if(p=la(a,"step"))a=this.cacheToCodeFragments(p.cache(a,ta,b)),this.step=a[0],this.stepVar=a[1];this.fromNum=this.from.isNumber()?Number(this.fromVar):
-null;this.toNum=this.to.isNumber()?Number(this.toVar):null;return this.stepNum=null!=p&&p.isNumber()?Number(this.stepVar):null};b.prototype.compileNode=function(a){var b,p,e,c;this.fromVar||this.compileVariables(a);if(!a.index)return this.compileArray(a);var t=null!=this.fromNum&&null!=this.toNum;var f=la(a,"index");var g=(a=la(a,"name"))&&a!==f;var k=f+" \x3d "+this.fromC;this.toC!==this.toVar&&(k+=", "+this.toC);this.step!==this.stepVar&&(k+=", "+this.step);var h=[f+" \x3c"+this.equals,f+" \x3e"+
-this.equals];var m=h[0];h=h[1];m=null!=this.stepNum?0<this.stepNum?m+" "+this.toVar:h+" "+this.toVar:t?(e=[this.fromNum,this.toNum],p=e[0],c=e[1],e,p<=c?m+" "+c:h+" "+c):(b=this.stepVar?this.stepVar+" \x3e 0":this.fromVar+" \x3c\x3d "+this.toVar,b+" ? "+m+" "+this.toVar+" : "+h+" "+this.toVar);b=this.stepVar?f+" +\x3d "+this.stepVar:t?g?p<=c?"++"+f:"--"+f:p<=c?f+"++":f+"--":g?b+" ? ++"+f+" : --"+f:b+" ? "+f+"++ : "+f+"--";g&&(k=a+" \x3d "+k);g&&(b=a+" \x3d "+b);return[this.makeCode(k+"; "+m+"; "+
-b)]};b.prototype.compileArray=function(a){var b,p,e;if((b=null!=this.fromNum&&null!=this.toNum)&&20>=Math.abs(this.fromNum-this.toNum)){var c=function(){e=[];for(var a=p=this.fromNum,b=this.toNum;p<=b?a<=b:a>=b;p<=b?a++:a--)e.push(a);return e}.apply(this);this.exclusive&&c.pop();return[this.makeCode("["+c.join(", ")+"]")]}var t=this.tab+Ca;var f=a.scope.freeVariable("i",{single:!0});var g=a.scope.freeVariable("results");var k="\n"+t+g+" \x3d [];";if(b)a.index=f,b=da(this.compileNode(a));else{var h=
-f+" \x3d "+this.fromC+(this.toC!==this.toVar?", "+this.toC:"");b=this.fromVar+" \x3c\x3d "+this.toVar;b="var "+h+"; "+b+" ? "+f+" \x3c"+this.equals+" "+this.toVar+" : "+f+" \x3e"+this.equals+" "+this.toVar+"; "+b+" ? "+f+"++ : "+f+"--"}f="{ "+g+".push("+f+"); }\n"+t+"return "+g+";\n"+a.indent;a=function(a){return null!=a?a.contains(Va):void 0};if(a(this.from)||a(this.to))c=", arguments";return[this.makeCode("(function() {"+k+"\n"+t+"for ("+b+")"+f+"}).apply(this"+(null!=c?c:"")+")")]};return b}(sa);
-f.Slice=aa=function(a){function b(a){this.range=a;b.__super__.constructor.call(this)}v(b,a);b.prototype.children=["range"];b.prototype.compileNode=function(a){var b=this.range;var p=b.to;var e=(b=b.from)&&b.compileToFragments(a,Ka)||[this.makeCode("0")];if(p){b=p.compileToFragments(a,Ka);var c=da(b);if(this.range.exclusive||-1!==+c)var t=", "+(this.range.exclusive?c:p.isNumber()?""+(+c+1):(b=p.compileToFragments(a,Ga),"+"+da(b)+" + 1 || 9e9"))}return[this.makeCode(".slice("+da(e)+(t||"")+")")]};return b}(sa);
-f.Obj=m=function(a){function b(a,b){this.generated=null!=b?b:!1;this.objects=this.properties=a||[]}v(b,a);b.prototype.children=["properties"];b.prototype.compileNode=function(a){var b,p,e;var c=this.properties;if(this.generated){var t=0;for(b=c.length;t<b;t++){var f=c[t];f instanceof C&&f.error("cannot have an implicit value in an implicit object")}}t=b=0;for(f=c.length;b<f;t=++b){var g=c[t];if((g.variable||g).base instanceof P)break}f=t<c.length;var k=a.indent+=Ca;var h=this.lastNonComment(this.properties);
-b=[];if(f){var m=a.scope.freeVariable("obj");b.push(this.makeCode("(\n"+k+m+" \x3d "))}b.push(this.makeCode("{"+(0===c.length||0===t?"}":"\n")));var l=p=0;for(e=c.length;p<e;l=++p){g=c[l];l===t&&(0!==l&&b.push(this.makeCode("\n"+k+"}")),b.push(this.makeCode(",\n")));var w=l===c.length-1||l===t-1?"":g===h||g instanceof n?"\n":",\n";var q=g instanceof n?"":k;f&&l<t&&(q+=Ca);g instanceof y&&("object"!==g.context&&g.operatorToken.error("unexpected "+g.operatorToken.value),g.variable instanceof C&&g.variable.hasProperties()&&
-g.variable.error("invalid object key"));g instanceof C&&g["this"]&&(g=new y(g.properties[0].name,g,"object"));g instanceof n||(l<t?g instanceof y||(g=new y(g,g,"object")):(g instanceof y?(l=g.variable,g=g.value):(g=g.base.cache(a),l=g[0],g=g[1],l instanceof x&&(l=new L(l.value))),g=new y(new C(new x(m),[new qa(l)]),g)));q&&b.push(this.makeCode(q));b.push.apply(b,g.compileToFragments(a,N));w&&b.push(this.makeCode(w))}f?b.push(this.makeCode(",\n"+k+m+"\n"+this.tab+")")):0!==c.length&&b.push(this.makeCode("\n"+
-this.tab+"}"));return this.front&&!f?this.wrapInBraces(b):b};b.prototype.assigns=function(a){var b;var p=this.properties;var e=0;for(b=p.length;e<b;e++){var c=p[e];if(c.assigns(a))return!0}return!1};return b}(sa);f.Arr=q=function(a){function b(a){this.objects=a||[]}v(b,a);b.prototype.children=["objects"];b.prototype.compileNode=function(a){var b;if(!this.objects.length)return[this.makeCode("[]")];a.indent+=Ca;var p=U.compileSplattedArray(a,this.objects);if(p.length)return p;p=[];var e=this.objects;
-var c=[];var t=0;for(b=e.length;t<b;t++){var f=e[t];c.push(f.compileToFragments(a,ta))}t=b=0;for(e=c.length;b<e;t=++b)f=c[t],t&&p.push(this.makeCode(", ")),p.push.apply(p,f);0<=da(p).indexOf("\n")?(p.unshift(this.makeCode("[\n"+a.indent)),p.push(this.makeCode("\n"+this.tab+"]"))):(p.unshift(this.makeCode("[")),p.push(this.makeCode("]")));return p};b.prototype.assigns=function(a){var b;var p=this.objects;var e=0;for(b=p.length;e<b;e++){var c=p[e];if(c.assigns(a))return!0}return!1};return b}(sa);f.Class=
-g=function(b){function c(b,d,c){this.variable=b;this.parent=d;this.body=null!=c?c:new a;this.boundFuncs=[];this.body.classBody=!0}v(c,b);c.prototype.children=["variable","parent","body"];c.prototype.defaultClassVariableName="_Class";c.prototype.determineName=function(){var a;if(!this.variable)return this.defaultClassVariableName;var b=this.variable.properties;b=(a=b[b.length-1])?a instanceof qa&&a.name:this.variable.base;if(!(b instanceof x||b instanceof L))return this.defaultClassVariableName;b=
-b.value;a||(a=za(b))&&this.variable.error(a);return 0<=S.call(ma,b)?"_"+b:b};c.prototype.setContext=function(a){return this.body.traverseChildren(!1,function(b){if(b.classBody)return!1;if(b instanceof E)return b.value=a;if(b instanceof h&&b.bound)return b.context=a})};c.prototype.addBoundFunctions=function(a){var b;var p=this.boundFuncs;var e=0;for(b=p.length;e<b;e++){var c=p[e];c=(new C(new E,[new qa(c)])).compile(a);this.ctor.body.unshift(new z(c+" \x3d "+Ia("bind",a)+"("+c+", this)"))}};c.prototype.addProperties=
-function(a,b,c){var d;var p=a.base.properties.slice(0);var f;for(f=[];d=p.shift();){if(d instanceof y){var t=d.variable.base;delete d.context;var g=d.value;"constructor"===t.value?(this.ctor&&d.error("cannot define more than one constructor in a class"),g.bound&&d.error("cannot define a constructor as a bound function"),g instanceof h?d=this.ctor=g:(this.externalCtor=c.classScope.freeVariable("ctor"),d=new y(new x(this.externalCtor),g))):d.variable["this"]?g["static"]=!0:(a=t.isComplex()?new R(t):
-new qa(t),d.variable=new C(new x(b),[new qa(new L("prototype")),a]),g instanceof h&&g.bound&&(this.boundFuncs.push(t),g.bound=!1))}f.push(d)}return fa(f)};c.prototype.walkBody=function(b,d){return this.traverseChildren(!1,function(p){return function(e){var f,t,g;var wa=!0;if(e instanceof c)return!1;if(e instanceof a){var k=f=e.expressions;var h=t=0;for(g=k.length;t<g;h=++t){var m=k[h];m instanceof y&&m.variable.looksStatic(b)?m.value["static"]=!0:m instanceof C&&m.isObject(!0)&&(wa=!1,f[h]=p.addProperties(m,
-b,d))}e.expressions=ia(f)}return wa&&!(e instanceof c)}}(this))};c.prototype.hoistDirectivePrologue=function(){var a,b;var c=0;for(a=this.body.expressions;(b=a[c])&&b instanceof n||b instanceof C&&b.isString();)++c;return this.directives=a.splice(0,c)};c.prototype.ensureConstructor=function(a){this.ctor||(this.ctor=new h,this.externalCtor?this.ctor.body.push(new z(this.externalCtor+".apply(this, arguments)")):this.parent&&this.ctor.body.push(new z(a+".__super__.constructor.apply(this, arguments)")),
-this.ctor.body.makeReturn(),this.body.expressions.unshift(this.ctor));this.ctor.ctor=this.ctor.name=a;this.ctor.klass=null;return this.ctor.noReturn=!0};c.prototype.compileNode=function(b){var d,c,e;(c=this.body.jumps())&&c.error("Class bodies cannot contain pure statements");(d=this.body.contains(Va))&&d.error("Class bodies shouldn't reference arguments");var p=this.determineName();var f=new x(p);c=new h([],a.wrap([this.body]));d=[];b.classScope=c.makeScope(b.scope);this.hoistDirectivePrologue();
-this.setContext(p);this.walkBody(p,b);this.ensureConstructor(p);this.addBoundFunctions(b);this.body.spaced=!0;this.body.expressions.push(f);this.parent&&(p=new x(b.classScope.freeVariable("superClass",{reserve:!1})),this.body.expressions.unshift(new F(f,p)),c.params.push(new K(p)),d.push(this.parent));(e=this.body.expressions).unshift.apply(e,this.directives);e=new P(new ya(c,d));this.variable&&(e=new y(this.variable,e,null,{moduleDeclaration:this.moduleDeclaration}));return e.compileToFragments(b)};
-return c}(sa);f.ModuleDeclaration=Z=function(a){function b(a,b){this.clause=a;this.source=b;this.checkSource()}v(b,a);b.prototype.children=["clause","source"];b.prototype.isStatement=ha;b.prototype.jumps=na;b.prototype.makeReturn=na;b.prototype.checkSource=function(){if(null!=this.source&&this.source instanceof A)return this.source.error("the name of the module to be imported from must be an uninterpolated string")};b.prototype.checkScope=function(a,b){if(0!==a.indent.length)return this.error(b+" statements must be at top-level scope")};
-return b}(sa);f.ImportDeclaration=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);b.prototype.compileNode=function(a){var b;this.checkScope(a,"import");a.importedSymbols=[];var c=[];c.push(this.makeCode(this.tab+"import "));null!=this.clause&&c.push.apply(c,this.clause.compileNode(a));null!=(null!=(b=this.source)?b.value:void 0)&&(null!==this.clause&&c.push(this.makeCode(" from ")),c.push(this.makeCode(this.source.value)));c.push(this.makeCode(";"));return c};
-return b}(Z);f.ImportClause=function(a){function b(a,b){this.defaultBinding=a;this.namedImports=b}v(b,a);b.prototype.children=["defaultBinding","namedImports"];b.prototype.compileNode=function(a){var b=[];null!=this.defaultBinding&&(b.push.apply(b,this.defaultBinding.compileNode(a)),null!=this.namedImports&&b.push(this.makeCode(", ")));null!=this.namedImports&&b.push.apply(b,this.namedImports.compileNode(a));return b};return b}(sa);f.ExportDeclaration=Z=function(b){function c(){return c.__super__.constructor.apply(this,
-arguments)}v(c,b);c.prototype.compileNode=function(b){var d;this.checkScope(b,"export");var c=[];c.push(this.makeCode(this.tab+"export "));this instanceof I&&c.push(this.makeCode("default "));this instanceof I||!(this.clause instanceof y||this.clause instanceof g)||(this.clause instanceof g&&!this.clause.variable&&this.clause.error("anonymous classes cannot be exported"),c.push(this.makeCode("var ")),this.clause.moduleDeclaration="export");c=null!=this.clause.body&&this.clause.body instanceof a?c.concat(this.clause.compileToFragments(b,
-N)):c.concat(this.clause.compileNode(b));null!=(null!=(d=this.source)?d.value:void 0)&&c.push(this.makeCode(" from "+this.source.value));c.push(this.makeCode(";"));return c};return c}(Z);f.ExportNamedDeclaration=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(Z);f.ExportDefaultDeclaration=I=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(Z);f.ExportAllDeclaration=function(a){function b(){return b.__super__.constructor.apply(this,
-arguments)}v(b,a);return b}(Z);f.ModuleSpecifierList=Z=function(a){function b(a){this.specifiers=a}v(b,a);b.prototype.children=["specifiers"];b.prototype.compileNode=function(a){var b;var c=[];a.indent+=Ca;var e=this.specifiers;var p=[];var f=0;for(b=e.length;f<b;f++){var g=e[f];p.push(g.compileToFragments(a,ta))}if(0!==this.specifiers.length){c.push(this.makeCode("{\n"+a.indent));f=b=0;for(e=p.length;b<e;f=++b)g=p[f],f&&c.push(this.makeCode(",\n"+a.indent)),c.push.apply(c,g);c.push(this.makeCode("\n}"))}else c.push(this.makeCode("{}"));
-return c};return b}(sa);f.ImportSpecifierList=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(Z);f.ExportSpecifierList=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(Z);f.ModuleSpecifier=l=function(a){function b(a,b,c){this.original=a;this.alias=b;this.moduleDeclarationType=c;this.identifier=null!=this.alias?this.alias.value:this.original.value}v(b,a);b.prototype.children=["original","alias"];b.prototype.compileNode=
-function(a){a.scope.find(this.identifier,this.moduleDeclarationType);a=[];a.push(this.makeCode(this.original.value));null!=this.alias&&a.push(this.makeCode(" as "+this.alias.value));return a};return b}(sa);f.ImportSpecifier=Z=function(a){function b(a,d){b.__super__.constructor.call(this,a,d,"import")}v(b,a);b.prototype.compileNode=function(a){var d;(d=this.identifier,0<=S.call(a.importedSymbols,d))||a.scope.check(this.identifier)?this.error("'"+this.identifier+"' has already been declared"):a.importedSymbols.push(this.identifier);
-return b.__super__.compileNode.call(this,a)};return b}(l);f.ImportDefaultSpecifier=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(Z);f.ImportNamespaceSpecifier=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(Z);f.ExportSpecifier=function(a){function b(a,d){b.__super__.constructor.call(this,a,d,"export")}v(b,a);return b}(l);f.Assign=y=function(a){function b(a,b,c,e){this.variable=a;this.value=b;this.context=
-c;null==e&&(e={});this.param=e.param;this.subpattern=e.subpattern;this.operatorToken=e.operatorToken;this.moduleDeclaration=e.moduleDeclaration}v(b,a);b.prototype.children=["variable","value"];b.prototype.isStatement=function(a){return(null!=a?a.level:void 0)===N&&null!=this.context&&(this.moduleDeclaration||0<=S.call(this.context,"?"))};b.prototype.checkAssignability=function(a,b){if(Object.prototype.hasOwnProperty.call(a.scope.positions,b.value)&&"import"===a.scope.variables[a.scope.positions[b.value]].type)return b.error("'"+
-b.value+"' is read-only")};b.prototype.assigns=function(a){return this["object"===this.context?"value":"variable"].assigns(a)};b.prototype.unfoldSoak=function(a){return Ba(a,this,"variable")};b.prototype.compileNode=function(a){var b,c,e,p,f,g,k;if(c=this.variable instanceof C){if(this.variable.isArray()||this.variable.isObject())return this.compilePatternMatch(a);if(this.variable.isSplice())return this.compileSplice(a);if("||\x3d"===(p=this.context)||"\x26\x26\x3d"===p||"?\x3d"===p)return this.compileConditional(a);
-if("**\x3d"===(f=this.context)||"//\x3d"===f||"%%\x3d"===f)return this.compileSpecialMath(a)}this.value instanceof h&&(this.value["static"]?(this.value.klass=this.variable.base,this.value.name=this.variable.properties[0],this.value.variable=this.variable):2<=(null!=(g=this.variable.properties)?g.length:void 0)&&(g=this.variable.properties,p=3<=g.length?M.call(g,0,e=g.length-2):(e=0,[]),f=g[e++],e=g[e++],"prototype"===(null!=(k=f.name)?k.value:void 0)&&(this.value.klass=new C(this.variable.base,p),
-this.value.name=e,this.value.variable=this.variable)));this.context||(k=this.variable.unwrapAll(),k.isAssignable()||this.variable.error("'"+this.variable.compile(a)+"' can't be assigned"),"function"===typeof k.hasProperties&&k.hasProperties()||(this.moduleDeclaration?(this.checkAssignability(a,k),a.scope.add(k.value,this.moduleDeclaration)):this.param?a.scope.add(k.value,"var"):(this.checkAssignability(a,k),a.scope.find(k.value))));k=this.value.compileToFragments(a,ta);c&&this.variable.base instanceof
-m&&(this.variable.front=!0);c=this.variable.compileToFragments(a,ta);if("object"===this.context){if(b=da(c),0<=S.call(ma,b))c.unshift(this.makeCode('"')),c.push(this.makeCode('"'));return c.concat(this.makeCode(": "),k)}b=c.concat(this.makeCode(" "+(this.context||"\x3d")+" "),k);return a.level<=ta?b:this.wrapInBraces(b)};b.prototype.compilePatternMatch=function(a){var d,c,e;var p=a.level===N;var f=this.value;var g=this.variable.base.objects;if(!(e=g.length)){var t=f.compileToFragments(a);return a.level>=
-Fa?this.wrapInBraces(t):t}var h=g[0];1===e&&h instanceof H&&h.error("Destructuring assignment has no target");var m=this.variable.isObject();if(p&&1===e&&!(h instanceof U)){var l=null;if(h instanceof b&&"object"===h.context){t=h;var n=t.variable;var q=n.base;h=t.value;h instanceof b&&(l=h.value,h=h.variable)}else h instanceof b&&(l=h.value,h=h.variable),q=m?h["this"]?h.properties[0].name:new L(h.unwrap().value):new w(0);var r=q.unwrap()instanceof L;f=new C(f);f.properties.push(new (r?qa:R)(q));(c=
-za(h.unwrap().value))&&h.error(c);l&&(f=new k("?",f,l));return(new b(h,f,null,{param:this.param})).compileToFragments(a,N)}var v=f.compileToFragments(a,ta);var y=da(v);t=[];n=!1;f.unwrap()instanceof x&&!this.variable.assigns(y)||(t.push([this.makeCode((l=a.scope.freeVariable("ref"))+" \x3d ")].concat(M.call(v))),v=[this.makeCode(l)],y=l);l=f=0;for(d=g.length;f<d;l=++f){h=g[l];q=l;if(!n&&h instanceof U){c=h.name.unwrap().value;h=h.unwrap();q=e+" \x3c\x3d "+y+".length ? "+Ia("slice",a)+".call("+y+", "+
-l;if(r=e-l-1){var u=a.scope.freeVariable("i",{single:!0});q+=", "+u+" \x3d "+y+".length - "+r+") : ("+u+" \x3d "+l+", [])"}else q+=") : []";q=new z(q);n=u+"++"}else if(!n&&h instanceof H){if(r=e-l-1)1===r?n=y+".length - 1":(u=a.scope.freeVariable("i",{single:!0}),q=new z(u+" \x3d "+y+".length - "+r),n=u+"++",t.push(q.compileToFragments(a,ta)));continue}else(h instanceof U||h instanceof H)&&h.error("multiple splats/expansions are disallowed in an assignment"),l=null,h instanceof b&&"object"===h.context?
-(q=h.variable,q=q.base,h=h.value,h instanceof b&&(l=h.value,h=h.variable)):(h instanceof b&&(l=h.value,h=h.variable),q=m?h["this"]?h.properties[0].name:new L(h.unwrap().value):new z(n||q)),c=h.unwrap().value,r=q.unwrap()instanceof L,q=new C(new z(y),[new (r?qa:R)(q)]),l&&(q=new k("?",q,l));null!=c&&(c=za(c))&&h.error(c);t.push((new b(h,q,null,{param:this.param,subpattern:!0})).compileToFragments(a,ta))}p||this.subpattern||t.push(v);t=this.joinFragmentArrays(t,", ");return a.level<ta?t:this.wrapInBraces(t)};
-b.prototype.compileConditional=function(a){var d=this.variable.cacheReference(a);var c=d[0];d=d[1];c.properties.length||!(c.base instanceof z)||c.base instanceof E||a.scope.check(c.base.value)||this.variable.error('the variable "'+c.base.value+"\" can't be assigned with "+this.context+" because it has not been declared before");if(0<=S.call(this.context,"?"))return a.isExistentialEquals=!0,(new J(new B(c),d,{type:"if"})).addElse(new b(d,this.value,"\x3d")).compileToFragments(a);c=(new k(this.context.slice(0,
--1),c,new b(d,this.value,"\x3d"))).compileToFragments(a);return a.level<=ta?c:this.wrapInBraces(c)};b.prototype.compileSpecialMath=function(a){var d=this.variable.cacheReference(a);var c=d[0];d=d[1];return(new b(c,new k(this.context.slice(0,-1),d,this.value))).compileToFragments(a)};b.prototype.compileSplice=function(a){var b=this.variable.properties.pop().range;var c=b.from;var e=b.to;var p=b.exclusive;var f=this.variable.compile(a);if(c){var g=this.cacheToCodeFragments(c.cache(a,Fa));b=g[0];g=g[1]}else b=
-g="0";e?null!=c&&c.isNumber()&&e.isNumber()?(e=e.compile(a)-g,p||(e+=1)):(e=e.compile(a,Ga)+" - "+g,p||(e+=" + 1")):e="9e9";p=this.value.cache(a,ta);c=p[0];p=p[1];e=[].concat(this.makeCode("[].splice.apply("+f+", ["+b+", "+e+"].concat("),c,this.makeCode(")), "),p);return a.level>N?this.wrapInBraces(e):e};return b}(sa);f.Code=h=function(b){function c(b,d,c){this.params=b||[];this.body=d||new a;this.bound="boundfunc"===c;this.isGenerator=!!this.body.contains(function(a){return a instanceof k&&a.isYield()||
-a instanceof T})}v(c,b);c.prototype.children=["params","body"];c.prototype.isStatement=function(){return!!this.ctor};c.prototype.jumps=ka;c.prototype.makeScope=function(a){return new xa(a,this.body,this)};c.prototype.compileNode=function(b){var d,f,e,g;this.bound&&null!=(d=b.scope.method)&&d.bound&&(this.context=b.scope.method.context);if(this.bound&&!this.context)return this.context="_this",d=new c([new K(new x(this.context))],new a([this])),d=new ya(d,[new E]),d.updateLocationDataIfMissing(this.locationData),
-d.compileNode(b);b.scope=la(b,"classScope")||this.makeScope(b.scope);b.scope.shared=la(b,"sharedScope");b.indent+=Ca;delete b.bare;delete b.isExistentialEquals;d=[];var p=[];var h=this.params;var t=0;for(e=h.length;t<e;t++){var l=h[t];l instanceof H||b.scope.parameter(l.asReference(b))}h=this.params;t=0;for(e=h.length;t<e;t++)if(l=h[t],l.splat||l instanceof H){t=this.params;var m=0;for(l=t.length;m<l;m++){var n=t[m];n instanceof H||!n.name.value||b.scope.add(n.name.value,"var",!0)}m=new y(new C(new q(function(){var a;
-var d=this.params;var e=[];var c=0;for(a=d.length;c<a;c++)n=d[c],e.push(n.asReference(b));return e}.call(this))),new C(new x("arguments")));break}var w=this.params;h=0;for(t=w.length;h<t;h++){l=w[h];if(l.isComplex()){var r=g=l.asReference(b);l.value&&(r=new k("?",g,l.value));p.push(new y(new C(l.name),r,"\x3d",{param:!0}))}else g=l,l.value&&(e=new z(g.name.value+" \x3d\x3d null"),r=new y(new C(l.name),l.value,"\x3d"),p.push(new J(e,r)));m||d.push(g)}l=this.body.isEmpty();m&&p.unshift(m);p.length&&
-(f=this.body.expressions).unshift.apply(f,p);f=m=0;for(p=d.length;m<p;f=++m)n=d[f],d[f]=n.compileToFragments(b),b.scope.parameter(da(d[f]));var v=[];this.eachParamName(function(a,b){0<=S.call(v,a)&&b.error("multiple parameters named "+a);return v.push(a)});l||this.noReturn||this.body.makeReturn();f="function";this.isGenerator&&(f+="*");this.ctor&&(f+=" "+this.name);p=[this.makeCode(f+"(")];f=l=0;for(m=d.length;l<m;f=++l)n=d[f],f&&p.push(this.makeCode(", ")),p.push.apply(p,n);p.push(this.makeCode(") {"));
-this.body.isEmpty()||(p=p.concat(this.makeCode("\n"),this.body.compileWithDeclarations(b),this.makeCode("\n"+this.tab)));p.push(this.makeCode("}"));return this.ctor?[this.makeCode(this.tab)].concat(M.call(p)):this.front||b.level>=Ga?this.wrapInBraces(p):p};c.prototype.eachParamName=function(a){var b;var c=this.params;var e=[];var f=0;for(b=c.length;f<b;f++){var p=c[f];e.push(p.eachName(a))}return e};c.prototype.traverseChildren=function(a,b){if(a)return c.__super__.traverseChildren.call(this,a,b)};
-return c}(sa);f.Param=K=function(a){function b(a,b,c){this.name=a;this.value=b;this.splat=c;(a=za(this.name.unwrapAll().value))&&this.name.error(a);this.name instanceof m&&this.name.generated&&(a=this.name.objects[0].operatorToken,a.error("unexpected "+a.value))}v(b,a);b.prototype.children=["name","value"];b.prototype.compileToFragments=function(a){return this.name.compileToFragments(a,ta)};b.prototype.asReference=function(a){if(this.reference)return this.reference;var b=this.name;b["this"]?(b=b.properties[0].name.value,
-0<=S.call(ma,b)&&(b="_"+b),b=new x(a.scope.freeVariable(b))):b.isComplex()&&(b=new x(a.scope.freeVariable("arg")));b=new C(b);this.splat&&(b=new U(b));b.updateLocationDataIfMissing(this.locationData);return this.reference=b};b.prototype.isComplex=function(){return this.name.isComplex()};b.prototype.eachName=function(a,b){var d,e;null==b&&(b=this.name);var c=function(b){return a("@"+b.properties[0].name.value,b)};if(b instanceof z)return a(b.value,b);if(b instanceof C)return c(b);b=null!=(d=b.objects)?
-d:[];d=0;for(e=b.length;d<e;d++){var f=b[d];f instanceof y&&null==f.context&&(f=f.variable);f instanceof y?(f.value instanceof y&&(f=f.value),this.eachName(a,f.value.unwrap())):f instanceof U?(f=f.name.unwrap(),a(f.value,f)):f instanceof C?f.isArray()||f.isObject()?this.eachName(a,f.base):f["this"]?c(f):a(f.base.value,f.base):f instanceof H||f.error("illegal parameter "+f.compile())}};return b}(sa);f.Splat=U=function(a){function b(a){this.name=a.compile?a:new z(a)}v(b,a);b.prototype.children=["name"];
-b.prototype.isAssignable=ha;b.prototype.assigns=function(a){return this.name.assigns(a)};b.prototype.compileToFragments=function(a){return this.name.compileToFragments(a)};b.prototype.unwrap=function(){return this.name};b.compileSplattedArray=function(a,d,c){var e,f,g,p;for(f=-1;(e=d[++f])&&!(e instanceof b););if(f>=d.length)return[];if(1===d.length)return e=d[0],d=e.compileToFragments(a,ta),c?d:[].concat(e.makeCode(Ia("slice",a)+".call("),d,e.makeCode(")"));c=d.slice(f);var h=g=0;for(p=c.length;g<
-p;h=++g){e=c[h];var k=e.compileToFragments(a,ta);c[h]=e instanceof b?[].concat(e.makeCode(Ia("slice",a)+".call("),k,e.makeCode(")")):[].concat(e.makeCode("["),k,e.makeCode("]"))}if(0===f)return e=d[0],a=e.joinFragmentArrays(c.slice(1),", "),c[0].concat(e.makeCode(".concat("),a,e.makeCode(")"));g=d.slice(0,f);p=[];k=0;for(h=g.length;k<h;k++)e=g[k],p.push(e.compileToFragments(a,ta));e=d[0].joinFragmentArrays(p,", ");a=d[f].joinFragmentArrays(c,", ");c=d[d.length-1];return[].concat(d[0].makeCode("["),
-e,d[f].makeCode("].concat("),a,c.makeCode(")"))};return b}(sa);f.Expansion=H=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);b.prototype.isComplex=ka;b.prototype.compileNode=function(a){return this.error("Expansion must be used inside a destructuring assignment or parameter list")};b.prototype.asReference=function(a){return this};b.prototype.eachName=function(a){};return b}(sa);f.While=Z=function(b){function c(a,b){this.condition=null!=b&&b.invert?a.invert():a;
-this.guard=null!=b?b.guard:void 0}v(c,b);c.prototype.children=["condition","guard","body"];c.prototype.isStatement=ha;c.prototype.makeReturn=function(a){if(a)return c.__super__.makeReturn.apply(this,arguments);this.returns=!this.jumps({loop:!0});return this};c.prototype.addBody=function(a){this.body=a;return this};c.prototype.jumps=function(){var a;var b=this.body.expressions;if(!b.length)return!1;var c=0;for(a=b.length;c<a;c++){var e=b[c];if(e=e.jumps({loop:!0}))return e}return!1};c.prototype.compileNode=
-function(b){var d;b.indent+=Ca;var c="";var e=this.body;e.isEmpty()?e=this.makeCode(""):(this.returns&&(e.makeReturn(d=b.scope.freeVariable("results")),c=""+this.tab+d+" \x3d [];\n"),this.guard&&(1<e.expressions.length?e.expressions.unshift(new J((new P(this.guard)).invert(),new W("continue"))):this.guard&&(e=a.wrap([new J(this.guard,e)]))),e=[].concat(this.makeCode("\n"),e.compileToFragments(b,N),this.makeCode("\n"+this.tab)));b=[].concat(this.makeCode(c+this.tab+"while ("),this.condition.compileToFragments(b,
-Ka),this.makeCode(") {"),e,this.makeCode("}"));this.returns&&b.push(this.makeCode("\n"+this.tab+"return "+d+";"));return b};return c}(sa);f.Op=k=function(a){function b(a,b,d,f){if("in"===a)return new O(b,d);if("do"===a)return this.generateDo(b);if("new"===a){if(b instanceof ya&&!b["do"]&&!b.isNew)return b.newInstance();if(b instanceof h&&b.bound||b["do"])b=new P(b)}this.operator=c[a]||a;this.first=b;this.second=d;this.flip=!!f;return this}v(b,a);var c={"\x3d\x3d":"\x3d\x3d\x3d","!\x3d":"!\x3d\x3d",
-of:"in",yieldfrom:"yield*"};var d={"!\x3d\x3d":"\x3d\x3d\x3d","\x3d\x3d\x3d":"!\x3d\x3d"};b.prototype.children=["first","second"];b.prototype.isNumber=function(){var a;return this.isUnary()&&("+"===(a=this.operator)||"-"===a)&&this.first instanceof C&&this.first.isNumber()};b.prototype.isYield=function(){var a;return"yield"===(a=this.operator)||"yield*"===a};b.prototype.isUnary=function(){return!this.second};b.prototype.isComplex=function(){return!this.isNumber()};b.prototype.isChainable=function(){var a;
-return"\x3c"===(a=this.operator)||"\x3e"===a||"\x3e\x3d"===a||"\x3c\x3d"===a||"\x3d\x3d\x3d"===a||"!\x3d\x3d"===a};b.prototype.invert=function(){var a,e;if(this.isChainable()&&this.first.isChainable()){var c=!0;for(a=this;a&&a.operator;)c&&(c=a.operator in d),a=a.first;if(!c)return(new P(this)).invert();for(a=this;a&&a.operator;)a.invert=!a.invert,a.operator=d[a.operator],a=a.first;return this}return(a=d[this.operator])?(this.operator=a,this.first.unwrap()instanceof b&&this.first.invert(),this):this.second?
-(new P(this)).invert():"!"===this.operator&&(c=this.first.unwrap())instanceof b&&("!"===(e=c.operator)||"in"===e||"instanceof"===e)?c:new b("!",this)};b.prototype.unfoldSoak=function(a){var b;return("++"===(b=this.operator)||"--"===b||"delete"===b)&&Ba(a,this,"first")};b.prototype.generateDo=function(a){var b,d;var c=[];var f=(a instanceof y&&(b=a.value.unwrap())instanceof h?b:a).params||[];b=0;for(d=f.length;b<d;b++){var g=f[b];g.value?(c.push(g.value),delete g.value):c.push(g)}a=new ya(a,c);a["do"]=
-!0;return a};b.prototype.compileNode=function(a){var b;var d=this.isChainable()&&this.first.isChainable();d||(this.first.front=this.front);"delete"===this.operator&&a.scope.check(this.first.unwrapAll().value)&&this.error("delete operand may not be argument or var");("--"===(b=this.operator)||"++"===b)&&(b=za(this.first.unwrapAll().value))&&this.first.error(b);if(this.isYield())return this.compileYield(a);if(this.isUnary())return this.compileUnary(a);if(d)return this.compileChain(a);switch(this.operator){case "?":return this.compileExistence(a);
-case "**":return this.compilePower(a);case "//":return this.compileFloorDivision(a);case "%%":return this.compileModulo(a);default:return d=this.first.compileToFragments(a,Fa),b=this.second.compileToFragments(a,Fa),d=[].concat(d,this.makeCode(" "+this.operator+" "),b),a.level<=Fa?d:this.wrapInBraces(d)}};b.prototype.compileChain=function(a){var b=this.first.second.cache(a);this.first.second=b[0];b=b[1];a=this.first.compileToFragments(a,Fa).concat(this.makeCode(" "+(this.invert?"\x26\x26":"||")+" "),
-b.compileToFragments(a),this.makeCode(" "+this.operator+" "),this.second.compileToFragments(a,Fa));return this.wrapInBraces(a)};b.prototype.compileExistence=function(a){if(this.first.isComplex()){var b=new x(a.scope.freeVariable("ref"));var d=new P(new y(b,this.first))}else b=d=this.first;return(new J(new B(d),b,{type:"if"})).addElse(this.second).compileToFragments(a)};b.prototype.compileUnary=function(a){var d=[];var c=this.operator;d.push([this.makeCode(c)]);if("!"===c&&this.first instanceof B)return this.first.negated=
-!this.first.negated,this.first.compileToFragments(a);if(a.level>=Ga)return(new P(this)).compileToFragments(a);var f="+"===c||"-"===c;("new"===c||"typeof"===c||"delete"===c||f&&this.first instanceof b&&this.first.operator===c)&&d.push([this.makeCode(" ")]);if(f&&this.first instanceof b||"new"===c&&this.first.isStatement(a))this.first=new P(this.first);d.push(this.first.compileToFragments(a,Fa));this.flip&&d.reverse();return this.joinFragmentArrays(d,"")};b.prototype.compileYield=function(a){var b;
-var d=[];var c=this.operator;null==a.scope.parent&&this.error("yield can only occur inside functions");0<=S.call(Object.keys(this.first),"expression")&&!(this.first instanceof ba)?null!=this.first.expression&&d.push(this.first.expression.compileToFragments(a,Fa)):(a.level>=Ka&&d.push([this.makeCode("(")]),d.push([this.makeCode(c)]),""!==(null!=(b=this.first.base)?b.value:void 0)&&d.push([this.makeCode(" ")]),d.push(this.first.compileToFragments(a,Fa)),a.level>=Ka&&d.push([this.makeCode(")")]));return this.joinFragmentArrays(d,
-"")};b.prototype.compilePower=function(a){var b=new C(new x("Math"),[new qa(new L("pow"))]);return(new ya(b,[this.first,this.second])).compileToFragments(a)};b.prototype.compileFloorDivision=function(a){var d=new C(new x("Math"),[new qa(new L("floor"))]);var c=this.second.isComplex()?new P(this.second):this.second;c=new b("/",this.first,c);return(new ya(d,[c])).compileToFragments(a)};b.prototype.compileModulo=function(a){var b=new C(new z(Ia("modulo",a)));return(new ya(b,[this.first,this.second])).compileToFragments(a)};
-b.prototype.toString=function(a){return b.__super__.toString.call(this,a,this.constructor.name+" "+this.operator)};return b}(sa);f.In=O=function(a){function b(a,b){this.object=a;this.array=b}v(b,a);b.prototype.children=["object","array"];b.prototype.invert=ra;b.prototype.compileNode=function(a){var b;if(this.array instanceof C&&this.array.isArray()&&this.array.base.objects.length){var c=this.array.base.objects;var e=0;for(b=c.length;e<b;e++){var f=c[e];if(f instanceof U){var g=!0;break}}if(!g)return this.compileOrTest(a)}return this.compileLoopTest(a)};
-b.prototype.compileOrTest=function(a){var b,c;var e=this.object.cache(a,Fa);var f=e[0];var g=e[1];var h=this.negated?[" !\x3d\x3d "," \x26\x26 "]:[" \x3d\x3d\x3d "," || "];e=h[0];h=h[1];var p=[];var k=this.array.base.objects;var l=b=0;for(c=k.length;b<c;l=++b){var m=k[l];l&&p.push(this.makeCode(h));p=p.concat(l?g:f,this.makeCode(e),m.compileToFragments(a,Ga))}return a.level<Fa?p:this.wrapInBraces(p)};b.prototype.compileLoopTest=function(a){var b=this.object.cache(a,ta);var c=b[0];var e=b[1];b=[].concat(this.makeCode(Ia("indexOf",
-a)+".call("),this.array.compileToFragments(a,ta),this.makeCode(", "),e,this.makeCode(") "+(this.negated?"\x3c 0":"\x3e\x3d 0")));if(da(c)===da(e))return b;b=c.concat(this.makeCode(", "),b);return a.level<ta?b:this.wrapInBraces(b)};b.prototype.toString=function(a){return b.__super__.toString.call(this,a,this.constructor.name+(this.negated?"!":""))};return b}(sa);f.Try=function(a){function b(a,b,c,e){this.attempt=a;this.errorVariable=b;this.recovery=c;this.ensure=e}v(b,a);b.prototype.children=["attempt",
-"recovery","ensure"];b.prototype.isStatement=ha;b.prototype.jumps=function(a){var b;return this.attempt.jumps(a)||(null!=(b=this.recovery)?b.jumps(a):void 0)};b.prototype.makeReturn=function(a){this.attempt&&(this.attempt=this.attempt.makeReturn(a));this.recovery&&(this.recovery=this.recovery.makeReturn(a));return this};b.prototype.compileNode=function(a){var b,c,e;a.indent+=Ca;var f=this.attempt.compileToFragments(a,N);var g=this.recovery?(b=a.scope.freeVariable("error",{reserve:!1}),e=new x(b),
-this.errorVariable?(c=za(this.errorVariable.unwrapAll().value),c?this.errorVariable.error(c):void 0,this.recovery.unshift(new y(this.errorVariable,e))):void 0,[].concat(this.makeCode(" catch ("),e.compileToFragments(a),this.makeCode(") {\n"),this.recovery.compileToFragments(a,N),this.makeCode("\n"+this.tab+"}"))):this.ensure||this.recovery?[]:(b=a.scope.freeVariable("error",{reserve:!1}),[this.makeCode(" catch ("+b+") {}")]);a=this.ensure?[].concat(this.makeCode(" finally {\n"),this.ensure.compileToFragments(a,
-N),this.makeCode("\n"+this.tab+"}")):[];return[].concat(this.makeCode(this.tab+"try {\n"),f,this.makeCode("\n"+this.tab+"}"),g,a)};return b}(sa);f.Throw=ba=function(a){function b(a){this.expression=a}v(b,a);b.prototype.children=["expression"];b.prototype.isStatement=ha;b.prototype.jumps=ka;b.prototype.makeReturn=na;b.prototype.compileNode=function(a){return[].concat(this.makeCode(this.tab+"throw "),this.expression.compileToFragments(a),this.makeCode(";"))};return b}(sa);f.Existence=B=function(a){function b(a){this.expression=
-a}v(b,a);b.prototype.children=["expression"];b.prototype.invert=ra;b.prototype.compileNode=function(a){this.expression.front=this.front;var b=this.expression.compile(a,Fa);if(this.expression.unwrap()instanceof x&&!a.scope.check(b)){var c=this.negated?["\x3d\x3d\x3d","||"]:["!\x3d\x3d","\x26\x26"];var e=c[0];c=c[1];b="typeof "+b+" "+e+' "undefined" '+c+" "+b+" "+e+" null"}else b=b+" "+(this.negated?"\x3d\x3d":"!\x3d")+" null";return[this.makeCode(a.level<=Na?b:"("+b+")")]};return b}(sa);f.Parens=P=
-function(a){function b(a){this.body=a}v(b,a);b.prototype.children=["body"];b.prototype.unwrap=function(){return this.body};b.prototype.isComplex=function(){return this.body.isComplex()};b.prototype.compileNode=function(a){var b=this.body.unwrap();if(b instanceof C&&b.isAtomic())return b.front=this.front,b.compileToFragments(a);var c=b.compileToFragments(a,Ka);return a.level<Fa&&(b instanceof k||b instanceof ya||b instanceof Q&&b.returns)&&(a.level<Na||3>=c.length)?c:this.wrapInBraces(c)};return b}(sa);
-f.StringWithInterpolations=A=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);b.prototype.compileNode=function(a){var d;if(!a.inTaggedTemplateCall)return b.__super__.compileNode.apply(this,arguments);var c=this.body.unwrap();var e=[];c.traverseChildren(!1,function(a){if(a instanceof D)e.push(a);else if(a instanceof P)return e.push(a),!1;return!0});c=[];c.push(this.makeCode("`"));var f=0;for(d=e.length;f<d;f++){var g=e[f];g instanceof D?(g=g.value.slice(1,-1),g=
-g.replace(/(\\*)(`|\$\{)/g,function(a,b,d){return 0===b.length%2?b+"\\"+d:a}),c.push(this.makeCode(g))):(c.push(this.makeCode("${")),c.push.apply(c,g.compileToFragments(a,Ka)),c.push(this.makeCode("}")))}c.push(this.makeCode("`"));return c};return b}(P);f.For=Q=function(b){function c(b,d){this.source=d.source;this.guard=d.guard;this.step=d.step;this.name=d.name;this.index=d.index;this.body=a.wrap([b]);this.own=!!d.own;this.object=!!d.object;(this.from=!!d.from)&&this.index&&this.index.error("cannot use index with for-from");
-this.own&&!this.object&&d.ownTag.error("cannot use own with for-"+(this.from?"from":"in"));this.object&&(b=[this.index,this.name],this.name=b[0],this.index=b[1]);this.index instanceof C&&!this.index.isAssignable()&&this.index.error("index cannot be a pattern matching expression");this.range=this.source instanceof C&&this.source.base instanceof V&&!this.source.properties.length&&!this.from;this.pattern=this.name instanceof C;this.range&&this.index&&this.index.error("indexes do not apply to range loops");
-this.range&&this.pattern&&this.name.error("cannot pattern match over range loops");this.returns=!1}v(c,b);c.prototype.children=["body","source","guard","step"];c.prototype.compileNode=function(b){var d,c,e,f,g,h,k;var l=a.wrap([this.body]);var p=l.expressions;p=p[p.length-1];(null!=p?p.jumps():void 0)instanceof G&&(this.returns=!1);var m=this.range?this.source.base:this.source;var n=b.scope;this.pattern||(e=this.name&&this.name.compile(b,ta));p=this.index&&this.index.compile(b,ta);e&&!this.pattern&&
-n.find(e);!p||this.index instanceof C||n.find(p);this.returns&&(c=n.freeVariable("results"));this.from?this.pattern&&(f=n.freeVariable("x",{single:!0})):f=this.object&&p||n.freeVariable("i",{single:!0});var q=(this.range||this.from)&&e||p||f;var t=q!==f?q+" \x3d ":"";if(this.step&&!this.range){p=this.cacheToCodeFragments(this.step.cache(b,ta,Ya));var w=p[0];var r=p[1];this.step.isNumber()&&(h=Number(r))}this.pattern&&(e=f);var v=p=k="";var u=this.tab+Ca;if(this.range)var K=m.compileToFragments(ja(b,
-{index:f,name:e,step:this.step,isComplex:Ya}));else{var A=this.source.compile(b,ta);!e&&!this.own||this.source.unwrap()instanceof x||(v+=""+this.tab+(m=n.freeVariable("ref"))+" \x3d "+A+";\n",A=m);!e||this.pattern||this.from||(g=e+" \x3d "+A+"["+q+"]");this.object||this.from||(w!==r&&(v+=""+this.tab+w+";\n"),e=0>h,this.step&&null!=h&&e||(d=n.freeVariable("len")),K=""+t+f+" \x3d 0, "+d+" \x3d "+A+".length",w=""+t+f+" \x3d "+A+".length - 1",d=f+" \x3c "+d,n=f+" \x3e\x3d 0",this.step?(null!=h?e&&(d=
-n,K=w):(d=r+" \x3e 0 ? "+d+" : "+n,K="("+r+" \x3e 0 ? ("+K+") : "+w+")"),f=f+" +\x3d "+r):f=""+(q!==f?"++"+f:f+"++"),K=[this.makeCode(K+"; "+d+"; "+t+f)])}if(this.returns){var B=""+this.tab+c+" \x3d [];\n";var V="\n"+this.tab+"return "+c+";";l.makeReturn(c)}this.guard&&(1<l.expressions.length?l.expressions.unshift(new J((new P(this.guard)).invert(),new W("continue"))):this.guard&&(l=a.wrap([new J(this.guard,l)])));this.pattern&&l.expressions.unshift(new y(this.name,this.from?new x(q):new z(A+"["+
-q+"]")));c=[].concat(this.makeCode(v),this.pluckDirectCall(b,l));g&&(k="\n"+u+g+";");this.object?(K=[this.makeCode(q+" in "+A)],this.own&&(p="\n"+u+"if (!"+Ia("hasProp",b)+".call("+A+", "+q+")) continue;")):this.from&&(K=[this.makeCode(q+" of "+A)]);(b=l.compileToFragments(ja(b,{indent:u}),N))&&0<b.length&&(b=[].concat(this.makeCode("\n"),b,this.makeCode("\n")));return[].concat(c,this.makeCode(""+(B||"")+this.tab+"for ("),K,this.makeCode(") {"+p+k),b,this.makeCode(this.tab+"}"+(V||"")))};c.prototype.pluckDirectCall=
-function(a,b){var d,c,f,g,k,l,p;var m=[];var n=b.expressions;var q=d=0;for(c=n.length;d<c;q=++d){var w=n[q];w=w.unwrapAll();if(w instanceof ya){var t=null!=(f=w.variable)?f.unwrapAll():void 0;if(t instanceof h||t instanceof C&&(null!=(g=t.base)?g.unwrapAll():void 0)instanceof h&&1===t.properties.length&&("call"===(k=null!=(l=t.properties[0].name)?l.value:void 0)||"apply"===k)){var r=(null!=(p=t.base)?p.unwrapAll():void 0)||t;var v=new x(a.scope.freeVariable("fn"));var u=new C(v);t.base&&(u=[u,t],
-t.base=u[0],u=u[1]);b.expressions[q]=new ya(u,w.args);m=m.concat(this.makeCode(this.tab),(new y(v,r)).compileToFragments(a,N),this.makeCode(";\n"))}}}return m};return c}(Z);f.Switch=function(b){function c(a,b,c){this.subject=a;this.cases=b;this.otherwise=c}v(c,b);c.prototype.children=["subject","cases","otherwise"];c.prototype.isStatement=ha;c.prototype.jumps=function(a){var b,c;null==a&&(a={block:!0});var e=this.cases;var f=0;for(b=e.length;f<b;f++){var g=e[f];g=g[1];if(g=g.jumps(a))return g}return null!=
-(c=this.otherwise)?c.jumps(a):void 0};c.prototype.makeReturn=function(b){var c,f;var e=this.cases;var g=0;for(c=e.length;g<c;g++){var h=e[g];h[1].makeReturn(b)}b&&(this.otherwise||(this.otherwise=new a([new z("void 0")])));null!=(f=this.otherwise)&&f.makeReturn(b);return this};c.prototype.compileNode=function(a){var b,c,e,f;var g=a.indent+Ca;var h=a.indent=g+Ca;var k=[].concat(this.makeCode(this.tab+"switch ("),this.subject?this.subject.compileToFragments(a,Ka):this.makeCode("false"),this.makeCode(") {\n"));
-var l=this.cases;var m=c=0;for(e=l.length;c<e;m=++c){var p=l[m];var n=p[0];p=p[1];var q=ia([n]);n=0;for(f=q.length;n<f;n++){var w=q[n];this.subject||(w=w.invert());k=k.concat(this.makeCode(g+"case "),w.compileToFragments(a,Ka),this.makeCode(":\n"))}0<(b=p.compileToFragments(a,N)).length&&(k=k.concat(b,this.makeCode("\n")));if(m===this.cases.length-1&&!this.otherwise)break;m=this.lastNonComment(p.expressions);m instanceof G||m instanceof z&&m.jumps()&&"debugger"!==m.value||k.push(w.makeCode(h+"break;\n"))}this.otherwise&&
-this.otherwise.expressions.length&&k.push.apply(k,[this.makeCode(g+"default:\n")].concat(M.call(this.otherwise.compileToFragments(a,N)),[this.makeCode("\n")]));k.push(this.makeCode(this.tab+"}"));return k};return c}(sa);f.If=J=function(b){function c(a,b,c){this.body=b;null==c&&(c={});this.condition="unless"===c.type?a.invert():a;this.elseBody=null;this.isChain=!1;this.soak=c.soak}v(c,b);c.prototype.children=["condition","body","elseBody"];c.prototype.bodyNode=function(){var a;return null!=(a=this.body)?
-a.unwrap():void 0};c.prototype.elseBodyNode=function(){var a;return null!=(a=this.elseBody)?a.unwrap():void 0};c.prototype.addElse=function(a){this.isChain?this.elseBodyNode().addElse(a):(this.isChain=a instanceof c,this.elseBody=this.ensureBlock(a),this.elseBody.updateLocationDataIfMissing(a.locationData));return this};c.prototype.isStatement=function(a){var b;return(null!=a?a.level:void 0)===N||this.bodyNode().isStatement(a)||(null!=(b=this.elseBodyNode())?b.isStatement(a):void 0)};c.prototype.jumps=
-function(a){var b;return this.body.jumps(a)||(null!=(b=this.elseBody)?b.jumps(a):void 0)};c.prototype.compileNode=function(a){return this.isStatement(a)?this.compileStatement(a):this.compileExpression(a)};c.prototype.makeReturn=function(b){b&&(this.elseBody||(this.elseBody=new a([new z("void 0")])));this.body&&(this.body=new a([this.body.makeReturn(b)]));this.elseBody&&(this.elseBody=new a([this.elseBody.makeReturn(b)]));return this};c.prototype.ensureBlock=function(b){return b instanceof a?b:new a([b])};
-c.prototype.compileStatement=function(a){var b=la(a,"chainChild");if(la(a,"isExistentialEquals"))return(new c(this.condition.invert(),this.elseBodyNode(),{type:"if"})).compileToFragments(a);var f=a.indent+Ca;var e=this.condition.compileToFragments(a,Ka);var g=this.ensureBlock(this.body).compileToFragments(ja(a,{indent:f}));g=[].concat(this.makeCode("if ("),e,this.makeCode(") {\n"),g,this.makeCode("\n"+this.tab+"}"));b||g.unshift(this.makeCode(this.tab));if(!this.elseBody)return g;b=g.concat(this.makeCode(" else "));
-this.isChain?(a.chainChild=!0,b=b.concat(this.elseBody.unwrap().compileToFragments(a,N))):b=b.concat(this.makeCode("{\n"),this.elseBody.compileToFragments(ja(a,{indent:f}),N),this.makeCode("\n"+this.tab+"}"));return b};c.prototype.compileExpression=function(a){var b=this.condition.compileToFragments(a,Na);var c=this.bodyNode().compileToFragments(a,ta);var e=this.elseBodyNode()?this.elseBodyNode().compileToFragments(a,ta):[this.makeCode("void 0")];e=b.concat(this.makeCode(" ? "),c,this.makeCode(" : "),
-e);return a.level>=Na?this.wrapInBraces(e):e};c.prototype.unfoldSoak=function(){return this.soak&&this};return c}(sa);var gc={extend:function(a){return"function(child, parent) { for (var key in parent) { if ("+Ia("hasProp",a)+".call(parent, key)) child[key] \x3d parent[key]; } function ctor() { this.constructor \x3d child; } ctor.prototype \x3d parent.prototype; child.prototype \x3d new ctor(); child.__super__ \x3d parent.prototype; return child; }"},bind:function(){return"function(fn, me){ return function(){ return fn.apply(me, arguments); }; }"},
-indexOf:function(){return"[].indexOf || function(item) { for (var i \x3d 0, l \x3d this.length; i \x3c l; i++) { if (i in this \x26\x26 this[i] \x3d\x3d\x3d item) return i; } return -1; }"},modulo:function(){return"function(a, b) { return (+a % (b \x3d +b) + b) % b; }"},hasProp:function(){return"{}.hasOwnProperty"},slice:function(){return"[].slice"}};var N=1;var Ka=2;var ta=3;var Na=4;var Fa=5;var Ga=6;var Ca="  ";var Pa=/^[+-]?\d+$/;var Ia=function(a,b){var c=b.scope.root;if(a in c.utilities)return c.utilities[a];
-var d=c.freeVariable(a);c.assign(d,gc[a](b));return c.utilities[a]=d};var Ea=function(a,b){a=a.replace(/\n/g,"$\x26"+b);return a.replace(/\s+$/,"")};var Va=function(a){return a instanceof x&&"arguments"===a.value};var ea=function(a){return a instanceof E||a instanceof h&&a.bound||a instanceof va};var Ya=function(a){return a.isComplex()||("function"===typeof a.isAssignable?a.isAssignable():void 0)};var Ba=function(a,b,c){if(a=b[c].unfoldSoak(a))return b[c]=a.body,a.body=new C(b),a}}).call(this);return f}();
-u["./sourcemap"]=function(){var f={};(function(){var u=function(){function f(f){this.line=f;this.columns=[]}f.prototype.add=function(f,a,b){var q=a[0];a=a[1];null==b&&(b={});if(!this.columns[f]||!b.noReplace)return this.columns[f]={line:this.line,column:f,sourceLine:q,sourceColumn:a}};f.prototype.sourceLocation=function(f){for(var a;!((a=this.columns[f])||0>=f);)f--;return a&&[a.sourceLine,a.sourceColumn]};return f}();f=function(){function f(){this.lines=[]}f.prototype.add=function(f,a,b){var q;null==
-b&&(b={});var g=a[0];a=a[1];return((q=this.lines)[g]||(q[g]=new u(g))).add(a,f,b)};f.prototype.sourceLocation=function(f){var a;var b=f[0];for(f=f[1];!((a=this.lines[b])||0>=b);)b--;return a&&a.sourceLocation(f)};f.prototype.generate=function(f,a){var b,q,g,h,r,n,u;null==f&&(f={});null==a&&(a=null);var y=g=q=u=0;var I=!1;var F="";var Q=this.lines;var x=b=0;for(h=Q.length;b<h;x=++b)if(x=Q[x]){var J=x.columns;x=0;for(r=J.length;x<r;x++)if(n=J[x]){for(;u<n.line;)q=0,I=!1,F+=";",u++;I&&(F+=",");F+=this.encodeVlq(n.column-
-q);q=n.column;F+=this.encodeVlq(0);F+=this.encodeVlq(n.sourceLine-g);g=n.sourceLine;F+=this.encodeVlq(n.sourceColumn-y);y=n.sourceColumn;I=!0}}F={version:3,file:f.generatedFile||"",sourceRoot:f.sourceRoot||"",sources:f.sourceFiles||[""],names:[],mappings:F};f.inlineMap&&(F.sourcesContent=[a]);return F};f.prototype.encodeVlq=function(f){var a;var b="";for(a=(Math.abs(f)<<1)+(0>f?1:0);a||!b;)f=a&31,(a>>=5)&&(f|=32),b+=this.encodeBase64(f);return b};f.prototype.encodeBase64=function(f){var a;if(!(a=
-"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[f]))throw Error("Cannot Base64 encode value: "+f);return a};return f}()}).call(this);return f}();u["./coffee-script"]=function(){var f={};(function(){var qa,q,y={}.hasOwnProperty;var a=u("fs");var b=u("vm");var ya=u("path");var g=u("./lexer").Lexer;var h=u("./parser").parser;var r=u("./helpers");var n=u("./sourcemap");var B=u("../../package.json");f.VERSION=B.version;f.FILE_EXTENSIONS=[".coffee",".litcoffee",".coffee.md"];f.helpers=
-r;var H=function(a){switch(!1){case "function"!==typeof Buffer:return(new Buffer(a)).toString("base64");case "function"!==typeof btoa:return btoa(encodeURIComponent(a).replace(/%([0-9A-F]{2})/g,function(a,b){return String.fromCharCode("0x"+b)}));default:throw Error("Unable to base64 encode inline sourcemap.");}};B=function(a){return function(b,f){null==f&&(f={});try{return a.call(this,b,f)}catch(m){if("string"!==typeof b)throw m;throw r.updateSyntaxError(m,b,f.filename);}}};var I={};var F={};f.compile=
-qa=B(function(a,b){var c,f,g,l;var q=r.extend;b=q({},b);var u=b.sourceMap||b.inlineMap||null==b.filename;q=b.filename||"\x3canonymous\x3e";I[q]=a;u&&(g=new n);var x=O.tokenize(a,b);var y=b;var G=[];var z=0;for(c=x.length;z<c;z++){var B=x[z];"IDENTIFIER"===B[0]&&G.push(B[1])}y.referencedVars=G;if(null==b.bare||!0!==b.bare)for(y=0,z=x.length;y<z;y++)if(B=x[y],"IMPORT"===(f=B[0])||"EXPORT"===f){b.bare=!0;break}z=h.parse(x).compileToFragments(b);x=0;b.header&&(x+=1);b.shiftLine&&(x+=1);B=0;f="";c=0;for(G=
-z.length;c<G;c++){y=z[c];if(u){y.locationData&&!/^[;\s]*$/.test(y.code)&&g.add([y.locationData.first_line,y.locationData.first_column],[x,B],{noReplace:!0});var J=r.count(y.code,"\n");x+=J;B=J?y.code.length-(y.code.lastIndexOf("\n")+1):B+y.code.length}f+=y.code}b.header&&(B="Generated by CoffeeScript "+this.VERSION,f="// "+B+"\n"+f);if(u){var D=g.generate(b,a);F[q]=g}b.inlineMap&&(a=H(JSON.stringify(D)),q="//# sourceURL\x3d"+(null!=(l=b.filename)?l:"coffeescript"),f=f+"\n"+("//# sourceMappingURL\x3ddata:application/json;base64,"+
-a)+"\n"+q);return b.sourceMap?{js:f,sourceMap:g,v3SourceMap:JSON.stringify(D,null,2)}:f});f.tokens=B(function(a,b){return O.tokenize(a,b)});f.nodes=B(function(a,b){return"string"===typeof a?h.parse(O.tokenize(a,b)):h.parse(a)});f.run=function(b,c){var f;null==c&&(c={});var g=u.main;g.filename=process.argv[1]=c.filename?a.realpathSync(c.filename):"\x3canonymous\x3e";g.moduleCache&&(g.moduleCache={});var h=null!=c.filename?ya.dirname(a.realpathSync(c.filename)):a.realpathSync(".");g.paths=u("module")._nodeModulePaths(h);
-if(!r.isCoffee(g.filename)||u.extensions)b=qa(b,c),b=null!=(f=b.js)?f:b;return g._compile(b,g.filename)};f.eval=function(a,c){var f,g,h,l,n;null==c&&(c={});if(a=a.trim()){var q=null!=(h=b.Script.createContext)?h:b.createContext;h=null!=(g=b.isContext)?g:function(a){return c.sandbox instanceof q().constructor};if(q){if(null!=c.sandbox){if(h(c.sandbox))var r=c.sandbox;else for(l in r=q(),h=c.sandbox,h)y.call(h,l)&&(g=h[l],r[l]=g);r.global=r.root=r.GLOBAL=r}else r=global;r.__filename=c.filename||"eval";
-r.__dirname=ya.dirname(r.__filename);if(r===global&&!r.module&&!r.require){var x=u("module");r.module=f=new x(c.modulename||"eval");r.require=g=function(a){return x._load(a,f,!0)};f.filename=r.__filename;var B=Object.getOwnPropertyNames(u);h=0;for(n=B.length;h<n;h++){var z=B[h];"paths"!==z&&"arguments"!==z&&"caller"!==z&&(g[z]=u[z])}g.paths=f.paths=x._nodeModulePaths(process.cwd());g.resolve=function(a){return x._resolveFilename(a,f)}}}h={};for(l in c)y.call(c,l)&&(g=c[l],h[l]=g);h.bare=!0;a=qa(a,
-h);return r===global?b.runInThisContext(a):b.runInContext(a,r)}};f.register=function(){return u("./register")};if(u.extensions){var Q=this.FILE_EXTENSIONS;var x=function(a){var b;return null!=(b=u.extensions)[a]?b[a]:b[a]=function(){throw Error("Use CoffeeScript.register() or require the coffee-script/register module to require "+a+" files.");}};var J=0;for(q=Q.length;J<q;J++)B=Q[J],x(B)}f._compileFile=function(b,c,f){null==c&&(c=!1);null==f&&(f=!1);var g=a.readFileSync(b,"utf8");g=65279===g.charCodeAt(0)?
-g.substring(1):g;try{var h=qa(g,{filename:b,sourceMap:c,inlineMap:f,sourceFiles:[b],literate:r.isLiterate(b)})}catch(K){throw r.updateSyntaxError(K,g,b);}return h};var O=new g;h.lexer={lex:function(){var a;if(a=h.tokens[this.pos++]){var b=a[0];this.yytext=a[1];this.yylloc=a[2];h.errorToken=a.origin||a;this.yylineno=this.yylloc.first_line}else b="";return b},setInput:function(a){h.tokens=a;return this.pos=0},upcomingInput:function(){return""}};h.yy=u("./nodes");h.yy.parseError=function(a,b){var c=
-h.errorToken;var f=h.tokens;var g=c[0];var l=c[1];a=c[2];l=function(){switch(!1){case c!==f[f.length-1]:return"end of input";case "INDENT"!==g&&"OUTDENT"!==g:return"indentation";case "IDENTIFIER"!==g&&"NUMBER"!==g&&"INFINITY"!==g&&"STRING"!==g&&"STRING_START"!==g&&"REGEX"!==g&&"REGEX_START"!==g:return g.replace(/_START$/,"").toLowerCase();default:return r.nameWhitespaceCharacter(l)}}();return r.throwSyntaxError("unexpected "+l,a)};var R=function(a,b){var c;if(a.isNative())var f="native";else{a.isEval()?
-(c=a.getScriptNameOrSourceURL())||a.getEvalOrigin():c=a.getFileName();c||(c="\x3canonymous\x3e");var g=a.getLineNumber();f=a.getColumnNumber();f=(b=b(c,g,f))?c+":"+b[0]+":"+b[1]:c+":"+g+":"+f}c=a.getFunctionName();g=a.isConstructor();if(a.isToplevel()||g)return g?"new "+(c||"\x3canonymous\x3e")+" ("+f+")":c?c+" ("+f+")":f;g=a.getMethodName();var h=a.getTypeName();return c?(b=a="",h&&c.indexOf(h)&&(b=h+"."),g&&c.indexOf("."+g)!==c.length-g.length-1&&(a=" [as "+g+"]"),""+b+c+a+" ("+f+")"):h+"."+(g||
-"\x3canonymous\x3e")+" ("+f+")"};var z=function(a){return null!=F[a]?F[a]:null!=F["\x3canonymous\x3e"]?F["\x3canonymous\x3e"]:null!=I[a]?(a=qa(I[a],{filename:a,sourceMap:!0,literate:r.isLiterate(a)}),a.sourceMap):null};Error.prepareStackTrace=function(a,b){var c;var g=function(a,b,c){var f;a=z(a);null!=a&&(f=a.sourceLocation([b-1,c-1]));return null!=f?[f[0]+1,f[1]+1]:null};var h=function(){var a;var h=[];var k=0;for(a=b.length;k<a;k++){c=b[k];if(c.getFunction()===f.run)break;h.push("    at "+R(c,
-g))}return h}();return a.toString()+"\n"+h.join("\n")+"\n"}}).call(this);return f}();u["./browser"]=function(){(function(){var f=[].indexOf||function(a){for(var b=0,f=this.length;b<f;b++)if(b in this&&this[b]===a)return b;return-1};var qa=u("./coffee-script");qa.require=u;var q=qa.compile;qa.eval=function(a,b){null==b&&(b={});null==b.bare&&(b.bare=!0);return eval(q(a,b))};qa.run=function(a,b){null==b&&(b={});b.bare=!0;b.shiftLine=!0;return Function(q(a,b))()};if("undefined"!==typeof window&&null!==
-window){"undefined"!==typeof btoa&&null!==btoa&&"undefined"!==typeof JSON&&null!==JSON&&(q=function(a,b){null==b&&(b={});b.inlineMap=!0;return qa.compile(a,b)});qa.load=function(a,b,f,g){null==f&&(f={});null==g&&(g=!1);f.sourceFiles=[a];var h=window.ActiveXObject?new window.ActiveXObject("Microsoft.XMLHTTP"):new window.XMLHttpRequest;h.open("GET",a,!0);"overrideMimeType"in h&&h.overrideMimeType("text/plain");h.onreadystatechange=function(){var q;if(4===h.readyState){if(0===(q=h.status)||200===q)q=
-[h.responseText,f],g||qa.run.apply(qa,q);else throw Error("Could not load "+a);if(b)return b(q)}};return h.send(null)};var y=function(){var a,b,q;var g=window.document.getElementsByTagName("script");var h=["text/coffeescript","text/literate-coffeescript"];var r=function(){var a,b;var n=[];var r=0;for(a=g.length;r<a;r++)q=g[r],(b=q.type,0<=f.call(h,b))&&n.push(q);return n}();var n=0;var u=function(){var a=r[n];if(a instanceof Array)return qa.run.apply(qa,a),n++,u()};var y=function(a,b){var f;var g=
-{literate:a.type===h[1]};if(f=a.src||a.getAttribute("data-src"))return qa.load(f,function(a){r[b]=a;return u()},g,!0);g.sourceFiles=["embedded"];return r[b]=[a.innerHTML,g]};var I=a=0;for(b=r.length;a<b;I=++a){var F=r[I];y(F,I)}return u()};window.addEventListener?window.addEventListener("DOMContentLoaded",y,!1):window.attachEvent("onload",y)}}).call(this);return{}}();return u["./coffee-script"]}();"function"===typeof define&&define.amd?define(function(){return xa}):u.CoffeeScript=xa})(this);
\ No newline at end of file
diff --git a/tools/coffee/coffee.cmd b/tools/coffee/coffee.cmd
deleted file mode 100644
index d912965c..00000000
--- a/tools/coffee/coffee.cmd
+++ /dev/null
@@ -1,2 +0,0 @@
-::For convenience
-@cscript //nologo "%~dp0coffee.wsf" %*
diff --git a/tools/coffee/coffee.wsf b/tools/coffee/coffee.wsf
deleted file mode 100644
index 25d590c9..00000000
--- a/tools/coffee/coffee.wsf
+++ /dev/null
@@ -1,109 +0,0 @@
-<job>
-<!-- https://github.com/jashkenas/coffee-script/raw/master/extras/coffee-script.js -->
-<script src="coffee-script.js" language="JScript" />
-<script language="JScript">
-(function() {
-
-    var args = [];
-    for (var i = 0; i < WScript.Arguments.Length; i++) {
-        args.push(WScript.Arguments.Item(i));
-    }
-
-    // FileSystemObject: http://msdn.microsoft.com/en-us/library/bkx696eh.aspx
-    var fso = new ActiveXObject("Scripting.FileSystemObject");
-
-    var isfolder = (args[0] && fso.folderExists(args[0]));
-
-    if (isfolder) {
-        f = fso.getFolder(args[0]);
-        e = new Enumerator(f.files);
-        for (; !e.atEnd(); e.moveNext()) {
-            if (e.item().path.toLowerCase().lastIndexOf('.coffee') != -1) {
-                convert(e.item(), args[1]);
-            }
-        }
-    }
-    else {
-        convert(args[0], args[1])
-    }
-
-})();
-
-
-function convert(input, output) {
-
-    var fso = new ActiveXObject("Scripting.FileSystemObject");
-
-    if (output) {
-        // if output specifies a folder name, output filename is same as input filename with .coffee extension
-        if (fso.folderExists(output)) {
-            output = output + '\\' + fso.getFile(input).name.replace('\.coffee', '.js')
-        }
-    }
-
-    var coffee;
-    if (!input) {
-        // Read all input data from STDIN
-        var chunks = [];
-        while (!WScript.StdIn.AtEndOfStream)
-            chunks.push(WScript.StdIn.ReadAll());
-        coffee = chunks.join('');
-    }
-    else {
-        coffee = readUtf8(input);
-    }
-
-    try {
-        if(!Object.create)
-            Object.create = function(proto)
-            {
-                function f(){}
-                f.prototype = proto;
-                return new f;
-            }
-
-        var js = CoffeeScript.compile(coffee, {filename: "temp.coffee"});
-        if (!output) {
-            WScript.StdOut.Write(js);
-        }
-        else {
-            writeUtf8(output, js);
-        }
-    }
-    catch (err) {
-        WScript.StdErr.WriteLine(err.message);
-        WScript.Quit(1);
-    }
-}
-
-function readUtf8(filename) {
-    var stream = new ActiveXObject("ADODB.Stream");
-    stream.Open();
-    stream.Type = 2; // Text
-    stream.Charset = 'utf-8';
-    stream.LoadFromFile(filename);
-    var text = stream.ReadText();
-    stream.Close();
-    return text;
-}
-
-function writeUtf8(filename, text) {
-    var stream = new ActiveXObject("ADODB.Stream");
-    stream.Type = 2; // Text
-    stream.Charset = "utf-8";
-    stream.Open();
-    stream.WriteText(text);
-
-    stream.Position = 0;
-    stream.Type = 1; // Binary
-    stream.Position = 3;
-    var binary = stream.Read();
-    stream.Close();
-
-    stream.Open();
-    stream.Write(binary);
-    stream.SaveToFile(filename, 2);
-    stream.Close();
-}
-</script>
-</job>
\ No newline at end of file

From 3e4f61e1a8cc089a4021e11ce1f4299539762316 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 12 May 2022 20:43:49 +0400
Subject: [PATCH 052/333] fix compatibility with modern hashlib

ripemd160 has been removed from hashlib, we have to mend it with
other implementation for now
---
 src/lib/libsecp256k1message/libsecp256k1message.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/lib/libsecp256k1message/libsecp256k1message.py b/src/lib/libsecp256k1message/libsecp256k1message.py
index 59768b88..5cc97ecf 100644
--- a/src/lib/libsecp256k1message/libsecp256k1message.py
+++ b/src/lib/libsecp256k1message/libsecp256k1message.py
@@ -4,6 +4,7 @@ from coincurve import PrivateKey, PublicKey
 from base58 import b58encode_check, b58decode_check
 from hmac import compare_digest
 from util.Electrum import format as zero_format
+from ..sslcrypto._ecc import ripemd160
 
 RECID_MIN = 0
 RECID_MAX = 3
@@ -41,7 +42,7 @@ def compute_secret_address(secretkey):
 def public_digest(publickey, compressed=False):
     """Convert a public key to ripemd160(sha256()) digest."""
     publickey_hex = publickey.format(compressed=compressed)
-    return hashlib.new('ripemd160', hashlib.sha256(publickey_hex).digest()).digest()
+    return ripemd160(hashlib.sha256(publickey_hex).digest()).digest()
 
 def address_public_digest(address):
     """Convert a public Bitcoin address to ripemd160(sha256()) digest."""

From 788ef5ca5fae2163e84c680e2d5b40d05bb71abe Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 13 May 2022 03:15:43 +0400
Subject: [PATCH 053/333] update README

- tor termux instructions
- limitations
- how to help
---
 README.md | 23 +++++++++++++++++++----
 1 file changed, 19 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 4fa19ac5..f3bdee4c 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,8 @@ Install autoconf and other basic development tools, python3 and pip.
  - in Termux install via `pkg install <package-names>`
  - `pkg update`
  - `pkg install python automake autoconf-dev git` (TODO: check fresh installation whether there are more dependencies to install)
+ - (optional) `pkg install tor`
+ - (optional) run tor via `tor --ControlPort 9051 --CookieAuthentication 1` command (you can then open new session by swiping to the right)
 
 #### Building python dependencies & running
  - clone this repo (NOTE: on Android/Termux you should clone it into "home" folder of Termux, because virtual environment cannot live in `storage/`)
@@ -91,14 +93,17 @@ Install autoconf and other basic development tools, python3 and pip.
 
 #### alternative script
  - after installing general dependencies and cloning repo (as above), run `start-venv.sh` which will create a virtual env for you and install python requirements
+ - more convenience scripts to be added soon
 
 ## Current limitations
 
 * File transactions are not compressed
 * No private sites
 * No DHT support
-* Centralized elements like zeroid
-* No reliable spam protection
+* Centralized elements like zeroid (we're working on this!)
+* No reliable spam protection (and on this too)
+* Doesn't work directly from browser (one of the top priorities for mid-future)
+* No data transparency
 
 
 ## How can I create a ZeroNet site?
@@ -114,8 +119,18 @@ Next steps: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_devel
 
 ### Become a maintainer
 
-We need more maintainers! Become one today! Seriously, there's not going to be
-that much new code to audit and auditing new code is the only requirement.
+We need more maintainers! Become one today! You don't need to know how to code,
+there's a lot of other work to do.
+
+### Fix bugs & add features
+
+We've decided to go ahead and make a perfect p2p web, so we need more help
+implementing it.
+
+### Make your site/bring your content
+
+We know the documentation is lacking, but we try our best to support anyone
+who wants to migrate. Don't hesitate to ask.
 
 ### Use it and spread the word
 

From 35fd399e01cf704999ee6510bb71592ad3b42ba6 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 27 Mar 2022 18:45:45 +0000
Subject: [PATCH 054/333] Don't check port in tor-only mode

refs https://github.com/HelloZeroNet/ZeroNet/issues/2456
---
 src/File/FileServer.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/File/FileServer.py b/src/File/FileServer.py
index 7f73017e..e5fd4edf 100644
--- a/src/File/FileServer.py
+++ b/src/File/FileServer.py
@@ -152,8 +152,9 @@ class FileServer(ConnectionServer):
         FileRequest = imp.load_source("FileRequest", "src/File/FileRequest.py").FileRequest
 
     def portCheck(self):
-        if config.offline:
-            self.log.info("Offline mode: port check disabled")
+        if config.offline or config.tor == 'always':
+            msg = "Offline mode" if config.offline else "Tor-only"
+            self.log.info(f'{msg}: port check disabled')
             res = {"ipv4": None, "ipv6": None}
             self.port_opened = res
             return res

From 629e5e8b92023cf174f1d4f4954a4161720600a4 Mon Sep 17 00:00:00 2001
From: d4708 <96319366+d4708@users.noreply.github.com>
Date: Sat, 14 May 2022 14:21:32 +0300
Subject: [PATCH 055/333] add pkg-config dependency #46

not preinstalled on some OS and causes error on 0net installation
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index f3bdee4c..b864501c 100644
--- a/README.md
+++ b/README.md
@@ -70,7 +70,7 @@ Install autoconf and other basic development tools, python3 and pip.
 
 ##### Apt-based (debian, ubuntu, etc)
  - `sudo apt update`
- - `sudo apt install python3-pip python3-venv`
+ - `sudo apt install pkg-config python3-pip python3-venv`
 
 ##### Android/Termux
  - install [Termux](https://termux.com/)

From d12ae0711d869545c2f1f0ae78967682602f85ba Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 14 May 2022 18:11:32 +0400
Subject: [PATCH 056/333] Update README.md

autoconf-dev doesn't exist on modern termux
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index b864501c..95657a17 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,7 @@ Install autoconf and other basic development tools, python3 and pip.
  - install [Termux](https://termux.com/)
  - in Termux install via `pkg install <package-names>`
  - `pkg update`
- - `pkg install python automake autoconf-dev git` (TODO: check fresh installation whether there are more dependencies to install)
+ - `pkg install python automake git` (TODO: check fresh installation whether there are more dependencies to install)
  - (optional) `pkg install tor`
  - (optional) run tor via `tor --ControlPort 9051 --CookieAuthentication 1` command (you can then open new session by swiping to the right)
 

From 1c24d90c707fd0e34ca9b06d6ec3c5390aa08e51 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 15 May 2022 14:43:12 +0400
Subject: [PATCH 057/333] new icon

refs #3
refs #50
---
 src/Ui/media/img/apple-touch-icon.png | Bin 8178 -> 0 bytes
 src/Ui/media/img/favicon.ico          | Bin 1150 -> 270398 bytes
 src/Ui/media/img/favicon.psd          | Bin 52520 -> 0 bytes
 src/Ui/media/img/logo-white.svg       |   1 -
 src/Ui/media/img/logo.png             | Bin 11379 -> 45820 bytes
 src/Ui/media/img/logo.psd             | Bin 62132 -> 0 bytes
 src/Ui/media/img/logo.svg             |   1 -
 7 files changed, 2 deletions(-)
 delete mode 100644 src/Ui/media/img/apple-touch-icon.png
 delete mode 100644 src/Ui/media/img/favicon.psd
 delete mode 100644 src/Ui/media/img/logo-white.svg
 delete mode 100644 src/Ui/media/img/logo.psd
 delete mode 100644 src/Ui/media/img/logo.svg

diff --git a/src/Ui/media/img/apple-touch-icon.png b/src/Ui/media/img/apple-touch-icon.png
deleted file mode 100644
index 962bd31ad5a55c8067094469026a1343b5624262..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 8178
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

diff --git a/src/Ui/media/img/favicon.ico b/src/Ui/media/img/favicon.ico
index 3f75912aeb9866bdc297c5050fa378bdfaf1e18c..240b9e4008fe687a218288fce9aa0dd583764b8c 100644
GIT binary patch
literal 270398
zcmeI*3EXwnya({Ox>0CQl12@hC4@}fBuSBoB#H`k4Jp!GhEmdiLdK9O<5gxVLL*I5
zRJ`3x8InvF$-d{i`}`m4+3!B*Im15ZIp;iUy`S@*;n~C5|Mma<=e5=nOWdvXPrKU^
zYb~+CTK8Du{;kg&-1Wl}OWggg-+f)8^|xv(a?2FR6v!0F6v!0F6v!0F6v!0F6v!0F
z6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F
z6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F
z6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6v!0F6vz~q
z5(?aM%PqyHKJ}^c_1*7&cS@crKSriNra(6o`07``THNh!cPn2z?X*)jr{rTY1u_Mu
zgaYv2+K;1rJ^krVpOUBQ?lJ!L&pV4>UvPbK%lSVle9kRXV4+c9A>#jEcmAum{kmTj
zmml=0;@l^lRGhobNyX&{o>lze+Fus_mRqL4LZQGy!vDYi^{?XpZn?eq@oDE3=WqYk
z;@l^mIP2xNAD?<|d5mLo%M_S)3M>@--+9MBi<>|5{o*^%efKQPS7m_jj``W=E-&u9
zqko^^&O7fc{_uxC6gS^|b8-9aw->$06UYAPPk$<Y_OqWAfBfSgdpfsCe!b(4JBnZb
z`q#z({_p=5x7~JI;hFkv|M<s0%4_}n=RYt0@|VBN+cWjsf9L)E?|(0T`O9Axzxc&3
z%FlybtI2a?ob>B&fBRccm!5X~-}#S!7QegXr^O{N`OuuWugZc;_dT<?^|D1RIq=Up
zX$rG&{*jM-q}XAH9g1a_UADOU-S1w${^LLXqj=DR9#p*I6|X2Rz4X!o7OKtpH{5VT
z@v@h_tXONUwF>9N`KzwFYO&vb`xQU<!4KME(0RM;vP-f1?z<O%{p(+=m!J85-g)Ph
z$2s;BpZLVg?>>+<pa1;li<iIr<;BAv{_tYyrI()RgTCDV{`W7Q`OIe)U;5IQ=HxT7
z;-;H!Dvmhfh~i-ndsy+G|M{OY*Ya%Ztg}w>yyrcy_}~XWSYoQ3Y&q_@<I4U%_qoq4
z=`*XtU4HrJKmS==cinZxTi)`PV(-28E_T~(x022u_`nBB+15!mJNH}P`c|>yjyo3j
zxzBxOo&jyP+itt!i(mX=Ii43@c;O6P&p6|ZnV!Ms>tFx+@^3GA!3)a!&HS<Q;mRwo
zoVmt3-tmsgU;q6Tt;l58eZ(UkF^jhMyyrcOM?LCM<+IW~ou17VKm6ej&&2<nH`nq1
z?#1|j`tzdS7C(6X*~NKJKDjOS&wuLMif``lwzhviZ@W{9YmWV5@t2!#Es72<!PQq^
zT|D~Hk1q2l5l^jqzt_F)RebZC-<(A}<N3)?ezL?@-8u2w^2;w@y#M|0uN@=u#c}u&
zj@vco$Mudp^2k|-z4yKEo%vfFQ}@2)l1mnQ?X_3i`{4Q0Pd~j_dF7S6y51UVtWjKh
z?X|P|5?|m>AKdejOD>6@yMEV~x4!kQ#d6CnSKF7BR$8gV%K!b}|1EY+<MPgTzOz_n
znPqCv)h}|5jQiNfJ~ngAt6uf0nQM{t+ibH<`8V<dpR{YKE`H$)Us!vXK9O4weBc9T
z?pHTPep`S2^*hRGSby5no;Guxd)(t5#gBgUquTpU;*XcV`mDCNK6jfF%lMy|-|av8
zRdK~@Td}~_Z&e#0zJKo(AFIV3y3j4FufF;W9q)bbdlxTy(Tj?Az3W{iUz~Q@Y2|nL
zH7;NH!WU+d^V{G4c5&bP-goBwO*h@Np!Yud(T|qANyf(*!#AB%ZB5`GX5ELb-*U?>
zi$f1RwD{16K2-eA|NKuur@=h=<rq2RLk>Bl+Kux2DW{xL;v)J?{~vYKQRTJRM<<?m
zV)2~kJf~Q3#TCnId8W%QyR7zFgW?};>BQ&@nZ&k%`}mDL^2%4faxt4;pVD!8%wrx?
z(956u+~*3i^4ZURc4?=PP3)q$-Z1fRjFwtzshNANyY9Nh!3Q5)+EHhod1mqEH@~@f
z@{^xj_G^U|R_JI8{^Tb=nHhuVyK$P9cG=55x;UO&7{B;MXB5Br&JD9-{o?+={`U37
zMSH$~@pD=+flK!JP%ZxX`1m5|_qfMBuJrlqXn5<bx0ZJ2*S_|(nLpD{n{2Yl%=v6Y
z{&CvqkbU>vcgD8ZXrqma-~H})=^>?mp$~)T|Fh0ItHk_IfBMtfXLZAiF1o0+gWZch
z<8!9%JKy=v4BqHXx{A)KYjiFw;a9eAT^khtZ2cIUV~#ndKyM9A`5yPX-~GzJlV>m;
zjc(*C#JPL!x#wc?!7==)RaRML<~YX>3;*mNy2U-{YPhfS_x0=Y%P%i|mO8s8p7CvO
zd)usjyY2=XY|z%Oi}NPsy^8<ywmrGH@x2!m|G2%4f84U~|9;zFil2PoI|aKxbiem@
z_-FUgmuSbQU9@OXNAlpi6yJOS=X%jiWJF#2?Qefuz`f(#@6(_D^sK|0(Esgln%>AU
zd~yAtH&a{0vxPmWk8gkb+uO>K^qS5STdQk>;2#dcE`yif|Ni&0ZsG5r`OIf#<mzgx
ztyac49Yjt%^q~(epOb#S=9+6}of}_VgAa`sF^<E)zy9pH>#j5R*kg}9YU4=JFYo-o
zc4JB|Y_-)^<@M-{pp$3)@|VAyUS?uHSMkrjzx1VNmhnL2)~?<0m%kU^-sN3$;@_Bv
z^~JceYdc-H?tZU+^{Z#j7k6vdT$jHd_hO5u_i6(F?e3la?w)Ls=m+^1jSnyG>2Div
zyzvZ{;_v;u4}yR3*zi}xF6(3reC)jQ&NI)$chShCps8zg(xa{$ve~l_1OIHYuvO5w
zQyifiI*TQTU&MYD=U@Zr|DG*dr=8sB=Gx<O%=h>I#4HS_`uysnKVSUmC%>8H14O?|
z{6Bkc{ImVThh^*j_{TqvOLX-POCf{N{j;C_Y{&EX4fL2}ANj~f&f>}7)j{n4jyG@p
z5l^r|!*|w~Z+zn$GpF(G_03B@qKP&L{`n^1E3dcSdTsTk_<q=MVijy^vVDs!wkV%P
zj1T@gwF3`4aOT;EfqyZWpzY%y|M)^)Vvr_g8P|LFyWd^<apA`pUwPU|Z}NUu9Q^6B
zzAwdoeE!x>r`_cb{9(2~&@KM)M9>iJ@Juhw@!~gejjOJ@s^jVOYxsZcHTo^uu<%bt
z@jb+*VLD=%`n=|vYtHIJ%2T07qksLr4}yRBufV@pIGtYC_*~?JXQEr#`ktFT7Gv|;
z*S@yn_(ts1@x#EsxL)+-zyJHc>mH-)-^h-5Ubc-`lIP`9#68~kzV~%~y>UJ8%0oU=
z-0+?Yil2Yss^Y>Iysxb-aN!>BD}MF$YYVjkTDSQB-uJ#YLofDjuZ@2u_l4&C$GLP2
zeh&K@4pV=&N&M6ES6p#L8DC->ic9bdgEze6n`#UKU&b-wp!@8zPuH(EzVVH<v8++x
z-}A<O^j}O;6GMx<AsawW&Y1HB$ddGx+CIa;Klu~1eckI`*DI^dI$n%6Y<)S5Xy=}L
z?#w;d7-YgExA6Zj-2J`9&(FTHB@4b)T=2|yw3P$j+yCRmAFlaDamSzkR(yB2_slx3
zF8;?Bjxmw5>$TC}fq%C2T=-WH82spc82DH3r-mqvXT>w(89AkGx81h5;DQSl<Bv2h
z5a)FJ&el%jqCv~RCHoxT_R`eIbff=WGqvCO!W#aUQ}fK}bMW=pK6Gfj!ghAtF!2AH
z*Suz?4{vzG8_NFm^PgvW=}TW)Udwo<@muz2*ai6Uhd=ybKerg0a{~XRz2A!cU3J9g
zirasDOYy_wzf_#JZEN07rT*V{^+4C1^wr`QU%a}w?B)N?AJ*ZYzaM-dc1$0zt-QLC
zosO3?h-=J6Z}*dPbG(|sI{TlV>bL%f9#oST{qZht2Jc57eROg5*=LtAj&^>IdVx5X
zJ@owNKfipDC-HK9FL^n+0{wLkd!a6u75I0|Ip>_y6L0l`fAtp8cXfi%j3b?|&)wED
z9dN(_aeB!UQUAktV^_6no$7zy^rkn>^l$(D_iuNJIe+KNsW0`c4}S22XXTqbtD3@~
z!{LV?KIiqu`G8sYZ(;j_XT7t`{oUD`>wD|>ZYnO_@65I}Ki_U0yXYeq7dL<Ed&PI2
zb9!;fi)ZKmVMUB2>O;ic#P8(u&OiVBGEdHr#Dn}xx-H~F$OiA~Y=Yi%@we*+UN+x+
z^I5eWVgI|<uYUEbw!<RU6ZbRLX<O9FU54H#|4%;o<nmhNR5wkoKdue?@tp0>6~hkt
zJ@u(it&KUS_&@g8W9PhPdaqvapW>^o_SV?J4xfO3Am^u+GG3|eLk>nmyTCtNgx%Dx
z(ZxZ(7r*$$vkp_65r2Eqlb+NQ9nuTQcg7gADXL?HXQSS6!wol_rDM|TOv2}O_>aEd
z`S&}DpPzGO$2mdt{x84$gW_kOy?hp}f*<HD<HD9w->;_rJ@0u>S^p}Af=~F#a;j>?
z(U%`jCaBX_*B|4mo}*JsV?gjl21QHpuRgzBgQ2)zHAY9j$KT+t*Cu`$yu*%!mrhMg
zJL2vsX3}#~{Hw3-G`Ar=&u8-wQv3hUfBthRhsomf<(g@%&m4$en=udCu_dCV`1g16
zw_Q_f8Q0*Cq>b$tvQm8BT#;TIyUn~0_n@!p>Z$d2`|Y<ckD&vqwW__|b3Tus3;)qK
zbAqooC%9$%*ZBnI4_tT3*A~OS{9kG}uD$l!1<b1>6AzA;I$)puuIB9HAOCnM53IMQ
zHa*7N`R4uAH8#DRpJT}yawTmk{?!mx<+@|Ztf*6U4Z0w0?2qV6RsW>NwfoF|f8rCL
zSY89JJ#$?@oR1eH-rT9?B*j0yCC=7uOYzUo;$Mj&(6g>X&$IQ(!KlF`zsMyr+MGwS
zA(gZ3?j1fg`q9<Vs`xi&s9i0A{7TSYZC={YHgM^fgAO{V9MANa>Sy;(>Ce84ScbVi
z{$AI}p^(dX3Exa&>lXi>?O!cj`o|l7U0nW}Pu{gANF_E<_5Y2BSef~J5i>C^;#lnF
zz<eqLU|qeC_>kYp3i2Ro2%Im5E^nRs3$P+S9%F)+t2Ta$f3bPkPWcueTzv7xGw}m?
zglfEjP9Ycc!!wC5r2JFTsdXLv;8@qIHnvCjC~_3)W7B>(-gj$GM*Rt2#`D&-z`tk5
zyW*baN}+$+H`iqE!*$#b{-ZBw#@~?ZM;qgVrhMr=TlFqsw>;nh4=C8hd?Ru}4ntiR
z+wI01Z!CWEo8J`d2sChiFSW~&7xheO?!1bBI#qlz#guE%@9vHE#z38QwW<FwwvJ)f
z;GsGUdwzWtb0KS7UyagBUh<N*@+G|&zK(b%-ZbZV5?dAj-`eTa;*U4}dKTYQpGyw#
zC-49EV)4N`{Nn{U;P-}YW^BaFtfh#z%#q<2h;>E$h@20(;GGYEFJkQZ6nIWNLHyZV
znZPAo>9=YN{NoEXEck>D$LoBL<BvanhQHiL&3)h`uH}39s*WMc*tKvjZe=cw{(3g{
zBVVH0@EY9|vPwVj9p8eiWMlA^1DE<sMzm{zf6on*@a5Wk0kowj`9*4y$uu>)A$$4s
zLDx?BPiwH)FUBKYVq|E+=Jq%8+1SvLWViawsE5)o-&4A#_}6#+W5@8{_=;kF<c$8g
zkC*vgbunDDRRbEd#S`oacBQc)qsUd`juzyl+`f1ueIM7ypY7_*V;oX@#+XlHtKy$+
z|D9)_UflGFON)R0ZE@R2-tWd%j__MgKXoxbAjQ9Nps(m)a@#zy6HYi`mL3mU*S(Vm
zDgN1^Fka2ESI@=EN4MhebuvHj5106$)BI4p(Qdw-_=Gti>Amq;#EjD4>+p|H*vw)X
zf${X5^s~OgDtfkSDgNn1xv#puq}Q$c%(oK9r$hN)Y%y4Zm8hv=TlgN>_pWZJ?t0bV
z(1=`QbA-<IOey~5Zp0SSTH)$-*db(PJDRY2<p0ACsGgsGr*H6R#QXgX&B?8H*N<`F
zo5r=&fXsD#oHtJIb@(s+`&KUSl9znA_}!(8$~Ath^Yhz_ZY<{o*7^T_OXL6KJKI=3
z9$xU}SHAL<Qdd6X8P6#7EZ&D#vf8`x;$z||^MO<R`^+X(;~{V6Sm)q#@AL+pe(I^G
zmiOZy&<|mQqB(tzuN=odRVygggXi>xY%%uqCL6fjc>0~aM~BNX>Wg#r!*%c{ohV+`
zX|6b*g-(KBwy|e$9r~WF#2#W-q`I<>chJjoZMNBFrTy-@;;FGdiEDc4*JB_1*wW^5
zeEK5Kw%&T{(*7_`d;~t)VTT=7(nJ5etbdzg-)Auj_L%&xXD}wl6yCj~2|CGVitWKS
zeqa}>&marT1=k<;)b-b2U-}39YWIw>L1)MCpXQ>8ahHd14L(DR5jyy+Kl;w!5u=j7
zML+(4{KH(=HxA_1Q=amavQKOla&6MuO&`0sR+k6H)c{x@<Ogp&r#R0#pcS7#zKaXq
z*s3E8%y{Qd!5RCC-wAK{nSNon!>YN|aD>n5Vggke;5&WDzH|<|O<dgfGtM}pwC!Bq
zecX?qNe;k7^sPEZ<UbtBOR`nE8T+_~XSB8fo>3cyuf@fBxrTnh0srCrR{oL?z;9~T
z-?;Z&@A?2^u1TIPYQ?tNU6Vbc-{1fK_e(jQ;-5WV#eBQ(Yy*A<8BczRrK$;x<N14X
z=EholmfywZz#sHQ)#sNVVXN_f#A1wNug$a3LB<#@d}br$c31t@AFscqtotd~`E7&m
z!1-eDEjz)wz_(rXv)Xw=DUI=-@r4;<ZC@3#o}CRl^n$r^^2PK-_^5R<pzb%vJC7Wo
z|3VJHXKD|meafHj;sH4o@`3CT`{%oh`}0wH!GGX<u6MSPYm2!UZ}vQ#hfRP-+R3Us
zX3bN;`hCB>=qJTRt#v;-t?`rM|GTZ4;akqVu23UTw0BS2cGmcYOdzA#2|sPk7ZNL`
zPvuqlp<;)D_ikiD9D|3|p^z<P4BbxVh)ZPNnmsJ&!Cs(W*|qF8HVB(m%>unCPAb+Z
zmdI|C1AyZ}<%2xFJOlrZ+-9rL_3oeL!0d4v_BZo-Zu;0I#kVVSdsEzBu=6_#>wmOe
zBV>S1;zRt$X6Iw`pJCS=+KBaw<#uBqkOT6EV#s_(d2Rk4o|d=n<P!|=2vd71+l}lL
z7hlw}75LX;^VVRqjtLBl&sZmHF8yEqI~hWrggw9(412&>rGCNG5_n7=3<m%5i`$AH
z9@m-!kgf~b^8dvG{(RG2b0LS_UHyPjwki9a9j_ki>tFwRsf*}$`Y7~$*a+!+{EZDk
zw>Z}|!Y6R;UVQaoKl3DAKV&tTARo@YB4gDCk<s!!Vl-+ws(5erojnn;ATbEBNbv!&
z2{ws)<~AA%*zb#9Z(YyJ_WMXVA7H-r01I9dtHldqIP`<~ky@A3M@aFXey+0x#GU9D
zHb>^I!T1XO=xH+57>Q?!!LSe1Ajl<(*LLGCIL=yx#*f_Q4~pmV39{Wb*f{liwtAn^
z$M5yTAstU&(>W18q#wkGW6nuCn}99gZ}?G-4W7cIVh&;t+1G4#z`}N`lLg`jbIAfV
zU~&a&e~cMlgU=zSEMAo5b+h9&X;%w5kk%N9^@$Vk4cf&B{O<3LOJjqslVgL;b3xc{
z;s)aT>^A;AdyKy?X29;R^9jfRHH;^nbW%BH;yB_#VmR4%YksUI?TUDd9H0-X_5eRr
zj+xF+{ebu^Pbj9H#stJ1v!6K}E{L&$TYeY2jeoCJJ8F&8eb&hVHN$F^#TAUJ+B@+E
zbrU&$Go107w5#$x<qg#Qq_sxsz1zhHsxm;nA<KZ_kG!!GcQFTtJs|(D4uCzN9;_}#
z5Hf(>Kn934svpTRVEE%TDR-b_)OM*Gvv&)BQC%PY5>KwP1=M<n9iaXtWI*->hCAxv
z7qHvJbJZA#<(lhZ9+O(Zsx3eUkP#sR*a2Av40oI+?dteS>`AVGooO9+d?Ht%769*r
z4-oHFJ3xLg?0{@v4wnOr7rSjytEND1cCPwE@c^}n<hEm-<6M4)Yvq<HfC6}d4j0#>
z)9Lo8Bf>j@{dnhxic7H<)WNeK*b%u_eE9ritjq)AyQ%*)PHMr`70DH(ae=zLfbq(C
zf#HwSq}>5uiRVQvVaNdXepLqW4e*@0W4S?m7`Z|90l5vI0&=kAwK`3?8S4a@3wm^G
zPIoE;eD<5aImWThac&-~;g8cK-68CNIvK$35B;w`Kzupsh4~D82R;N}BG;e}f7Fc?
z{E7*{v$_s30XR3OKdmiRKVW?Y`CPeN^ZVF&YB{pcHvF-gq&u{;1Mp4N4j>b%wZah#
zaGl(SPXRI@>;N(#>;N(#>;O4}nES1sBdra{Io#oo(WKoW?0{-Kpy~_IC)HR$tZO&Q
zSb+YrhsCwLU}7Q3AMpS_y&BD!XJkzxHGyIPas%uEGJ$=-P9Q7TZ0v{Z2TU45hu>dz
zVblniV`{w)yoGn<pJTp|9D(1|r^rt`7B5=2pb6U&j@2xwjm5L-+pH&NZ9!}CSW`0A
zu!}XOyjxqx+H}^zv7Qkf2ovHJVrS&jw8#k0VXVYptmh&w!&Wn|NX=m627UIMzd6RS
z&T;Ov&|>(=SU7i(8&rD|xj}U#Y)5`S|3BgZY|2<aK&_fQGrclM?KwRzR;9+pz9!bU
zO7T6&XM11Ulg|7(@<8rm(wni8gCT#b>k`MjLjC|7K;1yL0VaLKN7H|{r8%qW(dCZR
zp2+>F7Y_WV`2evc>tHNu<(bu+;md9s{o^H<XuTV2hFi~W5S({&u66kAO#>I~mPyuw
zVJ8_UwwgLFbAtE*Q76a-P!k|_!v;_jkZpi&ICNni0}qiGOxG0<_rpUW1AG=AkdKbq
zK=FfaY6JP7{0nQ~S$DD<+z#Uy>xo)RTP<X_a%-Hpg1;rEFSbt(i`kORV*hdg{D80l
zA|J@7%K5-?8pZMJ3w)ZBMo+|=W99{$Ybpjv280cu&Ma&IbI&^20Avf9V(nRL%?<<8
z{an);-u4+_i%gn55H<iA7V8tU0r)y$14J$$))kF;g5-Q|nF3V`gbiSAA@haUjMl4Q
z1ElkX@DtyFkH7|vTmT=U+QjR{^3wf^`oZcj&#@jh*)~bGKxzYsZLk5zXlsOo4dC5(
zzl+;AF4qH8IW&JyDZmDZbw}ly@J!VP=q48sIzQbTXqXrt^xD?uw?6kI*#eOdj5q*0
zEsX=P1KO<@QYWiZKFFUJCI#SDOg`2ZjQt7128euM)B}VKAZ}<as>rvoN!M6ojj^%y
zyOj^(k!0Q^H1(HgE*D^qhA}ce_B)EXg7PZXCuYBq<Ki!5ochg4^LZv=Os1iabvA$)
za>N1fm^dJQOV<jCJq7Suy0>As*c+GQ<RInEny70SCO^QhoIX59J%DjZ>jB6C>k#vA
z#BJpaW8J`9zdg*+ncQoqHbC9FA@HBh3s4WF#%xh57l_wJk<S~ptPrR4oZ~J7*jrH(
z7;B5Fe>&obBg&dUb%Ev!+FMFJhrErvEm<u#lUt@hl>+de)&r>ZVduyCVUY`zcTVRA
zn}0T+dA+7(f|>y}PvdMS)8Xni!UwRnQ`HBsK0xe0EN>%kOIGvqa?2E`qkveroE|=r
z3zX~Q1DFpi2S^66^Z5Yu4(!8A(-@rZ>)8ucY<*m1fPTfgVSM{^-7x!ys0FZ}khKDh
zTdWh7*9xrT`utl^fZfZtWLwfLcuPz#<^{k%ezG6gamO7uvmf?+<8>JKUumV4%I6<f
z8NdfH*CF;7AfxRs5I%rfAnS+OPmF!Qmb1PXyJ3=Yp23^>eKZu{1E}v;=g$XFryqL?
zurcM8)tH-qGYs6N*OZ@AL$Ln(>lgNx_Ohq=3M;HI^n2@vIY{!N<JiP&V{UNW8X?va
zXP;R+D2)M_!<}OQ<2a(@(I>toA3%Md+EMd@_>Xix-7lsDhbc~*`fOg`>Z`9_*0RxA
zYK+*qd{(u_UOwv|`Gl|4^4Z7UxlQ#Wz0PjC?KYNpNo#@lX=JqVNo#@B0;<ExwSdiF
z3(d)0I#NBQd9Z3{<zvZWFQ5G;*NRuwY&vJ$W6D7x_)q5r#+sqlp1@zve)h8qdO5|&
zpr2uP>#esg;|l%E*E1IS_k<@rp>a8Ik9*vsw4>S1<|%ioc@7$m;vGMQ95hB^vTC&W
z{MH8+0}u<~r}^FA9j9i)_)ZExj)Iom-UE*7*3?!P!#3t?)y<dXb4^n16%3MP)%8O0
zme|;!cu&t?bImo&bw<d^L7RS~f73Nc<`lcGm}smoFlb+!IN$Te9wOFt#)tGhy$}E9
z21hNhc%~XBb29SUVEs`)@Nce+xQn`qSW};0Oa`#E#9Ph1iuv-BhX0ronBrd@$>y7H
z-YE9f2{%12SG*1;*b}Lo8T7O38S_cNzxhCc|7sl&{L^8%4ybn-uu$;tIr%V=2c$R5
zp&t}OaXx$hMK5|$nQQ2M6uLV@9%Iiv_iW4`;Pa?|8Ylc4r)msPd`8?){9hfAe1O`Z
z*gG_2*d*Db-HrJ0k6K9hPv-+!Z$jKX@H^=HUVH5|l6865$1i;03mfY<`$yfa+}k)b
zI=ng;fDdXefHeVQE>Md9*gH7mzxNSbDEMbX)1T@^)dASEzHS}9e&oRR+izdSPJ5?*
ze`konKIJJ-X+#FFTi|OF@NW)4;NSc}b(?BA%o`NP6mJ>V+J^o{diZB8MEqM1!2Gy=
zuwFe!9$#(2sGEDbceOwLeBW%d&BmrC0Dk3}Vn1=QD6xKf14bR7TtK@VK*oP>oVbwi
zF9yiBWIHyI?^7p?Kj(wz+|#|As8vP__0Qwd*lFU?<g{AtGg@;2tpONo1E=^GmzD#J
zy+Y?>EXIXy{q$p@;2&+&h{;<#<RK4fL~n{k&1cNnz176^qw}lhJod4V9h>}l#(zIZ
z?(dw1gnvG+y1yoLzqPj567${kVU<-@8CV9OiTQNn&@%o<Hwp_4|GV$L`@mSCJH?~N
zrOAg^^?5(v?Nc%itsVZYvmP}7Q3IrgBWi$h4M6Xsw~+9!{(t3_R~{HEYVq*exHNgq
zCh7y(5o!VEyUF9Jp_12&eF4(7Kw=(Hx)z9e9p(Yy=iIudz(T^meZZQq{nZVPQ=Ca(
z%!}<u=cnhOjhf*3Zg>)wVcI-EbE2&OuLcPAz3SEi%J}bn<Q5A4J?9QP>@YA!mRoMQ
za?W}0lpgLGY8sl@b8M%bcAEFO^cnxd<(2g63kCm+TI>HbG0$h`op&CaIC(mb=DyjF
zDAnivd|r0hWy`q+>9t4wIpcqH6kjO#mqYFc59v8-<;+PR*CrRPk7=L#`E0(|e721L
z(NTY);J*poXpJ80BaCYkH*aDcA?qQ`XUq5>9rYIi{>_<bVjcfocinY#1Jmc7p7WgN
z46OgfIOcOMO~!wpgWtz#3jzPuiELu7kF_Yqy*1(Ipq2Hv=Bs7=kB;&S0snGE{rJJ@
zIm;}w%)H0Ny5S1zVe4z!_kKRlXMIz~e>WrD%rVoBe{u1xw%Tf7JUr@AkD7>mCd{4R
zVv8*Xe%>cO@rm<R<B;*+47R(u%CzI(JlWM&TWw%G?7Q#26H!}c?yR*)`qBC6Io2#T
zS86_5#{cN3KJECo)^`*8`0=aJYTTRrguM(>Ecf#n&FqIbA1&j5bX1>q{Hqb~hyT-a
z)Wn#JJ}LOOH)a#HPUOgZw2c4JQGJ^6uP%P?z4sm%|Ld%?PPx9+q~PBk#%ry$*1*rb
z&p!LiOAH|6KjXg%3dmtU<}r^M7zh9LU;kCEk2Ng(lT%(|mDcpImz{fgS+k^<mwAcS
zIDhn`A3gAMZ@J}`WxZ1`_iy4@>oi&K-26X#vf2mGo?r4f=KffB#rlo*28=a;tpVs9
z=MLHjbT<aZ!kBm&pJ{BK70*oL`v0D*i9P+q{>-sOt6?;JR$cQp_P(~}tvT<#zM9z2
z>|Xb}SGm@3ulJtR<J+w*YA$jUd$|mQcLu%YG~?gC5KYYYJ^AF5Cl3GecWDfx-_Or|
z?sI#VJsJOlqH=WSPc#1Q57jT<$9eY3v(Mb5>3{Pbnvi|g8J|=<_uB8Q|EFvJuVarr
zc3}L=gW2<J((rGOkS68=s(b1cN0WMd#{YETzlqqtxiI#}nKb;1Sv0XW_=X#9nDL+e
z|B2lHvk4z~qm4EyW22LXe>!-@6;~YC{#kX^RWtrG{wEUuP2|A0-FDlN;@|$u*0eqS
z^wSIL`r7ZBUE1pl{?}f6?Sb*Hj;PmrH+g)wzC;uEtlL{O_7=?X|H<0_Tke19rI#L<
z|MzU|w<|X~O#LsnEDnputl{zd-~Ya>y|-7PoVIm~yM0*)(0(}f&Dn9s9S44XK7=`i
z-QK^6<M><h`RW+$OJt8?`~TPj97er-_M5+b>QkR8j}@o6@x~hm>Z|pS%^5M?=2ApG
zff|5mY||S5$A?YS{o8YH*!XXfuhjp;z{M9|T&UlRJVw9oOD(ljiKiy}IM=oCWM2Pw
zE-E$i_i4rdMHgLE$ldkJ_i>&a!ldHgK6&vR{l23iSu)C&@jp6hPb>bdh1tZKKla9+
zRQy}RqhH*|d1#21qih-fqoek;;@@5$!NY^R+k0YC@vlC9kY|r`(Q=e6<9~G2o>u(7
z`OR-0nD^Dsefi5@o>=^U-~%5Rm<D73T8^@1{Ev>>(~5t4#fSdt_g((ryz|bRSp0wF
zBOhs`2I!!J4jLUpw(gYiKRR+xEB>2^eOpWao8SCqqxhHW`_rHPRL=Laceh;At+(D<
z*qgkGmd5|ho3(aHzw$56v!2)}Ta)-l?@q5#KG!(jYg+Md4~yX6e&6kfYaQ$+{C|0U
z^O4l$Z?@THWo;h&LVb<Bd!O)xCzNyA<*>~M?zg4*7rR(#rIiMz!!En*(r<VAI<HCm
zldIM-R8wGGg!Fa&_1BkjSgpvk=J)3sfN90Qd29XRKF(7=sFr3B{L?*R2%B%dc~|ps
z$OC(LENbmn*iTFG|H)5&vUuPFA2=`_w%cyIe!9~4IZfdo?Qgs7w&I2xZYb9>Jp1gk
ziw}PAgXLNx_7i{4d)`y#HmngzE?6(Zy%$oOR{S@y?$7$`uV1Jm=okO=$}z_r)0Ynp
zw_p0wmwMJqDgM>YHBkd?Eum4irtohKAG%*n<oT^VVAKi9=c=o=U-~JhoHDb1P}KPv
zGxnFdKQ>$S#dmXEvSk|DG~(abu(3mr_xt{khdiXX^wLZF!ap9_fB*dlnuCAud*8eG
z_{Trq6Pu;@=Wjgt!4Dpo4rn<_`?x9m!|I}z9c2u^_{A?4ANtUTiW5&fv3T9<URS*7
zO>ZjR{qA>{`?>P*=znn-&uPCnb=PW;#ed0Rwu5ybd=8#ar_H|@C2e|QhAs}CM*MrG
zO*YwNV7`CEBOX!C{qINr<E<v<-!H%X^5xpYT?}P;NQ!@Z6>q%p#skyAHLR&Os@4Sl
z>G06~Vz{CE*<<ie_sfrb^{Zbkaenj7H<$A-V42S#Ho*6T`S-o=eZ}#|A79!G<{a=L
z$dix*<jkbDX~aMIy}<?>49xfJUOJ#x{L>{(tUDNXi#f>M?!}SfU)>%XCa%-(J3oj$
zJgU|N{@L-??Ta-;trN=UXZs&_+;OGuXCH;`H{S>L>2ve0uDtTf;_F}kdTD>Dqv9{9
zr#$Vn(~7gsI;%MM+;fYoufDpRr=Y)++NKfzFtF~r>kiEKVhH%B7yRQPb9Vd1TD$Y;
zpKg546#sNr6YGGYrMYmUYJ=k67_iOFTNK+P2gUl>{B(claC0O>hx7A8_lp;X?pOPW
z_RdoWrKakfbIvK_LUIb|(#^Pxigvx;2d&9IJbuL$R}>dqa6$Qe<Ru%{I<#mYmdsW%
z7eqY8JjtM0ulF4G@%*?Z=Dt7fagXa6|7=HlXSKs|zrVA=yRkD<{PS5K_OOQy+?NME
z=s~4#JnV6c>ko>5x*var?pFsv_lFL*uL*wU+h23dHDwKkd-8GU{;RIKYDV{4cl_w1
zk1k*B;)T)gNqi6di<8vh-#QfhAN2alSH4opJ@{u^jVu1iGd$Zb*5W)lc{D*AGDS>p
zu4es?IKS!l%PzZYuG^RE^~d~Q_!q+%v>$Q^a$}=vgW#Y2Zmj5jw!Ih#-ESTz9G`H)
z38nu|_tRzc9zLeS=ZY7)j_ZZb5b?rM(PbF-nMVBM)h6;@PkY+aO5UJPy3P4CHRq-d
zvvOB+wN(GJQy=-rM-GhtHP>9Ttdp7R-c9{|5d4c%RCTyL;Mnh8>;O3xzCGFQSTdts
zyfDob*2N2(dgl4M&NSj*tV~T^9fo@OO$}kMt%-RD?fTeF9ex%6P3!}Nmg<y8)ds;o
zoy;${Un3oVMr+?z{yyxB`&lnYJV0E4jUZ0I?<Y6ddEx<be32`(&yctu*(--|eyfgv
z&w$6eu?I&*vwrWB@xMe{IiU`(*Jf?1cK99icWd0u)vEZnhu5I@ofQ1b;dzP6i_eSG
zi`T<%=zei6dy3KheAuw<;2Eyr8_vZDoOi?#N6d;Bk}Kge%rzF1@b_uNzrD2Btb_9X
zv!3;=o@2Y#3mf#l?ap`Yxf=Y_e|%f|&-(rCuGj7FXxVN4ajyP#^Y>K$i_yU`Jc{4h
z5A7ANctwe4zeO%i%#J>nd-d#mAF+Bddw##zKi$uN58WUBd-(mZ9d$x%2H1Dcx_IF<
z=ze1{jrbS;Yr_A3#xtJLGyeIP-Qud(<HVxpYHV4&$){BZ(Zm{{qmKWH#qr0*<LPJ5
zAx0<H#$NaG*>CLjIEF7Dx?jAyPWQ`U)#?6fjd1AxG+vnM{z=aHr93o^_$Sw!i2sZK
z!Ba1dZs`@*-5hT%5_+wd217gSu*1OpK}#}qTpKK=b`Rap?}y#+`{l60?^oLwet+nG
zet+nGHNv6$$&knu+E-m11ZJkCO(Xv0;MZAaoq_rOsZV`s&vS9zy9wK)DihXQZ@rSo
zd*QpL@Q;?BYg`*1ww3~X@)zJW{CD;{n@kNr`2A`F*bH?s|JZjK{>cw=ByxrPs>l_N
zD=m8M*EHgvO{iwIilJV=n?u~IpGOxRa>yYA-@DpZ_Gqs*r@l8a5851}ZfcKu<-Mkk
zM;HDM8zrszldto#4g#A%O<x-GCj;tggvATcJ8FH&6n+I6Gflc5{ihNC{Kh8cy*%kj
zPwH#D%^hpf1~8YQpZuG#^$b;6((8Bg58!JOTF3%Epg3^##qRek{5qdgc@Z_jbbrJP
z!)Hk4#w5_8+dfVs{*7%D^IjhR_{aCn_i{)}F1h5u*jr|qWy)HWZpSk{4xf95Uh!T%
ze$!1i?b#1X`!SB6>HfMJ;iHZ^s_^17$Q4%Oh2!`vgY;<{@lOXdQTL{HR_wW-Mh1yJ
z4YCIT`C$%5uXca*SKphc|J`DXE&A?5^kv-M(Uc63*Sz-HYs+~r)=T;Dhd*4t)Cj7d
zuI38I?KuYR&ots6O`5QK`4Hkg{Wi3ff9_`=LhJU?KfUH3(!T0@6MoP(+ia6wcIrNp
z1!Sc?$HW8D7aM^OF)98}uY)tK_<!azpE<DI+eF-3jM;v^z2qa+>{@rMU%ekZAZFjh
zJV<pAxlJbprWOBtw)X9<`mw!!w+3Gmd2cv&9dm>DCD!$_H>vf3?5}5!U2Ba${pn9H
z*9exsXrj5NHH>?G?&|S-?6F4^-O1M&LkdhQ{+n2XW4Yy)YrO82EKsK=7iC@Gn1?oq
zpV5x3__m&*isxRxTgNcB>7>B4;@>)Nz4CeWc)FSYH>r&u+r%Eo=D_AQofMc>{Hx=t
z;<=yiYRV?HsafggnXBiZWp2|+foa9R8c;R!RbKA(yLqIO+P?9PZw&nWXo!}%O(zAW
z75~<JZesmywWE{R)Igf=+N->)9*>4-ncH+yU|R8S9=I5Lm7jb4ZtbW^XyS^-(!AJ3
zt#xdy71+dDBxtDiHMi-cz$ow^y!qF^{<WM(A}?)?zNpPst8M<Kyq7u)IrBlA>&dJ2
ziudaAhaZ0Upj{ct`Dki>51N^ma@}>;l{IYEv{y4|tt;ynSD&ZX?`VjYy)-pT?2nkY
zug>0hd&x<XQRWk=zw->9CAayZK-2gStQ%i^X)RiNDOeZR94z*<IhE#Pn{VM|eQ~vJ
z>Mhxt)@?S|(wrCk-Rm4Yl+=$V_TYHI3tlik15<NXxPmEpJ@$>-RP$5V`S5<|p@)`x
zj?w%3?YCbedw;FG^2&4KGvttdSsTk*Jl4oHZ`OF*FUNSt>&!FHEbAP^C)g?M3pr78
z20vzbP&4>L`l$*02j-2Re3^P`YgL##VIGX}r<1HDE|%=Q`ikH2vKnmbG>N_859>9A
zEMOaS%O6dweQ6$%^&s&vIfaM4s}Hwc1U@I<@IC!T?={gpW8j^g6nKyItl|COgAXp(
z!-Dm?*8?8#fPwWt8rC)A5VnWDSw91w*&o)<B-h)GcZ@Zjz#HoBV{Zw(gNM{Z%q0uZ
zDEdH#*gM@^T{4l333+4gKUqWvg>FGp_QyhRgWw<D@jH9ooL9LrbuB4gsyOfFJN^jX
zz)$!Jf8jGWg)#4@E`GP&b{m-g?{~lZm21r5rQloZ%Hdh-Nsx1Rmi}V%bkna+JO@{F
zyyuMdD8=8cKWk3<t6%-<(yy<>d-XSS2Ya>mtH(d@dC!~GUC#$M@WZyVKic`NH8brs
zWUhVSzMFB5V~jPKLAJ$M!*h%^n$$JF^ChhLiT5tN@WQgDimawbTwj0SKb6%t-E>pe
z`#IFzcbb0d7yoEZCz~@-mH%Wq+6P`@ocnn<j>eNrVeiUm8$U8<F2CTg!wwsm|HZ#j
zp04r}TUUShxO4TV6W<sI_Cr<An}=@Q&zHaa<+VD#iuw4y_uhLqlK()%Xr2$<18-5o
zXbn#Cv7hmcbMT6><_Gc<?L!rImG}@nQ?>j2CZ51oq}S<pJ|sCrrnqLhca=F6<Gy!Q
z(0Www{otRhrSrV#D0H<q5Iu$7P2fKGBHqaqwg8`iEfD&FuB`G!(2gHb)n~nam;bVc
zR6LXC<I}KL2jwUI#=~?eTH;@Jg=bcCci@2s_JsMszV}^s-F2Y3$3Z7FjD7~zte;tj
zwHT{@--E{P$%bdkv+4PNVhiHOG1lJMTkI~ePiu3krA9yYSzL=B;96`Hwu-&|&|922
zt`+>U5Z`;nKe<c~#|t0ut=L#$bM-s^bDc*9gpC++BQ|2n7wH%}CiuADcl9s(?z``d
zZlhb$I3*jnAHSdN%72Qzy68N7Yt1=(h*;}C#dEKpx7%*Jfu9}C&@l9@7)M}VfBUuf
z=Ne~!Ph|kyh790)z%BZP-^nJ#kKuO)AF|aVPUG)Q*=i||P5S3v@NdkkukicA-ZFM%
zua|LvS+ar7B!~H7<TQEA*E43WL2f&aADH^uRbB}{ksUxcumkuZbuxf&Vy<1k*pKse
z+G(c}|DGp&5w<ToNZbP-w)0Wz_=6oyX3$&Yr}@F2lg>A9saLF5k9Q4w7e@d4eMdXb
zr>|A)v(t@vb$rtActG63ac8u0J}_<kynK%D=o&oZe4o9C48SYKn(k%?kOA~JJs!5d
zyb-xf-$%bvTP@-=Vv9}0Y3jyx5`XIk|LBR9^i%ca_ZV~IXw2$#G@9dkvODaqkmLRa
zt5rLW>~QbkhqzC-zJTjCu@2b7AO7(28RNd>R%(BA@<FORfd=@N9~$<)=YxIo?5bFv
z>pMHaei(8A>Hbzt;NQAmu_lK8@^>PqW8ACbVtnvG*xaf89{C&3FNO%eVkTlIRsD^}
z;##S^bzI0>`2n^c`EM+Y3IEU->6cgVVznPR?*DJD><0gVyLRt*AswewFS>r<{oi+8
z{1g3YOv&deuLVD(_Yd6=W1H%RxH>-DWtUwB)`P39wpzIsY>NMC3?lB?={@v*V4vO(
z?DwMg#R3mF;DB<SI(t&H!{Jg43_i#g*zH&E$9X&Nyz?xKvAM{0`a2y9&p=Ka6Sf?E
zo%-5gv)9oEO(Jicj=NYAn*ne6EWRYZFW%%C&29AVyWb=J6miJZ_LrkgZU0_uwYok}
z({KIa-<YSk=U=7rxm(<)w1ii>l&%Q5j^`q$9CjVvs~g`%t-9B09G+ece(v}EzW2Rv
zas2VeFBY42KQS@SI#(kD^^Kh3JHQEE72lWB>qgIu2ew<68r|@<`@lsLxqq>L&&cN&
z_u>21+5PxH4#0W_fl)aExQ;$`qK`h45o~+(iJW5O@yVOW1Jm_lrSc?RcrML3vp?{M
z_qsmG7vi(r?xH}i_)leh8lw&UTjz)OLTkJrzuzvd8+u1S;KvxJx*_;3##nq|kTp2T
z3^73ZR(wLNGirWdLvD%=o2z*ywl4nFCp>-9Nhg(i1$2t_+3(JJ)YcAi|5z`Coz%|n
zaLl03?0j|wtnr=II*8%0#n9e3#2AErj(iB+4Er$#bJ51%Lk6&2sxpAg3EGoGV*T;b
zH#Q(%WPj+B7u$h-g%*w_EBG?T!M$?3ivqpkpInWxsN$e5N81mb@r`jZUdAnQJI2$v
zh79n|*Gz3r@pL>C`5oU^Tye#LZ2&RAc<#tecZ+{Iu$|8j-*vh_a3AYX!yo$|w&1=K
zwwkbc)IEu*rSbl%?q`ohK13|TvEp%TqJDUxlME2YCx6I#{YqbK#K3>>M(~B>*b`O0
z=!Y+-@tj`ppZa2{tmn6P;>UjIMyE!6i0{U~kUPK!Z2!<fsUJ|y@xYzud+0+SIxzml
z5%CoL-z)x8-Oom*r|5q5`c>?+1=RcSH`q618=E@ztn*Br8oD*H=ierqY*Ole_Dbsa
z2i@W9j6}zZhlt+}s$<E3i0!9#fY?4C06nTXDBsBj{HcHB5?ST<kW2Cx-TG>s=sRtH
z>lXiLi^fgVy>v@Md?5EkcGG=ix$&oSLe8hQUeu0;trzw&jP>jL$9c;xyKL!G_KJUU
zksK4x2)~~VC01YO_lx7(dzybh-m!hy8FCrj*sc5#>y}2p`+eWDwSKBNZur*xtnjVT
z9>&;1Fc$I<#@ruI4bnU(nJK<+EXfvQ8!|wwx%$$Vi2dnj8jnunQgg}ALHa$3=XQgC
zIyP+Is?9&>Sa+fs?657^yb&9YI4^k=GN9T&_5#dAED-NDu@;bhVS2?s`iN)35<iu%
zY;Q_?gH&yQF$U`vdM>&SuIN-Uu$veITGDs@Vm{78D|_FC?GJZkZW@10b^kE@YdGyx
z+fVMddxf1kN?p~7kH`6M-Qb^Gt&8yt!|t!rDr~{34Zs!;{Ckgl;Q5u@lk@43Ce{LB
zSNDp4@lY{IJ}B7*M<<_rav`>!#{JCM73Zt-_j}1#vnc|{{l23WpCj}xStR$F(mVX?
zpm*wDSI4lQ?-2{A%Ly3kv@g}qA)8Wr5D(<o-{K=N?)aw%=y5qMb{rpH{5;|S#-1LS
zOXfE*7g$b>E)08@jnOIokB7y<#l*y^*~)m+o<M4RQ{SKNmkVUe$gz7)wy*1V8(U!`
zth?^I1LNPGVeBn^uB-Qnn2T}HPva7EpqjL=JvaK1pONPmKcS1l)~{mU_mE9>vA@L;
zu&{p_cl^UpyZXR17ghBE%pD7Sdd8;K1E-&R!9V=+{ozE6nr{l{)ww+f9duAB7d#U=
z0ef^R{_nP_k!`}x(HHdN>#=wEe&`!m(?|8%kz0$nU$-%eZlA|-?dk$L*#Mnv!CvmY
z5RRP${Ij9?&|=`?(fsJTIy5n}x|#$$#FuSiJ<w-8>sgKCKWzWV@yqqWpBz8`rJF`h
z${X}+^T&Di3!vA-_a}=|-=D3O;-8#?|5*P(KbmZ8fQSQx4Zx2Q)2Xuos<FSQMRkoP
zZIdRSaXzm#?)XO=xnwc0nCl_N6)~yMNx_$C%}3}ZejnYWjz9Rk-*@X_xp!(`w2RHb
zLO1#!{*OHJ$l~p9e|zcI(5tQwQ=Yw>#!p^rt+fVzKDkoy(`w!?=-esikC&QmXjqpE
zsH+9hNBAcj_z7|s@Xvq9b$|6EGYR-_7YnZH05)pmeZnslH;DOpV($IoKF$*xht+Yz
zKcBaW9EZ3Bxmm5bB40xWum{LMa#&uLTqd8pl~whe*#1}YRSalRE8apj@k``JtA0t~
zKX@SUj|Z~;ucz-M;6LI(X&z8qD&j!Fn-Q178+Zh-q;V-R_$KB9(~)B1_;IfI-zfAy
z|7NSLwi;Nr-RC~{DSi4*HJ2%k$=H;}?d+g>&g=eH<6k#7tUCVipU%-@!-&0z{SR7~
z-hFQ;>&QvLe~f?Bqo;A{&`aJU_FI>i!k=4jz4gF2VGHop#|8gl9$}vh@{Tra<)E=R
zM69P1G_K19i&G3U7t9Ax%N6xNaGlQgjlR(XVnAX;Y8C_kVn^_w+u{_M1pMoD)B&V5
zq4*x&dl`THF$hNDT=lWyk#p&PcB|t$#s3HK|JmGIY_UZn&%}2o(^4B-o;B)z;H!#%
zF}Z%``z+3hoqtJVKoOhdgQRm3GX6W$u<ozZgnx1%mH)AJTq^&?lEm-=$Nj#WA9v!3
zC(em~dW%lgmv;3({o<cqR8P=EUPH~W+I{{~75|a@n@{{l42UnE)+O`x$t`0a_*bu#
z>i@9+>-c}6|28T3=i}DR4VCLP?qLJWg@5C1J<on|pPskMDyx(>W+%V5oBF?g@z1VU
zef8BFc@{MZY?Z)2dnKK375Jk6#a+p=nCm|Z{ZH1(;iU7ztNux+*hJ3#)l0fb!hgG3
z&=mi5{7L_&^U>M3E3LFrBe*dyxRY<mZlzb{3;3+`l{}xh1nHXIG4DtGO3jV<yZJQy
z>bmB-O{{H1o{?i{G8g{IfXMyBo<52d3@i6veeQ?>#M&@v3;+*g{8uS9_xDM|Kb^;x
z7T*)+sp|iB^&gP~L=o|1HEyXc@AtEun|fZ?tZVd++!&n5m&se>)#`eGYIj@*zWBA~
zSF8V%x2iVxa%>a%KF@95LFCvv>3{g2&)T|(0kxY8lH$KE|5K%7{$7-GHO-4A|HW0*
zwyOC`<0{pdkJ@lH-!Nmo)v<{>&~ETwt^cUY|9W0}uV37!=Pk9=Qe_;HjaqGfgJC=i
z(`ak10{oIeF$ci<e=+xuj7aDHrM4^V^KqJ}^Ysk;2l0FN=tlp~C;rJO@z#)0X&rzV
zrI(t3H2-UyJH=?L<F(MfPow_t2LEu`R2%?~R#;($a(#q48NhGFhiV4Y*0S-%4i7ox
zkQw}2!&iUI4VKqd<1Ef4b{X*n*XBPgzx?uz$SwW0-++b>*eQIDG`^nJ{?51l5B#Gg
ze~OHZ`3V{S{YkEA#Xp?QRsY4GTxXqi8p9HuY+WC|dD`%;7_@nEa7{j$U#}i9VgO=*
zo#y|#CagB4_hEmdjW(K@Um%8&&ar5>=FfcUfAX(W3^4RRnZP#arbaUzr-k@=8u4FO
z|C###={U2so5Yk{+@7tqbJ(b13$WSKIzVv?af<3ZK()6pE~kfw&8<;e%MP#)c@ud)
zxc9o}J?~lC$w7-ru>bYZIP&Mn(pZ})a)25CF`C`KPa6KK^*`14Ut0gk-d4*>_tN!&
z$CNts>hquf{Kjm6z}FgUtWmDz5%Ih50o0`NPx<iS1H?KY_WuwUWM`90WPlh!Q*nyu
zoA;N#^rf@b>6!%mr}n=Zg?9FT*8eGocm6y{_-`uzU!@42ckQ*;Zd?Y4&s#HujW1V4
zkHf$DeRS5Tr=B{K2eii^Oj-woJlJNNZHncVTdwgw!M@r>xzw~x3jWg=Wg7nz`w#t}
z<A3SMcK#grkGM}={2yIoJym~C{VL~=qDAhfs{ebf|E=q*YqQD4@lzey^ymBE|Ne!w
zb)Nj>Cl~4lcHD8taz282$?L7RUMVY<UV7=F_F<W2mMQZPRXTKwe|Af?zAox3>+GxQ
zeFpg+IUqbB2P~GN2AmDZX6lsxix}!4_n*ddtN53ruIB&HHs;l&Ybp(M&S#a@z2Lvf
zpJD%3uNmLt+Sbl%DyQDG>}cY8;s@p})#0>?|In?~b=K<Yt42}(XRIPd8TCIAqvZ4D
z`k&zD-rn2cznb^)-P+smZ_c-T_NefWU)Alaqcb<1U(bf-Ba4-hPlL$+z`q(DHFeV{
z1I$+--`d%0^ds9^ZmnAX&;CPS7^~KTjB5Um80uVeK=rTAKFRg}?PWq5Q(@PW`{F0n
zwH0E`hxx$2u~)xK&%`?5b@Tu6DSegtf8y}?x|97E_-9x0Z<^RUw+VgS<Td0E$k29;
z{*Tytr};JWg@64MOYLU<Pdfisy;%4s?Vf!h|4z4(t7^FG)_f)R;VST-uK5!CJPn(E
zsq9bts7@x`2OzCY?`2)O6#v!;A^$eteDk6D;Z0(Du4~!HSN@?>OKq%9>wi@7Z#;%Q
z|6jjjjM$yVh#q2(rtu%Vn(~183Bz2QuoFEe<!^z1W0+zq-A@T!Q~y7;|BZ`l47176
zku`GR)!H<D^J4oMdp<$0>(T~qrLjeJ#FkrbInllVn}45u_UUTfdt*pOgpbv3{jb#j
zSI;~O`(NK8_80Zvu?B3!{(=wGRkW-Bq(^d_O@Vgfp04{6*di0m`=VQ8-WS_pnD}??
zz-P$*^ILO&>SRA&@Vmd$5spvy0-gQj|Na)&Px0?McwHV2j^@&_<MwwoP3j5ZrkjS&
z>HIn|7V)*Zy+7(~!C}UN!v6~$Medk?9QGgn4E(1#fKjba)(w5f`563f`~qK9+bZxs
zsx{wJTFoV&V(bSQ_jdSa^Qgz)efQnVdbx4Kc)R|XivXj1=3ZL6Sa3QQtm+H0$Jk}V
z+*`BSueuy{;NQD(lNSs<K(2=#=!G5&<=DE|Z#4!O^J=T>Dz~evZHjhjd@|zF(YJ_8
z8*{dQzxGKd{0CpD)!%T#4aeFakl)*W`|V3S_Cx<VoS}7!fApxv*wYwW)3%`NMc&VS
z)K5j;Pd{RO(swx_exRDGr~&2Ih<`TSzkcXEZs)MC(%7nXUErJ_1@~wRYjOhkVU%+#
z!tV>)yjt@S<IeW)SLSzvf6tGvUh<Nc6zX`|>E8MJ-8?2XP`CDe9S^pPv!=12QQLym
zKC%7Vt^JA@^iND!pB=|n9aW67j;53FTgX^>(x|PduC0Q$<n1VPD<PMx-xGVkROOO>
z;rn#XPwYq6t!>gR{zLxJEqm>?S7E*E`NDhLcf}P~EawauyFqoE^P|o}4uI~&2Tj#k
z^n(SlU%n|HBldu|J_`RPWPkOYtY)*&ReS|LiuzV_u-er>^wYO#Iwz%b8e>mwzf`W0
z!{q8P*8wqB#?08oeDAv2#L)e0gIM!ZeS5dMuvh#?4K!Qy=%bG==g}^?<dP$k1@6Tc
zIsW+LOTW8GJ`6q(3ktl6g{F9`;w$RGn~nigbw6F+N%w~w(7#SG%0cC1lk^^!YeoGB
zT^scuRb3k~w(tS?<BMALVsq8k^z)4Rqvl7<m+r5|d}FNZ;u2~^y4C&q)-V1&JNe}K
z<weZ%*?8lP3w!8=es1cWZ@=oQs}}YkH*XLY)NJT`lPzqnw9cl=2dNJb>i`ZT&mOWU
zbidk@(ETtTJWzd){-wEK_KX;S9B0P=;v9n};w-VoW;(wjjkBn=u8Xa-n_s*5o#y^U
zUsAunU0;lQ_#|{ceJn=g89Yn3<2)$-qqflf$Sm@TtbFl{UtG@jF^`YVhGDtHy82?j
zdHK8J%+pzIwbhDEH{G<HBecgJdzAZ*;d}IFgTNfyxYsy;rxEZBX&w+w>wLkGyVZ48
zdXc-G`jP5>x#rOQo}F$N@2z`<T!=a)_JVj;*sAF|u$}I|5dVgc!~v@|K&-DouHx&|
zw_^LHzE!uF>qOs(LoaIS*RcDmacF(1@*uyzSKY54P2s=YGvQ^rnjFKA=t*wrzn9N)
zR`}Wref>tyAA0Da<r-n;1p9pQ$tRcd-RNZ3Q{N1)O*VYMrm3#8m(B&M%3gg5*-Lg0
z!UxW67xNdBCIiHJt1mG>GC|+Sfr!_HzlNXKYay?jq}eoFE9g#t8q3hTVt)7$UEwa3
zt!W&f3E66F=#bR@q+>z`=|hZfyF72N{5Nd;cf%WG1l|;XRkwoXWJ9_}&?{g0%2{g!
z%5U@4)VDVo*TM%e*M{D##@z8hj6vvvYEEd7{zTjdUek3xVBgF94t_`aB3GkUN&md?
zY#Ogd+dLPbGdGg`fxo&nmSXJdXdCma`jP$4NwMGQ-ujTnBH4y)#D440#{vJIpTCQS
zFfGT8*R2yM#%O&|YXw^;T-?v|^M%>AVs-2`GNg$XeM#f)?R@Xh1+gE<Aa=I<R<WPX
zM-%6TasCJynZC$EaZ1O8j$jL@S*yk<aveZtE=l7qX)b_m8vd2{PWrc3UQFc<{x`O%
z-VeFsJ>@yNY0w(<alpS3!T+&NnB1G^4?n<K!F&L(!wx&F^aIcz?$kQ-*_w{QxE_5O
zIpMl`;200Gicb=I0_hK#WL&#xRqR*&KJt$LfhKqZU#73%m()jT%0KL;|I>7gv4}jd
z7*lmFNXS?3WNcmC9X{?x1~>+v)%hG`xp+g|53i9s<d8fdorDLX{*>I(_g>q$;9tMd
zgC8JXP4}^1J@c{09$T&r#s;YK1IR#FZPK@e-G~Rdep(ks2B+iVdy3CA7DK0Z8W(>v
z24Qa)Q#I%j>xW~!kk0K;4<_#9mGWrJTM2zZMmo>+s&hhm<&%YUyfJ}G_Gq;hL=GUv
z-aCxKWOaPhT#Mu2>2&;k_M5*sCUVW^w`{l=bNm!z=pKQ6{$`WdH>Tr+fBk_Mxhfc<
zbHzTa703^W^}?;?aolmol`#TwFl*t8*@)YrZBs316JwO(GUb67AG`sB{PC(^R+R(z
z!1YsqGxWG;j#%Cqt-8zV+!OIs@&bQ)?sj#-o%B=6lTFcUny!OBbuu90Ea~{iXLcG{
zZM@i~beK94V<es|u19vW@9=EIZOG}+DJkwdVV~@8Vhw`e@u)E-hsZL0iszz(M-@*F
zy0f9_EOaDaPdVk3()PDkfY;%NA71(a{x(cM0DWMsDg)35U4lmGyRqcEunpPlV(^}Y
zE^!?%F&lmkdXwoe59{<j9`K@D*cIuTE{j@mL2{1%F#h;}&EOu@`mi`Z{08yFZq|gE
zCL9m)EO?PGP-h1it8~n(pJNO=y;skv`;H%~JP>2pug#y*WZdwtZ*(}lNG6e!<Pq6Q
z=ZgWRe!x55`OXq&aKujMOEu{S;DI_B5Hhlz45%B+Zhs5xletyw^N-jPXe8&P2Gw(?
zHDam#$R?q)*iz(;cm{r^+j5(m0wDupZ3r~1TYs<HvF+u!u;17T#&?i7f4e6gNBrw6
z{;tLX=rgf^0}ni~lmX!fkPYz2M<+AL4&&BT3mH)L*HRe(v%QRMyJJ)A*VUSaAE8fC
zCsxf@M;^en$O3Bxu#wPbkQ_x*^qjQo1g)zv8uk|Zla6jTp1uAa<L|oS0qL4!?hmg`
z^7gpmACKr8Uzfi`=CXs*{ln!0<Ob9V(Q|Yq`OGG6y4DrVVK1!>;;*}>ex*9M*D+6z
zugd(utD1yrK0$xT$VDxmAk7)D8^SIbrSCRrxbC&TbW*pufpko}`5d?>yT!E9`8;CV
z>^1jqx{WjbVa{_$EI^Kp->nW%9uWS;1jPo$2>H<LTz)Qljepd1GX{Kbeg<13>coth
zv16ljGv4Vj#vV>R8=ImoUSB8k{TBQvwosR=a&5k<x<N9Od~_`OIJfS)P19_eu7_Ua
zFxtUo)%G%mVGp2vI?mP4@pnAqyuiI!5#DnBx*S52^sK{w@Us3zAEVYS>PhKXx~EBB
zyY9K*O&(6nj1R=-q9fBi#3Ls->{vkb4-a^5HU=D}IU>Hc*k8zjpkH<D<2&3tK5Bcy
zM-P1BwN4m~@k1YcbABrh665C{?(ZIvJBnJx7(X^dZv9gLACLv~fiZ}AqilCH=bNUy
zU_5-s!|X=Kk@wD%%Xb~u3qP;_o^^L7{^Cc9C5b1C4ark@7JTg`W~6UoO=J?><9)n0
z44b6dbA9Cdh<}hjbQ#`0=9puOSH0?0Gqr(a0=l9pS>YL)tPQO8%Xio`R_yc0$AwLx
zet{nsJP@*get>y-BK3K4g)xUve|s5w^fwO1!?+k9<7B*yTZ|!Eu%q<RIP0%*N88+*
zp+M*b<H2r4V`Bo-=pHZMje)U-b>m>nM}ehgo^<qAK_B!}AIUdTb7y@*Ym?FcasuWE
zi3PxxoR_@#qLy!r#-nP+P+yIy`{DudNaHPj=!GYYJ<PK2@C!ZCNzad}|M@<d0^>*l
z*nlbX9OS#{O>wTbzV)r;TEA)n!~%{y^2l<ZFSZqb*RioyK*rZNj%@Bzra*TTfL}V3
zPUk1n@pL_%A9Dow18;c48%qCxpTM``H_*G;*6ohY`KU~Raiu`W0P!m_K>khcj|@2Y
z;DgJYfcgM)hxyrjaCthuFuObZSmQdfxnG$A-BBRofnsJ64;14T4?N(21IkzcS#ZJ$
zClqJ2a$o9b<i^Cr_~Y3R=#I|$s7!%zrGR)@<OI}6MNZ(2Z+v5!6A%xGxxz6=KwMr7
zTx>SWfN>q!+^<Z5?kGS8@ay^aWPp5{99t>_%o8$qC}e;<D;Xf}o?E6srod!V0FLFm
z;o3Y2IF}QEdp5xS`|n@M19E|UAScw+%E6K!xn&Au3QR5q*zswNaNT@iIRQ2S8Ng2n
z8Nh$YGGKDYKKDFR00qbZc0kM%AOq5Q0_*^BK{7zyV8{URLoy)e1v4LI3QRTy!VXZo
ztPVb%E2J)v43HCe%Uj-3<_qNn<PYT$b53Bg$3FKyQve0z_RY^VCri%XJQy_qY5}Yj
zP@ONVekf#sx}uyD$b6J3FxeDf%aZ}>eL@D9Zxwq9q%y#K-H-v+RLU}7vd2F6K2rb%
zQaix<VPOYYKTK_)`9epx)(o(}fO$mL3^1?A{Gws38IVUMQy^1dv=o5jy0wGM4-yNo
zr--?OAp^`Q3>jelUT&EJnF7sI04~i@SF>X+3Ul?<?_1M{PCnv@Bl>#5b-a!`>ZtOw
z8UXWx4m#+dQtzt;h&9B`7nT!HGpu&lJOQ=DasuY+=6M3mkA1#krhvKb=I~mtF6QuB
z(+}?HWw;#GtFA79T*^7Y%%_<G!=XUN|8VG<ubL?^2^7fqpM+7$easXX4h1s)heOwV
z)l7j&pg_j|B#ct-W2V4xC}6&gwT7*4txiuIP@Rssd-C++@S}RkJ;?oAe^>5LEpV*A
zoa6t)8UK9MOo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+
zOo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+
zOo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+
zOo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+Oo2>+;ZR_S+%g3+1u_LP1u_LP1u_LP
z1u_LP1u_LP1u_LP1u_LP1u_LP1u_LP1u_LP1u_LP1u_LP1u_LP1u_LP1ty*X{||yh
B+I9c{

literal 1150
zcma)*%TE(g6vnU7gy=>RBP?8C;zn5#cdlI;{{h0Iv=u0nKA?abO-yjfBXnp>z^1lJ
zu`QPJh!6$EMA_&{*^-zD?ezVcPKzAB3*3a%5NY%E%$)Q6PVYT;hOrCy*48ro-ejGZ
z7^`E9-G-1t<~WbQ7;5MJ><@mr2W>-Zam%~L^K$Su#B3WcqP7i+4ZJ7ZQ_8qV`A*-s
z+UBS~tB=|uUC!&hPgBNlnftlYJt4|=`6;jWkkalVwN=VN!_bxLJ{iO7ol?6K9tSC!
z87gXKzZG<z`^AnKt=cAK)D}BxToTuZYJPucl@jJ46?C%)ipFU?T`xCJ)PZrFu$=#O
zy3Vg-w&h}MV2R{`F!me@BrL%P)gExlNsk}ZwQP@fByL^Uj9cFxts`j)P|7s_HPtuw
zP<W5`aJ2p%krT1fHvYZ%$mL5L=SINFX``Pqy*|nrJdiu0=v;TpEp|WV%-5e4oQR6f
z`A5+?sHmTgfV&2b<-6Qe=)^4b)1>Gz+YZT#S%%RgA~+G@OXj=Xlr_9mL=IdNdWKc4
zL}kq+ct~sCRW<fW;Smw*OlEptQL4}XTjWFp+oWnHz1SmF+Q)g1oT!P2eS@8+r0Gp1
zX%0?djyFV&$Al&H9K8=q+9^6{9nGJv@oR8DljgwD>~VGke<5xScjHW(akg^QwnBTu
zYZM(^C7jjvzkHwu%;SV*A;-@UEXn;`i+B$r&@x4B%h|o*^`ZU2)tYKv+=H!`t>H1b
zKZ0jz3EUFrpp{YRy72uk;y)X~t%KpZbC&<`31g)O#&)kV_VF2GU$z+A!qdIUb&Ijj
R7m?%ILLTCtG`yVQ>|c^jwR->n

diff --git a/src/Ui/media/img/favicon.psd b/src/Ui/media/img/favicon.psd
deleted file mode 100644
index 1eea4ccf8cec882098b1795cc3d10d31ee5564f8..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 52520
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
B;*<aY

diff --git a/src/Ui/media/img/logo-white.svg b/src/Ui/media/img/logo-white.svg
deleted file mode 100644
index a6d85c74..00000000
--- a/src/Ui/media/img/logo-white.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg version="1.1" viewBox="0 0 2050 2050" xmlns="http://www.w3.org/2000/svg"><g fill="white"><path d="m299 1211v-787.6l725.7-423.4 725.3 420-175.34 325.7-549.86-340.7-373.5 221v381.2z"/><path d="m1749.4 842.6v787.6l-725.4 423.3-724.5-419.5 219.38-278.79 505.12 293.39 373.2-221.2v-381z"/><path d="M299.5 1634L1750 786.4V420L299 1267.4z"/></g></svg>
diff --git a/src/Ui/media/img/logo.png b/src/Ui/media/img/logo.png
index a39cfc413525b04899e8137c7e24257fb8b10879..f228339b41e99b606a04f92209cee2bdf4bdd698 100644
GIT binary patch
literal 45820
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
YHiLQd&)(7QE#RZ3q^0;?-YV?>0m^{7RR910

literal 11379
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

diff --git a/src/Ui/media/img/logo.psd b/src/Ui/media/img/logo.psd
deleted file mode 100644
index 1cf6f35f3892c8028770669cf6cc5cdec8e8d2f8..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 62132
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
Iod@v$0UqI1d;kCd

diff --git a/src/Ui/media/img/logo.svg b/src/Ui/media/img/logo.svg
deleted file mode 100644
index fa79bfc8..00000000
--- a/src/Ui/media/img/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg version="1.1" viewBox="0 0 2049.3 2053.5" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="c" x1="1524.9" x2="520.7" y1="706" y2="1333.5" gradientUnits="userSpaceOnUse"><stop stop-color="#9563f9" offset="0"/><stop stop-color="#b073ec" offset="1"/></linearGradient><linearGradient id="a" x1="393.5" x2="1020.1" y1="961.9" y2="367.9" gradientUnits="userSpaceOnUse"><stop stop-color="#9764f8" offset="0"/><stop stop-color="#b576e9" offset="1"/></linearGradient><linearGradient id="b" x1="979.5" x2="947.9" y1="1540.5" y2="1984.5" gradientUnits="userSpaceOnUse"><stop stop-color="#965ff9" offset="0"/><stop stop-color="#c076e9" offset="1"/></linearGradient></defs><g fill-rule="evenodd"><path d="m299 1211v-787.6l725.7-423.4 725.3 420-175.34 325.7-549.86-340.7-373.5 221v381.2z" fill="url(#a)"/><path d="m1749.4 842.6v787.6l-725.4 423.3-724.5-419.5 219.38-278.79 505.12 293.39 373.2-221.2v-381z" fill="url(#b)"/><path d="M299.5 1634L1750 786.4V420L299 1267.4z" fill="url(#c)"/></g></svg>

From 66edfaab1a061c916b1c1de3b718f20fa5f50e71 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 15 May 2022 15:37:56 +0400
Subject: [PATCH 058/333] minor README update

- mention mac os x
- change addresses
---
 README.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index 95657a17..80a0ae75 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ brand new, completely transparent and audited network is ready and this project
 
 * After starting `zeronet.py` you will be able to visit zeronet sites using
   `http://127.0.0.1:43110/{zeronet_address}` (eg.
-  `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`).
+  `http://127.0.0.1:43110/126NXcevn1AUehWFZLTBw7FrX1crEizQdr`).
 * When you visit a new zeronet site, it tries to find peers using the BitTorrent
   network so it can download the site files (html, css, js...) from them.
 * Each visited site is also served by you.
@@ -64,7 +64,7 @@ Following links relate to original ZeroNet:
 
 #### System dependencies
 
-##### Generic unix-like
+##### Generic unix-like (including mac os x)
 
 Install autoconf and other basic development tools, python3 and pip.
 
@@ -108,7 +108,7 @@ Install autoconf and other basic development tools, python3 and pip.
 
 ## How can I create a ZeroNet site?
 
- * Click on **⋮** > **"Create new, empty site"** menu item on the site [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D).
+ * Click on **⋮** > **"Create new, empty site"** menu item on the [admin page](http://127.0.0.1:43110/126NXcevn1AUehWFZLTBw7FrX1crEizQdr).
  * You will be **redirected** to a completely new site that is only modifiable by you!
  * You can find and modify your site's content in **data/[yoursiteaddress]** directory
  * After the modifications open your site, drag the topright "0" button to the left, then press **sign** and **publish** buttons on the bottom

From 756082b7c843a47a95f2bc90b830d3a2215bd6cd Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 15 May 2022 16:03:48 +0400
Subject: [PATCH 059/333] temporary top-right button image replacement

---
 src/Ui/template/wrapper.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html
index fe5a3f9c..33925c67 100644
--- a/src/Ui/template/wrapper.html
+++ b/src/Ui/template/wrapper.html
@@ -38,7 +38,7 @@ else if (window.opener && window.opener.location.toString()) {
 
 <!-- Fixed button -->
 <div class='fixbutton'>
- <div class='fixbutton-text'><img width=30 src='/uimedia/img/logo-white.svg'/></div>
+ <div class='fixbutton-text' style='font-size:150%'>&#x2190;</div>
  <div class='fixbutton-burger'>&#x2261;</div>
  <a class='fixbutton-bg' href="{homepage}/"></a>
 </div>

From 69670df72be395159e23c7e59f234e3952ac99f1 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 15 May 2022 18:31:40 +0400
Subject: [PATCH 060/333] update changelog

---
 CHANGELOG.md | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0760986a..76f50103 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,15 @@
-### zeronet-conservancy 0.7.4 (2022-04-25)
+### zeronet-conservancy 0.7.4+
+maintainers: @caryoscelus , @d4708
+- new icon & minor rebranding
+- don't check port in tor-only mode
+- documentation updates & fixes (by @caryoscelus and @d4708)
+- fix compatibility with modern hashlib (affects Android/Termux & others)
+- cleanup
+- more active trackers
+- replace old start script with a better one
+- colourful greetings
+
+### zeronet-conservancy 0.7.4 (2022-04-25) (733b1b05b1)
 maintainers: @caryoscelus
 - fix UiRequest.parsePath & minor code improvements
 - Sidebar "Open site directory" feature (by @defder-su)

From a58959997f5178c913b05ace70f73dce369d9f09 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 15 May 2022 20:09:49 +0400
Subject: [PATCH 061/333] update Android/Termux instructions

mention binutils termux dependency
---
 README.md | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index 80a0ae75..a5acd81c 100644
--- a/README.md
+++ b/README.md
@@ -73,10 +73,9 @@ Install autoconf and other basic development tools, python3 and pip.
  - `sudo apt install pkg-config python3-pip python3-venv`
 
 ##### Android/Termux
- - install [Termux](https://termux.com/)
- - in Termux install via `pkg install <package-names>`
+ - install [Termux](https://termux.com/) (in Termux you can install packages via `pkg install <package-names>`)
  - `pkg update`
- - `pkg install python automake git` (TODO: check fresh installation whether there are more dependencies to install)
+ - `pkg install python automake git binutils` (TODO: check fresh installation whether there are more dependencies to install)
  - (optional) `pkg install tor`
  - (optional) run tor via `tor --ControlPort 9051 --CookieAuthentication 1` command (you can then open new session by swiping to the right)
 

From c1a526d7e225b54f0fcebfcfc9be02bee3dd05ac Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 17 May 2022 00:12:20 +0400
Subject: [PATCH 062/333] Update README.md

---
 README.md | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index a5acd81c..652cb36d 100644
--- a/README.md
+++ b/README.md
@@ -138,8 +138,10 @@ need to know their alternatives.
 
 ### Financially support maintainers
 
-Currently the only maintainer of this fork is @caryoscelus. You can see ways to
-donate to them on https://caryoscelus.github.io/donate/
+Currently the lead developer / maintainer of this fork is @caryoscelus. You can
+see ways to donate to them on https://caryoscelus.github.io/donate/ (or check
+sidebar if you're reading this on github for more ways). As our team grows, we
+will create team accounts on friendly crowdfunding platforms as well.
 
 If you want to make sure your donation is recognized as donation for this
 project, there is a dedicated bitcoin address for that, too:

From d5d5163a654979c9ac9d6c586e6bf84719d82ce2 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 17 May 2022 02:40:22 +0400
Subject: [PATCH 063/333] remove unused code

---
 src/Ui/UiRequest.py | 14 +-------------
 1 file changed, 1 insertion(+), 13 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 960da168..90329a8b 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -881,20 +881,8 @@ class UiRequest(object):
             return [b"No error! :)"]
 
     # Just raise an error to get console
+    # Is this even useful anymore?
     def actionConsole(self):
-        import sys
-        sites = self.server.sites
-        main = sys.modules["main"]
-
-        def bench(code, times=100, init=None):
-            sites = self.server.sites
-            main = sys.modules["main"]
-            s = time.time()
-            if init:
-                eval(compile(init, '<string>', 'exec'), globals(), locals())
-            for _ in range(times):
-                back = eval(code, globals(), locals())
-            return ["%s run: %.3fs" % (times, time.time() - s), back]
         raise Exception("Here is your console")
 
     # - Tests -

From c5e8841d0f3cfbb17add9dfed847f1a14de0c029 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 17 May 2022 17:45:26 +0400
Subject: [PATCH 064/333] disable UPnP port-punching until it's proven robust

---
 src/Peer/PeerPortchecker.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/Peer/PeerPortchecker.py b/src/Peer/PeerPortchecker.py
index 3c4daecf..1063cca5 100644
--- a/src/Peer/PeerPortchecker.py
+++ b/src/Peer/PeerPortchecker.py
@@ -28,7 +28,8 @@ class PeerPortchecker(object):
         return urllib.request.urlopen(req, timeout=20.0)
 
     def portOpen(self, port):
-        self.log.info("Trying to open port using UpnpPunch...")
+        self.log.info("Not trying to open port using UpnpPunch until it's proven robust...")
+        return False
 
         try:
             UpnpPunch.ask_to_open_port(port, 'ZeroNet', retries=3, protos=["TCP"])

From fca3c8544debd533b59b1dcdc1cd5bc66812294c Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 17 May 2022 18:03:20 +0400
Subject: [PATCH 065/333] v0.7.5

(and changelog update)
---
 CHANGELOG.md  | 4 +++-
 src/Config.py | 4 ++--
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76f50103..539bb8c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,10 @@
-### zeronet-conservancy 0.7.4+
+### zeronet-conservancy 0.7.5 (2022-05-17)
 maintainers: @caryoscelus , @d4708
+- disable UPnP until it's proven robust
 - new icon & minor rebranding
 - don't check port in tor-only mode
 - documentation updates & fixes (by @caryoscelus and @d4708)
+- update Android/Termux dependency list (thx to nnmnmknmki reports)
 - fix compatibility with modern hashlib (affects Android/Termux & others)
 - cleanup
 - more active trackers
diff --git a/src/Config.py b/src/Config.py
index ca7bed05..8a497c57 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -13,9 +13,9 @@ import time
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.4+"
+        self.version = "0.7.5"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5004
+        self.rev = 5010
         self.argv = argv
         self.action = None
         self.test_parser = None

From 98e51e4c73e35290faa43a4ef6618496d94b72e7 Mon Sep 17 00:00:00 2001
From: d4708 <96319366+d4708@users.noreply.github.com>
Date: Thu, 19 May 2022 13:53:31 +0300
Subject: [PATCH 066/333] visual updates for the top-right button #50, #58

---
 src/Ui/media/all.css | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css
index bd54cf34..db3b6987 100644
--- a/src/Ui/media/all.css
+++ b/src/Ui/media/all.css
@@ -38,14 +38,26 @@ a { color: black }
 	text-align: center; color: white; font-family: Consolas, Monaco, monospace; font-size: 25px;
 }
 .fixbutton-bg {
-	-webkit-border-radius: 80px; -moz-border-radius: 80px; -o-border-radius: 80px; -ms-border-radius: 80px; border-radius: 80px ; background-color: rgba(180, 180, 180, 0.5); cursor: pointer;
+	-webkit-border-radius: 80px; -moz-border-radius: 80px; -o-border-radius: 80px; -ms-border-radius: 80px; border-radius: 80px ; background-color: rgba(180, 180, 180, 0.5);
 	display: block; width: 80px; height: 80px; -webkit-transition: background-color 0.2s, box-shadow 0.5s; -moz-transition: background-color 0.2s, box-shadow 0.5s; -o-transition: background-color 0.2s, box-shadow 0.5s; -ms-transition: background-color 0.2s, box-shadow 0.5s; transition: background-color 0.2s, box-shadow 0.5s ; -webkit-transform: scale(0.6); -moz-transform: scale(0.6); -o-transform: scale(0.6); -ms-transform: scale(0.6); transform: scale(0.6) ; margin-left: -20px; margin-top: -20px; /* 2x size to prevent blur on anim */
 	/*box-shadow: inset 105px 260px 0 -200px rgba(0,0,0,0.1);*/ /* -webkit-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); -moz-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); -o-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); -ms-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1) ; */
+
+	/* #50, #58 */
+	cursor: move;
+	background-color: #FFF;
+	background-image: url('img/logo.png');
+	background-size: 60px;
+	background-position-x: 10px;
+	background-position-y: 10px;
+	background-repeat: no-repeat;
 }
+
+/* #50, #58 @TODO remove
 .fixbutton-text { pointer-events: none; position: absolute; z-index: 999; width: 40px; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; -webkit-perspective: 1000px; -moz-perspective: 1000px; -o-perspective: 1000px; -ms-perspective: 1000px; perspective: 1000px ; line-height: 0; padding-top: 5px; opacity: 0.9 }
 .fixbutton-burger { pointer-events: none; position: absolute; z-index: 999; width: 40px; opacity: 0; left: -20px; font-size: 40px; line-height: 0; font-family: Verdana, sans-serif; margin-top: 17px }
 .fixbutton-bg:hover { background-color: #AF3BFF }
 .fixbutton-bg:active { background-color: #9E2FEA; top: 1px; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none  }
+*/
 
 /* Notification */
 

From 970dda15c72fc97f8fa24a8049f8a47c92b887cf Mon Sep 17 00:00:00 2001
From: d4708 <96319366+d4708@users.noreply.github.com>
Date: Thu, 19 May 2022 13:57:38 +0300
Subject: [PATCH 067/333] visual updates for the top-right button #50, #53

---
 src/Ui/template/wrapper.html | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html
index 33925c67..f65c5066 100644
--- a/src/Ui/template/wrapper.html
+++ b/src/Ui/template/wrapper.html
@@ -38,8 +38,10 @@ else if (window.opener && window.opener.location.toString()) {
 
 <!-- Fixed button -->
 <div class='fixbutton'>
+ <!-- #50, #53 @TODO remove
  <div class='fixbutton-text' style='font-size:150%'>&#x2190;</div>
  <div class='fixbutton-burger'>&#x2261;</div>
+ -->
  <a class='fixbutton-bg' href="{homepage}/"></a>
 </div>
 

From a5f18c22b32d1e617d41ee4687cbd8277cef8aa2 Mon Sep 17 00:00:00 2001
From: d4708 <96319366+d4708@users.noreply.github.com>
Date: Thu, 19 May 2022 13:58:41 +0300
Subject: [PATCH 068/333] fix issues relation #50, #53

---
 src/Ui/media/all.css | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css
index db3b6987..a1ce9104 100644
--- a/src/Ui/media/all.css
+++ b/src/Ui/media/all.css
@@ -42,7 +42,7 @@ a { color: black }
 	display: block; width: 80px; height: 80px; -webkit-transition: background-color 0.2s, box-shadow 0.5s; -moz-transition: background-color 0.2s, box-shadow 0.5s; -o-transition: background-color 0.2s, box-shadow 0.5s; -ms-transition: background-color 0.2s, box-shadow 0.5s; transition: background-color 0.2s, box-shadow 0.5s ; -webkit-transform: scale(0.6); -moz-transform: scale(0.6); -o-transform: scale(0.6); -ms-transform: scale(0.6); transform: scale(0.6) ; margin-left: -20px; margin-top: -20px; /* 2x size to prevent blur on anim */
 	/*box-shadow: inset 105px 260px 0 -200px rgba(0,0,0,0.1);*/ /* -webkit-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); -moz-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); -o-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); -ms-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1) ; */
 
-	/* #50, #58 */
+	/* #50, #53 */
 	cursor: move;
 	background-color: #FFF;
 	background-image: url('img/logo.png');
@@ -52,7 +52,7 @@ a { color: black }
 	background-repeat: no-repeat;
 }
 
-/* #50, #58 @TODO remove
+/* #50, #53 @TODO remove
 .fixbutton-text { pointer-events: none; position: absolute; z-index: 999; width: 40px; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; -webkit-perspective: 1000px; -moz-perspective: 1000px; -o-perspective: 1000px; -ms-perspective: 1000px; perspective: 1000px ; line-height: 0; padding-top: 5px; opacity: 0.9 }
 .fixbutton-burger { pointer-events: none; position: absolute; z-index: 999; width: 40px; opacity: 0; left: -20px; font-size: 40px; line-height: 0; font-family: Verdana, sans-serif; margin-top: 17px }
 .fixbutton-bg:hover { background-color: #AF3BFF }

From 8d2c5990cd06c4eb47950b484a82aad7168882ed Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 21 May 2022 21:14:37 +0400
Subject: [PATCH 069/333] fuller Debug messages

`limit` argument is removed as unused
---
 src/Debug/Debug.py | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/src/Debug/Debug.py b/src/Debug/Debug.py
index 0ec42615..96b0f9c3 100644
--- a/src/Debug/Debug.py
+++ b/src/Debug/Debug.py
@@ -35,7 +35,7 @@ root_dir = os.path.realpath(os.path.dirname(__file__) + "/../../")
 root_dir = root_dir.replace("\\", "/")
 
 
-def formatTraceback(items, limit=None, fold_builtin=True):
+def formatTraceback(items, fold_builtin=False):
     back = []
     i = 0
     prev_file_title = ""
@@ -101,10 +101,6 @@ def formatTraceback(items, limit=None, fold_builtin=True):
 
         prev_file_title = file_title
         is_prev_builtin = is_builtin
-
-        if limit and i >= limit:
-            back.append("...")
-            break
     return back
 
 
@@ -131,9 +127,9 @@ def formatException(err=None, format="text"):
         return "%s: %s in %s" % (exc_type.__name__, err, " > ".join(tb))
 
 
-def formatStack(limit=None):
+def formatStack():
     import inspect
-    tb = formatTraceback([[frame[1], frame[2]] for frame in inspect.stack()[1:]], limit=limit)
+    tb = formatTraceback([[frame[1], frame[2]] for frame in inspect.stack()[1:]])
     return " > ".join(tb)
 
 

From 9c047f5de309aafe069979561b8e501412fb9912 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 21 May 2022 22:45:34 +0400
Subject: [PATCH 070/333] Update LICENSE

- place new contributions under GPLv3+
- clarify dubious legality of CC-BY-SA part of licensing and stop its action (if any) to avoid any further confusion
---
 LICENSE | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/LICENSE b/LICENSE
index 0d17b72d..9b5df6d9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -11,7 +11,16 @@ You should have received a copy of the GNU General Public License
 along with this program. If not, see <https://www.gnu.org/licenses/>.
 
 
-Additional Conditions :
+All contributions after commit 2a4927b77b55dea9c40d14ce2cfa8a0bf0a90b80
+(and/or made after 2022-05-22 UTC 00:00:00) can additionally be used
+under GNU General Public License as published by the Free Software
+Foundation, version 3 or (at your option) any later version.
+
+
+Additional Conditions (please note that these are likely to not be legally bounding,
+but before this is cleared we're leaving this note) applying to contributions from
+commit 1de748585846e935d9d88bc7f22c69c84f7b8252 till commit
+2a4927b77b55dea9c40d14ce2cfa8a0bf0a90b80:
 
 Contributing to this repo
  This repo is governed by GPLv3, same is located at the root of the ZeroNet git repo, 

From 267633ee0631d792d6200e3e9c24ec41362a12b2 Mon Sep 17 00:00:00 2001
From: d4708 <96319366+d4708@users.noreply.github.com>
Date: Mon, 23 May 2022 14:41:11 +0300
Subject: [PATCH 071/333] add fixbutton cursor

---
 src/Ui/media/img/fixbutton.png | Bin 0 -> 488 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 src/Ui/media/img/fixbutton.png

diff --git a/src/Ui/media/img/fixbutton.png b/src/Ui/media/img/fixbutton.png
new file mode 100644
index 0000000000000000000000000000000000000000..db74dbe0845df921653be9dca3f5ce1663875c02
GIT binary patch
literal 488
zcmV<E0T=#>P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80000PbVXQnLvL+u
zWo~o;Lvm$dbY)~9cWHEJAV*0}P*;Ht7XSbOib+I4R9M69)iG;SK@bP<-yR9(3kZUh
zNu>cRzs75}vN#pQ)?QGsNzmS2FfrP_X1|6Y1Vk^v*ok1T(Ioe5RydyTJum0oHNJnf
z?99&pKf}x{J$n4_2=fY57S-zSb4=l6X#z6wCWaW~>sQ#1Dg69r0cBCG;~{Rx6jmF^
zZ{jg7V+E)9j3?NQDg5Y)Kj<PZi|RQ>xSNC5aUD;wfwk<jhIMS=Mp;yyNv<>yZ{&<`
zwUAC`e;=1xTrOt&TYQ^Z06ugP*WPY+RB?c-xX>k_>G3@rE^7?`5D-&%k(J-ULC4!u
zyu-M~KE`{TzX@XsXEB8xT+6*MYVbM1Gkj@@{WaUa&ol)SLSAMe!v>dqJitdB=c*jz
z2#@e4rcljo5tT(Xh$)Phx`-CNihC>^Wl;?l)&pfxUCFzmZF3G|3VSn;;olHb_=KGn
z^9Zlcy$NT)cAoz_w(29h5CJiTBkb0$52nd}UKNo4{>dtA#}v+%CZJY@L+s7`8_uB@
erp~@c5BLQL!Ff@XtS1`)0000<MNUMnLSTZb-`2JO

literal 0
HcmV?d00001


From e495589534e1f4034cbbb71eb15a87089e7fda21 Mon Sep 17 00:00:00 2001
From: d4708 <96319366+d4708@users.noreply.github.com>
Date: Mon, 23 May 2022 14:42:29 +0300
Subject: [PATCH 072/333] change fixbutton cursor

---
 src/Ui/media/all.css | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css
index a1ce9104..0dd0eaea 100644
--- a/src/Ui/media/all.css
+++ b/src/Ui/media/all.css
@@ -43,7 +43,7 @@ a { color: black }
 	/*box-shadow: inset 105px 260px 0 -200px rgba(0,0,0,0.1);*/ /* -webkit-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); -moz-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); -o-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); -ms-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1) ; */
 
 	/* #50, #53 */
-	cursor: move;
+	cursor: url('img/fixbutton.png') 4 12, auto;
 	background-color: #FFF;
 	background-image: url('img/logo.png');
 	background-size: 60px;

From ff43c838e4fbf2e54c0698514f55421fd1bcf8f1 Mon Sep 17 00:00:00 2001
From: d4708 <96319366+d4708@users.noreply.github.com>
Date: Mon, 23 May 2022 18:06:13 +0300
Subject: [PATCH 073/333] Delete fixbutton.png

---
 src/Ui/media/img/fixbutton.png | Bin 488 -> 0 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 src/Ui/media/img/fixbutton.png

diff --git a/src/Ui/media/img/fixbutton.png b/src/Ui/media/img/fixbutton.png
deleted file mode 100644
index db74dbe0845df921653be9dca3f5ce1663875c02..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 488
zcmV<E0T=#>P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80000PbVXQnLvL+u
zWo~o;Lvm$dbY)~9cWHEJAV*0}P*;Ht7XSbOib+I4R9M69)iG;SK@bP<-yR9(3kZUh
zNu>cRzs75}vN#pQ)?QGsNzmS2FfrP_X1|6Y1Vk^v*ok1T(Ioe5RydyTJum0oHNJnf
z?99&pKf}x{J$n4_2=fY57S-zSb4=l6X#z6wCWaW~>sQ#1Dg69r0cBCG;~{Rx6jmF^
zZ{jg7V+E)9j3?NQDg5Y)Kj<PZi|RQ>xSNC5aUD;wfwk<jhIMS=Mp;yyNv<>yZ{&<`
zwUAC`e;=1xTrOt&TYQ^Z06ugP*WPY+RB?c-xX>k_>G3@rE^7?`5D-&%k(J-ULC4!u
zyu-M~KE`{TzX@XsXEB8xT+6*MYVbM1Gkj@@{WaUa&ol)SLSAMe!v>dqJitdB=c*jz
z2#@e4rcljo5tT(Xh$)Phx`-CNihC>^Wl;?l)&pfxUCFzmZF3G|3VSn;;olHb_=KGn
z^9Zlcy$NT)cAoz_w(29h5CJiTBkb0$52nd}UKNo4{>dtA#}v+%CZJY@L+s7`8_uB@
erp~@c5BLQL!Ff@XtS1`)0000<MNUMnLSTZb-`2JO


From 66981de74f0bcb483a069cb2e8e4ed089610f50c Mon Sep 17 00:00:00 2001
From: d4708 <96319366+d4708@users.noreply.github.com>
Date: Mon, 23 May 2022 18:07:24 +0300
Subject: [PATCH 074/333] add fixbutton cursors #53

---
 src/Ui/media/img/fixbutton-left-down.png  | Bin 0 -> 488 bytes
 src/Ui/media/img/fixbutton-left-up.png    | Bin 0 -> 470 bytes
 src/Ui/media/img/fixbutton-right-down.png | Bin 0 -> 466 bytes
 3 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 src/Ui/media/img/fixbutton-left-down.png
 create mode 100644 src/Ui/media/img/fixbutton-left-up.png
 create mode 100644 src/Ui/media/img/fixbutton-right-down.png

diff --git a/src/Ui/media/img/fixbutton-left-down.png b/src/Ui/media/img/fixbutton-left-down.png
new file mode 100644
index 0000000000000000000000000000000000000000..db74dbe0845df921653be9dca3f5ce1663875c02
GIT binary patch
literal 488
zcmV<E0T=#>P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80000PbVXQnLvL+u
zWo~o;Lvm$dbY)~9cWHEJAV*0}P*;Ht7XSbOib+I4R9M69)iG;SK@bP<-yR9(3kZUh
zNu>cRzs75}vN#pQ)?QGsNzmS2FfrP_X1|6Y1Vk^v*ok1T(Ioe5RydyTJum0oHNJnf
z?99&pKf}x{J$n4_2=fY57S-zSb4=l6X#z6wCWaW~>sQ#1Dg69r0cBCG;~{Rx6jmF^
zZ{jg7V+E)9j3?NQDg5Y)Kj<PZi|RQ>xSNC5aUD;wfwk<jhIMS=Mp;yyNv<>yZ{&<`
zwUAC`e;=1xTrOt&TYQ^Z06ugP*WPY+RB?c-xX>k_>G3@rE^7?`5D-&%k(J-ULC4!u
zyu-M~KE`{TzX@XsXEB8xT+6*MYVbM1Gkj@@{WaUa&ol)SLSAMe!v>dqJitdB=c*jz
z2#@e4rcljo5tT(Xh$)Phx`-CNihC>^Wl;?l)&pfxUCFzmZF3G|3VSn;;olHb_=KGn
z^9Zlcy$NT)cAoz_w(29h5CJiTBkb0$52nd}UKNo4{>dtA#}v+%CZJY@L+s7`8_uB@
erp~@c5BLQL!Ff@XtS1`)0000<MNUMnLSTZb-`2JO

literal 0
HcmV?d00001

diff --git a/src/Ui/media/img/fixbutton-left-up.png b/src/Ui/media/img/fixbutton-left-up.png
new file mode 100644
index 0000000000000000000000000000000000000000..9ffba108976f7d9cb31dee4bb649356e04e408eb
GIT binary patch
literal 470
zcmV;{0V)28P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80000PbVXQnLvL+u
zWo~o;Lvm$dbY)~9cWHEJAV*0}P*;Ht7XSbOcu7P-R9M69l|M@ZK@i1%dq&I`5Ckg|
z{2Q?HYizTXL1P+QdqKfQ(B4)H6)m^fuOSEl@gK1i!Cs@rTbl_4$(<K+xmet5?zq|6
zdG9eZ2RU-&7%@~@7g_D=z%j63oN6Y`3sNdR2J8R@d6FpsU=5g+_nOtdlo^38^&J#|
z-7Fob0&~B<C3YbgP<;@gHgPWmDX5>1h;3NyXNESvqCUsJ+JxhscjMGoW(1^g1z7KD
z>Pw(uocbIp0V%!;>;cm<er@0xH~`L#Qy&A3q1C=*wLh`i_p)^vI0VW-Q3w=)60i@f
zSnXp)7UBxalBxANC+fF=sfa5kC4LROB_?3CFUuCI#SxTPCC0@7SOg~eA^_a|Q8-of
zV!sJICdh3AH$WH>4}n{tza}(Jor-mJ;2|neX8j0wi8%5q@%O%`z&Q17oH_%Rq!;cZ
z@*BW5&;(lIN(*?B2EH;*`GNO<{CKLysf&>=BCCC1oC?!i#W_ZfA3wfxZhdB(761SM
M07*qoM6N<$g88bxSO5S3

literal 0
HcmV?d00001

diff --git a/src/Ui/media/img/fixbutton-right-down.png b/src/Ui/media/img/fixbutton-right-down.png
new file mode 100644
index 0000000000000000000000000000000000000000..f2e6c1885fb15c15f79aee9c4c91e657ede21c33
GIT binary patch
literal 466
zcmV;@0WJQCP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80000PbVXQnLvL+u
zWo~o;Lvm$dbY)~9cWHEJAV*0}P*;Ht7XSbObV)=(R9M69ma$3$K@f(&JtM{k5Ckg~
zjUrfxuW`**f<eLFXRx`Ty{#yE#B|&2n+O^bK|2wA0Exz|&89hTFL%j?6y9GQJOAGN
zGdnZ8P@=?thsZLj(993Nr54`~P9U`ycokqB?*Mfmu*>(rgVf?Hg?|XN#t|YM&JTby
zU<IfEec%l^2V$v3FR{k}9U!toe)0+kQr32X3t%0nS^-sH%{tgup-IbJ4Tym!6`CUz
znz_OPZ1ktVvV*^5?TZ=z+yricwhGO0(F|1V*#{Z?ZD1isa2vqYI7i?Eec%Sz8bz$6
zY<vXv9O4E*8|Y;?j)6#O@lv=3Y_DGe?+*TlwST5Kz6NSii<7+0y|tnDNmz6c&2$u(
zm<d#9f~nPj3e76eO_>ox)TI_pUjqI_BDHu0?i~CM(DIp0S1KE}_mhYwd3@v+U?*9e
zSnm3ce$-_YC9=>c^3a%AKq3pzKyw-$HL9J4wJ??FOOyck1<{py%Hoe(CjbBd07*qo
IM6N<$f@-+Ii~s-t

literal 0
HcmV?d00001


From a734c4b12f4b36b21aa2f63aea898c8852fa032f Mon Sep 17 00:00:00 2001
From: d4708 <96319366+d4708@users.noreply.github.com>
Date: Mon, 23 May 2022 18:09:44 +0300
Subject: [PATCH 075/333] add fixbutton combinations, cleanup filesize #53

---
 src/Ui/media/all.css | 20 +++++++++-----------
 1 file changed, 9 insertions(+), 11 deletions(-)

diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css
index 0dd0eaea..2d10bf85 100644
--- a/src/Ui/media/all.css
+++ b/src/Ui/media/all.css
@@ -31,7 +31,7 @@ a { color: black }
 .button.button-2 { background-color: transparent; border: 1px solid #EEE; color: #555 }
 .button.button-2:hover { border: 1px solid #CCC; color: #000 }
 
-/* Fixbutton */
+/* Fixbutton #50, #53 */
 
 .fixbutton {
 	position: absolute; right: 35px; top: 15px; width: 40px;  z-index: 999;
@@ -40,24 +40,22 @@ a { color: black }
 .fixbutton-bg {
 	-webkit-border-radius: 80px; -moz-border-radius: 80px; -o-border-radius: 80px; -ms-border-radius: 80px; border-radius: 80px ; background-color: rgba(180, 180, 180, 0.5);
 	display: block; width: 80px; height: 80px; -webkit-transition: background-color 0.2s, box-shadow 0.5s; -moz-transition: background-color 0.2s, box-shadow 0.5s; -o-transition: background-color 0.2s, box-shadow 0.5s; -ms-transition: background-color 0.2s, box-shadow 0.5s; transition: background-color 0.2s, box-shadow 0.5s ; -webkit-transform: scale(0.6); -moz-transform: scale(0.6); -o-transform: scale(0.6); -ms-transform: scale(0.6); transform: scale(0.6) ; margin-left: -20px; margin-top: -20px; /* 2x size to prevent blur on anim */
-	/*box-shadow: inset 105px 260px 0 -200px rgba(0,0,0,0.1);*/ /* -webkit-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); -moz-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); -o-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); -ms-box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1); box-shadow: inset -75px 183px 0 -200px rgba(0,0,0,0.1) ; */
 
-	/* #50, #53 */
-	cursor: url('img/fixbutton.png') 4 12, auto;
 	background-color: #FFF;
 	background-image: url('img/logo.png');
 	background-size: 60px;
 	background-position-x: 10px;
 	background-position-y: 10px;
 	background-repeat: no-repeat;
-}
 
-/* #50, #53 @TODO remove
-.fixbutton-text { pointer-events: none; position: absolute; z-index: 999; width: 40px; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; -webkit-perspective: 1000px; -moz-perspective: 1000px; -o-perspective: 1000px; -ms-perspective: 1000px; perspective: 1000px ; line-height: 0; padding-top: 5px; opacity: 0.9 }
-.fixbutton-burger { pointer-events: none; position: absolute; z-index: 999; width: 40px; opacity: 0; left: -20px; font-size: 40px; line-height: 0; font-family: Verdana, sans-serif; margin-top: 17px }
-.fixbutton-bg:hover { background-color: #AF3BFF }
-.fixbutton-bg:active { background-color: #9E2FEA; top: 1px; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none  }
-*/
+	cursor: url('img/fixbutton-left-down.png') 4 12, auto;
+}
+.body-sidebar .fixbutton-bg {
+	cursor: url('img/fixbutton-right-down.png') 4 12, auto;
+}
+.body-console .fixbutton-bg {
+	cursor: url('img/fixbutton-left-up.png') 4 12, auto;
+}
 
 /* Notification */
 

From a802972d14b07a6497e0ce49350c34218758bcf2 Mon Sep 17 00:00:00 2001
From: d4708 <96319366+d4708@users.noreply.github.com>
Date: Mon, 23 May 2022 18:27:58 +0300
Subject: [PATCH 076/333] add apple touch icon #63

---
 src/Ui/media/img/apple-touch-icon.png | Bin 0 -> 16672 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 src/Ui/media/img/apple-touch-icon.png

diff --git a/src/Ui/media/img/apple-touch-icon.png b/src/Ui/media/img/apple-touch-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..94d0df6e71c5f40259fcb00a7693f292ebb947a8
GIT binary patch
literal 16672
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
TWbdWIB@oIFH00mQnuh)tc}>NF

literal 0
HcmV?d00001


From 894af8127e8d4864925662dd3fce145dc9457f27 Mon Sep 17 00:00:00 2001
From: d4708 <96319366+d4708@users.noreply.github.com>
Date: Mon, 23 May 2022 18:42:03 +0300
Subject: [PATCH 077/333] add favicon with rounded corners

---
 src/Ui/media/img/favicon.ico | Bin 270398 -> 270398 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)

diff --git a/src/Ui/media/img/favicon.ico b/src/Ui/media/img/favicon.ico
index 240b9e4008fe687a218288fce9aa0dd583764b8c..38e78d7daa4460aaeda551481007ee822ef350f2 100644
GIT binary patch
delta 1622
zcmb_cF=$g!6!pLRU(=$cEm%lvOawC&bP^Xu3=aem92|uV8JfYNL%<AzLj$4driP|;
z6e9{j5FUX96iPu5irq4HY0223g*pgIzUSWmlKyXtI2gFRd(OM>{(J8`FPmG_n_JVb
zuC3qq7q0|CpkO&z8TJPD6}Aui4jWi=l`an)VwQI~ilcKdwQl^zG@Jx14{N~=t;y5m
zp+n3j5LUSWCkv~=`qn(9iM~TwY?pXD30r|7LzN~t1EVCii{o$#u%0z*q<f4&KW2I!
z*0AO+=>{Xv51GCN>sa%FbcYe>drUuoZCleOz0C;poguwr&1cd(j=nieg6J<{G$*;v
zG=CDgzlFK^lM6m5RM8`^0hnc2ux6J$t~14lxI2`;8<x*qB{^!vnm&0efEPK6M}+?S
zG{^mC&;9WOSF`4TW^2(6uzUj;>jBO1SQEot_<3uNX)+&STEeuL!9Gg>Upx*cE`qS;
zC+QscvNg|W?Nh$0MIa$6RJBMYM2l4|qN$s5yHi{xE%K?GrS``sFG-7#>ZaI!zqCQ#
z0A96OT5Xm*nCdo5^g81~Xh&Z6#tS;Hw8$-j`TTwaiwq-JB*}Q=?sGNXz1<{IQXWc2
z6I^zVy$Nn-)T+o+xdBAR;TRd&Omt+U@y^D^>*D1~=3-akc1G8!<vRJ6<CmU1TN){j
zIuVfQ=|o-&XNyp@$-0kAR+P-Tj|+UHdqhW`gS?W{k4N6r>8B<yuz=k0hgtmlbC%c6
zqdEJ}pz?2jDUJX02lj}9;0PBpKQ@Jmf(pCZgExSe@|T0)hd7xqEaf9?OBfvF=oBFu
zYg);Xv|9cpD97j0D&rRZMp{kn$&zpjCrT@FOGS_uT|Dod@h`<FHAL~ld$GjsYw;3X
mUECO|l$wV<_~?Q2rF=II>2uV%%tB)p;e5t;KH<~|<M<6X;ak=K

delta 409
zcmdmYKw#eifeG5e3M>rhU~{6u8RpFe27BpDY&@`)aq_O{W}wxz%#(M;!x+U6CLWO4
zye*}jdE$Y>&FeDyfpq5PWjWJ<bZYaWg7!rPj6lq^eNh4PeEE$BJ~D1M>SN|-BFvg@
kSIxw+-D(jtGt&V07i{Im1D;IV4>Ga*mD+xRiG`UJ00zyZU;qFB


From df3c0e6bbd4b6d7d7d234e3c2d3bfe48c076fc04 Mon Sep 17 00:00:00 2001
From: Programmator9000 <105989167+Programmator9000@users.noreply.github.com>
Date: Wed, 25 May 2022 22:49:10 +0400
Subject: [PATCH 078/333] Update README-ru.md

---
 README-ru.md | 16 +---------------
 1 file changed, 1 insertion(+), 15 deletions(-)

diff --git a/README-ru.md b/README-ru.md
index d20828ef..9b5912cb 100644
--- a/README-ru.md
+++ b/README-ru.md
@@ -1,8 +1,3 @@
-[English](./README.md)
-[简体中文](./README-zh-cn.md)
-
-(ОСТОРОЖНО: русскоязычная версия README может быть не полностью обновлена)
-
 # zeronet-conservancy
 
 Минималистичный форк [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) с
@@ -29,7 +24,6 @@ ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/compare/py3...zeronet-conse
 * Невозможно отключить: Он нигде, потому что он везде.
 * Быстр и работает оффлайн: Вы можете получить доступ к сайту, даже если Интернет недоступен.
 
-
 ## Особенности
  * Обновляемые в реальном времени сайты
  * Клонирование вебсайтов в один клик
@@ -62,18 +56,10 @@ ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/compare/py3...zeronet-conse
 ####  [Документация разработчика ZeroNet »](https://zeronet.io/docs/site_development/getting_started/)
 
 
-## Скриншоты
-
-![Screenshot](https://i.imgur.com/H60OAHY.png)
-![ZeroTalk](https://zeronet.io/docs/img/zerotalk.png)
-
-#### [Больше скриншотов в ZeroNet документации »](https://zeronet.io/docs/using_zeronet/sample_sites/)
-
+#### [Скриншоты в ZeroNet документации »](https://zeronet.io/docs/using_zeronet/sample_sites/)
 
 ## Как вступить
 
-(WIP)
-
 ### Install from source
 
  - clone this repo

From a86a364945fd6d40bec2285bc6bc3757e3c7165c Mon Sep 17 00:00:00 2001
From: Programmator9000 <105989167+Programmator9000@users.noreply.github.com>
Date: Thu, 26 May 2022 19:16:11 +0400
Subject: [PATCH 079/333] Update README-ru.md

---
 README-ru.md | 157 ++++++++++++++++++++++++++-------------------------
 1 file changed, 81 insertions(+), 76 deletions(-)

diff --git a/README-ru.md b/README-ru.md
index 9b5912cb..6486dbd3 100644
--- a/README-ru.md
+++ b/README-ru.md
@@ -1,22 +1,19 @@
 # zeronet-conservancy
 
-Минималистичный форк [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) с
-поддержкой onion-v3 tor (и возможных других необходимых фиксов и закрытий
-уязвимостей)
+[English](./README.md) [简体中文](./README-zh-cn.md)
 
-## Зачем форк?
+zeronet-conservancy — это форк/продолжение проекта [ZeroNet](https://github.com/HelloZeroNet/ZeroNet)
+(покинутого его создателем), предназначенный для поддержки существующей сети p2p и развития
+идей ценности децентрализации и свободы, постепенно развивающийся в более совершенную сеть
 
-Нам нужен форк работающий с onion-v3 и не зависящий от доверия к одному или двум
-личностям. Этот форк нужен прямо сейчас. Данный форк представляет из себя
-минимальный [сет изменений по сравнению с последним коммитом
-ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/compare/py3...zeronet-conservancy:master)
-, их легко проверить самостоятельно.
+## Зачем нужен этот форк?
 
-Этот форк является временной мерой и может закончиться, если/когда автор сего
-форка решит, что существует альтернативный, активный, заслуживающий доверия
-форк.
+Во время кризиса onion-v3 появилась необходимость в форке, который работал бы с onion-v3 и не зависел от доверия к конкретным личностям.
+Для выполнения этой задачи форк начался с внесения минимальных изменений в
+[ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3), которые легко проверяются. В то время как остается возможность использования ранних версий форка для работы с onion-v3, цель данного форка изменилась и мы стали стремиться решать больше проблем и повышать удобность и безопасность для пользователей до тех пор, пока новая, полностью прозрачная и проверенная сеть не будет готова, и необходимость в этом проекте не отпадет.
 
-## Зачем?
+
+## Зачем нужен 0net?
 
 * Мы верим в открытую, свободную, и не поддающуюся цензуре сеть и коммуникацию.
 * Нет единой точки отказа: Сайт онлайн пока по крайней мере 1 пир обслуживает его.
@@ -24,6 +21,9 @@ ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/compare/py3...zeronet-conse
 * Невозможно отключить: Он нигде, потому что он везде.
 * Быстр и работает оффлайн: Вы можете получить доступ к сайту, даже если Интернет недоступен.
 
+
+
+
 ## Особенности
  * Обновляемые в реальном времени сайты
  * Клонирование вебсайтов в один клик
@@ -38,7 +38,7 @@ ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/compare/py3...zeronet-conse
 
 ## Как это работает?
 
-* После запуска `zeronet.py` вы сможете посетить зайты (zeronet сайты) используя адрес
+* После запуска `zeronet.py` вы сможете посетить zeronet сайты используя адрес
   `http://127.0.0.1:43110/{zeronet_address}`
 (например. `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`).
 * Когда вы посещаете новый сайт zeronet, он пытается найти пиров с помощью BitTorrent
@@ -50,96 +50,101 @@ ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/compare/py3...zeronet-conse
   подписывает новый `content.json` и публикует его для пиров. После этого пиры проверяют целостность `content.json`
   (используя подпись), они загружают измененные файлы и публикуют новый контент для других пиров.
 
+
+Ссылки c информацией о ZeroNet:
+
 ####  [Слайд-шоу о криптографии ZeroNet, обновлениях сайтов, многопользовательских сайтах »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
 ####  [Часто задаваемые вопросы »](https://zeronet.io/docs/faq/)
-
 ####  [Документация разработчика ZeroNet »](https://zeronet.io/docs/site_development/getting_started/)
-
-
 #### [Скриншоты в ZeroNet документации »](https://zeronet.io/docs/using_zeronet/sample_sites/)
 
 ## Как вступить
 
-### Install from source
+### Install from source (recommended)
 
- - clone this repo
- - install python3 and pip if needed (the following instructions are for apt-based distributions)
-   - `sudo apt update`
-   - `sudo apt install python3-pip`
- - `python3 -m pip install -r requirements.txt`
- - Start with: `python3 zeronet.py`
- - Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
+#### System dependencies
 
-## Текущие ограничения
+##### Generic unix-like (including mac os x)
+
+Установите autoconf и другие базовые инструменты разработки, python3 и pip.
+
+##### Apt-based (debian, ubuntu, etc)
+ - `sudo apt update`
+ - `sudo apt install pkg-config python3-pip python3-venv`
+
+##### Android/Termux
+ - Установите [Termux](https://termux.com/) (в Termux вы можете устанавливать пакеты через команду `pkg install <package-names>`)
+ - `pkg update`
+ - `pkg install python automake git binutils` (TODO: проверьте новую установку на наличие дополнительных зависимостей для установки)
+ - (optional) `pkg install tor`
+ - (optional) запустить тор через команду `tor --ControlPort 9051 --CookieAuthentication 1` (вы можете открыть новый сеанс свайпом вправо)
+
+#### Создание зависимостей Python и запуск
+ - клонируйте репозиторий (NOTE: на Android/Termux вы должны клонировать его в «домашнюю» папку Termux, потому что виртуальная среда не может находиться в `storage/`)
+ - `python3 -m venv venv` (создайте виртуальную среду python, последнее `venv` это просто имя/название, если вы используете другое, вы должны заменить его в более поздних командах.)
+ - `source venv/bin/activate` (активируйте среду)
+ - `python3 -m pip install -r requirements.txt` (установите зависимости)
+ - `python3 zeronet.py` (**запустите zeronet-conservancy!**)
+ -  откройте основную страницу в браузере, перейдя по: http://127.0.0.1:43110/
+ -  для повторного запуска с нового терминала вам нужно перейти в деректорию репозитория и ввести :
+ - `source venv/bin/activate`
+ - `python3 zeronet.py`
+
+#### альтернативный скрипт
+ - после установки общих зависимостей и клонирования репозитория (как указано выше) запустите `start-venv.sh` который создаст для вас виртуальную среду и установит требования Python
+ - больше удобных скриптов будует добавлено в ближайшее время
+
+## Текущие ограничения 
 
 * Файловые транзакции не сжаты
 * Нет приватных сайтов
-* ...
+* Отсутствует поддержка DHT
+* Централизованные элементы, такие как Zeroid (мы работаем над этим!)
+* Нет надежной защиты от спама (в процессе разработки)
+* Не работает напрямую из браузера (один из главных приоритетов в ближайшем будущем)
+* Нет прозрачности данных
 
-## Как я могу создать сайт в Zeronet?
 
-Завершите работу zeronet, если он запущен
+## Как создать сайт ZeroNet?
 
-```bash
-$ zeronet.py siteCreate
-...
-- Site private key (Приватный ключ сайта): 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq
-- Site address (Адрес сайта): 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
-...
-- Site created! (Сайт создан)
-$ zeronet.py
-...
-```
-
-Поздравляем, вы закончили! Теперь каждый может получить доступ к вашему зайту используя
-`http://localhost:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2`
+ * Нажмите на **⋮** > **"Create new, empty site"** пункт меню на [admin page](http://127.0.0.1:43110/126NXcevn1AUehWFZLTBw7FrX1crEizQdr).
+ * Вы будете перенаправлены **redirected** на совершенно новый сайт, который можете изменить только вы!
+ * Вы можете найти и изменить содержимое своего сайта в каталоге **data/[yoursiteaddress]**
+ * После внесения изменений откройте свой сайт, перетащите верхнюю правую кнопку «0» влево, затем нажмите кнопки **sign** и **publish** , находящиеся внизу.
 
 Следующие шаги: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_development/getting_started/)
 
+## Поддержите проект
 
-## Как я могу модифицировать Zeronet сайт?
+### Вы можете стать одним из сопровождающих 
 
-* Измените файлы расположенные в data/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 директории.
-  Когда закончите с изменением:
+Нам нужно больше сопровождающих! Станьте им сегодня! Вам не нужно знать, как кодировать,
+есть много другой работы.
 
-```bash
-$ zeronet.py siteSign 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
-- Signing site (Подпись сайта): 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2...
-Private key (Приватный ключ) (input hidden):
-```
+### Исправленные баги & новые функции
 
-* Введите секретный ключ, который вы получили при создании сайта, потом:
+Мы решили пойти дальше и создать идеальную сеть p2p, поэтому нам нужна дополнительная помощь в воплощении этой идеи.
 
-```bash
-$ zeronet.py sitePublish 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
-...
-Site:13DNDk..bhC2 Publishing to 3/10 peers...
-Site:13DNDk..bhC2 Successfuly published to 3 peers
-- Serving files....
-```
+### Сделайте свой сайт / переносите свой контент
 
-* Вот и всё! Вы успешно подписали и опубликовали свои изменения.
+Мы знаем, что документации не хватает, но мы делаем все возможное, чтобы поддержать любого
+кто хочет переехать. Не стесняйтесь спрашивать.
 
-## Help this project stay alive
+### Используйте его и делитесь информацией о его существовании
 
-### Become a maintainer
+Обязательно расскажите людям, почему вы используете 0net и этот форк в частности! Люди
+должны знать об альтернативах.
 
-We need more maintainers! Become one today! Seriously, there's not going to be
-that much new code to audit and auditing new code is the only requirement.
+### Финансовая поддержка сопровождающих
 
-### Use it and spread the word
+В настоящее время ведущим разработчиком/сопровождающим этого форка является @caryoscelus. Вы можете
+посмотреть способы пожертвования на https://caryoscelus.github.io/donate/ (или проверьте
+боковую панель, если вы читаете это на github, чтобы узнать больше). По мере роста нашей команды мы
+также создаст командные аккаунты на дружественных краудфандинговых платформах.
 
-Make sure to tell people why do you use 0net and this fork in particular! People
-need to know their alternatives.
-
-### Financially support maintainers
-
-Currently the only maintainer of this fork is @caryoscelus. You can see ways to
-donate to them on https://caryoscelus.github.io/donate/
-
-If you want to make sure your donation is recognized as donation for this
-project, there is a dedicated bitcoin address for that, too:
+Если вы хотите, чтобы ваше пожертвование было признано пожертвованием для этого
+проекта, для этого также есть специальный биткойн-адрес:
 1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6
 
-If you want to donate in a different way, feel free to contact maintainer or
-create an issue
+Если вы хотите сделать пожертвование другим способом, не стесняйтесь обращаться к сопровождающему или
+создать запрос

From 5f8565dc6732c1ea22b55a0d75407d56764887b3 Mon Sep 17 00:00:00 2001
From: Programmator9000 <105989167+Programmator9000@users.noreply.github.com>
Date: Thu, 26 May 2022 19:19:25 +0400
Subject: [PATCH 080/333] Update README-ru.md

---
 README-ru.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/README-ru.md b/README-ru.md
index 6486dbd3..f01c2abe 100644
--- a/README-ru.md
+++ b/README-ru.md
@@ -125,7 +125,7 @@ zeronet-conservancy — это форк/продолжение проекта [Z
 
 Мы решили пойти дальше и создать идеальную сеть p2p, поэтому нам нужна дополнительная помощь в воплощении этой идеи.
 
-### Сделайте свой сайт / переносите свой контент
+### Создайте свой сайт / переносите свой контент
 
 Мы знаем, что документации не хватает, но мы делаем все возможное, чтобы поддержать любого
 кто хочет переехать. Не стесняйтесь спрашивать.
@@ -135,6 +135,7 @@ zeronet-conservancy — это форк/продолжение проекта [Z
 Обязательно расскажите людям, почему вы используете 0net и этот форк в частности! Люди
 должны знать об альтернативах.
 
+
 ### Финансовая поддержка сопровождающих
 
 В настоящее время ведущим разработчиком/сопровождающим этого форка является @caryoscelus. Вы можете

From 906e15a7a0623aec6ec22fcda3daafbb84d53dfa Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 28 May 2022 13:41:55 +0400
Subject: [PATCH 081/333] version bump (dev) / CHANGELOG

welcome new maintainers: @d4708 , @FraYoshi and @prtngn !
---
 CHANGELOG.md  | 8 +++++++-
 src/Config.py | 4 ++--
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 539bb8c8..a7218a72 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,10 @@
-### zeronet-conservancy 0.7.5 (2022-05-17)
+### zeronet-conservancy 0.7.5+
+maintainers: @caryoscelus, @d4708, @FraYoshi, @prtngn
+- multiple improvements in sidebar button UX and icons (@d4708)
+- fuller debug messages (@caryoscelus)
+- new contributions are GPLv3+
+
+### zeronet-conservancy 0.7.5 (2022-05-17) (fca3c8544debd533)
 maintainers: @caryoscelus , @d4708
 - disable UPnP until it's proven robust
 - new icon & minor rebranding
diff --git a/src/Config.py b/src/Config.py
index 8a497c57..e9b9429b 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -13,9 +13,9 @@ import time
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.5"
+        self.version = "0.7.5+"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5010
+        self.rev = 5020
         self.argv = argv
         self.action = None
         self.test_parser = None

From 7b1864c0d986137c33a4c9903bfd6c13b3025016 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 12 May 2022 01:49:51 +0400
Subject: [PATCH 082/333] branding in --version

---
 src/Config.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Config.py b/src/Config.py
index e9b9429b..057f0485 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -347,7 +347,7 @@ class Config(object):
         self.parser.add_argument('--tor_hs_limit', help='Maximum number of hidden services in Tor always mode', metavar='limit', type=int, default=10)
         self.parser.add_argument('--tor_hs_port', help='Hidden service port in Tor always mode', metavar='limit', type=int, default=15441)
 
-        self.parser.add_argument('--version', action='version', version='ZeroNet %s r%s' % (self.version, self.rev))
+        self.parser.add_argument('--version', action='version', version=f'zeronet-conservancy {self.version} r{self.rev}')
         self.parser.add_argument('--end', help='Stop multi value argument parsing', action='store_true')
 
         return self.parser

From bd6188eeb7aa07a18b2b7af2608f0160f1e53c59 Mon Sep 17 00:00:00 2001
From: Maxim Portnyagin <maxim@prtngn.cc>
Date: Sat, 28 May 2022 18:13:00 +0400
Subject: [PATCH 083/333] Fix dockerfile

---
 .dockerignore |  2 ++
 .gitignore    |  1 +
 Dockerfile    | 36 ++++++++++++------------------------
 3 files changed, 15 insertions(+), 24 deletions(-)
 create mode 100644 .dockerignore

diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..df655837
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,2 @@
+venv
+Dockerfile*
diff --git a/.gitignore b/.gitignore
index 40519faf..35e7c766 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ __pycache__/
 
 # Hidden files
 .*
+!.dockerignore
 !/.github
 !/.gitignore
 !/.travis.yml
diff --git a/Dockerfile b/Dockerfile
index 7839cfa0..cddb2206 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,33 +1,21 @@
-FROM alpine:3.11 
+FROM python:3.10.4-alpine
 
-#Base settings
-ENV HOME /root
-
-COPY requirements.txt /root/requirements.txt
-
-#Install ZeroNet
-RUN apk --update --no-cache --no-progress add python3 python3-dev gcc libffi-dev musl-dev make tor openssl \
- && pip3 install -r /root/requirements.txt \
- && apk del python3-dev gcc libffi-dev musl-dev make \
+RUN apk --update --no-cache --no-progress add gcc libffi-dev musl-dev make tor openssl g++ \
  && echo "ControlPort 9051" >> /etc/tor/torrc \
  && echo "CookieAuthentication 1" >> /etc/tor/torrc
- 
-RUN python3 -V \
- && python3 -m pip list \
- && tor --version \
- && openssl version
 
-#Add Zeronet source
-COPY . /root
-VOLUME /root/data
+WORKDIR /app
+VOLUME /app/data
+COPY . .
+
+RUN python3 -m venv venv \
+ && source venv/bin/activate \
+ && python3 -m pip install -r requirements.txt
 
-#Control if Tor proxy is started
 ENV ENABLE_TOR false
 
-WORKDIR /root
+CMD (! ${ENABLE_TOR} || tor&) \
+ && source venv/bin/activate \
+ && python3 zeronet.py --ui_ip "*" --fileserver_port 26552
 
-#Set upstart command
-CMD (! ${ENABLE_TOR} || tor&) && python3 zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26552
-
-#Expose ports
 EXPOSE 43110 26552

From 8abe489a1a014221f96a282a3a9b652ac14d8301 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 29 May 2022 20:42:56 +0400
Subject: [PATCH 084/333] SideBar plugin: remove unsafe/unused donation

we can still keep largely undocumented feature of custom
(for now BTC) addresses that site owners consciously put there
---
 plugins/Sidebar/SidebarPlugin.py | 14 ++------------
 1 file changed, 2 insertions(+), 12 deletions(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index b3823046..72bff1eb 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -425,7 +425,6 @@ class UiWebsocketPlugin(object):
             </li>
         """))
 
-        donate_key = site.content_manager.contents.get("content.json", {}).get("donate", True)
         site_address = self.site.address
         body.append(_("""
             <li>
@@ -433,9 +432,8 @@ class UiWebsocketPlugin(object):
              <div class='flex'>
               <span class='input text disabled'>{site_address}</span>
         """))
-        if donate_key == False or donate_key == "":
-            pass
-        elif (type(donate_key) == str or type(donate_key) == str) and len(donate_key) > 0:
+        donate_key = site.content_manager.contents.get("content.json", {}).get("donate", None)
+        if type(donate_key) is str and len(donate_key) > 0:
             body.append(_("""
              </div>
             </li>
@@ -444,14 +442,6 @@ class UiWebsocketPlugin(object):
              <div class='flex'>
              {donate_key}
             """))
-        else:
-            body.append(_("""
-              <a href='bitcoin:{site_address}' class='button' id='button-donate'>{_[Donate]}</a>
-            """))
-        body.append(_("""
-             </div>
-            </li>
-        """))
 
     def sidebarRenderOwnedCheckbox(self, body, site):
         if self.site.settings["own"]:

From 33282c4d2f5f57265bcdff3f4e9a355804264be9 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 29 May 2022 20:53:15 +0400
Subject: [PATCH 085/333] SideBarPlugin: donate button back (still only BTC)

---
 plugins/Sidebar/SidebarPlugin.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index 72bff1eb..69ad60f3 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -440,7 +440,10 @@ class UiWebsocketPlugin(object):
             <li>
              <label>{_[Donate]}</label><br>
              <div class='flex'>
-             {donate_key}
+               {donate_key}
+               <a href='bitcoin:{donate_key}' class='button' id='button-donate'>{_[Donate]}</a>
+             </div>
+            </li>
             """))
 
     def sidebarRenderOwnedCheckbox(self, body, site):

From bc313e3b6e688e6d7798065948c2d3b7d304231c Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 29 May 2022 20:58:10 +0400
Subject: [PATCH 086/333] fix SideBar

---
 plugins/Sidebar/SidebarPlugin.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index 69ad60f3..987751e6 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -431,12 +431,12 @@ class UiWebsocketPlugin(object):
              <label>{_[Site address]}</label><br>
              <div class='flex'>
               <span class='input text disabled'>{site_address}</span>
+             </div>
+            </li>
         """))
         donate_key = site.content_manager.contents.get("content.json", {}).get("donate", None)
         if type(donate_key) is str and len(donate_key) > 0:
             body.append(_("""
-             </div>
-            </li>
             <li>
              <label>{_[Donate]}</label><br>
              <div class='flex'>

From b9156bb91330bbfcc465a27778aca10e89c77f1f Mon Sep 17 00:00:00 2001
From: Maxim Portnyagin <maxim@prtngn.cc>
Date: Mon, 30 May 2022 18:14:02 +0400
Subject: [PATCH 087/333] Add docker-compose

---
 Dockerfile         | 14 +++++---------
 Dockerfile.arm64v8 | 34 ----------------------------------
 Dockerfile.tor     | 10 ++++++++++
 docker-compose.yml | 38 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 53 insertions(+), 43 deletions(-)
 delete mode 100644 Dockerfile.arm64v8
 create mode 100644 Dockerfile.tor
 create mode 100644 docker-compose.yml

diff --git a/Dockerfile b/Dockerfile
index cddb2206..81a1d290 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,21 +1,17 @@
 FROM python:3.10.4-alpine
 
-RUN apk --update --no-cache --no-progress add gcc libffi-dev musl-dev make tor openssl g++ \
- && echo "ControlPort 9051" >> /etc/tor/torrc \
- && echo "CookieAuthentication 1" >> /etc/tor/torrc
+RUN apk --update --no-cache --no-progress add gcc libffi-dev musl-dev make openssl g++
 
 WORKDIR /app
-VOLUME /app/data
 COPY . .
 
 RUN python3 -m venv venv \
  && source venv/bin/activate \
  && python3 -m pip install -r requirements.txt
 
-ENV ENABLE_TOR false
-
-CMD (! ${ENABLE_TOR} || tor&) \
- && source venv/bin/activate \
- && python3 zeronet.py --ui_ip "*" --fileserver_port 26552
+CMD source venv/bin/activate \
+ && python3 zeronet.py --ui_ip "*" --fileserver_port 26552 \
+    --tor $TOR_ENABLED --tor_controller tor:$TOR_CONTROL_PORT \
+    --tor_proxy tor:$TOR_SOCKS_PORT --tor_password $TOR_CONTROL_PASSWD
 
 EXPOSE 43110 26552
diff --git a/Dockerfile.arm64v8 b/Dockerfile.arm64v8
deleted file mode 100644
index d27b7620..00000000
--- a/Dockerfile.arm64v8
+++ /dev/null
@@ -1,34 +0,0 @@
-FROM alpine:3.12
-
-#Base settings
-ENV HOME /root
-
-COPY requirements.txt /root/requirements.txt
-
-#Install ZeroNet
-RUN apk --update --no-cache --no-progress add python3 python3-dev gcc libffi-dev musl-dev make tor openssl \
- && pip3 install -r /root/requirements.txt \
- && apk del python3-dev gcc libffi-dev musl-dev make \
- && echo "ControlPort 9051" >> /etc/tor/torrc \
- && echo "CookieAuthentication 1" >> /etc/tor/torrc
- 
-RUN python3 -V \
- && python3 -m pip list \
- && tor --version \
- && openssl version
-
-#Add Zeronet source
-COPY . /root
-VOLUME /root/data
-
-#Control if Tor proxy is started
-ENV ENABLE_TOR false
-
-WORKDIR /root
-
-#Set upstart command
-CMD (! ${ENABLE_TOR} || tor&) && python3 zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26552
-
-#Expose ports
-EXPOSE 43110 26552
-
diff --git a/Dockerfile.tor b/Dockerfile.tor
new file mode 100644
index 00000000..b085747f
--- /dev/null
+++ b/Dockerfile.tor
@@ -0,0 +1,10 @@
+FROM alpine:3.16.0
+
+RUN apk --update --no-cache --no-progress add tor
+
+CMD hashed_control_password=$(tor --quiet --hash-password $TOR_CONTROL_PASSWD) \
+ && tor --SocksPort 0.0.0.0:$TOR_SOCKS_PORT \
+        --ControlPort 0.0.0.0:$TOR_CONTROL_PORT \
+        --HashedControlPassword $hashed_control_password
+
+EXPOSE $TOR_SOCKS_PORT $TOR_CONTROL_PORT
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..0d0845f5
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,38 @@
+version: '3'
+services:
+  tor:
+    tty: true
+    stdin_open: true
+    build:
+      context: .
+      dockerfile: Dockerfile.tor
+    networks:
+      - 0net-network
+    ports:
+      - "9050:9050"
+      - "9051:9051"
+    environment: &tor-environments
+      TOR_CONTROL_PASSWD: some_password
+      TOR_SOCKS_PORT: 9050
+      TOR_CONTROL_PORT: 9051
+  0net:
+    tty: true
+    stdin_open: true
+    build:
+      context: .
+    networks:
+      - 0net-network
+    volumes:
+      - 0net-data:/app/data
+    ports:
+      - "26552:26552"
+      - "43110:43110"
+    depends_on:
+      - tor
+    environment:
+      TOR_ENABLED: enable
+      <<: *tor-environments
+volumes:
+  0net-data:
+networks:
+  0net-network:

From 9a94b34d6bae0f74a4c51bcceda50522c061a6ed Mon Sep 17 00:00:00 2001
From: Maxim Portnyagin <maxim@prtngn.cc>
Date: Mon, 30 May 2022 20:47:18 +0400
Subject: [PATCH 088/333] Add build with integrated tor

---
 Dockerfile.integrated_tor | 18 ++++++++++++++++++
 docker-compose.yml        | 13 +++++++++++++
 2 files changed, 31 insertions(+)
 create mode 100644 Dockerfile.integrated_tor

diff --git a/Dockerfile.integrated_tor b/Dockerfile.integrated_tor
new file mode 100644
index 00000000..20c4425a
--- /dev/null
+++ b/Dockerfile.integrated_tor
@@ -0,0 +1,18 @@
+FROM python:3.10.4-alpine
+
+RUN apk --update --no-cache --no-progress add tor gcc libffi-dev musl-dev make openssl g++ \
+ && echo "ControlPort 9051" >> /etc/tor/torrc \
+ && echo "CookieAuthentication 1" >> /etc/tor/torrc
+
+WORKDIR /app
+COPY . .
+
+RUN python3 -m venv venv \
+ && source venv/bin/activate \
+ && python3 -m pip install -r requirements.txt
+
+CMD (tor&) \
+ && source venv/bin/activate \
+ && python3 zeronet.py --ui_ip "*" --fileserver_port 26552
+
+EXPOSE 43110 26552
diff --git a/docker-compose.yml b/docker-compose.yml
index 0d0845f5..5da1fc26 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -32,6 +32,19 @@ services:
     environment:
       TOR_ENABLED: enable
       <<: *tor-environments
+  0net-tor:
+    tty: true
+    stdin_open: true
+    build:
+      context: .
+      dockerfile: Dockerfile.integrated_tor
+    networks:
+      - 0net-network
+    volumes:
+      - 0net-data:/app/data
+    ports:
+      - "26552:26552"
+      - "43110:43110"
 volumes:
   0net-data:
 networks:

From 6517ebc432ef87e5205d246460ec8e6907f57639 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 31 May 2022 01:24:37 +0400
Subject: [PATCH 089/333] reduce fingerprinting information sent to other nodes

version and revision are still accessible to users to see,
but not the other nodes. this will make it harder to collect
information for both informational, but also malicious purposes
---
 src/Connection/Connection.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py
index 22bcf29c..4adb3227 100644
--- a/src/Connection/Connection.py
+++ b/src/Connection/Connection.py
@@ -362,14 +362,14 @@ class Connection(object):
                 self.server.log.warning("Unknown target onion address: %s" % self.target_onion)
 
         handshake = {
-            "version": config.version,
+            "version": "conservancy",
             "protocol": "v2",
             "use_bin_type": True,
             "peer_id": peer_id,
             "fileserver_port": self.server.port,
             "port_opened": self.server.port_opened.get(self.ip_type, None),
             "target_ip": self.ip,
-            "rev": config.rev,
+            "rev": 8192,
             "crypt_supported": crypt_supported,
             "crypt": self.crypt,
             "time": int(time.time())

From fee2a4f2c9e1a4a22fd79b1fda7a3a5f51247691 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 12 May 2022 01:33:43 +0400
Subject: [PATCH 090/333] [cleanup] keep removing old coffeescript related
 bloat

---
 src/Config.py           |  8 ------
 src/Debug/DebugMedia.py | 61 ++---------------------------------------
 2 files changed, 3 insertions(+), 66 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index 057f0485..5fd9c091 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -124,11 +124,6 @@ class Config(object):
             "http://tracker.bt4g.com:2095/announce",  # Cloudflare
             "zero://2602:ffc5::c5b2:5360:26312"  # US/ATL
         ]
-        # Platform specific
-        if sys.platform.startswith("win"):
-            coffeescript = "type %s | tools\\coffee\\coffee.cmd"
-        else:
-            coffeescript = None
 
         try:
             language, enc = locale.getdefaultlocale()
@@ -336,9 +331,6 @@ class Config(object):
 
         self.parser.add_argument("--download_optional", choices=["manual", "auto"], default="manual")
 
-        self.parser.add_argument('--coffeescript_compiler', help='Coffeescript compiler for developing', default=coffeescript,
-                                 metavar='executable_path')
-
         self.parser.add_argument('--tor', help='enable: Use only for Tor peers, always: Use Tor for every connection', choices=["disable", "enable", "always"], default='enable')
         self.parser.add_argument('--tor_controller', help='Tor controller address', metavar='ip:port', default='127.0.0.1:9051')
         self.parser.add_argument('--tor_proxy', help='Tor proxy address', metavar='ip:port', default='127.0.0.1:9050')
diff --git a/src/Debug/DebugMedia.py b/src/Debug/DebugMedia.py
index a892dc56..25a84be1 100644
--- a/src/Debug/DebugMedia.py
+++ b/src/Debug/DebugMedia.py
@@ -29,30 +29,13 @@ def findfiles(path, find_ext):
                 yield file_path.replace("\\", "/")
 
 
-# Try to find coffeescript compiler in path
-def findCoffeescriptCompiler():
-    coffeescript_compiler = None
-    try:
-        import distutils.spawn
-        coffeescript_compiler = helper.shellquote(distutils.spawn.find_executable("coffee")) + " --no-header -p"
-    except:
-        pass
-    if coffeescript_compiler:
-        return coffeescript_compiler
-    else:
-        return False
-
-
-# Generates: all.js: merge *.js, compile coffeescript, all.css: merge *.css, vendor prefix features
+# Generates: all.js: merge *.js, all.css: merge *.css, vendor prefix features
 def merge(merged_path):
     merged_path = merged_path.replace("\\", "/")
     merge_dir = os.path.dirname(merged_path)
     s = time.time()
     ext = merged_path.split(".")[-1]
-    if ext == "js":  # If merging .js find .coffee too
-        find_ext = ["js", "coffee"]
-    else:
-        find_ext = [ext]
+    find_ext = [ext]
 
     # If exist check the other files modification date
     if os.path.isfile(merged_path):
@@ -80,44 +63,7 @@ def merge(merged_path):
     for file_path in findfiles(merge_dir, find_ext):
         file_relative_path = file_path.replace(merge_dir + "/", "")
         parts.append(b"\n/* ---- %s ---- */\n\n" % file_relative_path.encode("utf8"))
-        if file_path.endswith(".coffee"):  # Compile coffee script
-            if file_path in changed or file_relative_path not in old_parts:  # Only recompile if changed or its not compiled before
-                if config.coffeescript_compiler is None:
-                    config.coffeescript_compiler = findCoffeescriptCompiler()
-                if not config.coffeescript_compiler:
-                    logging.error("No coffeescript compiler defined, skipping compiling %s" % merged_path)
-                    return False  # No coffeescript compiler, skip this file
-
-                # Replace / with os separators and escape it
-                file_path_escaped = helper.shellquote(file_path.replace("/", os.path.sep))
-
-                if "%s" in config.coffeescript_compiler:  # Replace %s with coffeescript file
-                    command = config.coffeescript_compiler.replace("%s", file_path_escaped)
-                else:  # Put coffeescript file to end
-                    command = config.coffeescript_compiler + " " + file_path_escaped
-
-                # Start compiling
-                s = time.time()
-                compiler = subprocess.Popen(command, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
-                out = compiler.stdout.read()
-                compiler.wait()
-                logging.debug("Running: %s (Done in %.2fs)" % (command, time.time() - s))
-
-                # Check errors
-                if out and out.startswith(b"("):  # No error found
-                    parts.append(out)
-                else:  # Put error message in place of source code
-                    error = out
-                    logging.error("%s Compile error: %s" % (file_relative_path, error))
-                    error_escaped = re.escape(error).replace(b"\n", b"\\n").replace(br"\\n", br"\n")
-                    parts.append(
-                        b"alert('%s compile error: %s');" %
-                        (file_relative_path.encode(), error_escaped)
-                    )
-            else:  # Not changed use the old_part
-                parts.append(old_parts[file_relative_path])
-        else:  # Add to parts
-            parts.append(open(file_path, "rb").read())
+        parts.append(open(file_path, "rb").read())
 
     merged = b"\n".join(parts)
     if ext == "css":  # Vendor prefix css
@@ -131,5 +77,4 @@ def merge(merged_path):
 if __name__ == "__main__":
     logging.getLogger().setLevel(logging.DEBUG)
     os.chdir("..")
-    config.coffeescript_compiler = r'type "%s" | tools\coffee-node\bin\node.exe tools\coffee-node\bin\coffee --no-header -s -p'
     merge("data/12Hw8rTgzrNo4DSh2AkqwPRqDyTticwJyH/js/all.js")

From 574928e625746df2279fa6efbda5d620851ddf52 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 31 May 2022 13:32:47 +0400
Subject: [PATCH 091/333] [cleanup] remove coffee script files

---
 plugins/Sidebar/media/Class.coffee            |  23 -
 plugins/Sidebar/media/Console.coffee          | 201 -----
 plugins/Sidebar/media/Menu.coffee             |  49 --
 plugins/Sidebar/media/Prototypes.coffee       |   9 -
 plugins/Sidebar/media/RateLimit.coffee        |  14 -
 plugins/Sidebar/media/Sidebar.coffee          | 644 ----------------
 .../UiConfig/media/js/ConfigStorage.coffee    | 222 ------
 plugins/UiConfig/media/js/ConfigView.coffee   | 124 ---
 plugins/UiConfig/media/js/UiConfig.coffee     | 129 ----
 plugins/UiConfig/media/js/lib/Class.coffee    |  23 -
 plugins/UiConfig/media/js/lib/Promise.coffee  |  74 --
 .../UiConfig/media/js/lib/Prototypes.coffee   |   8 -
 .../UiConfig/media/js/utils/Animation.coffee  | 138 ----
 plugins/UiConfig/media/js/utils/Dollar.coffee |   3 -
 .../UiConfig/media/js/utils/ZeroFrame.coffee  |  85 ---
 plugins/UiFileManager/media/js/Config.coffee  |  15 -
 .../UiFileManager/media/js/FileEditor.coffee  | 179 -----
 .../media/js/FileItemList.coffee              | 194 -----
 .../UiFileManager/media/js/FileList.coffee    | 268 -------
 .../media/js/UiFileManager.coffee             |  79 --
 .../media/js/lib/Animation.coffee             | 138 ----
 .../UiFileManager/media/js/lib/Class.coffee   |  23 -
 .../UiFileManager/media/js/lib/Dollar.coffee  |   3 -
 .../media/js/lib/ItemList.coffee              |  26 -
 .../UiFileManager/media/js/lib/Menu.coffee    | 110 ---
 .../UiFileManager/media/js/lib/Promise.coffee |  74 --
 .../media/js/lib/Prototypes.coffee            |   9 -
 .../media/js/lib/RateLimitCb.coffee           |  62 --
 .../UiFileManager/media/js/lib/Text.coffee    | 147 ----
 .../UiFileManager/media/js/lib/Time.coffee    |  59 --
 .../media/js/lib/ZeroFrame.coffee             |  85 ---
 .../media/js/PluginList.coffee                | 132 ----
 .../media/js/UiPluginManager.coffee           |  71 --
 .../UiPluginManager/media/js/lib/Class.coffee |  23 -
 .../media/js/lib/Promise.coffee               |  74 --
 .../media/js/lib/Prototypes.coffee            |   8 -
 .../media/js/utils/Animation.coffee           | 138 ----
 .../media/js/utils/Dollar.coffee              |   3 -
 .../media/js/utils/ZeroFrame.coffee           |  85 ---
 src/Ui/media/Fixbutton.coffee                 |  32 -
 src/Ui/media/Infopanel.coffee                 |  57 --
 src/Ui/media/Loading.coffee                   |  91 ---
 src/Ui/media/Notifications.coffee             |  89 ---
 src/Ui/media/Wrapper.coffee                   | 714 ------------------
 src/Ui/media/WrapperZeroFrame.coffee          |  22 -
 src/Ui/media/ZeroSiteTheme.coffee             |  49 --
 src/Ui/media/lib/RateLimit.coffee             |  14 -
 src/Ui/media/lib/Translate.coffee             |   1 -
 src/Ui/media/lib/ZeroWebsocket.coffee         |  95 ---
 src/Ui/media/lib/jquery.csslater.coffee       |  36 -
 50 files changed, 4951 deletions(-)
 delete mode 100644 plugins/Sidebar/media/Class.coffee
 delete mode 100644 plugins/Sidebar/media/Console.coffee
 delete mode 100644 plugins/Sidebar/media/Menu.coffee
 delete mode 100644 plugins/Sidebar/media/Prototypes.coffee
 delete mode 100644 plugins/Sidebar/media/RateLimit.coffee
 delete mode 100644 plugins/Sidebar/media/Sidebar.coffee
 delete mode 100644 plugins/UiConfig/media/js/ConfigStorage.coffee
 delete mode 100644 plugins/UiConfig/media/js/ConfigView.coffee
 delete mode 100644 plugins/UiConfig/media/js/UiConfig.coffee
 delete mode 100644 plugins/UiConfig/media/js/lib/Class.coffee
 delete mode 100644 plugins/UiConfig/media/js/lib/Promise.coffee
 delete mode 100644 plugins/UiConfig/media/js/lib/Prototypes.coffee
 delete mode 100644 plugins/UiConfig/media/js/utils/Animation.coffee
 delete mode 100644 plugins/UiConfig/media/js/utils/Dollar.coffee
 delete mode 100644 plugins/UiConfig/media/js/utils/ZeroFrame.coffee
 delete mode 100644 plugins/UiFileManager/media/js/Config.coffee
 delete mode 100644 plugins/UiFileManager/media/js/FileEditor.coffee
 delete mode 100644 plugins/UiFileManager/media/js/FileItemList.coffee
 delete mode 100644 plugins/UiFileManager/media/js/FileList.coffee
 delete mode 100644 plugins/UiFileManager/media/js/UiFileManager.coffee
 delete mode 100644 plugins/UiFileManager/media/js/lib/Animation.coffee
 delete mode 100644 plugins/UiFileManager/media/js/lib/Class.coffee
 delete mode 100644 plugins/UiFileManager/media/js/lib/Dollar.coffee
 delete mode 100644 plugins/UiFileManager/media/js/lib/ItemList.coffee
 delete mode 100644 plugins/UiFileManager/media/js/lib/Menu.coffee
 delete mode 100644 plugins/UiFileManager/media/js/lib/Promise.coffee
 delete mode 100644 plugins/UiFileManager/media/js/lib/Prototypes.coffee
 delete mode 100644 plugins/UiFileManager/media/js/lib/RateLimitCb.coffee
 delete mode 100644 plugins/UiFileManager/media/js/lib/Text.coffee
 delete mode 100644 plugins/UiFileManager/media/js/lib/Time.coffee
 delete mode 100644 plugins/UiFileManager/media/js/lib/ZeroFrame.coffee
 delete mode 100644 plugins/UiPluginManager/media/js/PluginList.coffee
 delete mode 100644 plugins/UiPluginManager/media/js/UiPluginManager.coffee
 delete mode 100644 plugins/UiPluginManager/media/js/lib/Class.coffee
 delete mode 100644 plugins/UiPluginManager/media/js/lib/Promise.coffee
 delete mode 100644 plugins/UiPluginManager/media/js/lib/Prototypes.coffee
 delete mode 100644 plugins/UiPluginManager/media/js/utils/Animation.coffee
 delete mode 100644 plugins/UiPluginManager/media/js/utils/Dollar.coffee
 delete mode 100644 plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee
 delete mode 100644 src/Ui/media/Fixbutton.coffee
 delete mode 100644 src/Ui/media/Infopanel.coffee
 delete mode 100644 src/Ui/media/Loading.coffee
 delete mode 100644 src/Ui/media/Notifications.coffee
 delete mode 100644 src/Ui/media/Wrapper.coffee
 delete mode 100644 src/Ui/media/WrapperZeroFrame.coffee
 delete mode 100644 src/Ui/media/ZeroSiteTheme.coffee
 delete mode 100644 src/Ui/media/lib/RateLimit.coffee
 delete mode 100644 src/Ui/media/lib/Translate.coffee
 delete mode 100644 src/Ui/media/lib/ZeroWebsocket.coffee
 delete mode 100644 src/Ui/media/lib/jquery.csslater.coffee

diff --git a/plugins/Sidebar/media/Class.coffee b/plugins/Sidebar/media/Class.coffee
deleted file mode 100644
index d62ab25c..00000000
--- a/plugins/Sidebar/media/Class.coffee
+++ /dev/null
@@ -1,23 +0,0 @@
-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
\ No newline at end of file
diff --git a/plugins/Sidebar/media/Console.coffee b/plugins/Sidebar/media/Console.coffee
deleted file mode 100644
index d5a83346..00000000
--- a/plugins/Sidebar/media/Console.coffee
+++ /dev/null
@@ -1,201 +0,0 @@
-class Console extends Class
-	constructor: (@sidebar) ->
-		@tag = null
-		@opened = false
-		@filter = null
-		@tab_types = [
-			{title: "All", filter: ""},
-			{title: "Info", filter: "INFO"},
-			{title: "Warning", filter: "WARNING"},
-			{title: "Error", filter: "ERROR"}
-		]
-		@read_size = 32 * 1024
-		@tab_active = ""
-		#@filter = @sidebar.wrapper.site_info.address_short
-		handleMessageWebsocket_original = @sidebar.wrapper.handleMessageWebsocket
-		@sidebar.wrapper.handleMessageWebsocket = (message) =>
-			if message.cmd == "logLineAdd" and message.params.stream_id == @stream_id
-				@addLines(message.params.lines)
-			else
-				handleMessageWebsocket_original(message)
-
-		$(window).on "hashchange", =>
-			if window.top.location.hash.startsWith("#ZeroNet:Console")
-				@open()
-
-		if window.top.location.hash.startsWith("#ZeroNet:Console")
-			setTimeout (=> @open()), 10
-
-	createHtmltag: ->
-		if not @container
-			@container = $("""
-			<div class="console-container">
-				<div class="console">
-					<div class="console-top">
-						<div class="console-tabs"></div>
-						<div class="console-text">Loading...</div>
-					</div>
-					<div class="console-middle">
-						<div class="mynode"></div>
-						<div class="peers">
-							<div class="peer"><div class="line"></div><a href="#" class="icon">\u25BD</div></div>
-						</div>
-					</div>
-				</div>
-			</div>
-			""")
-			@text = @container.find(".console-text")
-			@text_elem = @text[0]
-			@tabs = @container.find(".console-tabs")
-
-			@text.on "mousewheel", (e) =>  # Stop animation on manual scrolling
-				if e.originalEvent.deltaY < 0
-					@text.stop()
-				RateLimit 300, @checkTextIsBottom
-
-			@text.is_bottom = true
-
-			@container.appendTo(document.body)
-			@tag = @container.find(".console")
-			for tab_type in @tab_types
-				tab = $("<a></a>", {href: "#", "data-filter": tab_type.filter, "data-title": tab_type.title}).text(tab_type.title)
-				if tab_type.filter == @tab_active
-					tab.addClass("active")
-				tab.on("click", @handleTabClick)
-				if window.top.location.hash.endsWith(tab_type.title)
-					@log "Triggering click on", tab
-					tab.trigger("click")
-				@tabs.append(tab)
-
-			@container.on "mousedown touchend touchcancel", (e) =>
-				if e.target != e.currentTarget
-					return true
-				@log "closing"
-				if $(document.body).hasClass("body-console")
-					@close()
-					return true
-
-			@loadConsoleText()
-
-	checkTextIsBottom: =>
-		@text.is_bottom = Math.round(@text_elem.scrollTop + @text_elem.clientHeight) >= @text_elem.scrollHeight - 15
-
-	toColor: (text, saturation=60, lightness=70) ->
-		hash = 0
-		for i in [0..text.length-1]
-			hash += text.charCodeAt(i)*i
-			hash = hash % 1777
-		return "hsl(" + (hash % 360) + ",#{saturation}%,#{lightness}%)";
-
-	formatLine: (line) =>
-		match = line.match(/(\[.*?\])[ ]+(.*?)[ ]+(.*?)[ ]+(.*)/)
-		if not match
-			return line.replace(/\</g, "&lt;").replace(/\>/g, "&gt;")
-
-		[line, added, level, module, text] = line.match(/(\[.*?\])[ ]+(.*?)[ ]+(.*?)[ ]+(.*)/)
-		added = "<span style='color: #dfd0fa'>#{added}</span>"
-		level = "<span style='color: #{@toColor(level, 100)};'>#{level}</span>"
-		module = "<span style='color: #{@toColor(module, 60)}; font-weight: bold;'>#{module}</span>"
-
-		text = text.replace(/(Site:[A-Za-z0-9\.]+)/g, "<span style='color: #AAAAFF'>$1</span>")
-		text = text.replace(/\</g, "&lt;").replace(/\>/g, "&gt;")
-		#text = text.replace(/( [0-9\.]+(|s|ms))/g, "<span style='color: #FFF;'>$1</span>")
-		return "#{added} #{level} #{module} #{text}"
-
-
-	addLines: (lines, animate=true) =>
-		html_lines = []
-		@logStart "formatting"
-		for line in lines
-			html_lines.push @formatLine(line)
-		@logEnd "formatting"
-		@logStart "adding"
-		@text.append(html_lines.join("<br>") + "<br>")
-		@logEnd "adding"
-		if @text.is_bottom and animate
-			@text.stop().animate({scrollTop: @text_elem.scrollHeight - @text_elem.clientHeight + 1}, 600, 'easeInOutCubic')
-
-
-	loadConsoleText: =>
-		@sidebar.wrapper.ws.cmd "consoleLogRead", {filter: @filter, read_size: @read_size}, (res) =>
-			@text.html("")
-			pos_diff = res["pos_end"] - res["pos_start"]
-			size_read = Math.round(pos_diff/1024)
-			size_total = Math.round(res['pos_end']/1024)
-			@text.append("<br><br>")
-			@text.append("Displaying #{res.lines.length} of #{res.num_found} lines found in the last #{size_read}kB of the log file. (#{size_total}kB total)<br>")
-			@addLines res.lines, false
-			@text_elem.scrollTop = @text_elem.scrollHeight
-		if @stream_id
-			@sidebar.wrapper.ws.cmd "consoleLogStreamRemove", {stream_id: @stream_id}
-		@sidebar.wrapper.ws.cmd "consoleLogStream", {filter: @filter}, (res) =>
-			@stream_id = res.stream_id
-
-	close: =>
-		window.top.location.hash = ""
-		@sidebar.move_lock = "y"
-		@sidebar.startDrag()
-		@sidebar.stopDrag()
-
-	open: =>
-		@sidebar.startDrag()
-		@sidebar.moved("y")
-		@sidebar.fixbutton_targety = @sidebar.page_height - @sidebar.fixbutton_inity - 50
-		@sidebar.stopDrag()
-
-	onOpened: =>
-		@sidebar.onClosed()
-		@log "onOpened"
-
-	onClosed: =>
-		$(document.body).removeClass("body-console")
-		if @stream_id
-			@sidebar.wrapper.ws.cmd "consoleLogStreamRemove", {stream_id: @stream_id}
-
-	cleanup: =>
-		if @container
-			@container.remove()
-			@container = null
-
-	stopDragY: =>
-		# Animate sidebar and iframe
-		if @sidebar.fixbutton_targety == @sidebar.fixbutton_inity
-			# Closed
-			targety = 0
-			@opened = false
-		else
-			# Opened
-			targety = @sidebar.fixbutton_targety - @sidebar.fixbutton_inity
-			@onOpened()
-			@opened = true
-
-		# Revent sidebar transitions
-		if @tag
-			@tag.css("transition", "0.5s ease-out")
-			@tag.css("transform", "translateY(#{targety}px)").one transitionEnd, =>
-				@tag.css("transition", "")
-				if not @opened
-					@cleanup()
-		# Revert body transformations
-		@log "stopDragY", "opened:", @opened, targety
-		if not @opened
-			@onClosed()
-
-	changeFilter: (filter) =>
-		@filter = filter
-		if @filter == ""
-			@read_size = 32 * 1024
-		else
-			@read_size = 5 * 1024 * 1024
-		@loadConsoleText()
-
-	handleTabClick: (e) =>
-		elem = $(e.currentTarget)
-		@tab_active = elem.data("filter")
-		$("a", @tabs).removeClass("active")
-		elem.addClass("active")
-		@changeFilter(@tab_active)
-		window.top.location.hash = "#ZeroNet:Console:" + elem.data("title")
-		return false
-
-window.Console = Console
diff --git a/plugins/Sidebar/media/Menu.coffee b/plugins/Sidebar/media/Menu.coffee
deleted file mode 100644
index 3e19fd9f..00000000
--- a/plugins/Sidebar/media/Menu.coffee
+++ /dev/null
@@ -1,49 +0,0 @@
-class Menu
-	constructor: (@button) ->
-		@elem = $(".menu.template").clone().removeClass("template")
-		@elem.appendTo("body")
-		@items = []
-
-	show: ->
-		if window.visible_menu and window.visible_menu.button[0] == @button[0] # Same menu visible then hide it
-			window.visible_menu.hide()
-			@hide()
-		else
-			button_pos = @button.offset()
-			left = button_pos.left
-			@elem.css({"top": button_pos.top+@button.outerHeight(), "left": left})
-			@button.addClass("menu-active")
-			@elem.addClass("visible")
-			if @elem.position().left + @elem.width() + 20 > window.innerWidth
-				@elem.css("left", window.innerWidth - @elem.width() - 20)
-			if window.visible_menu then window.visible_menu.hide()
-			window.visible_menu = @
-
-
-	hide: ->
-		@elem.removeClass("visible")
-		@button.removeClass("menu-active")
-		window.visible_menu = null
-
-
-	addItem: (title, cb) ->
-		item = $(".menu-item.template", @elem).clone().removeClass("template")
-		item.html(title)
-		item.on "click", =>
-			if not cb(item)
-				@hide()
-			return false
-		item.appendTo(@elem)
-		@items.push item
-		return item
-
-
-	log: (args...) ->
-		console.log "[Menu]", args...
-
-window.Menu = Menu
-
-# Hide menu on outside click
-$("body").on "click", (e) ->
-	if window.visible_menu and e.target != window.visible_menu.button[0] and $(e.target).parent()[0] != window.visible_menu.elem[0]
-		window.visible_menu.hide()
diff --git a/plugins/Sidebar/media/Prototypes.coffee b/plugins/Sidebar/media/Prototypes.coffee
deleted file mode 100644
index a9edd255..00000000
--- a/plugins/Sidebar/media/Prototypes.coffee
+++ /dev/null
@@ -1,9 +0,0 @@
-String::startsWith = (s) -> @[...s.length] is s
-String::endsWith = (s) -> s is '' or @[-s.length..] is s
-String::capitalize = ->  if @.length then @[0].toUpperCase() + @.slice(1) else ""
-String::repeat = (count) -> new Array( count + 1 ).join(@)
-
-window.isEmpty = (obj) ->
-	for key of obj
-		return false
-	return true
diff --git a/plugins/Sidebar/media/RateLimit.coffee b/plugins/Sidebar/media/RateLimit.coffee
deleted file mode 100644
index 17c67433..00000000
--- a/plugins/Sidebar/media/RateLimit.coffee
+++ /dev/null
@@ -1,14 +0,0 @@
-limits = {}
-call_after_interval = {}
-window.RateLimit = (interval, fn) ->
-	if not limits[fn]
-		call_after_interval[fn] = false
-		fn() # First call is not delayed
-		limits[fn] = setTimeout (->
-			if call_after_interval[fn]
-				fn()
-			delete limits[fn]
-			delete call_after_interval[fn]
-		), interval
-	else # Called within iterval, delay the call
-		call_after_interval[fn] = true
diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee
deleted file mode 100644
index 57d36eac..00000000
--- a/plugins/Sidebar/media/Sidebar.coffee
+++ /dev/null
@@ -1,644 +0,0 @@
-class Sidebar extends Class
-	constructor: (@wrapper) ->
-		@tag = null
-		@container = null
-		@opened = false
-		@width = 410
-		@console = new Console(@)
-		@fixbutton = $(".fixbutton")
-		@fixbutton_addx = 0
-		@fixbutton_addy = 0
-		@fixbutton_initx = 0
-		@fixbutton_inity = 15
-		@fixbutton_targetx = 0
-		@move_lock = null
-		@page_width = $(window).width()
-		@page_height = $(window).height()
-		@frame = $("#inner-iframe")
-		@initFixbutton()
-		@dragStarted = 0
-		@globe = null
-		@preload_html = null
-
-		@original_set_site_info = @wrapper.setSiteInfo  # We going to override this, save the original
-
-		# Start in opened state for debugging
-		if window.top.location.hash == "#ZeroNet:OpenSidebar"
-			@startDrag()
-			@moved("x")
-			@fixbutton_targetx = @fixbutton_initx - @width
-			@stopDrag()
-
-
-	initFixbutton: ->
-
-		# Detect dragging
-		@fixbutton.on "mousedown touchstart", (e) =>
-			if e.button > 0  # Right or middle click
-				return
-			e.preventDefault()
-
-			# Disable previous listeners
-			@fixbutton.off "click touchend touchcancel"
-
-			# Make sure its not a click
-			@dragStarted = (+ new Date)
-
-			# Fullscreen drag bg to capture mouse events over iframe
-			$(".drag-bg").remove()
-			$("<div class='drag-bg'></div>").appendTo(document.body)
-
-			$("body").one "mousemove touchmove", (e) =>
-				mousex = e.pageX
-				mousey = e.pageY
-				if not mousex
-					mousex = e.originalEvent.touches[0].pageX
-					mousey = e.originalEvent.touches[0].pageY
-
-				@fixbutton_addx = @fixbutton.offset().left - mousex
-				@fixbutton_addy = @fixbutton.offset().top - mousey
-				@startDrag()
-		@fixbutton.parent().on "click touchend touchcancel", (e) =>
-			if (+ new Date) - @dragStarted < 100
-				window.top.location = @fixbutton.find(".fixbutton-bg").attr("href")
-			@stopDrag()
-		@resized()
-		$(window).on "resize", @resized
-
-	resized: =>
-		@page_width = $(window).width()
-		@page_height = $(window).height()
-		@fixbutton_initx = @page_width - 75  # Initial x position
-		if @opened
-			@fixbutton.css
-				left: @fixbutton_initx - @width
-		else
-			@fixbutton.css
-				left: @fixbutton_initx
-
-	# Start dragging the fixbutton
-	startDrag: ->
-		#@move_lock = "x"  # Temporary until internals not finished
-		@log "startDrag", @fixbutton_initx, @fixbutton_inity
-		@fixbutton_targetx = @fixbutton_initx  # Fallback x position
-		@fixbutton_targety = @fixbutton_inity  # Fallback y position
-
-		@fixbutton.addClass("dragging")
-
-		# IE position wrap fix
-		if navigator.userAgent.indexOf('MSIE') != -1 or navigator.appVersion.indexOf('Trident/') > 0
-			@fixbutton.css("pointer-events", "none")
-
-		# Don't go to homepage
-		@fixbutton.one "click", (e) =>
-			@stopDrag()
-			@fixbutton.removeClass("dragging")
-			moved_x = Math.abs(@fixbutton.offset().left - @fixbutton_initx)
-			moved_y = Math.abs(@fixbutton.offset().top - @fixbutton_inity)
-			if moved_x > 5 or moved_y > 10
-				# If moved more than some pixel the button then don't go to homepage
-				e.preventDefault()
-
-		# Animate drag
-		@fixbutton.parents().on "mousemove touchmove", @animDrag
-		@fixbutton.parents().on "mousemove touchmove" ,@waitMove
-
-		# Stop dragging listener
-		@fixbutton.parents().one "mouseup touchend touchcancel", (e) =>
-			e.preventDefault()
-			@stopDrag()
-
-
-	# Wait for moving the fixbutton
-	waitMove: (e) =>
-		document.body.style.perspective = "1000px"
-		document.body.style.height = "100%"
-		document.body.style.willChange = "perspective"
-		document.documentElement.style.height = "100%"
-		#$(document.body).css("backface-visibility", "hidden").css("perspective", "1000px").css("height", "900px")
-		# $("iframe").css("backface-visibility", "hidden")
-
-		moved_x = Math.abs(parseInt(@fixbutton[0].style.left) - @fixbutton_targetx)
-		moved_y = Math.abs(parseInt(@fixbutton[0].style.top) - @fixbutton_targety)
-		if moved_x > 5 and (+ new Date) - @dragStarted + moved_x > 50
-			@moved("x")
-			@fixbutton.stop().animate {"top": @fixbutton_inity}, 1000
-			@fixbutton.parents().off "mousemove touchmove" ,@waitMove
-
-		else if moved_y > 5 and (+ new Date) - @dragStarted + moved_y > 50
-			@moved("y")
-			@fixbutton.parents().off "mousemove touchmove" ,@waitMove
-
-	moved: (direction) ->
-		@log "Moved", direction
-		@move_lock = direction
-		if direction == "y"
-			$(document.body).addClass("body-console")
-			return @console.createHtmltag()
-		@createHtmltag()
-		$(document.body).addClass("body-sidebar")
-		@container.on "mousedown touchend touchcancel", (e) =>
-			if e.target != e.currentTarget
-				return true
-			@log "closing"
-			if $(document.body).hasClass("body-sidebar")
-				@close()
-				return true
-
-		$(window).off "resize"
-		$(window).on "resize", =>
-			$(document.body).css "height", $(window).height()
-			@scrollable()
-			@resized()
-
-		# Override setsiteinfo to catch changes
-		@wrapper.setSiteInfo = (site_info) =>
-			@setSiteInfo(site_info)
-			@original_set_site_info.apply(@wrapper, arguments)
-
-		# Preload world.jpg
-		img = new Image();
-		img.src = "/uimedia/globe/world.jpg";
-
-	setSiteInfo: (site_info) ->
-		RateLimit 1500, =>
-			@updateHtmlTag()
-		RateLimit 30000, =>
-			@displayGlobe()
-
-	# Create the sidebar html tag
-	createHtmltag: ->
-		@when_loaded = $.Deferred()
-		if not @container
-			@container = $("""
-			<div class="sidebar-container"><div class="sidebar scrollable"><div class="content-wrapper"><div class="content">
-			</div></div></div></div>
-			""")
-			@container.appendTo(document.body)
-			@tag = @container.find(".sidebar")
-			@updateHtmlTag()
-			@scrollable = window.initScrollable()
-
-
-	updateHtmlTag: ->
-		if @preload_html
-			@setHtmlTag(@preload_html)
-			@preload_html = null
-		else
-			@wrapper.ws.cmd "sidebarGetHtmlTag", {}, @setHtmlTag
-
-	setHtmlTag: (res) =>
-		if @tag.find(".content").children().length == 0 # First update
-			@log "Creating content"
-			@container.addClass("loaded")
-			morphdom(@tag.find(".content")[0], '<div class="content">'+res+'</div>')
-			# @scrollable()
-			@when_loaded.resolve()
-
-		else  # Not first update, patch the html to keep unchanged dom elements
-			morphdom @tag.find(".content")[0], '<div class="content">'+res+'</div>', {
-				onBeforeMorphEl: (from_el, to_el) ->  # Ignore globe loaded state
-					if from_el.className == "globe" or from_el.className.indexOf("noupdate") >= 0
-						return false
-					else
-						return true
-				}
-
-		# Save and forget privatekey for site signing
-		@tag.find("#privatekey-add").off("click, touchend").on "click touchend", (e) =>
-			@wrapper.displayPrompt "Enter your private key:", "password", "Save", "", (privatekey) =>
-				@wrapper.ws.cmd "userSetSitePrivatekey", [privatekey], (res) =>
-					@wrapper.notifications.add "privatekey", "done", "Private key saved for site signing", 5000
-			return false
-
-		@tag.find("#privatekey-forget").off("click, touchend").on "click touchend", (e) =>
-			@wrapper.displayConfirm "Remove saved private key for this site?", "Forget", (res) =>
-				if not res
-					return false
-				@wrapper.ws.cmd "userSetSitePrivatekey", [""], (res) =>
-					@wrapper.notifications.add "privatekey", "done", "Saved private key removed", 5000
-			return false
-
-		# Use requested address for browse files urls
-		@tag.find("#browse-files").attr("href", document.location.pathname.replace(/(\/.*?(\/|$)).*$/, "/list$1"))
-
-
-
-	animDrag: (e) =>
-		mousex = e.pageX
-		mousey = e.pageY
-		if not mousex and e.originalEvent.touches
-			mousex = e.originalEvent.touches[0].pageX
-			mousey = e.originalEvent.touches[0].pageY
-
-		overdrag = @fixbutton_initx - @width - mousex
-		if overdrag > 0  # Overdragged
-			overdrag_percent = 1 + overdrag/300
-			mousex = (mousex + (@fixbutton_initx-@width)*overdrag_percent)/(1+overdrag_percent)
-		targetx = @fixbutton_initx - mousex - @fixbutton_addx
-		targety = @fixbutton_inity - mousey - @fixbutton_addy
-
-		if @move_lock == "x"
-			targety = @fixbutton_inity
-		else if @move_lock == "y"
-			targetx = @fixbutton_initx
-
-		if not @move_lock or @move_lock == "x"
-			@fixbutton[0].style.left = (mousex + @fixbutton_addx) + "px"
-			if @tag
-				@tag[0].style.transform = "translateX(#{0 - targetx}px)"
-
-		if not @move_lock or @move_lock == "y"
-			@fixbutton[0].style.top = (mousey + @fixbutton_addy) + "px"
-			if @console.tag
-				@console.tag[0].style.transform = "translateY(#{0 - targety}px)"
-
-		#if @move_lock == "x"
-			# @fixbutton[0].style.left = "#{@fixbutton_targetx} px"
-			#@fixbutton[0].style.top = "#{@fixbutton_inity}px"
-		#if @move_lock == "y"
-		#	@fixbutton[0].style.top = "#{@fixbutton_targety} px"
-
-		# Check if opened
-		if (not @opened and targetx > @width/3) or (@opened and targetx > @width*0.9)
-			@fixbutton_targetx = @fixbutton_initx - @width  # Make it opened
-		else
-			@fixbutton_targetx = @fixbutton_initx
-
-		if (not @console.opened and 0 - targety > @page_height/10) or (@console.opened and 0 - targety > @page_height*0.8)
-			@fixbutton_targety = @page_height - @fixbutton_inity - 50
-		else
-			@fixbutton_targety = @fixbutton_inity
-
-
-	# Stop dragging the fixbutton
-	stopDrag: ->
-		@fixbutton.parents().off "mousemove touchmove"
-		@fixbutton.off "mousemove touchmove"
-		@fixbutton.css("pointer-events", "")
-		$(".drag-bg").remove()
-		if not @fixbutton.hasClass("dragging")
-			return
-		@fixbutton.removeClass("dragging")
-
-		# Move back to initial position
-		if @fixbutton_targetx != @fixbutton.offset().left or @fixbutton_targety != @fixbutton.offset().top
-			# Animate fixbutton
-			if @move_lock == "y"
-				top = @fixbutton_targety
-				left = @fixbutton_initx
-			if @move_lock == "x"
-				top = @fixbutton_inity
-				left = @fixbutton_targetx
-			@fixbutton.stop().animate {"left": left, "top": top}, 500, "easeOutBack", =>
-				# Switch back to auto align
-				if @fixbutton_targetx == @fixbutton_initx  # Closed
-					@fixbutton.css("left", "auto")
-				else  # Opened
-					@fixbutton.css("left", left)
-
-				$(".fixbutton-bg").trigger "mouseout"  # Switch fixbutton back to normal status
-
-			@stopDragX()
-			@console.stopDragY()
-		@move_lock = null
-
-	stopDragX: ->
-		# Animate sidebar and iframe
-		if @fixbutton_targetx == @fixbutton_initx or @move_lock == "y"
-			# Closed
-			targetx = 0
-			@opened = false
-		else
-			# Opened
-			targetx = @width
-			if @opened
-				@onOpened()
-			else
-				@when_loaded.done =>
-					@onOpened()
-			@opened = true
-
-		# Revent sidebar transitions
-		if @tag
-			@tag.css("transition", "0.4s ease-out")
-			@tag.css("transform", "translateX(-#{targetx}px)").one transitionEnd, =>
-				@tag.css("transition", "")
-				if not @opened
-					@container.remove()
-					@container = null
-					if @tag
-						@tag.remove()
-						@tag = null
-
-		# Revert body transformations
-		@log "stopdrag", "opened:", @opened
-		if not @opened
-			@onClosed()
-
-	sign: (inner_path, privatekey) ->
-		@wrapper.displayProgress("sign", "Signing: #{inner_path}...", 0)
-		@wrapper.ws.cmd "siteSign", {privatekey: privatekey, inner_path: inner_path, update_changed_files: true}, (res) =>
-			if res == "ok"
-				@wrapper.displayProgress("sign", "#{inner_path} signed!", 100)
-			else
-				@wrapper.displayProgress("sign", "Error signing #{inner_path}", -1)
-
-	publish: (inner_path, privatekey) ->
-		@wrapper.ws.cmd "sitePublish", {privatekey: privatekey, inner_path: inner_path, sign: true, update_changed_files: true}, (res) =>
-			if res == "ok"
-				@wrapper.notifications.add "sign", "done", "#{inner_path} Signed and published!", 5000
-
-	handleSiteDeleteClick: ->
-		if @wrapper.site_info.privatekey
-			question = "Are you sure?<br>This site has a saved private key"
-			options = ["Forget private key and delete site"]
-		else
-			question = "Are you sure?"
-			options = ["Delete this site", "Blacklist"]
-		@wrapper.displayConfirm question, options, (confirmed) =>
-			if confirmed == 1
-				@tag.find("#button-delete").addClass("loading")
-				@wrapper.ws.cmd "siteDelete", @wrapper.site_info.address, ->
-					document.location = $(".fixbutton-bg").attr("href")
-			else if confirmed == 2
-				@wrapper.displayPrompt "Blacklist this site", "text", "Delete and Blacklist", "Reason", (reason) =>
-					@tag.find("#button-delete").addClass("loading")
-					@wrapper.ws.cmd "siteblockAdd", [@wrapper.site_info.address, reason]
-					@wrapper.ws.cmd "siteDelete", @wrapper.site_info.address, ->
-						document.location = $(".fixbutton-bg").attr("href")
-
-	onOpened: ->
-		@log "Opened"
-		@scrollable()
-
-		# Re-calculate height when site admin opened or closed
-		@tag.find("#checkbox-owned, #checkbox-autodownloadoptional").off("click touchend").on "click touchend", =>
-			setTimeout (=>
-				@scrollable()
-			), 300
-
-		# Site limit button
-		@tag.find("#button-sitelimit").off("click touchend").on "click touchend", =>
-			@wrapper.ws.cmd "siteSetLimit", $("#input-sitelimit").val(), (res) =>
-				if res == "ok"
-					@wrapper.notifications.add "done-sitelimit", "done", "Site storage limit modified!", 5000
-				@updateHtmlTag()
-			return false
-
-		# Site autodownload limit button
-		@tag.find("#button-autodownload_bigfile_size_limit").off("click touchend").on "click touchend", =>
-			@wrapper.ws.cmd "siteSetAutodownloadBigfileLimit", $("#input-autodownload_bigfile_size_limit").val(), (res) =>
-				if res == "ok"
-					@wrapper.notifications.add "done-bigfilelimit", "done", "Site bigfile auto download limit modified!", 5000
-				@updateHtmlTag()
-			return false
-
-		# Site start download optional files
-		@tag.find("#button-autodownload_previous").off("click touchend").on "click touchend", =>
-			@wrapper.ws.cmd "siteUpdate", {"address": @wrapper.site_info.address, "check_files": true}, =>
-				@wrapper.notifications.add "done-download_optional", "done", "Optional files downloaded", 5000
-
-			@wrapper.notifications.add "start-download_optional", "info", "Optional files download started", 5000
-			return false
-
-		# Database reload
-		@tag.find("#button-dbreload").off("click touchend").on "click touchend", =>
-			@wrapper.ws.cmd "dbReload", [], =>
-				@wrapper.notifications.add "done-dbreload", "done", "Database schema reloaded!", 5000
-				@updateHtmlTag()
-			return false
-
-		# Database rebuild
-		@tag.find("#button-dbrebuild").off("click touchend").on "click touchend", =>
-			@wrapper.notifications.add "done-dbrebuild", "info", "Database rebuilding...."
-			@wrapper.ws.cmd "dbRebuild", [], =>
-				@wrapper.notifications.add "done-dbrebuild", "done", "Database rebuilt!", 5000
-				@updateHtmlTag()
-			return false
-
-		# Update site
-		@tag.find("#button-update").off("click touchend").on "click touchend", =>
-			@tag.find("#button-update").addClass("loading")
-			@wrapper.ws.cmd "siteUpdate", @wrapper.site_info.address, =>
-				@wrapper.notifications.add "done-updated", "done", "Site updated!", 5000
-				@tag.find("#button-update").removeClass("loading")
-			return false
-
-		# Pause site
-		@tag.find("#button-pause").off("click touchend").on "click touchend", =>
-			@tag.find("#button-pause").addClass("hidden")
-			@wrapper.ws.cmd "sitePause", @wrapper.site_info.address
-			return false
-
-		# Resume site
-		@tag.find("#button-resume").off("click touchend").on "click touchend", =>
-			@tag.find("#button-resume").addClass("hidden")
-			@wrapper.ws.cmd "siteResume", @wrapper.site_info.address
-			return false
-
-		# Delete site
-		@tag.find("#button-delete").off("click touchend").on "click touchend", =>
-			@handleSiteDeleteClick()
-			return false
-
-		# Owned checkbox
-		@tag.find("#checkbox-owned").off("click touchend").on "click touchend", =>
-			owned = @tag.find("#checkbox-owned").is(":checked")
-			@wrapper.ws.cmd "siteSetOwned", [owned], (res_set_owned) =>
-				@log "Owned", owned
-				if owned
-					@wrapper.ws.cmd "siteRecoverPrivatekey", [], (res_recover) =>
-						if res_recover == "ok"
-							@wrapper.notifications.add("recover", "done", "Private key recovered from master seed", 5000)
-						else
-							@log "Unable to recover private key: #{res_recover.error}"
-
-
-		# Owned auto download checkbox
-		@tag.find("#checkbox-autodownloadoptional").off("click touchend").on "click touchend", =>
-			@wrapper.ws.cmd "siteSetAutodownloadoptional", [@tag.find("#checkbox-autodownloadoptional").is(":checked")]
-
-		# Change identity button
-		@tag.find("#button-identity").off("click touchend").on "click touchend", =>
-			@wrapper.ws.cmd "certSelect"
-			return false
-
-		# Save settings
-		@tag.find("#button-settings").off("click touchend").on "click touchend", =>
-			@wrapper.ws.cmd "fileGet", "content.json", (res) =>
-				data = JSON.parse(res)
-				data["title"] = $("#settings-title").val()
-				data["description"] = $("#settings-description").val()
-				json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t')))
-				@wrapper.ws.cmd "fileWrite", ["content.json", btoa(json_raw), true], (res) =>
-					if res != "ok" # fileWrite failed
-						@wrapper.notifications.add "file-write", "error", "File write error: #{res}"
-					else
-						@wrapper.notifications.add "file-write", "done", "Site settings saved!", 5000
-						if @wrapper.site_info.privatekey
-							@wrapper.ws.cmd "siteSign", {privatekey: "stored", inner_path: "content.json", update_changed_files: true}
-						@updateHtmlTag()
-			return false
-
-
-		# Open site directory
-		@tag.find("#link-directory").off("click touchend").on "click touchend", =>
-			@wrapper.ws.cmd "serverShowdirectory", ["site", @wrapper.site_info.address]
-			return false
-
-		# Copy site with peers
-		@tag.find("#link-copypeers").off("click touchend").on "click touchend", (e) =>
-			copy_text = e.currentTarget.href
-			handler = (e) =>
-				e.clipboardData.setData('text/plain', copy_text)
-				e.preventDefault()
-				@wrapper.notifications.add "copy", "done", "Site address with peers copied to your clipboard", 5000
-				document.removeEventListener('copy', handler, true)
-
-			document.addEventListener('copy', handler, true)
-			document.execCommand('copy')
-			return false
-
-		# Sign and publish content.json
-		$(document).on "click touchend", =>
-			@tag?.find("#button-sign-publish-menu").removeClass("visible")
-			@tag?.find(".contents + .flex").removeClass("sign-publish-flex")
-
-		@tag.find(".contents-content").off("click touchend").on "click touchend", (e) =>
-			$("#input-contents").val(e.currentTarget.innerText);
-			return false;
-
-		menu = new Menu(@tag.find("#menu-sign-publish"))
-		menu.elem.css("margin-top", "-130px")  # Open upwards
-		menu.addItem "Sign", =>
-			inner_path = @tag.find("#input-contents").val()
-
-			@wrapper.ws.cmd "fileRules", {inner_path: inner_path}, (rules) =>
-				if @wrapper.site_info.auth_address in rules.signers
-					# ZeroID or other ID provider
-					@sign(inner_path)
-				else if @wrapper.site_info.privatekey
-					# Privatekey stored in users.json
-					@sign(inner_path, "stored")
-				else
-					# Ask the user for privatekey
-					@wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key
-						@sign(inner_path, privatekey)
-
-			@tag.find(".contents + .flex").removeClass "active"
-			menu.hide()
-
-		menu.addItem "Publish", =>
-			inner_path = @tag.find("#input-contents").val()
-			@wrapper.ws.cmd "sitePublish", {"inner_path": inner_path, "sign": false}
-
-			@tag.find(".contents + .flex").removeClass "active"
-			menu.hide()
-
-		@tag.find("#menu-sign-publish").off("click touchend").on "click touchend", =>
-			if window.visible_menu == menu
-				@tag.find(".contents + .flex").removeClass "active"
-				menu.hide()
-			else
-				@tag.find(".contents + .flex").addClass "active"
-				@tag.find(".content-wrapper").prop "scrollTop", 10000
-				menu.show()
-			return false
-
-		$("body").on "click", =>
-			if @tag
-				@tag.find(".contents + .flex").removeClass "active"
-
-		@tag.find("#button-sign-publish").off("click touchend").on "click touchend", =>
-			inner_path = @tag.find("#input-contents").val()
-
-			@wrapper.ws.cmd "fileRules", {inner_path: inner_path}, (rules) =>
-				if @wrapper.site_info.auth_address in rules.signers
-					# ZeroID or other ID provider
-					@publish(inner_path, null)
-				else if @wrapper.site_info.privatekey
-					# Privatekey stored in users.json
-					@publish(inner_path, "stored")
-				else
-					# Ask the user for privatekey
-					@wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key
-						@publish(inner_path, privatekey)
-			return false
-
-		# Close
-		@tag.find(".close").off("click touchend").on "click touchend", (e) =>
-			@close()
-			return false
-
-		@loadGlobe()
-
-	close: ->
-		@move_lock = "x"
-		@startDrag()
-		@stopDrag()
-
-
-	onClosed: ->
-		$(window).off "resize"
-		$(window).on "resize", @resized
-		$(document.body).css("transition", "0.6s ease-in-out").removeClass("body-sidebar").on transitionEnd, (e) =>
-			if e.target == document.body and not $(document.body).hasClass("body-sidebar") and not $(document.body).hasClass("body-console")
-				$(document.body).css("height", "auto").css("perspective", "").css("will-change", "").css("transition", "").off transitionEnd
-				@unloadGlobe()
-
-		# We dont need site info anymore
-		@wrapper.setSiteInfo = @original_set_site_info
-
-
-	loadGlobe: =>
-		if @tag.find(".globe").hasClass("loading")
-			setTimeout (=>
-				if typeof(DAT) == "undefined"  # Globe script not loaded, do it first
-					script_tag = $("<script>")
-					script_tag.attr("nonce", @wrapper.script_nonce)
-					script_tag.attr("src", "/uimedia/globe/all.js")
-					script_tag.on("load", @displayGlobe)
-					document.head.appendChild(script_tag[0])
-				else
-					@displayGlobe()
-			), 600
-
-
-	displayGlobe: =>
-		img = new Image();
-		img.src = "/uimedia/globe/world.jpg";
-		img.onload = =>
-			@wrapper.ws.cmd "sidebarGetPeers", [], (globe_data) =>
-				if @globe
-					@globe.scene.remove(@globe.points)
-					@globe.addData( globe_data, {format: 'magnitude', name: "hello", animated: false} )
-					@globe.createPoints()
-					@tag?.find(".globe").removeClass("loading")
-				else if typeof(DAT) != "undefined"
-					try
-						@globe = new DAT.Globe( @tag.find(".globe")[0], {"imgDir": "/uimedia/globe/"} )
-						@globe.addData( globe_data, {format: 'magnitude', name: "hello"} )
-						@globe.createPoints()
-						@globe.animate()
-					catch e
-						console.log "WebGL error", e
-						@tag?.find(".globe").addClass("error").text("WebGL not supported")
-					@tag?.find(".globe").removeClass("loading")
-
-
-
-	unloadGlobe: =>
-		if not @globe
-			return false
-		@globe.unload()
-		@globe = null
-
-
-wrapper = window.wrapper
-setTimeout ( ->
-	window.sidebar = new Sidebar(wrapper)
-), 500
-
-
-window.transitionEnd = 'transitionend webkitTransitionEnd oTransitionEnd otransitionend'
diff --git a/plugins/UiConfig/media/js/ConfigStorage.coffee b/plugins/UiConfig/media/js/ConfigStorage.coffee
deleted file mode 100644
index 92327001..00000000
--- a/plugins/UiConfig/media/js/ConfigStorage.coffee
+++ /dev/null
@@ -1,222 +0,0 @@
-class ConfigStorage extends Class
-	constructor: (@config) ->
-		@items = []
-		@createSections()
-		@setValues(@config)
-
-	setValues: (values) ->
-		for section in @items
-			for item in section.items
-				if not values[item.key]
-					continue
-				item.value = @formatValue(values[item.key].value)
-				item.default = @formatValue(values[item.key].default)
-				item.pending = values[item.key].pending
-				values[item.key].item = item
-
-	formatValue: (value) ->
-		if not value
-			return false
-		else if typeof(value) == "object"
-			return value.join("\n")
-		else if typeof(value) == "number"
-			return value.toString()
-		else
-			return value
-
-	deformatValue: (value, type) ->
-		if type == "object" and typeof(value) == "string"
-			if not value.length
-				return value = null
-			else
-				return value.split("\n")
-		if type == "boolean" and not value
-			return false
-		else if type == "number"
-			if typeof(value) == "number"
-				return value.toString()
-			else if not value
-				return "0"
-			else
-				return value
-		else
-			return value
-
-	createSections: ->
-		# Web Interface
-		section = @createSection("Web Interface")
-
-		section.items.push
-			key: "open_browser"
-			title: "Open web browser on ZeroNet startup"
-			type: "checkbox"
-
-		# Network
-		section = @createSection("Network")
-		section.items.push
-			key: "offline"
-			title: "Offline mode"
-			type: "checkbox"
-			description: "Disable network communication."
-
-		section.items.push
-			key: "fileserver_ip_type"
-			title: "File server network"
-			type: "select"
-			options: [
-				{title: "IPv4", value: "ipv4"}
-				{title: "IPv6", value: "ipv6"}
-				{title: "Dual (IPv4 & IPv6)", value: "dual"}
-			]
-			description: "Accept incoming peers using IPv4 or IPv6 address. (default: dual)"
-
-		section.items.push
-			key: "fileserver_port"
-			title: "File server port"
-			type: "text"
-			valid_pattern: /[0-9]*/
-			description: "Other peers will use this port to reach your served sites. (default: randomize)"
-
-		section.items.push
-			key: "ip_external"
-			title: "File server external ip"
-			type: "textarea"
-			placeholder: "Detect automatically"
-			description: "Your file server is accessible on these ips. (default: detect automatically)"
-
-		section.items.push
-			title: "Tor"
-			key: "tor"
-			type: "select"
-			options: [
-				{title: "Disable", value: "disable"}
-				{title: "Enable", value: "enable"}
-				{title: "Always", value: "always"}
-			]
-			description: [
-				"Disable: Don't connect to peers on Tor network", h("br"),
-				"Enable: Only use Tor for Tor network peers", h("br"),
-				"Always: Use Tor for every connections to hide your IP address (slower)"
-			]
-
-		section.items.push
-			title: "Use Tor bridges"
-			key: "tor_use_bridges"
-			type: "checkbox"
-			description: "Use obfuscated bridge relays to avoid network level Tor block (even slower)"
-			isHidden: ->
-				return not Page.server_info.tor_has_meek_bridges
-
-		section.items.push
-			title: "Trackers"
-			key: "trackers"
-			type: "textarea"
-			description: "Discover new peers using these adresses"
-
-		section.items.push
-			title: "Trackers files"
-			key: "trackers_file"
-			type: "textarea"
-			description: "Load additional list of torrent trackers dynamically, from a file"
-			placeholder: "Eg.: {data_dir}/trackers.json"
-			value_pos: "fullwidth"
-
-		section.items.push
-			title: "Proxy for tracker connections"
-			key: "trackers_proxy"
-			type: "select"
-			options: [
-				{title: "Custom", value: ""}
-				{title: "Tor", value: "tor"}
-				{title: "Disable", value: "disable"}
-			]
-			isHidden: ->
-				Page.values["tor"] == "always"
-
-		section.items.push
-			title: "Custom socks proxy address for trackers"
-			key: "trackers_proxy"
-			type: "text"
-			placeholder: "Eg.: 127.0.0.1:1080"
-			value_pos: "fullwidth"
-			valid_pattern: /.+:[0-9]+/
-			isHidden: =>
-				Page.values["trackers_proxy"] in ["tor", "disable"]
-
-		# Performance
-		section = @createSection("Performance")
-
-		section.items.push
-			key: "log_level"
-			title: "Level of logging to file"
-			type: "select"
-			options: [
-				{title: "Everything", value: "DEBUG"}
-				{title: "Only important messages", value: "INFO"}
-				{title: "Only errors", value: "ERROR"}
-			]
-
-		section.items.push
-			key: "threads_fs_read"
-			title: "Threads for async file system reads"
-			type: "select"
-			options: [
-				{title: "Sync read", value: 0}
-				{title: "1 thread", value: 1}
-				{title: "2 threads", value: 2}
-				{title: "3 threads", value: 3}
-				{title: "4 threads", value: 4}
-				{title: "5 threads", value: 5}
-				{title: "10 threads", value: 10}
-			]
-
-		section.items.push
-			key: "threads_fs_write"
-			title: "Threads for async file system writes"
-			type: "select"
-			options: [
-				{title: "Sync write", value: 0}
-				{title: "1 thread", value: 1}
-				{title: "2 threads", value: 2}
-				{title: "3 threads", value: 3}
-				{title: "4 threads", value: 4}
-				{title: "5 threads", value: 5}
-				{title: "10 threads", value: 10}
-			]
-
-		section.items.push
-			key: "threads_crypt"
-			title: "Threads for cryptographic functions"
-			type: "select"
-			options: [
-				{title: "Sync execution", value: 0}
-				{title: "1 thread", value: 1}
-				{title: "2 threads", value: 2}
-				{title: "3 threads", value: 3}
-				{title: "4 threads", value: 4}
-				{title: "5 threads", value: 5}
-				{title: "10 threads", value: 10}
-			]
-
-		section.items.push
-			key: "threads_db"
-			title: "Threads for database operations"
-			type: "select"
-			options: [
-				{title: "Sync execution", value: 0}
-				{title: "1 thread", value: 1}
-				{title: "2 threads", value: 2}
-				{title: "3 threads", value: 3}
-				{title: "4 threads", value: 4}
-				{title: "5 threads", value: 5}
-				{title: "10 threads", value: 10}
-			]
-
-	createSection: (title) =>
-		section = {}
-		section.title = title
-		section.items = []
-		@items.push(section)
-		return section
-
-window.ConfigStorage = ConfigStorage
diff --git a/plugins/UiConfig/media/js/ConfigView.coffee b/plugins/UiConfig/media/js/ConfigView.coffee
deleted file mode 100644
index 64b86e5e..00000000
--- a/plugins/UiConfig/media/js/ConfigView.coffee
+++ /dev/null
@@ -1,124 +0,0 @@
-class ConfigView extends Class
-	constructor: () ->
-		@
-
-	render: ->
-		@config_storage.items.map @renderSection
-
-	renderSection: (section) =>
-		h("div.section", {key: section.title}, [
-			h("h2", section.title),
-			h("div.config-items", section.items.map @renderSectionItem)
-		])
-
-	handleResetClick: (e) =>
-		node = e.currentTarget
-		config_key = node.attributes.config_key.value
-		default_value = node.attributes.default_value?.value
-		Page.cmd "wrapperConfirm", ["Reset #{config_key} value?", "Reset to default"], (res) =>
-			if (res)
-				@values[config_key] = default_value
-			Page.projector.scheduleRender()
-
-	renderSectionItem: (item) =>
-		value_pos = item.value_pos
-
-		if item.type == "textarea"
-			value_pos ?= "fullwidth"
-		else
-			value_pos ?= "right"
-
-		value_changed = @config_storage.formatValue(@values[item.key]) != item.value
-		value_default = @config_storage.formatValue(@values[item.key]) == item.default
-
-		if item.key in ["open_browser", "fileserver_port"]  # Value default for some settings makes no sense
-			value_default = true
-
-		marker_title = "Changed from default value: #{item.default} -> #{@values[item.key]}"
-		if item.pending
-			marker_title += " (change pending until client restart)"
-
-		if item.isHidden?()
-			return null
-
-		h("div.config-item", {key: item.title, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUpInout}, [
-			h("div.title", [
-				h("h3", item.title),
-				h("div.description", item.description)
-			])
-			h("div.value.value-#{value_pos}",
-				if item.type == "select"
-					@renderValueSelect(item)
-				else if item.type == "checkbox"
-					@renderValueCheckbox(item)
-				else if item.type == "textarea"
-					@renderValueTextarea(item)
-				else
-					@renderValueText(item)
-				h("a.marker", {
-					href: "#Reset", title: marker_title,
-					onclick: @handleResetClick, config_key: item.key, default_value: item.default,
-					classes: {default: value_default, changed: value_changed, visible: not value_default or value_changed or item.pending, pending: item.pending}
-				}, "\u2022")
-			)
-		])
-
-	# Values
-	handleInputChange: (e) =>
-		node = e.target
-		config_key = node.attributes.config_key.value
-		@values[config_key] = node.value
-		Page.projector.scheduleRender()
-
-	handleCheckboxChange: (e) =>
-		node = e.currentTarget
-		config_key = node.attributes.config_key.value
-		value = not node.classList.contains("checked")
-		@values[config_key] = value
-		Page.projector.scheduleRender()
-
-	renderValueText: (item) =>
-		value = @values[item.key]
-		if not value
-			value = ""
-		h("input.input-#{item.type}", {type: item.type, config_key: item.key, value: value, placeholder: item.placeholder, oninput: @handleInputChange})
-
-	autosizeTextarea: (e) =>
-		if e.currentTarget
-			# @handleInputChange(e)
-			node = e.currentTarget
-		else
-			node = e
-		height_before = node.style.height
-		if height_before
-			node.style.height = "0px"
-		h = node.offsetHeight
-		scrollh = node.scrollHeight + 20
-		if scrollh > h
-			node.style.height = scrollh + "px"
-		else
-			node.style.height = height_before
-
-	renderValueTextarea: (item) =>
-		value = @values[item.key]
-		if not value
-			value = ""
-		h("textarea.input-#{item.type}.input-text",{
-			type: item.type, config_key: item.key, oninput: @handleInputChange, afterCreate: @autosizeTextarea,
-			updateAnimation: @autosizeTextarea, value: value, placeholder: item.placeholder
-		})
-
-	renderValueCheckbox: (item) =>
-		if @values[item.key] and @values[item.key] != "False"
-			checked = true
-		else
-			checked = false
-		h("div.checkbox", {onclick: @handleCheckboxChange, config_key: item.key, classes: {checked: checked}}, h("div.checkbox-skin"))
-
-	renderValueSelect: (item) =>
-		h("select.input-select", {config_key: item.key, oninput: @handleInputChange},
-			item.options.map (option) =>
-				h("option", {selected: option.value.toString() == @values[item.key], value: option.value}, option.title)
-		)
-
-window.ConfigView = ConfigView
\ No newline at end of file
diff --git a/plugins/UiConfig/media/js/UiConfig.coffee b/plugins/UiConfig/media/js/UiConfig.coffee
deleted file mode 100644
index bc1ac697..00000000
--- a/plugins/UiConfig/media/js/UiConfig.coffee
+++ /dev/null
@@ -1,129 +0,0 @@
-window.h = maquette.h
-
-class UiConfig extends ZeroFrame
-	init: ->
-		@save_visible = true
-		@config = null  # Setting currently set on the server
-		@values = null  # Entered values on the page
-		@config_view = new ConfigView()
-		window.onbeforeunload = =>
-			if @getValuesChanged().length > 0
-				return true
-			else
-				return null
-
-	onOpenWebsocket: =>
-		@cmd("wrapperSetTitle", "Config - ZeroNet")
-		@cmd "serverInfo", {}, (server_info) =>
-			@server_info = server_info
-		@restart_loading = false
-		@updateConfig()
-
-	updateConfig: (cb) =>
-		@cmd "configList", [], (res) =>
-			@config = res
-			@values = {}
-			@config_storage = new ConfigStorage(@config)
-			@config_view.values = @values
-			@config_view.config_storage = @config_storage
-			for key, item of res
-				value = item.value
-				@values[key] = @config_storage.formatValue(value)
-			@projector.scheduleRender()
-			cb?()
-
-	createProjector: =>
-		@projector = maquette.createProjector()
-		@projector.replace($("#content"), @render)
-		@projector.replace($("#bottom-save"), @renderBottomSave)
-		@projector.replace($("#bottom-restart"), @renderBottomRestart)
-
-	getValuesChanged: =>
-		values_changed = []
-		for key, value of @values
-			if @config_storage.formatValue(value) != @config_storage.formatValue(@config[key]?.value)
-				values_changed.push({key: key, value: value})
-		return values_changed
-
-	getValuesPending: =>
-		values_pending = []
-		for key, item of @config
-			if item.pending
-				values_pending.push(key)
-		return values_pending
-
-	saveValues: (cb) =>
-		changed_values = @getValuesChanged()
-		for item, i in changed_values
-			last = i == changed_values.length - 1
-			value = @config_storage.deformatValue(item.value, typeof(@config[item.key].default))
-			default_value = @config_storage.deformatValue(@config[item.key].default, typeof(@config[item.key].default))
-			value_same_as_default = JSON.stringify(default_value) == JSON.stringify(value)
-
-			if @config[item.key].item.valid_pattern and not @config[item.key].item.isHidden?()
-				match = value.match(@config[item.key].item.valid_pattern)
-				if not match or match[0] != value
-					message = "Invalid value of #{@config[item.key].item.title}: #{value} (does not matches #{@config[item.key].item.valid_pattern})"
-					Page.cmd("wrapperNotification", ["error", message])
-					cb(false)
-					break
-
-			if value_same_as_default
-				value = null
-
-			@saveValue(item.key, value, if last then cb else null)
-
-	saveValue: (key, value, cb) =>
-		if key == "open_browser"
-			if value
-				value = "default_browser"
-			else
-				value = "False"
-
-		Page.cmd "configSet", [key, value], (res) =>
-			if res != "ok"
-				Page.cmd "wrapperNotification", ["error", res.error]
-			cb?(true)
-
-	render: =>
-		if not @config
-			return h("div.content")
-
-		h("div.content", [
-			@config_view.render()
-		])
-
-	handleSaveClick: =>
-		@save_loading = true
-		@logStart "Save"
-		@saveValues (success) =>
-			@save_loading = false
-			@logEnd "Save"
-			if success
-				@updateConfig()
-			Page.projector.scheduleRender()
-		return false
-
-	renderBottomSave: =>
-		values_changed = @getValuesChanged()
-		h("div.bottom.bottom-save", {classes: {visible: values_changed.length}}, h("div.bottom-content", [
-			h("div.title", "#{values_changed.length} configuration item value changed"),
-			h("a.button.button-submit.button-save", {href: "#Save", classes: {loading: @save_loading}, onclick: @handleSaveClick}, "Save settings")
-		]))
-
-	handleRestartClick: =>
-		@restart_loading = true
-		Page.cmd("serverShutdown", {restart: true})
-		Page.projector.scheduleRender()
-		return false
-
-	renderBottomRestart: =>
-		values_pending = @getValuesPending()
-		values_changed = @getValuesChanged()
-		h("div.bottom.bottom-restart", {classes: {visible: values_pending.length and not values_changed.length}}, h("div.bottom-content", [
-			h("div.title", "Some changed settings requires restart"),
-			h("a.button.button-submit.button-restart", {href: "#Restart", classes: {loading: @restart_loading}, onclick: @handleRestartClick}, "Restart ZeroNet client")
-		]))
-
-window.Page = new UiConfig()
-window.Page.createProjector()
diff --git a/plugins/UiConfig/media/js/lib/Class.coffee b/plugins/UiConfig/media/js/lib/Class.coffee
deleted file mode 100644
index d62ab25c..00000000
--- a/plugins/UiConfig/media/js/lib/Class.coffee
+++ /dev/null
@@ -1,23 +0,0 @@
-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
\ No newline at end of file
diff --git a/plugins/UiConfig/media/js/lib/Promise.coffee b/plugins/UiConfig/media/js/lib/Promise.coffee
deleted file mode 100644
index 136e3ec7..00000000
--- a/plugins/UiConfig/media/js/lib/Promise.coffee
+++ /dev/null
@@ -1,74 +0,0 @@
-# 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
-###
\ No newline at end of file
diff --git a/plugins/UiConfig/media/js/lib/Prototypes.coffee b/plugins/UiConfig/media/js/lib/Prototypes.coffee
deleted file mode 100644
index 034add50..00000000
--- a/plugins/UiConfig/media/js/lib/Prototypes.coffee
+++ /dev/null
@@ -1,8 +0,0 @@
-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
diff --git a/plugins/UiConfig/media/js/utils/Animation.coffee b/plugins/UiConfig/media/js/utils/Animation.coffee
deleted file mode 100644
index 271b88c1..00000000
--- a/plugins/UiConfig/media/js/utils/Animation.coffee
+++ /dev/null
@@ -1,138 +0,0 @@
-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()
\ No newline at end of file
diff --git a/plugins/UiConfig/media/js/utils/Dollar.coffee b/plugins/UiConfig/media/js/utils/Dollar.coffee
deleted file mode 100644
index 7f19f551..00000000
--- a/plugins/UiConfig/media/js/utils/Dollar.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-window.$ = (selector) ->
-	if selector.startsWith("#")
-		return document.getElementById(selector.replace("#", ""))
diff --git a/plugins/UiConfig/media/js/utils/ZeroFrame.coffee b/plugins/UiConfig/media/js/utils/ZeroFrame.coffee
deleted file mode 100644
index 11512d16..00000000
--- a/plugins/UiConfig/media/js/utils/ZeroFrame.coffee
+++ /dev/null
@@ -1,85 +0,0 @@
-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
diff --git a/plugins/UiFileManager/media/js/Config.coffee b/plugins/UiFileManager/media/js/Config.coffee
deleted file mode 100644
index 0e156bef..00000000
--- a/plugins/UiFileManager/media/js/Config.coffee
+++ /dev/null
@@ -1,15 +0,0 @@
-window.BINARY_EXTENSIONS = [
-	"3dm", "3ds", "3g2", "3gp", "7z", "a", "aac", "adp", "ai", "aif", "aiff", "alz", "ape", "apk", "appimage", "ar", "arj", "asc", "asf", "au", "avi", "bak",
-	"baml", "bh", "bin", "bk", "bmp", "btif", "bz2", "bzip2", "cab", "caf", "cgm", "class", "cmx", "cpio", "cr2", "cur", "dat", "dcm", "deb", "dex", "djvu",
-	"dll", "dmg", "dng", "doc", "docm", "docx", "dot", "dotm", "dra", "DS_Store", "dsk", "dts", "dtshd", "dvb", "dwg", "dxf", "ecelp4800", "ecelp7470",
-	"ecelp9600", "egg", "eol", "eot", "epub", "exe", "f4v", "fbs", "fh", "fla", "flac", "flatpak", "fli", "flv", "fpx", "fst", "fvt", "g3", "gh", "gif",
-	"gpg", "graffle", "gz", "gzip", "h261", "h263", "h264", "icns", "ico", "ief", "img", "ipa", "iso", "jar", "jpeg", "jpg", "jpgv", "jpm", "jxr", "key",
-	"ktx", "lha", "lib", "lvp", "lz", "lzh", "lzma", "lzo", "m3u", "m4a", "m4v", "mar", "mdi", "mht", "mid", "midi", "mj2", "mka", "mkv", "mmr", "mng",
-	"mobi", "mov", "movie", "mp3", "mp4", "mp4a", "mpeg", "mpg", "mpga", "msgpack", "mxu", "nef", "npx", "numbers", "nupkg", "o", "oga", "ogg", "ogv",
-	"otf", "pages", "pbm", "pcx", "pdb", "pdf", "pea", "pgm", "pic", "png", "pnm", "pot", "potm", "potx", "ppa", "ppam", "ppm", "pps", "ppsm", "ppsx",
-	"ppt", "pptm", "pptx", "psd", "pya", "pyc", "pyo", "pyv", "qt", "rar", "ras", "raw", "resources", "rgb", "rip", "rlc", "rmf", "rmvb", "rpm", "rtf",
-	"rz", "s3m", "s7z", "scpt", "sgi", "shar", "sig", "sil", "sketch", "slk", "smv", "snap", "snk", "so", "stl", "sub", "suo", "swf", "tar", "tbz2", "tbz",
-	"tga", "tgz", "thmx", "tif", "tiff", "tlz", "ttc", "ttf", "txz", "udf", "uvh", "uvi", "uvm", "uvp", "uvs", "uvu", "viv", "vob", "war", "wav", "wax",
-	"wbmp", "wdp", "weba", "webm", "webp", "whl", "wim", "wm", "wma", "wmv", "wmx", "woff2", "woff", "wrm", "wvx", "xbm", "xif", "xla", "xlam", "xls",
-	"xlsb", "xlsm", "xlsx", "xlt", "xltm", "xltx", "xm", "xmind", "xpi", "xpm", "xwd", "xz", "z", "zip", "zipx"
-]
diff --git a/plugins/UiFileManager/media/js/FileEditor.coffee b/plugins/UiFileManager/media/js/FileEditor.coffee
deleted file mode 100644
index 3166881d..00000000
--- a/plugins/UiFileManager/media/js/FileEditor.coffee
+++ /dev/null
@@ -1,179 +0,0 @@
-class FileEditor extends Class
-	constructor: (@inner_path) ->
-		@need_update = true
-		@on_loaded = new Promise()
-		@is_loading = false
-		@content = ""
-		@node_cm = null
-		@cm = null
-		@error = null
-		@is_loaded = false
-		@is_modified = false
-		@is_saving = false
-		@mode = "Loading"
-
-	update: ->
-		is_required = Page.url_params.get("edit_mode") != "new"
-
-		Page.cmd "fileGet", {inner_path: @inner_path, required: is_required}, (res) =>
-			if res?.error
-				@error = res.error
-				@content = res.error
-				@log "Error loading: #{@error}"
-			else
-				if res
-					@content = res
-				else
-					@content = ""
-					@mode = "Create"
-			if not @content
-				@cm.getDoc().clearHistory()
-			@cm.setValue(@content)
-			if not @error
-				@is_loaded = true
-			Page.projector.scheduleRender()
-
-	isModified: =>
-		return @content != @cm.getValue()
-
-	storeCmNode: (node) =>
-		@node_cm = node
-
-	getMode: (inner_path) ->
-		ext = inner_path.split(".").pop()
-		types = {
-			"py": "python",
-			"json": "application/json",
-			"js": "javascript",
-			"coffee": "coffeescript",
-			"html": "htmlmixed",
-			"htm": "htmlmixed",
-			"php": "htmlmixed",
-			"rs": "rust",
-			"css": "css",
-			"md": "markdown",
-			"xml": "xml",
-			"svg": "xml"
-		}
-		return types[ext]
-
-	foldJson: (from, to) =>
-		@log "foldJson", from, to
-		# Get open / close token
-		startToken = '{'
-		endToken = '}'
-		prevLine = @cm.getLine(from.line)
-		if prevLine.lastIndexOf('[') > prevLine.lastIndexOf('{')
-		  startToken = '['
-		  endToken = ']'
-
-		# Get json content
-		internal = @cm.getRange(from, to)
-		toParse = startToken + internal + endToken
-
-		#Get key count
-		try
-			parsed = JSON.parse(toParse)
-			count = Object.keys(parsed).length
-		catch e
-			null
-
-		return if count then "\u21A4#{count}\u21A6" else "\u2194"
-
-	createCodeMirror: ->
-		mode = @getMode(@inner_path)
-		@log "Creating CodeMirror", @inner_path, mode
-		options = {
-			value: "Loading...",
-			mode: mode,
-			lineNumbers: true,
-			styleActiveLine: true,
-			matchBrackets: true,
-			keyMap: "sublime",
-			theme: "mdn-like",
-			extraKeys: {"Ctrl-Space": "autocomplete"},
-			foldGutter: true,
-			gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"]
-
-		}
-		if mode == "application/json"
-			options.gutters.unshift("CodeMirror-lint-markers")
-			options.lint = true
-			options.foldOptions = { widget: @foldJson }
-
-		@cm = CodeMirror(@node_cm, options)
-		@cm.on "changes", (changes) =>
-			if @is_loaded and not @is_modified
-				@is_modified = true
-				Page.projector.scheduleRender()
-
-
-	loadEditor: ->
-		if not @is_loading
-			document.getElementsByTagName("head")[0].insertAdjacentHTML(
-				"beforeend",
-				"""<link rel="stylesheet" href="codemirror/all.css" />"""
-			)
-			script = document.createElement('script')
-			script.src = "codemirror/all.js"
-			script.onload = =>
-				@createCodeMirror()
-				@on_loaded.resolve()
-			document.head.appendChild(script)
-		return @on_loaded
-
-	handleSidebarButtonClick: =>
-		Page.is_sidebar_closed = not Page.is_sidebar_closed
-		return false
-
-	handleSaveClick: =>
-		num_errors = (mark for mark in Page.file_editor.cm.getAllMarks() when mark.className == "CodeMirror-lint-mark-error").length
-		if num_errors > 0
-			Page.cmd "wrapperConfirm", ["<b>Warning:</b> The file looks invalid.", "Save anyway"], @save
-		else
-			@save()
-		return false
-
-	save: =>
-		Page.projector.scheduleRender()
-		@is_saving = true
-		Page.cmd "fileWrite", [@inner_path, Text.fileEncode(@cm.getValue())], (res) =>
-			@is_saving = false
-			if res.error
-				Page.cmd "wrapperNotification", ["error", "Error saving #{res.error}"]
-			else
-				@is_save_done = true
-				setTimeout (() =>
-					@is_save_done = false
-					Page.projector.scheduleRender()
-				), 2000
-				@content = @cm.getValue()
-				@is_modified = false
-				if @mode == "Create"
-					@mode = "Edit"
-				Page.file_list.need_update = true
-			Page.projector.scheduleRender()
-
-	render: ->
-		if @need_update
-			@loadEditor().then =>
-				@update()
-			@need_update = false
-		h("div.editor", {afterCreate: @storeCmNode, classes: {error: @error, loaded: @is_loaded}}, [
-			h("a.sidebar-button", {href: "#Sidebar", onclick: @handleSidebarButtonClick}, h("span", "\u2039")),
-			h("div.editor-head", [
-				if @mode in ["Edit", "Create"]
-					h("a.save.button",
-						{href: "#Save", classes: {loading: @is_saving, done: @is_save_done, disabled: not @is_modified}, onclick: @handleSaveClick},
-						if @is_save_done then "Save: done!" else "Save"
-					)
-				h("span.title", @mode, ": ", @inner_path)
-			]),
-			if @error
-				h("div.error-message",
-					h("h2", "Unable to load the file: #{@error}")
-					h("a", {href: Page.file_list.getHref(@inner_path)}, "View in browser")
-				)
-		])
-
-window.FileEditor = FileEditor
\ No newline at end of file
diff --git a/plugins/UiFileManager/media/js/FileItemList.coffee b/plugins/UiFileManager/media/js/FileItemList.coffee
deleted file mode 100644
index df22b81a..00000000
--- a/plugins/UiFileManager/media/js/FileItemList.coffee
+++ /dev/null
@@ -1,194 +0,0 @@
-class FileItemList extends Class
-	constructor: (@inner_path) ->
-		@items = []
-		@updating = false
-		@files_modified = {}
-		@dirs_modified = {}
-		@files_added = {}
-		@dirs_added = {}
-		@files_optional = {}
-		@items_by_name = {}
-
-	# Update item list
-	update: (cb) ->
-		@updating = true
-		@logStart("Updating dirlist")
-		Page.cmd "dirList", {inner_path: @inner_path, stats: true}, (res) =>
-			if res.error
-				@error = res.error
-			else
-				@error = null
-				pattern_ignore = RegExp("^" + Page.site_info.content?.ignore)
-
-				@items.splice(0, @items.length)  # Remove all items
-
-				@items_by_name = {}
-				for row in res
-					row.type = @getFileType(row)
-					row.inner_path = @inner_path + row.name
-					if Page.site_info.content?.ignore and row.inner_path.match(pattern_ignore)
-						row.ignored = true
-					@items.push(row)
-					@items_by_name[row.name] = row
-
-				@sort()
-
-			if Page.site_info?.settings?.own
-				@updateAddedFiles()
-
-			@updateOptionalFiles =>
-				@updating = false
-				cb?()
-				@logEnd("Updating dirlist", @inner_path)
-				Page.projector.scheduleRender()
-
-				@updateModifiedFiles =>
-					Page.projector.scheduleRender()
-
-
-	updateModifiedFiles: (cb) =>
-		# Add modified attribute to changed files
-		Page.cmd "siteListModifiedFiles", [], (res) =>
-			@files_modified = {}
-			@dirs_modified = {}
-			for inner_path in res.modified_files
-				@files_modified[inner_path] = true
-				dir_inner_path = ""
-				dir_parts = inner_path.split("/")
-				for dir_part in dir_parts[..-2]
-					if dir_inner_path
-						dir_inner_path += "/#{dir_part}"
-					else
-						dir_inner_path = dir_part
-					@dirs_modified[dir_inner_path] = true
-
-			cb?()
-
-	# Update newly added items list since last sign
-	updateAddedFiles: =>
-		Page.cmd "fileGet", "content.json", (res) =>
-			if not res
-				return false
-
-			content = JSON.parse(res)
-
-			# Check new files
-			if not content.files?
-				return false
-
-			@files_added = {}
-
-			for file in @items
-				if file.name == "content.json" or file.is_dir
-					continue
-				if not content.files[@inner_path + file.name]
-					@files_added[@inner_path + file.name] = true
-
-			# Check new dirs
-			@dirs_added = {}
-
-			dirs_content = {}
-			for file_name of Object.assign({}, content.files, content.files_optional)
-				if not file_name.startsWith(@inner_path)
-					continue
-
-				pattern = new RegExp("#{@inner_path}(.*?)/")
-				match = file_name.match(pattern)
-
-				if not match
-					continue
-
-				dirs_content[match[1]] = true
-
-			for file in @items
-				if not file.is_dir
-					continue
-				if not dirs_content[file.name]
-					@dirs_added[@inner_path + file.name] = true
-
-	# Update optional files list
-	updateOptionalFiles: (cb) =>
-		Page.cmd "optionalFileList", {filter: ""}, (res) =>
-			@files_optional = {}
-			for optional_file in res
-				@files_optional[optional_file.inner_path] = optional_file
-
-			@addOptionalFilesToItems()
-
-			cb?()
-
-	# Add optional files to item list
-	addOptionalFilesToItems: =>
-		is_added = false
-		for inner_path, optional_file of @files_optional
-			if optional_file.inner_path.startsWith(@inner_path)
-				if @getDirectory(optional_file.inner_path) == @inner_path
-					# Add optional file to list
-					file_name = @getFileName(optional_file.inner_path)
-					if not @items_by_name[file_name]
-						row = {
-							"name": file_name, "type": "file", "optional_empty": true,
-							"size": optional_file.size, "is_dir": false, "inner_path": optional_file.inner_path
-						}
-						@items.push(row)
-						@items_by_name[file_name] = row
-						is_added = true
-				else
-					# Add optional dir to list
-					dir_name = optional_file.inner_path.replace(@inner_path, "").match(/(.*?)\//, "")?[1]
-					if dir_name and not @items_by_name[dir_name]
-						row = {
-							"name": dir_name, "type": "dir", "optional_empty": true,
-							"size": 0, "is_dir": true, "inner_path": optional_file.inner_path
-						}
-						@items.push(row)
-						@items_by_name[dir_name] = row
-						is_added = true
-
-		if is_added
-			@sort()
-
-	getFileType: (file) =>
-		if file.is_dir
-			return "dir"
-		else
-			return "unknown"
-
-	getDirectory: (inner_path) ->
-		if inner_path.indexOf("/") != -1
-			return inner_path.replace(/^(.*\/)(.*?)$/, "$1")
-		else
-			return ""
-
-	getFileName: (inner_path) ->
-		return inner_path.replace(/^(.*\/)(.*?)$/, "$2")
-
-
-	isModified: (inner_path) =>
-		return @files_modified[inner_path] or @dirs_modified[inner_path]
-
-	isAdded: (inner_path) =>
-		return @files_added[inner_path] or @dirs_added[inner_path]
-
-	hasPermissionDelete: (file) =>
-		if file.type in ["dir", "parent"]
-			return false
-
-		if file.inner_path == "content.json"
-			return false
-
-		optional_info = @getOptionalInfo(file.inner_path)
-		if optional_info and optional_info.downloaded_percent > 0
-			return true
-		else
-			return Page.site_info?.settings?.own
-
-	getOptionalInfo: (inner_path) =>
-		return @files_optional[inner_path]
-
-	sort: =>
-		@items.sort (a, b) ->
-			return (b.is_dir - a.is_dir) || a.name.localeCompare(b.name)
-
-
-window.FileItemList = FileItemList
\ No newline at end of file
diff --git a/plugins/UiFileManager/media/js/FileList.coffee b/plugins/UiFileManager/media/js/FileList.coffee
deleted file mode 100644
index 1b5089b0..00000000
--- a/plugins/UiFileManager/media/js/FileList.coffee
+++ /dev/null
@@ -1,268 +0,0 @@
-class FileList extends Class
-	constructor: (@site, @inner_path, @is_owner=false) ->
-		@need_update = true
-		@error = null
-		@url_root = "/list/" + @site + "/"
-		if @inner_path
-			@inner_path += "/"
-			@url_root += @inner_path
-		@log("inited", @url_root)
-		@item_list = new FileItemList(@inner_path)
-		@item_list.items = @item_list.items
-		@menu_create = new Menu()
-
-		@select_action = null
-		@selected = {}
-		@selected_items_num = 0
-		@selected_items_size = 0
-		@selected_optional_empty_num = 0
-
-	isSelectedAll: ->
-		false
-
-	update: =>
-		@item_list.update =>
-			document.body.classList.add("loaded")
-
-	getHref: (inner_path) =>
-		return "/" + @site + "/" + inner_path
-
-	getListHref: (inner_path) =>
-		return "/list/" + @site + "/" + inner_path
-
-	getEditHref: (inner_path, mode=null) =>
-		href = @url_root + "?file=" + inner_path
-		if mode
-			href += "&edit_mode=#{mode}"
-		return href
-
-	checkSelectedItems: =>
-		@selected_items_num = 0
-		@selected_items_size = 0
-		@selected_optional_empty_num = 0
-		for item in @item_list.items
-			if @selected[item.inner_path]
-				@selected_items_num += 1
-				@selected_items_size += item.size
-				optional_info = @item_list.getOptionalInfo(item.inner_path)
-				if optional_info and not optional_info.downloaded_percent > 0
-					@selected_optional_empty_num += 1
-
-	handleMenuCreateClick: =>
-		@menu_create.items = []
-		@menu_create.items.push ["File", @handleNewFileClick]
-		@menu_create.items.push ["Directory", @handleNewDirectoryClick]
-		@menu_create.toggle()
-		return false
-
-	handleNewFileClick: =>
-		Page.cmd "wrapperPrompt", "New file name:", (file_name) =>
-			window.top.location.href = @getEditHref(@inner_path + file_name, "new")
-		return false
-
-	handleNewDirectoryClick: =>
-		Page.cmd "wrapperPrompt", "New directory name:", (res) =>
-			alert("directory name #{res}")
-		return false
-
-	handleSelectClick: (e) =>
-		return false
-
-	handleSelectEnd: (e) =>
-		document.body.removeEventListener('mouseup', @handleSelectEnd)
-		@select_action = null
-
-	handleSelectMousedown: (e) =>
-		inner_path = e.currentTarget.attributes.inner_path.value
-		if @selected[inner_path]
-			delete @selected[inner_path]
-			@select_action = "deselect"
-		else
-			@selected[inner_path] = true
-			@select_action = "select"
-		@checkSelectedItems()
-		document.body.addEventListener('mouseup', @handleSelectEnd)
-		e.stopPropagation()
-		Page.projector.scheduleRender()
-		return false
-
-	handleRowMouseenter: (e) =>
-		if e.buttons and @select_action
-			inner_path = e.target.attributes.inner_path.value
-			if @select_action == "select"
-				@selected[inner_path] = true
-			else
-				delete @selected[inner_path]
-			@checkSelectedItems()
-			Page.projector.scheduleRender()
-		return false
-
-	handleSelectbarCancel: =>
-		@selected = {}
-		@checkSelectedItems()
-		Page.projector.scheduleRender()
-		return false
-
-	handleSelectbarDelete: (e, remove_optional=false) =>
-		for inner_path of @selected
-			optional_info = @item_list.getOptionalInfo(inner_path)
-			delete @selected[inner_path]
-			if optional_info and not remove_optional
-				Page.cmd "optionalFileDelete", inner_path
-			else
-				Page.cmd "fileDelete", inner_path
-		@need_update = true
-		Page.projector.scheduleRender()
-		@checkSelectedItems()
-		return false
-
-	handleSelectbarRemoveOptional: (e) =>
-		return @handleSelectbarDelete(e, true)
-
-	renderSelectbar: =>
-		h("div.selectbar", {classes: {visible: @selected_items_num > 0}}, [
-			"Selected:",
-			h("span.info", [
-				h("span.num", "#{@selected_items_num} files"),
-				h("span.size", "(#{Text.formatSize(@selected_items_size)})"),
-			])
-			h("div.actions", [
-				if @selected_optional_empty_num > 0
-					h("a.action.delete.remove_optional", {href: "#", onclick: @handleSelectbarRemoveOptional}, "Delete and remove optional")
-				else
-					h("a.action.delete", {href: "#", onclick: @handleSelectbarDelete}, "Delete")
-			])
-			h("a.cancel.link", {href: "#", onclick: @handleSelectbarCancel}, "Cancel")
-		])
-
-	renderHead: =>
-		parent_links = []
-		inner_path_parent = ""
-		for parent_dir in @inner_path.split("/")
-			if not parent_dir
-				continue
-			if inner_path_parent
-				inner_path_parent += "/"
-			inner_path_parent += "#{parent_dir}"
-			parent_links.push(
-				[" / ", h("a", {href: @getListHref(inner_path_parent)}, parent_dir)]
-			)
-		return h("div.tr.thead", h("div.td.full",
-			h("a", {href: @getListHref("")}, "root"),
-			parent_links
-		))
-
-	renderItemCheckbox: (item) =>
-		if not @item_list.hasPermissionDelete(item)
-			return [" "]
-
-		return h("a.checkbox-outer", {
-			href: "#Select",
-			onmousedown: @handleSelectMousedown,
-			onclick: @handleSelectClick,
-			inner_path: item.inner_path
-		}, h("span.checkbox"))
-
-	renderItem: (item) =>
-		if item.type == "parent"
-			href = @url_root.replace(/^(.*)\/.{2,255}?$/, "$1/")
-		else if item.type == "dir"
-			href = @url_root + item.name
-		else
-			href = @url_root.replace(/^\/list\//, "/") + item.name
-
-		inner_path = @inner_path + item.name
-		href_edit = @getEditHref(inner_path)
-		is_dir = item.type in ["dir", "parent"]
-		ext = item.name.split(".").pop()
-
-		is_editing = inner_path == Page.file_editor?.inner_path
-		is_editable = not is_dir and item.size < 1024 * 1024 and ext not in window.BINARY_EXTENSIONS
-		is_modified = @item_list.isModified(inner_path)
-		is_added = @item_list.isAdded(inner_path)
-		optional_info = @item_list.getOptionalInfo(inner_path)
-
-		style = ""
-		title = ""
-
-		if optional_info
-			downloaded_percent = optional_info.downloaded_percent
-			if not downloaded_percent
-				downloaded_percent = 0
-			style += "background: linear-gradient(90deg, #fff6dd, #{downloaded_percent}%, white, #{downloaded_percent}%, white);"
-			is_added = false
-
-		if item.ignored
-			is_added = false
-
-		if is_modified then title += " (modified)"
-		if is_added then title += " (new)"
-		if optional_info or item.optional_empty then title += " (optional)"
-		if item.ignored then title += " (ignored from content.json)"
-
-		classes = {
-			"type-#{item.type}": true, editing: is_editing, nobuttons: not is_editable, selected: @selected[inner_path],
-			modified: is_modified, added: is_added, ignored: item.ignored, optional: optional_info, optional_empty: item.optional_empty
-		}
-
-		h("div.tr", {key: item.name, classes: classes, style: style, onmouseenter: @handleRowMouseenter, inner_path: inner_path}, [
-			h("div.td.pre", {title: title},
-				@renderItemCheckbox(item)
-			),
-			h("div.td.name", h("a.link", {href: href}, item.name))
-			h("div.td.buttons", if is_editable then h("a.edit", {href: href_edit}, if Page.site_info.settings.own then "Edit" else "View"))
-			h("div.td.size", if is_dir then "[DIR]" else Text.formatSize(item.size))
-		])
-
-
-	renderItems: =>
-		return [
-			if @item_list.error and not @item_list.items.length and not @item_list.updating then [
-					h("div.tr", {key: "error"}, h("div.td.full.error", @item_list.error))
-				],
-			if @inner_path then @renderItem({"name": "..", type: "parent", size: 0})
-			@item_list.items.map @renderItem
-		]
-
-	renderFoot: =>
-		files = (item for item in @item_list.items when item.type not in ["parent", "dir"])
-		dirs = (item for item in @item_list.items when item.type == "dir")
-		if files.length
-			total_size = (item.size for file in files).reduce (a, b) -> a + b
-		else
-			total_size = 0
-
-		foot_text = "Total: "
-		foot_text += "#{dirs.length} dir, #{files.length} file in #{Text.formatSize(total_size)}"
-
-		return [
-			if dirs.length or files.length or Page.site_info?.settings?.own
-				h("div.tr.foot-info.foot", h("div.td.full", [
-					if @item_list.updating
-						"Updating file list..."
-					else
-						if dirs.length or files.length then foot_text
-					if Page.site_info?.settings?.own
-						h("div.create", [
-							h("a.link", {href: "#Create+new+file", onclick: @handleNewFileClick}, "+ New")
-							@menu_create.render()
-						])
-				]))
-		]
-
-	render: =>
-		if @need_update
-			@update()
-			@need_update = false
-
-			if not @item_list.items
-				return []
-
-		return h("div.files", [
-			@renderSelectbar(),
-			@renderHead(),
-			h("div.tbody", @renderItems()),
-			@renderFoot()
-		])
-
-window.FileList = FileList
diff --git a/plugins/UiFileManager/media/js/UiFileManager.coffee b/plugins/UiFileManager/media/js/UiFileManager.coffee
deleted file mode 100644
index 2126f3b1..00000000
--- a/plugins/UiFileManager/media/js/UiFileManager.coffee
+++ /dev/null
@@ -1,79 +0,0 @@
-window.h = maquette.h
-
-class UiFileManager extends ZeroFrame
-	init: ->
-		@url_params = new URLSearchParams(window.location.search)
-		@list_site = @url_params.get("site")
-		@list_address = @url_params.get("address")
-		@list_inner_path = @url_params.get("inner_path")
-		@editor_inner_path = @url_params.get("file")
-		@file_list = new FileList(@list_site, @list_inner_path)
-
-		@site_info = null
-		@server_info = null
-
-		@is_sidebar_closed = false
-
-		if @editor_inner_path
-			@file_editor = new FileEditor(@editor_inner_path)
-
-		window.onbeforeunload = =>
-			if @file_editor?.isModified()
-				return true
-			else
-				return null
-
-		window.onresize = =>
-			@checkBodyWidth()
-
-		@checkBodyWidth()
-
-		@cmd("wrapperSetViewport", "width=device-width, initial-scale=0.8")
-
-		@cmd "serverInfo", {}, (server_info) =>
-			@server_info = server_info
-		@cmd "siteInfo", {}, (site_info) =>
-			@cmd("wrapperSetTitle", "List: /#{@list_inner_path} - #{site_info.content.title} - ZeroNet")
-			@site_info = site_info
-			if @file_editor then @file_editor.on_loaded.then =>
-				@file_editor.cm.setOption("readOnly", not site_info.settings.own)
-				@file_editor.mode = if site_info.settings.own then "Edit" else "View"
-			@projector.scheduleRender()
-
-	checkBodyWidth: =>
-		if not @file_editor
-			return false
-
-		if document.body.offsetWidth < 960 and not @is_sidebar_closed
-			@is_sidebar_closed = true
-			@projector?.scheduleRender()
-		else if document.body.offsetWidth > 960 and @is_sidebar_closed
-			@is_sidebar_closed = false
-			@projector?.scheduleRender()
-
-	onRequest: (cmd, message) =>
-		if cmd == "setSiteInfo"
-			@site_info = message
-			RateLimitCb 1000, (cb_done) =>
-				@file_list.update(cb_done)
-			@projector.scheduleRender()
-		else if cmd == "setServerInfo"
-			@server_info = message
-			@projector.scheduleRender()
-		else
-			@log "Unknown incoming message:", cmd
-
-	createProjector: =>
-		@projector = maquette.createProjector()
-		@projector.replace($("#content"), @render)
-
-	render: =>
-		return h("div.content#content", [
-			h("div.manager", {classes: {editing: @file_editor, sidebar_closed: @is_sidebar_closed}}, [
-				@file_list.render(),
-				if @file_editor then @file_editor.render()
-			])
-		])
-
-window.Page = new UiFileManager()
-window.Page.createProjector()
diff --git a/plugins/UiFileManager/media/js/lib/Animation.coffee b/plugins/UiFileManager/media/js/lib/Animation.coffee
deleted file mode 100644
index 271b88c1..00000000
--- a/plugins/UiFileManager/media/js/lib/Animation.coffee
+++ /dev/null
@@ -1,138 +0,0 @@
-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()
\ No newline at end of file
diff --git a/plugins/UiFileManager/media/js/lib/Class.coffee b/plugins/UiFileManager/media/js/lib/Class.coffee
deleted file mode 100644
index d62ab25c..00000000
--- a/plugins/UiFileManager/media/js/lib/Class.coffee
+++ /dev/null
@@ -1,23 +0,0 @@
-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
\ No newline at end of file
diff --git a/plugins/UiFileManager/media/js/lib/Dollar.coffee b/plugins/UiFileManager/media/js/lib/Dollar.coffee
deleted file mode 100644
index 7f19f551..00000000
--- a/plugins/UiFileManager/media/js/lib/Dollar.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-window.$ = (selector) ->
-	if selector.startsWith("#")
-		return document.getElementById(selector.replace("#", ""))
diff --git a/plugins/UiFileManager/media/js/lib/ItemList.coffee b/plugins/UiFileManager/media/js/lib/ItemList.coffee
deleted file mode 100644
index 902e76cd..00000000
--- a/plugins/UiFileManager/media/js/lib/ItemList.coffee
+++ /dev/null
@@ -1,26 +0,0 @@
-class ItemList
-	constructor: (@item_class, @key) ->
-		@items = []
-		@items_bykey = {}
-
-	sync: (rows, item_class, key) ->
-		@items.splice(0, @items.length)  # Empty items
-		for row in rows
-			current_obj = @items_bykey[row[@key]]
-			if current_obj
-				current_obj.row = row
-				@items.push current_obj
-			else
-				item = new @item_class(row, @)
-				@items_bykey[row[@key]] = item
-				@items.push item
-
-	deleteItem: (item) ->
-		index = @items.indexOf(item)
-		if index > -1
-			@items.splice(index, 1)
-		else
-			console.log "Can't delete item", item
-		delete @items_bykey[item.row[@key]]
-
-window.ItemList = ItemList
\ No newline at end of file
diff --git a/plugins/UiFileManager/media/js/lib/Menu.coffee b/plugins/UiFileManager/media/js/lib/Menu.coffee
deleted file mode 100644
index ed5fbd54..00000000
--- a/plugins/UiFileManager/media/js/lib/Menu.coffee
+++ /dev/null
@@ -1,110 +0,0 @@
-class Menu
-	constructor: ->
-		@visible = false
-		@items = []
-		@node = null
-		@height = 0
-		@direction = "bottom"
-
-	show: =>
-		window.visible_menu?.hide()
-		@visible = true
-		window.visible_menu = @
-		@direction = @getDirection()
-
-	hide: =>
-		@visible = false
-
-	toggle: =>
-		if @visible
-			@hide()
-		else
-			@show()
-		Page.projector.scheduleRender()
-
-
-	addItem: (title, cb, selected=false) ->
-		@items.push([title, cb, selected])
-
-
-	storeNode: (node) =>
-		@node = node
-		# Animate visible
-		if @visible
-			node.className = node.className.replace("visible", "")
-			setTimeout (=>
-				node.className += " visible"
-				node.attributes.style.value = @getStyle()
-			), 20
-			node.style.maxHeight = "none"
-			@height = node.offsetHeight
-			node.style.maxHeight = "0px"
-			@direction = @getDirection()
-
-	getDirection: =>
-		if @node and @node.parentNode.getBoundingClientRect().top + @height + 60 > document.body.clientHeight and @node.parentNode.getBoundingClientRect().top - @height > 0
-			return "top"
-		else
-			return "bottom"
-
-	handleClick: (e) =>
-		keep_menu = false
-		for item in @items
-			[title, cb, selected] = item
-			if title == e.currentTarget.textContent or e.currentTarget["data-title"] == title
-				keep_menu = cb?(item)
-				break
-		if keep_menu != true and cb != null
-			@hide()
-		return false
-
-	renderItem: (item) =>
-		[title, cb, selected] = item
-		if typeof(selected) == "function"
-			selected = selected()
-
-		if title == "---"
-			return h("div.menu-item-separator", {key: Time.timestamp()})
-		else
-			if cb == null
-				href = undefined
-				onclick = @handleClick
-			else if typeof(cb) == "string"  # Url
-				href = cb
-				onclick = true
-			else  # Callback
-				href = "#"+title
-				onclick = @handleClick
-			classes = {
-				"selected": selected,
-				"noaction": (cb == null)
-			}
-			return h("a.menu-item", {href: href, onclick: onclick, "data-title": title, key: title, classes: classes}, title)
-
-	getStyle: =>
-		if @visible
-			max_height = @height
-		else
-			max_height = 0
-		style = "max-height: #{max_height}px"
-		if @direction == "top"
-			style += ";margin-top: #{0 - @height - 50}px"
-		else
-			style += ";margin-top: 0px"
-		return style
-
-	render: (class_name="") =>
-		if @visible or @node
-			h("div.menu#{class_name}", {classes: {"visible": @visible}, style: @getStyle(), afterCreate: @storeNode}, @items.map(@renderItem))
-
-window.Menu = Menu
-
-# Hide menu on outside click
-document.body.addEventListener "mouseup", (e) ->
-	if not window.visible_menu or not window.visible_menu.node
-		return false
-	menu_node = window.visible_menu.node
-	menu_parents = [menu_node, menu_node.parentNode]
-	if e.target.parentNode not in menu_parents and e.target.parentNode.parentNode not in menu_parents
-		window.visible_menu.hide()
-		Page.projector.scheduleRender()
diff --git a/plugins/UiFileManager/media/js/lib/Promise.coffee b/plugins/UiFileManager/media/js/lib/Promise.coffee
deleted file mode 100644
index 136e3ec7..00000000
--- a/plugins/UiFileManager/media/js/lib/Promise.coffee
+++ /dev/null
@@ -1,74 +0,0 @@
-# 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
-###
\ No newline at end of file
diff --git a/plugins/UiFileManager/media/js/lib/Prototypes.coffee b/plugins/UiFileManager/media/js/lib/Prototypes.coffee
deleted file mode 100644
index 8026ee5d..00000000
--- a/plugins/UiFileManager/media/js/lib/Prototypes.coffee
+++ /dev/null
@@ -1,9 +0,0 @@
-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
-
diff --git a/plugins/UiFileManager/media/js/lib/RateLimitCb.coffee b/plugins/UiFileManager/media/js/lib/RateLimitCb.coffee
deleted file mode 100644
index a7316f53..00000000
--- a/plugins/UiFileManager/media/js/lib/RateLimitCb.coffee
+++ /dev/null
@@ -1,62 +0,0 @@
-last_time = {}
-calling = {}
-calling_iterval = {}
-call_after_interval = {}
-
-# Rate limit function call and don't allow to run in parallel (until callback is called)
-window.RateLimitCb = (interval, fn, args=[]) ->
-    cb = ->  # Callback when function finished
-        left = interval - (Date.now() - last_time[fn])  # Time life until next call
-        # console.log "CB, left", left, "Calling:", calling[fn]
-        if left <= 0  # No time left from rate limit interval
-            delete last_time[fn]
-            if calling[fn]  # Function called within interval
-                RateLimitCb(interval, fn, calling[fn])
-            delete calling[fn]
-        else  # Time left from rate limit interval
-            setTimeout (->
-                delete last_time[fn]
-                if calling[fn]  # Function called within interval
-                    RateLimitCb(interval, fn, calling[fn])
-                delete calling[fn]
-            ), left
-    if last_time[fn]  # Function called within interval
-        calling[fn] = args  # Schedule call and update arguments
-    else  # Not called within interval, call instantly
-        last_time[fn] = Date.now()
-        fn.apply(this, [cb, args...])
-
-
-window.RateLimit = (interval, fn) ->
-    if calling_iterval[fn] > interval
-        clearInterval calling[fn]
-        delete calling[fn]
-
-    if not calling[fn]
-        call_after_interval[fn] = false
-        fn() # First call is not delayed
-        calling_iterval[fn] = interval
-        calling[fn] = setTimeout (->
-            if call_after_interval[fn]
-                fn()
-            delete calling[fn]
-            delete call_after_interval[fn]
-        ), interval
-    else # Called within iterval, delay the call
-        call_after_interval[fn] = true
-
-
-###
-window.s = Date.now()
-window.load = (done, num) ->
-  console.log "Loading #{num}...", Date.now()-window.s
-  setTimeout (-> done()), 1000
-
-RateLimit 500, window.load, [0] # Called instantly
-RateLimit 500, window.load, [1]
-setTimeout (-> RateLimit 500, window.load, [300]), 300
-setTimeout (-> RateLimit 500, window.load, [600]), 600 # Called after 1000ms
-setTimeout (-> RateLimit 500, window.load, [1000]), 1000
-setTimeout (-> RateLimit 500, window.load, [1200]), 1200  # Called after 2000ms
-setTimeout (-> RateLimit 500, window.load, [3000]), 3000  # Called after 3000ms
-###
\ No newline at end of file
diff --git a/plugins/UiFileManager/media/js/lib/Text.coffee b/plugins/UiFileManager/media/js/lib/Text.coffee
deleted file mode 100644
index a9338983..00000000
--- a/plugins/UiFileManager/media/js/lib/Text.coffee
+++ /dev/null
@@ -1,147 +0,0 @@
-class Text
-	toColor: (text, saturation=30, lightness=50) ->
-		hash = 0
-		for i in [0..text.length-1]
-			hash += text.charCodeAt(i)*i
-			hash = hash % 1777
-		return "hsl(" + (hash % 360) + ",#{saturation}%,#{lightness}%)";
-
-
-	renderMarked: (text, options={}) ->
-		options["gfm"] = true
-		options["breaks"] = true
-		options["sanitize"] = true
-		options["renderer"] = marked_renderer
-		text = marked(text, options)
-		return @fixHtmlLinks text
-
-	emailLinks: (text) ->
-		return text.replace(/([a-zA-Z0-9]+)@zeroid.bit/g, "<a href='?to=$1' onclick='return Page.message_create.show(\"$1\")'>$1@zeroid.bit</a>")
-
-	# Convert zeronet html links to relaitve
-	fixHtmlLinks: (text) ->
-		if window.is_proxy
-			return text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="http://zero')
-		else
-			return text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="')
-
-	# Convert a single link to relative
-	fixLink: (link) ->
-		if window.is_proxy
-			back = link.replace(/http:\/\/(127.0.0.1|localhost):43110/, 'http://zero')
-			return back.replace(/http:\/\/zero\/([^\/]+\.bit)/, "http://$1")  # Domain links
-		else
-			return link.replace(/http:\/\/(127.0.0.1|localhost):43110/, '')
-
-	toUrl: (text) ->
-		return text.replace(/[^A-Za-z0-9]/g, "+").replace(/[+]+/g, "+").replace(/[+]+$/, "")
-
-	getSiteUrl: (address) ->
-		if window.is_proxy
-			if "." in address # Domain
-				return "http://"+address+"/"
-			else
-				return "http://zero/"+address+"/"
-		else
-			return "/"+address+"/"
-
-
-	fixReply: (text) ->
-		return text.replace(/(>.*\n)([^\n>])/gm, "$1\n$2")
-
-	toBitcoinAddress: (text) ->
-		return text.replace(/[^A-Za-z0-9]/g, "")
-
-
-	jsonEncode: (obj) ->
-		return unescape(encodeURIComponent(JSON.stringify(obj)))
-
-	jsonDecode: (obj) ->
-		return JSON.parse(decodeURIComponent(escape(obj)))
-
-	fileEncode: (obj) ->
-		if typeof(obj) == "string"
-			return btoa(unescape(encodeURIComponent(obj)))
-		else
-			return btoa(unescape(encodeURIComponent(JSON.stringify(obj, undefined, '\t'))))
-
-	utf8Encode: (s) ->
-		return unescape(encodeURIComponent(s))
-
-	utf8Decode: (s) ->
-		return decodeURIComponent(escape(s))
-
-
-	distance: (s1, s2) ->
-		s1 = s1.toLocaleLowerCase()
-		s2 = s2.toLocaleLowerCase()
-		next_find_i = 0
-		next_find = s2[0]
-		match = true
-		extra_parts = {}
-		for char in s1
-			if char != next_find
-				if extra_parts[next_find_i]
-					extra_parts[next_find_i] += char
-				else
-					extra_parts[next_find_i] = char
-			else
-				next_find_i++
-				next_find = s2[next_find_i]
-
-		if extra_parts[next_find_i]
-			extra_parts[next_find_i] = ""  # Extra chars on the end doesnt matter
-		extra_parts = (val for key, val of extra_parts)
-		if next_find_i >= s2.length
-			return extra_parts.length + extra_parts.join("").length
-		else
-			return false
-
-
-	parseQuery: (query) ->
-		params = {}
-		parts = query.split('&')
-		for part in parts
-			[key, val] = part.split("=")
-			if val
-				params[decodeURIComponent(key)] = decodeURIComponent(val)
-			else
-				params["url"] = decodeURIComponent(key)
-		return params
-
-	encodeQuery: (params) ->
-		back = []
-		if params.url
-			back.push(params.url)
-		for key, val of params
-			if not val or key == "url"
-				continue
-			back.push("#{encodeURIComponent(key)}=#{encodeURIComponent(val)}")
-		return back.join("&")
-
-	highlight: (text, search) ->
-		if not text
-			return [""]
-		parts = text.split(RegExp(search, "i"))
-		back = []
-		for part, i in parts
-			back.push(part)
-			if i < parts.length-1
-				back.push(h("span.highlight", {key: i}, search))
-		return back
-
-	formatSize: (size) ->
-		if isNaN(parseInt(size))
-			return ""
-		size_mb = size/1024/1024
-		if size_mb >= 1000
-			return (size_mb/1024).toFixed(1)+" GB"
-		else if size_mb >= 100
-			return size_mb.toFixed(0)+" MB"
-		else if size/1024 >= 1000
-			return size_mb.toFixed(2)+" MB"
-		else
-			return (parseInt(size)/1024).toFixed(2)+" KB"
-
-window.is_proxy = (document.location.host == "zero" or window.location.pathname == "/")
-window.Text = new Text()
diff --git a/plugins/UiFileManager/media/js/lib/Time.coffee b/plugins/UiFileManager/media/js/lib/Time.coffee
deleted file mode 100644
index 7adf3d6b..00000000
--- a/plugins/UiFileManager/media/js/lib/Time.coffee
+++ /dev/null
@@ -1,59 +0,0 @@
-class Time
-	since: (timestamp) ->
-		now = +(new Date)/1000
-		if timestamp > 1000000000000  # In ms
-			timestamp = timestamp/1000
-		secs = now - timestamp
-		if secs < 60
-			back = "Just now"
-		else if secs < 60*60
-			minutes = Math.round(secs/60)
-			back = "" + minutes + " minutes ago"
-		else if secs < 60*60*24
-			back = "#{Math.round(secs/60/60)} hours ago"
-		else if secs < 60*60*24*3
-			back = "#{Math.round(secs/60/60/24)} days ago"
-		else
-			back = "on "+@date(timestamp)
-		back = back.replace(/^1 ([a-z]+)s/, "1 $1") # 1 days ago fix
-		return back
-
-	dateIso: (timestamp=null) ->
-		if not timestamp
-			timestamp = window.Time.timestamp()
-
-		if timestamp > 1000000000000  # In ms
-			timestamp = timestamp/1000
-		tzoffset = (new Date()).getTimezoneOffset() * 60
-		return (new Date((timestamp - tzoffset) * 1000)).toISOString().split("T")[0]
-
-	date: (timestamp=null, format="short") ->
-		if not timestamp
-			timestamp = window.Time.timestamp()
-
-		if timestamp > 1000000000000  # In ms
-			timestamp = timestamp/1000
-		parts = (new Date(timestamp * 1000)).toString().split(" ")
-		if format == "short"
-			display = parts.slice(1, 4)
-		else if format == "day"
-			display = parts.slice(1, 3)
-		else if format == "month"
-			display = [parts[1], parts[3]]
-		else if format == "long"
-			display = parts.slice(1, 5)
-		return display.join(" ").replace(/( [0-9]{4})/, ",$1")
-
-	weekDay: (timestamp) ->
-		if timestamp > 1000000000000  # In ms
-			timestamp = timestamp/1000
-		return ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][ (new Date(timestamp * 1000)).getDay() ]
-
-	timestamp: (date="") ->
-		if date == "now" or date == ""
-			return parseInt(+(new Date)/1000)
-		else
-			return parseInt(Date.parse(date)/1000)
-
-
-window.Time = new Time
\ No newline at end of file
diff --git a/plugins/UiFileManager/media/js/lib/ZeroFrame.coffee b/plugins/UiFileManager/media/js/lib/ZeroFrame.coffee
deleted file mode 100644
index 11512d16..00000000
--- a/plugins/UiFileManager/media/js/lib/ZeroFrame.coffee
+++ /dev/null
@@ -1,85 +0,0 @@
-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
diff --git a/plugins/UiPluginManager/media/js/PluginList.coffee b/plugins/UiPluginManager/media/js/PluginList.coffee
deleted file mode 100644
index 45e352f0..00000000
--- a/plugins/UiPluginManager/media/js/PluginList.coffee
+++ /dev/null
@@ -1,132 +0,0 @@
-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
\ No newline at end of file
diff --git a/plugins/UiPluginManager/media/js/UiPluginManager.coffee b/plugins/UiPluginManager/media/js/UiPluginManager.coffee
deleted file mode 100644
index 6a0adee5..00000000
--- a/plugins/UiPluginManager/media/js/UiPluginManager.coffee
+++ /dev/null
@@ -1,71 +0,0 @@
-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()
diff --git a/plugins/UiPluginManager/media/js/lib/Class.coffee b/plugins/UiPluginManager/media/js/lib/Class.coffee
deleted file mode 100644
index d62ab25c..00000000
--- a/plugins/UiPluginManager/media/js/lib/Class.coffee
+++ /dev/null
@@ -1,23 +0,0 @@
-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
\ No newline at end of file
diff --git a/plugins/UiPluginManager/media/js/lib/Promise.coffee b/plugins/UiPluginManager/media/js/lib/Promise.coffee
deleted file mode 100644
index 136e3ec7..00000000
--- a/plugins/UiPluginManager/media/js/lib/Promise.coffee
+++ /dev/null
@@ -1,74 +0,0 @@
-# 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
-###
\ No newline at end of file
diff --git a/plugins/UiPluginManager/media/js/lib/Prototypes.coffee b/plugins/UiPluginManager/media/js/lib/Prototypes.coffee
deleted file mode 100644
index 034add50..00000000
--- a/plugins/UiPluginManager/media/js/lib/Prototypes.coffee
+++ /dev/null
@@ -1,8 +0,0 @@
-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
diff --git a/plugins/UiPluginManager/media/js/utils/Animation.coffee b/plugins/UiPluginManager/media/js/utils/Animation.coffee
deleted file mode 100644
index 271b88c1..00000000
--- a/plugins/UiPluginManager/media/js/utils/Animation.coffee
+++ /dev/null
@@ -1,138 +0,0 @@
-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()
\ No newline at end of file
diff --git a/plugins/UiPluginManager/media/js/utils/Dollar.coffee b/plugins/UiPluginManager/media/js/utils/Dollar.coffee
deleted file mode 100644
index 7f19f551..00000000
--- a/plugins/UiPluginManager/media/js/utils/Dollar.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-window.$ = (selector) ->
-	if selector.startsWith("#")
-		return document.getElementById(selector.replace("#", ""))
diff --git a/plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee b/plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee
deleted file mode 100644
index 11512d16..00000000
--- a/plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee
+++ /dev/null
@@ -1,85 +0,0 @@
-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
diff --git a/src/Ui/media/Fixbutton.coffee b/src/Ui/media/Fixbutton.coffee
deleted file mode 100644
index 954d2b56..00000000
--- a/src/Ui/media/Fixbutton.coffee
+++ /dev/null
@@ -1,32 +0,0 @@
-class Fixbutton
-	constructor: ->
-		@dragging = false
-		$(".fixbutton-bg").on "mouseover", ->
-			$(".fixbutton-bg").stop().animate({"scale": 0.7}, 800, "easeOutElastic")
-			$(".fixbutton-burger").stop().animate({"opacity": 1.5, "left": 0}, 800, "easeOutElastic")
-			$(".fixbutton-text").stop().animate({"opacity": 0, "left": 20}, 300, "easeOutCubic")
-
-		$(".fixbutton-bg").on "mouseout", ->
-			if $(".fixbutton").hasClass("dragging")
-				return true
-			$(".fixbutton-bg").stop().animate({"scale": 0.6}, 300, "easeOutCubic")
-			$(".fixbutton-burger").stop().animate({"opacity": 0, "left": -20}, 300, "easeOutCubic")
-			$(".fixbutton-text").stop().animate({"opacity": 0.9, "left": 0}, 300, "easeOutBack")
-
-
-		###$(".fixbutton-bg").on "click", ->
-			return false
-		###
-
-		$(".fixbutton-bg").on "mousedown", ->
-			# $(".fixbutton-burger").stop().animate({"scale": 0.7, "left": 0}, 300, "easeOutCubic")
-			#$("#inner-iframe").toggleClass("back")
-			#$(".wrapper-iframe").stop().animate({"scale": 0.9}, 600, "easeOutCubic")
-			#$("body").addClass("back")
-
-		$(".fixbutton-bg").on "mouseup", ->
-			# $(".fixbutton-burger").stop().animate({"scale": 1, "left": 0}, 600, "easeOutElastic")
-
-
-
-window.Fixbutton = Fixbutton
diff --git a/src/Ui/media/Infopanel.coffee b/src/Ui/media/Infopanel.coffee
deleted file mode 100644
index 3a490364..00000000
--- a/src/Ui/media/Infopanel.coffee
+++ /dev/null
@@ -1,57 +0,0 @@
-class Infopanel
-	constructor: (@elem) ->
-		@visible = false
-
-	show: (closed=false) =>
-		@elem.parent().addClass("visible")
-		if closed
-			@close()
-		else
-			@open()
-
-	unfold: =>
-		@elem.toggleClass("unfolded")
-		return false
-
-	updateEvents: =>
-		@elem.off("click")
-		@elem.find(".close").off("click")
-		@elem.find(".line").off("click")
-
-		@elem.find(".line").on("click", @unfold)
-
-		if @elem.hasClass("closed")
-			@elem.on "click", =>
-				@onOpened()
-				@open()
-		else
-			@elem.find(".close").on "click", =>
-				@onClosed()
-				@close()
-
-	hide: =>
-		@elem.parent().removeClass("visible")
-
-	close: =>
-		@elem.addClass("closed")
-		@updateEvents()
-		return false
-
-	open: =>
-		@elem.removeClass("closed")
-		@updateEvents()
-		return false
-
-	setTitle: (line1, line2) =>
-		@elem.find(".line-1").text(line1)
-		@elem.find(".line-2").text(line2)
-
-	setClosedNum: (num) =>
-		@elem.find(".closed-num").text(num)
-
-	setAction: (title, func) =>
-		@elem.find(".button").text(title).off("click").on("click", func)
-
-
-
-window.Infopanel = Infopanel
diff --git a/src/Ui/media/Loading.coffee b/src/Ui/media/Loading.coffee
deleted file mode 100644
index 8e35ce66..00000000
--- a/src/Ui/media/Loading.coffee
+++ /dev/null
@@ -1,91 +0,0 @@
-class Loading
-	constructor: (@wrapper) ->
-		if window.show_loadingscreen then @showScreen()
-		@timer_hide = null
-		@timer_set = null
-
-	setProgress: (percent) ->
-		if @timer_hide
-			clearInterval @timer_hide
-		@timer_set = RateLimit 500, ->
-			$(".progressbar").css("transform": "scaleX(#{parseInt(percent*100)/100})").css("opacity", "1").css("display", "block")
-
-	hideProgress: ->
-		@log "hideProgress"
-		if @timer_set
-			clearInterval @timer_set
-		@timer_hide = setTimeout ( =>
-			$(".progressbar").css("transform": "scaleX(1)").css("opacity", "0").hideLater(1000)
-		), 300
-
-
-	showScreen: ->
-		$(".loadingscreen").css("display", "block").addClassLater("ready")
-		@screen_visible = true
-		@printLine "&nbsp;&nbsp;&nbsp;Connecting..."
-
-
-	showTooLarge: (site_info) ->
-		@log "Displaying large site confirmation"
-		if $(".console .button-setlimit").length == 0 # Not displaying it yet
-			line = @printLine("Site size: <b>#{parseInt(site_info.settings.size/1024/1024)}MB</b> is larger than default allowed #{parseInt(site_info.size_limit)}MB", "warning")
-			button = $("<a href='#Set+limit' class='button button-setlimit'>" + "Open site and set size limit to #{site_info.next_size_limit}MB" + "</a>")
-			button.on "click", =>
-				button.addClass("loading")
-				return @wrapper.setSizeLimit(site_info.next_size_limit)
-			line.after(button)
-			setTimeout (=>
-				@printLine('Ready.')
-			), 100
-
-	showTrackerTorBridge: (server_info) ->
-		if $(".console .button-settrackerbridge").length == 0 and not server_info.tor_use_meek_bridges
-			line = @printLine("Tracker connection error detected.", "error")
-			button = $("<a href='#Enable+Tor+bridges' class='button button-settrackerbridge'>" + "Use Tor meek bridges for tracker connections" + "</a>")
-			button.on "click", =>
-				button.addClass("loading")
-				@wrapper.ws.cmd "configSet", ["tor_use_bridges", ""]
-				@wrapper.ws.cmd "configSet", ["trackers_proxy", "tor"]
-				@wrapper.ws.cmd "siteUpdate", {address: @wrapper.site_info.address, announce: true}
-				@wrapper.reloadIframe()
-				return false
-			line.after(button)
-			if not server_info.tor_has_meek_bridges
-				button.addClass("disabled")
-				@printLine("No meek bridge support in your client, please <a href='https://github.com/HelloZeroNet/ZeroNet#how-to-join'>download the latest bundle</a>.", "warning")
-
-	# We dont need loadingscreen anymore
-	hideScreen: ->
-		@log "hideScreen"
-		if not $(".loadingscreen").hasClass("done") # Only if its not animating already
-			if @screen_visible # Hide with animate
-				$(".loadingscreen").addClass("done").removeLater(2000)
-			else # Not visible, just remove
-				$(".loadingscreen").remove()
-		@screen_visible = false
-
-
-	# Append text to last line of loadingscreen
-	print: (text, type="normal") ->
-		if not @screen_visible then return false
-		$(".loadingscreen .console .cursor").remove() # Remove previous cursor
-		last_line = $(".loadingscreen .console .console-line:last-child")
-		if type == "error" then text = "<span class='console-error'>#{text}</span>"
-		last_line.html(last_line.html()+text)
-
-
-	# Add line to loading screen
-	printLine: (text, type="normal") ->
-		if not @screen_visible then return false
-		$(".loadingscreen .console .cursor").remove() # Remove previous cursor
-		if type == "error" then text = "<span class='console-error'>#{text}</span>" else text = text+"<span class='cursor'> </span>"
-
-		line = $("<div class='console-line'>#{text}</div>").appendTo(".loadingscreen .console")
-		if type == "warning" then line.addClass("console-warning")
-		return line
-
-	log: (args...) ->
-		console.log "[Loading]", args...
-
-
-window.Loading = Loading
diff --git a/src/Ui/media/Notifications.coffee b/src/Ui/media/Notifications.coffee
deleted file mode 100644
index 8898b645..00000000
--- a/src/Ui/media/Notifications.coffee
+++ /dev/null
@@ -1,89 +0,0 @@
-class Notifications
-	constructor: (@elem) ->
-		@
-
-	test: ->
-		setTimeout (=>
-			@add("connection", "error", "Connection lost to <b>UiServer</b> on <b>localhost</b>!")
-			@add("message-Anyone", "info", "New  from <b>Anyone</b>.")
-		), 1000
-		setTimeout (=>
-			@add("connection", "done", "<b>UiServer</b> connection recovered.", 5000)
-		), 3000
-
-
-	add: (id, type, body, timeout=0) ->
-		id = id.replace /[^A-Za-z0-9-]/g, ""
-		# Close notifications with same id
-		for elem in $(".notification-#{id}")
-			@close $(elem)
-
-		# Create element
-		elem = $(".notification.template", @elem).clone().removeClass("template")
-		elem.addClass("notification-#{type}").addClass("notification-#{id}")
-		if type == "progress"
-			elem.addClass("notification-done")
-
-		# Update text
-		if type == "error"
-			$(".notification-icon", elem).html("!")
-		else if type == "done"
-			$(".notification-icon", elem).html("<div class='icon-success'></div>")
-		else if type == "progress"
-			$(".notification-icon", elem).html("<div class='icon-success'></div>")
-		else if type == "ask"
-			$(".notification-icon", elem).html("?")
-		else
-			$(".notification-icon", elem).html("i")
-
-		if typeof(body) == "string"
-			$(".body", elem).html("<div class='message'><span class='multiline'>"+body+"</span></div>")
-		else
-			$(".body", elem).html("").append(body)
-
-		elem.appendTo(@elem)
-
-		# Timeout
-		if timeout
-			$(".close", elem).remove() # No need of close button
-			setTimeout (=>
-				@close elem
-			), timeout
-
-		# Animate
-		width = Math.min(elem.outerWidth() + 50, 580)
-		if not timeout then width += 20 # Add space for close button
-		if elem.outerHeight() > 55 then elem.addClass("long")
-		elem.css({"width": "50px", "transform": "scale(0.01)"})
-		elem.animate({"scale": 1}, 800, "easeOutElastic")
-		elem.animate({"width": width}, 700, "easeInOutCubic")
-		$(".body", elem).css("width": (width - 50))
-		$(".body", elem).cssLater("box-shadow", "0px 0px 5px rgba(0,0,0,0.1)", 1000)
-
-		# Close button or Confirm button
-		$(".close, .button", elem).on "click", =>
-			@close elem
-			return false
-
-		# Select list
-		$(".select", elem).on "click", =>
-			@close elem
-
-		# Input enter
-		$("input", elem).on "keyup", (e) =>
-			if e.keyCode == 13
-				@close elem
-
-		return elem
-
-
-	close: (elem) ->
-		elem.stop().animate {"width": 0, "opacity": 0}, 700, "easeInOutCubic"
-		elem.slideUp 300, (-> elem.remove())
-
-
-	log: (args...) ->
-		console.log "[Notifications]", args...
-
-
-window.Notifications = Notifications
diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee
deleted file mode 100644
index 1b98855e..00000000
--- a/src/Ui/media/Wrapper.coffee
+++ /dev/null
@@ -1,714 +0,0 @@
-class Wrapper
-	constructor: (ws_url) ->
-		@log "Created!"
-
-		@loading = new Loading(@)
-		@notifications = new Notifications($(".notifications"))
-		@infopanel = new Infopanel($(".infopanel"))
-		@infopanel.onClosed = =>
-			@ws.cmd("siteSetSettingsValue", ["modified_files_notification", false])
-		@infopanel.onOpened = =>
-			@ws.cmd("siteSetSettingsValue", ["modified_files_notification", true])
-		@fixbutton = new Fixbutton()
-
-		window.addEventListener("message", @onMessageInner, false)
-		@inner = document.getElementById("inner-iframe").contentWindow
-		@ws = new ZeroWebsocket(ws_url)
-		@ws.next_message_id = 1000000 # Avoid messageid collision :)
-		@ws.onOpen = @onOpenWebsocket
-		@ws.onClose = @onCloseWebsocket
-		@ws.onMessage = @onMessageWebsocket
-		@ws.connect()
-		@ws_error = null # Ws error message
-
-		@next_cmd_message_id = -1
-
-		@site_info = null # Hold latest site info
-		@server_info = null # Hold latest server info
-		@event_site_info =  $.Deferred() # Event when site_info received
-		@inner_loaded = false # If iframe loaded or not
-		@inner_ready = false # Inner frame ready to receive messages
-		@wrapperWsInited = false # Wrapper notified on websocket open
-		@site_error = null # Latest failed file download
-		@address = null
-		@opener_tested = false
-		@announcer_line = null
-		@web_notifications = {}
-		@is_title_changed = false
-
-		@allowed_event_constructors = [window.MouseEvent, window.KeyboardEvent, window.PointerEvent] # Allowed event constructors
-
-		window.onload = @onPageLoad # On iframe loaded
-		window.onhashchange = (e) => # On hash change
-			@log "Hashchange", window.location.hash
-			if window.location.hash
-				src = $("#inner-iframe").attr("src").replace(/#.*/, "")+window.location.hash
-				$("#inner-iframe").attr("src", src)
-
-		window.onpopstate = (e) =>
-			@sendInner {"cmd": "wrapperPopState", "params": {"href": document.location.href, "state": e.state}}
-
-		$("#inner-iframe").focus()
-
-
-	verifyEvent: (allowed_target, e) =>
-		if not e.originalEvent.isTrusted
-			throw "Event not trusted"
-
-		if e.originalEvent.constructor not in @allowed_event_constructors
-			throw "Invalid event constructor: #{e.constructor} not in #{JSON.stringify(@allowed_event_constructors)}"
-
-		if e.originalEvent.currentTarget != allowed_target[0]
-			throw "Invalid event target: #{e.originalEvent.currentTarget} != #{allowed_target[0]}"
-
-	# Incoming message from UiServer websocket
-	onMessageWebsocket: (e) =>
-		message = JSON.parse(e.data)
-		@handleMessageWebsocket(message)
-
-	handleMessageWebsocket: (message) =>
-		cmd = message.cmd
-		if cmd == "response"
-			if @ws.waiting_cb[message.to]? # We are waiting for response
-				@ws.waiting_cb[message.to](message.result)
-			else
-				@sendInner message # Pass message to inner frame
-		else if cmd == "notification" # Display notification
-			type = message.params[0]
-			id = "notification-ws-#{message.id}"
-			if "-" in message.params[0]  # - in first param: message id defined
-				[id, type] = message.params[0].split("-")
-			@notifications.add(id, type, message.params[1], message.params[2])
-		else if cmd == "progress" # Display notification
-			@actionProgress(message)
-		else if cmd == "prompt" # Prompt input
-			@displayPrompt message.params[0], message.params[1], message.params[2], message.params[3], (res) =>
-				@ws.response message.id, res
-		else if cmd == "confirm" # Confirm action
-			@displayConfirm message.params[0], message.params[1], (res) =>
-				@ws.response message.id, res
-		else if cmd == "setSiteInfo"
-			@sendInner message # Pass to inner frame
-			if message.params.address == @address # Current page
-				@setSiteInfo message.params
-			@updateProgress message.params
-		else if cmd == "setAnnouncerInfo"
-			@sendInner message # Pass to inner frame
-			if message.params.address == @address # Current page
-				@setAnnouncerInfo message.params
-			@updateProgress message.params
-		else if cmd == "error"
-			@notifications.add("notification-#{message.id}", "error", message.params, 0)
-		else if cmd == "updating" # Close connection
-			@log "Updating: Closing websocket"
-			@ws.ws.close()
-			@ws.onCloseWebsocket(null, 4000)
-		else if cmd == "redirect"
-			window.top.location = message.params
-		else if cmd == "injectHtml"
-			$("body").append(message.params)
-		else if cmd == "injectScript"
-			script_tag = $("<script>")
-			script_tag.attr("nonce", @script_nonce)
-			script_tag.html(message.params)
-			document.head.appendChild(script_tag[0])
-		else
-			@sendInner message # Pass message to inner frame
-
-	# Incoming message from inner frame
-	onMessageInner: (e) =>
-		# No nonce security enabled, test if window opener present
-		if not window.postmessage_nonce_security and @opener_tested == false
-			if window.opener and window.opener != window
-				@log "Opener present", window.opener
-				@displayOpenerDialog()
-				return false
-			else
-				@opener_tested = true
-
-		message = e.data
-		# Invalid message (probably not for us)
-		if not message.cmd
-			@log "Invalid message:", message
-			return false
-
-		# Test nonce security to avoid third-party messages
-		if window.postmessage_nonce_security and message.wrapper_nonce != window.wrapper_nonce
-			@log "Message nonce error:", message.wrapper_nonce, '!=', window.wrapper_nonce
-			return
-
-		@handleMessage message
-
-	cmd: (cmd, params={}, cb=null) =>
-		message = {}
-		message.cmd = cmd
-		message.params = params
-		message.id = @next_cmd_message_id
-		if cb
-			@ws.waiting_cb[message.id] = cb
-		@next_cmd_message_id -= 1
-
-		@handleMessage(message)
-
-	handleMessage: (message) =>
-		cmd = message.cmd
-		if cmd == "innerReady"
-			@inner_ready = true
-			if @ws.ws.readyState == 1 and not @wrapperWsInited # If ws already opened
-				@sendInner {"cmd": "wrapperOpenedWebsocket"}
-				@wrapperWsInited = true
-		else if cmd == "innerLoaded" or cmd == "wrapperInnerLoaded"
-			if window.location.hash
-				$("#inner-iframe")[0].src += window.location.hash # Hash tag
-				@log "Added hash to location", $("#inner-iframe")[0].src
-		else if cmd == "wrapperNotification" # Display notification
-			@actionNotification(message)
-		else if cmd == "wrapperConfirm" # Display confirm message
-			@actionConfirm(message)
-		else if cmd == "wrapperPrompt" # Prompt input
-			@actionPrompt(message)
-		else if cmd == "wrapperProgress" # Progress bar
-			@actionProgress(message)
-		else if cmd == "wrapperSetViewport" # Set the viewport
-			@actionSetViewport(message)
-		else if cmd == "wrapperSetTitle"
-			@log "wrapperSetTitle", message.params
-			$("head title").text(message.params)
-			@is_title_changed = true
-		else if cmd == "wrapperReload" # Reload current page
-			@actionReload(message)
-		else if cmd == "wrapperGetLocalStorage"
-			@actionGetLocalStorage(message)
-		else if cmd == "wrapperSetLocalStorage"
-			@actionSetLocalStorage(message)
-		else if cmd == "wrapperPushState"
-			query = @toRelativeQuery(message.params[2])
-			window.history.pushState(message.params[0], message.params[1], query)
-		else if cmd == "wrapperReplaceState"
-			query = @toRelativeQuery(message.params[2])
-			window.history.replaceState(message.params[0], message.params[1], query)
-		else if cmd == "wrapperGetState"
-			@sendInner {"cmd": "response", "to": message.id, "result": window.history.state}
-		else if cmd == "wrapperGetAjaxKey"
-			@sendInner {"cmd": "response", "to": message.id, "result": window.ajax_key}
-		else if cmd == "wrapperOpenWindow"
-			@actionOpenWindow(message.params)
-		else if cmd == "wrapperPermissionAdd"
-			@actionPermissionAdd(message)
-		else if cmd == "wrapperRequestFullscreen"
-			@actionRequestFullscreen()
-		else if cmd == "wrapperWebNotification"
-			@actionWebNotification(message)
-		else if cmd == "wrapperCloseWebNotification"
-			@actionCloseWebNotification(message)
-		else # Send to websocket
-			if message.id < 1000000
-				if message.cmd == "fileWrite" and not @modified_panel_updater_timer and site_info?.settings?.own
-					@modified_panel_updater_timer = setTimeout ( => @updateModifiedPanel(); @modified_panel_updater_timer = null ), 1000
-				@ws.send(message) # Pass message to websocket
-			else
-				@log "Invalid inner message id"
-
-	toRelativeQuery: (query=null) ->
-		if query == null
-			query = window.location.search
-		back = window.location.pathname
-		if back.match /^\/[^\/]+$/ # Add / after site address if called without it
-			back += "/"
-		if query.startsWith("#")
-			back = query
-		else if query.replace("?", "")
-			back += "?"+query.replace("?", "")
-		return back
-
-
-	displayOpenerDialog: ->
-		elem = $("<div class='opener-overlay'><div class='dialog'>You have opened this page by clicking on a link. Please, confirm if you want to load this site.<a href='?' target='_blank' class='button'>Open site</a></div></div>")
-		elem.find('a').on "click", ->
-			window.open("?", "_blank")
-			window.close()
-			return false
-		$("body").prepend(elem)
-
-	# - Actions -
-
-	actionOpenWindow: (params) ->
-		if typeof(params) == "string"
-			w = window.open()
-			w.opener = null
-			w.location = params
-		else
-			w = window.open(null, params[1], params[2])
-			w.opener = null
-			w.location = params[0]
-
-	actionRequestFullscreen: ->
-		elem = document.getElementById("inner-iframe")
-		request_fullscreen = elem.requestFullScreen || elem.webkitRequestFullscreen || elem.mozRequestFullScreen || elem.msRequestFullScreen
-		request_fullscreen.call(elem)
-
-	actionWebNotification: (message) ->
-		$.when(@event_site_info).done =>
-			# Check that the wrapper may send notifications
-			if Notification.permission == "granted"
-				@displayWebNotification message
-			else if Notification.permission == "denied"
-				res = {"error": "Web notifications are disabled by the user"}
-				@sendInner {"cmd": "response", "to": message.id, "result": res}
-			else
-				Notification.requestPermission().then (permission) =>
-					if permission == "granted"
-						@displayWebNotification message
-
-	actionCloseWebNotification: (message) ->
-		$.when(@event_site_info).done =>
-			id = message.params[0]
-			@web_notifications[id].close()
-
-	displayWebNotification: (message) ->
-		title = message.params[0]
-		id = message.params[1]
-		options = message.params[2]
-		notification = new Notification(title, options)
-		@web_notifications[id] = notification
-		notification.onshow = () =>
-			@sendInner {"cmd": "response", "to": message.id, "result": "ok"}
-		notification.onclick = (e) =>
-			if not options.focus_tab
-				e.preventDefault()
-			@sendInner {"cmd": "webNotificationClick", "params": {"id": id}}
-		notification.onclose = () =>
-			@sendInner {"cmd": "webNotificationClose", "params": {"id": id}}
-			delete @web_notifications[id]
-
-	actionPermissionAdd: (message) ->
-		permission = message.params
-		$.when(@event_site_info).done =>
-			if permission in @site_info.settings.permissions
-				return false
-			@ws.cmd "permissionDetails", permission, (permission_details) =>
-				@displayConfirm "This site requests permission:" + " <b>#{@toHtmlSafe(permission)}</b>" + "<br><small style='color: #4F4F4F'>#{permission_details}</small>", "Grant", =>
-					@ws.cmd "permissionAdd", permission, (res) =>
-						@sendInner {"cmd": "response", "to": message.id, "result": res}
-
-	actionNotification: (message) ->
-		message.params = @toHtmlSafe(message.params) # Escape html
-		body =  $("<span class='message'>"+message.params[1]+"</span>")
-		@notifications.add("notification-#{message.id}", message.params[0], body, message.params[2])
-
-	displayConfirm: (body, captions, cb) ->
-		body = $("<span class='message-outer'><span class='message'>"+body+"</span></span>")
-		buttons = $("<span class='buttons'></span>")
-		if captions not instanceof Array then captions = [captions]  # Convert to list if necessary
-		for caption, i in captions
-			button = $("<a></a>", {href: "#" + caption, class: "button button-confirm button-#{caption} button-#{i+1}", "data-value": i + 1})  # Add confirm button
-			button.text(caption)
-			((button) =>
-				button.on "click", (e) =>
-					@verifyEvent button, e
-					cb(parseInt(e.currentTarget.dataset.value))
-					return false
-			)(button)
-			buttons.append(button)
-		body.append(buttons)
-		@notifications.add("notification-#{caption}", "ask", body)
-
-		buttons.first().focus()
-		$(".notification").scrollLeft(0)
-
-
-	actionConfirm: (message, cb=false) ->
-		message.params = @toHtmlSafe(message.params) # Escape html
-		if message.params[1] then caption = message.params[1] else caption = "ok"
-		@displayConfirm message.params[0], caption, (res) =>
-			@sendInner {"cmd": "response", "to": message.id, "result": res} # Response to confirm
-			return false
-
-
-	displayPrompt: (message, type, caption, placeholder, cb) ->
-		body = $("<span class='message'></span>").html(message)
-		placeholder ?= ""
-
-		input = $("<input/>", {type: type, class: "input button-#{type}", placeholder: placeholder}) # Add input
-		input.on "keyup", (e) => # Send on enter
-			@verifyEvent input, e
-			if e.keyCode == 13
-				cb input.val() # Response to confirm
-		body.append(input)
-
-		button = $("<a></a>", {href: "#" + caption, class: "button button-#{caption}"}).text(caption) # Add confirm button
-		button.on "click", (e) => # Response on button click
-			@verifyEvent button, e
-			cb input.val()
-			return false
-		body.append(button)
-
-		@notifications.add("notification-#{message.id}", "ask", body)
-
-		input.focus()
-		$(".notification").scrollLeft(0)
-
-
-	actionPrompt: (message) ->
-		message.params = @toHtmlSafe(message.params) # Escape html
-		if message.params[1] then type = message.params[1] else type = "text"
-		caption = if message.params[2] then message.params[2] else "OK"
-		if message.params[3]?
-			placeholder = message.params[3]
-		else
-			placeholder = ""
-
-		@displayPrompt message.params[0], type, caption, placeholder, (res) =>
-			@sendInner {"cmd": "response", "to": message.id, "result": res} # Response to confirm
-
-	displayProgress: (type, body, percent) ->
-		percent = Math.min(100, percent)/100
-		offset = 75-(percent*75)
-		circle = """
-			<div class="circle"><svg class="circle-svg" width="30" height="30" viewport="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg">
-  				<circle r="12" cx="15" cy="15" fill="transparent" class="circle-bg"></circle>
-  				<circle r="12" cx="15" cy="15" fill="transparent" class="circle-fg" style="stroke-dashoffset: #{offset}"></circle>
-			</svg></div>
-		"""
-		body = "<span class='message'>"+body+"</span>" + circle
-		elem = $(".notification-#{type}")
-		if elem.length
-			width = $(".body .message", elem).outerWidth()
-			$(".body .message", elem).html(body)
-			if $(".body .message", elem).css("width") == ""
-				$(".body .message", elem).css("width", width)
-			$(".body .circle-fg", elem).css("stroke-dashoffset", offset)
-		else
-			elem = @notifications.add(type, "progress", $(body))
-		if percent > 0
-			$(".body .circle-bg", elem).css {"animation-play-state": "paused", "stroke-dasharray": "180px"}
-
-		if $(".notification-icon", elem).data("done")
-			return false
-		else if percent >= 1  # Done
-			$(".circle-fg", elem).css("transition", "all 0.3s ease-in-out")
-			setTimeout (->
-				$(".notification-icon", elem).css {transform: "scale(1)", opacity: 1}
-				$(".notification-icon .icon-success", elem).css {transform: "rotate(45deg) scale(1)"}
-			), 300
-			setTimeout (=>
-				@notifications.close elem
-			), 3000
-			$(".notification-icon", elem).data("done", true)
-		else if percent < 0  # Error
-			$(".body .circle-fg", elem).css("stroke", "#ec6f47").css("transition", "transition: all 0.3s ease-in-out")
-			setTimeout (=>
-				$(".notification-icon", elem).css {transform: "scale(1)", opacity: 1}
-				elem.removeClass("notification-done").addClass("notification-error")
-				$(".notification-icon .icon-success", elem).removeClass("icon-success").html("!")
-			), 300
-			$(".notification-icon", elem).data("done", true)
-
-
-	actionProgress: (message) ->
-		message.params = @toHtmlSafe(message.params) # Escape html
-		@displayProgress(message.params[0], message.params[1], message.params[2])
-
-	actionSetViewport: (message) ->
-		@log "actionSetViewport", message
-		if $("#viewport").length > 0
-			$("#viewport").attr("content", @toHtmlSafe message.params)
-		else
-			$('<meta name="viewport" id="viewport">').attr("content", @toHtmlSafe message.params).appendTo("head")
-
-	actionReload: (message) ->
-		@reload(message.params[0])
-
-	reload: (url_post="") ->
-		@log "Reload"
-		current_url = window.location.toString().replace(/#.*/g, "")
-		if url_post
-			if current_url.indexOf("?") > 0
-				window.location = current_url + "&" + url_post
-			else
-				window.location = current_url + "?" + url_post
-		else
-			window.location.reload()
-
-
-	actionGetLocalStorage: (message) ->
-		$.when(@event_site_info).done =>
-			data = localStorage.getItem "site.#{@site_info.address}.#{@site_info.auth_address}"
-			if not data # Migrate from non auth_address based local storage
-				data = localStorage.getItem "site.#{@site_info.address}"
-				if data
-					localStorage.setItem "site.#{@site_info.address}.#{@site_info.auth_address}", data
-					localStorage.removeItem "site.#{@site_info.address}"
-					@log "Migrated LocalStorage from global to auth_address based"
-			if data then data = JSON.parse(data)
-			@sendInner {"cmd": "response", "to": message.id, "result": data}
-
-
-	actionSetLocalStorage: (message) ->
-		$.when(@event_site_info).done =>
-			back = localStorage.setItem "site.#{@site_info.address}.#{@site_info.auth_address}", JSON.stringify(message.params)
-			@sendInner {"cmd": "response", "to": message.id, "result": back}
-
-
-	# EOF actions
-
-
-	onOpenWebsocket: (e) =>
-		if window.show_loadingscreen   # Get info on modifications
-			@ws.cmd "channelJoin", {"channels": ["siteChanged", "serverChanged", "announcerChanged"]}
-		else
-			@ws.cmd "channelJoin", {"channels": ["siteChanged", "serverChanged"]}
-		if not @wrapperWsInited and @inner_ready
-			@sendInner {"cmd": "wrapperOpenedWebsocket"} # Send to inner frame
-			@wrapperWsInited = true
-		if window.show_loadingscreen
-			@ws.cmd "serverInfo", [], (server_info) =>
-				@server_info = server_info
-
-			@ws.cmd "announcerInfo", [], (announcer_info) =>
-				@setAnnouncerInfo(announcer_info)
-
-		if @inner_loaded # Update site info
-			@reloadSiteInfo()
-
-		# If inner frame not loaded for 2 sec show peer informations on loading screen by loading site info
-		setTimeout (=>
-			if not @site_info then @reloadSiteInfo()
-		), 2000
-
-		if @ws_error
-			@notifications.add("connection", "done", "Connection with <b>UiServer Websocket</b> recovered.", 6000)
-			@ws_error = null
-
-
-	onCloseWebsocket: (e) =>
-		@wrapperWsInited = false
-		setTimeout (=> # Wait a bit, maybe its page closing
-			@sendInner {"cmd": "wrapperClosedWebsocket"} # Send to inner frame
-			if e and e.code == 1000 and e.wasClean == false # Server error please reload page
-				@ws_error = @notifications.add("connection", "error", "UiServer Websocket error, please reload the page.")
-			else if e and e.code == 1001 and e.wasClean == true  # Navigating to other page
-				return
-			else if not @ws_error
-				@ws_error = @notifications.add("connection", "error", "Connection with <b>UiServer Websocket</b> was lost. Reconnecting...")
-		), 1000
-
-
-	# Iframe loaded
-	onPageLoad: (e) =>
-		@log "onPageLoad"
-		@inner_loaded = true
-		if not @inner_ready then @sendInner {"cmd": "wrapperReady"} # Inner frame loaded before wrapper
-		#if not @site_error then @loading.hideScreen() # Hide loading screen
-		if @ws.ws.readyState == 1 and not @site_info # Ws opened
-			@reloadSiteInfo()
-		else if @site_info and @site_info.content?.title? and not @is_title_changed
-			window.document.title = @site_info.content.title + " - ZeroNet"
-			@log "Setting title to", window.document.title
-
-	onWrapperLoad: =>
-		@script_nonce = window.script_nonce
-		@wrapper_key = window.wrapper_key
-		# Cleanup secret variables
-		delete window.wrapper
-		delete window.wrapper_key
-		delete window.script_nonce
-		$("#script_init").remove()
-
-	# Send message to innerframe
-	sendInner: (message) ->
-		@inner.postMessage(message, '*')
-
-
-	# Get site info from UiServer
-	reloadSiteInfo: ->
-		if @loading.screen_visible # Loading screen visible
-			params = {"file_status": window.file_inner_path} # Query the current required file status
-		else
-			params = {}
-
-		@ws.cmd "siteInfo", params, (site_info) =>
-			@address = site_info.address
-			@setSiteInfo site_info
-
-			if site_info.settings.size > site_info.size_limit * 1024 * 1024 and not @loading.screen_visible  # Site size too large and not displaying it yet
-				@displayConfirm "Site is larger than allowed: #{(site_info.settings.size/1024/1024).toFixed(1)}MB/#{site_info.size_limit}MB", "Set limit to #{site_info.next_size_limit}MB", =>
-					@ws.cmd "siteSetLimit", [site_info.next_size_limit], (res) =>
-						if res == "ok"
-							@notifications.add("size_limit", "done", "Site storage limit modified!", 5000)
-
-			if site_info.content?.title? and not @is_title_changed
-				window.document.title = site_info.content.title + " - ZeroNet"
-				@log "Setting title to", window.document.title
-
-
-	# Got setSiteInfo from websocket UiServer
-	setSiteInfo: (site_info) ->
-		if site_info.event? # If loading screen visible add event to it
-			# File started downloading
-			if site_info.event[0] == "file_added" and site_info.bad_files
-				@loading.printLine("#{site_info.bad_files} files needs to be downloaded")
-			# File finished downloading
-			else if site_info.event[0] == "file_done"
-				@loading.printLine("#{site_info.event[1]} downloaded")
-				if site_info.event[1] == window.file_inner_path # File downloaded we currently on
-					@loading.hideScreen()
-					if not @site_info then @reloadSiteInfo()
-					if site_info.content and not @is_title_changed
-						window.document.title = site_info.content.title + " - ZeroNet"
-						@log "Required file #{window.file_inner_path} done, setting title to", window.document.title
-					if not window.show_loadingscreen
-						@notifications.add("modified", "info", "New version of this page has just released.<br>Reload to see the modified content.")
-			# File failed downloading
-			else if site_info.event[0] == "file_failed"
-				@site_error = site_info.event[1]
-				if site_info.settings.size > site_info.size_limit*1024*1024 # Site size too large and not displaying it yet
-					@loading.showTooLarge(site_info)
-
-				else
-					@loading.printLine("#{site_info.event[1]} download failed", "error")
-			# New peers found
-			else if site_info.event[0] == "peers_added"
-				@loading.printLine("Peers found: #{site_info.peers}")
-
-		if @loading.screen_visible and not @site_info # First site info display current peers
-			if site_info.peers > 1
-				@loading.printLine "Peers found: #{site_info.peers}"
-			else
-				@site_error = "No peers found"
-				@loading.printLine "No peers found"
-
-		if not @site_info and not @loading.screen_visible and $("#inner-iframe").attr("src").replace("?wrapper=False", "").replace(/\?wrapper_nonce=[A-Za-z0-9]+/, "").indexOf("?") == -1 # First site info and we are on mainpage (does not have other parameter thatn wrapper)
-			if site_info.size_limit*1.1 < site_info.next_size_limit # Need upgrade soon
-				@displayConfirm "Running out of size limit (#{(site_info.settings.size/1024/1024).toFixed(1)}MB/#{site_info.size_limit}MB)", "Set limit to #{site_info.next_size_limit}MB", =>
-					@ws.cmd "siteSetLimit", [site_info.next_size_limit], (res) =>
-						if res == "ok"
-							@notifications.add("size_limit", "done", "Site storage limit modified!", 5000)
-					return false
-
-		if @loading.screen_visible and @inner_loaded and site_info.settings.size < site_info.size_limit * 1024 * 1024 and site_info.settings.size > 0 # Loading screen still visible, but inner loaded
-			@log "Loading screen visible, but inner loaded"
-			@loading.hideScreen()
-
-		if site_info?.settings?.own and site_info?.settings?.modified != @site_info?.settings?.modified
-			@updateModifiedPanel()
-
-		if @loading.screen_visible and site_info.settings.size > site_info.size_limit * 1024 * 1024
-			@log "Site too large"
-			@loading.showTooLarge(site_info)
-
-		@site_info = site_info
-		@event_site_info.resolve()
-
-	siteSign: (inner_path, cb) =>
-		if @site_info.privatekey
-			# Privatekey stored in users.json
-			@infopanel.elem.find(".button").addClass("loading")
-			@ws.cmd "siteSign", {privatekey: "stored", inner_path: inner_path, update_changed_files: true}, (res) =>
-				if res == "ok"
-					cb?(true)
-				else
-					cb?(false)
-				@infopanel.elem.find(".button").removeClass("loading")
-		else
-			# Ask the user for privatekey
-			@displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key
-				@infopanel.elem.find(".button").addClass("loading")
-				@ws.cmd "siteSign", {privatekey: privatekey, inner_path: inner_path, update_changed_files: true}, (res) =>
-					if res == "ok"
-						cb?(true)
-					else
-						cb?(false)
-					@infopanel.elem.find(".button").removeClass("loading")
-
-	sitePublish: (inner_path) =>
-		@ws.cmd "sitePublish", {"inner_path": inner_path, "sign": false}
-
-	updateModifiedPanel: =>
-		@ws.cmd "siteListModifiedFiles", [], (res) =>
-			num = res.modified_files?.length
-			if num > 0
-				closed = @site_info.settings.modified_files_notification == false
-				@infopanel.show(closed)
-			else
-				@infopanel.hide()
-
-			if num > 0
-				@infopanel.setTitle(
-					"#{res.modified_files.length} modified file#{if num > 1 then 's' else ''}",
-					res.modified_files.join(", ")
-				)
-				@infopanel.setClosedNum(num)
-				@infopanel.setAction "Sign & Publish", =>
-					@siteSign "content.json", (res) =>
-						if (res)
-							@notifications.add "sign", "done", "content.json Signed!", 5000
-							@sitePublish("content.json")
-					return false
-			@log "siteListModifiedFiles", num, res
-
-	setAnnouncerInfo: (announcer_info) ->
-		status_db = {announcing: [], error: [], announced: []}
-		for key, val of announcer_info.stats
-			if val.status
-				status_db[val.status].push(val)
-		status_line = "Trackers announcing: #{status_db.announcing.length}, error: #{status_db.error.length}, done: #{status_db.announced.length}"
-		if @announcer_line
-			@announcer_line.text(status_line)
-		else
-			@announcer_line = @loading.printLine(status_line)
-
-		if status_db.error.length > (status_db.announced.length + status_db.announcing.length) and status_db.announced.length < 3
-			@loading.showTrackerTorBridge(@server_info)
-
-	updateProgress: (site_info) ->
-		if site_info.tasks > 0 and site_info.started_task_num > 0
-			@loading.setProgress 1-(Math.max(site_info.tasks, site_info.bad_files) / site_info.started_task_num)
-		else
-			@loading.hideProgress()
-
-
-	toHtmlSafe: (values) ->
-		if values not instanceof Array then values = [values] # Convert to array if its not
-		for value, i in values
-			if value instanceof Array
-				value = @toHtmlSafe(value)
-			else
-				value = String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;') # Escape dangerous characters
-				value = value.replace(/&lt;([\/]{0,1}(br|b|u|i|small))&gt;/g, "<$1>") # Unescape b, i, u, br tags
-			values[i] = value
-		return values
-
-
-	setSizeLimit: (size_limit, reload=true) =>
-		@log "setSizeLimit: #{size_limit}, reload: #{reload}"
-		@inner_loaded = false  # Inner frame not loaded, just a 404 page displayed
-		@ws.cmd "siteSetLimit", [size_limit], (res) =>
-			if res != "ok"
-				return false
-			@loading.printLine res
-			@inner_loaded = false
-			if reload then @reloadIframe()
-		return false
-
-	reloadIframe: =>
-		src = $("iframe").attr("src")
-		@ws.cmd "serverGetWrapperNonce", [], (wrapper_nonce) =>
-			src = src.replace(/wrapper_nonce=[A-Za-z0-9]+/, "wrapper_nonce=" + wrapper_nonce)
-			@log "Reloading iframe using url", src
-			$("iframe").attr "src", src
-
-	log: (args...) ->
-		console.log "[Wrapper]", args...
-
-origin = window.server_url or window.location.href.replace(/(\:\/\/.*?)\/.*/, "$1")
-
-if origin.indexOf("https:") == 0
-	proto = { ws: 'wss', http: 'https' }
-else
-	proto = { ws: 'ws', http: 'http' }
-
-ws_url = proto.ws + ":" + origin.replace(proto.http+":", "") + "/ZeroNet-Internal/Websocket?wrapper_key=" + window.wrapper_key
-
-window.wrapper = new Wrapper(ws_url)
-
diff --git a/src/Ui/media/WrapperZeroFrame.coffee b/src/Ui/media/WrapperZeroFrame.coffee
deleted file mode 100644
index 14313dd3..00000000
--- a/src/Ui/media/WrapperZeroFrame.coffee
+++ /dev/null
@@ -1,22 +0,0 @@
-class WrapperZeroFrame
-	constructor: (wrapper) ->
-		@wrapperCmd = wrapper.cmd
-		@wrapperResponse = wrapper.ws.response
-		console.log "WrapperZeroFrame", wrapper
-
-	cmd: (cmd, params={}, cb=null) =>
-		@wrapperCmd(cmd, params, cb)
-
-	response: (to, result) =>
-		@wrapperResponse(to, result)
-
-	isProxyRequest: ->
-		return window.location.pathname == "/"
-
-	certSelectGotoSite: (elem) =>
-		href = $(elem).attr("href")
-		if @isProxyRequest() # Fix for proxy request
-			$(elem).attr("href", "http://zero#{href}")
-
-
-window.zeroframe = new WrapperZeroFrame(window.wrapper)
diff --git a/src/Ui/media/ZeroSiteTheme.coffee b/src/Ui/media/ZeroSiteTheme.coffee
deleted file mode 100644
index 79adb671..00000000
--- a/src/Ui/media/ZeroSiteTheme.coffee
+++ /dev/null
@@ -1,49 +0,0 @@
-DARK = "(prefers-color-scheme: dark)"
-LIGHT = "(prefers-color-scheme: light)"
-
-mqDark = window.matchMedia(DARK)
-mqLight = window.matchMedia(LIGHT)
-
-
-changeColorScheme = (theme) ->
-    zeroframe.cmd "userGetGlobalSettings", [], (user_settings) ->
-        if user_settings.theme != theme
-            user_settings.theme = theme
-            zeroframe.cmd "userSetGlobalSettings", [user_settings], (status) ->
-                if status == "ok"
-                    location.reload()
-                return
-        return
-    return
-
-
-displayNotification = ({matches, media}) ->
-    if !matches
-        return
-
-    zeroframe.cmd "siteInfo", [], (site_info) ->
-        if "ADMIN" in site_info.settings.permissions
-            zeroframe.cmd "wrapperNotification", ["info", "Your system's theme has been changed.<br>Please reload site to use it."]
-        else
-            zeroframe.cmd "wrapperNotification", ["info", "Your system's theme has been changed.<br>Please open ZeroHello to use it."]
-        return
-    return
-
-
-detectColorScheme = ->
-    if mqDark.matches
-        changeColorScheme("dark")
-    else if mqLight.matches
-        changeColorScheme("light")
-
-    mqDark.addListener(displayNotification)
-    mqLight.addListener(displayNotification)
-
-    return
-
-
-zeroframe.cmd "userGetGlobalSettings", [], (user_settings) ->
-    if user_settings.use_system_theme == true
-        detectColorScheme()
-
-    return
diff --git a/src/Ui/media/lib/RateLimit.coffee b/src/Ui/media/lib/RateLimit.coffee
deleted file mode 100644
index 17c67433..00000000
--- a/src/Ui/media/lib/RateLimit.coffee
+++ /dev/null
@@ -1,14 +0,0 @@
-limits = {}
-call_after_interval = {}
-window.RateLimit = (interval, fn) ->
-	if not limits[fn]
-		call_after_interval[fn] = false
-		fn() # First call is not delayed
-		limits[fn] = setTimeout (->
-			if call_after_interval[fn]
-				fn()
-			delete limits[fn]
-			delete call_after_interval[fn]
-		), interval
-	else # Called within iterval, delay the call
-		call_after_interval[fn] = true
diff --git a/src/Ui/media/lib/Translate.coffee b/src/Ui/media/lib/Translate.coffee
deleted file mode 100644
index 2b323494..00000000
--- a/src/Ui/media/lib/Translate.coffee
+++ /dev/null
@@ -1 +0,0 @@
-window._ = (s) -> return s
\ No newline at end of file
diff --git a/src/Ui/media/lib/ZeroWebsocket.coffee b/src/Ui/media/lib/ZeroWebsocket.coffee
deleted file mode 100644
index aa795677..00000000
--- a/src/Ui/media/lib/ZeroWebsocket.coffee
+++ /dev/null
@@ -1,95 +0,0 @@
-class ZeroWebsocket
-	constructor: (url) ->
-		@url = url
-		@next_message_id = 1
-		@waiting_cb = {}
-		@init()
-
-
-	init: ->
-		@
-
-
-	connect: ->
-		@ws = new WebSocket(@url)
-		@ws.onmessage = @onMessage
-		@ws.onopen = @onOpenWebsocket
-		@ws.onerror = @onErrorWebsocket
-		@ws.onclose = @onCloseWebsocket
-		@connected = false
-		@message_queue = []
-
-
-	onMessage: (e) =>
-		message = JSON.parse(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 == "ping"
-			@response message.id, "pong"
-		else
-			@route cmd, message
-
-	route: (cmd, message) =>
-		@log "Unknown command", 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) ->
-		if not message.id?
-			message.id = @next_message_id
-			@next_message_id += 1
-		if @connected
-			@ws.send(JSON.stringify(message))
-		else
-			@log "Not connected, adding message to queue"
-			@message_queue.push(message)
-		if cb
-			@waiting_cb[message.id] = cb
-
-
-	log: (args...) =>
-		console.log "[ZeroWebsocket]", args...
-
-
-	onOpenWebsocket: (e) =>
-		@log "Open"
-		@connected = true
-
-		# Process messages sent before websocket opened
-		for message in @message_queue
-			@ws.send(JSON.stringify(message))
-		@message_queue = []
-
-		if @onOpen? then @onOpen(e)
-
-
-	onErrorWebsocket: (e) =>
-		@log "Error", e
-		if @onError? then @onError(e)
-
-
-	onCloseWebsocket: (e, reconnect=10000) =>
-		@log "Closed", e
-		@connected = false
-		if e and e.code == 1000 and e.wasClean == false
-			@log "Server error, please reload the page", e.wasClean
-		else # Connection error
-			setTimeout (=>
-				@log "Reconnecting..."
-				@connect()
-			), reconnect
-		if @onClose? then @onClose(e)
-
-
-window.ZeroWebsocket = ZeroWebsocket
diff --git a/src/Ui/media/lib/jquery.csslater.coffee b/src/Ui/media/lib/jquery.csslater.coffee
deleted file mode 100644
index ec266622..00000000
--- a/src/Ui/media/lib/jquery.csslater.coffee
+++ /dev/null
@@ -1,36 +0,0 @@
-jQuery.fn.readdClass = (class_name) ->
-	elem = @
-	elem.removeClass class_name
-	setTimeout ( ->
-		elem.addClass class_name
-	), 1
-	return @
-
-jQuery.fn.removeLater = (time = 500) ->
-	elem = @
-	setTimeout ( ->
-		elem.remove()
-	), time
-	return @
-
-jQuery.fn.hideLater = (time = 500) ->
-	elem = @
-	setTimeout ( ->
-		if elem.css("opacity") == 0
-			elem.css("display", "none")
-	), time
-	return @
-
-jQuery.fn.addClassLater = (class_name, time = 5) ->
-	elem = @
-	setTimeout ( ->
-		elem.addClass(class_name)
-	), time
-	return @
-
-jQuery.fn.cssLater = (name, val, time = 500) ->
-	elem = @
-	setTimeout ( ->
-		elem.css name, val
-	), time
-	return @
\ No newline at end of file

From cd5190ec4d473038de2bd10b8a862394f1c35642 Mon Sep 17 00:00:00 2001
From: Maxim Portnyagin <maxim@prtngn.cc>
Date: Tue, 31 May 2022 15:29:58 +0400
Subject: [PATCH 092/333] Add default command to start

---
 Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Dockerfile b/Dockerfile
index 81a1d290..955ce752 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,6 +12,6 @@ RUN python3 -m venv venv \
 CMD source venv/bin/activate \
  && python3 zeronet.py --ui_ip "*" --fileserver_port 26552 \
     --tor $TOR_ENABLED --tor_controller tor:$TOR_CONTROL_PORT \
-    --tor_proxy tor:$TOR_SOCKS_PORT --tor_password $TOR_CONTROL_PASSWD
+    --tor_proxy tor:$TOR_SOCKS_PORT --tor_password $TOR_CONTROL_PASSWD main
 
 EXPOSE 43110 26552

From 020620c68acddf132416efe7de3b2608a8cc68d3 Mon Sep 17 00:00:00 2001
From: Maxim Portnyagin <maxim@prtngn.cc>
Date: Tue, 31 May 2022 17:20:59 +0400
Subject: [PATCH 093/333] Add docs

---
 README-ru.md | 7 +++++++
 README.md    | 7 +++++++
 2 files changed, 14 insertions(+)

diff --git a/README-ru.md b/README-ru.md
index f01c2abe..5b3ddbc1 100644
--- a/README-ru.md
+++ b/README-ru.md
@@ -90,6 +90,13 @@ zeronet-conservancy — это форк/продолжение проекта [Z
  - `source venv/bin/activate`
  - `python3 zeronet.py`
 
+#### Создание образа Docker
+- создание образа: `docker build -t 0net:conservancy . -f Dockerfile`
+- и его запуск: `docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net:conservancy`
+- /path/to/0n/data/directory - директория, куда будут сохраняться все данные в том числе секретные ключи. Если вы запускаете в боевом режиме, не потеряйте эту папку!
+- или вы можете воспользоваться docker-compose: `docker compose up -d 0net` запускает два контейнера раздельно, для 0net и tor сервисов.
+- или: `docker compose up -d 0net-tor` запускает один контейнер с tor и 0net.
+
 #### альтернативный скрипт
  - после установки общих зависимостей и клонирования репозитория (как указано выше) запустите `start-venv.sh` который создаст для вас виртуальную среду и установит требования Python
  - больше удобных скриптов будует добавлено в ближайшее время
diff --git a/README.md b/README.md
index 652cb36d..bc9570c6 100644
--- a/README.md
+++ b/README.md
@@ -90,6 +90,13 @@ Install autoconf and other basic development tools, python3 and pip.
  - `source venv/bin/activate`
  - `python3 zeronet.py`
 
+#### Build Docker image
+- build 0net image: `docker build -t 0net:conservancy . -f Dockerfile`
+- and run it: `docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net:conservancy`
+- /path/to/0n/data/directory - directory, where all data will be saved, including your secret certificates. If you run it with production mode, do not remove this folder!
+- or you can run it with docker-compose: `docker compose up -d 0net` up two containers - 0net and tor separately.
+- or: `docker compose up -d 0net-tor` for run 0net and tor in one container.
+
 #### alternative script
  - after installing general dependencies and cloning repo (as above), run `start-venv.sh` which will create a virtual env for you and install python requirements
  - more convenience scripts to be added soon

From 1eb461ed0e026688891f49a25a86e9cd77b66f39 Mon Sep 17 00:00:00 2001
From: Maxim Portnyagin <maxim@prtngn.cc>
Date: Tue, 31 May 2022 17:35:04 +0400
Subject: [PATCH 094/333] Add about image with tor

---
 README-ru.md | 1 +
 README.md    | 1 +
 2 files changed, 2 insertions(+)

diff --git a/README-ru.md b/README-ru.md
index 5b3ddbc1..943ea8d8 100644
--- a/README-ru.md
+++ b/README-ru.md
@@ -92,6 +92,7 @@ zeronet-conservancy — это форк/продолжение проекта [Z
 
 #### Создание образа Docker
 - создание образа: `docker build -t 0net:conservancy . -f Dockerfile`
+- или создрание образа с встроенным tor: `docker build -t 0net:conservancy . -f Dockerfile.integrated_tor`
 - и его запуск: `docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net:conservancy`
 - /path/to/0n/data/directory - директория, куда будут сохраняться все данные в том числе секретные ключи. Если вы запускаете в боевом режиме, не потеряйте эту папку!
 - или вы можете воспользоваться docker-compose: `docker compose up -d 0net` запускает два контейнера раздельно, для 0net и tor сервисов.
diff --git a/README.md b/README.md
index bc9570c6..1c4ea57c 100644
--- a/README.md
+++ b/README.md
@@ -92,6 +92,7 @@ Install autoconf and other basic development tools, python3 and pip.
 
 #### Build Docker image
 - build 0net image: `docker build -t 0net:conservancy . -f Dockerfile`
+- or build 0net image with integrated tor: `docker build -t 0net:conservancy . -f Dockerfile.integrated_tor`
 - and run it: `docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net:conservancy`
 - /path/to/0n/data/directory - directory, where all data will be saved, including your secret certificates. If you run it with production mode, do not remove this folder!
 - or you can run it with docker-compose: `docker compose up -d 0net` up two containers - 0net and tor separately.

From 3a906635fb3a5b8a5bebd1481c5fa17a3c16930e Mon Sep 17 00:00:00 2001
From: Lucas Santos <lukas_santos.silva@hotmail.com>
Date: Fri, 3 Jun 2022 09:59:36 -0300
Subject: [PATCH 095/333] <iFallenHunt> I translated the README file into
 Brazilian Portuguese

---
 README-ptbr.md | 161 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 161 insertions(+)
 create mode 100644 README-ptbr.md

diff --git a/README-ptbr.md b/README-ptbr.md
new file mode 100644
index 00000000..5e1efa45
--- /dev/null
+++ b/README-ptbr.md
@@ -0,0 +1,161 @@
+
+# zeronet-conservancy
+
+zeronet-conservancy é um garfo/continuação do projeto [ZeroNet](https://github.com/HelloZeroNet/ZeroNet)
+(que foi abandonada por seu criador) que se dedica a sustentar a rede p2p existente e a desenvolver
+seus valores de descentralização e liberdade, enquanto muda gradualmente para uma rede mais bem projetada
+
+## Por que garfo?
+
+Durante a crise da onion-v3, precisávamos de um garfo que funcionasse com onion-v3 e não dependesse da confiança de um ou
+duas pessoas. Este garfo começou a partir do cumprimento dessa missão, implementando mudanças mínimas para
+[ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3) ramo que é fácil de ser auditado por qualquer pessoa. Enquanto
+você ainda pode usar as primeiras liberações do garfo para fazer funcionar a onion-v3, o objetivo deste garfo mudou desde então
+e nos dedicamos a resolver mais problemas e melhorar a experiência do usuário e a segurança por toda parte, até 
+a nova rede, completamente transparente e auditada está pronta e este projeto pode ser colocado para descansar
+
+## Por que 0net?
+
+* Acreditamos em redes e comunicação abertas, livres e não censuradas.
+* Nenhum ponto único de falha: O site permanece online desde que pelo menos 1 par seja
+  servindo-o.
+* Sem custos de hospedagem: Os sites são servidos por visitantes.
+* Impossível de ser desligado: Não está em lugar nenhum porque está em toda parte.
+* Rápido e funciona offline: Você pode acessar o site, mesmo que a Internet seja
+  indisponível.
+
+
+## Características
+
+ * Sites atualizados em tempo real
+ * Clonar websites em um clique
+ * Autorização sem senha usando chaves públicas/privadas
+ * Servidor SQL integrado com sincronização de dados P2P: permite um desenvolvimento dinâmico mais fácil do site
+ * Anonimato: Suporte de rede Tor com serviços ocultos .onion (incluindo suporte a onion-v3)
+ * conexões criptografadas TLS (através de clearnet)
+ * Abertura automática da porta uPnP (se optado por entrar)
+ * Plugin para suporte multiusuário (openproxy)
+ * Funciona com qualquer navegador/OS moderno
+
+
+## Como funciona?
+
+* Após iniciar o `zeronet.py` você poderá visitar os sites da zeronet utilizando
+  `http://127.0.0.1:43110/{zeronet_address}` (ex.
+  `http://127.0.0.1:43110/126NXcevn1AUehWFZLTBw7FrX1crEizQdr`).
+* Quando você visita um novo site zeronet, ele tenta encontrar pares usando o BitTorrent
+  para poder baixar os arquivos do site (html, css, js...) a partir deles.
+* Cada site visitado também é servido por você.
+* Cada site contém um arquivo `content.json` que contém todos os outros arquivos em um hash sha512
+  e uma assinatura gerada usando a chave privada do site.
+* Se o proprietário do site (que tem a chave privada para o endereço do site) modificar o
+  então ele assina o novo `content.json` e o publica para os colegas.
+  Em seguida, os pares verificam a integridade do `content.json` (utilizando o
+  assinatura), eles baixam os arquivos modificados e publicam o novo conteúdo para
+  outros colegas.
+
+Os links a seguir referem-se à ZeroNet original:
+
+- [Slideshow sobre criptografia ZeroNet, atualizações de sites, sites multiusuário "](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
+- [Perguntas mais freqüentes "](https://zeronet.io/docs/faq/)
+- [Documentação do Desenvolvedor da ZeroNet "](https://zeronet.io/docs/site_development/getting_started/)
+
+## Como aderir
+
+### Instalar a partir da fonte (recomendado)
+
+#### Dependências do sistema
+
+##### Genéricos unix-like (incluindo mac os x)
+
+Instalar o autoconf e outras ferramentas básicas de desenvolvimento, python3 e pip.
+
+##### Apt-based (debian, ubuntu, etc)
+ - `sudo apt update`
+ - `sudo apt install pkg-config python3-pip python3-venv`
+
+##### Android/Termux
+ - install [Termux](https://termux.com/) (no Termux você pode instalar pacotes via `pkg install <nomes de pacotes>`)
+ - Atualização do "pkg".
+ - Pkg install python automake git binutils" (TODO: verificar nova instalação se há mais dependências para instalar)
+ - (opcional) `pkg install tor`
+ - (opcional) rodar tor via comando `tor --ControlPort 9051 --CookieAuthentication 1` (você pode então abrir uma nova sessão deslizando para a direita)
+
+#### Construindo dependências python & running
+ - clonar este repo (NOTA: no Android/Termux você deve cloná-lo na pasta "home" do Termux, porque o ambiente virtual não pode viver no `storage/`)
+ - "python3 -m venv venv" (fazer python ambiente virtual, o último `venv` é apenas um nome, se você usar diferente você deve substituí-lo em comandos posteriores)
+ - "fonte venv/bin/activate" (activar ambiente)
+ - `python3 -m pip install -r requirements.txt` (instalar dependências)
+ - zeronet.py` (**run zeronet-conservancy!**)
+ - abra a página de desembarque em seu navegador navegando para: http://127.0.0.1:43110/
+ - para reiniciá-lo a partir de um terminal novo, você precisa navegar para redirecionar o diretório e:
+ - "fonte venv/bin/activate
+ - "python3 zeronet.py
+
+#### Construir imagem do Docker
+- construir imagem 0net: `docker build -t 0net:conservancy . -f Dockerfile`
+- ou construir imagem 0net com tor integrado: `docker build -t 0net:conservancy . -f Dockerfile.integrated_tor`
+- e dirigi-lo: `docker run --rm -it -v </path/to/0n/data/directório>:/app/data -p 43110:43110 -p 26552:26552 0net:conservancy''.
+- /caminho/até/0n/dados/diretório - diretório, onde todos os dados serão salvos, incluindo seus certificados secretos. Se você executá-lo com o modo de produção, não remova esta pasta!
+- ou você pode executá-lo com o docker-compose: `docker compose up -d 0net` sobe dois containers - 0net e tor separadamente.
+- ou: "docker compose up -d 0net-tor" para rodar 0net e tor em um recipiente.
+
+#### roteiro alternativo
+ - após instalar as dependências gerais e clonagem repo (como acima), execute `start-venv.sh` que criará um ambiente virtual para você e instalará os requisitos python
+ - mais roteiros de conveniência a serem adicionados em breve
+
+## Limitações atuais
+
+* As transações de arquivos não são comprimidas
+* Sem sites privados
+* Sem suporte de DHT
+* Elementos centralizados como o zeroid (estamos trabalhando nisso!)
+* Nenhuma proteção confiável contra spam (e nisto também)
+* Não funciona diretamente do navegador (uma das principais prioridades para o futuro médio)
+* Sem transparência de dados
+
+
+## Como posso criar um site ZeroNet?
+
+ Clique em **⋮*** > **"Criar site novo, vazio "** item do menu [página admin](http://127.0.0.1:43110/126NXcevn1AUehWFZLTBw7FrX1crEizQdr).
+ * Você será **re-direcionado *** para um site completamente novo que só pode ser modificado por você!
+ * Você pode encontrar e modificar o conteúdo de seu site no diretório **data/[endereço de seu site]**.
+ * Após as modificações abrir seu site, arraste o botão superior direito "0" para a esquerda, depois pressione **sign** e **publish** botões na parte inferior
+
+Próximos passos: [Documentação do Desenvolvedor da ZeroNet](https://zeronet.io/docs/site_development/getting_started/)
+
+## Ajude este projeto a permanecer vivo
+
+### Torne-se um mantenedor
+
+Precisamos de mais mantenedores! Torne-se um hoje! Você não precisa saber como codificar,
+há muito mais trabalho a ser feito.
+
+### Corrigir bugs e adicionar recursos
+
+Decidimos ir em frente e fazer uma web p2p perfeita, então precisamos de mais ajuda
+implementando-o.
+
+#### Faça seu site/bring seu conteúdo
+
+Sabemos que a documentação está faltando, mas tentamos o melhor para apoiar qualquer um
+que quer migrar. Não hesite em perguntar.
+
+#### Use-o e espalhe a palavra
+
+Certifique-se de dizer às pessoas por que você usa 0net e este garfo em particular! Pessoas
+precisam conhecer suas alternativas.
+
+### Mantenedores de suporte financeiro
+
+Atualmente, o principal desenvolvedor/mantenedor deste garfo é @caryoscelus. Você pode
+veja maneiras de doar para eles em https://caryoscelus.github.io/donate/ (ou verifique
+sidebar se você estiver lendo isto no github para mais maneiras). À medida que nossa equipe cresce, nós
+também criará contas de equipe em plataformas amigáveis de financiamento de multidões.
+
+Se você quiser ter certeza de que sua doação é reconhecida como doação para isto
+projeto, também há um endereço dedicado ao bitcoin para isso:
+1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6
+
+Se você quiser doar de uma maneira diferente, sinta-se à vontade para contatar o mantenedor ou
+criar uma publicação 
\ No newline at end of file

From ddc8fb9c733d5d74e7ee806da67ec54cbefabe80 Mon Sep 17 00:00:00 2001
From: Lucas Santos <lukas_santos.silva@hotmail.com>
Date: Fri, 3 Jun 2022 10:03:28 -0300
Subject: [PATCH 096/333] <iFallenHunt> I translated the README file into
 Brazlian Portuguese

---
 README-ptbr.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README-ptbr.md b/README-ptbr.md
index 5e1efa45..0fe0da11 100644
--- a/README-ptbr.md
+++ b/README-ptbr.md
@@ -158,4 +158,4 @@ projeto, também há um endereço dedicado ao bitcoin para isso:
 1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6
 
 Se você quiser doar de uma maneira diferente, sinta-se à vontade para contatar o mantenedor ou
-criar uma publicação 
\ No newline at end of file
+criar uma publicação!
\ No newline at end of file

From 2bc26bfb31443740f46d8bf59310e53625bcd4f2 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 4 Jun 2022 07:27:59 +0000
Subject: [PATCH 097/333] Split custom/BTC/Monero donations for sites

---
 plugins/Sidebar/SidebarPlugin.py | 36 +++++++++++++++++++++++++-------
 1 file changed, 29 insertions(+), 7 deletions(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index 987751e6..5c771543 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -434,15 +434,37 @@ class UiWebsocketPlugin(object):
              </div>
             </li>
         """))
-        donate_key = site.content_manager.contents.get("content.json", {}).get("donate", None)
-        if type(donate_key) is str and len(donate_key) > 0:
+        donate_generic = site.content_manager.contents.get("content.json", {}).get("donate", None) or site.content_manager.contents.get("content.json", {}).get("donate-generic", None)
+        donate_btc = site.content_manager.contents.get("content.json", {}).get("donate-btc", None)
+        donate_xmr = site.content_manager.contents.get("content.json", {}).get("donate-xmr", None)
+        donate_enabled = bool(donate_generic or donate_btc or donate_xmr)
+        if donate_enabled:
             body.append(_("""
             <li>
-             <label>{_[Donate]}</label><br>
-             <div class='flex'>
-               {donate_key}
-               <a href='bitcoin:{donate_key}' class='button' id='button-donate'>{_[Donate]}</a>
-             </div>
+              <label>{_[Donate]}</label><br>
+            """))
+        if donate_generic:
+            body.append(_("""
+            <div class='flex'>
+              {donate_generic}
+            </div>
+            """))
+        if donate_btc:
+            body.append(_("""
+            <div class='flex'>
+              {donate_btc}
+              <a href='bitcoin:{donate_btc}' class='button'>{_[Donate BTC]}</a>
+            </div>
+            """))
+        if donate_xmr:
+            body.append(_("""
+            <div class='flex'>
+              {donate_key}
+              <a href='monero:{donate_xmr}' class='button'>{_[Donate Monero]}</a>
+            </div>
+            """))
+        if donate_enabled:
+            body.append(_("""
             </li>
             """))
 

From 35eaa2a4d4829faa1d0f304ba14f33b4f82cd57c Mon Sep 17 00:00:00 2001
From: unmanbearpig <unmanbearpig@gmail.com>
Date: Sat, 4 Jun 2022 19:17:37 +0400
Subject: [PATCH 098/333] Add script for running zeronet-conservancy in Termux
 on Android

Instructions:
1. Download the script in termux
2. Run it
---
 termux.sh | 9 +++++++++
 1 file changed, 9 insertions(+)
 create mode 100644 termux.sh

diff --git a/termux.sh b/termux.sh
new file mode 100644
index 00000000..752eb3ac
--- /dev/null
+++ b/termux.sh
@@ -0,0 +1,9 @@
+
+# Script for running zeronet-conservancy in Termux on Android
+
+git clone https://github.com/zeronet-conservancy/zeronet-conservancy
+cd zeronet-conservancy
+pkg update
+pkg install python automake git binutils tor
+tor --ControlPort 9051 --CookieAuthentication 1 >/dev/null &
+./start-venv.sh

From ba77c99688d7a6dc61946a77b9d8dd1a0d48cd00 Mon Sep 17 00:00:00 2001
From: unmanbearpig <unmanbearpig@gmail.com>
Date: Sat, 4 Jun 2022 19:26:20 +0400
Subject: [PATCH 099/333] termux.sh: Pull zeronet if already cloned

---
 termux.sh | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

diff --git a/termux.sh b/termux.sh
index 752eb3ac..1eb365b1 100644
--- a/termux.sh
+++ b/termux.sh
@@ -1,9 +1,20 @@
 
 # Script for running zeronet-conservancy in Termux on Android
 
-git clone https://github.com/zeronet-conservancy/zeronet-conservancy
-cd zeronet-conservancy
-pkg update
-pkg install python automake git binutils tor
+if [[ -d zeronet-conservancy ]]; then
+	cd zeronet-conservancy
+	git pull --ff-only
+else
+	git clone https://github.com/zeronet-conservancy/zeronet-conservancy
+	cd zeronet-conservancy
+fi
+
+pkg update -y
+pkg install -y python automake git binutils tor
+
+echo "Starting tor..."
 tor --ControlPort 9051 --CookieAuthentication 1 >/dev/null &
+
+echo "Starting zeronet-conservancy..."
 ./start-venv.sh
+cd ..

From 6e702e4cd2fe3a691389940385eb0f60ad7ffb23 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 7 Jun 2022 15:54:48 +0000
Subject: [PATCH 100/333] README translations cross-references

---
 README-ptbr.md | 3 ++-
 README-ru.md   | 2 +-
 README.md      | 2 ++
 3 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/README-ptbr.md b/README-ptbr.md
index 0fe0da11..1421b9d0 100644
--- a/README-ptbr.md
+++ b/README-ptbr.md
@@ -1,6 +1,7 @@
-
 # zeronet-conservancy
 
+[in English](README.md) | [по-русски](README-ru.md) | [简体中文](README-zh-cn.md)
+
 zeronet-conservancy é um garfo/continuação do projeto [ZeroNet](https://github.com/HelloZeroNet/ZeroNet)
 (que foi abandonada por seu criador) que se dedica a sustentar a rede p2p existente e a desenvolver
 seus valores de descentralização e liberdade, enquanto muda gradualmente para uma rede mais bem projetada
diff --git a/README-ru.md b/README-ru.md
index 943ea8d8..36e1bc1c 100644
--- a/README-ru.md
+++ b/README-ru.md
@@ -1,6 +1,6 @@
 # zeronet-conservancy
 
-[English](./README.md) [简体中文](./README-zh-cn.md)
+[in English](README.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md)
 
 zeronet-conservancy — это форк/продолжение проекта [ZeroNet](https://github.com/HelloZeroNet/ZeroNet)
 (покинутого его создателем), предназначенный для поддержки существующей сети p2p и развития
diff --git a/README.md b/README.md
index 1c4ea57c..a592dcba 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
 # zeronet-conservancy
 
+[по-русски](README-ru.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md)
+
 zeronet-conservancy is a fork/continuation of [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) project
 (that has been abandoned by its creator) that is dedicated to sustaining existing p2p network and developing
 its values of decentralization and freedom, while gradually switching to a better designed network

From d919d295de85ff79bc2de683363d7cac78592d9a Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 7 Jun 2022 15:55:01 +0000
Subject: [PATCH 101/333] Chinese README translation requires update!

---
 README-zh-cn.md | 54 +++++--------------------------------------------
 1 file changed, 5 insertions(+), 49 deletions(-)

diff --git a/README-zh-cn.md b/README-zh-cn.md
index fabdb0e5..b9b86d66 100644
--- a/README-zh-cn.md
+++ b/README-zh-cn.md
@@ -1,9 +1,8 @@
-# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=py3)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/)
+# zeronet-conservancy
 
-[English](./README.md)
-
-使用 Bitcoin 加密和 BitTorrent 网络的去中心化网络 - https://zeronet.io
+**警告**:这个翻译已经过时了。请阅读英文版。对此翻译的任何贡献都将受到高度赞赏
 
+[in English](README.md) | [em português](README-ptbr.md) | [по-русски](README-ru.md)
 
 ## 为什么?
 
@@ -16,7 +15,6 @@
 
 ## 功能
  * 实时站点更新
- * 支持 Namecoin 的 .bit 域名
  * 安装方便:只需解压并运行
  * 一键克隆存在的站点
  * 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
@@ -48,46 +46,12 @@
 
 ## 屏幕截图
 
-![Screenshot](https://i.imgur.com/H60OAHY.png)
-![ZeroTalk](https://zeronet.io/docs/img/zerotalk.png)
-
 #### [ZeroNet 文档中的更多屏幕截图 »](https://zeronet.io/docs/using_zeronet/sample_sites/)
 
 
 ## 如何加入
 
-### Windows
 
- - 下载 [ZeroNet-py3-win64.zip](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist-win64/ZeroNet-py3-win64.zip) (18MB)
- - 在任意位置解压缩
- - 运行 `ZeroNet.exe`
- 
-### macOS
-
- - 下载 [ZeroNet-dist-mac.zip](https://github.com/HelloZeroNet/ZeroNet-dist/archive/mac/ZeroNet-dist-mac.zip) (13.2MB)
- - 在任意位置解压缩
- - 运行 `ZeroNet.app`
- 
-### Linux (x86-64bit)
-
- - `wget https://github.com/HelloZeroNet/ZeroNet-linux/archive/dist-linux64/ZeroNet-py3-linux64.tar.gz`
- - `tar xvpfz ZeroNet-py3-linux64.tar.gz`
- - `cd ZeroNet-linux-dist-linux64/`
- - 使用以下命令启动 `./ZeroNet.sh`
- - 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面
- 
- __提示:__ 若要允许在 Web 界面上的远程连接,使用以下命令启动 `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address`
-
-### 从源代码安装
-
- - `wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz`
- - `tar xvpfz ZeroNet-py3.tar.gz`
- - `cd ZeroNet-py3`
- - `sudo apt-get update`
- - `sudo apt-get install python3-pip`
- - `sudo python3 -m pip install -r requirements.txt`
- - 使用以下命令启动 `python3 zeronet.py`
- - 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面
 
 ## 现有限制
 
@@ -99,7 +63,7 @@
 
 ## 如何创建一个 ZeroNet 站点?
 
- * 点击 [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D) 站点的 **⋮** > **「新建空站点」** 菜单项
+ * 点击 [ZeroHello](http://127.0.0.1:43110/126NXcevn1AUehWFZLTBw7FrX1crEizQdr) 站点的 **⋮** > **「新建空站点」** 菜单项
  * 您将被**重定向**到一个全新的站点,该站点只能由您修改
  * 您可以在 **data/[您的站点地址]** 目录中找到并修改网站的内容
  * 修改后打开您的网站,将右上角的「0」按钮拖到左侧,然后点击底部的**签名**并**发布**按钮
@@ -108,16 +72,8 @@
 
 ## 帮助这个项目
 
-- Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX
-- Paypal: https://zeronet.io/docs/help_zeronet/donate/
-
-### 赞助商
-
-* [BrowserStack.com](https://www.browserstack.com) 使更好的 macOS/Safari 兼容性成为可能
+- Bitcoin: 1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6
 
 #### 感谢您!
 
 * 更多信息,帮助,变更记录和 zeronet 站点:https://www.reddit.com/r/zeronet/
-* 前往 [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 或 [gitter](https://gitter.im/HelloZeroNet/ZeroNet) 和我们聊天
-* [这里](https://gitter.im/ZeroNet-zh/Lobby)是一个 gitter 上的中文聊天室
-* Email: hello@zeronet.io (PGP: [960F FF2D 6C14 5AA6 13E8 491B 5B63 BAE6 CB96 13AE](https://zeronet.io/files/tamas@zeronet.io_pub.asc))

From 43f3685e4d55841f5cff8ccdb48db87dd086346c Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 7 Jun 2022 16:02:20 +0000
Subject: [PATCH 102/333] update changelog

---
 CHANGELOG.md | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a7218a72..d18d0011 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 ### zeronet-conservancy 0.7.5+
 maintainers: @caryoscelus, @d4708, @FraYoshi, @prtngn
+- easier termux startup script (@unmanbearpig)
+- Brazilian Portuguese readme translation (@iFallenHunt)
+- reduce fingerprinting information (@caryoscelus)
+- only enable site donations when activated (@caryoscelus)
+- new docker files (@prtngn)
+- updated Russian readme (@Programmator9000)
 - multiple improvements in sidebar button UX and icons (@d4708)
 - fuller debug messages (@caryoscelus)
 - new contributions are GPLv3+

From 4eda8fced2dc6c0f0eec36ce5cd695aea3789f97 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 8 Jun 2022 16:48:19 +0000
Subject: [PATCH 103/333] README: mention NixOS package

---
 README.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/README.md b/README.md
index a592dcba..b4c71302 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,10 @@ Following links relate to original ZeroNet:
 
 ## How to join
 
+### Install from your distribution repository
+
+- NixOS: https://search.nixos.org/packages?channel=22.05&show=zeronet-conservancy&type=packages&query=zeronet-conservancy
+
 ### Install from source (recommended)
 
 #### System dependencies

From 8d7c45d038626506639e3cd39d4260a7d64883df Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 8 Jun 2022 16:48:51 +0000
Subject: [PATCH 104/333] README-ru: mention NixOS and minor fixes

---
 README-ru.md | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/README-ru.md b/README-ru.md
index 36e1bc1c..c19d2874 100644
--- a/README-ru.md
+++ b/README-ru.md
@@ -58,9 +58,13 @@ zeronet-conservancy — это форк/продолжение проекта [Z
 ####  [Документация разработчика ZeroNet »](https://zeronet.io/docs/site_development/getting_started/)
 #### [Скриншоты в ZeroNet документации »](https://zeronet.io/docs/using_zeronet/sample_sites/)
 
-## Как вступить
+## Как присоединиться
 
-### Install from source (recommended)
+### Установить из репозитория вашего дистрибутива
+
+- NixOS: https://search.nixos.org/packages?channel=22.05&show=zeronet-conservancy&type=packages&query=zeronet-conservancy
+
+### Установить из исходного кода (рекомендовано)
 
 #### System dependencies
 

From f10e09633cd5d1da00f65d6528518e05f6790ce4 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 8 Jun 2022 18:51:03 +0000
Subject: [PATCH 105/333] Better warning about ADMIN permissions

refs #82
---
 src/Ui/UiWebsocket.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index cf386e8a..cfae3743 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -859,7 +859,7 @@ class UiWebsocket(object):
     @flag.admin
     def actionPermissionDetails(self, to, permission):
         if permission == "ADMIN":
-            self.response(to, _["Modify your client's configuration and access all site"] + " <span style='color: red'>" + _["(Dangerous!)"] + "</span>")
+            self.response(to, _["Allow this site to administrate your 0net node"] + " <span style='color: red'>" + _["(Make sure you trust site developer before accepting!)"] + "</span>")
         elif permission == "NOSANDBOX":
             self.response(to, _["Modify your client's configuration and access all site"] + " <span style='color: red'>" + _["(Dangerous!)"] + "</span>")
         elif permission == "PushNotification":

From dc1464d70206941b671545bee1bd56e2fc24d377 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 8 Jun 2022 19:04:23 +0000
Subject: [PATCH 106/333] change start page to a new dashboard/admin

---
 src/Config.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Config.py b/src/Config.py
index 5fd9c091..3a91aa62 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -274,7 +274,7 @@ class Config(object):
 
         self.parser.add_argument('--open_browser', help='Open homepage in web browser automatically',
                                  nargs='?', const="default_browser", metavar='browser_name')
-        self.parser.add_argument('--homepage', help='Web interface Homepage', default='126NXcevn1AUehWFZLTBw7FrX1crEizQdr',
+        self.parser.add_argument('--homepage', help='Web interface Homepage', default='191CazMVNaAcT9Y1zhkxd9ixMBPs59g2um',
                                  metavar='address')
         # self.parser.add_argument('--updatesite', help='Source code update site', default='1uPDaT3uSyWAPdCv1WkMb5hBQjWSNNACf',
                                  # metavar='address')

From 51951a81c8bf41f8056f7405d16c7330cdfd670b Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 8 Jun 2022 19:19:19 +0000
Subject: [PATCH 107/333] more new trackers from Syncronite

---
 src/Config.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 69 insertions(+)

diff --git a/src/Config.py b/src/Config.py
index 3a91aa62..d2aecc6e 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -82,6 +82,75 @@ class Config(object):
     def createArguments(self):
         trackers = [
             # more trackers from Syncronite (TODO: check if any of the old ones still work)
+            'zero://145.239.95.38:15441',
+            'zero://23.184.48.134:15441',
+            'zero://95.110.227.231:15441',
+            'zero://styromaniac.com:15441',
+            'zero://2kcb2fqesyaevc4lntogupa4mkdssth2ypfwczd2ov5a3zo6ytwwbayd.onion:15441',
+            'zero://zy7wttvjtsijt5uwmlar4yguvjc2gppzbdj4v6bujng6xwjmkdg7uvqd.onion:15441',
+            'zero://gugt43coc5tkyrhrc3esf6t6aeycvcqzw7qafxrjpqbwt4ssz5czgzyd.onion:15441',
+            'zero://57hzgtu62yzxqgbvgxs7g3lfck3za4zrda7qkskar3tlak5recxcebyd.onion:15445',
+            'zero://hb6ozikfiaafeuqvgseiik4r46szbpjfu66l67wjinnyv6dtopuwhtqd.onion:15445',
+            'zero://ow7in4ftwsix5klcbdfqvfqjvimqshbm2o75rhtpdnsderrcbx74wbad.onion:15441',
+            'zero://skdeywpgm5xncpxbbr4cuiip6ey4dkambpanog6nruvmef4f3e7o47qd.onion:15441',
+            'zero://rlcjomszyitxpwv7kzopmqgzk3bdpsxeull4c3s6goszkk6h2sotfoad.onion:15441',
+            'udp://tracker.opentrackr.org:1337/announce',
+            'udp://tracker.moeking.me:6969/announce',
+            'udp://tracker.altrosky.nl:6969/announce',
+            'udp://torrentclub.space:6969/announce',
+            'http://tracker.logirl.moe:17052/announce',
+            'udp://p4p.arenabg.com:1337/announce',
+            'http://tracker.files.fm:6969/announce',
+            'udp://opentracker.i2p.rocks:6969/announce',
+            'http://open.acgnxtracker.com:80/announce',
+            'udp://fe.dealclub.de:6969/announce',
+            'udp://tracker.publictracker.xyz:6969/announce',
+            'udp://tracker.0x.tf:6969/announce',
+            'udp://tracker.openbittorrent.com:6969/announce',
+            'udp://vibe.sleepyinternetfun.xyz:1738/announce',
+            'udp://tracker.jordan.im:6969/announce',
+            'udp://tracker.pomf.se:80/announce',
+            'http://tracker.mywaifu.best:6969/announce',
+            'udp://open.xxtor.com:3074/announce',
+            'udp://open.stealth.si:80/announce',
+            'http://t.nyaatracker.com:80/announce',
+            'http://tracker.ipv6tracker.ru:80/announce',
+            'http://tracker.noobsubs.net:80/announce',
+            'udp://movies.zsw.ca:6969/announce',
+            'http://bt.okmp3.ru:2710/announce',
+            'udp://zecircle.xyz:6969/announce',
+            'udp://exodus.desync.com:6969/announce',
+            'http://t.acg.rip:6699/announce',
+            'http://fxtt.ru:80/announce',
+            'udp://run.publictracker.xyz:6969/announce',
+            'udp://open.free-tracker.ga:6969/announce',
+            'udp://v2.iperson.xyz:6969/announce',
+            'udp://960303.xyz:6969/announce',
+            'http://t.overflow.biz:6969/announce',
+            'udp://tracker.dump.cl:6969/announce',
+            'udp://oldboystudy.com:6969/announce',
+            'udp://tracker1.bt.moack.co.kr:80/announce',
+            'udp://tracker.torrent.eu.org:451/announce',
+            'udp://mirror.aptus.co.tz:6969/announce',
+            'http://tracker3.ctix.cn:2095/announce',
+            'http://tracker2.ctix.cn:2095/announce',
+            'udp://tracker1.myporn.club:9337/announce',
+            'udp://bt1.archive.org:6969/announce',
+            'udp://ipv4.tracker.harry.lu:80/announce',
+            'udp://mts.tvbit.co:6969/announce',
+            'http://tracker3.520.jp:2095/announce',
+            'udp://public.publictracker.xyz:6969/announce',
+            'https://tracker.nodetopia.xyz:443/announce',
+            'udp://bubu.mapfactor.com:6969/announce',
+            'http://tracker2.520.jp:2095/announce',
+            'https://track.plop.pm:8989/announce',
+            'udp://bt2.archive.org:6969/announce',
+            'http://uraniumhexafluori.de:1919/announce',
+            'http://tracker.skyts.net:6969/announce',
+            'http://vps02.net.orel.ru:80/announce',
+            'udp://www.torrent.eu.org:451/announce',
+            'udp://bclearning.top:6969/announce',
+
             'zero://kgsvasoakvj4gnjiy7zemu34l3hq46dn5eauqkn76jpowmilci5t2vqd.onion:15445',
             'zero://hb6ozikfiaafeuqvgseiik4r46szbpjfu66l67wjinnyv6dtopuwhtqd.onion:15445',
             'zero://75pmmcbp4vvo2zndmjnrkandvbg6jyptygvvpwsf2zguj7urq7t4jzyd.onion:7777',

From 828c297788a932d3c66f990154be85dfd7837304 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 8 Jun 2022 19:50:22 +0000
Subject: [PATCH 108/333] fix sidebar donations

---
 plugins/Sidebar/SidebarPlugin.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index 5c771543..27f1e636 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -452,14 +452,18 @@ class UiWebsocketPlugin(object):
         if donate_btc:
             body.append(_("""
             <div class='flex'>
-              {donate_btc}
+              <span style="font-size:90%">{donate_btc}</span><br/>
+            </div>
+            <div class='flex'>
               <a href='bitcoin:{donate_btc}' class='button'>{_[Donate BTC]}</a>
             </div>
             """))
         if donate_xmr:
             body.append(_("""
             <div class='flex'>
-              {donate_key}
+              <span style="font-size:90%">{donate_key}</span><br/>
+            </div>
+            <div class='flex'>
               <a href='monero:{donate_xmr}' class='button'>{_[Donate Monero]}</a>
             </div>
             """))

From 7a9a91d08e76ae734bc0bae70e90ecadb12040cb Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 8 Jun 2022 19:50:39 +0000
Subject: [PATCH 109/333] CHANGELOG sidebar donations

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d18d0011..d1465392 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ### zeronet-conservancy 0.7.5+
 maintainers: @caryoscelus, @d4708, @FraYoshi, @prtngn
+- introduce multiple donations methods (@caryoscelus)
 - easier termux startup script (@unmanbearpig)
 - Brazilian Portuguese readme translation (@iFallenHunt)
 - reduce fingerprinting information (@caryoscelus)

From a614ab0787dc82adb540210f2ac4edb6567d51f3 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 9 Jun 2022 11:21:04 +0000
Subject: [PATCH 110/333] v0.7.6

---
 CHANGELOG.md  | 2 +-
 src/Config.py | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d1465392..e9c8d4c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-### zeronet-conservancy 0.7.5+
+### zeronet-conservancy 0.7.6
 maintainers: @caryoscelus, @d4708, @FraYoshi, @prtngn
 - introduce multiple donations methods (@caryoscelus)
 - easier termux startup script (@unmanbearpig)
diff --git a/src/Config.py b/src/Config.py
index d2aecc6e..b36b625f 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -13,9 +13,9 @@ import time
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.5+"
+        self.version = "0.7.6"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5020
+        self.rev = 5030
         self.argv = argv
         self.action = None
         self.test_parser = None

From 449d736d64b09c5a83a8028e91150a997c4fc4a6 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 9 Jun 2022 13:22:24 +0000
Subject: [PATCH 111/333] up changelog

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9c8d4c6..0b74e968 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ### zeronet-conservancy 0.7.6
 maintainers: @caryoscelus, @d4708, @FraYoshi, @prtngn
+- more trackers from Syncronite by default
 - introduce multiple donations methods (@caryoscelus)
 - easier termux startup script (@unmanbearpig)
 - Brazilian Portuguese readme translation (@iFallenHunt)

From 7bfd44ebb183d31c54f7020d1ebc93121b3395eb Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 9 Jun 2022 22:05:10 +0000
Subject: [PATCH 112/333] more details on Nix package install

---
 README.md | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index b4c71302..30ae5ea5 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,22 @@ Following links relate to original ZeroNet:
 
 ### Install from your distribution repository
 
-- NixOS: https://search.nixos.org/packages?channel=22.05&show=zeronet-conservancy&type=packages&query=zeronet-conservancy
+- NixOS: https://search.nixos.org/packages?channel=22.05&show=zeronet-conservancy&type=packages&query=zeronet-conservancy (and see below)
+
+### Install from Nix package manager (Linux or MacOS)
+
+```
+# install & configure nix package manager
+nix-env -iA nixpkgs.zeronet-conservancy
+```
+
+or
+
+`nix-env -iA nixos.zeronet-conservancy`
+
+if you're on NixOS
+
+(thanks @fgaz for making & maintaining the package)
 
 ### Install from source (recommended)
 

From e2e7ea601efb34d9f6c10097ab2c49ef37d11711 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 10 Jun 2022 13:06:36 +0000
Subject: [PATCH 113/333] nix package status badge in README

---
 README.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/README.md b/README.md
index 30ae5ea5..f56cccf3 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
 # zeronet-conservancy
 
+[![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
+
 [по-русски](README-ru.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md)
 
 zeronet-conservancy is a fork/continuation of [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) project

From 3c0151393c7558d4c88f9e161fd5c433d50fbb4e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 27 Jun 2022 22:35:36 +0000
Subject: [PATCH 114/333] quick fix xmr donations

---
 plugins/Sidebar/SidebarPlugin.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index 27f1e636..06e9dafd 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -461,7 +461,7 @@ class UiWebsocketPlugin(object):
         if donate_xmr:
             body.append(_("""
             <div class='flex'>
-              <span style="font-size:90%">{donate_key}</span><br/>
+              <span style="font-size:90%">{donate_xmr}</span><br/>
             </div>
             <div class='flex'>
               <a href='monero:{donate_xmr}' class='button'>{_[Donate Monero]}</a>

From 66001ee8b7d6045989492b2232ec6f19194edf66 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 27 Jun 2022 22:38:28 +0000
Subject: [PATCH 115/333] nix-packaging status on translated readmes

---
 README-ptbr.md  | 2 ++
 README-ru.md    | 2 ++
 README-zh-cn.md | 2 ++
 3 files changed, 6 insertions(+)

diff --git a/README-ptbr.md b/README-ptbr.md
index 1421b9d0..38f01bc1 100644
--- a/README-ptbr.md
+++ b/README-ptbr.md
@@ -2,6 +2,8 @@
 
 [in English](README.md) | [по-русски](README-ru.md) | [简体中文](README-zh-cn.md)
 
+[![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
+
 zeronet-conservancy é um garfo/continuação do projeto [ZeroNet](https://github.com/HelloZeroNet/ZeroNet)
 (que foi abandonada por seu criador) que se dedica a sustentar a rede p2p existente e a desenvolver
 seus valores de descentralização e liberdade, enquanto muda gradualmente para uma rede mais bem projetada
diff --git a/README-ru.md b/README-ru.md
index c19d2874..13cd1971 100644
--- a/README-ru.md
+++ b/README-ru.md
@@ -2,6 +2,8 @@
 
 [in English](README.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md)
 
+[![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
+
 zeronet-conservancy — это форк/продолжение проекта [ZeroNet](https://github.com/HelloZeroNet/ZeroNet)
 (покинутого его создателем), предназначенный для поддержки существующей сети p2p и развития
 идей ценности децентрализации и свободы, постепенно развивающийся в более совершенную сеть
diff --git a/README-zh-cn.md b/README-zh-cn.md
index b9b86d66..2a4bb346 100644
--- a/README-zh-cn.md
+++ b/README-zh-cn.md
@@ -2,6 +2,8 @@
 
 **警告**:这个翻译已经过时了。请阅读英文版。对此翻译的任何贡献都将受到高度赞赏
 
+[![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
+
 [in English](README.md) | [em português](README-ptbr.md) | [по-русски](README-ru.md)
 
 ## 为什么?

From cb2347e7d62199bd41692cf08ad0419e6f8b9fb3 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 28 Jun 2022 23:21:47 +0000
Subject: [PATCH 116/333] return upnp punch using secure xml library

fixes #105
---
 requirements.txt            | 1 +
 src/Peer/PeerPortchecker.py | 5 ++---
 src/util/UpnpPunch.py       | 5 ++---
 3 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/requirements.txt b/requirements.txt
index 81669ff1..c2825ee4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,3 +12,4 @@ gevent-ws
 coincurve
 maxminddb
 rich
+defusedxml>=0.7
diff --git a/src/Peer/PeerPortchecker.py b/src/Peer/PeerPortchecker.py
index 1063cca5..f7ca68e3 100644
--- a/src/Peer/PeerPortchecker.py
+++ b/src/Peer/PeerPortchecker.py
@@ -28,8 +28,8 @@ class PeerPortchecker(object):
         return urllib.request.urlopen(req, timeout=20.0)
 
     def portOpen(self, port):
-        self.log.info("Not trying to open port using UpnpPunch until it's proven robust...")
-        return False
+        # self.log.info("Not trying to open port using UpnpPunch until it's proven robust...")
+        # return False
 
         try:
             UpnpPunch.ask_to_open_port(port, 'ZeroNet', retries=3, protos=["TCP"])
@@ -37,7 +37,6 @@ class PeerPortchecker(object):
         except Exception as err:
             self.log.warning("UpnpPunch run error: %s" % Debug.formatException(err))
             return False
-
         return True
 
     def portClose(self, port):
diff --git a/src/util/UpnpPunch.py b/src/util/UpnpPunch.py
index 18f4aaee..2fa85209 100644
--- a/src/util/UpnpPunch.py
+++ b/src/util/UpnpPunch.py
@@ -3,8 +3,7 @@ import urllib.request
 import http.client
 import logging
 from urllib.parse import urlparse
-from xml.dom.minidom import parseString
-from xml.parsers.expat import ExpatError
+from defusedxml.minidom import parseString
 
 from gevent import socket
 import gevent
@@ -105,7 +104,7 @@ def _parse_igd_profile(profile_xml):
     """
     try:
         dom = parseString(profile_xml)
-    except ExpatError as e:
+    except Exception as e:
         raise IGDError(
             'Unable to parse IGD reply: {0} \n\n\n {1}'.format(profile_xml, e))
 

From 4786633b9dd4f9ce612632ee7c16a5f650d11c98 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 28 Jun 2022 23:41:18 +0000
Subject: [PATCH 117/333] version bump

---
 CHANGELOG.md  | 4 ++++
 src/Config.py | 4 ++--
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b74e968..1c945512 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+### zeronet-conservancy 0.7.6+
+maintainers: @caryoscelus, @FraYoshi, @prtngn, @unmanbearpig
+- xmr donations in sidebar fixed (@caryoscelus)
+
 ### zeronet-conservancy 0.7.6
 maintainers: @caryoscelus, @d4708, @FraYoshi, @prtngn
 - more trackers from Syncronite by default
diff --git a/src/Config.py b/src/Config.py
index b36b625f..fcb304f2 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -13,9 +13,9 @@ import time
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.6"
+        self.version = "0.7.6+"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5030
+        self.rev = 5031
         self.argv = argv
         self.action = None
         self.test_parser = None

From 277b48d71e68fc20a152dde5ed9e94e951665934 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 29 Jun 2022 00:04:19 +0000
Subject: [PATCH 118/333] skip redundant check

---
 src/Ui/UiWebsocket.py | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index cfae3743..2bea8f2a 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -424,10 +424,6 @@ class UiWebsocket(object):
             extend["cert_sign"] = cert["cert_sign"]
             self.log.debug("Extending content.json with cert %s" % extend["cert_user_id"])
 
-        if not self.hasFilePermission(inner_path):
-            self.log.error("SiteSign error: you don't own this site & site owner doesn't allow you to do so.")
-            return self.response(to, {"error": "Forbidden, you can only modify your own sites"})
-
         if privatekey == "stored":  # Get privatekey from sites.json
             privatekey = self.user.getSiteData(self.site.address).get("privatekey")
             if not privatekey:

From 9ff7408cc6242226da246962a08b56294039140c Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 29 Jun 2022 00:20:33 +0000
Subject: [PATCH 119/333] improve code in ContentManager

---
 src/Content/ContentManager.py | 29 +++++++++--------------------
 1 file changed, 9 insertions(+), 20 deletions(-)

diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index 27da402b..77e9c943 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -401,10 +401,10 @@ class ContentManager(object):
     # Return: { "sha512": "c29d73d...21f518", "size": 41 , "content_inner_path": "content.json"}
     def getFileInfo(self, inner_path, new_file=False):
         dirs = inner_path.split("/")  # Parent dirs of content.json
-        inner_path_parts = [dirs.pop()]  # Filename relative to content.json
-        while True:
-            content_inner_path = "%s/content.json" % "/".join(dirs)
-            content_inner_path = content_inner_path.strip("/")
+        inner_path_parts = []  # Filename relative to content.json
+        while dirs:
+            inner_path_parts.insert(0, dirs.pop())
+            content_inner_path = f'{"/".join(dirs)}/content.json'.strip('/')
             content = self.contents.get(content_inner_path)
 
             # Check in files
@@ -447,12 +447,6 @@ class ContentManager(object):
                 back["optional"] = None
                 return back
 
-            # No inner path in this dir, lets try the parent dir
-            if dirs:
-                inner_path_parts.insert(0, dirs.pop())
-            else:  # No more parent dirs
-                break
-
         # Not found
         return False
 
@@ -472,20 +466,15 @@ class ContentManager(object):
 
         dirs = inner_path.split("/")  # Parent dirs of content.json
         inner_path_parts = [dirs.pop()]  # Filename relative to content.json
-        inner_path_parts.insert(0, dirs.pop())  # Dont check in self dir
-        while True:
-            content_inner_path = "%s/content.json" % "/".join(dirs)
-            parent_content = self.contents.get(content_inner_path.strip("/"))
+        # Dont check in self dir
+        while dirs:
+            inner_path_parts.insert(0, dirs.pop())
+            content_inner_path = f'{"/".join(dirs)}/content.json'.strip('/')
+            parent_content = self.contents.get(content_inner_path)
             if parent_content and "includes" in parent_content:
                 return parent_content["includes"].get("/".join(inner_path_parts))
             elif parent_content and "user_contents" in parent_content:
                 return self.getUserContentRules(parent_content, inner_path, content)
-            else:  # No inner path in this dir, lets try the parent dir
-                if dirs:
-                    inner_path_parts.insert(0, dirs.pop())
-                else:  # No more parent dirs
-                    break
-
         return False
 
     # Get rules for a user file

From 8d0db14fbbfca3b685ce05f40ef68c4b3c1246d4 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 29 Jun 2022 17:26:35 +0000
Subject: [PATCH 120/333] changelog

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c945512..819b10cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ### zeronet-conservancy 0.7.6+
 maintainers: @caryoscelus, @FraYoshi, @prtngn, @unmanbearpig
+- return UPnP using secure xml library (@caryoscelus)
 - xmr donations in sidebar fixed (@caryoscelus)
 
 ### zeronet-conservancy 0.7.6

From 981769b051292981992c5d7ee37e5d0ebfae18df Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 29 Jun 2022 17:53:41 +0000
Subject: [PATCH 121/333] improve error message

this commit is adopted from d0069471b8bcf0a61b6c5a9849621f255580065a
(https://github.com/zeronet-enhanced/ZeroNet/commit/d0069471b8bcf0a61b6c5a9849621f255580065a)
by @geekless

with original commit message:

Don't raise VerifyError with misleading message
"Invalid old-style sign" when the file has no sign at all.
---
 src/Content/ContentManager.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index 27da402b..20568701 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -998,8 +998,10 @@ class ContentManager(object):
                         raise VerifyError("Valid signs: %s/%s" % (valid_signs, signs_required))
                     else:
                         return self.verifyContent(inner_path, new_content)
-                else:  # Old style signing
+                elif sign:
                     raise VerifyError("Invalid old-style sign")
+                else:
+                    raise VerifyError("Not signed")
 
             except Exception as err:
                 self.log.warning("%s: verify sign error: %s" % (inner_path, Debug.formatException(err)))

From da0a627ac073048a6127d11889bf2743b6639089 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 29 Jun 2022 18:39:54 +0000
Subject: [PATCH 122/333] improve and speed up content.json loading

parse json (which is required anyway) instead of ad-hock
re-based parsing
---
 src/Content/ContentManager.py | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index 27da402b..0c0f27a7 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -78,19 +78,19 @@ class ContentManager(object):
 
         if os.path.isfile(content_path):
             try:
+                new_content = self.site.storage.loadJson(content_inner_path)
                 # Check if file is newer than what we have
                 if not force and old_content and not self.site.settings.get("own"):
-                    for line in open(content_path):
-                        if '"modified"' not in line:
-                            continue
-                        match = re.search(r"([0-9\.]+),$", line.strip(" \r\n"))
-                        if match and float(match.group(1)) <= old_content.get("modified", 0):
-                            self.log.debug("%s loadContent same json file, skipping" % content_inner_path)
-                            return [], []
-
-                new_content = self.site.storage.loadJson(content_inner_path)
+                    new_ts = int(float(new_content.get('modified', 0)))
+                    old_ts = int(float(old_content.get('modified', 0)))
+                    if new_ts < old_ts:
+                        self.log.debug('got older version of {content_inner_path} ({new_ts} < {old_ts}), ignoring')
+                        return [], []
+                    elif new_ts == old_ts:
+                        self.log.debug('got same timestamp version of {content_inner_path} ({new_ts}), ignoring')
+                        return [], []
             except Exception as err:
-                self.log.warning("%s load error: %s" % (content_path, Debug.formatException(err)))
+                self.log.warning(f'{content_path} load error: {Debug.formatException(err)}')
                 return [], []
         else:
             self.log.debug("Content.json not exist: %s" % content_path)

From aa19bcf2e0d4a14c63532b8bd1393429ff0a2ec3 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 29 Jun 2022 18:50:55 +0000
Subject: [PATCH 123/333] remove potential fingerprinting of site owner

inspired by @geekless 's 829fd4678133e527d34ac395c6c5bf3da20f8050 commit at
https://github.com/zeronet-enhanced/ZeroNet/commit/829fd4678133e527d34ac395c6c5bf3da20f8050

previously 0net would announce owned sites more frequently
which can help determine site owner
---
 src/File/FileServer.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/File/FileServer.py b/src/File/FileServer.py
index e5fd4edf..7c803041 100644
--- a/src/File/FileServer.py
+++ b/src/File/FileServer.py
@@ -313,8 +313,8 @@ class FileServer(ConnectionServer):
     def announceSite(self, site):
         site.announce(mode="update", pex=False)
         active_site = time.time() - site.settings.get("modified", 0) < 24 * 60 * 60
-        if site.settings["own"] or active_site:
-            # Check connections more frequently on own and active sites to speed-up first connections
+        if active_site:
+            # Check connections more frequently on active sites to speed-up first connections
             site.needConnections(check_site_on_reconnect=True)
         site.sendMyHashfield(3)
         site.updateHashfield(3)

From 7353c8ff909afa947919a2c0f474809eb13a493f Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 1 Jul 2022 19:28:53 +0000
Subject: [PATCH 124/333] reduce fingerprinting of site owner

replace version with generic `user_agent` that will remain constant
---
 src/Config.py                 | 3 ++-
 src/Connection/Connection.py  | 2 +-
 src/Content/ContentManager.py | 3 ++-
 3 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index fcb304f2..d7a25d7e 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -14,8 +14,9 @@ class Config(object):
 
     def __init__(self, argv):
         self.version = "0.7.6+"
+        self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5031
+        self.rev = 5032
         self.argv = argv
         self.action = None
         self.test_parser = None
diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py
index 4adb3227..de95d867 100644
--- a/src/Connection/Connection.py
+++ b/src/Connection/Connection.py
@@ -362,7 +362,7 @@ class Connection(object):
                 self.server.log.warning("Unknown target onion address: %s" % self.target_onion)
 
         handshake = {
-            "version": "conservancy",
+            "version": config.user_agent,
             "protocol": "v2",
             "use_bin_type": True,
             "peer_id": peer_id,
diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index 0c0f27a7..42f222b0 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -729,7 +729,8 @@ class ContentManager(object):
 
         new_content["modified"] = int(time.time())  # Add timestamp
         if inner_path == "content.json":
-            new_content["zeronet_version"] = config.version
+            # add for backward compatibility, but don't expose user version
+            new_content["zeronet_version"] = config.user_agent
             new_content["signs_required"] = content.get("signs_required", 1)
 
         new_content["address"] = self.site.address

From ab9e8322fa7e896a196749d2986ad7ef4c40cabc Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 1 Jul 2022 23:16:42 +0000
Subject: [PATCH 125/333] fix comment

---
 src/File/FileServer.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/File/FileServer.py b/src/File/FileServer.py
index e5fd4edf..ce6139b4 100644
--- a/src/File/FileServer.py
+++ b/src/File/FileServer.py
@@ -306,7 +306,7 @@ class FileServer(ConnectionServer):
                 time.sleep(1)  # Prevent too quick request
 
             site = None
-            gc.collect()  # Implicit garbage collection
+            gc.collect()  # Explicit garbage collection
             startup = False
             time.sleep(60 * 20)
 

From 5bcec3c0badef18bba03a608ddff9d1846587b06 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 7 Jul 2022 18:05:02 +0000
Subject: [PATCH 126/333] Update README.md

readme monero wallet
---
 README.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index f56cccf3..f24858e3 100644
--- a/README.md
+++ b/README.md
@@ -176,7 +176,9 @@ will create team accounts on friendly crowdfunding platforms as well.
 
 If you want to make sure your donation is recognized as donation for this
 project, there is a dedicated bitcoin address for that, too:
-1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6
+1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6. And if you want to stay more anonymous and
+private, a Monero wallet:
+4AiYUcqVRH4C2CVr9zbBdkhRnJnHiJoypHEsq4N7mQziGUoosPCpPeg8SPr87nvwypaRzDgMHEbWWDekKtq8hm9LBmgcMzC
 
 If you want to donate in a different way, feel free to contact maintainer or
 create an issue

From 74f8a6e11ec0ca89144b87a771f5c4e9cdb5fb6e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 7 Jul 2022 21:31:24 +0000
Subject: [PATCH 127/333] minor improvement in ConnectionServer

---
 src/Connection/ConnectionServer.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py
index 8d377aca..96dce243 100644
--- a/src/Connection/ConnectionServer.py
+++ b/src/Connection/ConnectionServer.py
@@ -164,9 +164,9 @@ class ConnectionServer(object):
 
     def getConnection(self, ip=None, port=None, peer_id=None, create=True, site=None, is_tracker_connection=False):
         ip_type = helper.getIpType(ip)
-        has_per_site_onion = (ip.endswith(".onion") or self.port_opened.get(ip_type, None) == False) and self.tor_manager.start_onions and site
+        has_per_site_onion = (ip_type == 'onion' or self.port_opened.get(ip_type, None) == False) and self.tor_manager.start_onions and site
         if has_per_site_onion:  # Site-unique connection for Tor
-            if ip.endswith(".onion"):
+            if ip_type == 'onion':
                 site_onion = self.tor_manager.getOnion(site.address)
             else:
                 site_onion = self.tor_manager.getOnion("global")

From a78014adbcc92c0ca3ea5f1501864355e269106c Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 7 Jul 2022 22:56:53 +0000
Subject: [PATCH 128/333] add pyaes requirement

this is usually unused, but works as fallback
---
 requirements.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/requirements.txt b/requirements.txt
index c2825ee4..642b6eb5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,3 +13,4 @@ coincurve
 maxminddb
 rich
 defusedxml>=0.7
+pyaes

From 1f305615a45c3c930577a287a4711b65a2bc1e7b Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 10 Jul 2022 11:13:56 +0000
Subject: [PATCH 129/333] fix error message due to disabling updater sites in
 sidebar

---
 plugins/Sidebar/SidebarPlugin.py | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index 06e9dafd..876ea37a 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -758,9 +758,6 @@ class UiWebsocketPlugin(object):
     @flag.admin
     @flag.no_multiuser
     def actionSiteSetOwned(self, to, owned):
-        if self.site.address == config.updatesite:
-            return {"error": "You can't change the ownership of the updater site"}
-
         self.site.settings["own"] = bool(owned)
         self.site.updateWebsocket(owned=owned)
         return "ok"

From 0917ca43ba5583daf2b87e8e64c6efcac6aa5e7f Mon Sep 17 00:00:00 2001
From: d47081 <108541346+d47081@users.noreply.github.com>
Date: Sun, 17 Jul 2022 16:55:04 +0300
Subject: [PATCH 130/333] add libffi-dev dependency

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index f24858e3..090fcc76 100644
--- a/README.md
+++ b/README.md
@@ -93,7 +93,7 @@ Install autoconf and other basic development tools, python3 and pip.
 
 ##### Apt-based (debian, ubuntu, etc)
  - `sudo apt update`
- - `sudo apt install pkg-config python3-pip python3-venv`
+ - `sudo apt install pkg-config libffi-dev python3-pip python3-venv`
 
 ##### Android/Termux
  - install [Termux](https://termux.com/) (in Termux you can install packages via `pkg install <package-names>`)

From 1e6bb43ac04e1272e87211fecc2e06a24dea31f4 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 18 Jul 2022 23:25:58 +0000
Subject: [PATCH 131/333] remove pyaes from src/lib

it's now a standard dependency
---
 src/lib/pyaes/LICENSE.txt    |  22 --
 src/lib/pyaes/README.md      | 363 ---------------------
 src/lib/pyaes/__init__.py    |  53 ----
 src/lib/pyaes/aes.py         | 589 -----------------------------------
 src/lib/pyaes/blockfeeder.py | 227 --------------
 src/lib/pyaes/util.py        |  60 ----
 6 files changed, 1314 deletions(-)
 delete mode 100644 src/lib/pyaes/LICENSE.txt
 delete mode 100644 src/lib/pyaes/README.md
 delete mode 100644 src/lib/pyaes/__init__.py
 delete mode 100644 src/lib/pyaes/aes.py
 delete mode 100644 src/lib/pyaes/blockfeeder.py
 delete mode 100644 src/lib/pyaes/util.py

diff --git a/src/lib/pyaes/LICENSE.txt b/src/lib/pyaes/LICENSE.txt
deleted file mode 100644
index 0417a6c2..00000000
--- a/src/lib/pyaes/LICENSE.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2014 Richard Moore
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
diff --git a/src/lib/pyaes/README.md b/src/lib/pyaes/README.md
deleted file mode 100644
index 26e3b2ba..00000000
--- a/src/lib/pyaes/README.md
+++ /dev/null
@@ -1,363 +0,0 @@
-pyaes
-=====
-
-A pure-Python implementation of the AES block cipher algorithm and the common modes of operation (CBC, CFB, CTR, ECB and OFB).
-
-
-Features
---------
-
-* Supports all AES key sizes
-* Supports all AES common modes
-* Pure-Python (no external dependencies)
-* BlockFeeder API allows streams to easily be encrypted and decrypted
-* Python 2.x and 3.x support (make sure you pass in bytes(), not strings for Python 3)
-
-
-API
----
-
-All keys may be 128 bits (16 bytes), 192 bits (24 bytes) or 256 bits (32 bytes) long.
-
-To generate a random key use:
-```python
-import os
-
-# 128 bit, 192 bit and 256 bit keys
-key_128 = os.urandom(16)
-key_192 = os.urandom(24)
-key_256 = os.urandom(32)
-```
-
-To generate keys from simple-to-remember passwords, consider using a _password-based key-derivation function_ such as [scrypt](https://github.com/ricmoo/pyscrypt).
-
-
-### Common Modes of Operation
-
-There are many modes of operations, each with various pros and cons. In general though, the **CBC** and **CTR** modes are recommended. The **ECB is NOT recommended.**, and is included primarily for completeness.
-
-Each of the following examples assumes the following key:
-```python
-import pyaes
-
-# A 256 bit (32 byte) key
-key = "This_key_for_demo_purposes_only!"
-
-# For some modes of operation we need a random initialization vector
-# of 16 bytes
-iv = "InitializationVe"
-```
-
-
-#### Counter Mode of Operation (recommended)
-
-```python
-aes = pyaes.AESModeOfOperationCTR(key)
-plaintext = "Text may be any length you wish, no padding is required"
-ciphertext = aes.encrypt(plaintext)
-
-# '''\xb6\x99\x10=\xa4\x96\x88\xd1\x89\x1co\xe6\x1d\xef;\x11\x03\xe3\xee
-#    \xa9V?wY\xbfe\xcdO\xe3\xdf\x9dV\x19\xe5\x8dk\x9fh\xb87>\xdb\xa3\xd6
-#    \x86\xf4\xbd\xb0\x97\xf1\t\x02\xe9 \xed'''
-print repr(ciphertext)
-
-# The counter mode of operation maintains state, so decryption requires
-# a new instance be created
-aes = pyaes.AESModeOfOperationCTR(key)
-decrypted = aes.decrypt(ciphertext)
-
-# True
-print decrypted == plaintext
-
-# To use a custom initial value
-counter = pyaes.Counter(initial_value = 100)
-aes = pyaes.AESModeOfOperationCTR(key, counter = counter)
-ciphertext = aes.encrypt(plaintext)
-
-# '''WZ\x844\x02\xbfoY\x1f\x12\xa6\xce\x03\x82Ei)\xf6\x97mX\x86\xe3\x9d
-#    _1\xdd\xbd\x87\xb5\xccEM_4\x01$\xa6\x81\x0b\xd5\x04\xd7Al\x07\xe5
-#    \xb2\x0e\\\x0f\x00\x13,\x07'''
-print repr(ciphertext)
-```
-
-
-#### Cipher-Block Chaining (recommended)
-
-```python
-aes = pyaes.AESModeOfOperationCBC(key, iv = iv)
-plaintext = "TextMustBe16Byte"
-ciphertext = aes.encrypt(plaintext)
-
-# '\xd6:\x18\xe6\xb1\xb3\xc3\xdc\x87\xdf\xa7|\x08{k\xb6'
-print repr(ciphertext)
-
-
-# The cipher-block chaining mode of operation maintains state, so
-# decryption requires a new instance be created
-aes = pyaes.AESModeOfOperationCBC(key, iv = iv)
-decrypted = aes.decrypt(ciphertext)
-
-# True
-print decrypted == plaintext
-```
-
-
-#### Cipher Feedback
-
-```python
-# Each block into the mode of operation must be a multiple of the segment
-# size. For this example we choose 8 bytes.
-aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8)
-plaintext =  "TextMustBeAMultipleOfSegmentSize"
-ciphertext = aes.encrypt(plaintext)
-
-# '''v\xa9\xc1w"\x8aL\x93\xcb\xdf\xa0/\xf8Y\x0b\x8d\x88i\xcb\x85rmp
-#    \x85\xfe\xafM\x0c)\xd5\xeb\xaf'''
-print repr(ciphertext)
-
-
-# The cipher-block chaining mode of operation maintains state, so
-# decryption requires a new instance be created
-aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8)
-decrypted = aes.decrypt(ciphertext)
-
-# True
-print decrypted == plaintext
-```
-
-
-#### Output Feedback Mode of Operation
-
-```python
-aes = pyaes.AESModeOfOperationOFB(key, iv = iv)
-plaintext = "Text may be any length you wish, no padding is required"
-ciphertext = aes.encrypt(plaintext)
-
-# '''v\xa9\xc1wO\x92^\x9e\rR\x1e\xf7\xb1\xa2\x9d"l1\xc7\xe7\x9d\x87(\xc26s
-#    \xdd8\xc8@\xb6\xd9!\xf5\x0cM\xaa\x9b\xc4\xedLD\xe4\xb9\xd8\xdf\x9e\xac
-#    \xa1\xb8\xea\x0f\x8ev\xb5'''
-print repr(ciphertext)
-
-# The counter mode of operation maintains state, so decryption requires
-# a new instance be created
-aes = pyaes.AESModeOfOperationOFB(key, iv = iv)
-decrypted = aes.decrypt(ciphertext)
-
-# True
-print decrypted == plaintext
-```
-
-
-#### Electronic Codebook (NOT recommended)
-
-```python
-aes = pyaes.AESModeOfOperationECB(key)
-plaintext = "TextMustBe16Byte"
-ciphertext = aes.encrypt(plaintext)
-
-# 'L6\x95\x85\xe4\xd9\xf1\x8a\xfb\xe5\x94X\x80|\x19\xc3'
-print repr(ciphertext)
-
-# Since there is no state stored in this mode of operation, it
-# is not necessary to create a new aes object for decryption.
-#aes = pyaes.AESModeOfOperationECB(key)
-decrypted = aes.decrypt(ciphertext)
-
-# True
-print decrypted == plaintext
-```
-
-
-### BlockFeeder
-
-Since most of the modes of operations require data in specific block-sized or segment-sized blocks, it can be difficult when working with large arbitrary streams or strings of data.
-
-The BlockFeeder class is meant to make life easier for you, by buffering bytes across multiple calls and returning bytes as they are available, as well as padding or stripping the output when finished, if necessary.
-
-```python
-import pyaes
-
-# Any mode of operation can be used; for this example CBC
-key = "This_key_for_demo_purposes_only!"
-iv = "InitializationVe"
-
-ciphertext = ''
-
-# We can encrypt one line at a time, regardles of length
-encrypter = pyaes.Encrypter(pyaes.AESModeOfOperationCBC(key, iv))
-for line in file('/etc/passwd'):
-    ciphertext += encrypter.feed(line)
-
-# Make a final call to flush any remaining bytes and add paddin
-ciphertext += encrypter.feed()
-
-# We can decrypt the cipher text in chunks (here we split it in half)
-decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv))
-decrypted = decrypter.feed(ciphertext[:len(ciphertext) / 2])
-decrypted += decrypter.feed(ciphertext[len(ciphertext) / 2:])
-
-# Again, make a final call to flush any remaining bytes and strip padding
-decrypted += decrypter.feed()
-
-print file('/etc/passwd').read() == decrypted
-```
-
-### Stream Feeder
-
-This is meant to make it even easier to encrypt and decrypt streams and large files.
-
-```python
-import pyaes
-
-# Any mode of operation can be used; for this example CTR
-key = "This_key_for_demo_purposes_only!"
-
-# Create the mode of operation to encrypt with
-mode = pyaes.AESModeOfOperationCTR(key)
-
-# The input and output files
-file_in = file('/etc/passwd')
-file_out = file('/tmp/encrypted.bin', 'wb')
-
-# Encrypt the data as a stream, the file is read in 8kb chunks, be default
-pyaes.encrypt_stream(mode, file_in, file_out)
-
-# Close the files
-file_in.close()
-file_out.close()
-```
-
-Decrypting is identical, except you would use `pyaes.decrypt_stream`, and the encrypted file would be the `file_in` and target for decryption the `file_out`.
-
-### AES block cipher
-
-Generally you should use one of the modes of operation above. This may however be useful for experimenting with a custom mode of operation or dealing with encrypted blocks.
-
-The block cipher requires exactly one block of data to encrypt or decrypt, and each block should be an array with each element an integer representation of a byte.
-
-```python
-import pyaes
-
-# 16 byte block of plain text
-plaintext = "Hello World!!!!!"
-plaintext_bytes = [ ord(c) for c in plaintext ]
-
-# 32 byte key (256 bit)
-key = "This_key_for_demo_purposes_only!"
-
-# Our AES instance
-aes = pyaes.AES(key)
-
-# Encrypt!
-ciphertext = aes.encrypt(plaintext_bytes)
-
-# [55, 250, 182, 25, 185, 208, 186, 95, 206, 115, 50, 115, 108, 58, 174, 115]
-print repr(ciphertext)
-
-# Decrypt!
-decrypted = aes.decrypt(ciphertext)
-
-# True
-print decrypted == plaintext_bytes
-```
-
-What is a key?
---------------
-
-This seems to be a point of confusion for many people new to using encryption. You can think of the key as the *"password"*. However, these algorithms require the *"password"* to be a specific length.
-
-With AES, there are three possible key lengths, 16-bytes, 24-bytes or 32-bytes. When you create an AES object, the key size is automatically detected, so it is important to pass in a key of the correct length.
-
-Often, you wish to provide a password of arbitrary length, for example, something easy to remember or write down. In these cases, you must come up with a way to transform the password into a key, of a specific length. A **Password-Based Key Derivation Function** (PBKDF) is an algorithm designed for this exact purpose.
-
-Here is an example, using the popular (possibly obsolete?) *crypt* PBKDF:
-
-```
-# See: https://www.dlitz.net/software/python-pbkdf2/
-import pbkdf2
-
-password = "HelloWorld"
-
-# The crypt PBKDF returns a 48-byte string
-key = pbkdf2.crypt(password)
-
-# A 16-byte, 24-byte and 32-byte key, respectively
-key_16 = key[:16]
-key_24 = key[:24]
-key_32 = key[:32]
-```
-
-The [scrypt](https://github.com/ricmoo/pyscrypt) PBKDF is intentionally slow, to make it more difficult to brute-force guess a password:
-
-```
-# See: https://github.com/ricmoo/pyscrypt
-import pyscrypt
-
-password = "HelloWorld"
-
-# Salt is required, and prevents Rainbow Table attacks
-salt = "SeaSalt"
-
-# N, r, and p are parameters to specify how difficult it should be to
-# generate a key; bigger numbers take longer and more memory
-N = 1024
-r = 1
-p = 1
-
-# A 16-byte, 24-byte and 32-byte key, respectively; the scrypt algorithm takes
-# a 6-th parameter, indicating key length
-key_16 = pyscrypt.hash(password, salt, N, r, p, 16)
-key_24 = pyscrypt.hash(password, salt, N, r, p, 24)
-key_32 = pyscrypt.hash(password, salt, N, r, p, 32)
-```
-
-Another possibility, is to use a hashing function, such as SHA256 to hash the password, but this method may be vulnerable to [Rainbow Attacks](http://en.wikipedia.org/wiki/Rainbow_table), unless you use a [salt](http://en.wikipedia.org/wiki/Salt_(cryptography)).
-
-```python
-import hashlib
-
-password = "HelloWorld"
-
-# The SHA256 hash algorithm returns a 32-byte string
-hashed = hashlib.sha256(password).digest()
-
-# A 16-byte, 24-byte and 32-byte key, respectively
-key_16 = hashed[:16]
-key_24 = hashed[:24]
-key_32 = hashed
-```
-
-
-
-
-Performance
------------
-
-There is a test case provided in _/tests/test-aes.py_ which does some basic performance testing (its primary purpose is moreso as a regression test).
-
-Based on that test, in **CPython**, this library is about 30x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 80x slower for CFB; and 300x slower for CTR.
-
-Based on that same test, in **Pypy**, this library is about 4x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 12x slower for CFB; and 19x slower for CTR.
-
-The PyCrypto documentation makes reference to the counter call being responsible for the speed problems of the counter (CTR) mode of operation, which is why they use a specially optimized counter. I will investigate this problem further in the future.
-
-
-FAQ
----
-
-#### Why do this?
-
-The short answer, *why not?*
-
-The longer answer, is for my [pyscrypt](https://github.com/ricmoo/pyscrypt) library. I required a pure-Python AES implementation that supported 256-bit keys with the counter (CTR) mode of operation. After searching, I found several implementations, but all were missing CTR or only supported 128 bit keys. After all the work of learning AES inside and out to implement the library, it was only a marginal amount of extra work to library-ify a more general solution. So, *why not?*
-
-#### How do I get a question I have added?
-
-E-mail me at pyaes@ricmoo.com with any questions, suggestions, comments, et cetera.
-
-
-#### Can I give you my money?
-
-Umm... Ok? :-)
-
-_Bitcoin_  - `18UDs4qV1shu2CgTS2tKojhCtM69kpnWg9`
diff --git a/src/lib/pyaes/__init__.py b/src/lib/pyaes/__init__.py
deleted file mode 100644
index 5712f794..00000000
--- a/src/lib/pyaes/__init__.py
+++ /dev/null
@@ -1,53 +0,0 @@
-# The MIT License (MIT)
-#
-# Copyright (c) 2014 Richard Moore
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-# This is a pure-Python implementation of the AES algorithm and AES common
-# modes of operation.
-
-# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
-# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation
-
-
-# Supported key sizes:
-#   128-bit
-#   192-bit
-#   256-bit
-
-
-# Supported modes of operation:
-#   ECB - Electronic Codebook
-#   CBC - Cipher-Block Chaining
-#   CFB - Cipher Feedback
-#   OFB - Output Feedback
-#   CTR - Counter
-
-# See the README.md for API details and general information.
-
-# Also useful, PyCrypto, a crypto library implemented in C with Python bindings:
-# https://www.dlitz.net/software/pycrypto/
-
-
-VERSION = [1, 3, 0]
-
-from .aes import AES, AESModeOfOperationCTR, AESModeOfOperationCBC, AESModeOfOperationCFB, AESModeOfOperationECB, AESModeOfOperationOFB, AESModesOfOperation, Counter
-from .blockfeeder import decrypt_stream, Decrypter, encrypt_stream, Encrypter
-from .blockfeeder import PADDING_NONE, PADDING_DEFAULT
diff --git a/src/lib/pyaes/aes.py b/src/lib/pyaes/aes.py
deleted file mode 100644
index c6e8bc02..00000000
--- a/src/lib/pyaes/aes.py
+++ /dev/null
@@ -1,589 +0,0 @@
-# The MIT License (MIT)
-#
-# Copyright (c) 2014 Richard Moore
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-# This is a pure-Python implementation of the AES algorithm and AES common
-# modes of operation.
-
-# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
-
-# Honestly, the best description of the modes of operations are the wonderful
-# diagrams on Wikipedia. They explain in moments what my words could never
-# achieve. Hence the inline documentation here is sparer than I'd prefer.
-# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation
-
-# Also useful, PyCrypto, a crypto library implemented in C with Python bindings:
-# https://www.dlitz.net/software/pycrypto/
-
-
-# Supported key sizes:
-#   128-bit
-#   192-bit
-#   256-bit
-
-
-# Supported modes of operation:
-#   ECB - Electronic Codebook
-#   CBC - Cipher-Block Chaining
-#   CFB - Cipher Feedback
-#   OFB - Output Feedback
-#   CTR - Counter
-
-
-# See the README.md for API details and general information.
-
-
-import copy
-import struct
-
-__all__ = ["AES", "AESModeOfOperationCTR", "AESModeOfOperationCBC", "AESModeOfOperationCFB",
-           "AESModeOfOperationECB", "AESModeOfOperationOFB", "AESModesOfOperation", "Counter"]
-
-
-def _compact_word(word):
-    return (word[0] << 24) | (word[1] << 16) | (word[2] << 8) | word[3]
-
-def _string_to_bytes(text):
-    return list(ord(c) for c in text)
-
-def _bytes_to_string(binary):
-    return "".join(chr(b) for b in binary)
-
-def _concat_list(a, b):
-    return a + b
-
-
-# Python 3 compatibility
-try:
-    xrange
-except Exception:
-    xrange = range
-
-    # Python 3 supports bytes, which is already an array of integers
-    def _string_to_bytes(text):
-        if isinstance(text, bytes):
-            return text
-        return [ord(c) for c in text]
-
-    # In Python 3, we return bytes
-    def _bytes_to_string(binary):
-        return bytes(binary)
-
-    # Python 3 cannot concatenate a list onto a bytes, so we bytes-ify it first
-    def _concat_list(a, b):
-        return a + bytes(b)
-
-
-# Based *largely* on the Rijndael implementation
-# See: http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf
-class AES(object):
-    '''Encapsulates the AES block cipher.
-
-    You generally should not need this. Use the AESModeOfOperation classes
-    below instead.'''
-
-    # Number of rounds by keysize
-    number_of_rounds = {16: 10, 24: 12, 32: 14}
-
-    # Round constant words
-    rcon = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ]
-
-    # S-box and Inverse S-box (S is for Substitution)
-    S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ]
-    Si =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ] 
-
-    # Transformations for encryption
-    T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ]
-    T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ]
-    T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ]
-    T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ]
-
-    # Transformations for decryption
-    T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ]
-    T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ]
-    T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ]
-    T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ]
-
-    # Transformations for decryption key expansion
-    U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ]
-    U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ]
-    U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ]
-    U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ]
-
-    def __init__(self, key):
-
-        if len(key) not in (16, 24, 32):
-            raise ValueError('Invalid key size')
-
-        rounds = self.number_of_rounds[len(key)]
-
-        # Encryption round keys
-        self._Ke = [[0] * 4 for i in xrange(rounds + 1)]
-
-        # Decryption round keys
-        self._Kd = [[0] * 4 for i in xrange(rounds + 1)]
-
-        round_key_count = (rounds + 1) * 4
-        KC = len(key) // 4
-
-        # Convert the key into ints
-        tk = [ struct.unpack('>i', key[i:i + 4])[0] for i in xrange(0, len(key), 4) ]
-
-        # Copy values into round key arrays
-        for i in xrange(0, KC):
-            self._Ke[i // 4][i % 4] = tk[i]
-            self._Kd[rounds - (i // 4)][i % 4] = tk[i]
-
-        # Key expansion (fips-197 section 5.2)
-        rconpointer = 0
-        t = KC
-        while t < round_key_count:
-
-            tt = tk[KC - 1]
-            tk[0] ^= ((self.S[(tt >> 16) & 0xFF] << 24) ^
-                      (self.S[(tt >>  8) & 0xFF] << 16) ^
-                      (self.S[ tt        & 0xFF] <<  8) ^
-                       self.S[(tt >> 24) & 0xFF]        ^
-                      (self.rcon[rconpointer] << 24))
-            rconpointer += 1
-
-            if KC != 8:
-                for i in xrange(1, KC):
-                    tk[i] ^= tk[i - 1]
-
-            # Key expansion for 256-bit keys is "slightly different" (fips-197)
-            else:
-                for i in xrange(1, KC // 2):
-                    tk[i] ^= tk[i - 1]
-                tt = tk[KC // 2 - 1]
-
-                tk[KC // 2] ^= (self.S[ tt        & 0xFF]        ^
-                               (self.S[(tt >>  8) & 0xFF] <<  8) ^
-                               (self.S[(tt >> 16) & 0xFF] << 16) ^
-                               (self.S[(tt >> 24) & 0xFF] << 24))
-
-                for i in xrange(KC // 2 + 1, KC):
-                    tk[i] ^= tk[i - 1]
-
-            # Copy values into round key arrays
-            j = 0
-            while j < KC and t < round_key_count:
-                self._Ke[t // 4][t % 4] = tk[j]
-                self._Kd[rounds - (t // 4)][t % 4] = tk[j]
-                j += 1
-                t += 1
-
-        # Inverse-Cipher-ify the decryption round key (fips-197 section 5.3)
-        for r in xrange(1, rounds):
-            for j in xrange(0, 4):
-                tt = self._Kd[r][j]
-                self._Kd[r][j] = (self.U1[(tt >> 24) & 0xFF] ^
-                                  self.U2[(tt >> 16) & 0xFF] ^
-                                  self.U3[(tt >>  8) & 0xFF] ^
-                                  self.U4[ tt        & 0xFF])
-
-    def encrypt(self, plaintext):
-        'Encrypt a block of plain text using the AES block cipher.'
-
-        if len(plaintext) != 16:
-            raise ValueError('wrong block length')
-
-        rounds = len(self._Ke) - 1
-        (s1, s2, s3) = [1, 2, 3]
-        a = [0, 0, 0, 0]
-
-        # Convert plaintext to (ints ^ key)
-        t = [(_compact_word(plaintext[4 * i:4 * i + 4]) ^ self._Ke[0][i]) for i in xrange(0, 4)]
-
-        # Apply round transforms
-        for r in xrange(1, rounds):
-            for i in xrange(0, 4):
-                a[i] = (self.T1[(t[ i          ] >> 24) & 0xFF] ^
-                        self.T2[(t[(i + s1) % 4] >> 16) & 0xFF] ^
-                        self.T3[(t[(i + s2) % 4] >>  8) & 0xFF] ^
-                        self.T4[ t[(i + s3) % 4]        & 0xFF] ^
-                        self._Ke[r][i])
-            t = copy.copy(a)
-
-        # The last round is special
-        result = [ ]
-        for i in xrange(0, 4):
-            tt = self._Ke[rounds][i]
-            result.append((self.S[(t[ i           ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
-            result.append((self.S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
-            result.append((self.S[(t[(i + s2) % 4] >>  8) & 0xFF] ^ (tt >>  8)) & 0xFF)
-            result.append((self.S[ t[(i + s3) % 4]        & 0xFF] ^  tt       ) & 0xFF)
-
-        return result
-
-    def decrypt(self, ciphertext):
-        'Decrypt a block of cipher text using the AES block cipher.'
-
-        if len(ciphertext) != 16:
-            raise ValueError('wrong block length')
-
-        rounds = len(self._Kd) - 1
-        (s1, s2, s3) = [3, 2, 1]
-        a = [0, 0, 0, 0]
-
-        # Convert ciphertext to (ints ^ key)
-        t = [(_compact_word(ciphertext[4 * i:4 * i + 4]) ^ self._Kd[0][i]) for i in xrange(0, 4)]
-
-        # Apply round transforms
-        for r in xrange(1, rounds):
-            for i in xrange(0, 4):
-                a[i] = (self.T5[(t[ i          ] >> 24) & 0xFF] ^
-                        self.T6[(t[(i + s1) % 4] >> 16) & 0xFF] ^
-                        self.T7[(t[(i + s2) % 4] >>  8) & 0xFF] ^
-                        self.T8[ t[(i + s3) % 4]        & 0xFF] ^
-                        self._Kd[r][i])
-            t = copy.copy(a)
-
-        # The last round is special
-        result = [ ]
-        for i in xrange(0, 4):
-            tt = self._Kd[rounds][i]
-            result.append((self.Si[(t[ i           ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
-            result.append((self.Si[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
-            result.append((self.Si[(t[(i + s2) % 4] >>  8) & 0xFF] ^ (tt >>  8)) & 0xFF)
-            result.append((self.Si[ t[(i + s3) % 4]        & 0xFF] ^  tt       ) & 0xFF)
-
-        return result
-
-
-class Counter(object):
-    '''A counter object for the Counter (CTR) mode of operation.
-
-       To create a custom counter, you can usually just override the
-       increment method.'''
-
-    def __init__(self, initial_value = 1):
-
-        # Convert the value into an array of bytes long
-        self._counter = [ ((initial_value >> i) % 256) for i in xrange(128 - 8, -1, -8) ]
-
-    value = property(lambda s: s._counter)
-
-    def increment(self):
-        '''Increment the counter (overflow rolls back to 0).'''
-
-        for i in xrange(len(self._counter) - 1, -1, -1):
-            self._counter[i] += 1
-
-            if self._counter[i] < 256: break
-
-            # Carry the one
-            self._counter[i] = 0
-
-        # Overflow
-        else:
-            self._counter = [ 0 ] * len(self._counter)
-
-
-class AESBlockModeOfOperation(object):
-    '''Super-class for AES modes of operation that require blocks.'''
-    def __init__(self, key):
-        self._aes = AES(key)
-
-    def decrypt(self, ciphertext):
-        raise Exception('not implemented')
-
-    def encrypt(self, plaintext):
-        raise Exception('not implemented')
-
-
-class AESStreamModeOfOperation(AESBlockModeOfOperation):
-    '''Super-class for AES modes of operation that are stream-ciphers.'''
-
-class AESSegmentModeOfOperation(AESStreamModeOfOperation):
-    '''Super-class for AES modes of operation that segment data.'''
-
-    segment_bytes = 16
-
-
-
-class AESModeOfOperationECB(AESBlockModeOfOperation):
-    '''AES Electronic Codebook Mode of Operation.
-
-       o Block-cipher, so data must be padded to 16 byte boundaries
-
-   Security Notes:
-       o This mode is not recommended
-       o Any two identical blocks produce identical encrypted values,
-         exposing data patterns. (See the image of Tux on wikipedia)
-
-   Also see:
-       o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_.28ECB.29
-       o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.1'''
-
-
-    name = "Electronic Codebook (ECB)"
-
-    def encrypt(self, plaintext):
-        if len(plaintext) != 16:
-            raise ValueError('plaintext block must be 16 bytes')
-
-        plaintext = _string_to_bytes(plaintext)
-        return _bytes_to_string(self._aes.encrypt(plaintext))
-
-    def decrypt(self, ciphertext):
-        if len(ciphertext) != 16:
-            raise ValueError('ciphertext block must be 16 bytes')
-
-        ciphertext = _string_to_bytes(ciphertext)
-        return _bytes_to_string(self._aes.decrypt(ciphertext))
-
-
-
-class AESModeOfOperationCBC(AESBlockModeOfOperation):
-    '''AES Cipher-Block Chaining Mode of Operation.
-
-       o The Initialization Vector (IV)
-       o Block-cipher, so data must be padded to 16 byte boundaries
-       o An incorrect initialization vector will only cause the first
-         block to be corrupt; all other blocks will be intact
-       o A corrupt bit in the cipher text will cause a block to be
-         corrupted, and the next block to be inverted, but all other
-         blocks will be intact.
-
-   Security Notes:
-       o This method (and CTR) ARE recommended.
-
-   Also see:
-       o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_.28CBC.29
-       o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.2'''
-
-
-    name = "Cipher-Block Chaining (CBC)"
-
-    def __init__(self, key, iv = None):
-        if iv is None:
-            self._last_cipherblock = [ 0 ] * 16
-        elif len(iv) != 16:
-            raise ValueError('initialization vector must be 16 bytes')
-        else:
-            self._last_cipherblock = _string_to_bytes(iv)
-
-        AESBlockModeOfOperation.__init__(self, key)
-
-    def encrypt(self, plaintext):
-        if len(plaintext) != 16:
-            raise ValueError('plaintext block must be 16 bytes')
-
-        plaintext = _string_to_bytes(plaintext)
-        precipherblock = [ (p ^ l) for (p, l) in zip(plaintext, self._last_cipherblock) ]
-        self._last_cipherblock = self._aes.encrypt(precipherblock)
-
-        return _bytes_to_string(self._last_cipherblock)
-
-    def decrypt(self, ciphertext):
-        if len(ciphertext) != 16:
-            raise ValueError('ciphertext block must be 16 bytes')
-
-        cipherblock = _string_to_bytes(ciphertext)
-        plaintext = [ (p ^ l) for (p, l) in zip(self._aes.decrypt(cipherblock), self._last_cipherblock) ]
-        self._last_cipherblock = cipherblock
-
-        return _bytes_to_string(plaintext)
-
-
-
-class AESModeOfOperationCFB(AESSegmentModeOfOperation):
-    '''AES Cipher Feedback Mode of Operation.
-
-       o A stream-cipher, so input does not need to be padded to blocks,
-         but does need to be padded to segment_size
-
-    Also see:
-       o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_feedback_.28CFB.29
-       o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.3'''
-
-
-    name = "Cipher Feedback (CFB)"
-
-    def __init__(self, key, iv, segment_size = 1):
-        if segment_size == 0: segment_size = 1
-
-        if iv is None:
-            self._shift_register = [ 0 ] * 16
-        elif len(iv) != 16:
-            raise ValueError('initialization vector must be 16 bytes')
-        else:
-          self._shift_register = _string_to_bytes(iv)
-
-        self._segment_bytes = segment_size
-
-        AESBlockModeOfOperation.__init__(self, key)
-
-    segment_bytes = property(lambda s: s._segment_bytes)
-
-    def encrypt(self, plaintext):
-        if len(plaintext) % self._segment_bytes != 0:
-            raise ValueError('plaintext block must be a multiple of segment_size')
-
-        plaintext = _string_to_bytes(plaintext)
-
-        # Break block into segments
-        encrypted = [ ]
-        for i in xrange(0, len(plaintext), self._segment_bytes):
-            plaintext_segment = plaintext[i: i + self._segment_bytes]
-            xor_segment = self._aes.encrypt(self._shift_register)[:len(plaintext_segment)]
-            cipher_segment = [ (p ^ x) for (p, x) in zip(plaintext_segment, xor_segment) ]
-
-            # Shift the top bits out and the ciphertext in
-            self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment)
-
-            encrypted.extend(cipher_segment)
-
-        return _bytes_to_string(encrypted)
-
-    def decrypt(self, ciphertext):
-        if len(ciphertext) % self._segment_bytes != 0:
-            raise ValueError('ciphertext block must be a multiple of segment_size')
-
-        ciphertext = _string_to_bytes(ciphertext)
-
-        # Break block into segments
-        decrypted = [ ]
-        for i in xrange(0, len(ciphertext), self._segment_bytes):
-            cipher_segment = ciphertext[i: i + self._segment_bytes]
-            xor_segment = self._aes.encrypt(self._shift_register)[:len(cipher_segment)]
-            plaintext_segment = [ (p ^ x) for (p, x) in zip(cipher_segment, xor_segment) ]
-
-            # Shift the top bits out and the ciphertext in
-            self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment)
-
-            decrypted.extend(plaintext_segment)
-
-        return _bytes_to_string(decrypted)
-
-
-
-class AESModeOfOperationOFB(AESStreamModeOfOperation):
-    '''AES Output Feedback Mode of Operation.
-
-       o A stream-cipher, so input does not need to be padded to blocks,
-         allowing arbitrary length data.
-       o A bit twiddled in the cipher text, twiddles the same bit in the
-         same bit in the plain text, which can be useful for error
-         correction techniques.
-
-    Also see:
-       o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Output_feedback_.28OFB.29
-       o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.4'''
-
-
-    name = "Output Feedback (OFB)"
-
-    def __init__(self, key, iv = None):
-        if iv is None:
-            self._last_precipherblock = [ 0 ] * 16
-        elif len(iv) != 16:
-            raise ValueError('initialization vector must be 16 bytes')
-        else:
-          self._last_precipherblock = _string_to_bytes(iv)
-
-        self._remaining_block = [ ]
-
-        AESBlockModeOfOperation.__init__(self, key)
-
-    def encrypt(self, plaintext):
-        encrypted = [ ]
-        for p in _string_to_bytes(plaintext):
-            if len(self._remaining_block) == 0:
-                self._remaining_block = self._aes.encrypt(self._last_precipherblock)
-                self._last_precipherblock = [ ]
-            precipherbyte = self._remaining_block.pop(0)
-            self._last_precipherblock.append(precipherbyte)
-            cipherbyte = p ^ precipherbyte
-            encrypted.append(cipherbyte)
-
-        return _bytes_to_string(encrypted)
-
-    def decrypt(self, ciphertext):
-        # AES-OFB is symetric
-        return self.encrypt(ciphertext)
-
-
-
-class AESModeOfOperationCTR(AESStreamModeOfOperation):
-    '''AES Counter Mode of Operation.
-
-       o A stream-cipher, so input does not need to be padded to blocks,
-         allowing arbitrary length data.
-       o The counter must be the same size as the key size (ie. len(key))
-       o Each block independant of the other, so a corrupt byte will not
-         damage future blocks.
-       o Each block has a uniue counter value associated with it, which
-         contributes to the encrypted value, so no data patterns are
-         leaked.
-       o Also known as: Counter Mode (CM), Integer Counter Mode (ICM) and
-         Segmented Integer Counter (SIC
-
-   Security Notes:
-       o This method (and CBC) ARE recommended.
-       o Each message block is associated with a counter value which must be
-         unique for ALL messages with the same key. Otherwise security may be
-         compromised.
-
-    Also see:
-
-       o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_.28CTR.29
-       o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.5
-         and Appendix B for managing the initial counter'''
-
-
-    name = "Counter (CTR)"
-
-    def __init__(self, key, counter = None):
-        AESBlockModeOfOperation.__init__(self, key)
-
-        if counter is None:
-            counter = Counter()
-
-        self._counter = counter
-        self._remaining_counter = [ ]
-
-    def encrypt(self, plaintext):
-        while len(self._remaining_counter) < len(plaintext):
-            self._remaining_counter += self._aes.encrypt(self._counter.value)
-            self._counter.increment()
-
-        plaintext = _string_to_bytes(plaintext)
-
-        encrypted = [ (p ^ c) for (p, c) in zip(plaintext, self._remaining_counter) ]
-        self._remaining_counter = self._remaining_counter[len(encrypted):]
-
-        return _bytes_to_string(encrypted)
-
-    def decrypt(self, crypttext):
-        # AES-CTR is symetric
-        return self.encrypt(crypttext)
-
-
-# Simple lookup table for each mode
-AESModesOfOperation = dict(
-    ctr = AESModeOfOperationCTR,
-    cbc = AESModeOfOperationCBC,
-    cfb = AESModeOfOperationCFB,
-    ecb = AESModeOfOperationECB,
-    ofb = AESModeOfOperationOFB,
-)
diff --git a/src/lib/pyaes/blockfeeder.py b/src/lib/pyaes/blockfeeder.py
deleted file mode 100644
index b9a904d2..00000000
--- a/src/lib/pyaes/blockfeeder.py
+++ /dev/null
@@ -1,227 +0,0 @@
-# The MIT License (MIT)
-#
-# Copyright (c) 2014 Richard Moore
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-
-from .aes import AESBlockModeOfOperation, AESSegmentModeOfOperation, AESStreamModeOfOperation
-from .util import append_PKCS7_padding, strip_PKCS7_padding, to_bufferable
-
-
-# First we inject three functions to each of the modes of operations
-#
-#    _can_consume(size)
-#       - Given a size, determine how many bytes could be consumed in
-#         a single call to either the decrypt or encrypt method
-#
-#    _final_encrypt(data, padding = PADDING_DEFAULT)
-#       - call and return encrypt on this (last) chunk of data,
-#         padding as necessary; this will always be at least 16
-#         bytes unless the total incoming input was less than 16
-#         bytes
-#
-#    _final_decrypt(data, padding = PADDING_DEFAULT)
-#       - same as _final_encrypt except for decrypt, for
-#         stripping off padding
-#
-
-PADDING_NONE       = 'none'
-PADDING_DEFAULT    = 'default'
-
-# @TODO: Ciphertext stealing and explicit PKCS#7
-# PADDING_CIPHERTEXT_STEALING
-# PADDING_PKCS7
-
-# ECB and CBC are block-only ciphers
-
-def _block_can_consume(self, size):
-    if size >= 16: return 16
-    return 0
-
-# After padding, we may have more than one block
-def _block_final_encrypt(self, data, padding = PADDING_DEFAULT):
-    if padding == PADDING_DEFAULT:
-        data = append_PKCS7_padding(data)
-
-    elif padding == PADDING_NONE:
-        if len(data) != 16:
-            raise Exception('invalid data length for final block')
-    else:
-        raise Exception('invalid padding option')
-
-    if len(data) == 32:
-        return self.encrypt(data[:16]) + self.encrypt(data[16:])
-
-    return self.encrypt(data)
-
-
-def _block_final_decrypt(self, data, padding = PADDING_DEFAULT):
-    if padding == PADDING_DEFAULT:
-        return strip_PKCS7_padding(self.decrypt(data))
-
-    if padding == PADDING_NONE:
-        if len(data) != 16:
-            raise Exception('invalid data length for final block')
-        return self.decrypt(data)
-
-    raise Exception('invalid padding option')
-
-AESBlockModeOfOperation._can_consume = _block_can_consume
-AESBlockModeOfOperation._final_encrypt = _block_final_encrypt
-AESBlockModeOfOperation._final_decrypt = _block_final_decrypt
-
-
-
-# CFB is a segment cipher
-
-def _segment_can_consume(self, size):
-    return self.segment_bytes * int(size // self.segment_bytes)
-
-# CFB can handle a non-segment-sized block at the end using the remaining cipherblock
-def _segment_final_encrypt(self, data, padding = PADDING_DEFAULT):
-    if padding != PADDING_DEFAULT:
-        raise Exception('invalid padding option')
-
-    faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes)))
-    padded = data + to_bufferable(faux_padding)
-    return self.encrypt(padded)[:len(data)]
-
-# CFB can handle a non-segment-sized block at the end using the remaining cipherblock
-def _segment_final_decrypt(self, data, padding = PADDING_DEFAULT):
-    if padding != PADDING_DEFAULT:
-        raise Exception('invalid padding option')
-
-    faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes)))
-    padded = data + to_bufferable(faux_padding)
-    return self.decrypt(padded)[:len(data)]
-
-AESSegmentModeOfOperation._can_consume = _segment_can_consume
-AESSegmentModeOfOperation._final_encrypt = _segment_final_encrypt
-AESSegmentModeOfOperation._final_decrypt = _segment_final_decrypt
-
-
-
-# OFB and CTR are stream ciphers
-
-def _stream_can_consume(self, size):
-    return size
-
-def _stream_final_encrypt(self, data, padding = PADDING_DEFAULT):
-    if padding not in [PADDING_NONE, PADDING_DEFAULT]:
-        raise Exception('invalid padding option')
-
-    return self.encrypt(data)
-
-def _stream_final_decrypt(self, data, padding = PADDING_DEFAULT):
-    if padding not in [PADDING_NONE, PADDING_DEFAULT]:
-        raise Exception('invalid padding option')
-
-    return self.decrypt(data)
-
-AESStreamModeOfOperation._can_consume = _stream_can_consume
-AESStreamModeOfOperation._final_encrypt = _stream_final_encrypt
-AESStreamModeOfOperation._final_decrypt = _stream_final_decrypt
-
-
-
-class BlockFeeder(object):
-    '''The super-class for objects to handle chunking a stream of bytes
-       into the appropriate block size for the underlying mode of operation
-       and applying (or stripping) padding, as necessary.'''
-
-    def __init__(self, mode, feed, final, padding = PADDING_DEFAULT):
-        self._mode = mode
-        self._feed = feed
-        self._final = final
-        self._buffer = to_bufferable("")
-        self._padding = padding
-
-    def feed(self, data = None):
-        '''Provide bytes to encrypt (or decrypt), returning any bytes
-           possible from this or any previous calls to feed.
-
-           Call with None or an empty string to flush the mode of
-           operation and return any final bytes; no further calls to
-           feed may be made.'''
-
-        if self._buffer is None:
-            raise ValueError('already finished feeder')
-
-        # Finalize; process the spare bytes we were keeping
-        if data is None:
-            result = self._final(self._buffer, self._padding)
-            self._buffer = None
-            return result
-
-        self._buffer += to_bufferable(data)
-
-        # We keep 16 bytes around so we can determine padding
-        result = to_bufferable('')
-        while len(self._buffer) > 16:
-            can_consume = self._mode._can_consume(len(self._buffer) - 16)
-            if can_consume == 0: break
-            result += self._feed(self._buffer[:can_consume])
-            self._buffer = self._buffer[can_consume:]
-
-        return result
-
-
-class Encrypter(BlockFeeder):
-    'Accepts bytes of plaintext and returns encrypted ciphertext.'
-
-    def __init__(self, mode, padding = PADDING_DEFAULT):
-        BlockFeeder.__init__(self, mode, mode.encrypt, mode._final_encrypt, padding)
-
-
-class Decrypter(BlockFeeder):
-    'Accepts bytes of ciphertext and returns decrypted plaintext.'
-
-    def __init__(self, mode, padding = PADDING_DEFAULT):
-        BlockFeeder.__init__(self, mode, mode.decrypt, mode._final_decrypt, padding)
-
-
-# 8kb blocks
-BLOCK_SIZE = (1 << 13)
-
-def _feed_stream(feeder, in_stream, out_stream, block_size = BLOCK_SIZE):
-    'Uses feeder to read and convert from in_stream and write to out_stream.'
-
-    while True:
-        chunk = in_stream.read(block_size)
-        if not chunk:
-            break
-        converted = feeder.feed(chunk)
-        out_stream.write(converted)
-    converted = feeder.feed()
-    out_stream.write(converted)
-
-
-def encrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT):
-    'Encrypts a stream of bytes from in_stream to out_stream using mode.'
-
-    encrypter = Encrypter(mode, padding = padding)
-    _feed_stream(encrypter, in_stream, out_stream, block_size)
-
-
-def decrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT):
-    'Decrypts a stream of bytes from in_stream to out_stream using mode.'
-
-    decrypter = Decrypter(mode, padding = padding)
-    _feed_stream(decrypter, in_stream, out_stream, block_size)
diff --git a/src/lib/pyaes/util.py b/src/lib/pyaes/util.py
deleted file mode 100644
index 081a3759..00000000
--- a/src/lib/pyaes/util.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# The MIT License (MIT)
-#
-# Copyright (c) 2014 Richard Moore
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-# Why to_bufferable?
-# Python 3 is very different from Python 2.x when it comes to strings of text
-# and strings of bytes; in Python 3, strings of bytes do not exist, instead to
-# represent arbitrary binary data, we must use the "bytes" object. This method
-# ensures the object behaves as we need it to.
-
-def to_bufferable(binary):
-    return binary
-
-def _get_byte(c):
-    return ord(c)
-
-try:
-    xrange
-except:
-
-    def to_bufferable(binary):
-        if isinstance(binary, bytes):
-            return binary
-        return bytes(ord(b) for b in binary)
-
-    def _get_byte(c):
-        return c
-
-def append_PKCS7_padding(data):
-    pad = 16 - (len(data) % 16)
-    return data + to_bufferable(chr(pad) * pad)
-
-def strip_PKCS7_padding(data):
-    if len(data) % 16 != 0:
-        raise ValueError("invalid length")
-
-    pad = _get_byte(data[-1])
-
-    if pad > 16:
-        raise ValueError("invalid padding byte")
-
-    return data[:-pad]

From 012b3d53df0ba3a34c702df11ae9461d0eb5ed91 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 19 Jul 2022 07:12:31 +0000
Subject: [PATCH 132/333] Update feature_request.md

---
 .github/ISSUE_TEMPLATE/feature_request.md | 14 ++------------
 1 file changed, 2 insertions(+), 12 deletions(-)

diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index fe7c8178..66793649 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -2,19 +2,9 @@
 name: Feature request
 about: Suggest an idea for ZeroNet
 title: ''
-labels: ''
+labels: 'enhancement'
 assignees: ''
 
 ---
 
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
+*we have to rigid structure for feature requests right now, but please try to include important details on the matter*

From 8355bd177c819ca767d7fd41d6fafccb71deadde Mon Sep 17 00:00:00 2001
From: BlueRook <bluerook@localhost>
Date: Wed, 20 Jul 2022 09:53:54 +0000
Subject: [PATCH 133/333] Add more deb-based dependencies

these are required e.g. on Tails, although I still couldn't get
0net to work on it yet

Signed-off-by: caryoscelus <caryoscelus@gmx.com>
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 090fcc76..a56be7e4 100644
--- a/README.md
+++ b/README.md
@@ -93,7 +93,7 @@ Install autoconf and other basic development tools, python3 and pip.
 
 ##### Apt-based (debian, ubuntu, etc)
  - `sudo apt update`
- - `sudo apt install pkg-config libffi-dev python3-pip python3-venv`
+ - `sudo apt install pkg-config libffi-dev python3-pip python3-venv python3-dev build-essential`
 
 ##### Android/Termux
  - install [Termux](https://termux.com/) (in Termux you can install packages via `pkg install <package-names>`)

From 8913363ca8f6b65e62f7a6a6c8b052b4952e74b5 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 26 Jul 2022 19:57:14 +0000
Subject: [PATCH 134/333] minor code improvements

fixes #138
---
 src/Config.py |  2 +-
 src/main.py   | 49 +++++++++++++++++++++++++++++++------------------
 zeronet.py    |  2 +-
 3 files changed, 33 insertions(+), 20 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index d7a25d7e..ba020c9a 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -16,7 +16,7 @@ class Config(object):
         self.version = "0.7.6+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5032
+        self.rev = 5033
         self.argv = argv
         self.action = None
         self.test_parser = None
diff --git a/src/main.py b/src/main.py
index 8a677193..b86f09c3 100644
--- a/src/main.py
+++ b/src/main.py
@@ -25,23 +25,34 @@ gevent.monkey.patch_all(thread=False, subprocess=False)
 update_after_shutdown = False  # If set True then update and restart zeronet after main loop ended
 restart_after_shutdown = False  # If set True then restart zeronet after main loop ended
 
-# Load config
 from Config import config
-config.parse(silent=True)  # Plugins need to access the configuration
-if not config.arguments:  # Config parse failed, show the help screen and exit
+
+def load_config():
+    config.parse(silent=True)  # Plugins need to access the configuration
+    if not config.arguments:  # Config parse failed, show the help screen and exit
+        config.parse()
+
+load_config()
+
+def init_dirs():
     config.parse()
+    if not os.path.isdir(config.data_dir):
+        os.mkdir(config.data_dir)
+        try:
+            os.chmod(config.data_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
+        except Exception as err:
+            startupError("Can't change permission of %s: %s" % (config.data_dir, err))
 
-if not os.path.isdir(config.data_dir):
-    os.mkdir(config.data_dir)
-    try:
-        os.chmod(config.data_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
-    except Exception as err:
-        startupError("Can't change permission of %s: %s" % (config.data_dir, err))
+    sites_json = f"{config.data_dir}/sites.json"
+    if not os.path.isfile(sites_json):
+        with open(sites_json, "w") as f:
+            f.write("{}")
+    users_json = f"{config.data_dir}/users.json"
+    if not os.path.isfile(users_json):
+        with open(users_json, "w") as f:
+            f.write("{}")
 
-if not os.path.isfile("%s/sites.json" % config.data_dir):
-    open("%s/sites.json" % config.data_dir, "w").write("{}")
-if not os.path.isfile("%s/users.json" % config.data_dir):
-    open("%s/users.json" % config.data_dir, "w").write("{}")
+init_dirs()
 
 if config.action == "main":
     from util import helper
@@ -69,12 +80,14 @@ config.initLogging()
 
 # Debug dependent configuration
 from Debug import DebugHook
-
-# Load plugins
 from Plugin import PluginManager
-PluginManager.plugin_manager.loadPlugins()
-config.loadPlugins()
-config.parse()  # Parse again to add plugin configuration options
+
+def load_plugins():
+    PluginManager.plugin_manager.loadPlugins()
+    config.loadPlugins()
+    config.parse()  # Parse again to add plugin configuration options
+
+load_plugins()
 
 # Log current config
 logging.debug("Config: %s" % config)
diff --git a/zeronet.py b/zeronet.py
index 36b2ed74..97371e5f 100755
--- a/zeronet.py
+++ b/zeronet.py
@@ -44,7 +44,7 @@ def main():
         print("Error: Python 3.x is required")
         sys.exit(0)
 
-    if "--silent" not in sys.argv:
+    if '--silent' not in sys.argv:
         fancy_greet()
 
     main = None

From 3c9fe8aa08d23e38405ec18c65c452bc3589c97d Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 26 Jul 2022 21:21:12 +0000
Subject: [PATCH 135/333] CHANGELOG update

prepare for v0.7.7
---
 CHANGELOG.md | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 819b10cd..d33e3c43 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,18 @@
-### zeronet-conservancy 0.7.6+
-maintainers: @caryoscelus, @FraYoshi, @prtngn, @unmanbearpig
+### zeronet-conservancy 0.7.7+
+
+### zeronet-conservancy 0.7.7
+maintainers: @caryoscelus, @FraYoshi, @prtngn, @d47081 (ex @d4708)
 - return UPnP using secure xml library (@caryoscelus)
 - xmr donations in sidebar fixed (@caryoscelus)
+- add libffi-dev dependency (@d47081)
+- add more deb dependencies (@BlueRook)
+- add pyaes as external dependency (@caryoscelus)
+- remove built-in pyaes (thanks to @emdee-net)
+- fix error messages in sidebar (@caryoscelus)
+- reduce fingerprinting of site owner (@caryoscelus)
+- minor code improvements and reduce fingerprinting from zeronet-enhanced (by @geekless, adopted by @caryoscelus)
+- improve and speed up content.json loading (@caryoscelus)
+- multiple code improvements
 
 ### zeronet-conservancy 0.7.6
 maintainers: @caryoscelus, @d4708, @FraYoshi, @prtngn

From acc696ff09f1150272b939b725b70bbcb80c1deb Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 26 Jul 2022 21:55:21 +0000
Subject: [PATCH 136/333] ask for optional mute reason

refs #109
---
 plugins/ContentFilter/ContentFilterPlugin.py | 13 +++++--------
 1 file changed, 5 insertions(+), 8 deletions(-)

diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py
index f2f84b49..9d8dc6e8 100644
--- a/plugins/ContentFilter/ContentFilterPlugin.py
+++ b/plugins/ContentFilter/ContentFilterPlugin.py
@@ -56,14 +56,11 @@ class UiWebsocketPlugin(object):
 
     @flag.no_multiuser
     def actionMuteAdd(self, to, auth_address, cert_user_id, reason):
-        if "ADMIN" in self.getPermissions(to):
-            self.cbMuteAdd(to, auth_address, cert_user_id, reason)
-        else:
-            self.cmd(
-                "confirm",
-                [_["Hide all content from <b>%s</b>?"] % html.escape(cert_user_id), _["Mute"]],
-                lambda res: self.cbMuteAdd(to, auth_address, cert_user_id, reason)
-            )
+        self.cmd(
+            "prompt",
+            [_["Hide all content from <b>%s</b>?"] % html.escape(cert_user_id), reason, _["Mute"]],
+            lambda res: self.cbMuteAdd(to, auth_address, cert_user_id, res if res else reason)
+        )
 
     @flag.no_multiuser
     def cbMuteRemove(self, to, auth_address):

From f40dbfeb2163b9902495ba6e033e458463b78950 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 27 Jul 2022 13:59:38 +0000
Subject: [PATCH 137/333] v0.7.7

---
 CHANGELOG.md  | 6 ++++--
 src/Config.py | 4 ++--
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d33e3c43..db283cfb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
 ### zeronet-conservancy 0.7.7+
 
-### zeronet-conservancy 0.7.7
+### zeronet-conservancy 0.7.7 (2022-07-27)
 maintainers: @caryoscelus, @FraYoshi, @prtngn, @d47081 (ex @d4708)
 - return UPnP using secure xml library (@caryoscelus)
 - xmr donations in sidebar fixed (@caryoscelus)
@@ -12,9 +12,11 @@ maintainers: @caryoscelus, @FraYoshi, @prtngn, @d47081 (ex @d4708)
 - reduce fingerprinting of site owner (@caryoscelus)
 - minor code improvements and reduce fingerprinting from zeronet-enhanced (by @geekless, adopted by @caryoscelus)
 - improve and speed up content.json loading (@caryoscelus)
+- show `--help` even if data directory is inaccessible (@caryoscelus)
+- ask for optional user mute reason (@caryoscelus)
 - multiple code improvements
 
-### zeronet-conservancy 0.7.6
+### zeronet-conservancy 0.7.6 (2022-06-10) (864b7feb797d12077)
 maintainers: @caryoscelus, @d4708, @FraYoshi, @prtngn
 - more trackers from Syncronite by default
 - introduce multiple donations methods (@caryoscelus)
diff --git a/src/Config.py b/src/Config.py
index ba020c9a..c15ee8bf 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -13,10 +13,10 @@ import time
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.6+"
+        self.version = "0.7.7"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5033
+        self.rev = 5034
         self.argv = argv
         self.action = None
         self.test_parser = None

From ba9ee9907309065129b8dee928afbb64aca11663 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 30 Jul 2022 08:49:39 +0000
Subject: [PATCH 138/333] CHANGELOG: version hash

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index db283cfb..95bac884 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
 ### zeronet-conservancy 0.7.7+
 
-### zeronet-conservancy 0.7.7 (2022-07-27)
+### zeronet-conservancy 0.7.7 (2022-07-27) (f40dbfeb2163b9902495ba)
 maintainers: @caryoscelus, @FraYoshi, @prtngn, @d47081 (ex @d4708)
 - return UPnP using secure xml library (@caryoscelus)
 - xmr donations in sidebar fixed (@caryoscelus)

From 8509a3c644f56fc9e46177243ddbe9c009d32d67 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 30 Jul 2022 08:58:51 +0000
Subject: [PATCH 139/333] don't fail when plugin options are provided

fix regression introduced in 8913363ca8f6b65e62f7a6a6c8b052b4952e74b5
fixes #142
---
 src/main.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main.py b/src/main.py
index b86f09c3..b90884ae 100644
--- a/src/main.py
+++ b/src/main.py
@@ -35,7 +35,6 @@ def load_config():
 load_config()
 
 def init_dirs():
-    config.parse()
     if not os.path.isdir(config.data_dir):
         os.mkdir(config.data_dir)
         try:

From 9c74871d395c1f314b4f1e1a541c260e5e2614bf Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 30 Jul 2022 23:10:42 +0000
Subject: [PATCH 140/333] show help if exception is thrown early in startup

---
 src/main.py | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/main.py b/src/main.py
index b90884ae..d348f37a 100644
--- a/src/main.py
+++ b/src/main.py
@@ -29,7 +29,8 @@ from Config import config
 
 def load_config():
     config.parse(silent=True)  # Plugins need to access the configuration
-    if not config.arguments:  # Config parse failed, show the help screen and exit
+    if not config.arguments:
+        # Config parse failed completely, show the help screen and exit
         config.parse()
 
 load_config()
@@ -51,7 +52,16 @@ def init_dirs():
         with open(users_json, "w") as f:
             f.write("{}")
 
-init_dirs()
+# TODO: GET RID OF TOP-LEVEL CODE!!!
+
+try:
+    init_dirs()
+except:
+    import traceback as tb
+    print(tb.format_exc())
+    # at least make sure to print help if we're otherwise so helpless
+    config.parser.print_help()
+    sys.exit(1)
 
 if config.action == "main":
     from util import helper

From 74573966f12317fd5619f0ce42675f5c3a5a33ab Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 5 Aug 2022 12:43:08 +0000
Subject: [PATCH 141/333] No new sites plugin (#26)

NoNewSites plugin

a small plugin that disables adding new sites (similar to multi user plugin setting), good as a source of sample plugin code
---
 plugins/disabled-NoNewSites/NoNewSites.py    | 39 ++++++++++++++++++++
 plugins/disabled-NoNewSites/__init__.py      |  1 +
 plugins/disabled-NoNewSites/plugin_info.json |  5 +++
 3 files changed, 45 insertions(+)
 create mode 100644 plugins/disabled-NoNewSites/NoNewSites.py
 create mode 100644 plugins/disabled-NoNewSites/__init__.py
 create mode 100644 plugins/disabled-NoNewSites/plugin_info.json

diff --git a/plugins/disabled-NoNewSites/NoNewSites.py b/plugins/disabled-NoNewSites/NoNewSites.py
new file mode 100644
index 00000000..9bef8753
--- /dev/null
+++ b/plugins/disabled-NoNewSites/NoNewSites.py
@@ -0,0 +1,39 @@
+##
+##  Copyright (c) 2022 caryoscelus
+##
+##  zeronet-conservancy is free software: you can redistribute it and/or modify it under the
+##  terms of the GNU General Public License as published by the Free Software
+##  Foundation, either version 3 of the License, or (at your option) any later version.
+##
+##  zeronet-conservancy is distributed in the hope that it will be useful, but
+##  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+##  FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+##  details.
+##
+## You should have received a copy of the GNU General Public License along with
+## zeronet-conservancy. If not, see <https://www.gnu.org/licenses/>.
+##
+
+import re
+from Plugin import PluginManager
+
+# based on the code from Multiuser plugin
+@PluginManager.registerTo("UiRequest")
+class NoNewSites(object):
+    def __init__(self, *args, **kwargs):
+        return super(NoNewSites, self).__init__(*args, **kwargs)
+    def actionWrapper(self, path, extra_headers=None):
+        match = re.match("/(media/)?(?P<address>[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
+        if not match:
+            self.sendHeader(500)
+            return self.formatError("Plugin error", "No match for address found")
+
+        addr = match.group("address")
+
+        if not self.server.site_manager.get(addr):
+            self.sendHeader(404)
+            return self.formatError("Not Found", "Adding new sites disabled", details=False)
+        return super(NoNewSites, self).actionWrapper(path, extra_headers)
+
+    # def parsePath(self, path):
+        # return super(NoNewSites, self).parsePath(path)
diff --git a/plugins/disabled-NoNewSites/__init__.py b/plugins/disabled-NoNewSites/__init__.py
new file mode 100644
index 00000000..1781711b
--- /dev/null
+++ b/plugins/disabled-NoNewSites/__init__.py
@@ -0,0 +1 @@
+from . import NoNewSites
diff --git a/plugins/disabled-NoNewSites/plugin_info.json b/plugins/disabled-NoNewSites/plugin_info.json
new file mode 100644
index 00000000..c924d126
--- /dev/null
+++ b/plugins/disabled-NoNewSites/plugin_info.json
@@ -0,0 +1,5 @@
+{
+	"name": "NoNewSites",
+	"description": "disables adding new sites (e.g. for proxies)",
+	"default": "disabled"
+}

From 80320105d8a4af7b6802ba23a37cb76142442758 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 6 Aug 2022 00:45:27 +0000
Subject: [PATCH 142/333] favourite/unfavourite in the sidebar

there's still a bug that doesn't update unfavouriting in real time
---
 plugins/Sidebar/SidebarPlugin.py | 15 +++++++++++++++
 plugins/Sidebar/media/all.js     | 18 +++++++++++++++++-
 src/Ui/UiWebsocket.py            | 31 +++++++++++++++++++++++++++++++
 3 files changed, 63 insertions(+), 1 deletion(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index 876ea37a..ed77bbf5 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -415,11 +415,26 @@ class UiWebsocketPlugin(object):
             class_pause = "hidden"
             class_resume = ""
 
+        dashboard = config.homepage
+        dsite = self.user.sites.get(dashboard, None)
+        if not dsite:
+            print('No dashboard found, cannot favourite')
+            class_favourite = "hidden"
+            class_unfavourite = "hidden"
+        elif dsite.get('sittings', {}).get('favorite_sites', {}).get(self.site.address, False):
+            class_favourite = ""
+            class_unfavourite = "hidden"
+        else:
+            class_favourite = "hidden"
+            class_unfavourite = ""
+
         body.append(_("""
             <li>
              <label>{_[Site control]}</label>
              <a href='#Update' class='button noupdate' id='button-update'>{_[Update]}</a>
              <a href='#Pause' class='button {class_pause}' id='button-pause'>{_[Pause]}</a>
+             <a href='#Favourite' class='button {class_favourite}' id='button-favourite'>{_[Favourite]}</a>
+             <a href='#Unfavourite' class='button {class_unfavourite}' id='button-unfavourite'>{_[Unfavourite]}</a>
              <a href='#Resume' class='button {class_resume}' id='button-resume'>{_[Resume]}</a>
              <a href='#Delete' class='button noupdate' id='button-delete'>{_[Delete]}</a>
             </li>
diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js
index 5a03a37c..2bff5cef 100644
--- a/plugins/Sidebar/media/all.js
+++ b/plugins/Sidebar/media/all.js
@@ -1137,6 +1137,22 @@ window.initScrollable = function () {
           return false;
         };
       })(this));
+      this.tag.find("#button-favourite").off("click touched").on("click touched", (function(_this) {
+	return function() {
+	  _this.tag.find("#button-favourite").addClass("hidden");
+	  _this.tag.find("#button-unfavourite").removeClass("hidden");
+	  _this.wrapper.ws.cmd("siteFavourite", _this.wrapper.site_info.address);
+	  return false;
+	};
+      })(this));
+      this.tag.find("#button-unfavourite").off("click touched").on("click touched", (function(_this) {
+	return function() {
+	  _this.tag.find("#button-favourite").removeClass("hidden");
+	  _this.tag.find("#button-unfavourite").addClass("hidden");
+	  _this.wrapper.ws.cmd("siteUnfavourite", _this.wrapper.site_info.address);
+	  return false;
+	};
+      })(this));
       this.tag.find("#button-delete").off("click touchend").on("click touchend", (function(_this) {
         return function() {
           _this.handleSiteDeleteClick();
@@ -1767,4 +1783,4 @@ function morphdom(fromNode, toNode, options) {
 
 module.exports = morphdom;
 },{}]},{},[1])(1)
-});
\ No newline at end of file
+});
diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index 2bea8f2a..48a30ee2 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -10,6 +10,8 @@ import stat
 
 import gevent
 
+from rich import print
+
 from Config import config
 from Site import SiteManager
 from Crypt import CryptBitcoin
@@ -950,6 +952,35 @@ class UiWebsocket(object):
         else:
             self.response(to, {"error": "Unknown site: %s" % address})
 
+    def siteFavUnfav(self, to, address, action, response):
+        dashboard = config.homepage
+        dsite = self.user.sites.get(dashboard, None)
+        if not dsite:
+            raise RuntimeError(f'No dashboard {dashboard} found to add site to favourites')
+        if 'settings' not in dsite:
+            dsite['settings'] = {}
+        dsettings = dsite['settings']
+        if 'favorite_sites' not in dsettings:
+            dsettings['favorite_sites'] = {}
+        favs = dsettings['favorite_sites']
+        action(favs)
+        self.user.setSiteSettings(dashboard, dsettings)
+        self.response(to, response)
+
+    @flag.admin
+    @flag.no_multiuser
+    def actionSiteFavourite(self, to, address):
+        def do_add(favs):
+            favs[address] = True
+        self.siteFavUnfav(to, address, do_add, "Added to favourites")
+
+    @flag.admin
+    @flag.no_multiuser
+    def actionSiteUnfavourite(self, to, address):
+        def do_del(favs):
+            del favs[address]
+        self.siteFavUnfav(to, address, do_del, "Removed from favourites")
+
     @flag.admin
     @flag.no_multiuser
     def actionSiteDelete(self, to, address):

From 39a3830debd795233e108c0531c82a380ea5f8dd Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 6 Aug 2022 01:10:19 +0000
Subject: [PATCH 143/333] bump version

---
 src/Config.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index c15ee8bf..8ee6da06 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -13,10 +13,10 @@ import time
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.7"
+        self.version = "0.7.7+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5034
+        self.rev = 5035
         self.argv = argv
         self.action = None
         self.test_parser = None

From a0da2c25dbac224420d89651ca39e6d6175e590d Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 17 Aug 2022 12:26:51 +0000
Subject: [PATCH 144/333] fix report_url

---
 zeronet.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/zeronet.py b/zeronet.py
index 97371e5f..ac942128 100755
--- a/zeronet.py
+++ b/zeronet.py
@@ -101,8 +101,8 @@ def displayErrorMessage(err, error_log_path):
     res = ctypes.windll.user32.MessageBoxW(0, err_title, "ZeroNet error", MB_YESNOCANCEL | MB_ICONEXCLAIMATION)
     if res == ID_YES:
         import webbrowser
-        report_url = "https://github.com/HelloZeroNet/ZeroNet/issues/new?assignees=&labels=&template=bug-report.md&title=%s"
-        webbrowser.open(report_url % urllib.parse.quote("Unhandled exception: %s" % err_message))
+        report_url = "https://github.com/zeronet-conservancy/zeronet-conservancy/issues/new"
+        webbrowser.open(report_url)
     if res in [ID_YES, ID_NO]:
         subprocess.Popen(['notepad.exe', error_log_path])
 

From 46aa7b45f5996952e0fc7c79e021c5f01888e5a9 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 17 Aug 2022 12:27:05 +0000
Subject: [PATCH 145/333] be more flexible about opening any browser

---
 src/Ui/UiServer.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py
index 9d93ccfd..63738c99 100644
--- a/src/Ui/UiServer.py
+++ b/src/Ui/UiServer.py
@@ -147,13 +147,17 @@ class UiServer:
         if config.open_browser and config.open_browser != "False":
             logging.info("Opening browser: %s...", config.open_browser)
             import webbrowser
+            ui_ip = config.ui_ip if config.ui_ip != "*" else "127.0.0.1"
+            url = f'http://{ui_ip}:{config.ui_port}/{config.homepage}'
             try:
                 if config.open_browser == "default_browser":
                     browser = webbrowser.get()
                 else:
                     browser = webbrowser.get(config.open_browser)
-                url = "http://%s:%s/%s" % (config.ui_ip if config.ui_ip != "*" else "127.0.0.1", config.ui_port, config.homepage)
                 gevent.spawn_later(0.3, browser.open, url, new=2)
+            except webbrowser.Error as err:
+                import subprocess
+                subprocess.Popen([config.open_browser, url])
             except Exception as err:
                 print("Error starting browser: %s" % err)
 

From 82b6944728afb53687639b11d687cd6d96c6e104 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 24 Aug 2022 20:49:03 +0000
Subject: [PATCH 146/333] update ascii logo

---
 zeronet.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/zeronet.py b/zeronet.py
index ac942128..a9cbd7ed 100755
--- a/zeronet.py
+++ b/zeronet.py
@@ -24,10 +24,10 @@ def fancy_greet():
     from rich.console import Console
     from rich.text import Text
     zc_msg = f'''
-|||  __.  _. . _  .   . _    _.  _|_          _.   .   . _  .--   _. . _  .  .  __.  . _    _.  .  .
-|||   /  /_| |/  / \  |/ |  /_|   |     ==   /    / \  |/ |  \   /_| |/   |  |  __|  |/ |  /     \_|
-|||  /_. \_  |   \_/  |  |  \_    |.         \__  \_/  |  | ._|  \_  |     \/  |__|  |  |  \__     |
-|||                                                                                              _/
+|||  __. _.. _ . . _  _._|_     _. . . _ .-- _.. _.  . __.. _  _..  .
+|||   / /_||/ / \|/ |/_| |  == /  / \|/ | \ /_||/ |  | __||/ |/   \_|
+|||  /_.\_ |  \_/|  |\_  |.    \__\_/|  |._|\_ |   \/ |__||  |\__   |
+|||                                                                _/
 |||  v{config.version}
 '''
     lns = zc_msg.split('\n')

From 427bea581bc5cb7821930e0f81fbcbeb61a12b84 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 15 Jun 2022 15:10:41 +0000
Subject: [PATCH 147/333] fix import style

---
 src/Site/SiteManager.py | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py
index 684d69fc..5c051a9f 100644
--- a/src/Site/SiteManager.py
+++ b/src/Site/SiteManager.py
@@ -14,7 +14,8 @@ from Config import config
 from util import helper
 from util import RateLimit
 from util import Cached
-
+from .Site import Site
+from Debug import Debug
 
 @PluginManager.acceptPlugins
 class SiteManager(object):
@@ -30,10 +31,8 @@ class SiteManager(object):
     # Load all sites from data/sites.json
     @util.Noparallel()
     def load(self, cleanup=True, startup=False):
-        from Debug import Debug
         self.log.info("Loading sites... (cleanup: %s, startup: %s)" % (cleanup, startup))
         self.loaded = False
-        from .Site import Site
         address_found = []
         added = 0
         load_s = time.time()
@@ -170,7 +169,6 @@ class SiteManager(object):
         return site
 
     def add(self, address, all_file=True, settings=None, **kwargs):
-        from .Site import Site
         self.sites_changed = int(time.time())
         # Try to find site with differect case
         for recover_address, recover_site in list(self.sites.items()):

From 887fb24a85ea7b375c1e4c97d93cfbfce3bb3f20 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 10 May 2022 12:37:31 +0400
Subject: [PATCH 148/333] minor code improvements

---
 src/Content/ContentManager.py | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index a9367529..e43fef2b 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -10,6 +10,7 @@ import gevent
 
 from Debug import Debug
 from Crypt import CryptHash
+from Crypt import CryptBitcoin
 from Config import config
 from util import helper
 from util import Diff
@@ -726,7 +727,6 @@ class ContentManager(object):
         new_content["inner_path"] = inner_path
 
         # Verify private key
-        from Crypt import CryptBitcoin
         self.log.info("Verifying private key...")
         privatekey_address = CryptBitcoin.privatekeyToAddress(privatekey)
         valid_signers = self.getValidSigners(inner_path, new_content)
@@ -793,7 +793,6 @@ class ContentManager(object):
         return 1  # Todo: Multisig
 
     def verifyCertSign(self, user_address, user_auth_type, user_name, issuer_address, sign):
-        from Crypt import CryptBitcoin
         cert_subject = "%s#%s/%s" % (user_address, user_auth_type, user_name)
         return CryptBitcoin.verify(cert_subject, issuer_address, sign)
 
@@ -919,7 +918,6 @@ class ContentManager(object):
     # Return: None = Same as before, False = Invalid, True = Valid
     def verifyFile(self, inner_path, file, ignore_same=True):
         if inner_path.endswith("content.json"):  # content.json: Check using sign
-            from Crypt import CryptBitcoin
             try:
                 if type(file) is dict:
                     new_content = file
@@ -930,7 +928,7 @@ class ContentManager(object):
                         else:
                             new_content = json.load(file)
                     except Exception as err:
-                        raise VerifyError("Invalid json file: %s" % err)
+                        raise VerifyError(f"Invalid json file: {err}")
                 if inner_path in self.contents:
                     old_content = self.contents.get(inner_path, {"modified": 0})
                     # Checks if its newer the ours

From ae52b9eefeec83e064ebf405e1bec573d3353394 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 31 Aug 2022 08:58:51 +0000
Subject: [PATCH 149/333] update android/termux instructions

add libtool to dependencies
thanks to oseido for reporting
---
 README-ptbr.md | 4 ++--
 README-ru.md   | 2 +-
 README.md      | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/README-ptbr.md b/README-ptbr.md
index 38f01bc1..a53bc9c2 100644
--- a/README-ptbr.md
+++ b/README-ptbr.md
@@ -80,7 +80,7 @@ Instalar o autoconf e outras ferramentas básicas de desenvolvimento, python3 e
 ##### Android/Termux
  - install [Termux](https://termux.com/) (no Termux você pode instalar pacotes via `pkg install <nomes de pacotes>`)
  - Atualização do "pkg".
- - Pkg install python automake git binutils" (TODO: verificar nova instalação se há mais dependências para instalar)
+ - `pkg install python automake git binutils libtool` (TODO: verificar nova instalação se há mais dependências para instalar)
  - (opcional) `pkg install tor`
  - (opcional) rodar tor via comando `tor --ControlPort 9051 --CookieAuthentication 1` (você pode então abrir uma nova sessão deslizando para a direita)
 
@@ -161,4 +161,4 @@ projeto, também há um endereço dedicado ao bitcoin para isso:
 1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6
 
 Se você quiser doar de uma maneira diferente, sinta-se à vontade para contatar o mantenedor ou
-criar uma publicação!
\ No newline at end of file
+criar uma publicação!
diff --git a/README-ru.md b/README-ru.md
index 13cd1971..2546431e 100644
--- a/README-ru.md
+++ b/README-ru.md
@@ -81,7 +81,7 @@ zeronet-conservancy — это форк/продолжение проекта [Z
 ##### Android/Termux
  - Установите [Termux](https://termux.com/) (в Termux вы можете устанавливать пакеты через команду `pkg install <package-names>`)
  - `pkg update`
- - `pkg install python automake git binutils` (TODO: проверьте новую установку на наличие дополнительных зависимостей для установки)
+ - `pkg install python automake git binutils libtool`
  - (optional) `pkg install tor`
  - (optional) запустить тор через команду `tor --ControlPort 9051 --CookieAuthentication 1` (вы можете открыть новый сеанс свайпом вправо)
 
diff --git a/README.md b/README.md
index a56be7e4..a1eaa6af 100644
--- a/README.md
+++ b/README.md
@@ -98,7 +98,7 @@ Install autoconf and other basic development tools, python3 and pip.
 ##### Android/Termux
  - install [Termux](https://termux.com/) (in Termux you can install packages via `pkg install <package-names>`)
  - `pkg update`
- - `pkg install python automake git binutils` (TODO: check fresh installation whether there are more dependencies to install)
+ - `pkg install python automake git binutils libtool` (TODO: check fresh installation whether there are more dependencies to install)
  - (optional) `pkg install tor`
  - (optional) run tor via `tor --ControlPort 9051 --CookieAuthentication 1` command (you can then open new session by swiping to the right)
 

From 4c32cc224d1f0c901590887967dbe708c1d3d3f8 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 6 Sep 2022 16:04:15 +0000
Subject: [PATCH 150/333] add ArchLinux AUR package link

---
 README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.md b/README.md
index a1eaa6af..47a5a3d3 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,7 @@ Following links relate to original ZeroNet:
 ### Install from your distribution repository
 
 - NixOS: https://search.nixos.org/packages?channel=22.05&show=zeronet-conservancy&type=packages&query=zeronet-conservancy (and see below)
+- ArchLinux: https://aur.archlinux.org/packages/zeronet-conservancy-git (fresh git version)
 
 ### Install from Nix package manager (Linux or MacOS)
 

From f7b009099a1a54a3160e637f5acd543adf2705b7 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 6 Sep 2022 16:04:58 +0000
Subject: [PATCH 151/333] consolidate & fix open browser behaviour

---
 src/Ui/UiServer.py | 18 ++----------------
 src/main.py        | 16 ++--------------
 src/util/helper.py | 11 +++++++++++
 3 files changed, 15 insertions(+), 30 deletions(-)

diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py
index 63738c99..08820832 100644
--- a/src/Ui/UiServer.py
+++ b/src/Ui/UiServer.py
@@ -13,6 +13,7 @@ from Config import config
 from Debug import Debug
 import importlib
 
+from util import helper
 
 # Skip websocket handler if not necessary
 class UiWSGIHandler(WebSocketHandler):
@@ -144,22 +145,7 @@ class UiServer:
             self.log.info("Web interface: http://%s:%s/" % (config.ui_ip, config.ui_port))
         self.log.info("--------------------------------------")
 
-        if config.open_browser and config.open_browser != "False":
-            logging.info("Opening browser: %s...", config.open_browser)
-            import webbrowser
-            ui_ip = config.ui_ip if config.ui_ip != "*" else "127.0.0.1"
-            url = f'http://{ui_ip}:{config.ui_port}/{config.homepage}'
-            try:
-                if config.open_browser == "default_browser":
-                    browser = webbrowser.get()
-                else:
-                    browser = webbrowser.get(config.open_browser)
-                gevent.spawn_later(0.3, browser.open, url, new=2)
-            except webbrowser.Error as err:
-                import subprocess
-                subprocess.Popen([config.open_browser, url])
-            except Exception as err:
-                print("Error starting browser: %s" % err)
+        helper.openBrowser(config.open_browser)
 
         self.server = WSGIServer((self.ip, self.port), handler, handler_class=UiWSGIHandler, log=self.log)
         self.server.sockets = {}
diff --git a/src/main.py b/src/main.py
index d348f37a..47701b99 100644
--- a/src/main.py
+++ b/src/main.py
@@ -69,20 +69,8 @@ if config.action == "main":
         lock = helper.openLocked("%s/lock.pid" % config.data_dir, "w")
         lock.write("%s" % os.getpid())
     except BlockingIOError as err:
-        startupError("Can't open lock file, your ZeroNet client is probably already running, exiting... (%s)" % err)
-        if config.open_browser and config.open_browser != "False":
-            print("Opening browser: %s...", config.open_browser)
-            import webbrowser
-            try:
-                if config.open_browser == "default_browser":
-                    browser = webbrowser.get()
-                else:
-                    browser = webbrowser.get(config.open_browser)
-                browser.open("http://%s:%s/%s" % (
-                    config.ui_ip if config.ui_ip != "*" else "127.0.0.1", config.ui_port, config.homepage
-                ), new=2)
-            except Exception as err:
-                startupError("Error starting browser: %s" % err)
+        startupError(f"Can't open lock file, your 0net client is probably already running, exiting... ({err})")
+        helper.openBrowser(config.open_browser)
         sys.exit()
 
 config.initLogging()
diff --git a/src/util/helper.py b/src/util/helper.py
index 61455b08..fa979864 100644
--- a/src/util/helper.py
+++ b/src/util/helper.py
@@ -354,3 +354,14 @@ def encodeResponse(func):  # Encode returned data from utf8 to bytes
                 yield back.encode()
 
     return wrapper
+
+def openBrowser(agent):
+    if agent and agent != "False":
+        print(f"Opening browser: {agent}...")
+        ui_ip = config.ui_ip if config.ui_ip != "*" else "127.0.0.1"
+        url = f'http://{ui_ip}:{config.ui_port}/{config.homepage}'
+        try:
+            import subprocess
+            subprocess.Popen([config.open_browser, url])
+        except Exception as err:
+            print(f"Error starting browser: {err}")

From 579a32820e803bab852c348279f4ed9e80b173f1 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 7 Sep 2022 12:43:40 +0000
Subject: [PATCH 152/333] android/termux instruction update

thanks to oseido for testing and finding required packages
---
 README.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 47a5a3d3..4ae6f58b 100644
--- a/README.md
+++ b/README.md
@@ -99,7 +99,9 @@ Install autoconf and other basic development tools, python3 and pip.
 ##### Android/Termux
  - install [Termux](https://termux.com/) (in Termux you can install packages via `pkg install <package-names>`)
  - `pkg update`
- - `pkg install python automake git binutils libtool` (TODO: check fresh installation whether there are more dependencies to install)
+ - `pkg install python automake git binutils libtool`
+ - (on an older android versions you may also need to install) `pkg install openssl-tool libcrypt clang`
+ - (if you've installed the above packages and still run into launch issues, please report)
  - (optional) `pkg install tor`
  - (optional) run tor via `tor --ControlPort 9051 --CookieAuthentication 1` command (you can then open new session by swiping to the right)
 

From 39042c528375f3a14fa109e2379f0f3cae65bdfe Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 7 Sep 2022 19:31:23 +0000
Subject: [PATCH 153/333] allow only passing port as tor_* parameters

fixes #153
---
 src/Config.py         | 24 +++++++++++++++++++++++-
 src/Tor/TorManager.py |  7 ++-----
 src/main.py           |  2 +-
 3 files changed, 26 insertions(+), 7 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index 8ee6da06..4484ae60 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -16,7 +16,7 @@ class Config(object):
         self.version = "0.7.7+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5035
+        self.rev = 5036
         self.argv = argv
         self.action = None
         self.test_parser = None
@@ -754,6 +754,28 @@ class Config(object):
         if file_logging:
             self.initFileLogger()
 
+    def tor_proxy_split(self):
+        if self.tor_proxy:
+            if ':' in config.tor_proxy:
+                ip, port = config.tor_proxy.rsplit(":", 1)
+            else:
+                ip = 'localhost'
+                port = config.tor_proxy
+            return ip, int(port)
+        else:
+            return 'localhost', 9050
+
+    def tor_controller_split(self):
+        if self.tor_controller:
+            if ':' in config.tor_controller:
+                ip, port = config.tor_controller.rsplit(":", 1)
+            else:
+                ip = 'localhost'
+                port = config.tor_controller
+            return ip, int(port)
+        else:
+            return 'localhost', 9051
+
 
 class ErrorLogHandler(logging.StreamHandler):
     def __init__(self):
diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py
index 2fbca8c9..e2a945e7 100644
--- a/src/Tor/TorManager.py
+++ b/src/Tor/TorManager.py
@@ -51,11 +51,8 @@ class TorManager(object):
         else:
             self.fileserver_port = config.fileserver_port
 
-        self.ip, self.port = config.tor_controller.rsplit(":", 1)
-        self.port = int(self.port)
-
-        self.proxy_ip, self.proxy_port = config.tor_proxy.rsplit(":", 1)
-        self.proxy_port = int(self.proxy_port)
+        self.ip, self.port = config.tor_controller_split()
+        self.proxy_ip, self.proxy_port = config.tor_proxy_split()
 
     def start(self):
         self.log.debug("Starting (Tor: %s)" % config.tor)
diff --git a/src/main.py b/src/main.py
index 47701b99..61bbb7b9 100644
--- a/src/main.py
+++ b/src/main.py
@@ -122,7 +122,7 @@ elif config.tor == "always":
     logging.info("Patching sockets to tor socks proxy: %s" % config.tor_proxy)
     if config.fileserver_ip == "*":
         config.fileserver_ip = '127.0.0.1'  # Do not accept connections anywhere but localhost
-    SocksProxy.monkeyPatch(*config.tor_proxy.split(":"))
+    SocksProxy.monkeyPatch(*config.tor_proxy_split())
     config.disable_udp = True
 elif config.bind:
     bind = config.bind

From 09bb67d904c64a305de62aebeae95d62af312c69 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 8 Sep 2022 12:06:48 +0000
Subject: [PATCH 154/333] keep browser process alive

---
 src/main.py        | 9 +++++----
 src/util/helper.py | 2 +-
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/src/main.py b/src/main.py
index 47701b99..22927955 100644
--- a/src/main.py
+++ b/src/main.py
@@ -66,12 +66,13 @@ except:
 if config.action == "main":
     from util import helper
     try:
-        lock = helper.openLocked("%s/lock.pid" % config.data_dir, "w")
-        lock.write("%s" % os.getpid())
+        lock = helper.openLocked(f"{config.data_dir}/lock.pid", "w")
+        lock.write(f"{os.getpid()}")
     except BlockingIOError as err:
         startupError(f"Can't open lock file, your 0net client is probably already running, exiting... ({err})")
-        helper.openBrowser(config.open_browser)
-        sys.exit()
+        proc = helper.openBrowser(config.open_browser)
+        r = proc.wait()
+        sys.exit(r)
 
 config.initLogging()
 
diff --git a/src/util/helper.py b/src/util/helper.py
index fa979864..ac330fd8 100644
--- a/src/util/helper.py
+++ b/src/util/helper.py
@@ -362,6 +362,6 @@ def openBrowser(agent):
         url = f'http://{ui_ip}:{config.ui_port}/{config.homepage}'
         try:
             import subprocess
-            subprocess.Popen([config.open_browser, url])
+            return subprocess.Popen([config.open_browser, url])
         except Exception as err:
             print(f"Error starting browser: {err}")

From 6c1bbef54b62f3119d533eaaddbb2c55016f6ba2 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 8 Sep 2022 12:41:39 +0000
Subject: [PATCH 155/333] update trackers from Syncronite

---
 src/Config.py | 198 +++++++++++++++++++++-----------------------------
 1 file changed, 84 insertions(+), 114 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index 8ee6da06..bb8eac75 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -9,6 +9,90 @@ import logging.handlers
 import stat
 import time
 
+trackers = [
+    'zero://188.242.242.224:26474',
+    'zero://2001:19f0:8001:1d2f:5400:2ff:fe83:5bf7:23141',
+    'zero://200:1e7a:5100:ef7c:6fa4:d8ae:b91c:a74:15441',
+    'zero://23.184.48.134:15441',
+    'zero://57hzgtu62yzxqgbvgxs7g3lfck3za4zrda7qkskar3tlak5recxcebyd.onion:15445',
+    'zero://6i54dd5th73oelv636ivix6sjnwfgk2qsltnyvswagwphub375t3xcad.onion:15441',
+    'zero://f2hnjbggc3c2u2apvxdugirnk6bral54ibdoul3hhvu7pd4fso5fq3yd.onion:15441',
+    'zero://gugt43coc5tkyrhrc3esf6t6aeycvcqzw7qafxrjpqbwt4ssz5czgzyd.onion:15441',
+    'zero://k5w77dozo3hy5zualyhni6vrh73iwfkaofa64abbilwyhhd3wgenbjqd.onion:15441',
+    'zero://ow7in4ftwsix5klcbdfqvfqjvimqshbm2o75rhtpdnsderrcbx74wbad.onion:15441',
+    'zero://pn4q2zzt2pw4nk7yidxvsxmydko7dfibuzxdswi6gu6ninjpofvqs2id.onion:15441',
+    'zero://skdeywpgm5xncpxbbr4cuiip6ey4dkambpanog6nruvmef4f3e7o47qd.onion:15441',
+    'zero://wlxav3szbrdhest4j7dib2vgbrd7uj7u7rnuzg22cxbih7yxyg2hsmid.onion:15441',
+    'zero://zy7wttvjtsijt5uwmlar4yguvjc2gppzbdj4v6bujng6xwjmkdg7uvqd.onion:15441',
+    'http://bt.okmp3.ru:2710/announce',
+    'http://fxtt.ru:80/announce',
+    'http://incine.ru:6969/announce',
+    'http://moeweb.pw:6969/announce',
+    'http://open.acgnxtracker.com:80/announce',
+    'http://t.acg.rip:6699/announce',
+    'http://t.nyaatracker.com:80/announce',
+    'http://t.overflow.biz:6969/announce',
+    'http://tracker.files.fm:6969/announce',
+    'http://tracker.mywaifu.best:6969/announce',
+    'http://tracker.vrpnet.org:6969/announce',
+    'http://vps02.net.orel.ru:80/announce',
+    'udp://960303.xyz:6969/announce',
+    'udp://aarsen.me:6969/announce',
+    'udp://astrr.ru:6969/announce',
+    'udp://ben.kerbertools.xyz:6969/announce',
+    'udp://bt1.archive.org:6969/announce',
+    'udp://bt2.archive.org:6969/announce',
+    'udp://bt.ktrackers.com:6666/announce',
+    'udp://bubu.mapfactor.com:6969/announce',
+    'udp://c.ns.cluefone.com:6969/announce',
+    'udp://cutscloud.duckdns.org:6969/announce',
+    'udp://download.nerocloud.me:6969/announce',
+    'udp://epider.me:6969/announce',
+    'udp://exodus.desync.com:6969/announce',
+    'udp://htz3.noho.st:6969/announce',
+    'udp://ipv4.tracker.harry.lu:80/announce',
+    'udp://laze.cc:6969/announce',
+    'udp://mail.artixlinux.org:6969/announce',
+    'udp://mirror.aptus.co.tz:6969/announce',
+    'udp://moonburrow.club:6969/announce',
+    'udp://movies.zsw.ca:6969/announce',
+    'udp://mts.tvbit.co:6969/announce',
+    'udp://new-line.net:6969/announce',
+    'udp://open.demonii.com:1337/announce',
+    'udp://open.stealth.si:80/announce',
+    'udp://opentracker.i2p.rocks:6969/announce',
+    'udp://p4p.arenabg.com:1337/announce',
+    'udp://psyco.fr:6969/announce',
+    'udp://public.publictracker.xyz:6969/announce',
+    'udp://rep-art.ynh.fr:6969/announce',
+    'udp://run.publictracker.xyz:6969/announce',
+    'udp://sanincode.com:6969/announce',
+    'udp://slicie.icon256.com:8000/announce',
+    'udp://tamas3.ynh.fr:6969/announce',
+    'udp://thouvenin.cloud:6969/announce',
+    'udp://torrentclub.space:6969/announce',
+    'udp://tracker.0x.tf:6969/announce',
+    'udp://tracker1.bt.moack.co.kr:80/announce',
+    'udp://tracker.4.babico.name.tr:3131/announce',
+    'udp://tracker.altrosky.nl:6969/announce',
+    'udp://tracker.artixlinux.org:6969/announce',
+    'udp://tracker.farted.net:6969/announce',
+    'udp://tracker.jonaslsa.com:6969/announce',
+    'udp://tracker.joybomb.tw:6969/announce',
+    'udp://tracker.monitorit4.me:6969/announce',
+    'udp://tracker.opentrackr.org:1337/announce',
+    'udp://tracker.pomf.se:80/announce',
+    'udp://tracker.publictracker.xyz:6969/announce',
+    'udp://tracker.srv00.com:6969/announce',
+    'udp://tracker.tcp.exchange:6969/announce',
+    'udp://tracker.theoks.net:6969/announce',
+    'udp://transkaroo.joustasie.net:6969/announce',
+    'udp://uploads.gamecoast.net:6969/announce',
+    'udp://v2.iperson.xyz:6969/announce',
+    'udp://vibe.sleepyinternetfun.xyz:1738/announce',
+    'udp://www.skynetcenter.me:6969/announce',
+    'udp://www.torrent.eu.org:451/announce',
+]
 
 class Config(object):
 
@@ -81,120 +165,6 @@ class Config(object):
 
     # Create command line arguments
     def createArguments(self):
-        trackers = [
-            # more trackers from Syncronite (TODO: check if any of the old ones still work)
-            'zero://145.239.95.38:15441',
-            'zero://23.184.48.134:15441',
-            'zero://95.110.227.231:15441',
-            'zero://styromaniac.com:15441',
-            'zero://2kcb2fqesyaevc4lntogupa4mkdssth2ypfwczd2ov5a3zo6ytwwbayd.onion:15441',
-            'zero://zy7wttvjtsijt5uwmlar4yguvjc2gppzbdj4v6bujng6xwjmkdg7uvqd.onion:15441',
-            'zero://gugt43coc5tkyrhrc3esf6t6aeycvcqzw7qafxrjpqbwt4ssz5czgzyd.onion:15441',
-            'zero://57hzgtu62yzxqgbvgxs7g3lfck3za4zrda7qkskar3tlak5recxcebyd.onion:15445',
-            'zero://hb6ozikfiaafeuqvgseiik4r46szbpjfu66l67wjinnyv6dtopuwhtqd.onion:15445',
-            'zero://ow7in4ftwsix5klcbdfqvfqjvimqshbm2o75rhtpdnsderrcbx74wbad.onion:15441',
-            'zero://skdeywpgm5xncpxbbr4cuiip6ey4dkambpanog6nruvmef4f3e7o47qd.onion:15441',
-            'zero://rlcjomszyitxpwv7kzopmqgzk3bdpsxeull4c3s6goszkk6h2sotfoad.onion:15441',
-            'udp://tracker.opentrackr.org:1337/announce',
-            'udp://tracker.moeking.me:6969/announce',
-            'udp://tracker.altrosky.nl:6969/announce',
-            'udp://torrentclub.space:6969/announce',
-            'http://tracker.logirl.moe:17052/announce',
-            'udp://p4p.arenabg.com:1337/announce',
-            'http://tracker.files.fm:6969/announce',
-            'udp://opentracker.i2p.rocks:6969/announce',
-            'http://open.acgnxtracker.com:80/announce',
-            'udp://fe.dealclub.de:6969/announce',
-            'udp://tracker.publictracker.xyz:6969/announce',
-            'udp://tracker.0x.tf:6969/announce',
-            'udp://tracker.openbittorrent.com:6969/announce',
-            'udp://vibe.sleepyinternetfun.xyz:1738/announce',
-            'udp://tracker.jordan.im:6969/announce',
-            'udp://tracker.pomf.se:80/announce',
-            'http://tracker.mywaifu.best:6969/announce',
-            'udp://open.xxtor.com:3074/announce',
-            'udp://open.stealth.si:80/announce',
-            'http://t.nyaatracker.com:80/announce',
-            'http://tracker.ipv6tracker.ru:80/announce',
-            'http://tracker.noobsubs.net:80/announce',
-            'udp://movies.zsw.ca:6969/announce',
-            'http://bt.okmp3.ru:2710/announce',
-            'udp://zecircle.xyz:6969/announce',
-            'udp://exodus.desync.com:6969/announce',
-            'http://t.acg.rip:6699/announce',
-            'http://fxtt.ru:80/announce',
-            'udp://run.publictracker.xyz:6969/announce',
-            'udp://open.free-tracker.ga:6969/announce',
-            'udp://v2.iperson.xyz:6969/announce',
-            'udp://960303.xyz:6969/announce',
-            'http://t.overflow.biz:6969/announce',
-            'udp://tracker.dump.cl:6969/announce',
-            'udp://oldboystudy.com:6969/announce',
-            'udp://tracker1.bt.moack.co.kr:80/announce',
-            'udp://tracker.torrent.eu.org:451/announce',
-            'udp://mirror.aptus.co.tz:6969/announce',
-            'http://tracker3.ctix.cn:2095/announce',
-            'http://tracker2.ctix.cn:2095/announce',
-            'udp://tracker1.myporn.club:9337/announce',
-            'udp://bt1.archive.org:6969/announce',
-            'udp://ipv4.tracker.harry.lu:80/announce',
-            'udp://mts.tvbit.co:6969/announce',
-            'http://tracker3.520.jp:2095/announce',
-            'udp://public.publictracker.xyz:6969/announce',
-            'https://tracker.nodetopia.xyz:443/announce',
-            'udp://bubu.mapfactor.com:6969/announce',
-            'http://tracker2.520.jp:2095/announce',
-            'https://track.plop.pm:8989/announce',
-            'udp://bt2.archive.org:6969/announce',
-            'http://uraniumhexafluori.de:1919/announce',
-            'http://tracker.skyts.net:6969/announce',
-            'http://vps02.net.orel.ru:80/announce',
-            'udp://www.torrent.eu.org:451/announce',
-            'udp://bclearning.top:6969/announce',
-
-            'zero://kgsvasoakvj4gnjiy7zemu34l3hq46dn5eauqkn76jpowmilci5t2vqd.onion:15445',
-            'zero://hb6ozikfiaafeuqvgseiik4r46szbpjfu66l67wjinnyv6dtopuwhtqd.onion:15445',
-            'zero://75pmmcbp4vvo2zndmjnrkandvbg6jyptygvvpwsf2zguj7urq7t4jzyd.onion:7777',
-            'zero://dw4f4sckg2ultdj5qu7vtkf3jsfxsah3mz6pivwfd6nv3quji3vfvhyd.onion:6969',
-            'zero://5vczpwawviukvd7grfhsfxp7a6huz77hlis4fstjkym5kmf4pu7i7myd.onion:15441',
-            'zero://ow7in4ftwsix5klcbdfqvfqjvimqshbm2o75rhtpdnsderrcbx74wbad.onion:15441',
-            'zero://agufghdtniyfwty3wk55drxxwj2zxgzzo7dbrtje73gmvcpxy4ngs4ad.onion:15441',
-            'zero://qn65si4gtcwdiliq7vzrwu62qrweoxb6tx2cchwslaervj6szuje66qd.onion:26117',
-            'udp://tracker.opentrackr.org:1337/announce',
-            'udp://tracker.moeking.me:6969/announce',
-            'http://tracker.files.fm:6969/announce',
-            'http://t.overflow.biz:6969/announce',
-            'udp://fe.dealclub.de:6969/announce',
-            'udp://movies.zsw.ca:6969/announce',
-            'udp://6ahddutb1ucc3cp.ru:6969/announce',
-            'zero://145.239.95.38:15441',
-            'zero://23.184.48.134:15441',
-            'zero://95.110.227.231:15441',
-            'zero://159.65.50.3:26117',
-            'zero://2a03:b0c0:1:d0::f52:1:26117',
-            'zero://k5w77dozo3hy5zualyhni6vrh73iwfkaofa64abbilwyhhd3wgenbjqd.onion:15441',
-            # by zeroseed at http://127.0.0.1:43110/19HKdTAeBh5nRiKn791czY7TwRB1QNrf1Q/?:users/1HvNGwHKqhj3ZMEM53tz6jbdqe4LRpanEu:zn:dc17f896-bf3f-4962-bdd4-0a470040c9c5
-            "zero://k5w77dozo3hy5zualyhni6vrh73iwfkaofa64abbilwyhhd3wgenbjqd.onion:15441",
-            "zero://2kcb2fqesyaevc4lntogupa4mkdssth2ypfwczd2ov5a3zo6ytwwbayd.onion:15441",
-            "zero://my562dxpjropcd5hy3nd5pemsc4aavbiptci5amwxzbelmzgkkuxpvid.onion:15441",
-            "zero://pn4q2zzt2pw4nk7yidxvsxmydko7dfibuzxdswi6gu6ninjpofvqs2id.onion:15441",
-            "zero://6i54dd5th73oelv636ivix6sjnwfgk2qsltnyvswagwphub375t3xcad.onion:15441",
-            "zero://tl74auz4tyqv4bieeclmyoe4uwtoc2dj7fdqv4nc4gl5j2bwg2r26bqd.onion:15441",
-            "zero://wlxav3szbrdhest4j7dib2vgbrd7uj7u7rnuzg22cxbih7yxyg2hsmid.onion:15441",
-            "zero://zy7wttvjtsijt5uwmlar4yguvjc2gppzbdj4v6bujng6xwjmkdg7uvqd.onion:15441",
-
-            # ZeroNet 0.7.2 defaults:
-            "zero://boot3rdez4rzn36x.onion:15441",
-            "zero://zero.booth.moe#f36ca555bee6ba216b14d10f38c16f7769ff064e0e37d887603548cc2e64191d:443",  # US/NY
-            "udp://tracker.coppersurfer.tk:6969",  # DE
-            "udp://104.238.198.186:8000",  # US/LA
-            "udp://retracker.akado-ural.ru:80",  # RU
-            "http://h4.trakx.nibba.trade:80/announce",  # US/VA
-            "http://open.acgnxtracker.com:80/announce",  # DE
-            "http://tracker.bt4g.com:2095/announce",  # Cloudflare
-            "zero://2602:ffc5::c5b2:5360:26312"  # US/ATL
-        ]
-
         try:
             language, enc = locale.getdefaultlocale()
             language = language.lower().replace("_", "-")

From 44271420e53827d0eb72b670abbcedec03e0916c Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 9 Sep 2022 12:41:09 +0000
Subject: [PATCH 156/333] replace py3.5 with 3.10 in github workflow

fixes #133
---
 .github/workflows/tests.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 9c6f6f3b..febe263d 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -9,7 +9,7 @@ jobs:
     strategy:
       max-parallel: 16
       matrix:
-        python-version: [3.5, 3.6, 3.7, 3.8, 3.9]
+        python-version: [3.6, 3.7, 3.8, 3.9, 3.10]
 
     steps:
     - uses: actions/checkout@v2

From a420666926fb38ad9edcb1dbd25f886b4d21ea2f Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 19 Sep 2022 12:12:02 +0000
Subject: [PATCH 157/333] minor README update

---
 README.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/README.md b/README.md
index 4ae6f58b..86bbb11e 100644
--- a/README.md
+++ b/README.md
@@ -133,10 +133,12 @@ Install autoconf and other basic development tools, python3 and pip.
 * File transactions are not compressed
 * No private sites
 * No DHT support
+* No I2P support
 * Centralized elements like zeroid (we're working on this!)
 * No reliable spam protection (and on this too)
 * Doesn't work directly from browser (one of the top priorities for mid-future)
 * No data transparency
+* No reproducible builds
 
 
 ## How can I create a ZeroNet site?

From 9a3fd9563665cb5e6034233c548c321ebefed686 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 19 Sep 2022 12:12:22 +0000
Subject: [PATCH 158/333] comments

---
 src/Ui/UiRequest.py | 1 +
 src/main.py         | 1 -
 2 files changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 90329a8b..e34d22cb 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -124,6 +124,7 @@ class UiRequest(object):
             ).encode("utf8")
             return iter([ret_error, ret_body])
 
+        # TODO: phase out .bit support
         # Prepend .bit host for transparent proxy
         if self.isDomain(self.env.get("HTTP_HOST")):
             path = re.sub("^/", "/" + self.env.get("HTTP_HOST") + "/", path)
diff --git a/src/main.py b/src/main.py
index b076a8f4..a5e15070 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,4 +1,3 @@
-# Included modules
 import os
 import sys
 import stat

From f3deb8cc0b10b15eb41ba24586a52a79e6d67917 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 19 Sep 2022 11:41:14 +0000
Subject: [PATCH 159/333] windows os build/install instructions WIP

---
 README.md | 28 +++++++++++++++++++++++++---
 1 file changed, 25 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index 86bbb11e..0b37d5f4 100644
--- a/README.md
+++ b/README.md
@@ -90,7 +90,7 @@ if you're on NixOS
 
 ##### Generic unix-like (including mac os x)
 
-Install autoconf and other basic development tools, python3 and pip.
+Install autoconf and other basic development tools, python3 and pip, then proceed to "building python dependencies"
 
 ##### Apt-based (debian, ubuntu, etc)
  - `sudo apt update`
@@ -116,7 +116,7 @@ Install autoconf and other basic development tools, python3 and pip.
  - `source venv/bin/activate`
  - `python3 zeronet.py`
 
-#### Build Docker image
+#### (alternatively) Build Docker image
 - build 0net image: `docker build -t 0net:conservancy . -f Dockerfile`
 - or build 0net image with integrated tor: `docker build -t 0net:conservancy . -f Dockerfile.integrated_tor`
 - and run it: `docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net:conservancy`
@@ -124,10 +124,32 @@ Install autoconf and other basic development tools, python3 and pip.
 - or you can run it with docker-compose: `docker compose up -d 0net` up two containers - 0net and tor separately.
 - or: `docker compose up -d 0net-tor` for run 0net and tor in one container.
 
-#### alternative script
+#### Alternative script
  - after installing general dependencies and cloning repo (as above), run `start-venv.sh` which will create a virtual env for you and install python requirements
  - more convenience scripts to be added soon
 
+### Building under windows os
+
+(this instruction is work-in-progress, please help us test it and improve it!)
+
+- install python from https://www.python.org/downloads/
+- install some windows compiler suitable for python , this proved to be the most difficult part for me as non-windows user (see here https://wiki.python.org/moin/WindowsCompilers and i'll link more references later)
+- [optionally to get latest dev version] install git from https://git-scm.com/downloads
+- [optionally to use tor for better connectivity and anonymization] install tor browser from https://www.torproject.org/download/
+- open git bash console
+- type/copypaste `git clone https://github.com/zeronet-conservancy/zeronet-conservancy.git` into command line
+- wait till git downloads latest dev version and continue in console
+- `cd zeronet-conservancy`
+- `python -m venv venv` (create virtual python environment)
+- `venv\Scripts\activate` (this activates the environment)
+- `pip install -r requirements.txt` (install python dependencies)
+- (NOTE: if previous step fails, it most likely means you haven't installed c/c++ compiler successfully)
+- [optional for tor for better connectivity and anonymity] launch Tor Browser
+- (NOTE: windows might show a window saying it blocked access to internet for "security reasons" — you should allow the access)
+- `python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151` (launch zeronet-conservancy!)
+- [for full tor anonymity launch this instead] `python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151 --tor always`
+- navigate to http://127.0.0.1:43110 in your favourite browser!
+
 ## Current limitations
 
 * File transactions are not compressed

From dc804b9d5f3a2a9f1fffa1b97d82e0e04c44508b Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 19 Sep 2022 15:00:15 +0000
Subject: [PATCH 160/333] remove unused code

---
 src/Site/Site.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/Site/Site.py b/src/Site/Site.py
index ea19c4a2..7deab5cc 100644
--- a/src/Site/Site.py
+++ b/src/Site/Site.py
@@ -35,7 +35,6 @@ class Site(object):
     def __init__(self, address, allow_create=True, settings=None):
         self.address = str(re.sub("[^A-Za-z0-9]", "", address))  # Make sure its correct address
         self.address_hash = hashlib.sha256(self.address.encode("ascii")).digest()
-        self.address_sha1 = hashlib.sha1(self.address.encode("ascii")).digest()
         self.address_short = "%s..%s" % (self.address[:6], self.address[-4:])  # Short address for logging
         self.log = logging.getLogger("Site:%s" % self.address_short)
         self.addEventListeners()

From 128ff2dc3984072bd379e697f9d13637b38ddd3d Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 19 Sep 2022 15:27:45 +0000
Subject: [PATCH 161/333] comment in Db

---
 src/Db/Db.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/Db/Db.py b/src/Db/Db.py
index d1d9ce15..3d4b6d7d 100644
--- a/src/Db/Db.py
+++ b/src/Db/Db.py
@@ -1,3 +1,5 @@
+## please note that this file uses custom db cursor and thus may surprise you with how sql queries are performed
+
 import sqlite3
 import json
 import time

From 53d51e8bc88ae408709f48ffcedc63251da1a8e8 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 19 Sep 2022 16:05:25 +0000
Subject: [PATCH 162/333] redirect .bit domains to hash actual addresses

refs #23
---
 src/Ui/UiRequest.py | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index e34d22cb..1b2d5cf3 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -372,7 +372,7 @@ class UiRequest(object):
     # Redirect to an url
     def actionRedirect(self, url):
         self.start_response('301 Redirect', [('Location', str(url))])
-        yield self.formatRedirect(url)
+        return self.formatRedirect(url)
 
     def actionIndex(self):
         return self.actionRedirect("/" + config.homepage + "/")
@@ -634,7 +634,9 @@ class UiRequest(object):
         match = re.match(r"/(media/)?(?P<address>[A-Za-z0-9]+[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
         if match:
             path_parts = match.groupdict()
-            if self.isDomain(path_parts["address"]):
+            addr = path_parts["address"]
+            if self.isDomain(addr):
+                path_parts["domain"] = addr
                 path_parts["address"] = self.resolveDomain(path_parts["address"])
             path_parts["request_address"] = path_parts["address"]  # Original request address (for Merger sites)
             path_parts["inner_path"] = path_parts["inner_path"].lstrip("/")
@@ -651,6 +653,12 @@ class UiRequest(object):
         except SecurityError as err:
             return self.error403(err)
 
+        if "domain" in path_parts:
+            addr = path_parts['address']
+            path = path_parts['inner_path']
+            query = self.env['QUERY_STRING']
+            return self.actionRedirect(f"/{addr}/{path}?{query}")
+
         if not path_parts:
             return self.error404(path)
 

From 2ab48a89b489cfd64d43d9e13c61bf0779d0e089 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 19 Sep 2022 16:29:39 +0000
Subject: [PATCH 163/333] temporarily disable tests, fix CodeQL

---
 .github/workflows/codeql-analysis.yml | 4 ++--
 .github/workflows/tests.yml           | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 66a64b3a..cf9cd7b0 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -13,10 +13,10 @@ name: "CodeQL"
 
 on:
   push:
-    branches: [ py3 ]
+    branches: [ master ]
   pull_request:
     # The branches below must be a subset of the branches above
-    branches: [ py3 ]
+    branches: [ master ]
   schedule:
     - cron: '37 18 * * 5'
 
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index febe263d..5b80bff1 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,6 +1,6 @@
 name: tests
 
-on: [push, pull_request]
+on: []
 
 jobs:
   test:

From 494f9b922ddff3ccc0cc585aa29d65615083ae9f Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 19 Sep 2022 16:35:42 +0000
Subject: [PATCH 164/333] update CodeQL

---
 .github/workflows/codeql-analysis.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index cf9cd7b0..ab2b4a78 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -42,7 +42,7 @@ jobs:
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v1
+      uses: github/codeql-action/init@v2
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -53,7 +53,7 @@ jobs:
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
-      uses: github/codeql-action/autobuild@v1
+      uses: github/codeql-action/autobuild@v2
 
     # ℹ️ Command-line programs to run using the OS shell.
     # 📚 https://git.io/JvXDl
@@ -67,4 +67,4 @@ jobs:
     #   make release
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v1
+      uses: github/codeql-action/analyze@v2

From 350cd8ce67a2809016dfdc6add9fccf6f53d4fd2 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 19 Sep 2022 19:57:16 +0000
Subject: [PATCH 165/333] disable ip address leak when downloading geoip db in
 tor-only mode

---
 plugins/Sidebar/SidebarPlugin.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index ed77bbf5..ca4968cc 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -593,7 +593,7 @@ class UiWebsocketPlugin(object):
         import shutil
         from util import helper
 
-        if config.offline:
+        if config.offline or config.tor == 'always':
             return False
 
         self.log.info("Downloading GeoLite2 City database...")

From 813245718406ec3933532010af05a48bf5bcc5b6 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 19 Sep 2022 20:03:16 +0000
Subject: [PATCH 166/333] fix default ssl version to be secure

---
 src/util/helper.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/util/helper.py b/src/util/helper.py
index ac330fd8..a0daa557 100644
--- a/src/util/helper.py
+++ b/src/util/helper.py
@@ -209,7 +209,11 @@ def httpRequest(url, as_file=False):
 
         conn = http.client.HTTPSConnection(host)
         sock = socket.create_connection((conn.host, conn.port), conn.timeout, conn.source_address)
-        conn.sock = ssl.wrap_socket(sock, conn.key_file, conn.cert_file)
+
+        context = ssl.create_default_context()
+        context.minimum_version = ssl.TLSVersion.TLSv1_2
+
+        conn.sock = context.wrap_socket(sock, conn.key_file, conn.cert_file)
         conn.request("GET", request)
         response = conn.getresponse()
         if response.status in [301, 302, 303, 307, 308]:

From c8de3ebf2b8e75b85164d0f5d53af78a67548dd1 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 20 Sep 2022 17:48:46 +0000
Subject: [PATCH 167/333] fix codeql python flow

---
 .github/workflows/codeql-analysis.yml | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index ab2b4a78..582eae0f 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -52,8 +52,8 @@ jobs:
 
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
-    - name: Autobuild
-      uses: github/codeql-action/autobuild@v2
+    # - name: Autobuild
+      # uses: github/codeql-action/autobuild@v2
 
     # ℹ️ Command-line programs to run using the OS shell.
     # 📚 https://git.io/JvXDl
@@ -62,9 +62,10 @@ jobs:
     #    and modify them (or add more) to build your code if your project
     #    uses a compiled language
 
-    #- run: |
-    #   make bootstrap
-    #   make release
+    - run: |
+       python3 -m venv venv
+       source venv/bin/activate
+       pip install -r requirements.txt
 
     - name: Perform CodeQL Analysis
       uses: github/codeql-action/analyze@v2

From 16b114c42798fc021a527eba41ace39ffcb3e807 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 19 Oct 2022 12:48:46 +0000
Subject: [PATCH 168/333] try fix codeql

No event triggers defined in `on`
---
 .github/workflows/codeql-analysis.yml | 9 +--------
 1 file changed, 1 insertion(+), 8 deletions(-)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 582eae0f..9e592adf 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -11,14 +11,7 @@
 #
 name: "CodeQL"
 
-on:
-  push:
-    branches: [ master ]
-  pull_request:
-    # The branches below must be a subset of the branches above
-    branches: [ master ]
-  schedule:
-    - cron: '37 18 * * 5'
+on: [push]
 
 jobs:
   analyze:

From 75d25c48540317dafb47d07e4cc1bfb87e0db39d Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 23 Oct 2022 14:23:34 +0000
Subject: [PATCH 169/333] improve start-venv.sh (pass command line arguments)

---
 start-venv.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/start-venv.sh b/start-venv.sh
index be8b5888..33ac3216 100755
--- a/start-venv.sh
+++ b/start-venv.sh
@@ -5,4 +5,4 @@ if [ ! -f venv/bin/activate ] ; then
 fi
 source venv/bin/activate
 python3 -m pip install -r requirements.txt
-python3 zeronet.py
+python3 zeronet.py $1 $2 $3 $4 $5 $6 $7 $8 $9

From 79ffcac22d18525ac4f570813953569c8346618e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 14 Nov 2022 13:55:38 +0000
Subject: [PATCH 170/333] reduce fingerprinting information accessible to
 unprivileged sites

refs #163
---
 src/Config.py                |  1 +
 src/Connection/Connection.py |  2 +-
 src/Ui/UiWebsocket.py        | 86 +++++++++++++++++++++++-------------
 3 files changed, 57 insertions(+), 32 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index 1b621d5d..be0b873e 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -101,6 +101,7 @@ class Config(object):
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
         self.rev = 5036
+        self.user_agent_rev = 8192
         self.argv = argv
         self.action = None
         self.test_parser = None
diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py
index de95d867..879bcfab 100644
--- a/src/Connection/Connection.py
+++ b/src/Connection/Connection.py
@@ -369,7 +369,7 @@ class Connection(object):
             "fileserver_port": self.server.port,
             "port_opened": self.server.port_opened.get(self.ip_type, None),
             "target_ip": self.ip,
-            "rev": 8192,
+            "rev": config.user_agent_rev,
             "crypt_supported": crypt_supported,
             "crypt": self.crypt,
             "time": int(time.time())
diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index 48a30ee2..e4f98fca 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -288,38 +288,62 @@ class UiWebsocket(object):
         return ret
 
     def formatServerInfo(self):
-        import main
-        file_server = main.file_server
-        if file_server.port_opened == {}:
-            ip_external = None
+        # unprivileged sites should not get any fingerprinting information
+        if "ADMIN" in self.site.settings['permissions']:
+            import main
+            file_server = main.file_server
+            if file_server.port_opened == {}:
+                ip_external = None
+            else:
+                ip_external = any(file_server.port_opened.values())
+            back = {
+                'ip_external' : ip_external,
+                'port_opened' : file_server.port_opened,
+                'platform' : sys.platform,
+                'dist_type' : config.dist_type,
+                'fileserver_ip' : config.fileserver_ip,
+                'fileserver_port' : config.fileserver_port,
+                'tor_enabled' : file_server.tor_manager.enabled,
+                'tor_status' : file_server.tor_manager.status,
+                'tor_has_meek_bridges' : file_server.tor_manager.has_meek_bridges,
+                'tor_use_bridges' : config.tor_use_bridges,
+                'ui_ip' : config.ui_ip,
+                'ui_port' : config.ui_port,
+                'version' : config.version,
+                'rev' : config.rev,
+                'timecorrection' : file_server.timecorrection,
+                'language' : config.language,
+                'debug' : config.debug,
+                'offline' : config.offline,
+                'plugins' : PluginManager.plugin_manager.plugin_names,
+                'plugins_rev' : PluginManager.plugin_manager.plugins_rev,
+                'user_settings' : self.user.settings,
+                'lib_verify_best' : CryptBitcoin.lib_verify_best
+            }
         else:
-            ip_external = any(file_server.port_opened.values())
-        back = {
-            "ip_external": ip_external,
-            "port_opened": file_server.port_opened,
-            "platform": sys.platform,
-            "fileserver_ip": config.fileserver_ip,
-            "fileserver_port": config.fileserver_port,
-            "tor_enabled": file_server.tor_manager.enabled,
-            "tor_status": file_server.tor_manager.status,
-            "tor_has_meek_bridges": file_server.tor_manager.has_meek_bridges,
-            "tor_use_bridges": config.tor_use_bridges,
-            "ui_ip": config.ui_ip,
-            "ui_port": config.ui_port,
-            "version": config.version,
-            "rev": config.rev,
-            "timecorrection": file_server.timecorrection,
-            "language": config.language,
-            "debug": config.debug,
-            "offline": config.offline,
-            "plugins": PluginManager.plugin_manager.plugin_names,
-            "plugins_rev": PluginManager.plugin_manager.plugins_rev,
-            "user_settings": self.user.settings
-        }
-        if "ADMIN" in self.site.settings["permissions"]:
-            # back["updatesite"] = config.updatesite
-            back["dist_type"] = config.dist_type
-            back["lib_verify_best"] = CryptBitcoin.lib_verify_best
+            back = {
+                'ip_external' : None,
+                'port_opened' : False,
+                'platform' : 'generic',
+                'dist_type' : 'generic',
+                'fileserver_ip' : '127.0.0.1',
+                'fileserver_port' : 15441,
+                'tor_enabled' : True,
+                'tor_status' : 'OK',
+                'tor_has_meek_bridges' : True,
+                'tor_use_bridges' : True,
+                'ui_ip' : '127.0.0.1',
+                'ui_port' : 43110,
+                'version' : config.user_agent,
+                'rev' : config.user_agent_rev,
+                'timecorrection' : 0.0,
+                'language' : config.language, #?
+                'debug' : False,
+                'offline' : False,
+                'plugins' : [],
+                'plugins_rev' : {},
+                'user_settings' : self.user.settings #?
+            }
         return back
 
     def formatAnnouncerInfo(self, site):

From 9f8524f66d54696c403e55dee9c9a258f60fd031 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 14 Nov 2022 14:28:12 +0000
Subject: [PATCH 171/333] reduce fingerprinting information in siteInfo

refs #163
---
 src/Ui/UiWebsocket.py | 22 ++++++++++++++++++++--
 1 file changed, 20 insertions(+), 2 deletions(-)

diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index e4f98fca..896f5b7c 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -262,7 +262,14 @@ class UiWebsocket(object):
                 del(content["signers_sign"])
 
         settings = site.settings.copy()
-        del settings["wrapper_key"]  # Dont expose wrapper key
+        # remove fingerprinting information for non-admin sites
+        if 'ADMIN' not in self.site.settings['permissions']:
+            del settings['wrapper_key']
+            settings['added'] = 0
+            settings['serving'] = True
+            settings['ajax_key'] = ''
+            settings['peers'] = 1
+            settings['cache'] = {}
 
         ret = {
             "auth_address": self.user.getAuthAddress(site.address, create=create_user),
@@ -281,9 +288,20 @@ class UiWebsocket(object):
             "workers": len(site.worker_manager.workers),
             "content": content
         }
+        if 'ADMIN' not in self.site.settings['permissions']:
+            ret.update({
+                "content_updated": 0,
+                "bad_files": len(site.bad_files), # ?
+                "size_limit": site.getSizeLimit(), # ?
+                "next_size_limit": site.getNextSizeLimit(), # ?
+                "peers": 1,
+                "started_task_num": 0,
+                "tasks": 0,
+                "workers": 0,
+            })
         if site.settings["own"]:
             ret["privatekey"] = bool(self.user.getSiteData(site.address, create=create_user).get("privatekey"))
-        if site.isServing() and content:
+        if site.isServing() and content and "ADMIN" in self.site.settings['permissions']:
             ret["peers"] += 1  # Add myself if serving
         return ret
 

From f2884f3c7c7a188f1af4e136c5200b7e31c8f371 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 14 Nov 2022 14:32:13 +0000
Subject: [PATCH 172/333] reduce fingerprinting information: trackers

refs #163
---
 src/Ui/UiWebsocket.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index 896f5b7c..e982b990 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -365,7 +365,11 @@ class UiWebsocket(object):
         return back
 
     def formatAnnouncerInfo(self, site):
-        return {"address": site.address, "stats": site.announcer.stats}
+        if "ADMIN" in self.site.settings['permissions']:
+            stats = site.announcer.stats
+        else:
+            stats = {}
+        return {"address": site.address, "stats": stats}
 
     # - Actions -
 

From 21699d012e8e48cff447d458880bb489ee3b6c1a Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 14 Nov 2022 19:54:24 +0000
Subject: [PATCH 173/333] update CHANGELOG

---
 CHANGELOG.md | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95bac884..78d96378 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,18 @@
 ### zeronet-conservancy 0.7.7+
+maintainers: @caryoscelus
+- improve starting script
+- fix default ssl version to be secure
+- disable geoip-related ip address leak when in tor-only mode
+- windows os build/running instruction (WIP)
+- better command line parsing
+- ArchLinux AUR package
+- update android instruction (thanks oseido for reporting)
+- better browser launch handling
+- ability to add/remove from favourites from sidebar
+- NoNewSites plugin
+- show help message even when startup fails
+- fix plugin options handling regression
+- multiple code improvements
 
 ### zeronet-conservancy 0.7.7 (2022-07-27) (f40dbfeb2163b9902495ba)
 maintainers: @caryoscelus, @FraYoshi, @prtngn, @d47081 (ex @d4708)

From c9ea546321da7f805680449bdbcc1acf2d7ff6c5 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 14 Nov 2022 21:14:36 +0000
Subject: [PATCH 174/333] SideBar plugin: add self-onion in copy-nodes-ip

refs #161
---
 plugins/Sidebar/SidebarPlugin.py | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index ca4968cc..ddbd7cd6 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -12,6 +12,7 @@ import urllib.parse
 import gevent
 
 import util
+import main
 from Config import config
 from Plugin import PluginManager
 from Debug import Debug
@@ -115,11 +116,11 @@ class UiWebsocketPlugin(object):
             local_html = ""
 
         peer_ips = [peer.key for peer in site.getConnectablePeers(20, allow_private=False)]
+        self_onion = main.file_server.tor_manager.site_onions.get(site.address, None)
+        if self_onion is not None:
+            peer_ips.append(self_onion+'.onion')
         peer_ips.sort(key=lambda peer_ip: ".onion:" in peer_ip)
-        copy_link = "http://127.0.0.1:43110/%s/?zeronet_peers=%s" % (
-            site.content_manager.contents.get("content.json", {}).get("domain", site.address),
-            ",".join(peer_ips)
-        )
+        copy_link = f'http://127.0.0.1:43110/{site.address}/?zeronet_peers={",".join(peer_ips)}'
 
         body.append(_("""
             <li>

From dd46831e623803888d7a169055f060a21fd3e0d4 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 15 Nov 2022 09:16:38 +0000
Subject: [PATCH 175/333] update CHANGELOG

---
 CHANGELOG.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78d96378..5f49c4da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
 ### zeronet-conservancy 0.7.7+
 maintainers: @caryoscelus
+- improve copying peers from sidebar
+- reduce fingerprinting information available to unprivileged sites
 - improve starting script
 - fix default ssl version to be secure
 - disable geoip-related ip address leak when in tor-only mode

From 9228b4cbbfb4923b714d5099dec9883eb52dc1ca Mon Sep 17 00:00:00 2001
From: snyk-bot <snyk-bot@snyk.io>
Date: Tue, 15 Nov 2022 10:51:38 +0000
Subject: [PATCH 176/333] fix: requirements.txt to reduce vulnerabilities

The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-SETUPTOOLS-3113904
---
 requirements.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/requirements.txt b/requirements.txt
index e5cfb71e..4e8543b7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 gevent>=1.1.0
 msgpack>=0.4.4
+setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability

From cf6338f532627095361b6644a10836e58797039a Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 15 Nov 2022 20:01:59 +0000
Subject: [PATCH 177/333] update CHANGELOG

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f49c4da..83a43622 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ### zeronet-conservancy 0.7.7+
 maintainers: @caryoscelus
+- remove potential vulnerability via setuptools (@ajesse11x)
 - improve copying peers from sidebar
 - reduce fingerprinting information available to unprivileged sites
 - improve starting script

From bdd63b42d00fe6b415786f14bd8467449205f4b0 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 15 Nov 2022 20:15:48 +0000
Subject: [PATCH 178/333] add support for ada/cardano donation addresses in
 sidebar

refs #95
---
 plugins/Sidebar/SidebarPlugin.py | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index ddbd7cd6..ececb10b 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -453,7 +453,8 @@ class UiWebsocketPlugin(object):
         donate_generic = site.content_manager.contents.get("content.json", {}).get("donate", None) or site.content_manager.contents.get("content.json", {}).get("donate-generic", None)
         donate_btc = site.content_manager.contents.get("content.json", {}).get("donate-btc", None)
         donate_xmr = site.content_manager.contents.get("content.json", {}).get("donate-xmr", None)
-        donate_enabled = bool(donate_generic or donate_btc or donate_xmr)
+        donate_ada = site.content_manager.contents.get("content.json", {}).get("donate-ada", None)
+        donate_enabled = bool(donate_generic or donate_btc or donate_xmr or donate_ada)
         if donate_enabled:
             body.append(_("""
             <li>
@@ -483,6 +484,15 @@ class UiWebsocketPlugin(object):
               <a href='monero:{donate_xmr}' class='button'>{_[Donate Monero]}</a>
             </div>
             """))
+        if donate_ada:
+            body.append(_("""
+            <div class='flex'>
+              <span style="font-size:90%">{donate_ada}</span><br/>
+            </div>
+            <div class='flex'>
+              <a href='web+cardano:{donate_ada}' class='button'>{_[Donate Ada/Cardano]}</a>
+            </div>
+            """))
         if donate_enabled:
             body.append(_("""
             </li>

From b2acdc8e47cc5dc16e77f4391059be90138af2d5 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 20 Nov 2022 14:48:47 +0000
Subject: [PATCH 179/333] use archived version of .bit domain list (deprecated)
 by default

---
 plugins/Zeroname/SiteManagerPlugin.py | 2 +-
 src/Config.py                         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/plugins/Zeroname/SiteManagerPlugin.py b/plugins/Zeroname/SiteManagerPlugin.py
index 2553a50c..c25fafa1 100644
--- a/plugins/Zeroname/SiteManagerPlugin.py
+++ b/plugins/Zeroname/SiteManagerPlugin.py
@@ -63,7 +63,7 @@ class ConfigPlugin(object):
         group = self.parser.add_argument_group("Zeroname plugin")
         group.add_argument(
             "--bit_resolver", help="ZeroNet site to resolve .bit domains",
-            default="1Name2NXVi1RDPDgf5617UoW7xA6YrhM9F", metavar="address"
+            default="1GnACKctkJrGWHTqxk9T9zXo2bLQc2PDnF", metavar="address"
         )
 
         return super(ConfigPlugin, self).createArguments()
diff --git a/src/Config.py b/src/Config.py
index be0b873e..763d35c9 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -100,7 +100,7 @@ class Config(object):
         self.version = "0.7.7+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5036
+        self.rev = 5037
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None

From 4a6b9982b8af21077e045cb9ce270d19d57fb285 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 20 Nov 2022 17:00:18 +0000
Subject: [PATCH 180/333] update CHANGELOG

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83a43622..9f1f39fe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ### zeronet-conservancy 0.7.7+
 maintainers: @caryoscelus
+- readdress .bit domains as part of their deprecation
 - remove potential vulnerability via setuptools (@ajesse11x)
 - improve copying peers from sidebar
 - reduce fingerprinting information available to unprivileged sites

From 110307a4198cb13cc907ae209f8e869971504ca6 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 23 Nov 2022 11:18:07 +0000
Subject: [PATCH 181/333] v0.7.8

---
 CHANGELOG.md  | 1 +
 src/Config.py | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f1f39fe..8954a60b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ### zeronet-conservancy 0.7.7+
 maintainers: @caryoscelus
+- use archived version of .bit domain registry to avoid malicious rewrites
 - readdress .bit domains as part of their deprecation
 - remove potential vulnerability via setuptools (@ajesse11x)
 - improve copying peers from sidebar
diff --git a/src/Config.py b/src/Config.py
index 763d35c9..00a99263 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -97,7 +97,7 @@ trackers = [
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.7+"
+        self.version = "0.7.8"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
         self.rev = 5037

From 0475a39fe17281e04720dbd2ff61ef8d875dce99 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 23 Nov 2022 21:34:14 +0000
Subject: [PATCH 182/333] release commit hash in CHANGELOG

---
 CHANGELOG.md  | 4 +++-
 src/Config.py | 2 +-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8954a60b..aa013abf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,6 @@
-### zeronet-conservancy 0.7.7+
+### zeronet-conservancy 0.7.8+
+
+### zeronet-conservancy 0.7.8 (2022-11-23) (110307a4198cb13cc907ae)
 maintainers: @caryoscelus
 - use archived version of .bit domain registry to avoid malicious rewrites
 - readdress .bit domains as part of their deprecation
diff --git a/src/Config.py b/src/Config.py
index 00a99263..0f9c89b8 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -97,7 +97,7 @@ trackers = [
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.8"
+        self.version = "0.7.8+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
         self.rev = 5037

From f88260a7706784aef8d8e66fea84cb571a32f6fc Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 27 Nov 2022 09:33:48 +0000
Subject: [PATCH 183/333] minor code improvement

use format strings
---
 plugins/ContentFilter/ContentFilterPlugin.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py
index 9d8dc6e8..6cec1bc3 100644
--- a/plugins/ContentFilter/ContentFilterPlugin.py
+++ b/plugins/ContentFilter/ContentFilterPlugin.py
@@ -38,7 +38,7 @@ class SiteManagerPlugin(object):
                 block_details = None
 
         if block_details:
-            raise Exception("Site blocked: %s" % html.escape(block_details.get("reason", "unknown reason")))
+            raise Exception(f'Site blocked: {html.escape(block_details.get("reason", "unknown reason"))}')
         else:
             return super(SiteManagerPlugin, self).add(address, *args, **kwargs)
 
@@ -204,15 +204,15 @@ class SiteStoragePlugin(object):
             # Check if any of the adresses are in the mute list
             for auth_address in matches:
                 if filter_storage.isMuted(auth_address):
-                    self.log.debug("Mute match: %s, ignoring %s" % (auth_address, inner_path))
+                    self.log.debug(f'Mute match: {auth_address}, ignoring {inner_path}')
                     return False
 
         return super(SiteStoragePlugin, self).updateDbFile(inner_path, file=file, cur=cur)
 
     def onUpdated(self, inner_path, file=None):
-        file_path = "%s/%s" % (self.site.address, inner_path)
-        if file_path in filter_storage.file_content["includes"]:
-            self.log.debug("Filter file updated: %s" % inner_path)
+        file_path = f'{self.site.address}/{inner_path}'
+        if file_path in filter_storage.file_content['includes']:
+            self.log.debug('Filter file updated: {inner_path}')
             filter_storage.includeUpdateAll()
         return super(SiteStoragePlugin, self).onUpdated(inner_path, file=file)
 

From 32bd9bbc60d343821bd9f6f81011881d61400141 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 27 Nov 2022 09:56:53 +0000
Subject: [PATCH 184/333] more compact boot-logo

so that it doesn't spill on small screens
---
 zeronet.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/zeronet.py b/zeronet.py
index a9cbd7ed..9d9cec79 100755
--- a/zeronet.py
+++ b/zeronet.py
@@ -24,10 +24,10 @@ def fancy_greet():
     from rich.console import Console
     from rich.text import Text
     zc_msg = f'''
-|||  __. _.. _ . . _  _._|_     _. . . _ .-- _.. _.  . __.. _  _..  .
-|||   / /_||/ / \|/ |/_| |  == /  / \|/ | \ /_||/ |  | __||/ |/   \_|
-|||  /_.\_ |  \_/|  |\_  |.    \__\_/|  |._|\_ |   \/ |__||  |\__   |
-|||                                                                _/
+|||   . . _  _._|_     _. . . _ .__ _.. _.  . __.. _  __.  .
+|||  //\|/ |/_| |  == /  / \|/ |(  /_||/ |  | __||/ |/   \_|
+|||  \_/|  |\_  |.    \__\_/|  |_) \_ |   \/ |__||  |\__ _/
+|||
 |||  v{config.version}
 '''
     lns = zc_msg.split('\n')

From b6e18fd3738b4725726c5e170040deb3048c9048 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 27 Nov 2022 11:00:39 +0000
Subject: [PATCH 185/333] update README

---
 README.md | 21 ++++++++++++++++-----
 1 file changed, 16 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index 0b37d5f4..ed4d6190 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
 
 [![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
 
+(NOTE THAT TRANSLATIONS ARE USUALLY BEHIND THIS FILE)
+
 [по-русски](README-ru.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md)
 
 zeronet-conservancy is a fork/continuation of [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) project
@@ -38,13 +40,14 @@ brand new, completely transparent and audited network is ready and this project
  * Automatic uPnP port opening (if opted in)
  * Plugin for multiuser (openproxy) support
  * Works with any modern browser/OS
+ * Works offline and can be synced via alternative transports (or when connection is back)
 
 
 ## How does it work?
 
 * After starting `zeronet.py` you will be able to visit zeronet sites using
   `http://127.0.0.1:43110/{zeronet_address}` (eg.
-  `http://127.0.0.1:43110/126NXcevn1AUehWFZLTBw7FrX1crEizQdr`).
+  `http://127.0.0.1:43110/1MCoA8rQHhwu4LY2t2aabqcGSRqrL8uf2X/`).
 * When you visit a new zeronet site, it tries to find peers using the BitTorrent
   network so it can download the site files (html, css, js...) from them.
 * Each visited site is also served by you.
@@ -60,14 +63,14 @@ Following links relate to original ZeroNet:
 
 - [Slideshow about ZeroNet cryptography, site updates, multi-user sites »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
 - [Frequently asked questions »](https://zeronet.io/docs/faq/)
-- [ZeroNet Developer Documentation »](https://zeronet.io/docs/site_development/getting_started/)
+- [ZeroNet Developer Documentation »](https://zeronet.io/docs/site_development/getting_started/) (getting outdated)
 
 ## How to join
 
 ### Install from your distribution repository
 
 - NixOS: https://search.nixos.org/packages?channel=22.05&show=zeronet-conservancy&type=packages&query=zeronet-conservancy (and see below)
-- ArchLinux: https://aur.archlinux.org/packages/zeronet-conservancy-git (fresh git version)
+- ArchLinux: [latest release](https://aur.archlinux.org/packages/zeronet-conservancy), [fresh git version](https://aur.archlinux.org/packages/zeronet-conservancy-git)
 
 ### Install from Nix package manager (Linux or MacOS)
 
@@ -161,14 +164,16 @@ Install autoconf and other basic development tools, python3 and pip, then procee
 * Doesn't work directly from browser (one of the top priorities for mid-future)
 * No data transparency
 * No reproducible builds
+* No on-disk encryption
+* No reproducible builds (hence no builds beyond certain GNU/Linux distributions)
 
 
 ## How can I create a ZeroNet site?
 
- * Click on **⋮** > **"Create new, empty site"** menu item on the [admin page](http://127.0.0.1:43110/126NXcevn1AUehWFZLTBw7FrX1crEizQdr).
+ * Click on **⋮** > **"Create new, empty site"** menu item on the [dashboard](http://127.0.0.1:43110/191CazMVNaAcT9Y1zhkxd9ixMBPs59g2um/).
  * You will be **redirected** to a completely new site that is only modifiable by you!
  * You can find and modify your site's content in **data/[yoursiteaddress]** directory
- * After the modifications open your site, drag the topright "0" button to the left, then press **sign** and **publish** buttons on the bottom
+ * After the modifications open your site, drag the topright "0" button to the left, then press **sign and publish** button on the bottom
 
 Next steps: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_development/getting_started/)
 
@@ -179,6 +184,12 @@ Next steps: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_devel
 We need more maintainers! Become one today! You don't need to know how to code,
 there's a lot of other work to do.
 
+### Make builds for your platforms
+
+We need reproducible stand-alone builds for major platforms, as well as presense in various FLOSS
+repositories. If you're using one of Linux distributions which don't have packages yet, why not make
+a package for it or (if you don't know how) ask a maintainer now?
+
 ### Fix bugs & add features
 
 We've decided to go ahead and make a perfect p2p web, so we need more help

From 119352a685fd7bd397906be3ac1507d76f4c298f Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 28 Nov 2022 09:13:32 +0000
Subject: [PATCH 186/333] Revert "remove unused code"

This reverts commit dc804b9d5f3a2a9f1fffa1b97d82e0e04c44508b.

fixes #182
---
 src/Site/Site.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/Site/Site.py b/src/Site/Site.py
index 7deab5cc..ea19c4a2 100644
--- a/src/Site/Site.py
+++ b/src/Site/Site.py
@@ -35,6 +35,7 @@ class Site(object):
     def __init__(self, address, allow_create=True, settings=None):
         self.address = str(re.sub("[^A-Za-z0-9]", "", address))  # Make sure its correct address
         self.address_hash = hashlib.sha256(self.address.encode("ascii")).digest()
+        self.address_sha1 = hashlib.sha1(self.address.encode("ascii")).digest()
         self.address_short = "%s..%s" % (self.address[:6], self.address[-4:])  # Short address for logging
         self.log = logging.getLogger("Site:%s" % self.address_short)
         self.addEventListeners()

From f02e57697304c19c04f2a4412a54f62455f7d1e3 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 28 Nov 2022 09:28:52 +0000
Subject: [PATCH 187/333] code comment

---
 src/Config.py    | 2 +-
 src/Site/Site.py | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/Config.py b/src/Config.py
index 0f9c89b8..9149d165 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -100,7 +100,7 @@ class Config(object):
         self.version = "0.7.8+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5037
+        self.rev = 5038
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None
diff --git a/src/Site/Site.py b/src/Site/Site.py
index ea19c4a2..ffdb2bb0 100644
--- a/src/Site/Site.py
+++ b/src/Site/Site.py
@@ -35,6 +35,7 @@ class Site(object):
     def __init__(self, address, allow_create=True, settings=None):
         self.address = str(re.sub("[^A-Za-z0-9]", "", address))  # Make sure its correct address
         self.address_hash = hashlib.sha256(self.address.encode("ascii")).digest()
+        # sha1 is used for clearnet trackers
         self.address_sha1 = hashlib.sha1(self.address.encode("ascii")).digest()
         self.address_short = "%s..%s" % (self.address[:6], self.address[-4:])  # Short address for logging
         self.log = logging.getLogger("Site:%s" % self.address_short)

From b5a80504a5329fe67ded6e7baa44171403238591 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 28 Nov 2022 12:58:48 +0000
Subject: [PATCH 188/333] GiveUpGitHub notice

we're not ready to move on yet, but we're on the path!
---
 README.md | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/README.md b/README.md
index ed4d6190..f3d1810b 100644
--- a/README.md
+++ b/README.md
@@ -220,3 +220,26 @@ private, a Monero wallet:
 
 If you want to donate in a different way, feel free to contact maintainer or
 create an issue
+
+# We're using GitHub under protest
+
+This project is currently hosted on GitHub. This is not ideal; GitHub is a
+proprietary, trade-secret system that is not Free/Libre and Open Souce Software
+(FLOSS). We are deeply concerned about using a proprietary system like GitHub
+to develop our FLOSS project. We have an
+[open issue](https://github.com/zeronet-conservancy/zeronet-conservancy/issues/89)
+to track moving away from GitHub in the long term.  We urge you to read about the
+[Give up GitHub](https://GiveUpGitHub.org) campaign from
+[the Software Freedom Conservancy](https://sfconservancy.org) to understand
+some of the reasons why GitHub is not a good place to host FOSS projects.
+
+If you are a contributor who personally has already quit using GitHub, feel
+free to [check out from our mirror on notabug](https://notabug.org/caryoscelus/zeronet-conservancy)
+and develop there or send git patches directly to project maintainer via
+preffered [contact channel](https://caryoscelus.github.io/contacts/).
+
+Any use of this project's code by GitHub Copilot, past or present, is done
+without our permission. We do not consent to GitHub's use of this project's
+code in Copilot.
+
+![Logo of the GiveUpGitHub campaign](https://sfconservancy.org/img/GiveUpGitHub.png)

From b1f2560037f40930cae1a3f6fee0d432c6025082 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 28 Nov 2022 13:03:51 +0000
Subject: [PATCH 189/333] update CHANGELOG

preparing v0.7.8.1
---
 CHANGELOG.md  | 9 ++++++++-
 src/Config.py | 2 +-
 2 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa013abf..f74757a9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,11 @@
-### zeronet-conservancy 0.7.8+
+### zeronet-conservancy 0.7.8.1+
+
+### zeronet-conservancy 0.7.8.1 (2022-11-28)
+maintainers: @caryoscelus
+- fix tracker connection regression introduced in dc804b9d5f3a2a9f1fffa1b97d82e0e04c44508b
+- GiveUpGitHub notice
+- update README
+- new, more compact boot logo in console (more suitable for small screens)
 
 ### zeronet-conservancy 0.7.8 (2022-11-23) (110307a4198cb13cc907ae)
 maintainers: @caryoscelus
diff --git a/src/Config.py b/src/Config.py
index 9149d165..e46a2c68 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -100,7 +100,7 @@ class Config(object):
         self.version = "0.7.8+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5038
+        self.rev = 5039
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None

From 32042a330eb90293cb3cd20836908c9e93050c47 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 28 Nov 2022 15:47:48 +0000
Subject: [PATCH 190/333] fix favourite/unfavourite in sidebar

refs #146
---
 plugins/Sidebar/SidebarPlugin.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index ececb10b..c117de0e 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -422,7 +422,7 @@ class UiWebsocketPlugin(object):
             print('No dashboard found, cannot favourite')
             class_favourite = "hidden"
             class_unfavourite = "hidden"
-        elif dsite.get('sittings', {}).get('favorite_sites', {}).get(self.site.address, False):
+        elif not dsite.get('settings', {}).get('favorite_sites', {}).get(self.site.address, False):
             class_favourite = ""
             class_unfavourite = "hidden"
         else:

From 0054eca9df0c9c8c2f4a7837461a7d001f996c2e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 28 Nov 2022 15:53:08 +0000
Subject: [PATCH 191/333] v0.7.8.1

update CHANGELOG & bump revision
---
 CHANGELOG.md  | 3 ++-
 src/Config.py | 4 ++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f74757a9..e9858232 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,8 @@
 
 ### zeronet-conservancy 0.7.8.1 (2022-11-28)
 maintainers: @caryoscelus
-- fix tracker connection regression introduced in dc804b9d5f3a2a9f1fffa1b97d82e0e04c44508b
+- fix favourite/unfavourite in sidebar
+- fix tracker connection regression introduced in dc804b9d5f3a2a9f1fffa1b97d82e0e04c44508b (thanks to @bitcoren)
 - GiveUpGitHub notice
 - update README
 - new, more compact boot logo in console (more suitable for small screens)
diff --git a/src/Config.py b/src/Config.py
index e46a2c68..db498f89 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -97,10 +97,10 @@ trackers = [
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.8+"
+        self.version = "0.7.8.1"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5039
+        self.rev = 5040
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None

From 0d9a00cf6512c1bb0b24ab3ec680bedce57a5379 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 29 Nov 2022 09:57:39 +0000
Subject: [PATCH 192/333] release version hash

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9858232..9f5c9062 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
 ### zeronet-conservancy 0.7.8.1+
 
-### zeronet-conservancy 0.7.8.1 (2022-11-28)
+### zeronet-conservancy 0.7.8.1 (2022-11-28) (0054eca9df0c9c8c2f4a78)
 maintainers: @caryoscelus
 - fix favourite/unfavourite in sidebar
 - fix tracker connection regression introduced in dc804b9d5f3a2a9f1fffa1b97d82e0e04c44508b (thanks to @bitcoren)

From 7c73d7543c74b9d82f579164fffc6167db71e9d6 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 29 Nov 2022 16:39:21 +0000
Subject: [PATCH 193/333] minor code improvement

use format strings for readability
---
 src/Content/ContentManager.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index e43fef2b..d6086e19 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -793,7 +793,7 @@ class ContentManager(object):
         return 1  # Todo: Multisig
 
     def verifyCertSign(self, user_address, user_auth_type, user_name, issuer_address, sign):
-        cert_subject = "%s#%s/%s" % (user_address, user_auth_type, user_name)
+        cert_subject = f'{user_address}#{user_auth_type}/{user_name}'
         return CryptBitcoin.verify(cert_subject, issuer_address, sign)
 
     def verifyCert(self, inner_path, content):

From 423dd46c67e7909125b949791472ca61f027cec3 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 29 Nov 2022 16:39:42 +0000
Subject: [PATCH 194/333] update README

---
 README.md | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index f3d1810b..df1d39e4 100644
--- a/README.md
+++ b/README.md
@@ -120,12 +120,13 @@ Install autoconf and other basic development tools, python3 and pip, then procee
  - `python3 zeronet.py`
 
 #### (alternatively) Build Docker image
-- build 0net image: `docker build -t 0net:conservancy . -f Dockerfile`
-- or build 0net image with integrated tor: `docker build -t 0net:conservancy . -f Dockerfile.integrated_tor`
-- and run it: `docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net:conservancy`
+- build 0net image: `docker build -t 0net-conservancy:latest . -f Dockerfile`
+- or build 0net image with integrated tor: `docker build -t 0net-conservancy:latest . -f Dockerfile.integrated_tor`
+- and run it: `docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net-conservancy:latest`
 - /path/to/0n/data/directory - directory, where all data will be saved, including your secret certificates. If you run it with production mode, do not remove this folder!
-- or you can run it with docker-compose: `docker compose up -d 0net` up two containers - 0net and tor separately.
+- or you can run it with docker-compose: `docker compose up -d 0net-conservancy` up two containers - 0net and tor separately.
 - or: `docker compose up -d 0net-tor` for run 0net and tor in one container.
+(please check if these instructions are still accurate)
 
 #### Alternative script
  - after installing general dependencies and cloning repo (as above), run `start-venv.sh` which will create a virtual env for you and install python requirements

From 06db221bbd5d6710a5655af507ecbb56a72adf3f Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 29 Nov 2022 16:40:08 +0000
Subject: [PATCH 195/333] update README-ru

---
 README-ru.md | 29 ++++++++++++++++-------------
 1 file changed, 16 insertions(+), 13 deletions(-)

diff --git a/README-ru.md b/README-ru.md
index 2546431e..1c9df426 100644
--- a/README-ru.md
+++ b/README-ru.md
@@ -42,7 +42,7 @@ zeronet-conservancy — это форк/продолжение проекта [Z
 
 * После запуска `zeronet.py` вы сможете посетить zeronet сайты используя адрес
   `http://127.0.0.1:43110/{zeronet_address}`
-(например. `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`).
+(например. `http://127.0.0.1:43110/1MCoA8rQHhwu4LY2t2aabqcGSRqrL8uf2X`).
 * Когда вы посещаете новый сайт zeronet, он пытается найти пиров с помощью BitTorrent
   чтобы загрузить файлы сайтов (html, css, js ...) из них.
 * Каждый посещенный зайт также обслуживается вами. (Т.е хранится у вас на компьютере)
@@ -65,6 +65,7 @@ zeronet-conservancy — это форк/продолжение проекта [Z
 ### Установить из репозитория вашего дистрибутива
 
 - NixOS: https://search.nixos.org/packages?channel=22.05&show=zeronet-conservancy&type=packages&query=zeronet-conservancy
+- ArchLinux: [последний релиз](https://aur.archlinux.org/packages/zeronet-conservancy), [git-версия](https://aur.archlinux.org/packages/zeronet-conservancy-git)
 
 ### Установить из исходного кода (рекомендовано)
 
@@ -85,7 +86,11 @@ zeronet-conservancy — это форк/продолжение проекта [Z
  - (optional) `pkg install tor`
  - (optional) запустить тор через команду `tor --ControlPort 9051 --CookieAuthentication 1` (вы можете открыть новый сеанс свайпом вправо)
 
-#### Создание зависимостей Python и запуск
+#### Скрипт, который всё сделает за вас
+ - после установки общих зависимостей и клонирования репозитория (как указано выше) запустите `start-venv.sh` который создаст для вас виртуальную среду (если её ещё нет) и установит необходимые пакеты Python
+ - больше удобных скриптов будует добавлено в ближайшее время
+
+#### Установка Python-зависимостей и запуск
  - клонируйте репозиторий (NOTE: на Android/Termux вы должны клонировать его в «домашнюю» папку Termux, потому что виртуальная среда не может находиться в `storage/`)
  - `python3 -m venv venv` (создайте виртуальную среду python, последнее `venv` это просто имя/название, если вы используете другое, вы должны заменить его в более поздних командах.)
  - `source venv/bin/activate` (активируйте среду)
@@ -96,18 +101,14 @@ zeronet-conservancy — это форк/продолжение проекта [Z
  - `source venv/bin/activate`
  - `python3 zeronet.py`
 
-#### Создание образа Docker
-- создание образа: `docker build -t 0net:conservancy . -f Dockerfile`
-- или создрание образа с встроенным tor: `docker build -t 0net:conservancy . -f Dockerfile.integrated_tor`
-- и его запуск: `docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net:conservancy`
+#### (альтернативно) Создание образа Docker
+- создание образа: `docker build -t 0net-conservancy:latest . -f Dockerfile`
+- или создрание образа с встроенным tor: `docker build -t 0net-conservancy:latest . -f Dockerfile.integrated_tor`
+- и его запуск: `docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net-conservancy:latest`
 - /path/to/0n/data/directory - директория, куда будут сохраняться все данные в том числе секретные ключи. Если вы запускаете в боевом режиме, не потеряйте эту папку!
-- или вы можете воспользоваться docker-compose: `docker compose up -d 0net` запускает два контейнера раздельно, для 0net и tor сервисов.
+- или вы можете воспользоваться docker-compose: `docker compose up -d 0net-conservancy` запускает два контейнера раздельно, для 0net и tor сервисов.
 - или: `docker compose up -d 0net-tor` запускает один контейнер с tor и 0net.
 
-#### альтернативный скрипт
- - после установки общих зависимостей и клонирования репозитория (как указано выше) запустите `start-venv.sh` который создаст для вас виртуальную среду и установит требования Python
- - больше удобных скриптов будует добавлено в ближайшее время
-
 ## Текущие ограничения 
 
 * Файловые транзакции не сжаты
@@ -158,8 +159,10 @@ zeronet-conservancy — это форк/продолжение проекта [Z
 также создаст командные аккаунты на дружественных краудфандинговых платформах.
 
 Если вы хотите, чтобы ваше пожертвование было признано пожертвованием для этого
-проекта, для этого также есть специальный биткойн-адрес:
-1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6
+проекта, для этого также есть специальный биткоин-адрес:
+1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6. Либо если хотите сделать более анонимный донат, вы
+можете пожертвовать Monero:
+4AiYUcqVRH4C2CVr9zbBdkhRnJnHiJoypHEsq4N7mQziGUoosPCpPeg8SPr87nvwypaRzDgMHEbWWDekKtq8hm9LBmgcMzC
 
 Если вы хотите сделать пожертвование другим способом, не стесняйтесь обращаться к сопровождающему или
 создать запрос

From 03a9f2c49026aaab659f5395083bbf35db39b6bb Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 29 Nov 2022 16:40:46 +0000
Subject: [PATCH 196/333] new development cycle

---
 src/Config.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index db498f89..f4f48619 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -97,10 +97,10 @@ trackers = [
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.8.1"
+        self.version = "0.7.8.1+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5040
+        self.rev = 5041
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None

From 8616af3f2e7592ff17ebf76e13ede79f3051a914 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 1 Dec 2022 15:27:55 +0000
Subject: [PATCH 197/333] remove mention of python-3.6 in requirements.txt

after analyzing the old version of requirements.txt, it becomes
obvious that any real python 3.6.X versions would fail to
install required package, and since no one complained that means
there are no <py3.7 users now
---
 requirements.txt | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/requirements.txt b/requirements.txt
index 79a7f349..960da11a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,5 @@
-gevent==1.4.0; python_version <= "3.6"
-greenlet==0.4.16; python_version <= "3.6"
-gevent>=20.9.0; python_version >= "3.7"
+setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability
+gevent>=20.9.0
 msgpack>=0.4.4
 base58
 merkletools
@@ -14,4 +13,3 @@ maxminddb
 rich
 defusedxml>=0.7
 pyaes
-setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability

From 2eac2d3f3533d5b39574370c257d8fc10b7768de Mon Sep 17 00:00:00 2001
From: chncaption <101684156+chncaption@users.noreply.github.com>
Date: Sun, 4 Dec 2022 16:06:54 +0800
Subject: [PATCH 198/333] update msgpack 0.4.4 to 0.6.0

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index b3df57ea..c46f20f4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
 gevent==1.4.0; python_version <= "3.6"
 greenlet==0.4.16; python_version <= "3.6"
 gevent>=20.9.0; python_version >= "3.7"
-msgpack>=0.4.4
+msgpack>=0.6.0
 base58
 merkletools
 rsa

From fa1c25326a5c40f6cf47e58024284316a8fc0f0b Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 4 Dec 2022 10:30:54 +0000
Subject: [PATCH 199/333] report error when peer rejects our update

also improve code and comment
---
 src/Peer/Peer.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py
index e525b421..ec48f0fc 100644
--- a/src/Peer/Peer.py
+++ b/src/Peer/Peer.py
@@ -154,7 +154,7 @@ class Peer(object):
 
         self.log("Send request: %s %s %s %s" % (params.get("site", ""), cmd, params.get("inner_path", ""), params.get("location", "")))
 
-        for retry in range(1, 4):  # Retry 3 times
+        for retry in range(3):
             try:
                 if not self.connection:
                     # this is redundant, already established that self.connection is present
@@ -165,7 +165,7 @@ class Peer(object):
                 if "error" in res:
                     self.log("%s error: %s" % (cmd, res["error"]))
                     self.onConnectionError("Response error")
-                    break
+                    return res
                 else:  # Successful request, reset connection error num
                     self.connection_error = 0
                 self.time_response = time.time()
@@ -183,9 +183,9 @@ class Peer(object):
                         "%s (connection_error: %s, hash_failed: %s, retry: %s)" %
                         (Debug.formatException(err), self.connection_error, self.hash_failed, retry)
                     )
-                    time.sleep(1 * retry)
+                    time.sleep(retry+1)
                     self.connect()
-        return None  # Failed after 4 retry
+        return None  # Failed after 3 attempts
 
     # Get a file content from peer
     def getFile(self, site, inner_path, file_size=None, pos_from=0, pos_to=None, streaming=False):

From 0fa90c5d176377fdbc1c6b083d65a64c9a7f3dda Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 5 Dec 2022 10:45:26 +0000
Subject: [PATCH 200/333] update CHANGELOG

---
 CHANGELOG.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f5c9062..2140701d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,7 @@
 ### zeronet-conservancy 0.7.8.1+
+- better debugging of update non-propagation
+- sec update of msgpck dependency (@chncaption)
+- deprecate python-3.6 as it apparently is no longer used (by active users)
 
 ### zeronet-conservancy 0.7.8.1 (2022-11-28) (0054eca9df0c9c8c2f4a78)
 maintainers: @caryoscelus

From ab9fc61efc8e179fddbf9627e8fc2c4559f79d65 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 5 Dec 2022 16:50:10 +0000
Subject: [PATCH 201/333] Update README (NixOS dev instructions)

thanks @fgaz for explanations

fixes #189
---
 README.md | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index df1d39e4..4a10fdf6 100644
--- a/README.md
+++ b/README.md
@@ -87,7 +87,7 @@ if you're on NixOS
 
 (thanks @fgaz for making & maintaining the package)
 
-### Install from source (recommended)
+### Install from source
 
 #### System dependencies
 
@@ -108,7 +108,7 @@ Install autoconf and other basic development tools, python3 and pip, then procee
  - (optional) `pkg install tor`
  - (optional) run tor via `tor --ControlPort 9051 --CookieAuthentication 1` command (you can then open new session by swiping to the right)
 
-#### Building python dependencies & running
+#### Building python dependencies venv & running
  - clone this repo (NOTE: on Android/Termux you should clone it into "home" folder of Termux, because virtual environment cannot live in `storage/`)
  - `python3 -m venv venv` (make python virtual environment, the last `venv` is just a name, if you use different you should replace it in later commands)
  - `source venv/bin/activate` (activate environment)
@@ -119,6 +119,11 @@ Install autoconf and other basic development tools, python3 and pip, then procee
  - `source venv/bin/activate`
  - `python3 zeronet.py`
 
+#### (alternatively) On NixOS
+- clone this repo
+- `nix-shell '<nixpkgs>' -A zeronet-conservancy` to enter shell with installed dependencies
+- `./zeronet.py`
+
 #### (alternatively) Build Docker image
 - build 0net image: `docker build -t 0net-conservancy:latest . -f Dockerfile`
 - or build 0net image with integrated tor: `docker build -t 0net-conservancy:latest . -f Dockerfile.integrated_tor`

From 30bf28df12e4c8b9a29f8c5f811ea1603763e9fc Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 5 Dec 2022 16:52:54 +0000
Subject: [PATCH 202/333] update CHANGELOG

---
 CHANGELOG.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2140701d..32e431fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,6 @@
 ### zeronet-conservancy 0.7.8.1+
+maintainers: @caryoscelus
+- update README (build/dev instructions; thanks to @fgaz)
 - better debugging of update non-propagation
 - sec update of msgpck dependency (@chncaption)
 - deprecate python-3.6 as it apparently is no longer used (by active users)

From da10d2fb762ae96dfe6758d58ff956b7dcc33ee6 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 6 Dec 2022 12:38:05 +0000
Subject: [PATCH 203/333] update README (windows instruction minor change)

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 4a10fdf6..fe0ead3f 100644
--- a/README.md
+++ b/README.md
@@ -151,7 +151,7 @@ Install autoconf and other basic development tools, python3 and pip, then procee
 - `cd zeronet-conservancy`
 - `python -m venv venv` (create virtual python environment)
 - `venv\Scripts\activate` (this activates the environment)
-- `pip install -r requirements.txt` (install python dependencies)
+- `pip install -r requirements.txt` (install python dependencies) (some users reported that this command doesn't successfully install requirements and only manual installation of dependencies one by one works)
 - (NOTE: if previous step fails, it most likely means you haven't installed c/c++ compiler successfully)
 - [optional for tor for better connectivity and anonymity] launch Tor Browser
 - (NOTE: windows might show a window saying it blocked access to internet for "security reasons" — you should allow the access)

From 091323d4e34096d4e4dd8d466de080008c3a4e0b Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 6 Dec 2022 15:59:52 +0000
Subject: [PATCH 204/333] TODO comments

---
 src/Ui/UiWebsocket.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index e982b990..e6f2f405 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -511,6 +511,8 @@ class UiWebsocket(object):
 
     # Sign and publish content.json
     def actionSitePublish(self, to, privatekey=None, inner_path="content.json", sign=True, remove_missing_optional=False, update_changed_files=False):
+        # TODO: check certificates (https://github.com/zeronet-conservancy/zeronet-conservancy/issues/190)
+        # TODO: update certificates (https://github.com/zeronet-conservancy/zeronet-conservancy/issues/194)
         if sign:
             inner_path = self.actionSiteSign(
                 to, privatekey, inner_path, response_ok=False,

From 5e39f37fba7dfaf9b14e4cb9e5179d7c22786a2f Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 6 Dec 2022 16:59:18 +0000
Subject: [PATCH 205/333] docker-ignore data/ directory

---
 .dockerignore | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.dockerignore b/.dockerignore
index df655837..06de9748 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,2 +1,3 @@
 venv
 Dockerfile*
+data

From 3c7670a70327e164c119bad54b2bfd4970931fa1 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 10 Dec 2022 16:39:18 +0000
Subject: [PATCH 206/333] fix debug messages

---
 src/Content/ContentManager.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index d6086e19..350370d0 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -85,16 +85,16 @@ class ContentManager(object):
                     new_ts = int(float(new_content.get('modified', 0)))
                     old_ts = int(float(old_content.get('modified', 0)))
                     if new_ts < old_ts:
-                        self.log.debug('got older version of {content_inner_path} ({new_ts} < {old_ts}), ignoring')
+                        self.log.debug(f'got older version of {content_inner_path} ({new_ts} < {old_ts}), ignoring')
                         return [], []
                     elif new_ts == old_ts:
-                        self.log.debug('got same timestamp version of {content_inner_path} ({new_ts}), ignoring')
+                        self.log.debug(f'got same timestamp version of {content_inner_path} ({new_ts}), ignoring')
                         return [], []
             except Exception as err:
                 self.log.warning(f'{content_path} load error: {Debug.formatException(err)}')
                 return [], []
         else:
-            self.log.debug("Content.json not exist: %s" % content_path)
+            self.log.debug(f'Content.json not exist: {content_path}')
             return [], []  # Content.json not exist
 
         try:

From b5380f6b260588416ca897758e073c2a97417d0a Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 11 Dec 2022 17:48:37 +0000
Subject: [PATCH 207/333] Fix /raw readdress

fixes #199
---
 src/Ui/UiRequest.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 1b2d5cf3..d30ff4e3 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -176,7 +176,7 @@ class UiRequest(object):
             return self.actionConsole()
         # Wrapper-less static files
         elif path.startswith("/raw/"):
-            return self.actionSiteMedia(path.replace("/raw", "/media", 1), header_noscript=True)
+            return self.actionSiteMedia(path.replace("/raw", "/media", 1), header_noscript=True, raw=True)
 
         elif path.startswith("/add/"):
             return self.actionSiteAdd()
@@ -647,7 +647,7 @@ class UiRequest(object):
             return None
 
     # Serve a media for site
-    def actionSiteMedia(self, path, header_length=True, header_noscript=False):
+    def actionSiteMedia(self, path, header_length=True, header_noscript=False, raw=False):
         try:
             path_parts = self.parsePath(path)
         except SecurityError as err:
@@ -657,7 +657,8 @@ class UiRequest(object):
             addr = path_parts['address']
             path = path_parts['inner_path']
             query = self.env['QUERY_STRING']
-            return self.actionRedirect(f"/{addr}/{path}?{query}")
+            raw = "/raw" if raw else ""
+            return self.actionRedirect(f"{raw}/{addr}/{path}?{query}")
 
         if not path_parts:
             return self.error404(path)

From 26d7e17c32d93c92899d9693c22506ef933f0993 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 3 Jan 2023 08:04:13 +0000
Subject: [PATCH 208/333] modularize greeting

---
 greet.py   | 35 +++++++++++++++++++++++++++++++++++
 zeronet.py | 39 ++-------------------------------------
 2 files changed, 37 insertions(+), 37 deletions(-)
 create mode 100644 greet.py

diff --git a/greet.py b/greet.py
new file mode 100644
index 00000000..d918bd6c
--- /dev/null
+++ b/greet.py
@@ -0,0 +1,35 @@
+def grad(n):
+    s = 0x08
+    r = 0xff
+    g = 0x00
+    b = 0x00
+    for i in range(n):
+        if r >= s and b < s:
+            r -= s
+            g += s
+        elif g >= s and r < s:
+            g -= s
+            b += s
+        elif b >= s and g < s:
+            b -= s
+            r += s
+    return f'#{r:02x}{g:02x}{b:02x}'
+
+def fancy_greet(version):
+    from rich.console import Console
+    from rich.text import Text
+    zc_msg = f'''
+|||   . . _  _._|_     _. . . _ .__ _.. _.  . __.. _  __.  .
+|||  //\|/ |/_| |  == /  / \|/ |(  /_||/ |  | __||/ |/   \_|
+|||  \_/|  |\_  |.    \__\_/|  |_) \_ |   \/ |__||  |\__ _/
+|||
+|||  v{version}
+'''
+    lns = zc_msg.split('\n')
+    console = Console()
+    for l in lns:
+        txt = Text(l)
+        txt.stylize('bold')
+        for i in range(len(l)):
+            txt.stylize(grad(i), i, i+1)
+        console.print(txt)
diff --git a/zeronet.py b/zeronet.py
index 9d9cec79..1106c925 100755
--- a/zeronet.py
+++ b/zeronet.py
@@ -3,49 +3,14 @@ import os
 import sys
 from src.Config import config
 
-def grad(n):
-    s = 0x08
-    r = 0xff
-    g = 0x00
-    b = 0x00
-    for i in range(n):
-        if r >= s and b < s:
-            r -= s
-            g += s
-        elif g >= s and r < s:
-            g -= s
-            b += s
-        elif b >= s and g < s:
-            b -= s
-            r += s
-    return f'#{r:02x}{g:02x}{b:02x}'
-
-def fancy_greet():
-    from rich.console import Console
-    from rich.text import Text
-    zc_msg = f'''
-|||   . . _  _._|_     _. . . _ .__ _.. _.  . __.. _  __.  .
-|||  //\|/ |/_| |  == /  / \|/ |(  /_||/ |  | __||/ |/   \_|
-|||  \_/|  |\_  |.    \__\_/|  |_) \_ |   \/ |__||  |\__ _/
-|||
-|||  v{config.version}
-'''
-    lns = zc_msg.split('\n')
-    console = Console()
-    for l in lns:
-        txt = Text(l)
-        txt.stylize('bold')
-        for i in range(len(l)):
-            txt.stylize(grad(i), i, i+1)
-        console.print(txt)
-
 def main():
     if sys.version_info.major < 3:
         print("Error: Python 3.x is required")
         sys.exit(0)
 
     if '--silent' not in sys.argv:
-        fancy_greet()
+        from greet import fancy_greet
+        fancy_greet(config.version)
 
     main = None
     try:

From 8706f5f712faef07afa96b69b538ade38ba610d3 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 14 Mar 2023 12:18:42 +0000
Subject: [PATCH 209/333] revert circular import style in Site/SiteManager

fixes #203
---
 src/Site/SiteManager.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py
index 5c051a9f..d7ba6e94 100644
--- a/src/Site/SiteManager.py
+++ b/src/Site/SiteManager.py
@@ -14,7 +14,6 @@ from Config import config
 from util import helper
 from util import RateLimit
 from util import Cached
-from .Site import Site
 from Debug import Debug
 
 @PluginManager.acceptPlugins
@@ -31,6 +30,7 @@ class SiteManager(object):
     # Load all sites from data/sites.json
     @util.Noparallel()
     def load(self, cleanup=True, startup=False):
+        from .Site import Site
         self.log.info("Loading sites... (cleanup: %s, startup: %s)" % (cleanup, startup))
         self.loaded = False
         address_found = []
@@ -169,6 +169,7 @@ class SiteManager(object):
         return site
 
     def add(self, address, all_file=True, settings=None, **kwargs):
+        from .Site import Site
         self.sites_changed = int(time.time())
         # Try to find site with differect case
         for recover_address, recover_site in list(self.sites.items()):

From 62d1c9d27a2f63b02c8acc71aea10a9e639432f2 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 2 Jul 2023 03:35:34 +0000
Subject: [PATCH 210/333] improve zeronet.py: don't reimport config and fix
 name clash

previously due to zeronet.py and most of the source files living in
different import 'namespaces', Config module was imported twice.
this if fixed by editing sys.modules
---
 CHANGELOG.md | 1 +
 zeronet.py   | 9 ++++++---
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32e431fb..ce2cb131 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ maintainers: @caryoscelus
 - better debugging of update non-propagation
 - sec update of msgpck dependency (@chncaption)
 - deprecate python-3.6 as it apparently is no longer used (by active users)
+- improvement in imports and naming (@caryoscelus)
 
 ### zeronet-conservancy 0.7.8.1 (2022-11-28) (0054eca9df0c9c8c2f4a78)
 maintainers: @caryoscelus
diff --git a/zeronet.py b/zeronet.py
index 1106c925..6f0e63b9 100755
--- a/zeronet.py
+++ b/zeronet.py
@@ -3,7 +3,11 @@ import os
 import sys
 from src.Config import config
 
-def main():
+# fix further imports from src dir
+sys.modules['Config'] = sys.modules['src.Config']
+
+def launch():
+    '''renamed from main to avoid clashes with main module'''
     if sys.version_info.major < 3:
         print("Error: Python 3.x is required")
         sys.exit(0)
@@ -12,7 +16,6 @@ def main():
         from greet import fancy_greet
         fancy_greet(config.version)
 
-    main = None
     try:
         import main
         main.start()
@@ -131,7 +134,7 @@ def start():
         import update
         update.update()
     else:
-        main()
+        launch()
 
 
 if __name__ == '__main__':

From e36f7bb3a5ad74024c0bc539429e59acc88977c8 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 2 Jul 2023 03:37:46 +0000
Subject: [PATCH 211/333] siteSign accepts absolute paths as well as paths
 relative to working directory

- also store working_dir in config so it's possible to use from other
  actions as well

fixes #209
---
 CHANGELOG.md |  1 +
 src/main.py  | 13 +++++++++++++
 zeronet.py   |  1 +
 3 files changed, 15 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ce2cb131..86a9aa72 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ maintainers: @caryoscelus
 - sec update of msgpck dependency (@chncaption)
 - deprecate python-3.6 as it apparently is no longer used (by active users)
 - improvement in imports and naming (@caryoscelus)
+- siteSign accepts absolute paths as well as paths relative to working directory (@caryoscelus)
 
 ### zeronet-conservancy 0.7.8.1 (2022-11-28) (0054eca9df0c9c8c2f4a78)
 maintainers: @caryoscelus
diff --git a/src/main.py b/src/main.py
index a5e15070..b4b656db 100644
--- a/src/main.py
+++ b/src/main.py
@@ -234,6 +234,19 @@ class Actions(object):
                 # Not found in users.json, ask from console
                 import getpass
                 privatekey = getpass.getpass("Private key (input hidden):")
+        # inner_path can be either relative to site directory or absolute/relative path
+        if os.path.isabs(inner_path):
+            full_path = os.path.abspath(inner_path)
+        else:
+            full_path = os.path.abspath(config.working_dir + '/' + inner_path)
+        print(full_path)
+        if os.path.isfile(full_path):
+            if address in full_path:
+                # assuming site address is unique, keep only path after it
+                inner_path = full_path.split(address+'/')[1]
+            else:
+                # oops, file that we found seems to be rogue, so reverting to old behaviour
+                logging.warning(f'using {inner_path} relative to site directory')
         try:
             succ = site.content_manager.sign(
                 inner_path=inner_path, privatekey=privatekey,
diff --git a/zeronet.py b/zeronet.py
index 6f0e63b9..bb53404f 100755
--- a/zeronet.py
+++ b/zeronet.py
@@ -123,6 +123,7 @@ def restart():
 
 
 def start():
+    config.working_dir = os.getcwd()
     app_dir = os.path.dirname(os.path.abspath(__file__))
     os.chdir(app_dir)  # Change working dir to zeronet.py dir
     sys.path.insert(0, os.path.join(app_dir, "src/lib"))  # External liblary directory

From 2cd22720e8883d3e9cb840e92fbba188ae8a5d18 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 2 Jul 2023 03:40:57 +0000
Subject: [PATCH 212/333] properly attribute @imachug in CHANGELOG

this was not done initially to avoid spreading the vulnerability information
before users updated
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 86a9aa72..e380eae1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -100,6 +100,7 @@ maintainers: @caryoscelus
 ### zeronet-conservancy 0.7.3 (2022-01-21) Rev5000
 maintainers: @caryoscelus
 - forked from the latest py3 branch of ZeroNet
+- fixed potential vulnerability discovered by @imachug
 - onion v3 support (thanks to @anonymoose, @zeroseed and @geekless)
 - partial readme rewrite (thanks to @mitya57)
 - disable updating through zite (unsafe)

From 053eb8e7d6e83a332cffb5a93a7dd229d431d3c4 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 2 Jul 2023 03:45:59 +0000
Subject: [PATCH 213/333] updated trackers from Syncronite by @Styromaniac

---
 CHANGELOG.md  |  1 +
 src/Config.py | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 88 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e380eae1..91e45147 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ maintainers: @caryoscelus
 - deprecate python-3.6 as it apparently is no longer used (by active users)
 - improvement in imports and naming (@caryoscelus)
 - siteSign accepts absolute paths as well as paths relative to working directory (@caryoscelus)
+- updated trackers from Syncronite by @Styromaniac
 
 ### zeronet-conservancy 0.7.8.1 (2022-11-28) (0054eca9df0c9c8c2f4a78)
 maintainers: @caryoscelus
diff --git a/src/Config.py b/src/Config.py
index f4f48619..b6824771 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -92,6 +92,93 @@ trackers = [
     'udp://vibe.sleepyinternetfun.xyz:1738/announce',
     'udp://www.skynetcenter.me:6969/announce',
     'udp://www.torrent.eu.org:451/announce',
+    'zero://194.5.98.39:15441',
+    'zero://145.239.95.38:15441',
+    'zero://178.128.34.249:26117',
+    'zero://217.18.217.143:39288',
+    'zero://83.246.141.203:22207',
+    'zero://syncronite.loki:15441',
+    'zero://2a05:dfc1:4000:1e00::a:15441',
+    'zero://2400:6180:100:d0::8fd:8001:21697',
+    'zero://2001:19f0:8001:1d2f:5400:2ff:fe83:5bf7:30530',
+    'zero://73pyhfwfwsrhfw76knkjfnw6o3lk53zfo7hlxdmxbj75sjcnol5cioad.onion:15442',
+    'zero://fzlzmxuz2bust72cuy5g4w6d62tx624xcjaupf2kp7ffuitbiniy2hqd.onion:15441',
+    'zero://rlcjomszyitxpwv7kzopmqgzk3bdpsxeull4c3s6goszkk6h2sotfoad.onion:15441',
+    'zero://tqmo2nffqo4qc5jgmz3me5eri3zpgf3v2zciufzmhnvznjve5c3argad.onion:15441',
+    'http://107.189.31.134:6969/announce',
+    'http://119.28.71.45:8080/announce',
+    'http://129.146.193.240:6699/announce',
+    'http://159.69.65.157:6969/announce',
+    'http://163.172.29.130:80/announce',
+    'http://185.130.47.2:6969/announce',
+    'http://45.67.35.111:6969/announce',
+    'http://61.222.178.254:6969/announce',
+    'http://83.31.30.182:6969/announce',
+    'http://93.158.213.92:1337/announce',
+    'http://95.217.167.10:6969/announce',
+    'udp://102.223.180.235:6969/announce',
+    'udp://103.122.21.50:6969/announce',
+    'udp://104.131.98.232:6969/announce',
+    'udp://104.244.77.87:6969/announce',
+    'udp://107.189.11.58:6969/announce',
+    'udp://107.189.31.134:6969/announce',
+    'udp://139.144.68.88:6969/announce',
+    'udp://149.28.239.70:6969/announce',
+    'udp://15.204.205.14:6969/announce',
+    'udp://156.234.201.18:80/announce',
+    'udp://158.101.161.60:3131/announce',
+    'udp://163.172.29.130:80/announce',
+    'udp://167.99.185.219:6969/announce',
+    'udp://176.31.250.174:6969/announce',
+    'udp://176.56.4.238:6969/announce',
+    'udp://178.32.222.98:3391/announce',
+    'udp://184.105.151.166:6969/announce',
+    'udp://185.102.219.163:6969/announce',
+    'udp://185.181.60.155:80/announce',
+    'udp://185.217.199.21:6969/announce',
+    'udp://185.44.82.25:1337/announce',
+    'udp://185.68.21.244:6969/announce',
+    'udp://192.3.165.191:6969/announce',
+    'udp://192.3.165.198:6969/announce',
+    'udp://192.95.46.115:6969/announce',
+    'udp://193.176.158.162:6969/announce',
+    'udp://193.37.214.12:6969/announce',
+    'udp://193.42.111.57:9337/announce',
+    'udp://198.100.149.66:6969/announce',
+    'udp://20.100.205.229:6969/announce',
+    'udp://207.241.226.111:6969/announce',
+    'udp://207.241.231.226:6969/announce',
+    'udp://209.141.59.16:6969/announce',
+    'udp://212.237.53.230:6969/announce',
+    'udp://23.153.248.2:6969/announce',
+    'udp://23.254.228.89:6969/announce',
+    'udp://37.187.111.136:6969/announce',
+    'udp://37.27.4.53:6969/announce',
+    'udp://38.7.201.142:6969/announce',
+    'udp://45.154.253.6:6969/announce',
+    'udp://45.63.30.114:6969/announce',
+    'udp://45.9.60.30:6969/announce',
+    'udp://46.38.238.105:6969/announce',
+    'udp://49.12.76.8:8080/announce',
+    'udp://5.102.159.190:6969/announce',
+    'udp://5.196.89.204:6969/announce',
+    'udp://51.15.79.209:6969/announce',
+    'udp://51.159.54.68:6666/announce',
+    'udp://51.68.174.87:6969/announce',
+    'udp://51.81.222.188:6969/announce',
+    'udp://52.58.128.163:6969/announce',
+    'udp://61.222.178.254:6969/announce',
+    'udp://77.73.69.230:6969/announce',
+    'udp://83.102.180.21:80/announce',
+    'udp://83.31.30.182:6969/announce',
+    'udp://85.206.172.159:6969/announce',
+    'udp://85.239.33.28:6969/announce',
+    'udp://86.57.161.157:6969/announce',
+    'udp://91.216.110.52:451/announce',
+    'udp://93.158.213.92:1337/announce',
+    'udp://94.103.87.87:6969/announce',
+    'udp://95.216.74.39:6969/announce',
+    'udp://95.31.11.224:6969/announce',
 ]
 
 class Config(object):

From f966a4203fe33bd9f35695ee89893f5938f569e0 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 2 Jul 2023 03:46:37 +0000
Subject: [PATCH 214/333] v0.7.9

no longer officially maintained
---
 CHANGELOG.md  | 7 +++++--
 README.md     | 8 +++++++-
 src/Config.py | 4 ++--
 3 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 91e45147..378cb429 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
-### zeronet-conservancy 0.7.8.1+
-maintainers: @caryoscelus
+### zeronet-conservancy 0.7.9+
+
+### zeronet-conservancy 0.7.9 (2023-07-02)
+maintainers: @caryoscelus -> none
 - update README (build/dev instructions; thanks to @fgaz)
 - better debugging of update non-propagation
 - sec update of msgpck dependency (@chncaption)
@@ -7,6 +9,7 @@ maintainers: @caryoscelus
 - improvement in imports and naming (@caryoscelus)
 - siteSign accepts absolute paths as well as paths relative to working directory (@caryoscelus)
 - updated trackers from Syncronite by @Styromaniac
+- no longer officially maintained
 
 ### zeronet-conservancy 0.7.8.1 (2022-11-28) (0054eca9df0c9c8c2f4a78)
 maintainers: @caryoscelus
diff --git a/README.md b/README.md
index fe0ead3f..3d5a64f2 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,12 @@ zeronet-conservancy is a fork/continuation of [ZeroNet](https://github.com/Hello
 (that has been abandoned by its creator) that is dedicated to sustaining existing p2p network and developing
 its values of decentralization and freedom, while gradually switching to a better designed network
 
+## No active maintainer warning
+
+This fork was created and maintained by @caryoscelus, but due to vanishing interest and in order to avoid having
+another one-person project, they stepped down. This means there currently is no active maintainer (you're are
+welcome to become one!), however some development might still happen.
+
 ## Why fork?
 
 During onion-v3 switch crisis, we needed a fork that worked with onion-v3 and didn't depend on trust to one or
@@ -213,7 +219,7 @@ need to know their alternatives.
 
 ### Financially support maintainers
 
-Currently the lead developer / maintainer of this fork is @caryoscelus. You can
+This fork was created and maintained by @caryoscelus. You can
 see ways to donate to them on https://caryoscelus.github.io/donate/ (or check
 sidebar if you're reading this on github for more ways). As our team grows, we
 will create team accounts on friendly crowdfunding platforms as well.
diff --git a/src/Config.py b/src/Config.py
index b6824771..f19b9bbe 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -184,10 +184,10 @@ trackers = [
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.8.1+"
+        self.version = "0.7.9"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5041
+        self.rev = 5100
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None

From 8355b82eeff154c1fd5716ea88649605ce9334ef Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 3 Jul 2023 15:19:30 +0000
Subject: [PATCH 215/333] CHANGELOG: re-attribute @purplesyringa's
 contributions to her new nickname

https://github.com/zeronet-conservancy/zeronet-conservancy/commit/2cd22720e8883d3e9cb840e92fbba188ae8a5d18
---
 CHANGELOG.md | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 378cb429..4fc71e22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -104,7 +104,7 @@ maintainers: @caryoscelus
 ### zeronet-conservancy 0.7.3 (2022-01-21) Rev5000
 maintainers: @caryoscelus
 - forked from the latest py3 branch of ZeroNet
-- fixed potential vulnerability discovered by @imachug
+- fixed potential vulnerability discovered by @purplesyringa
 - onion v3 support (thanks to @anonymoose, @zeroseed and @geekless)
 - partial readme rewrite (thanks to @mitya57)
 - disable updating through zite (unsafe)
@@ -193,7 +193,7 @@ maintainers: shortcutme a.k.a nofish a.k.a HelloZeroNet a.k.a Tamas Kocsis
  - Link to site's sidebar with "#ZeroNet:OpenSidebar" hash
 
 ### Changed
- - Allow .. in file names [Thanks to imachug]
+ - Allow .. in file names [Thanks to purplesyringa]
  - Change unstable trackers
  - More clean errors on sites.json/users.json load error
  - Various tweaks for tracker rating on unstable connections
@@ -204,12 +204,12 @@ maintainers: shortcutme a.k.a nofish a.k.a HelloZeroNet a.k.a Tamas Kocsis
 
 ### Fixed
  - Fix parsing config lines that have no value
- - Fix start.py [Thanks to imachug]
+ - Fix start.py [Thanks to purplesyringa]
  - Allow multiple values of the same key in the config file [Thanks ssdifnskdjfnsdjk for reporting]
  - Fix parsing config file lines that has % in the value [Thanks slrslr for reporting]
  - Fix bootstrapper plugin hash reloads [Thanks geekless for reporting]
  - Fix CryptMessage plugin OpenSSL dll loading on Windows (ZeroMail errors) [Thanks cxgreat2014 for reporting]
- - Fix startup error when using OpenSSL 1.1 [Thanks to imachug]
+ - Fix startup error when using OpenSSL 1.1 [Thanks to purplesyringa]
  - Fix a bug that did not loaded merged site data for 5 sec after the merged site got added
  - Fix typo that allowed to add new plugins in public proxy mode. [Thanks styromaniac for reporting]
  - Fix loading non-big files with "|all" postfix [Thanks to krzotr]
@@ -232,10 +232,10 @@ Note: The fix is also back ported to ZeroNet Py 2.x version (Rev3870)
  - Generated SSL certificate randomization to avoid protocol filters (Thanks to ValdikSS)
  - Offline mode
  - P2P source code update using ZeroNet protocol
- - ecdsaSign/Verify commands to CryptMessage plugin (Thanks to imachug)
+ - ecdsaSign/Verify commands to CryptMessage plugin (Thanks to purplesyringa)
  - Efficient file rename: change file names instead of re-downloading the file.
  - Make redirect optional on site cloning (Thanks to Lola)
- - EccPrivToPub / EccPubToPriv functions (Thanks to imachug)
+ - EccPrivToPub / EccPubToPriv functions (Thanks to purplesyringa)
  - Detect and change dark/light theme based on OS setting (Thanks to filips123)
 
 ### Changed
@@ -254,7 +254,7 @@ Note: The fix is also back ported to ZeroNet Py 2.x version (Rev3870)
  - Fix site download as zip file
  - Fix displaying sites with utf8 title
  - Error message if dbRebuild fails (Thanks to Lola)
- - Fix browser reopen if executing start.py again. (Thanks to imachug)
+ - Fix browser reopen if executing start.py again. (Thanks to purplesyringa)
 
 
 ### ZeroNet 0.6.5 (2019-02-16) Rev3851 (Last release targeting Python 2.7.x)
@@ -343,7 +343,7 @@ Affected versions: All versions before ZeroNet Rev3616
  - Detect network level tracker blocking and easy setting meek proxy for tracker connections.
  - Support downloading 2GB+ sites as .zip (Thx to Radtoo)
  - Support ZeroNet as a transparent proxy (Thx to JeremyRand)
- - Allow fileQuery as CORS command (Thx to imachug)
+ - Allow fileQuery as CORS command (Thx to purplesyringa)
  - Windows distribution includes Tor and meek client by default
  - Download sites as zip link to sidebar
  - File server port randomization
@@ -406,7 +406,7 @@ Affected versions: All versions before ZeroNet Rev3616
 ### Added
  - New plugin: Chart
  - Collect and display charts about your contribution to ZeroNet network
- - Allow list as argument replacement in sql queries. (Thanks to imachug)
+ - Allow list as argument replacement in sql queries. (Thanks to purplesyringa)
  - Newsfeed query time statistics (Click on "From XX sites in X.Xs on ZeroHello)
  - New UiWebsocket API command: As to run commands as other site
  - Ranged ajax queries for big files
@@ -427,7 +427,7 @@ Affected versions: All versions before ZeroNet Rev3616
  - Only zoom sidebar globe if mouse button is pressed down
 
 ### Fixed
- - Open port checking error reporting (Thanks to imachug)
+ - Open port checking error reporting (Thanks to purplesyringa)
  - Out-of-range big file requests
  - Don't output errors happened on gevent greenlets twice
  - Newsfeed skip sites with no database
@@ -507,7 +507,7 @@ Affected versions: All versions before ZeroNet Rev3616
  - Opened port checking (Thanks l5h5t7 & saber28 for reporting)
  - Standalone update.py argument parsing (Thanks Zalex for reporting)
  - uPnP crash on startup (Thanks Vertux for reporting)
- - CoffeeScript 1.12.6 compatibility (Thanks kavamaken & imachug)
+ - CoffeeScript 1.12.6 compatibility (Thanks kavamaken & purplesyringa)
  - Multi value argument parsing
  - Database error when running from directory that contains special characters (Thanks Pupiloho for reporting)
  - Site lock violation logging

From d16c71966b119c3c456e0614e32615eb9fa0f008 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 3 Jul 2023 21:19:40 +0000
Subject: [PATCH 216/333] fix ReDoS in file editor (UiFileManager plugin) due
 to outdated codemirror

just patched from updated version, ideally codemirror dependency should be
included during build stage, but there's no infrastructure for that (yet)
---
 CHANGELOG.md                                              | 3 ++-
 plugins/UiFileManager/media/codemirror/all.js             | 5 ++++-
 plugins/UiFileManager/media/codemirror/mode/javascript.js | 5 ++++-
 3 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4fc71e22..2aa0a146 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 ### zeronet-conservancy 0.7.9+
+- fixed ReDoS in file editor (UiFileManager plugin) due to outdated codemirror (@caryoscelus)
 
-### zeronet-conservancy 0.7.9 (2023-07-02)
+### zeronet-conservancy 0.7.9 (2023-07-02) (f966a4203fe33bd9f35)
 maintainers: @caryoscelus -> none
 - update README (build/dev instructions; thanks to @fgaz)
 - better debugging of update non-propagation
diff --git a/plugins/UiFileManager/media/codemirror/all.js b/plugins/UiFileManager/media/codemirror/all.js
index ef2a423a..4b87e42d 100644
--- a/plugins/UiFileManager/media/codemirror/all.js
+++ b/plugins/UiFileManager/media/codemirror/all.js
@@ -17366,7 +17366,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
           var kw = keywords[word]
           return ret(kw.type, kw.style, word)
         }
-        if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/, false))
+	// backported ReDoS fix from
+	// https://github.com/codemirror/codemirror5/blob/a0854c752a76e4ba9512a9beedb9076f36e4f8f9/mode/javascript/javascript.js#L130C36-L130C36
+	// https://security.snyk.io/vuln/SNYK-JS-CODEMIRROR-1016937
+	if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false))
           return ret("async", "keyword", word)
       }
       return ret("variable", "variable", word)
diff --git a/plugins/UiFileManager/media/codemirror/mode/javascript.js b/plugins/UiFileManager/media/codemirror/mode/javascript.js
index 9c751d23..ba590d18 100644
--- a/plugins/UiFileManager/media/codemirror/mode/javascript.js
+++ b/plugins/UiFileManager/media/codemirror/mode/javascript.js
@@ -126,7 +126,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
           var kw = keywords[word]
           return ret(kw.type, kw.style, word)
         }
-        if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/, false))
+	// backported ReDoS fix from
+	// https://github.com/codemirror/codemirror5/blob/a0854c752a76e4ba9512a9beedb9076f36e4f8f9/mode/javascript/javascript.js#L130C36-L130C36
+	// https://security.snyk.io/vuln/SNYK-JS-CODEMIRROR-1016937
+	if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false))
           return ret("async", "keyword", word)
       }
       return ret("variable", "variable", word)

From 1f19ab604e54563a20330b66933205cd87e44915 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 5 Jul 2023 15:03:44 +0000
Subject: [PATCH 217/333] update merkletools dep

---
 CHANGELOG.md     | 3 ++-
 requirements.txt | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2aa0a146..9186fbb5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ### zeronet-conservancy 0.7.9+
-- fixed ReDoS in file editor (UiFileManager plugin) due to outdated codemirror (@caryoscelus)
+- update merkletools dependency to avoid legacy pysha3 (@caryoscelus)
+- fix ReDoS in file editor (UiFileManager plugin) due to outdated codemirror (@caryoscelus)
 
 ### zeronet-conservancy 0.7.9 (2023-07-02) (f966a4203fe33bd9f35)
 maintainers: @caryoscelus -> none
diff --git a/requirements.txt b/requirements.txt
index 4444b3f4..4298ed61 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,8 @@ setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerabil
 gevent>=20.9.0
 msgpack>=0.6.0
 base58
-merkletools
+# for some reason nobody released fresh merkletools that don't require on outdated pysha3
+git+https://github.com/Tierion/pymerkletools.git@f10d71e2cd529a833728e836dc301f9af502d0b0
 rsa
 PySocks>=1.6.8
 pyasn1

From e8f83590eeac2ae156d0f5f9a409b32f40bff2f1 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 18 Jul 2023 14:38:17 +0000
Subject: [PATCH 218/333] disable plugins in data dir

---
 src/Plugin/PluginManager.py | 33 ++-------------------------------
 1 file changed, 2 insertions(+), 31 deletions(-)

diff --git a/src/Plugin/PluginManager.py b/src/Plugin/PluginManager.py
index dbafa98f..5855c842 100644
--- a/src/Plugin/PluginManager.py
+++ b/src/Plugin/PluginManager.py
@@ -17,7 +17,6 @@ class PluginManager:
     def __init__(self):
         self.log = logging.getLogger("PluginManager")
         self.path_plugins = os.path.abspath(os.path.dirname(plugins.__file__))
-        self.path_installed_plugins = config.data_dir + "/__plugins__"
         self.plugins = defaultdict(list)  # Registered plugins (key: class name, value: list of plugins for class)
         self.subclass_order = {}  # Record the load order of the plugins, to keep it after reload
         self.pluggable = {}
@@ -93,34 +92,6 @@ class PluginManager:
             plugin["loaded"] = plugin_name in self.plugin_names
             plugins.append(plugin)
 
-        plugins += self.listInstalledPlugins(list_disabled)
-        return plugins
-
-    def listInstalledPlugins(self, list_disabled=False):
-        plugins = []
-
-        for address, site_plugins in sorted(self.config.items()):
-            if address == "builtin":
-                continue
-            for plugin_inner_path, plugin_config in sorted(site_plugins.items()):
-                is_enabled = plugin_config.get("enabled", False)
-                if not is_enabled and not list_disabled:
-                    continue
-                plugin_name = os.path.basename(plugin_inner_path)
-
-                dir_path = "%s/%s/%s" % (self.path_installed_plugins, address, plugin_inner_path)
-
-                plugin = {}
-                plugin["source"] = address
-                plugin["name"] = plugin_name
-                plugin["dir_name"] = plugin_name
-                plugin["dir_path"] = dir_path
-                plugin["inner_path"] = plugin_inner_path
-                plugin["enabled"] = is_enabled
-                plugin["rev"] = plugin_config.get("rev", 0)
-                plugin["loaded"] = plugin_name in self.plugin_names
-                plugins.append(plugin)
-
         return plugins
 
     # Load all plugin
@@ -156,10 +127,10 @@ class PluginManager:
         for module_name, module in list(sys.modules.items()):
             if not module or not getattr(module, "__file__", None):
                 continue
-            if self.path_plugins not in module.__file__ and self.path_installed_plugins not in module.__file__:
+            if self.path_plugins not in module.__file__:
                 continue
 
-            if "allow_reload" in dir(module) and not module.allow_reload:  # Reload disabled
+            if not getattr(module, 'allow_reload', True):  # Reload disabled
                 # Re-add non-reloadable plugins
                 for class_name, classes in self.plugins_before.items():
                     for c in classes:

From 8b7d7d5f7c425ad1415b45a172919f172c18c10a Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 18 Jul 2023 19:40:45 +0000
Subject: [PATCH 219/333] bump version

---
 src/Config.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index f19b9bbe..9f5de779 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -184,10 +184,10 @@ trackers = [
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.9"
+        self.version = "0.7.9+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5100
+        self.rev = 5110
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None

From 3dc22837184a33ff874eea79f9dab14ed7bdbb26 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 18 Jul 2023 19:41:22 +0000
Subject: [PATCH 220/333] allow setting permission_rules to null to forbid
 everything

---
 src/Content/ContentManager.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index 350370d0..bcf96c0b 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -514,6 +514,11 @@ class ContentManager(object):
         for permission_pattern, permission_rules in list(user_contents["permission_rules"].items()):  # Regexp rules
             if not SafeRe.match(permission_pattern, user_urn):
                 continue  # Rule is not valid for user
+            if permission_rules is None:
+                self.log.info(f'Permission rule for {permission_pattern} is null, '
+                               'we set max_size[_optional] to zero. '
+                               'NOTE: This is not supported by <0.7.10')
+                permission_rules = {'max_size': 0, 'max_size_optional': 0}
             # Update rules if its better than current recorded ones
             for key, val in permission_rules.items():
                 if key not in rules:

From 32e8f70878713d886772d64fad5aa431cd53314e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 18 Jul 2023 20:21:57 +0000
Subject: [PATCH 221/333] fix workflow file

---
 .github/workflows/tests.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 5b80bff1..67e067fe 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,6 +1,7 @@
 name: tests
 
-on: []
+on:
+  workflow_dispatch:
 
 jobs:
   test:

From b7e3f10a85771da040f6b142b34e95950cce9bce Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 20 Jul 2023 03:37:18 +0000
Subject: [PATCH 222/333] allow manual codeql dispatch

---
 .github/workflows/codeql-analysis.yml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 9e592adf..f467e83d 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -11,7 +11,9 @@
 #
 name: "CodeQL"
 
-on: [push]
+on:
+  push:
+  workflow_dispatch:
 
 jobs:
   analyze:

From 8700351f3315ad7bd2abb593f112b8c075899257 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 20 Jul 2023 16:16:54 +0000
Subject: [PATCH 223/333] remove unused broken code

---
 src/Content/ContentDbDict.py | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/src/Content/ContentDbDict.py b/src/Content/ContentDbDict.py
index 01df0427..7e1bca05 100644
--- a/src/Content/ContentDbDict.py
+++ b/src/Content/ContentDbDict.py
@@ -95,17 +95,6 @@ class ContentDbDict(dict):
             back.append((key, val))
         return back
 
-    def values(self):
-        back = []
-        for key, val in dict.iteritems(self):
-            if not val:
-                try:
-                    val = self.loadItem(key)
-                except Exception:
-                    continue
-            back.append(val)
-        return back
-
     def get(self, key, default=None):
         try:
             return self.__getitem__(key)

From c09b5e16c9f4646b46cb6ed3ab8202217fc4613f Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 20 Jul 2023 16:29:04 +0000
Subject: [PATCH 224/333] sitePublish --recursive option

---
 src/Config.py |  2 ++
 src/main.py   | 70 ++++++++++++++++++++++++++++++---------------------
 2 files changed, 44 insertions(+), 28 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index 9f5de779..8a101cde 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -309,6 +309,8 @@ class Config(object):
                             default=15441, nargs='?')
         action.add_argument('--inner_path', help='Content.json you want to publish (default: content.json)',
                             default="content.json", metavar="inner_path")
+        action.add_argument('--recursive', help="Whether to publish all of site's content.json. "
+                            "Overrides --inner_path. (default: false)", action='store_true', dest='recursive')
 
         # SiteVerify
         action = self.subparsers.add_parser("siteVerify", help='Verify site files using sha512: address')
diff --git a/src/main.py b/src/main.py
index b4b656db..57735ae2 100644
--- a/src/main.py
+++ b/src/main.py
@@ -422,47 +422,61 @@ class Actions(object):
         ws = websocket.create_connection(ws_address)
         return ws
 
-    def sitePublish(self, address, peer_ip=None, peer_port=15441, inner_path="content.json"):
-        global file_server
-        from Site.Site import Site
+    def sitePublish(self, address, peer_ip=None, peer_port=15441, inner_path="content.json", recursive=False):
         from Site import SiteManager
-        from File import FileServer  # We need fileserver to handle incoming file requests
-        from Peer import Peer
-        file_server = FileServer()
-        site = SiteManager.site_manager.get(address)
         logging.info("Loading site...")
+        site = SiteManager.site_manager.get(address)
         site.settings["serving"] = True  # Serving the site even if its disabled
 
+        if not recursive:
+            inner_paths = [inner_path]
+        else:
+            inner_paths = list(site.content_manager.contents.keys())
+
         try:
             ws = self.getWebsocket(site)
+
+        except Exception as err:
+            self.sitePublishFallback(site, peer_ip, peer_port, inner_paths, err)
+
+        else:
             logging.info("Sending siteReload")
             self.siteCmd(address, "siteReload", inner_path)
 
-            logging.info("Sending sitePublish")
-            self.siteCmd(address, "sitePublish", {"inner_path": inner_path, "sign": False})
+            for inner_path in inner_paths:
+                logging.info(f"Sending sitePublish for {inner_path}")
+                self.siteCmd(address, "sitePublish", {"inner_path": inner_path, "sign": False})
             logging.info("Done.")
+            ws.close()
 
-        except Exception as err:
-            logging.info("Can't connect to local websocket client: %s" % err)
-            logging.info("Creating FileServer....")
-            file_server_thread = gevent.spawn(file_server.start, check_sites=False)  # Dont check every site integrity
-            time.sleep(0.001)
+    def sitePublishFallback(self, site, peer_ip, peer_port, inner_paths, err):
+        if err is not None:
+            logging.info(f"Can't connect to local websocket client: {err}")
+        logging.info("Publish using fallback mechanism. "
+                     "Note that there might be not enough time for peer discovery, "
+                     "but you can specify target peer on command line.")
+        logging.info("Creating FileServer....")
+        file_server_thread = gevent.spawn(file_server.start, check_sites=False)  # Dont check every site integrity
+        time.sleep(0.001)
 
-            # Started fileserver
-            file_server.portCheck()
-            if peer_ip:  # Announce ip specificed
-                site.addPeer(peer_ip, peer_port)
-            else:  # Just ask the tracker
-                logging.info("Gathering peers from tracker")
-                site.announce()  # Gather peers
+        # Started fileserver
+        file_server.portCheck()
+        if peer_ip:  # Announce ip specificed
+            site.addPeer(peer_ip, peer_port)
+        else:  # Just ask the tracker
+            logging.info("Gathering peers from tracker")
+            site.announce()  # Gather peers
+
+        for inner_path in inner_paths:
             published = site.publish(5, inner_path)  # Push to peers
-            if published > 0:
-                time.sleep(3)
-                logging.info("Serving files (max 60s)...")
-                gevent.joinall([file_server_thread], timeout=60)
-                logging.info("Done.")
-            else:
-                logging.info("No peers found, sitePublish command only works if you already have visitors serving your site")
+
+        if published > 0:
+            time.sleep(3)
+            logging.info("Serving files (max 60s)...")
+            gevent.joinall([file_server_thread], timeout=60)
+            logging.info("Done.")
+        else:
+            logging.info("No peers found, sitePublish command only works if you already have visitors serving your site")
 
     # Crypto commands
     def cryptPrivatekeyToAddress(self, privatekey=None):

From 260c4e626bc9021ae3e92d80b920ff9fa44c0fc2 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 20 Jul 2023 18:28:04 +0000
Subject: [PATCH 225/333] make tests launch

---
 src/Test/conftest.py | 3 ---
 src/Test/pytest.ini  | 2 +-
 2 files changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/Test/conftest.py b/src/Test/conftest.py
index c8739086..57c61f3a 100644
--- a/src/Test/conftest.py
+++ b/src/Test/conftest.py
@@ -63,9 +63,6 @@ config.debug = True
 
 os.chdir(os.path.abspath(os.path.dirname(__file__) + "/../.."))  # Set working dir
 
-all_loaded = PluginManager.plugin_manager.loadPlugins()
-assert all_loaded, "Not all plugin loaded successfully"
-
 config.loadPlugins()
 config.parse(parse_config=False)  # Parse again to add plugin configuration options
 
diff --git a/src/Test/pytest.ini b/src/Test/pytest.ini
index 556389a2..0ffb385f 100644
--- a/src/Test/pytest.ini
+++ b/src/Test/pytest.ini
@@ -1,6 +1,6 @@
 [pytest]
 python_files = Test*.py
-addopts = -rsxX -v --durations=6 --no-print-logs --capture=fd
+addopts = -rsxX -v --durations=6 --capture=fd
 markers =
     slow: mark a tests as slow.
     webtest: mark a test as a webtest.

From 51a6eaa057f07deda864355fd2bc62d19072f1a0 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 20 Jul 2023 18:28:21 +0000
Subject: [PATCH 226/333] fix threadpool test

---
 src/Test/TestThreadPool.py | 11 +++--------
 1 file changed, 3 insertions(+), 8 deletions(-)

diff --git a/src/Test/TestThreadPool.py b/src/Test/TestThreadPool.py
index 5e95005e..5e71155d 100644
--- a/src/Test/TestThreadPool.py
+++ b/src/Test/TestThreadPool.py
@@ -15,11 +15,9 @@ class TestThreadPool:
             @pool.wrap
             def blocker():
                 events.append("S")
-                out = 0
-                for i in range(10000000):
-                    if i == 3000000:
-                        events.append("M")
-                    out += 1
+                time.sleep(0.001)
+                events.append("M")
+                time.sleep(0.001)
                 events.append("D")
                 return out
 
@@ -30,9 +28,6 @@ class TestThreadPool:
 
             assert events == ["S"] * 3 + ["M"] * 3 + ["D"] * 3
 
-            res = blocker()
-            assert res == 10000000
-
     def testLockBlockingSameThread(self):
         lock = ThreadPool.Lock()
 

From f8e24d47e34cc234b4e37e369f0b84f2efee5217 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 20 Jul 2023 18:38:20 +0000
Subject: [PATCH 227/333] remove ZeronameLocal plugin

we no longer need it as all domains have been freezed

refs #23
---
 .../SiteManagerPlugin.py                      | 180 ------------------
 .../disabled-ZeronameLocal/UiRequestPlugin.py |  39 ----
 plugins/disabled-ZeronameLocal/__init__.py    |   2 -
 3 files changed, 221 deletions(-)
 delete mode 100644 plugins/disabled-ZeronameLocal/SiteManagerPlugin.py
 delete mode 100644 plugins/disabled-ZeronameLocal/UiRequestPlugin.py
 delete mode 100644 plugins/disabled-ZeronameLocal/__init__.py

diff --git a/plugins/disabled-ZeronameLocal/SiteManagerPlugin.py b/plugins/disabled-ZeronameLocal/SiteManagerPlugin.py
deleted file mode 100644
index 579e31c1..00000000
--- a/plugins/disabled-ZeronameLocal/SiteManagerPlugin.py
+++ /dev/null
@@ -1,180 +0,0 @@
-import logging, json, os, re, sys, time, socket
-from Plugin import PluginManager
-from Config import config
-from Debug import Debug
-from http.client import HTTPSConnection, HTTPConnection, HTTPException
-from base64 import b64encode
-
-allow_reload = False # No reload supported
-
-@PluginManager.registerTo("SiteManager")
-class SiteManagerPlugin(object):
-    def load(self, *args, **kwargs):
-        super(SiteManagerPlugin, self).load(*args, **kwargs)
-        self.log = logging.getLogger("ZeronetLocal Plugin")
-        self.error_message = None
-        if not config.namecoin_host or not config.namecoin_rpcport or not config.namecoin_rpcuser or not config.namecoin_rpcpassword:
-            self.error_message = "Missing parameters"
-            self.log.error("Missing parameters to connect to namecoin node. Please check all the arguments needed with '--help'. Zeronet will continue working without it.")
-            return
-
-        url = "%(host)s:%(port)s" % {"host": config.namecoin_host, "port": config.namecoin_rpcport}
-        self.c = HTTPConnection(url, timeout=3)
-        user_pass = "%(user)s:%(password)s" % {"user": config.namecoin_rpcuser, "password": config.namecoin_rpcpassword}
-        userAndPass = b64encode(bytes(user_pass, "utf-8")).decode("ascii")
-        self.headers = {"Authorization" : "Basic %s" %  userAndPass, "Content-Type": " application/json " }
-
-        payload = json.dumps({
-            "jsonrpc": "2.0",
-            "id": "zeronet",
-            "method": "ping",
-            "params": []
-        })
-
-        try:
-            self.c.request("POST", "/", payload, headers=self.headers)
-            response = self.c.getresponse()
-            data = response.read()
-            self.c.close()
-            if response.status == 200:
-                result = json.loads(data.decode())["result"]
-            else:
-                raise Exception(response.reason)
-        except Exception as err:
-            self.log.error("The Namecoin node is unreachable. Please check the configuration value are correct. Zeronet will continue working without it.")
-            self.error_message = err
-        self.cache = dict()
-
-    # Checks if it's a valid address
-    def isAddress(self, address):
-        return self.isBitDomain(address) or super(SiteManagerPlugin, self).isAddress(address)
-
-    # Return: True if the address is domain
-    def isDomain(self, address):
-        return self.isBitDomain(address) or super(SiteManagerPlugin, self).isDomain(address)
-
-    # Return: True if the address is .bit domain
-    def isBitDomain(self, address):
-        return re.match(r"(.*?)([A-Za-z0-9_-]+\.bit)$", address)
-
-    # Return: Site object or None if not found
-    def get(self, address):
-        if self.isBitDomain(address):  # Its looks like a domain
-            address_resolved = self.resolveDomain(address)
-            if address_resolved:  # Domain found
-                site = self.sites.get(address_resolved)
-                if site:
-                    site_domain = site.settings.get("domain")
-                    if site_domain != address:
-                        site.settings["domain"] = address
-            else:  # Domain not found
-                site = self.sites.get(address)
-
-        else:  # Access by site address
-            site = super(SiteManagerPlugin, self).get(address)
-        return site
-
-    # Return or create site and start download site files
-    # Return: Site or None if dns resolve failed
-    def need(self, address, *args, **kwargs):
-        if self.isBitDomain(address):  # Its looks like a domain
-            address_resolved = self.resolveDomain(address)
-            if address_resolved:
-                address = address_resolved
-            else:
-                return None
-
-        return super(SiteManagerPlugin, self).need(address, *args, **kwargs)
-
-    # Resolve domain
-    # Return: The address or None
-    def resolveDomain(self, domain):
-        domain = domain.lower()
-
-        #remove .bit on end
-        if domain[-4:] == ".bit":
-            domain = domain[0:-4]
-
-        domain_array = domain.split(".")
-
-        if self.error_message:
-            self.log.error("Not able to connect to Namecoin node : {!s}".format(self.error_message))
-            return None
-
-        if len(domain_array) > 2:
-            self.log.error("Too many subdomains! Can only handle one level (eg. staging.mixtape.bit)")
-            return None
-
-        subdomain = ""
-        if len(domain_array) == 1:
-            domain = domain_array[0]
-        else:
-            subdomain = domain_array[0]
-            domain = domain_array[1]
-
-        if domain in self.cache:
-            delta = time.time() - self.cache[domain]["time"]
-            if delta < 3600:
-                # Must have been less than 1hour
-                return self.cache[domain]["addresses_resolved"][subdomain]
-
-        payload = json.dumps({
-            "jsonrpc": "2.0",
-            "id": "zeronet",
-            "method": "name_show",
-            "params": ["d/"+domain]
-        })
-
-        try:
-            self.c.request("POST", "/", payload, headers=self.headers)
-            response = self.c.getresponse()
-            data = response.read()
-            self.c.close()
-            domain_object = json.loads(data.decode())["result"]
-        except Exception as err:
-            #domain doesn't exist
-            return None
-
-        if "zeronet" in domain_object["value"]:
-            zeronet_domains = json.loads(domain_object["value"])["zeronet"]
-
-            if isinstance(zeronet_domains, str):
-                # {
-                #    "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9"
-                # } is valid
-                zeronet_domains = {"": zeronet_domains}
-
-            self.cache[domain] = {"addresses_resolved": zeronet_domains, "time": time.time()}
-
-        elif "map" in domain_object["value"]:
-            # Namecoin standard use {"map": { "blog": {"zeronet": "1D..."} }}
-            data_map = json.loads(domain_object["value"])["map"]
-
-            zeronet_domains = dict()
-            for subdomain in data_map:
-                if "zeronet" in data_map[subdomain]:
-                    zeronet_domains[subdomain] = data_map[subdomain]["zeronet"]
-            if "zeronet" in data_map and isinstance(data_map["zeronet"], str):
-                # {"map":{
-                #    "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9",
-                # }}
-                zeronet_domains[""] = data_map["zeronet"]
-
-            self.cache[domain] = {"addresses_resolved": zeronet_domains, "time": time.time()}
-
-        else:
-            # No Zeronet address registered
-            return None
-
-        return self.cache[domain]["addresses_resolved"][subdomain]
-
-@PluginManager.registerTo("ConfigPlugin")
-class ConfigPlugin(object):
-    def createArguments(self):
-        group = self.parser.add_argument_group("Zeroname Local plugin")
-        group.add_argument('--namecoin_host', help="Host to namecoin node (eg. 127.0.0.1)")
-        group.add_argument('--namecoin_rpcport', help="Port to connect (eg. 8336)")
-        group.add_argument('--namecoin_rpcuser', help="RPC user to connect to the namecoin node (eg. nofish)")
-        group.add_argument('--namecoin_rpcpassword', help="RPC password to connect to namecoin node")
-
-        return super(ConfigPlugin, self).createArguments()
diff --git a/plugins/disabled-ZeronameLocal/UiRequestPlugin.py b/plugins/disabled-ZeronameLocal/UiRequestPlugin.py
deleted file mode 100644
index 0ccfb530..00000000
--- a/plugins/disabled-ZeronameLocal/UiRequestPlugin.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import re
-from Plugin import PluginManager
-
-@PluginManager.registerTo("UiRequest")
-class UiRequestPlugin(object):
-    def __init__(self, *args, **kwargs):
-        from Site import SiteManager
-        self.site_manager = SiteManager.site_manager
-        super(UiRequestPlugin, self).__init__(*args, **kwargs)
-
-
-    # Media request
-    def actionSiteMedia(self, path):
-        match = re.match(r"/media/(?P<address>[A-Za-z0-9-]+\.[A-Za-z0-9\.-]+)(?P<inner_path>/.*|$)", path)
-        if match: # Its a valid domain, resolve first
-            domain = match.group("address")
-            address = self.site_manager.resolveDomain(domain)
-            if address:
-                path = "/media/"+address+match.group("inner_path")
-        return super(UiRequestPlugin, self).actionSiteMedia(path) # Get the wrapper frame output
-
-
-    # Is mediarequest allowed from that referer
-    def isMediaRequestAllowed(self, site_address, referer):
-        referer_path = re.sub("http[s]{0,1}://.*?/", "/", referer).replace("/media", "") # Remove site address
-        referer_path = re.sub(r"\?.*", "", referer_path) # Remove http params
-
-        if self.isProxyRequest(): # Match to site domain
-            referer = re.sub("^http://zero[/]+", "http://", referer) # Allow /zero access
-            referer_site_address = re.match("http[s]{0,1}://(.*?)(/|$)", referer).group(1)
-        else: # Match to request path
-            referer_site_address = re.match(r"/(?P<address>[A-Za-z0-9\.-]+)(?P<inner_path>/.*|$)", referer_path).group("address")
-
-        if referer_site_address == site_address: # Referer site address as simple address
-            return True
-        elif self.site_manager.resolveDomain(referer_site_address) == site_address: # Referer site address as dns
-            return True
-        else: # Invalid referer
-            return False
diff --git a/plugins/disabled-ZeronameLocal/__init__.py b/plugins/disabled-ZeronameLocal/__init__.py
deleted file mode 100644
index cf724069..00000000
--- a/plugins/disabled-ZeronameLocal/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from . import UiRequestPlugin
-from . import SiteManagerPlugin
\ No newline at end of file

From c07334f2d518bd23b83bd77693061ac7ce5bd1de Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 20 Jul 2023 18:41:47 +0000
Subject: [PATCH 228/333] remove Dnschain plugin (alternative .bit resolution)

as we freezed domains, this is no longer needed

refs #23
---
 .../disabled-Dnschain/SiteManagerPlugin.py    | 153 ------------------
 plugins/disabled-Dnschain/UiRequestPlugin.py  |  34 ----
 plugins/disabled-Dnschain/__init__.py         |   3 -
 3 files changed, 190 deletions(-)
 delete mode 100644 plugins/disabled-Dnschain/SiteManagerPlugin.py
 delete mode 100644 plugins/disabled-Dnschain/UiRequestPlugin.py
 delete mode 100644 plugins/disabled-Dnschain/__init__.py

diff --git a/plugins/disabled-Dnschain/SiteManagerPlugin.py b/plugins/disabled-Dnschain/SiteManagerPlugin.py
deleted file mode 100644
index 8b9508f1..00000000
--- a/plugins/disabled-Dnschain/SiteManagerPlugin.py
+++ /dev/null
@@ -1,153 +0,0 @@
-import logging, json, os, re, sys, time
-import gevent
-from Plugin import PluginManager
-from Config import config
-from util import Http
-from Debug import Debug
-
-allow_reload = False # No reload supported
-
-log = logging.getLogger("DnschainPlugin")
-
-@PluginManager.registerTo("SiteManager")
-class SiteManagerPlugin(object):
-	dns_cache_path = "%s/dns_cache.json" % config.data_dir
-	dns_cache = None
-
-	# Checks if its a valid address
-	def isAddress(self, address):
-		if self.isDomain(address): 
-			return True
-		else:
-			return super(SiteManagerPlugin, self).isAddress(address)
-
-
-	# Return: True if the address is domain
-	def isDomain(self, address):
-		return re.match(r"(.*?)([A-Za-z0-9_-]+\.[A-Za-z0-9]+)$", address)
-
-
-	# Load dns entries from data/dns_cache.json
-	def loadDnsCache(self):
-		if os.path.isfile(self.dns_cache_path):
-			self.dns_cache = json.load(open(self.dns_cache_path))
-		else:
-			self.dns_cache = {}
-		log.debug("Loaded dns cache, entries: %s" % len(self.dns_cache))
-
-
-	# Save dns entries to data/dns_cache.json
-	def saveDnsCache(self):
-		json.dump(self.dns_cache, open(self.dns_cache_path, "wb"), indent=2)
-
-
-	# Resolve domain using dnschain.net
-	# Return: The address or None
-	def resolveDomainDnschainNet(self, domain):
-		try:
-			match = self.isDomain(domain)
-			sub_domain = match.group(1).strip(".")
-			top_domain = match.group(2)
-			if not sub_domain: sub_domain = "@"
-			address = None
-			with gevent.Timeout(5, Exception("Timeout: 5s")):
-				res = Http.get("https://api.dnschain.net/v1/namecoin/key/%s" % top_domain).read()
-				data = json.loads(res)["data"]["value"]
-				if "zeronet" in data:
-					for key, val in data["zeronet"].items():
-						self.dns_cache[key+"."+top_domain] = [val, time.time()+60*60*5] # Cache for 5 hours
-					self.saveDnsCache()
-					return data["zeronet"].get(sub_domain)
-			# Not found
-			return address
-		except Exception as err:
-			log.debug("Dnschain.net %s resolve error: %s" % (domain, Debug.formatException(err)))
-
-
-	# Resolve domain using dnschain.info
-	# Return: The address or None
-	def resolveDomainDnschainInfo(self, domain):
-		try:
-			match = self.isDomain(domain)
-			sub_domain = match.group(1).strip(".")
-			top_domain = match.group(2)
-			if not sub_domain: sub_domain = "@"
-			address = None
-			with gevent.Timeout(5, Exception("Timeout: 5s")):
-				res = Http.get("https://dnschain.info/bit/d/%s" % re.sub(r"\.bit$", "", top_domain)).read()
-				data = json.loads(res)["value"]
-				for key, val in data["zeronet"].items():
-					self.dns_cache[key+"."+top_domain] = [val, time.time()+60*60*5] # Cache for 5 hours
-				self.saveDnsCache()
-				return data["zeronet"].get(sub_domain)
-			# Not found
-			return address
-		except Exception as err:
-			log.debug("Dnschain.info %s resolve error: %s" % (domain, Debug.formatException(err)))
-
-
-	# Resolve domain
-	# Return: The address or None
-	def resolveDomain(self, domain):
-		domain = domain.lower()
-		if self.dns_cache == None:
-			self.loadDnsCache()
-		if domain.count(".") < 2: # Its a topleved request, prepend @. to it
-			domain = "@."+domain
-
-		domain_details = self.dns_cache.get(domain)
-		if domain_details and time.time() < domain_details[1]: # Found in cache and its not expired
-			return domain_details[0]
-		else:
-			# Resovle dns using dnschain
-			thread_dnschain_info = gevent.spawn(self.resolveDomainDnschainInfo, domain)
-			thread_dnschain_net = gevent.spawn(self.resolveDomainDnschainNet, domain)
-			gevent.joinall([thread_dnschain_net, thread_dnschain_info]) # Wait for finish
-
-			if thread_dnschain_info.value and thread_dnschain_net.value: # Booth successfull
-				if thread_dnschain_info.value == thread_dnschain_net.value: # Same returned value
-					return thread_dnschain_info.value 
-				else:
-					log.error("Dns %s missmatch: %s != %s" % (domain, thread_dnschain_info.value, thread_dnschain_net.value))
-
-			# Problem during resolve
-			if domain_details: # Resolve failed, but we have it in the cache
-				domain_details[1] = time.time()+60*60 # Dont try again for 1 hour
-				return domain_details[0]
-			else: # Not found in cache
-				self.dns_cache[domain] = [None, time.time()+60] # Don't check again for 1 min
-				return None
-
-
-	# Return or create site and start download site files
-	# Return: Site or None if dns resolve failed
-	def need(self, address, all_file=True):
-		if self.isDomain(address): # Its looks like a domain
-			address_resolved = self.resolveDomain(address)
-			if address_resolved:
-				address = address_resolved
-			else:
-				return None
-		
-		return super(SiteManagerPlugin, self).need(address, all_file)
-
-
-	# Return: Site object or None if not found
-	def get(self, address):
-		if self.sites == None: # Not loaded yet
-			self.load()
-		if self.isDomain(address): # Its looks like a domain
-			address_resolved = self.resolveDomain(address)
-			if address_resolved: # Domain found
-				site = self.sites.get(address_resolved)
-				if site:
-					site_domain = site.settings.get("domain")
-					if site_domain != address:
-						site.settings["domain"] = address
-			else: # Domain not found
-				site = self.sites.get(address)
-
-		else: # Access by site address
-			site = self.sites.get(address)
-		return site
-
diff --git a/plugins/disabled-Dnschain/UiRequestPlugin.py b/plugins/disabled-Dnschain/UiRequestPlugin.py
deleted file mode 100644
index 8ab9d5c5..00000000
--- a/plugins/disabled-Dnschain/UiRequestPlugin.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import re
-from Plugin import PluginManager
-
-@PluginManager.registerTo("UiRequest")
-class UiRequestPlugin(object):
-	def __init__(self, server = None):
-		from Site import SiteManager
-		self.site_manager = SiteManager.site_manager
-		super(UiRequestPlugin, self).__init__(server)
-
-
-	# Media request
-	def actionSiteMedia(self, path):
-		match = re.match(r"/media/(?P<address>[A-Za-z0-9-]+\.[A-Za-z0-9\.-]+)(?P<inner_path>/.*|$)", path)
-		if match: # Its a valid domain, resolve first
-			domain = match.group("address")
-			address = self.site_manager.resolveDomain(domain)
-			if address:
-				path = "/media/"+address+match.group("inner_path")
-		return super(UiRequestPlugin, self).actionSiteMedia(path) # Get the wrapper frame output
-
-
-	# Is mediarequest allowed from that referer
-	def isMediaRequestAllowed(self, site_address, referer):
-		referer_path = re.sub("http[s]{0,1}://.*?/", "/", referer).replace("/media", "") # Remove site address
-		referer_site_address = re.match(r"/(?P<address>[A-Za-z0-9\.-]+)(?P<inner_path>/.*|$)", referer_path).group("address")
-
-		if referer_site_address == site_address: # Referer site address as simple address
-			return True
-		elif self.site_manager.resolveDomain(referer_site_address) == site_address: # Referer site address as dns
-			return True
-		else: # Invalid referer
-			return False
-
diff --git a/plugins/disabled-Dnschain/__init__.py b/plugins/disabled-Dnschain/__init__.py
deleted file mode 100644
index 2b36af5d..00000000
--- a/plugins/disabled-Dnschain/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# This plugin is experimental, if you really want to enable uncomment the following lines:
-# import DnschainPlugin
-# import SiteManagerPlugin
\ No newline at end of file

From 07b7c5824fa57fff95d35320878cb62b4c6bc234 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 21 Jul 2023 11:33:46 +0000
Subject: [PATCH 229/333] remove third-party plugin management

refs #216
---
 .../UiPluginManager/UiPluginManagerPlugin.py  | 93 -------------------
 1 file changed, 93 deletions(-)

diff --git a/plugins/UiPluginManager/UiPluginManagerPlugin.py b/plugins/UiPluginManager/UiPluginManagerPlugin.py
index 1ab80f53..0bfa8ec5 100644
--- a/plugins/UiPluginManager/UiPluginManagerPlugin.py
+++ b/plugins/UiPluginManager/UiPluginManagerPlugin.py
@@ -126,96 +126,3 @@ class UiWebsocketPlugin(object):
         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"

From 3330b19e3107945a8b0ef871a38d3d7dfeaa6cbc Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 21 Jul 2023 12:00:23 +0000
Subject: [PATCH 230/333] don't fail if http_accept header is */*

fixes #67
---
 src/Ui/UiRequest.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index d30ff4e3..281a5e5c 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -398,8 +398,9 @@ class UiRequest(object):
             if self.isWebSocketRequest():
                 return self.error403("WebSocket request not allowed to load wrapper")  # No websocket
 
-            if "text/html" not in self.env.get("HTTP_ACCEPT", ""):
-                return self.error403("Invalid Accept header to load wrapper: %s" % self.env.get("HTTP_ACCEPT", ""))
+            http_accept = self.env.get("HTTP_ACCEPT", "")
+            if "text/html" not in http_accept and "*/*" not in http_accept:
+                return self.error403(f"Invalid Accept header to load wrapper: {http_accept}")
             if "prefetch" in self.env.get("HTTP_X_MOZ", "") or "prefetch" in self.env.get("HTTP_PURPOSE", ""):
                 return self.error403("Prefetch not allowed to load wrapper")
 

From f94765f7b989263605c9148e1036e176fa71a45e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 23 Jul 2023 09:37:52 +0000
Subject: [PATCH 231/333] more anonymous upnp

thanks to 0netdwf for reporting
---
 src/Peer/PeerPortchecker.py |  5 +----
 src/util/UpnpPunch.py       | 17 ++++++-----------
 2 files changed, 7 insertions(+), 15 deletions(-)

diff --git a/src/Peer/PeerPortchecker.py b/src/Peer/PeerPortchecker.py
index f7ca68e3..8bf337cf 100644
--- a/src/Peer/PeerPortchecker.py
+++ b/src/Peer/PeerPortchecker.py
@@ -28,11 +28,8 @@ class PeerPortchecker(object):
         return urllib.request.urlopen(req, timeout=20.0)
 
     def portOpen(self, port):
-        # self.log.info("Not trying to open port using UpnpPunch until it's proven robust...")
-        # return False
-
         try:
-            UpnpPunch.ask_to_open_port(port, 'ZeroNet', retries=3, protos=["TCP"])
+            UpnpPunch.ask_to_open_port(port, retries=3, protos=["TCP"])
             self.upnp_port_opened = True
         except Exception as err:
             self.log.warning("UpnpPunch run error: %s" % Debug.formatException(err))
diff --git a/src/util/UpnpPunch.py b/src/util/UpnpPunch.py
index 2fa85209..4fd17c61 100644
--- a/src/util/UpnpPunch.py
+++ b/src/util/UpnpPunch.py
@@ -181,7 +181,6 @@ def _get_local_ips():
 
 def _create_open_message(local_ip,
                          port,
-                         description="UPnPPunch",
                          protocol="TCP",
                          upnp_schema='WANIPConnection'):
     """
@@ -205,14 +204,13 @@ def _create_open_message(local_ip,
 </s:Envelope>""".format(port=port,
                         protocol=protocol,
                         host_ip=local_ip,
-                        description=description,
+                        description='',
                         upnp_schema=upnp_schema)
     return (REMOVE_WHITESPACE.sub('><', soap_message), 'AddPortMapping')
 
 
 def _create_close_message(local_ip,
                           port,
-                          description=None,
                           protocol='TCP',
                           upnp_schema='WANIPConnection'):
     soap_message = """<?xml version="1.0"?>
@@ -294,12 +292,12 @@ def _send_requests(messages, location, upnp_schema, control_path):
     raise UpnpError('Sending requests using UPnP failed.')
 
 
-def _orchestrate_soap_request(ip, port, msg_fn, desc=None, protos=("TCP", "UDP")):
+def _orchestrate_soap_request(ip, port, msg_fn, protos=("TCP", "UDP")):
     logger.debug("Trying using local ip: %s" % ip)
     idg_data = _collect_idg_data(ip)
 
     soap_messages = [
-        msg_fn(ip, port, desc, proto, idg_data['upnp_schema'])
+        msg_fn(ip, port, proto, idg_data['upnp_schema'])
         for proto in protos
     ]
 
@@ -307,7 +305,6 @@ def _orchestrate_soap_request(ip, port, msg_fn, desc=None, protos=("TCP", "UDP")
 
 
 def _communicate_with_igd(port=15441,
-                          desc="UpnpPunch",
                           retries=3,
                           fn=_create_open_message,
                           protos=("TCP", "UDP")):
@@ -321,7 +318,7 @@ def _communicate_with_igd(port=15441,
     def job(local_ip):
         for retry in range(retries):
             try:
-                _orchestrate_soap_request(local_ip, port, fn, desc, protos)
+                _orchestrate_soap_request(local_ip, port, fn, protos)
                 return True
             except Exception as e:
                 logger.debug('Upnp request using "{0}" failed: {1}'.format(local_ip, e))
@@ -357,20 +354,18 @@ def _communicate_with_igd(port=15441,
     return success
 
 
-def ask_to_open_port(port=15441, desc="UpnpPunch", retries=3, protos=("TCP", "UDP")):
+def ask_to_open_port(port=15441, retries=3, protos=("TCP", "UDP")):
     logger.debug("Trying to open port %d." % port)
     return _communicate_with_igd(port=port,
-                          desc=desc,
                           retries=retries,
                           fn=_create_open_message,
                           protos=protos)
 
 
-def ask_to_close_port(port=15441, desc="UpnpPunch", retries=3, protos=("TCP", "UDP")):
+def ask_to_close_port(port=15441, retries=3, protos=("TCP", "UDP")):
     logger.debug("Trying to close port %d." % port)
     # retries=1 because multiple successes cause 500 response and failure
     return _communicate_with_igd(port=port,
-                          desc=desc,
                           retries=retries,
                           fn=_create_close_message,
                           protos=protos)

From cbf9c78e8ce4c3d0b27ad3d47c420797f93d0f3e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 23 Jul 2023 09:31:46 +0000
Subject: [PATCH 232/333] helper function: quickly check if a string looks like
 a valid Bitcoin address

---
 src/Crypt/CryptBitcoin.py | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/Crypt/CryptBitcoin.py b/src/Crypt/CryptBitcoin.py
index 68b2caa2..a0807187 100644
--- a/src/Crypt/CryptBitcoin.py
+++ b/src/Crypt/CryptBitcoin.py
@@ -99,3 +99,16 @@ def verify(data, valid_address, sign, lib_verify=None):  # Verify data using add
         return sign_address in valid_address
     else:  # One possible address
         return sign_address == valid_address
+
+def isValidAddress(addr):
+    '''Check if provided address is valid bitcoin address'''
+    if addr[0] != '1':
+        # no support for new-style addrs
+        return False
+    from base58 import b58decode
+    bs = b58decode(addr)
+    main = bs[:-4]
+    checksum = bs[-4:]
+    h1 = hashlib.sha256(main).digest()
+    h2 = hashlib.sha256(h1).digest()
+    return h2[:4] == checksum

From 9d8be57025f0cf70795a62016f8fe385d4592382 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 23 Jul 2023 18:07:45 +0000
Subject: [PATCH 233/333] bootstrap.url

---
 bootstrap.url | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 bootstrap.url

diff --git a/bootstrap.url b/bootstrap.url
new file mode 100644
index 00000000..f23462eb
--- /dev/null
+++ b/bootstrap.url
@@ -0,0 +1 @@
+https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.9/data-default-2023-07-23.zip
\ No newline at end of file

From 76d1b14eaf222cb084aa57e4b0105ed205482dc9 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 23 Jul 2023 19:55:59 +0000
Subject: [PATCH 234/333] Ignore missing sites.json gracefully

---
 src/Site/Site.py        | 6 +++++-
 src/Site/SiteManager.py | 7 ++++---
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/Site/Site.py b/src/Site/Site.py
index ffdb2bb0..ae2be36a 100644
--- a/src/Site/Site.py
+++ b/src/Site/Site.py
@@ -87,7 +87,11 @@ class Site(object):
     # Load site settings from data/sites.json
     def loadSettings(self, settings=None):
         if not settings:
-            settings = json.load(open("%s/sites.json" % config.data_dir)).get(self.address)
+            try:
+                settings = json.load(open(f'{config.data_dir}/sites.json')).get(self.address)
+            except Exception as err:
+                logging.error(f'Error loading {config.data_dir}/sites.json: {err}')
+                settings = {}
         if settings:
             self.settings = settings
             if "cache" not in settings:
diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py
index d7ba6e94..78c20f86 100644
--- a/src/Site/SiteManager.py
+++ b/src/Site/SiteManager.py
@@ -31,17 +31,18 @@ class SiteManager(object):
     @util.Noparallel()
     def load(self, cleanup=True, startup=False):
         from .Site import Site
-        self.log.info("Loading sites... (cleanup: %s, startup: %s)" % (cleanup, startup))
+        self.log.info(f'Loading sites... ({cleanup=}, {startup=})')
         self.loaded = False
         address_found = []
         added = 0
         load_s = time.time()
         # Load new adresses
         try:
-            json_path = "%s/sites.json" % config.data_dir
+            json_path = f"{config.data_dir}/sites.json"
             data = json.load(open(json_path))
         except Exception as err:
-            raise Exception("Unable to load %s: %s" % (json_path, err))
+            self.log.error(f"Unable to load {json_path}: {err}")
+            data = {}
 
         sites_need = []
 

From 5a184a5489b1da7f7c3c0fc88ab51e4c420d5cec Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 23 Jul 2023 20:57:09 +0000
Subject: [PATCH 235/333] Don't hide warnings in console

WARNING logging level is higher than INFO so really shouldn't hide them..
---
 src/Config.py | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index 9f5de779..e1c397e2 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -797,10 +797,6 @@ class Config(object):
             except Exception as err:
                 print("Can't change permission of %s: %s" % (self.log_dir, err))
 
-        # Make warning hidden from console
-        logging.WARNING = 15  # Don't display warnings if not in debug mode
-        logging.addLevelName(15, "WARNING")
-
         logging.getLogger('').name = "-"  # Remove root prefix
 
         self.error_logger = ErrorLogHandler()

From 9444e097a66f9ab12562a27231fa0ba4420f37ae Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 23 Jul 2023 21:14:54 +0000
Subject: [PATCH 236/333] New command line action: importBundle; load bootstrap
 bundle on first run

- importBundle imports zip archive of sites
- latest bootstrapping bundle is downloaded if data/ dir is empty; primary
  reason for this for now is to avoid constantly updating tracker list in
  git tree and use Syncronite instead
---
 requirements.txt |  1 +
 src/Config.py    |  6 ++++
 src/main.py      | 71 +++++++++++++++++++++++++++++++++++++++++-------
 3 files changed, 68 insertions(+), 10 deletions(-)

diff --git a/requirements.txt b/requirements.txt
index 4298ed61..887138d8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,3 +14,4 @@ maxminddb
 rich
 defusedxml>=0.7
 pyaes
+requests
diff --git a/src/Config.py b/src/Config.py
index e1c397e2..8850679b 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -320,6 +320,10 @@ class Config(object):
         action.add_argument('cmd', help='API command name')
         action.add_argument('parameters', help='Parameters of the command', nargs='?')
 
+        # Import bundled sites
+        action = self.subparsers.add_parser("importBundle", help='Import sites from a .zip bundle')
+        action.add_argument('bundle', help='Path to a data bundle')
+
         # dbRebuild
         action = self.subparsers.add_parser("dbRebuild", help='Rebuild site database cache')
         action.add_argument('address', help='Site to rebuild')
@@ -426,6 +430,8 @@ class Config(object):
         self.parser.add_argument('--disable_udp', help='Disable UDP connections', action='store_true')
         self.parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port')
         self.parser.add_argument('--bind', help='Bind outgoing sockets to this address', metavar='ip')
+        self.parser.add_argument('--bootstrap_url', help='URL of file with link to bootstrap bundle', default='https://raw.githubusercontent.com/caryoscelus/zeronet-conservancy/bootstrap/bootstrap.url', type=str)
+        self.parser.add_argument('--disable_bootstrap', help='Disable downloading bootstrap information from clearnet', action='store_true')
         self.parser.add_argument('--trackers', help='Bootstraping torrent trackers', default=trackers, metavar='protocol://address', nargs='*')
         self.parser.add_argument('--trackers_file', help='Load torrent trackers dynamically from a file', metavar='path', nargs='*')
         self.parser.add_argument('--trackers_proxy', help='Force use proxy to connect to trackers (disable, tor, ip:port)', default="disable")
diff --git a/src/main.py b/src/main.py
index b4b656db..b99e0076 100644
--- a/src/main.py
+++ b/src/main.py
@@ -34,24 +34,72 @@ def load_config():
 
 load_config()
 
-def init_dirs():
-    if not os.path.isdir(config.data_dir):
-        os.mkdir(config.data_dir)
-        try:
-            os.chmod(config.data_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
-        except Exception as err:
-            startupError("Can't change permission of %s: %s" % (config.data_dir, err))
+def importBundle(bundle):
+    from zipfile import ZipFile
+    from Crypt.CryptBitcoin import isValidAddress
+    from Site import SiteManager
+    SiteManager.site_manager.load()
 
-    sites_json = f"{config.data_dir}/sites.json"
+    with ZipFile(bundle) as zf:
+        all_files = zf.namelist()
+        top_files = list(set(map(lambda f: f.split('/')[0], all_files)))
+        if len(top_files) == 1 and not isValidAddress(top_files[0]):
+            prefix = top_files[0]+'/'
+        else:
+            prefix = ''
+        top_2 = list(set(filter(lambda f: len(f)>0,
+                                map(lambda f: f.removeprefix(prefix).split('/')[0], all_files))))
+        for d in top_2:
+            if isValidAddress(d):
+                logging.info(f'unpack {d} into {config.data_dir}')
+                for fname in filter(lambda f: f.startswith(prefix+d) and not f.endswith('/'), all_files):
+                    tgt = config.data_dir + '/' + fname.removeprefix(prefix)
+                    logging.info(f'-- {fname} --> {tgt}')
+                    info = zf.getinfo(fname)
+                    info.filename = tgt
+                    zf.extract(info)
+                logging.info(f'add site {d}')
+                SiteManager.site_manager.add(d, noload=True)
+            else:
+                logging.info(f'Warning: unknown file in a bundle: {prefix+d}')
+    SiteManager.site_manager.save()
+
+def init_dirs():
+    data_dir = config.data_dir
+    if not os.path.isdir(data_dir):
+        os.mkdir(data_dir)
+        try:
+            os.chmod(data_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
+        except Exception as err:
+            startupError(f"Can't change permission of {data_dir}: {err}")
+
+        # download latest bootstrap bundle
+        if not config.disable_bootstrap and not config.offline:
+            import requests
+            from io import BytesIO
+
+            print(f'fetching {config.bootstrap_url}')
+            response = requests.get(config.bootstrap_url)
+            if response.status_code != 200:
+                startupError(f"Cannot load bootstrap bundle (response status: {response.status_code})")
+            url = response.text
+            print(f'got {url}')
+            response = requests.get(url)
+            if response.status_code < 200 or response.status_code >= 300:
+                startupError(f"Cannot load boostrap bundle (response status: {response.status_code})")
+            importBundle(BytesIO(response.content))
+
+    sites_json = f"{data_dir}/sites.json"
     if not os.path.isfile(sites_json):
         with open(sites_json, "w") as f:
             f.write("{}")
-    users_json = f"{config.data_dir}/users.json"
+    users_json = f"{data_dir}/users.json"
     if not os.path.isfile(users_json):
         with open(users_json, "w") as f:
             f.write("{}")
 
 # TODO: GET RID OF TOP-LEVEL CODE!!!
+config.initConsoleLogger()
 
 try:
     init_dirs()
@@ -73,7 +121,7 @@ if config.action == "main":
         r = proc.wait()
         sys.exit(r)
 
-config.initLogging()
+config.initLogging(console_logging=False)
 
 # Debug dependent configuration
 from Debug import DebugHook
@@ -414,6 +462,9 @@ class Actions(object):
         else:
             return res
 
+    def importBundle(self, bundle):
+        importBundle(bundle)
+
     def getWebsocket(self, site):
         import websocket
 

From 2b51e23650a82fc24cd52680a3979fe1833a66bb Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 24 Jul 2023 08:21:31 +0000
Subject: [PATCH 237/333] Fix importBundle

Avoid using SiteManager as initializing it out of order breaks things
---
 src/main.py | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/src/main.py b/src/main.py
index b99e0076..660bf362 100644
--- a/src/main.py
+++ b/src/main.py
@@ -37,8 +37,14 @@ load_config()
 def importBundle(bundle):
     from zipfile import ZipFile
     from Crypt.CryptBitcoin import isValidAddress
-    from Site import SiteManager
-    SiteManager.site_manager.load()
+    import json
+
+    sites_json_path = f"{config.data_dir}/sites.json"
+    try:
+        with open(sites_json_path) as f:
+            sites = json.load(f)
+    except Exception as err:
+        sites = {}
 
     with ZipFile(bundle) as zf:
         all_files = zf.namelist()
@@ -59,10 +65,11 @@ def importBundle(bundle):
                     info.filename = tgt
                     zf.extract(info)
                 logging.info(f'add site {d}')
-                SiteManager.site_manager.add(d, noload=True)
+                sites[d] = {}
             else:
                 logging.info(f'Warning: unknown file in a bundle: {prefix+d}')
-    SiteManager.site_manager.save()
+    with open(sites_json_path, 'w') as f:
+        json.dump(sites, f)
 
 def init_dirs():
     data_dir = config.data_dir

From 41c7bd47c861eadc0beb58ad551db23fae63a104 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 24 Jul 2023 08:22:19 +0000
Subject: [PATCH 238/333] Use Syncronite directly by default

---
 src/Config.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Config.py b/src/Config.py
index 8850679b..42d0721c 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -433,7 +433,7 @@ class Config(object):
         self.parser.add_argument('--bootstrap_url', help='URL of file with link to bootstrap bundle', default='https://raw.githubusercontent.com/caryoscelus/zeronet-conservancy/bootstrap/bootstrap.url', type=str)
         self.parser.add_argument('--disable_bootstrap', help='Disable downloading bootstrap information from clearnet', action='store_true')
         self.parser.add_argument('--trackers', help='Bootstraping torrent trackers', default=trackers, metavar='protocol://address', nargs='*')
-        self.parser.add_argument('--trackers_file', help='Load torrent trackers dynamically from a file', metavar='path', nargs='*')
+        self.parser.add_argument('--trackers_file', help='Load torrent trackers dynamically from a file', default=['{data_dir}/15CEFKBRHFfAP9rmL6hhLmHoXrrgmw4B5o/cache/1/Syncronite.html'], metavar='path', nargs='*')
         self.parser.add_argument('--trackers_proxy', help='Force use proxy to connect to trackers (disable, tor, ip:port)', default="disable")
         self.parser.add_argument('--use_libsecp256k1', help='Use Libsecp256k1 liblary for speedup', type='bool', choices=[True, False], default=True)
         self.parser.add_argument('--use_openssl', help='Use OpenSSL liblary for speedup', type='bool', choices=[True, False], default=True)

From 1d2e264a44765defcc27890bc1986dbcc570f0c1 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 24 Jul 2023 08:36:00 +0000
Subject: [PATCH 239/333] Remove manually copied tracker list

---
 src/Config.py | 174 +-------------------------------------------------
 1 file changed, 1 insertion(+), 173 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index 42d0721c..bb779b16 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -9,178 +9,6 @@ import logging.handlers
 import stat
 import time
 
-trackers = [
-    'zero://188.242.242.224:26474',
-    'zero://2001:19f0:8001:1d2f:5400:2ff:fe83:5bf7:23141',
-    'zero://200:1e7a:5100:ef7c:6fa4:d8ae:b91c:a74:15441',
-    'zero://23.184.48.134:15441',
-    'zero://57hzgtu62yzxqgbvgxs7g3lfck3za4zrda7qkskar3tlak5recxcebyd.onion:15445',
-    'zero://6i54dd5th73oelv636ivix6sjnwfgk2qsltnyvswagwphub375t3xcad.onion:15441',
-    'zero://f2hnjbggc3c2u2apvxdugirnk6bral54ibdoul3hhvu7pd4fso5fq3yd.onion:15441',
-    'zero://gugt43coc5tkyrhrc3esf6t6aeycvcqzw7qafxrjpqbwt4ssz5czgzyd.onion:15441',
-    'zero://k5w77dozo3hy5zualyhni6vrh73iwfkaofa64abbilwyhhd3wgenbjqd.onion:15441',
-    'zero://ow7in4ftwsix5klcbdfqvfqjvimqshbm2o75rhtpdnsderrcbx74wbad.onion:15441',
-    'zero://pn4q2zzt2pw4nk7yidxvsxmydko7dfibuzxdswi6gu6ninjpofvqs2id.onion:15441',
-    'zero://skdeywpgm5xncpxbbr4cuiip6ey4dkambpanog6nruvmef4f3e7o47qd.onion:15441',
-    'zero://wlxav3szbrdhest4j7dib2vgbrd7uj7u7rnuzg22cxbih7yxyg2hsmid.onion:15441',
-    'zero://zy7wttvjtsijt5uwmlar4yguvjc2gppzbdj4v6bujng6xwjmkdg7uvqd.onion:15441',
-    'http://bt.okmp3.ru:2710/announce',
-    'http://fxtt.ru:80/announce',
-    'http://incine.ru:6969/announce',
-    'http://moeweb.pw:6969/announce',
-    'http://open.acgnxtracker.com:80/announce',
-    'http://t.acg.rip:6699/announce',
-    'http://t.nyaatracker.com:80/announce',
-    'http://t.overflow.biz:6969/announce',
-    'http://tracker.files.fm:6969/announce',
-    'http://tracker.mywaifu.best:6969/announce',
-    'http://tracker.vrpnet.org:6969/announce',
-    'http://vps02.net.orel.ru:80/announce',
-    'udp://960303.xyz:6969/announce',
-    'udp://aarsen.me:6969/announce',
-    'udp://astrr.ru:6969/announce',
-    'udp://ben.kerbertools.xyz:6969/announce',
-    'udp://bt1.archive.org:6969/announce',
-    'udp://bt2.archive.org:6969/announce',
-    'udp://bt.ktrackers.com:6666/announce',
-    'udp://bubu.mapfactor.com:6969/announce',
-    'udp://c.ns.cluefone.com:6969/announce',
-    'udp://cutscloud.duckdns.org:6969/announce',
-    'udp://download.nerocloud.me:6969/announce',
-    'udp://epider.me:6969/announce',
-    'udp://exodus.desync.com:6969/announce',
-    'udp://htz3.noho.st:6969/announce',
-    'udp://ipv4.tracker.harry.lu:80/announce',
-    'udp://laze.cc:6969/announce',
-    'udp://mail.artixlinux.org:6969/announce',
-    'udp://mirror.aptus.co.tz:6969/announce',
-    'udp://moonburrow.club:6969/announce',
-    'udp://movies.zsw.ca:6969/announce',
-    'udp://mts.tvbit.co:6969/announce',
-    'udp://new-line.net:6969/announce',
-    'udp://open.demonii.com:1337/announce',
-    'udp://open.stealth.si:80/announce',
-    'udp://opentracker.i2p.rocks:6969/announce',
-    'udp://p4p.arenabg.com:1337/announce',
-    'udp://psyco.fr:6969/announce',
-    'udp://public.publictracker.xyz:6969/announce',
-    'udp://rep-art.ynh.fr:6969/announce',
-    'udp://run.publictracker.xyz:6969/announce',
-    'udp://sanincode.com:6969/announce',
-    'udp://slicie.icon256.com:8000/announce',
-    'udp://tamas3.ynh.fr:6969/announce',
-    'udp://thouvenin.cloud:6969/announce',
-    'udp://torrentclub.space:6969/announce',
-    'udp://tracker.0x.tf:6969/announce',
-    'udp://tracker1.bt.moack.co.kr:80/announce',
-    'udp://tracker.4.babico.name.tr:3131/announce',
-    'udp://tracker.altrosky.nl:6969/announce',
-    'udp://tracker.artixlinux.org:6969/announce',
-    'udp://tracker.farted.net:6969/announce',
-    'udp://tracker.jonaslsa.com:6969/announce',
-    'udp://tracker.joybomb.tw:6969/announce',
-    'udp://tracker.monitorit4.me:6969/announce',
-    'udp://tracker.opentrackr.org:1337/announce',
-    'udp://tracker.pomf.se:80/announce',
-    'udp://tracker.publictracker.xyz:6969/announce',
-    'udp://tracker.srv00.com:6969/announce',
-    'udp://tracker.tcp.exchange:6969/announce',
-    'udp://tracker.theoks.net:6969/announce',
-    'udp://transkaroo.joustasie.net:6969/announce',
-    'udp://uploads.gamecoast.net:6969/announce',
-    'udp://v2.iperson.xyz:6969/announce',
-    'udp://vibe.sleepyinternetfun.xyz:1738/announce',
-    'udp://www.skynetcenter.me:6969/announce',
-    'udp://www.torrent.eu.org:451/announce',
-    'zero://194.5.98.39:15441',
-    'zero://145.239.95.38:15441',
-    'zero://178.128.34.249:26117',
-    'zero://217.18.217.143:39288',
-    'zero://83.246.141.203:22207',
-    'zero://syncronite.loki:15441',
-    'zero://2a05:dfc1:4000:1e00::a:15441',
-    'zero://2400:6180:100:d0::8fd:8001:21697',
-    'zero://2001:19f0:8001:1d2f:5400:2ff:fe83:5bf7:30530',
-    'zero://73pyhfwfwsrhfw76knkjfnw6o3lk53zfo7hlxdmxbj75sjcnol5cioad.onion:15442',
-    'zero://fzlzmxuz2bust72cuy5g4w6d62tx624xcjaupf2kp7ffuitbiniy2hqd.onion:15441',
-    'zero://rlcjomszyitxpwv7kzopmqgzk3bdpsxeull4c3s6goszkk6h2sotfoad.onion:15441',
-    'zero://tqmo2nffqo4qc5jgmz3me5eri3zpgf3v2zciufzmhnvznjve5c3argad.onion:15441',
-    'http://107.189.31.134:6969/announce',
-    'http://119.28.71.45:8080/announce',
-    'http://129.146.193.240:6699/announce',
-    'http://159.69.65.157:6969/announce',
-    'http://163.172.29.130:80/announce',
-    'http://185.130.47.2:6969/announce',
-    'http://45.67.35.111:6969/announce',
-    'http://61.222.178.254:6969/announce',
-    'http://83.31.30.182:6969/announce',
-    'http://93.158.213.92:1337/announce',
-    'http://95.217.167.10:6969/announce',
-    'udp://102.223.180.235:6969/announce',
-    'udp://103.122.21.50:6969/announce',
-    'udp://104.131.98.232:6969/announce',
-    'udp://104.244.77.87:6969/announce',
-    'udp://107.189.11.58:6969/announce',
-    'udp://107.189.31.134:6969/announce',
-    'udp://139.144.68.88:6969/announce',
-    'udp://149.28.239.70:6969/announce',
-    'udp://15.204.205.14:6969/announce',
-    'udp://156.234.201.18:80/announce',
-    'udp://158.101.161.60:3131/announce',
-    'udp://163.172.29.130:80/announce',
-    'udp://167.99.185.219:6969/announce',
-    'udp://176.31.250.174:6969/announce',
-    'udp://176.56.4.238:6969/announce',
-    'udp://178.32.222.98:3391/announce',
-    'udp://184.105.151.166:6969/announce',
-    'udp://185.102.219.163:6969/announce',
-    'udp://185.181.60.155:80/announce',
-    'udp://185.217.199.21:6969/announce',
-    'udp://185.44.82.25:1337/announce',
-    'udp://185.68.21.244:6969/announce',
-    'udp://192.3.165.191:6969/announce',
-    'udp://192.3.165.198:6969/announce',
-    'udp://192.95.46.115:6969/announce',
-    'udp://193.176.158.162:6969/announce',
-    'udp://193.37.214.12:6969/announce',
-    'udp://193.42.111.57:9337/announce',
-    'udp://198.100.149.66:6969/announce',
-    'udp://20.100.205.229:6969/announce',
-    'udp://207.241.226.111:6969/announce',
-    'udp://207.241.231.226:6969/announce',
-    'udp://209.141.59.16:6969/announce',
-    'udp://212.237.53.230:6969/announce',
-    'udp://23.153.248.2:6969/announce',
-    'udp://23.254.228.89:6969/announce',
-    'udp://37.187.111.136:6969/announce',
-    'udp://37.27.4.53:6969/announce',
-    'udp://38.7.201.142:6969/announce',
-    'udp://45.154.253.6:6969/announce',
-    'udp://45.63.30.114:6969/announce',
-    'udp://45.9.60.30:6969/announce',
-    'udp://46.38.238.105:6969/announce',
-    'udp://49.12.76.8:8080/announce',
-    'udp://5.102.159.190:6969/announce',
-    'udp://5.196.89.204:6969/announce',
-    'udp://51.15.79.209:6969/announce',
-    'udp://51.159.54.68:6666/announce',
-    'udp://51.68.174.87:6969/announce',
-    'udp://51.81.222.188:6969/announce',
-    'udp://52.58.128.163:6969/announce',
-    'udp://61.222.178.254:6969/announce',
-    'udp://77.73.69.230:6969/announce',
-    'udp://83.102.180.21:80/announce',
-    'udp://83.31.30.182:6969/announce',
-    'udp://85.206.172.159:6969/announce',
-    'udp://85.239.33.28:6969/announce',
-    'udp://86.57.161.157:6969/announce',
-    'udp://91.216.110.52:451/announce',
-    'udp://93.158.213.92:1337/announce',
-    'udp://94.103.87.87:6969/announce',
-    'udp://95.216.74.39:6969/announce',
-    'udp://95.31.11.224:6969/announce',
-]
-
 class Config(object):
 
     def __init__(self, argv):
@@ -432,7 +260,7 @@ class Config(object):
         self.parser.add_argument('--bind', help='Bind outgoing sockets to this address', metavar='ip')
         self.parser.add_argument('--bootstrap_url', help='URL of file with link to bootstrap bundle', default='https://raw.githubusercontent.com/caryoscelus/zeronet-conservancy/bootstrap/bootstrap.url', type=str)
         self.parser.add_argument('--disable_bootstrap', help='Disable downloading bootstrap information from clearnet', action='store_true')
-        self.parser.add_argument('--trackers', help='Bootstraping torrent trackers', default=trackers, metavar='protocol://address', nargs='*')
+        self.parser.add_argument('--trackers', help='Bootstraping torrent trackers', default=[], metavar='protocol://address', nargs='*')
         self.parser.add_argument('--trackers_file', help='Load torrent trackers dynamically from a file', default=['{data_dir}/15CEFKBRHFfAP9rmL6hhLmHoXrrgmw4B5o/cache/1/Syncronite.html'], metavar='path', nargs='*')
         self.parser.add_argument('--trackers_proxy', help='Force use proxy to connect to trackers (disable, tor, ip:port)', default="disable")
         self.parser.add_argument('--use_libsecp256k1', help='Use Libsecp256k1 liblary for speedup', type='bool', choices=[True, False], default=True)

From 70f2eeada1e30ac893404109c4a0584693fafde5 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 24 Jul 2023 09:18:42 +0000
Subject: [PATCH 240/333] Don't spam console with failed announcements

These should be debug, not warning (previously warning reporting was disabled)
---
 src/Site/SiteAnnouncer.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py
index 2fd63e82..1c4252ae 100644
--- a/src/Site/SiteAnnouncer.py
+++ b/src/Site/SiteAnnouncer.py
@@ -202,7 +202,7 @@ class SiteAnnouncer(object):
             else:
                 raise AnnounceError("Unknown protocol: %s" % address_parts["protocol"])
         except Exception as err:
-            self.site.log.warning("Tracker %s announce failed: %s in mode %s" % (tracker, Debug.formatException(err), mode))
+            self.site.log.debug("Tracker %s announce failed: %s in mode %s" % (tracker, Debug.formatException(err), mode))
             error = err
 
         if error:

From d924e9bb2ec0afc71c156a94229cd3fd3c1e98a9 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 24 Jul 2023 10:29:25 +0000
Subject: [PATCH 241/333] fix error handling (was: unbound local variable)

---
 src/Site/SiteStorage.py | 6 ++++--
 src/main.py             | 5 +++--
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py
index 4cbed75d..be8f88e9 100644
--- a/src/Site/SiteStorage.py
+++ b/src/Site/SiteStorage.py
@@ -452,19 +452,21 @@ class SiteStorage(object):
                     bad_files.append(file_inner_path)
                     continue
 
+                error = None
                 if quick_check:
                     ok = os.path.getsize(file_path) == content["files"][file_relative_path]["size"]
                     if not ok:
-                        err = "Invalid size"
+                        error = "Invalid size"
                 else:
                     try:
                         ok = self.site.content_manager.verifyFile(file_inner_path, open(file_path, "rb"))
                     except Exception as err:
+                        error = err
                         ok = False
 
                 if not ok:
                     back["num_file_invalid"] += 1
-                    self.log.debug("[INVALID] %s: %s" % (file_inner_path, err))
+                    self.log.debug("[INVALID] %s: %s" % (file_inner_path, error))
                     if add_changed or content.get("cert_user_id"):  # If updating own site only add changed user files
                         bad_files.append(file_inner_path)
 
diff --git a/src/main.py b/src/main.py
index 57735ae2..181d883b 100644
--- a/src/main.py
+++ b/src/main.py
@@ -272,18 +272,19 @@ class Actions(object):
         for content_inner_path in site.content_manager.contents:
             s = time.time()
             logging.info("Verifing %s signature..." % content_inner_path)
-            err = None
+            error = None
             try:
                 file_correct = site.content_manager.verifyFile(
                     content_inner_path, site.storage.open(content_inner_path, "rb"), ignore_same=False
                 )
             except Exception as err:
                 file_correct = False
+                error = err
 
             if file_correct is True:
                 logging.info("[OK] %s (Done in %.3fs)" % (content_inner_path, time.time() - s))
             else:
-                logging.error("[ERROR] %s: invalid file: %s!" % (content_inner_path, err))
+                logging.error("[ERROR] %s: invalid file: %s!" % (content_inner_path, error))
                 input("Continue?")
                 bad_files += content_inner_path
 

From 433b32417386d46d36025e6dc159929065bb710c Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 25 Jul 2023 15:11:11 +0000
Subject: [PATCH 242/333] windows os .exe build instruction

---
 README.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/README.md b/README.md
index 3d5a64f2..0716c869 100644
--- a/README.md
+++ b/README.md
@@ -165,6 +165,13 @@ Install autoconf and other basic development tools, python3 and pip, then procee
 - [for full tor anonymity launch this instead] `python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151 --tor always`
 - navigate to http://127.0.0.1:43110 in your favourite browser!
 
+To build .exe
+
+- follow the same steps as above, but additionally
+- `pip install pyinstaller`
+- `pyinstaller -p src -p plugins --hidden-import merkletools --hidden-import lib.bencode_open --hidden-import Crypt.Crypt --hidden-import Db.DbQuery --hidden-import lib.subtl --hidden-import lib.subtl.subtl --hidden-import sockshandler --add-data "src;src" --add-data "plugins;plugins" --clean zeronet.py`
+- dist/zeronet should contain working zeronet.exe!
+
 ## Current limitations
 
 * File transactions are not compressed

From a8c2117a55c68605c1d91fc030caefb4e522d45f Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 25 Jul 2023 15:11:33 +0000
Subject: [PATCH 243/333] CHANGELOG & revision bump

---
 CHANGELOG.md  | 5 +++++
 src/Config.py | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9186fbb5..32a7ac44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,11 @@
 ### zeronet-conservancy 0.7.9+
 - update merkletools dependency to avoid legacy pysha3 (@caryoscelus)
 - fix ReDoS in file editor (UiFileManager plugin) due to outdated codemirror (@caryoscelus)
+- more anonymous UPnP (thanks to 0netdwf for reporting)
+- remove old zeroname plugins (.bit deprecation)
+- sitePublish --recursive option
+- windows build instruction improvement
+- various improvements
 
 ### zeronet-conservancy 0.7.9 (2023-07-02) (f966a4203fe33bd9f35)
 maintainers: @caryoscelus -> none
diff --git a/src/Config.py b/src/Config.py
index 8a101cde..fc30a924 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -187,7 +187,7 @@ class Config(object):
         self.version = "0.7.9+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5110
+        self.rev = 5115
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None

From 0811902ff6849320b19bcb336e748f64a66eb409 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 25 Jul 2023 18:59:09 +0000
Subject: [PATCH 244/333] Disable third-party access to 0net server.

This previously enabled clearnet sites to detect if user is running 0net instance
on their machine as well as to detect which 0net sites are downloaded.

Check online at https://riza-committee.github.io/demos/0scan.html

Intra-0net version of this is still available at
http://127.0.0.1:43110/1ScanCY9fjmjanDt7NwvyNQCL16hqWnVM/
---
 src/Ui/UiRequest.py | 24 ++++++++++++++++++++++--
 1 file changed, 22 insertions(+), 2 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 281a5e5c..8f1e4c18 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -282,13 +282,17 @@ class UiRequest(object):
 
     # Send response headers
     def sendHeader(self, status=200, content_type="text/html", noscript=False, allow_ajax=False, script_nonce=None, extra_headers=[]):
+        ref = self.env.get("HTTP_REFERER")
+        url = self.getRequestUrl()
+        if status != 404 and ref and not self.isSameHost(ref, url):
+            # pretend nothing is here for third-party access
+            return self.error404()
+
         headers = {}
         headers["Version"] = "HTTP/1.1"
         headers["Connection"] = "Keep-Alive"
         headers["Keep-Alive"] = "max=25, timeout=30"
         headers["X-Frame-Options"] = "SAMEORIGIN"
-        if content_type != "text/html" and self.env.get("HTTP_REFERER") and self.isSameOrigin(self.getReferer(), self.getRequestUrl()):
-            headers["Access-Control-Allow-Origin"] = "*"  # Allow load font files from css
 
         if noscript:
             headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src *; font-src * data:; media-src *; style-src * 'unsafe-inline';"
@@ -605,7 +609,23 @@ class UiRequest(object):
         self.server.add_nonces.append(add_nonce)
         return add_nonce
 
+    def isSameHost(self, url_a, url_b):
+        """Check if urls have the same HOST (to prevent leaking resources to clearnet sites)"""
+        if not url_a or not url_b:
+            return False
+
+        url_a = url_a.replace("/raw/", "/")
+        url_b = url_b.replace("/raw/", "/")
+
+        origin_pattern = "http[s]{0,1}://(.*?/).*"
+
+        origin_a = re.sub(origin_pattern, "\\1", url_a)
+        origin_b = re.sub(origin_pattern, "\\1", url_b)
+
+        return origin_a == origin_b
+
     def isSameOrigin(self, url_a, url_b):
+        """Check if 0net origin is the same"""
         if not url_a or not url_b:
             return False
 

From f336cd02bd9cdc3893741cd9fa110431e5929ebd Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 25 Jul 2023 20:55:40 +0000
Subject: [PATCH 245/333] More sophisticated detection of cross-site info leak

see previous commit for more info
---
 src/Ui/UiRequest.py | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 8f1e4c18..b5d1736e 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -282,9 +282,15 @@ class UiRequest(object):
 
     # Send response headers
     def sendHeader(self, status=200, content_type="text/html", noscript=False, allow_ajax=False, script_nonce=None, extra_headers=[]):
-        ref = self.env.get("HTTP_REFERER")
         url = self.getRequestUrl()
-        if status != 404 and ref and not self.isSameHost(ref, url):
+        referer = self.env.get('HTTP_REFERER')
+        origin = self.env.get('HTTP_ORIGIN')
+        fetch_site = self.env.get('HTTP_SEC_FETCH_SITE')
+        fetch_mode = self.env.get('HTTP_SEC_FETCH_MODE')
+        not_same_ref = referer and not self.isSameHost(referer, url)
+        not_same_origin = origin and not self.isSameHost(origin, url)
+        cross_site_not_navigate = not referer and fetch_site == 'cross-site' and not fetch_mode == 'navigate'
+        if status != 404 and (not_same_ref or not_same_origin or cross_site_not_navigate):
             # pretend nothing is here for third-party access
             return self.error404()
 

From eeaded23f9d3acc587ae93976e6e20ba66be5e7e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 26 Jul 2023 07:14:14 +0000
Subject: [PATCH 246/333] Update bootstrap url

---
 bootstrap.url | 2 +-
 src/Config.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/bootstrap.url b/bootstrap.url
index f23462eb..8b287997 100644
--- a/bootstrap.url
+++ b/bootstrap.url
@@ -1 +1 @@
-https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.9/data-default-2023-07-23.zip
\ No newline at end of file
+https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.10/data-default-2023-07-26.zip
\ No newline at end of file
diff --git a/src/Config.py b/src/Config.py
index 28a6e19e..f971146b 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -260,7 +260,7 @@ class Config(object):
         self.parser.add_argument('--disable_udp', help='Disable UDP connections', action='store_true')
         self.parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port')
         self.parser.add_argument('--bind', help='Bind outgoing sockets to this address', metavar='ip')
-        self.parser.add_argument('--bootstrap_url', help='URL of file with link to bootstrap bundle', default='https://raw.githubusercontent.com/caryoscelus/zeronet-conservancy/bootstrap/bootstrap.url', type=str)
+        self.parser.add_argument('--bootstrap_url', help='URL of file with link to bootstrap bundle', default='https://raw.githubusercontent.com/zeronet-conservancy/zeronet-conservancy/master/bootstrap.url', type=str)
         self.parser.add_argument('--disable_bootstrap', help='Disable downloading bootstrap information from clearnet', action='store_true')
         self.parser.add_argument('--trackers', help='Bootstraping torrent trackers', default=[], metavar='protocol://address', nargs='*')
         self.parser.add_argument('--trackers_file', help='Load torrent trackers dynamically from a file', default=['{data_dir}/15CEFKBRHFfAP9rmL6hhLmHoXrrgmw4B5o/cache/1/Syncronite.html'], metavar='path', nargs='*')

From 40ae09dca88d813b760e6cf23598f29d29f17eb9 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 26 Jul 2023 07:56:31 +0000
Subject: [PATCH 247/333] py3.8 compat

---
 src/main.py        |  5 +++--
 src/util/compat.py | 16 ++++++++++++++++
 2 files changed, 19 insertions(+), 2 deletions(-)
 create mode 100644 src/util/compat.py

diff --git a/src/main.py b/src/main.py
index 7f5c1b33..5c69c884 100644
--- a/src/main.py
+++ b/src/main.py
@@ -3,6 +3,7 @@ import sys
 import stat
 import time
 import logging
+from util.compat import *
 
 startup_errors = []
 def startupError(msg):
@@ -54,12 +55,12 @@ def importBundle(bundle):
         else:
             prefix = ''
         top_2 = list(set(filter(lambda f: len(f)>0,
-                                map(lambda f: f.removeprefix(prefix).split('/')[0], all_files))))
+                                map(lambda f: removeprefix(f, prefix).split('/')[0], all_files))))
         for d in top_2:
             if isValidAddress(d):
                 logging.info(f'unpack {d} into {config.data_dir}')
                 for fname in filter(lambda f: f.startswith(prefix+d) and not f.endswith('/'), all_files):
-                    tgt = config.data_dir + '/' + fname.removeprefix(prefix)
+                    tgt = config.data_dir + '/' + removeprefix(fname, prefix)
                     logging.info(f'-- {fname} --> {tgt}')
                     info = zf.getinfo(fname)
                     info.filename = tgt
diff --git a/src/util/compat.py b/src/util/compat.py
new file mode 100644
index 00000000..f41e67b2
--- /dev/null
+++ b/src/util/compat.py
@@ -0,0 +1,16 @@
+import sys
+
+if sys.version_info.major == 3 and sys.version_info.minor < 9:
+    def removeprefix(s, prefix, /):
+        if s.startswith(prefix):
+            return s[len(prefix):]
+        return s
+    def removesuffix(s, suffix, /):
+        if s.endswith(suffix):
+            return s[:-len(suffix)]
+        return s
+else:
+    def removeprefix(s, prefix, /):
+        return s.removeprefix(prefix)
+    def removesuffix(s, suffix, /):
+        return s.removesuffix(suffix)

From 18d35d3bed4f0683e99f8af5a86a8d76ed866e1e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 26 Jul 2023 08:01:47 +0000
Subject: [PATCH 248/333] v0.7.10

---
 CHANGELOG.md  | 8 +++++++-
 src/Config.py | 4 ++--
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32a7ac44..b228efca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,16 @@
-### zeronet-conservancy 0.7.9+
+### zeronet-conservancy 0.7.10+
+
+### zeronet-conservancy 0.7.10 (2023-07-26)
+prepared by @caryoscelus
 - update merkletools dependency to avoid legacy pysha3 (@caryoscelus)
 - fix ReDoS in file editor (UiFileManager plugin) due to outdated codemirror (@caryoscelus)
 - more anonymous UPnP (thanks to 0netdwf for reporting)
 - remove old zeroname plugins (.bit deprecation)
 - sitePublish --recursive option
 - windows build instruction improvement
+- importBundle command line action
+- use Syncronite tracker site by default
+- fix leak of local 0net sites to clearnet sites (originally reported by @egosown/beardog)
 - various improvements
 
 ### zeronet-conservancy 0.7.9 (2023-07-02) (f966a4203fe33bd9f35)
diff --git a/src/Config.py b/src/Config.py
index f971146b..2366b89c 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -12,10 +12,10 @@ import time
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.9+"
+        self.version = "0.7.10"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5115
+        self.rev = 5120
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None

From 714729edab7df31030ed6c3db3661f970c8eaaed Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 29 Jul 2023 16:47:11 +0000
Subject: [PATCH 249/333] New development cycle

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b228efca..b59e09fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
 ### zeronet-conservancy 0.7.10+
 
-### zeronet-conservancy 0.7.10 (2023-07-26)
+### zeronet-conservancy 0.7.10 (2023-07-26) (18d35d3bed4f0683e99)
 prepared by @caryoscelus
 - update merkletools dependency to avoid legacy pysha3 (@caryoscelus)
 - fix ReDoS in file editor (UiFileManager plugin) due to outdated codemirror (@caryoscelus)

From e79924d502d5b7d0e1b7354d5d9c4565c8631386 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 29 Jul 2023 17:17:06 +0000
Subject: [PATCH 250/333] Fix downloading GeoIP database

fixes #222
---
 plugins/Sidebar/SidebarPlugin.py | 19 +++++++++----------
 1 file changed, 9 insertions(+), 10 deletions(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index c117de0e..b8c5f0f3 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -602,7 +602,7 @@ class UiWebsocketPlugin(object):
     def downloadGeoLiteDb(self, db_path):
         import gzip
         import shutil
-        from util import helper
+        import requests
 
         if config.offline or config.tor == 'always':
             return False
@@ -617,19 +617,18 @@ class UiWebsocketPlugin(object):
             downloadl_err = None
             try:
                 # Download
-                response = helper.httpRequest(db_url)
-                data_size = response.getheader('content-length')
+                response = requests.get(db_url, stream=True)
+                data_size = response.headers.get('content-length')
+                if data_size is None:
+                    data.write(response.content)
+                data_size = int(data_size)
                 data_recv = 0
                 data = io.BytesIO()
-                while True:
-                    buff = response.read(1024 * 512)
-                    if not buff:
-                        break
+                for buff in response.iter_content(chunk_size=1024 * 512):
                     data.write(buff)
                     data_recv += 1024 * 512
-                    if data_size:
-                        progress = int(float(data_recv) / int(data_size) * 100)
-                        self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], progress])
+                    progress = int(float(data_recv) / data_size * 100)
+                    self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], progress])
                 self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell())
                 data.seek(0)
 

From b358435016231a3f53c37224bbf68860aaf586f9 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 29 Jul 2023 17:20:45 +0000
Subject: [PATCH 251/333] Remove dead code

---
 src/util/helper.py | 38 --------------------------------------
 1 file changed, 38 deletions(-)

diff --git a/src/util/helper.py b/src/util/helper.py
index a0daa557..af65f727 100644
--- a/src/util/helper.py
+++ b/src/util/helper.py
@@ -195,44 +195,6 @@ def mergeDicts(dicts):
     return dict(back)
 
 
-# Request https url using gevent SSL error workaround
-def httpRequest(url, as_file=False):
-    if url.startswith("http://"):
-        import urllib.request
-        response = urllib.request.urlopen(url)
-    else:  # Hack to avoid Python gevent ssl errors
-        import socket
-        import http.client
-        import ssl
-
-        host, request = re.match("https://(.*?)(/.*?)$", url).groups()
-
-        conn = http.client.HTTPSConnection(host)
-        sock = socket.create_connection((conn.host, conn.port), conn.timeout, conn.source_address)
-
-        context = ssl.create_default_context()
-        context.minimum_version = ssl.TLSVersion.TLSv1_2
-
-        conn.sock = context.wrap_socket(sock, conn.key_file, conn.cert_file)
-        conn.request("GET", request)
-        response = conn.getresponse()
-        if response.status in [301, 302, 303, 307, 308]:
-            logging.info("Redirect to: %s" % response.getheader('Location'))
-            response = httpRequest(response.getheader('Location'))
-
-    if as_file:
-        import io
-        data = io.BytesIO()
-        while True:
-            buff = response.read(1024 * 16)
-            if not buff:
-                break
-            data.write(buff)
-        return data
-    else:
-        return response
-
-
 def timerCaller(secs, func, *args, **kwargs):
     gevent.spawn_later(secs, timerCaller, secs, func, *args, **kwargs)
     func(*args, **kwargs)

From a509032c8e4bbfc5c81c80d44d79660d7eaf060a Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 29 Jul 2023 18:03:30 +0000
Subject: [PATCH 252/333] Drop attempts at py<3.6 compatibility

Wake up, it's 2023 now. Also f-strings has been used prominently already
---
 src/Content/ContentManager.py | 5 +----
 src/Db/Db.py                  | 5 +----
 2 files changed, 2 insertions(+), 8 deletions(-)

diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index bcf96c0b..a06ba523 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -928,10 +928,7 @@ class ContentManager(object):
                     new_content = file
                 else:
                     try:
-                        if sys.version_info.major == 3 and sys.version_info.minor < 6:
-                            new_content = json.loads(file.read().decode("utf8"))
-                        else:
-                            new_content = json.load(file)
+                        new_content = json.load(file)
                     except Exception as err:
                         raise VerifyError(f"Invalid json file: {err}")
                 if inner_path in self.contents:
diff --git a/src/Db/Db.py b/src/Db/Db.py
index 3d4b6d7d..6ee3a977 100644
--- a/src/Db/Db.py
+++ b/src/Db/Db.py
@@ -377,10 +377,7 @@ class Db(object):
                 if file_path.endswith("json.gz"):
                     file = helper.limitedGzipFile(fileobj=file)
 
-                if sys.version_info.major == 3 and sys.version_info.minor < 6:
-                    data = json.loads(file.read().decode("utf8"))
-                else:
-                    data = json.load(file)
+                data = json.load(file)
         except Exception as err:
             self.log.debug("Json file %s load error: %s" % (file_path, err))
             data = {}

From ebd81dad9d99745e903ed905644101684a16e682 Mon Sep 17 00:00:00 2001
From: slrslr <6596726+slrslr@users.noreply.github.com>
Date: Sun, 30 Jul 2023 05:17:15 +0000
Subject: [PATCH 253/333] Update README.md

Adding new simplified installation commands. It should allow easier installation and update on most Linux distributions using 2 commands.
---
 README.md | 42 +++++++++++++++++++++++++-----------------
 1 file changed, 25 insertions(+), 17 deletions(-)

diff --git a/README.md b/README.md
index 0716c869..0a1f641f 100644
--- a/README.md
+++ b/README.md
@@ -6,15 +6,14 @@
 
 [по-русски](README-ru.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md)
 
-zeronet-conservancy is a fork/continuation of [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) project
+Zeronet-Conservancy is a fork/continuation of [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) project
 (that has been abandoned by its creator) that is dedicated to sustaining existing p2p network and developing
 its values of decentralization and freedom, while gradually switching to a better designed network
 
 ## No active maintainer warning
 
 This fork was created and maintained by @caryoscelus, but due to vanishing interest and in order to avoid having
-another one-person project, they stepped down. This means there currently is no active maintainer (you're are
-welcome to become one!), however some development might still happen.
+another one-person project, the development is limitted.
 
 ## Why fork?
 
@@ -75,25 +74,34 @@ Following links relate to original ZeroNet:
 
 ### Install from your distribution repository
 
-- NixOS: https://search.nixos.org/packages?channel=22.05&show=zeronet-conservancy&type=packages&query=zeronet-conservancy (and see below)
+- NixOS: [zeronet-conservancy packages search](https://search.nixos.org/packages?from=0&size=50&sort=relevance&type=packages&query=zeronet-conservancy) (and see below)
 - ArchLinux: [latest release](https://aur.archlinux.org/packages/zeronet-conservancy), [fresh git version](https://aur.archlinux.org/packages/zeronet-conservancy-git)
 
 ### Install from Nix package manager (Linux or MacOS)
 
-```
-# install & configure nix package manager
-nix-env -iA nixpkgs.zeronet-conservancy
-```
-
+if you're on NixOS, install & configure nix package manager:
+```nix-env -iA nixpkgs.zeronet-conservancy```
 or
-
-`nix-env -iA nixos.zeronet-conservancy`
-
-if you're on NixOS
-
+```nix-env -iA nixos.zeronet-conservancy```
 (thanks @fgaz for making & maintaining the package)
 
-### Install from source
+### Install from source - simplified, using Git and Python PIP
+
+1. Install Git and Python PIP package:
+
+- Debian and Ubuntu based: `sudo apt install git python3-pip -y`
+- Red Hat and Fedora based: `yum install epel-release -y 2>/dev/null;yum install git python3 python3-wheel`
+- Fedora based dandified: `sudo dnf install git python3-pip python3-wheel -y`
+- Arch and Manjaro based: `sudo pacman -S git python-pip -v --no-confirm`
+- openSUSE: `sudo zypper install python3-pip python3-setuptools python3-wheel`
+
+2. Clone Github repository and install required Python modules. First edit zndir path at the begining of the command, to be the path where you want to store Zeronet-Conservancy:
+
+`zndir="/home/user/myapps/zeronet" ; if [[ ! -d "$zndir" ]]; then git clone --recursive "https://github.com/zeronet-conservancy/zeronet-conservancy.git" "$zndir" && cd "$zndir"||exit; else cd "$zndir";git pull origin master; fi; cd "$zndir" && pip install -r requirements.txt|grep -v "already satisfied"; echo "Try to run: python3 $(pwd)/zeronet.py"`
+
+(This command can also be used to keep Zeronet-Conservancy up to date)
+
+### Install from source - detailed
 
 #### System dependencies
 
@@ -143,9 +151,9 @@ Install autoconf and other basic development tools, python3 and pip, then procee
  - after installing general dependencies and cloning repo (as above), run `start-venv.sh` which will create a virtual env for you and install python requirements
  - more convenience scripts to be added soon
 
-### Building under windows os
+### Building under Windows OS
 
-(this instruction is work-in-progress, please help us test it and improve it!)
+(These instructions are work-in-progress, please help us test it and improve it!)
 
 - install python from https://www.python.org/downloads/
 - install some windows compiler suitable for python , this proved to be the most difficult part for me as non-windows user (see here https://wiki.python.org/moin/WindowsCompilers and i'll link more references later)

From a334eefdf733449455c3eb312a02a2b8d0dd1128 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 30 Jul 2023 19:00:43 +0000
Subject: [PATCH 254/333] Bump version

---
 src/Config.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index 2366b89c..e92c653c 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -12,10 +12,10 @@ import time
 class Config(object):
 
     def __init__(self, argv):
-        self.version = "0.7.10"
+        self.version = "0.7.10+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5120
+        self.rev = 5121
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None

From 30db5a4652dd65f7de47871dd41161d45de11c34 Mon Sep 17 00:00:00 2001
From: Vadim Ushakov <igeekless@gmail.com>
Date: Wed, 20 Oct 2021 19:01:55 +0700
Subject: [PATCH 255/333] Fix
 https://github.com/HelloZeroNet/ZeroNet/issues/2757

---
 src/util/SafeRe.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/util/SafeRe.py b/src/util/SafeRe.py
index 6018e2d3..20827f38 100644
--- a/src/util/SafeRe.py
+++ b/src/util/SafeRe.py
@@ -15,9 +15,10 @@ def isSafePattern(pattern):
     if unsafe_pattern_match:
         raise UnsafePatternError("Potentially unsafe part of the pattern: %s in %s" % (unsafe_pattern_match.group(0), pattern))
 
-    repetitions = re.findall(r"\.[\*\{\+]", pattern)
-    if len(repetitions) >= 10:
-        raise UnsafePatternError("More than 10 repetitions of %s in %s" % (repetitions[0], pattern))
+    repetitions1 = re.findall(r"\.[\*\{\+]", pattern)
+    repetitions2 = re.findall(r"[^(][?]", pattern)
+    if len(repetitions1) + len(repetitions2) >= 10:
+        raise UnsafePatternError("More than 10 repetitions in %s" % pattern)
 
     return True
 

From 5fadd5f9bda78e307e82e52e4f889c2ff755e950 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 31 Jul 2023 08:28:29 +0000
Subject: [PATCH 256/333] Improve SafeRe code readability

function isSafePattern was never used as boolean function, its only
useful behaviour being raising exception on bad regexp, so it's renamed
and reused accordingly
---
 plugins/Sidebar/ConsolePlugin.py |  5 ++---
 src/util/SafeRe.py               | 12 ++++++------
 2 files changed, 8 insertions(+), 9 deletions(-)

diff --git a/plugins/Sidebar/ConsolePlugin.py b/plugins/Sidebar/ConsolePlugin.py
index 15f6a1ba..12d49fbf 100644
--- a/plugins/Sidebar/ConsolePlugin.py
+++ b/plugins/Sidebar/ConsolePlugin.py
@@ -14,8 +14,7 @@ class WsLogStreamer(logging.StreamHandler):
         self.ui_websocket = ui_websocket
 
         if filter:
-            if not SafeRe.isSafePattern(filter):
-                raise Exception("Not a safe prex pattern")
+            SafeRe.guard(filter):
             self.filter_re = re.compile(".*" + filter)
         else:
             self.filter_re = None
@@ -55,7 +54,7 @@ class UiWebsocketPlugin(object):
         pos_start = log_file.tell()
         lines = []
         if filter:
-            assert SafeRe.isSafePattern(filter)
+            SafeRe.guard(filter)
             filter_re = re.compile(".*" + filter)
 
         last_match = False
diff --git a/src/util/SafeRe.py b/src/util/SafeRe.py
index 20827f38..30aa1f29 100644
--- a/src/util/SafeRe.py
+++ b/src/util/SafeRe.py
@@ -7,7 +7,8 @@ class UnsafePatternError(Exception):
 cached_patterns = {}
 
 
-def isSafePattern(pattern):
+def guard(pattern):
+    '''Checks if pattern is safe and raises exception if it isn't'''
     if len(pattern) > 255:
         raise UnsafePatternError("Pattern too long: %s characters in %s" % (len(pattern), pattern))
 
@@ -20,14 +21,13 @@ def isSafePattern(pattern):
     if len(repetitions1) + len(repetitions2) >= 10:
         raise UnsafePatternError("More than 10 repetitions in %s" % pattern)
 
-    return True
-
 
 def match(pattern, *args, **kwargs):
+    '''Guard for safety, compile, cache and match regexp'''
     cached_pattern = cached_patterns.get(pattern)
     if cached_pattern:
         return cached_pattern.match(*args, **kwargs)
     else:
-        if isSafePattern(pattern):
-            cached_patterns[pattern] = re.compile(pattern)
-            return cached_patterns[pattern].match(*args, **kwargs)
+        guard(pattern)
+        cached_patterns[pattern] = re.compile(pattern)
+        return cached_patterns[pattern].match(*args, **kwargs)

From 7d4e66e07b6ced6bfc9c13cf3e9296c21ac517ef Mon Sep 17 00:00:00 2001
From: slrslr <6596726+slrslr@users.noreply.github.com>
Date: Mon, 31 Jul 2023 18:34:11 +0000
Subject: [PATCH 257/333] Update README.md

Mention first Windows build available for download.
---
 README.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/README.md b/README.md
index 0a1f641f..1319b13a 100644
--- a/README.md
+++ b/README.md
@@ -151,6 +151,10 @@ Install autoconf and other basic development tools, python3 and pip, then procee
  - after installing general dependencies and cloning repo (as above), run `start-venv.sh` which will create a virtual env for you and install python requirements
  - more convenience scripts to be added soon
 
+### Windows build
+
+Download and extract .zip archive [zeronet-conservancy-0.7.10-unofficial-win64.zip](https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.10/zeronet-conservancy-0.7.10-unofficial-win64.zip)
+
 ### Building under Windows OS
 
 (These instructions are work-in-progress, please help us test it and improve it!)

From b95b979c7fc31a337fc3ca545bab51a0576a1227 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 10 Aug 2023 20:30:24 +0000
Subject: [PATCH 258/333] README: partially rewrite installing

---
 README.md | 64 +++++++++++++++++++++++++++++++++----------------------
 1 file changed, 38 insertions(+), 26 deletions(-)

diff --git a/README.md b/README.md
index 1319b13a..00ce2fda 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 
 [по-русски](README-ru.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md)
 
-Zeronet-Conservancy is a fork/continuation of [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) project
+`zeronet-conservancy` is a fork/continuation of [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) project
 (that has been abandoned by its creator) that is dedicated to sustaining existing p2p network and developing
 its values of decentralization and freedom, while gradually switching to a better designed network
 
@@ -79,40 +79,39 @@ Following links relate to original ZeroNet:
 
 ### Install from Nix package manager (Linux or MacOS)
 
-if you're on NixOS, install & configure nix package manager:
-```nix-env -iA nixpkgs.zeronet-conservancy```
-or
-```nix-env -iA nixos.zeronet-conservancy```
+ - install & configure nix package manager (if needed)
+ - `nix-env -iA nixpkgs.zeronet-conservancy`
+
+or add `zeronet-conservancy` to your system configuration if you're on NixOS
+
 (thanks @fgaz for making & maintaining the package)
 
-### Install from source - simplified, using Git and Python PIP
-
-1. Install Git and Python PIP package:
-
-- Debian and Ubuntu based: `sudo apt install git python3-pip -y`
-- Red Hat and Fedora based: `yum install epel-release -y 2>/dev/null;yum install git python3 python3-wheel`
-- Fedora based dandified: `sudo dnf install git python3-pip python3-wheel -y`
-- Arch and Manjaro based: `sudo pacman -S git python-pip -v --no-confirm`
-- openSUSE: `sudo zypper install python3-pip python3-setuptools python3-wheel`
-
-2. Clone Github repository and install required Python modules. First edit zndir path at the begining of the command, to be the path where you want to store Zeronet-Conservancy:
-
-`zndir="/home/user/myapps/zeronet" ; if [[ ! -d "$zndir" ]]; then git clone --recursive "https://github.com/zeronet-conservancy/zeronet-conservancy.git" "$zndir" && cd "$zndir"||exit; else cd "$zndir";git pull origin master; fi; cd "$zndir" && pip install -r requirements.txt|grep -v "already satisfied"; echo "Try to run: python3 $(pwd)/zeronet.py"`
-
-(This command can also be used to keep Zeronet-Conservancy up to date)
-
-### Install from source - detailed
+### Install from source
 
 #### System dependencies
 
 ##### Generic unix-like (including mac os x)
 
 Install autoconf and other basic development tools, python3 and pip, then proceed to "building python dependencies"
+(if running fails due to missing dependency, please report it/make pull request to fix dependency list)
 
 ##### Apt-based (debian, ubuntu, etc)
  - `sudo apt update`
  - `sudo apt install pkg-config libffi-dev python3-pip python3-venv python3-dev build-essential`
 
+##### Red Hat and Fedora based
+ - `yum install epel-release -y 2>/dev/null`
+ - `yum install git python3 python3-wheel`
+
+##### Fedora based dandified
+ - `sudo dnf install git python3-pip python3-wheel -y`
+
+##### openSUSE
+ - `sudo zypper install python3-pip python3-setuptools python3-wheel`
+
+##### Arch and Manjaro based
+ - `sudo pacman -S git python-pip -v --no-confirm`
+
 ##### Android/Termux
  - install [Termux](https://termux.com/) (in Termux you can install packages via `pkg install <package-names>`)
  - `pkg update`
@@ -122,7 +121,7 @@ Install autoconf and other basic development tools, python3 and pip, then procee
  - (optional) `pkg install tor`
  - (optional) run tor via `tor --ControlPort 9051 --CookieAuthentication 1` command (you can then open new session by swiping to the right)
 
-#### Building python dependencies venv & running
+#### Building python dependencies, venv & running
  - clone this repo (NOTE: on Android/Termux you should clone it into "home" folder of Termux, because virtual environment cannot live in `storage/`)
  - `python3 -m venv venv` (make python virtual environment, the last `venv` is just a name, if you use different you should replace it in later commands)
  - `source venv/bin/activate` (activate environment)
@@ -147,13 +146,26 @@ Install autoconf and other basic development tools, python3 and pip, then procee
 - or: `docker compose up -d 0net-tor` for run 0net and tor in one container.
 (please check if these instructions are still accurate)
 
+#### Alternative one-liner (installing python dependencies globally)
+
+Clone Github repository and install required Python modules. First
+edit zndir path at the begining of the command, to be the path where
+you want to store `zeronet-conservancy`:
+
+`zndir="/home/user/myapps/zeronet" ; if [[ ! -d "$zndir" ]]; then git clone --recursive "https://github.com/zeronet-conservancy/zeronet-conservancy.git" "$zndir" && cd "$zndir"||exit; else cd "$zndir";git pull origin master; fi; cd "$zndir" && pip install -r requirements.txt|grep -v "already satisfied"; echo "Try to run: python3 $(pwd)/zeronet.py"`
+
+(This command can also be used to keep `zeronet-conservancy` up to date)
+
 #### Alternative script
- - after installing general dependencies and cloning repo (as above), run `start-venv.sh` which will create a virtual env for you and install python requirements
+ - after installing general dependencies and cloning repo (as above),
+   run `start-venv.sh` which will create a virtual env for you and
+   install python requirements
  - more convenience scripts to be added soon
 
-### Windows build
+### (unofficial) Windows OS build
 
-Download and extract .zip archive [zeronet-conservancy-0.7.10-unofficial-win64.zip](https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.10/zeronet-conservancy-0.7.10-unofficial-win64.zip)
+Download and extract .zip archive
+[zeronet-conservancy-0.7.10-unofficial-win64.zip](https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.10/zeronet-conservancy-0.7.10-unofficial-win64.zip)
 
 ### Building under Windows OS
 

From 4153f60258ef9d941e21eede2973ec69f3fd17be Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 18 Aug 2023 16:12:31 +0000
Subject: [PATCH 259/333] Don't accept file update if it's from muted user

refs https://github.com/HelloZeroNet/ZeroNet/issues/1855
---
 plugins/ContentFilter/ContentFilterPlugin.py | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py
index 6cec1bc3..8a9db880 100644
--- a/plugins/ContentFilter/ContentFilterPlugin.py
+++ b/plugins/ContentFilter/ContentFilterPlugin.py
@@ -216,6 +216,18 @@ class SiteStoragePlugin(object):
             filter_storage.includeUpdateAll()
         return super(SiteStoragePlugin, self).onUpdated(inner_path, file=file)
 
+@PluginManager.registerTo("FileRequest")
+class FileRequestPlugin:
+    def actionUpdate(self, params):
+        inner_path = params.get('inner_path', '')
+        self.log.info(f'FileRequest.actionUpdate {inner_path}')
+        matches = re.findall('/(1[A-Za-z0-9]{26,35})/', inner_path)
+        for auth_address in matches:
+            if filter_storage.isMuted(auth_address):
+                self.log.info(f'Mute match in FileRequest.actionUpdate: {auth_address}, ignoring {inner_path}')
+                self.response({'ok': f'Thanks, file {inner_path} updated!'})
+                return False
+        return super(FileRequestPlugin, self).actionUpdate(params)
 
 @PluginManager.registerTo("UiRequest")
 class UiRequestPlugin(object):

From 4d4880a5c8d94d85900898f9742f32d2e4e70812 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 18 Aug 2023 16:51:16 +0000
Subject: [PATCH 260/333] Ignore muted files in Site.needFile

---
 plugins/ContentFilter/ContentFilterPlugin.py | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py
index 8a9db880..ec88b3b3 100644
--- a/plugins/ContentFilter/ContentFilterPlugin.py
+++ b/plugins/ContentFilter/ContentFilterPlugin.py
@@ -216,11 +216,21 @@ class SiteStoragePlugin(object):
             filter_storage.includeUpdateAll()
         return super(SiteStoragePlugin, self).onUpdated(inner_path, file=file)
 
+@PluginManager.registerTo("Site")
+class SitePlugin(object):
+    def needFile(self, inner_path, update=False, blocking=True, peer=None, priority=0):
+        self.log.debug(f'needFile {inner_path}')
+        matches = re.findall('/(1[A-Za-z0-9]{26,35})/', inner_path)
+        for auth_address in matches:
+            if filter_storage.isMuted(auth_address):
+                self.log.info(f'Mute match in Site.needFile: {auth_address}, ignoring {inner_path}')
+                return False
+        return super(SitePlugin, self).needFile(inner_path, update, blocking, peer, priority)
+
 @PluginManager.registerTo("FileRequest")
 class FileRequestPlugin:
     def actionUpdate(self, params):
         inner_path = params.get('inner_path', '')
-        self.log.info(f'FileRequest.actionUpdate {inner_path}')
         matches = re.findall('/(1[A-Za-z0-9]{26,35})/', inner_path)
         for auth_address in matches:
             if filter_storage.isMuted(auth_address):

From 21eb421a8f995c5ad6f75f5354e376832efc714d Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 4 Sep 2023 10:26:37 +0000
Subject: [PATCH 261/333] Update data-default

---
 bootstrap.url | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/bootstrap.url b/bootstrap.url
index 8b287997..ec7a3944 100644
--- a/bootstrap.url
+++ b/bootstrap.url
@@ -1 +1 @@
-https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.10/data-default-2023-07-26.zip
\ No newline at end of file
+https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.10/data-default-2023-09-03.zip
\ No newline at end of file

From 598b8222554812d64e12ab93d4a013f1e5ff9b5c Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 4 Sep 2023 21:25:03 +0000
Subject: [PATCH 262/333] Fix typo in Sidebar/ConsolePlugin

---
 plugins/Sidebar/ConsolePlugin.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/plugins/Sidebar/ConsolePlugin.py b/plugins/Sidebar/ConsolePlugin.py
index 12d49fbf..46cd9aca 100644
--- a/plugins/Sidebar/ConsolePlugin.py
+++ b/plugins/Sidebar/ConsolePlugin.py
@@ -14,7 +14,7 @@ class WsLogStreamer(logging.StreamHandler):
         self.ui_websocket = ui_websocket
 
         if filter:
-            SafeRe.guard(filter):
+            SafeRe.guard(filter)
             self.filter_re = re.compile(".*" + filter)
         else:
             self.filter_re = None

From 337f98a479aae4f8ab1424edf209a94274c125ec Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 4 Sep 2023 21:39:46 +0000
Subject: [PATCH 263/333] Add --disable_port_check option

---
 src/Config.py          |  1 +
 src/File/FileServer.py | 10 +++++++---
 2 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index e92c653c..1f62b184 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -256,6 +256,7 @@ class Config(object):
         self.parser.add_argument('--ip_local', help='My local ips', default=ip_local, type=int, metavar='ip', nargs='*')
         self.parser.add_argument('--ip_external', help='Set reported external ip (tested on start if None)', metavar='ip', nargs='*')
         self.parser.add_argument('--offline', help='Disable network communication', action='store_true')
+        self.parser.add_argument('--disable_port_check', help='Disable checking port', action='store_true')
 
         self.parser.add_argument('--disable_udp', help='Disable UDP connections', action='store_true')
         self.parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port')
diff --git a/src/File/FileServer.py b/src/File/FileServer.py
index 41f76817..d1de4761 100644
--- a/src/File/FileServer.py
+++ b/src/File/FileServer.py
@@ -152,9 +152,13 @@ class FileServer(ConnectionServer):
         FileRequest = imp.load_source("FileRequest", "src/File/FileRequest.py").FileRequest
 
     def portCheck(self):
-        if config.offline or config.tor == 'always':
-            msg = "Offline mode" if config.offline else "Tor-only"
-            self.log.info(f'{msg}: port check disabled')
+        if config.offline or config.tor == 'always' or config.disable_port_check:
+            if config.offline:
+                self.log.info(f'Offline mode: port check disabled')
+            elif config.tor == 'always':
+                self.log.info('Tor-only mode: port check disabled')
+            else:
+                self.log.info('Port check disabled')
             res = {"ipv4": None, "ipv6": None}
             self.port_opened = res
             return res

From 77a70e513dffaabbca10cd0659f28dac3d599e2f Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 7 Sep 2023 20:09:33 +0000
Subject: [PATCH 264/333] README: attribution

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 00ce2fda..59fa72f0 100644
--- a/README.md
+++ b/README.md
@@ -146,7 +146,7 @@ Install autoconf and other basic development tools, python3 and pip, then procee
 - or: `docker compose up -d 0net-tor` for run 0net and tor in one container.
 (please check if these instructions are still accurate)
 
-#### Alternative one-liner (installing python dependencies globally)
+#### Alternative one-liner (by @ssdifnskdjfnsdjk) (installing python dependencies globally)
 
 Clone Github repository and install required Python modules. First
 edit zndir path at the begining of the command, to be the path where

From 1eb094bba50fd08d5fba74cba86902f953630815 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 14 Sep 2023 11:03:23 +0000
Subject: [PATCH 265/333] Fix detection of non-writeable start directory

---
 src/Config.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Config.py b/src/Config.py
index e92c653c..d5e459e7 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -71,7 +71,7 @@ class Config(object):
         elif this_file.endswith("/core/src/Config.py"):
             # Running as exe or source is at Application Support directory, put var files to outside of core dir
             start_dir = this_file.replace("/core/src/Config.py", "")
-        elif this_file.endswith("usr/share/zeronet/src/Config.py"):
+        elif not os.access(this_file.replace('/src/Config.py', ''), os.R_OK | os.W_OK):
             # Running from non-writeable location, e.g., AppImage
             start_dir = os.path.expanduser("~/ZeroNet")
         else:

From 537d7337e2aa0dcd9a5917e6c341a18d2b9c2895 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 15 Sep 2023 14:23:19 +0000
Subject: [PATCH 266/333] debug log

---
 src/Worker/Worker.py | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py
index b7111ba1..9adba30d 100644
--- a/src/Worker/Worker.py
+++ b/src/Worker/Worker.py
@@ -7,6 +7,8 @@ from Debug import Debug
 from Config import config
 from Content.ContentManager import VerifyError
 
+import traceback
+
 
 class WorkerDownloadError(Exception):
     pass
@@ -119,13 +121,15 @@ class Worker(object):
                 self.manager.log.error("%s: Error writing: %s (%s: %s)" % (self.key, task["inner_path"], type(err), err))
             raise WorkerIOError(str(err))
 
-    def onTaskVerifyFail(self, task, error_message):
+    def onTaskVerifyFail(self, task, error):
         self.num_failed += 1
         if self.manager.started_task_num < 50 or config.verbose:
-            self.manager.log.debug(
+            self.manager.log.info(
                 "%s: Verify failed: %s, error: %s, failed peers: %s, workers: %s" %
-                (self.key, task["inner_path"], error_message, len(task["failed"]), task["workers_num"])
+                (self.key, task["inner_path"], error, len(task["failed"]), task["workers_num"])
             )
+            # traceback.format_
+            self.manager.log.debug(''.join(traceback.format_exception(error)))
         task["failed"].append(self.peer)
         self.peer.hash_failed += 1
         if self.peer.hash_failed >= max(len(self.manager.tasks), 3) or self.peer.connection_error > 10:

From c9dbc49375e6c17579251982fc6421db3a120d23 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 16 Sep 2023 12:27:00 +0000
Subject: [PATCH 267/333] Delete muted content

---
 plugins/ContentFilter/ContentFilterPlugin.py  | 2 +-
 plugins/ContentFilter/ContentFilterStorage.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py
index ec88b3b3..c4d8bf3d 100644
--- a/plugins/ContentFilter/ContentFilterPlugin.py
+++ b/plugins/ContentFilter/ContentFilterPlugin.py
@@ -58,7 +58,7 @@ class UiWebsocketPlugin(object):
     def actionMuteAdd(self, to, auth_address, cert_user_id, reason):
         self.cmd(
             "prompt",
-            [_["Hide all content from <b>%s</b>?"] % html.escape(cert_user_id), reason, _["Mute"]],
+            [_["Remove all content from <b>%s</b>?"] % html.escape(cert_user_id), reason, _["Mute"]],
             lambda res: self.cbMuteAdd(to, auth_address, cert_user_id, res if res else reason)
         )
 
diff --git a/plugins/ContentFilter/ContentFilterStorage.py b/plugins/ContentFilter/ContentFilterStorage.py
index 289ec2a9..2ad378d6 100644
--- a/plugins/ContentFilter/ContentFilterStorage.py
+++ b/plugins/ContentFilter/ContentFilterStorage.py
@@ -158,7 +158,7 @@ class ContentFilterStorage(object):
             dir_inner_path = helper.getDirname(row["inner_path"])
             for file_name in site.storage.walk(dir_inner_path):
                 if action == "remove":
-                    site.storage.onUpdated(dir_inner_path + file_name, False)
+                    site.storage.delete(dir_inner_path + file_name)
                 else:
                     site.storage.onUpdated(dir_inner_path + file_name)
                 site.onFileDone(dir_inner_path + file_name)

From 19056b408ad9c956c70e4c0237c1ab8e17955228 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 21 Sep 2023 15:28:05 +0000
Subject: [PATCH 268/333] Update gevent to 23.9.0

https://security.snyk.io/vuln/SNYK-PYTHON-GEVENT-5906371
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 887138d8..1059795c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability
-gevent>=20.9.0
+gevent>=23.9.0
 msgpack>=0.6.0
 base58
 # for some reason nobody released fresh merkletools that don't require on outdated pysha3

From c92b8bc56c796a962baec69f42072433c65bb3b7 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 1 Nov 2023 22:12:14 +0000
Subject: [PATCH 269/333] Fix UiServer.getPosted hanging in some circumstances

fixes #198

while it's not exactly clear what causes the difference in behaviour,
but under certain conditions UiServer.getPosted used to hang trying
to readline() POST request (e.g. from UiPassword login). using
read(CONTENT_LENGTH) seems to fix the issue
---
 src/Ui/UiRequest.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index b5d1736e..0f8437d6 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -231,8 +231,12 @@ class UiRequest(object):
     # Return: <dict> Posted variables
     def getPosted(self):
         if self.env['REQUEST_METHOD'] == "POST":
+            try:
+                content_length = int(self.env.get('CONTENT_LENGTH', 0))
+            except ValueError:
+                content_length = 0
             return dict(urllib.parse.parse_qsl(
-                self.env['wsgi.input'].readline().decode()
+                self.env['wsgi.input'].read(content_length).decode()
             ))
         else:
             return {}

From 14e8130acb4a092b143d0a8046f29926f28e1b25 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 14 Nov 2023 23:11:51 +0000
Subject: [PATCH 270/333] Rewrite cross-site and cross-host requests detection

Make sure browsers send referrers so we can track cross-site
requests (could be used to identify which sites user hosts)

This breaks /raw because there are no referrers there

fixes #227
fixes #223
fixes #224
---
 src/Ui/UiRequest.py          | 97 ++++++++++++++++++++++++++++--------
 src/Ui/template/wrapper.html |  2 +-
 2 files changed, 76 insertions(+), 23 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 0f8437d6..fa03bdca 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -49,7 +49,7 @@ class SecurityError(Exception):
 
 
 @PluginManager.acceptPlugins
-class UiRequest(object):
+class UiRequest:
 
     def __init__(self, server, get, env, start_response):
         if server:
@@ -99,8 +99,52 @@ class UiRequest(object):
     def resolveDomain(self, domain):
         return self.server.site_manager.resolveDomainCached(domain)
 
-    # Call the request handler function base on path
+    def isCrossOriginRequest(self):
+        """Prevent detecting sites on this 0net instance
+
+        In particular, we block non-user requests from other hosts as well as
+        cross-site
+        """
+
+        url = self.getRequestUrl()
+        fetch_mode = self.env.get('HTTP_SEC_FETCH_MODE')
+        origin = self.env.get('HTTP_ORIGIN')
+        referer = self.env.get('HTTP_REFERER')
+
+        # Allow all user-initiated requests
+        if fetch_mode == 'navigate':
+            return False
+
+        # Deny requests that cannot be traced
+        if not origin and not referer:
+            return True
+
+        # Deny requests from non-0net origins
+        if origin and not self.isSameHost(origin, url):
+            return True
+
+        # Allow non-site specific requests
+        if self.getRequestSite() == '/':
+            return False
+
+        # Deny cross site requests
+        if not self.isSameOrigin(referer, url):
+            return True
+
+        return False
+
     def route(self, path):
+        """Main routing
+
+        If no internal action is performed, calls action[Path] from plugins
+
+        This behaviour is not very flexible or easy to follow, so perhaps
+        we'd want something else..
+        """
+
+        if self.isCrossOriginRequest():
+            return self.error404()
+
         # Restict Ui access by ip
         if config.ui_restrict and self.env['REMOTE_ADDR'] not in config.ui_restrict:
             return self.error403(details=False)
@@ -284,37 +328,43 @@ class UiRequest(object):
             is_script_nonce_supported = True
         return is_script_nonce_supported
 
+    def getRequestSite(self):
+        """Return 0net site addr associated with current request
+
+        If request is site-agnostic, returns /
+        """
+        path = self.env["PATH_INFO"]
+        match = re.match(r'(/raw)?(?P<site>/1[a-zA-Z0-9]*)', path)
+        if not match:
+            match = re.match(r'(/raw)?/(?P<domain>[a-zA-Z0-9\.\-_]*)', path)
+            if match:
+                domain = match.group('domain')
+                if self.isDomain(domain):
+                    addr = self.resolveDomain(domain)
+                    return '/'+addr
+            return '/'
+        return match.group('site')
+
     # Send response headers
     def sendHeader(self, status=200, content_type="text/html", noscript=False, allow_ajax=False, script_nonce=None, extra_headers=[]):
-        url = self.getRequestUrl()
-        referer = self.env.get('HTTP_REFERER')
-        origin = self.env.get('HTTP_ORIGIN')
-        fetch_site = self.env.get('HTTP_SEC_FETCH_SITE')
-        fetch_mode = self.env.get('HTTP_SEC_FETCH_MODE')
-        not_same_ref = referer and not self.isSameHost(referer, url)
-        not_same_origin = origin and not self.isSameHost(origin, url)
-        cross_site_not_navigate = not referer and fetch_site == 'cross-site' and not fetch_mode == 'navigate'
-        if status != 404 and (not_same_ref or not_same_origin or cross_site_not_navigate):
-            # pretend nothing is here for third-party access
-            return self.error404()
-
         headers = {}
         headers["Version"] = "HTTP/1.1"
         headers["Connection"] = "Keep-Alive"
         headers["Keep-Alive"] = "max=25, timeout=30"
         headers["X-Frame-Options"] = "SAMEORIGIN"
+        headers["Referrer-Policy"] = "same-origin"
 
         if noscript:
             headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src *; font-src * data:; media-src *; style-src * 'unsafe-inline';"
         elif script_nonce and self.isScriptNonceSupported():
-            headers["Content-Security-Policy"] = "default-src 'none'; script-src 'nonce-{0}'; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src 'self' blob:".format(script_nonce)
+            headers["Content-Security-Policy"] = f"default-src 'none'; script-src 'nonce-{script_nonce}'; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src 'self' blob:"
 
         if allow_ajax:
             headers["Access-Control-Allow-Origin"] = "null"
 
         if self.env["REQUEST_METHOD"] == "OPTIONS":
             # Allow json access
-            headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range"
+            headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range, Referer"
             headers["Access-Control-Allow-Credentials"] = "true"
 
         # Download instead of display file types that can be dangerous
@@ -624,15 +674,18 @@ class UiRequest(object):
         if not url_a or not url_b:
             return False
 
-        url_a = url_a.replace("/raw/", "/")
-        url_b = url_b.replace("/raw/", "/")
+        host_pattern = r'(?P<host>http[s]?://.*?)(/|$)'
 
-        origin_pattern = "http[s]{0,1}://(.*?/).*"
+        match_a = re.match(host_pattern, url_a)
+        match_b = re.match(host_pattern, url_b)
 
-        origin_a = re.sub(origin_pattern, "\\1", url_a)
-        origin_b = re.sub(origin_pattern, "\\1", url_b)
+        if not match_a or not match_b:
+            return False
 
-        return origin_a == origin_b
+        host_a = match_a.group('host')
+        host_b = match_b.group('host')
+
+        return host_a == host_b
 
     def isSameOrigin(self, url_a, url_b):
         """Check if 0net origin is the same"""
diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html
index f65c5066..2cce69cf 100644
--- a/src/Ui/template/wrapper.html
+++ b/src/Ui/template/wrapper.html
@@ -74,7 +74,7 @@ else if (window.opener && window.opener.location.toString()) {
 
 
 <!-- Site Iframe -->
-<iframe src='about:blank' id='inner-iframe' sandbox="allow-forms allow-scripts allow-top-navigation allow-popups allow-modals allow-presentation allow-pointer-lock allow-popups-to-escape-sandbox {sandbox_permissions}" allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true" oallowfullscreen="true" msallowfullscreen="true"></iframe>
+<iframe src='about:blank' id='inner-iframe' sandbox="allow-forms allow-scripts allow-top-navigation allow-popups allow-modals allow-presentation allow-pointer-lock allow-popups-to-escape-sandbox allow-same-origin {sandbox_permissions}" allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true" oallowfullscreen="true" msallowfullscreen="true" referrerpolicy="same-origin"></iframe>
 
 <!-- Site info -->
 <script id="script_init" nonce="{script_nonce}">

From fc408ef985152217dd2f999489b6a477b380a31d Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 16 Nov 2023 14:04:33 +0000
Subject: [PATCH 271/333] Modern browsers all support nonce now so drop
 checking UA

---
 src/Ui/UiRequest.py | 12 +-----------
 1 file changed, 1 insertion(+), 11 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 0f8437d6..fb4e6e32 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -274,16 +274,6 @@ class UiRequest(object):
         else:
             return referer
 
-    def isScriptNonceSupported(self):
-        user_agent = self.env.get("HTTP_USER_AGENT")
-        if "Edge/" in user_agent:
-            is_script_nonce_supported = False
-        elif "Safari/" in user_agent and "Chrome/" not in user_agent:
-            is_script_nonce_supported = False
-        else:
-            is_script_nonce_supported = True
-        return is_script_nonce_supported
-
     # Send response headers
     def sendHeader(self, status=200, content_type="text/html", noscript=False, allow_ajax=False, script_nonce=None, extra_headers=[]):
         url = self.getRequestUrl()
@@ -306,7 +296,7 @@ class UiRequest(object):
 
         if noscript:
             headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src *; font-src * data:; media-src *; style-src * 'unsafe-inline';"
-        elif script_nonce and self.isScriptNonceSupported():
+        elif script_nonce:
             headers["Content-Security-Policy"] = "default-src 'none'; script-src 'nonce-{0}'; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src 'self' blob:".format(script_nonce)
 
         if allow_ajax:

From 23321bd300406d0363700489bdd9fe4c068482a7 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 17 Nov 2023 17:56:28 +0000
Subject: [PATCH 272/333] Fix SyntaxWarning: invalid escape sequence

---
 greet.py                           |  2 +-
 plugins/FilePack/FilePackPlugin.py | 10 +++++-----
 src/Crypt/ed25519.py               |  2 +-
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/greet.py b/greet.py
index d918bd6c..5f72dca2 100644
--- a/greet.py
+++ b/greet.py
@@ -18,7 +18,7 @@ def grad(n):
 def fancy_greet(version):
     from rich.console import Console
     from rich.text import Text
-    zc_msg = f'''
+    zc_msg = fr'''
 |||   . . _  _._|_     _. . . _ .__ _.. _.  . __.. _  __.  .
 |||  //\|/ |/_| |  == /  / \|/ |(  /_||/ |  | __||/ |/   \_|
 |||  \_/|  |\_  |.    \__\_/|  |_) \_ |   \/ |__||  |\__ _/
diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py
index a095c6d4..1c931316 100644
--- a/plugins/FilePack/FilePackPlugin.py
+++ b/plugins/FilePack/FilePackPlugin.py
@@ -45,7 +45,7 @@ class UiRequestPlugin(object):
             file_obj = None
             path_parts = self.parsePath(path)
             file_path = "%s/%s/%s" % (config.data_dir, path_parts["address"], path_parts["inner_path"])
-            match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", file_path)
+            match = re.match(r"^(.*\.(?:tar.gz|zip))/(.*)", file_path)
             archive_path, path_within = match.groups()
             if archive_path not in archive_cache:
                 site = self.server.site_manager.get(path_parts["address"])
@@ -99,7 +99,7 @@ class UiRequestPlugin(object):
 class SiteStoragePlugin(object):
     def isFile(self, inner_path):
         if ".zip/" in inner_path or ".tar.gz/" in inner_path:
-            match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", inner_path)
+            match = re.match(r"^(.*\.(?:tar.gz|zip))/(.*)", inner_path)
             archive_inner_path, path_within = match.groups()
             return super(SiteStoragePlugin, self).isFile(archive_inner_path)
         else:
@@ -127,7 +127,7 @@ class SiteStoragePlugin(object):
 
     def walk(self, inner_path, *args, **kwags):
         if ".zip" in inner_path or ".tar.gz" in inner_path:
-            match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path)
+            match = re.match(r"^(.*\.(?:tar.gz|zip))(.*)", inner_path)
             archive_inner_path, path_within = match.groups()
             archive = self.openArchive(archive_inner_path)
             path_within = path_within.lstrip("/")
@@ -151,7 +151,7 @@ class SiteStoragePlugin(object):
 
     def list(self, inner_path, *args, **kwags):
         if ".zip" in inner_path or ".tar.gz" in inner_path:
-            match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path)
+            match = re.match(r"^(.*\.(?:tar.gz|zip))(.*)", inner_path)
             archive_inner_path, path_within = match.groups()
             archive = self.openArchive(archive_inner_path)
             path_within = path_within.lstrip("/")
@@ -178,7 +178,7 @@ class SiteStoragePlugin(object):
 
     def read(self, inner_path, mode="rb", **kwargs):
         if ".zip/" in inner_path or ".tar.gz/" in inner_path:
-            match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path)
+            match = re.match(r"^(.*\.(?:tar.gz|zip))(.*)", inner_path)
             archive_inner_path, path_within = match.groups()
             archive = self.openArchive(archive_inner_path)
             path_within = path_within.lstrip("/")
diff --git a/src/Crypt/ed25519.py b/src/Crypt/ed25519.py
index 7c0161dc..8bd4ab86 100644
--- a/src/Crypt/ed25519.py
+++ b/src/Crypt/ed25519.py
@@ -64,7 +64,7 @@ def pow2(x, p):
 
 
 def inv(z):
-    """$= z^{-1} \mod q$, for z != 0"""
+    r"""$= z^{-1} \mod q$, for z != 0"""
     # Adapted from curve25519_athlon.c in djb's Curve25519.
     z2 = z * z % q                                # 2
     z9 = pow2(z2, 2) * z % q                      # 9

From 47db0898e25874da9c25062acdaf0fc3798b7854 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 17 Nov 2023 17:57:25 +0000
Subject: [PATCH 273/333] Rewrite untitialized directory detection

---
 src/main.py | 32 +++++++++++++++++---------------
 1 file changed, 17 insertions(+), 15 deletions(-)

diff --git a/src/main.py b/src/main.py
index 5c69c884..67480aaf 100644
--- a/src/main.py
+++ b/src/main.py
@@ -74,28 +74,30 @@ def importBundle(bundle):
 
 def init_dirs():
     data_dir = config.data_dir
-    if not os.path.isdir(data_dir):
+    has_data_dir = os.path.isdir(data_dir)
+    need_bootstrap = not config.disable_bootstrap and (not has_data_dir or not os.path.isfile(f'{data_dir}/sites.json')) and not config.offline
+
+    if not has_data_dir:
         os.mkdir(data_dir)
         try:
             os.chmod(data_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
         except Exception as err:
             startupError(f"Can't change permission of {data_dir}: {err}")
 
-        # download latest bootstrap bundle
-        if not config.disable_bootstrap and not config.offline:
-            import requests
-            from io import BytesIO
+    if need_bootstrap:
+        import requests
+        from io import BytesIO
 
-            print(f'fetching {config.bootstrap_url}')
-            response = requests.get(config.bootstrap_url)
-            if response.status_code != 200:
-                startupError(f"Cannot load bootstrap bundle (response status: {response.status_code})")
-            url = response.text
-            print(f'got {url}')
-            response = requests.get(url)
-            if response.status_code < 200 or response.status_code >= 300:
-                startupError(f"Cannot load boostrap bundle (response status: {response.status_code})")
-            importBundle(BytesIO(response.content))
+        print(f'fetching {config.bootstrap_url}')
+        response = requests.get(config.bootstrap_url)
+        if response.status_code != 200:
+            startupError(f"Cannot load bootstrap bundle (response status: {response.status_code})")
+        url = response.text
+        print(f'got {url}')
+        response = requests.get(url)
+        if response.status_code < 200 or response.status_code >= 300:
+            startupError(f"Cannot load boostrap bundle (response status: {response.status_code})")
+        importBundle(BytesIO(response.content))
 
     sites_json = f"{data_dir}/sites.json"
     if not os.path.isfile(sites_json):

From 92eb6c8ca1fca9d2d3691b754cf8102dd83408fd Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 17 Nov 2023 23:39:17 +0000
Subject: [PATCH 274/333] Update/rewrite docker files

---
 .dockerignore                                 |  4 ++-
 .gitignore                                    |  1 +
 Dockerfile                                    | 17 ----------
 Dockerfile.integrated_tor                     | 18 -----------
 Dockerfile.tor                                | 10 ------
 docker/Dockerfile                             | 25 +++++++++++++++
 docker/debian.Dockerfile                      | 26 ++++++++++++++++
 .../docker-compose.yml                        | 29 +++++++++--------
 docker/tor.Dockerfile                         |  9 ++++++
 docker/znctor.Dockerfile                      | 31 +++++++++++++++++++
 10 files changed, 111 insertions(+), 59 deletions(-)
 delete mode 100644 Dockerfile
 delete mode 100644 Dockerfile.integrated_tor
 delete mode 100644 Dockerfile.tor
 create mode 100644 docker/Dockerfile
 create mode 100644 docker/debian.Dockerfile
 rename docker-compose.yml => docker/docker-compose.yml (60%)
 create mode 100644 docker/tor.Dockerfile
 create mode 100644 docker/znctor.Dockerfile

diff --git a/.dockerignore b/.dockerignore
index 06de9748..620039c9 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,3 +1,5 @@
 venv
-Dockerfile*
+docker
 data
+__pycahce__
+log
diff --git a/.gitignore b/.gitignore
index 35e7c766..2fce8187 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ __pycache__/
 
 # Data dir
 data/*
+docker/data/
 *.db
 
 # Virtualenv
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 955ce752..00000000
--- a/Dockerfile
+++ /dev/null
@@ -1,17 +0,0 @@
-FROM python:3.10.4-alpine
-
-RUN apk --update --no-cache --no-progress add gcc libffi-dev musl-dev make openssl g++
-
-WORKDIR /app
-COPY . .
-
-RUN python3 -m venv venv \
- && source venv/bin/activate \
- && python3 -m pip install -r requirements.txt
-
-CMD source venv/bin/activate \
- && python3 zeronet.py --ui_ip "*" --fileserver_port 26552 \
-    --tor $TOR_ENABLED --tor_controller tor:$TOR_CONTROL_PORT \
-    --tor_proxy tor:$TOR_SOCKS_PORT --tor_password $TOR_CONTROL_PASSWD main
-
-EXPOSE 43110 26552
diff --git a/Dockerfile.integrated_tor b/Dockerfile.integrated_tor
deleted file mode 100644
index 20c4425a..00000000
--- a/Dockerfile.integrated_tor
+++ /dev/null
@@ -1,18 +0,0 @@
-FROM python:3.10.4-alpine
-
-RUN apk --update --no-cache --no-progress add tor gcc libffi-dev musl-dev make openssl g++ \
- && echo "ControlPort 9051" >> /etc/tor/torrc \
- && echo "CookieAuthentication 1" >> /etc/tor/torrc
-
-WORKDIR /app
-COPY . .
-
-RUN python3 -m venv venv \
- && source venv/bin/activate \
- && python3 -m pip install -r requirements.txt
-
-CMD (tor&) \
- && source venv/bin/activate \
- && python3 zeronet.py --ui_ip "*" --fileserver_port 26552
-
-EXPOSE 43110 26552
diff --git a/Dockerfile.tor b/Dockerfile.tor
deleted file mode 100644
index b085747f..00000000
--- a/Dockerfile.tor
+++ /dev/null
@@ -1,10 +0,0 @@
-FROM alpine:3.16.0
-
-RUN apk --update --no-cache --no-progress add tor
-
-CMD hashed_control_password=$(tor --quiet --hash-password $TOR_CONTROL_PASSWD) \
- && tor --SocksPort 0.0.0.0:$TOR_SOCKS_PORT \
-        --ControlPort 0.0.0.0:$TOR_CONTROL_PORT \
-        --HashedControlPassword $hashed_control_password
-
-EXPOSE $TOR_SOCKS_PORT $TOR_CONTROL_PORT
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 00000000..71613800
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,25 @@
+FROM python:3.12-alpine
+
+RUN apk --update --no-cache --no-progress add git gcc libffi-dev musl-dev make openssl g++ autoconf automake libtool
+
+RUN adduser -u 1600 -D service-0net
+
+USER service-0net:service-0net
+
+WORKDIR /home/service-0net
+
+COPY requirements.txt .
+
+RUN python3 -m pip install -r requirements.txt
+
+# the part below is updated with source updates
+
+COPY . .
+
+ENTRYPOINT python3 zeronet.py --ui_ip "*" --fileserver_port 26552 \
+    --tor $TOR_ENABLED --tor_controller tor:$TOR_CONTROL_PORT \
+    --tor_proxy tor:$TOR_SOCKS_PORT --tor_password $TOR_CONTROL_PASSWD
+
+CMD main
+
+EXPOSE 43110 26552
diff --git a/docker/debian.Dockerfile b/docker/debian.Dockerfile
new file mode 100644
index 00000000..8507914b
--- /dev/null
+++ b/docker/debian.Dockerfile
@@ -0,0 +1,26 @@
+FROM python:3.12-slim-bookworm
+
+RUN apt-get update
+RUN apt-get -y install git openssl pkg-config libffi-dev python3-pip python3-dev build-essential libtool
+
+RUN useradd -u 1600 -m service-0net
+
+USER service-0net:service-0net
+
+WORKDIR /home/service-0net
+
+COPY requirements.txt .
+
+RUN python3 -m pip install -r requirements.txt
+
+# the part below is updated with source updates
+
+COPY . .
+
+ENTRYPOINT python3 zeronet.py --ui_ip "*" --fileserver_port 26552 \
+    --tor $TOR_ENABLED --tor_controller tor:$TOR_CONTROL_PORT \
+    --tor_proxy tor:$TOR_SOCKS_PORT --tor_password $TOR_CONTROL_PASSWD
+
+CMD main
+
+EXPOSE 43110 26552
diff --git a/docker-compose.yml b/docker/docker-compose.yml
similarity index 60%
rename from docker-compose.yml
rename to docker/docker-compose.yml
index 5da1fc26..b953699f 100644
--- a/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -1,29 +1,31 @@
 version: '3'
+
 services:
   tor:
     tty: true
     stdin_open: true
     build:
-      context: .
-      dockerfile: Dockerfile.tor
+      context: ..
+      dockerfile: docker/tor.Dockerfile
     networks:
       - 0net-network
-    ports:
-      - "9050:9050"
-      - "9051:9051"
     environment: &tor-environments
+      # since we are using tor internally, password doesn't really matter
       TOR_CONTROL_PASSWD: some_password
       TOR_SOCKS_PORT: 9050
       TOR_CONTROL_PORT: 9051
-  0net:
+
+  0net-conservancy:
     tty: true
     stdin_open: true
     build:
-      context: .
+      context: ..
+      dockerfile: docker/Dockerfile
     networks:
       - 0net-network
     volumes:
-      - 0net-data:/app/data
+      # NOTE: this refers to docker/data..
+      - ./data:/home/service-0net/data
     ports:
       - "26552:26552"
       - "43110:43110"
@@ -32,20 +34,21 @@ services:
     environment:
       TOR_ENABLED: enable
       <<: *tor-environments
+
   0net-tor:
     tty: true
     stdin_open: true
     build:
-      context: .
-      dockerfile: Dockerfile.integrated_tor
+      context: ..
+      dockerfile: docker/znctor.Dockerfile
     networks:
       - 0net-network
     volumes:
-      - 0net-data:/app/data
+      # NOTE: this refers to docker/data..
+      - ./data:/home/service-0net/data
     ports:
       - "26552:26552"
       - "43110:43110"
-volumes:
-  0net-data:
+
 networks:
   0net-network:
diff --git a/docker/tor.Dockerfile b/docker/tor.Dockerfile
new file mode 100644
index 00000000..77fa00e5
--- /dev/null
+++ b/docker/tor.Dockerfile
@@ -0,0 +1,9 @@
+FROM alpine:3.18
+
+RUN apk --update --no-cache --no-progress add tor
+
+USER tor
+
+CMD tor --SocksPort 0.0.0.0:${TOR_SOCKS_PORT} --ControlPort 0.0.0.0:${TOR_CONTROL_PORT} --HashedControlPassword $(tor --quiet --hash-password $TOR_CONTROL_PASSWD)
+
+EXPOSE $TOR_SOCKS_PORT $TOR_CONTROL_PORT
diff --git a/docker/znctor.Dockerfile b/docker/znctor.Dockerfile
new file mode 100644
index 00000000..152bca70
--- /dev/null
+++ b/docker/znctor.Dockerfile
@@ -0,0 +1,31 @@
+FROM python:3.12-alpine
+
+RUN apk --update --no-cache --no-progress add git gcc libffi-dev musl-dev make openssl g++ autoconf automake libtool
+RUN apk add tor
+
+RUN echo "ControlPort 9051" >> /etc/tor/torrc
+RUN echo "CookieAuthentication 1" >> /etc/tor/torrc
+
+RUN adduser -u 1600 -D service-0net
+
+USER service-0net:service-0net
+
+WORKDIR /home/service-0net
+
+COPY requirements.txt .
+
+RUN python3 -m pip install -r requirements.txt
+
+RUN echo "tor &" > start.sh
+RUN echo "python3 zeronet.py --ui_ip '*' --fileserver_port 26552" >> start.sh
+RUN chmod +x start.sh
+
+# the part below is updated with source updates
+
+COPY . .
+
+ENTRYPOINT ./start.sh
+
+CMD main
+
+EXPOSE 43110 26552

From 1945f5a0b7ff00610fc27abf33eaf042b7ccaa93 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 19 Nov 2023 17:26:33 +0000
Subject: [PATCH 275/333] Fix docker compose setup exposing ports to outside
 world by default

---
 docker/docker-compose.yml | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index b953699f..78a6908e 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -28,13 +28,14 @@ services:
       - ./data:/home/service-0net/data
     ports:
       - "26552:26552"
-      - "43110:43110"
+      - "127.0.0.1:43110:43110"
     depends_on:
       - tor
     environment:
       TOR_ENABLED: enable
       <<: *tor-environments
 
+  # integrated container with tor
   0net-tor:
     tty: true
     stdin_open: true
@@ -48,7 +49,7 @@ services:
       - ./data:/home/service-0net/data
     ports:
       - "26552:26552"
-      - "43110:43110"
+      - "127.0.0.1:43110:43110"
 
 networks:
   0net-network:

From 61f0b839ca8a4e5d66cf5b15bdc2830ce2b09a60 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 19 Nov 2023 22:29:03 +0000
Subject: [PATCH 276/333] REAMDE apt dependencies

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 59fa72f0..9f1cdb43 100644
--- a/README.md
+++ b/README.md
@@ -97,7 +97,7 @@ Install autoconf and other basic development tools, python3 and pip, then procee
 
 ##### Apt-based (debian, ubuntu, etc)
  - `sudo apt update`
- - `sudo apt install pkg-config libffi-dev python3-pip python3-venv python3-dev build-essential`
+ - `sudo apt install git pkg-config libffi-dev python3-pip python3-venv python3-dev build-essential libtool`
 
 ##### Red Hat and Fedora based
  - `yum install epel-release -y 2>/dev/null`

From ae3c4350b5739ccd39aa9309ea9219362a07728b Mon Sep 17 00:00:00 2001
From: Tanvir <tanvir.ahmed.tonoy@skiff.com>
Date: Fri, 24 Nov 2023 01:14:38 +0600
Subject: [PATCH 277/333] Refactor start-venv.sh

---
 start-venv.sh | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/start-venv.sh b/start-venv.sh
index 33ac3216..1fd3a9af 100755
--- a/start-venv.sh
+++ b/start-venv.sh
@@ -1,8 +1,11 @@
-#! /usr/bin/env bash
+#!/usr/bin/env bash
 
-if [ ! -f venv/bin/activate ] ; then
+VENVPATH="venv/bin/activate"
+
+if [ ! -f "$VENVPATH" ]; then
     python3 -m venv venv
 fi
-source venv/bin/activate
+
+source "$VENVPATH"
 python3 -m pip install -r requirements.txt
-python3 zeronet.py $1 $2 $3 $4 $5 $6 $7 $8 $9
+python3 zeronet.py "$@"

From b89cd79d6a49acb47357cd3a610b7ca70b4e7b3a Mon Sep 17 00:00:00 2001
From: Tanvir <tanvir.ahmed.tonoy@skiff.com>
Date: Fri, 24 Nov 2023 01:18:03 +0600
Subject: [PATCH 278/333] Refactor termux.sh

---
 termux.sh | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/termux.sh b/termux.sh
index 1eb365b1..c73eade8 100644
--- a/termux.sh
+++ b/termux.sh
@@ -1,12 +1,13 @@
-
+#!/usr/bin/env bash
 # Script for running zeronet-conservancy in Termux on Android
 
-if [[ -d zeronet-conservancy ]]; then
-	cd zeronet-conservancy
-	git pull --ff-only
+REPO_DIR="zeronet-conservancy"
+VENV_SCRIPT="start-venv.sh"
+
+if [[ -d "$REPO_DIR" ]]; then
+    (cd "$REPO_DIR" && git pull --ff-only)
 else
-	git clone https://github.com/zeronet-conservancy/zeronet-conservancy
-	cd zeronet-conservancy
+    git clone https://github.com/zeronet-conservancy/zeronet-conservancy "$REPO_DIR"
 fi
 
 pkg update -y
@@ -16,5 +17,4 @@ echo "Starting tor..."
 tor --ControlPort 9051 --CookieAuthentication 1 >/dev/null &
 
 echo "Starting zeronet-conservancy..."
-./start-venv.sh
-cd ..
+(cd "$REPO_DIR" && ./"$VENV_SCRIPT")

From ba60f5dc0e7486f0a7e9c49819228ff1ee947f8b Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 26 Nov 2023 18:09:56 +0000
Subject: [PATCH 279/333] Add basic ipython-based repl via --repl

---
 CHANGELOG.md     |  1 +
 requirements.txt |  1 +
 src/Config.py    |  5 +++--
 src/main.py      | 28 ++++++++++++++++++++++++++--
 4 files changed, 31 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b59e09fb..ecea0d38 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,5 @@
 ### zeronet-conservancy 0.7.10+
+- ipython-based repl via --repl for debug/interactive development (@caryoscelus)
 
 ### zeronet-conservancy 0.7.10 (2023-07-26) (18d35d3bed4f0683e99)
 prepared by @caryoscelus
diff --git a/requirements.txt b/requirements.txt
index 1059795c..b9eb9fc9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -15,3 +15,4 @@ rich
 defusedxml>=0.7
 pyaes
 requests
+ipython>=8
diff --git a/src/Config.py b/src/Config.py
index 6c25af00..1dba0007 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -15,7 +15,7 @@ class Config(object):
         self.version = "0.7.10+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5121
+        self.rev = 5130
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None
@@ -304,6 +304,7 @@ class Config(object):
         self.parser.add_argument('--tor_hs_limit', help='Maximum number of hidden services in Tor always mode', metavar='limit', type=int, default=10)
         self.parser.add_argument('--tor_hs_port', help='Hidden service port in Tor always mode', metavar='limit', type=int, default=15441)
 
+        self.parser.add_argument('--repl', help='Instead of printing logs in console, drop into REPL after initialization', action='store_true')
         self.parser.add_argument('--version', action='version', version=f'zeronet-conservancy {self.version} r{self.rev}')
         self.parser.add_argument('--end', help='Stop multi value argument parsing', action='store_true')
 
@@ -583,7 +584,7 @@ class Config(object):
             format = '%(name)s %(message)s'
 
         if self.console_log_level == "default":
-            if self.silent:
+            if self.silent or self.repl:
                 level = logging.ERROR
             elif self.debug:
                 level = logging.DEBUG
diff --git a/src/main.py b/src/main.py
index 67480aaf..b15f46ec 100644
--- a/src/main.py
+++ b/src/main.py
@@ -157,6 +157,7 @@ if config.msgpack_purepython:
     os.environ["MSGPACK_PUREPYTHON"] = "True"
 
 # Fix console encoding on Windows
+# TODO: check if this is still required
 if sys.platform.startswith("win"):
     import subprocess
     try:
@@ -193,7 +194,7 @@ elif config.bind:
 
 
 @PluginManager.acceptPlugins
-class Actions(object):
+class Actions:
     def call(self, function_name, kwargs):
         logging.info("Version: %s r%s, Python %s, Gevent: %s" % (config.version, config.rev, sys.version, gevent.__version__))
 
@@ -202,6 +203,11 @@ class Actions(object):
         if back:
             print(back)
 
+    def ipythonThread(self):
+        import IPython
+        IPython.embed()
+        self.gevent_quit.set()
+
     # Default action: Start serving UiServer and FileServer
     def main(self):
         global ui_server, file_server
@@ -221,7 +227,25 @@ class Actions(object):
         CryptConnection.manager.removeCerts()
 
         logging.info("Starting servers....")
-        gevent.joinall([gevent.spawn(ui_server.start), gevent.spawn(file_server.start)])
+
+        import threading
+        self.gevent_quit = threading.Event()
+        launched_greenlets = [gevent.spawn(ui_server.start), gevent.spawn(file_server.start)]
+
+        # if --repl, start ipython thread
+        # FIXME: Unfortunately this leads to exceptions on exit so use with care
+        if config.repl:
+            threading.Thread(target=self.ipythonThread).start()
+
+        stopped = 0
+        # Process all greenlets in main thread
+        while not self.gevent_quit.is_set() and stopped < len(launched_greenlets):
+            stopped += len(gevent.joinall(launched_greenlets, timeout=1))
+
+        # Exited due to repl, so must kill greenlets
+        if stopped < len(launched_greenlets):
+            gevent.killall(launched_greenlets, exception=KeyboardInterrupt)
+
         logging.info("All server stopped")
 
     # Site commands

From db382f00759aae52b9e8f0181f7935a9df821e75 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 26 Nov 2023 23:54:57 +0000
Subject: [PATCH 280/333] Remove Vagrantfile as outdated and unused

If anyone needs it at any point, it's probably easier to write a new one
and if consulting with a historic version is required, it's still available
in history

fixes #180
---
 Vagrantfile | 45 ---------------------------------------------
 1 file changed, 45 deletions(-)
 delete mode 100644 Vagrantfile

diff --git a/Vagrantfile b/Vagrantfile
deleted file mode 100644
index 24fe0c45..00000000
--- a/Vagrantfile
+++ /dev/null
@@ -1,45 +0,0 @@
-# -*- mode: ruby -*-
-# vi: set ft=ruby :
-
-VAGRANTFILE_API_VERSION = "2"
-
-Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
-
-  #Set box
-  config.vm.box = "ubuntu/trusty64"
-
-  #Do not check fo updates
-  config.vm.box_check_update = false
-
-  #Add private network
-  config.vm.network "private_network", type: "dhcp"
-
-  #Redirect ports
-  config.vm.network "forwarded_port", guest: 43110, host: 43110
-  config.vm.network "forwarded_port", guest: 15441, host: 15441
-
-  #Sync folder using NFS if not windows
-  config.vm.synced_folder ".", "/vagrant",
-      :nfs => !Vagrant::Util::Platform.windows?
-
-  #Virtal Box settings
-  config.vm.provider "virtualbox" do |vb|
-    # Don't boot with headless mode
-    #vb.gui = true
-
-    # Set VM settings
-    vb.customize ["modifyvm", :id, "--memory", "512"]
-    vb.customize ["modifyvm", :id, "--cpus", 1]
-  end
-
-  #Update system
-  config.vm.provision "shell",
-      inline: "sudo apt-get update -y && sudo apt-get upgrade -y"
-
-  #Install deps
-  config.vm.provision "shell",
-      inline: "sudo apt-get install msgpack-python python-gevent python-pip python-dev -y"
-  config.vm.provision "shell",
-      inline: "sudo pip install msgpack --upgrade"
-
-end

From a4cc2eeb9fccff2aeece00a5665fea0e906b9075 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 7 Dec 2023 07:30:24 +0000
Subject: [PATCH 281/333] Separate http server for site content WIP

This resolves issue introduced by 14e8130acb4a092b143d0a8046f29926f28e1b25
by having wrapper and iframe exist in different origins

Note that this does introduce minor UX issue: copying links now shows them
with a different port
---
 src/Ui/UiRequest.py          | 24 +++++++++++++++++++-----
 src/Ui/UiServer.py           | 21 +++++++++++++++------
 src/Ui/template/wrapper.html |  4 ++--
 src/main.py                  |  2 +-
 4 files changed, 37 insertions(+), 14 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index b55fa78d..ced55a00 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -51,11 +51,11 @@ class SecurityError(Exception):
 @PluginManager.acceptPlugins
 class UiRequest:
 
-    def __init__(self, server, get, env, start_response):
+    def __init__(self, server, env, start_response, is_data_request=False):
         if server:
             self.server = server
             self.log = server.log
-        self.get = get  # Get parameters
+        self.get = dict(urllib.parse.parse_qsl(env.get('QUERY_STRING', '')))
         self.env = env  # Enviroment settings
         # ['CONTENT_LENGTH', 'CONTENT_TYPE', 'GATEWAY_INTERFACE', 'HTTP_ACCEPT', 'HTTP_ACCEPT_ENCODING', 'HTTP_ACCEPT_LANGUAGE',
         #  'HTTP_COOKIE', 'HTTP_CACHE_CONTROL', 'HTTP_HOST', 'HTTP_HTTPS', 'HTTP_ORIGIN', 'HTTP_PROXY_CONNECTION', 'HTTP_REFERER',
@@ -66,6 +66,7 @@ class UiRequest:
         self.start_response = start_response  # Start response function
         self.user = None
         self.script_nonce = None  # Nonce for script tags in wrapper html
+        self.is_data_request = is_data_request
 
     def learnHost(self, host):
         self.server.allowed_hosts.add(host)
@@ -142,8 +143,21 @@ class UiRequest:
         we'd want something else..
         """
 
+        is_navigate = self.env.get('HTTP_SEC_FETCH_MODE') == 'navigate'
+        is_iframe = self.env.get('HTTP_SEC_FETCH_DEST') == 'iframe'
+
+        if is_navigate and not is_iframe and self.is_data_request:
+            # remove port from host
+            host = ':'.join(self.env['HTTP_HOST'].split(':')[:-1])
+            path_info = self.env['PATH_INFO']
+            query_string = self.env['QUERY_STRING']
+            protocol = self.env['wsgi.url_scheme']
+            return self.actionRedirect(f'{protocol}://{host}:43110{path_info}?{query_string}')
+
         if self.isCrossOriginRequest():
-            return self.error404()
+            # we are still exposed by answering on port
+            self.log.warning('Cross-origin request detected. Someone might be trying to analyze your 0net usage')
+            return []
 
         # Restict Ui access by ip
         if config.ui_restrict and self.env['REMOTE_ADDR'] not in config.ui_restrict:
@@ -341,13 +355,12 @@ class UiRequest:
         headers["Version"] = "HTTP/1.1"
         headers["Connection"] = "Keep-Alive"
         headers["Keep-Alive"] = "max=25, timeout=30"
-        headers["X-Frame-Options"] = "SAMEORIGIN"
         headers["Referrer-Policy"] = "same-origin"
 
         if noscript:
             headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src *; font-src * data:; media-src *; style-src * 'unsafe-inline';"
         elif script_nonce:
-            headers["Content-Security-Policy"] = "default-src 'none'; script-src 'nonce-{0}'; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src 'self' blob:".format(script_nonce)
+            headers["Content-Security-Policy"] = f"default-src 'none'; script-src 'nonce-{script_nonce}'; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src 'self' blob: http://127.0.0.1:43111"
 
         if allow_ajax:
             headers["Access-Control-Allow-Origin"] = "null"
@@ -618,6 +631,7 @@ class UiRequest:
 
         return self.render(
             "src/Ui/template/wrapper.html",
+            site_file_server='http://127.0.0.1:43111',
             server_url=server_url,
             inner_path=inner_path,
             file_url=xescape(file_url),
diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py
index 08820832..24fd04d7 100644
--- a/src/Ui/UiServer.py
+++ b/src/Ui/UiServer.py
@@ -29,7 +29,7 @@ class UiWSGIHandler(WebSocketHandler):
             import main
             main.DebugHook.handleError()
         else:
-            ui_request = UiRequest(self.server, {}, self.environ, self.start_response)
+            ui_request = UiRequest(self.server, self.environ, self.start_response, is_data_request=False)
             block_gen = ui_request.error500("UiWSGIHandler error: %s" % Debug.formatExceptionMessage(err))
             for block in block_gen:
                 self.write(block)
@@ -96,11 +96,7 @@ class UiServer:
     # Handle WSGI request
     def handleRequest(self, env, start_response):
         path = bytes(env["PATH_INFO"], "raw-unicode-escape").decode("utf8")
-        if env.get("QUERY_STRING"):
-            get = dict(urllib.parse.parse_qsl(env['QUERY_STRING']))
-        else:
-            get = {}
-        ui_request = UiRequest(self, get, env, start_response)
+        ui_request = UiRequest(self, env, start_response, is_data_request=False)
         if config.debug:  # Let the exception catched by werkezung
             return ui_request.route(path)
         else:  # Catch and display the error
@@ -158,6 +154,19 @@ class UiServer:
             main.file_server.stop()
         self.log.debug("Stopped.")
 
+    def handleSiteRequest(self, env, start_response):
+        path = bytes(env["PATH_INFO"], "raw-unicode-escape").decode("utf8")
+        ui_request = UiRequest(self, env, start_response, is_data_request=True)
+        try:
+            return ui_request.route(path)
+        except Exception as err:
+            logging.debug(f"UiRequest @ site error: {Debug.formatException(err)}")
+            return ui_request.error500('Error while trying to server site data')
+
+    def startSiteServer(self):
+        self.site_server = WSGIServer((self.ip, 43111), self.handleSiteRequest, log=self.log)
+        self.site_server.serve_forever()
+
     def stop(self):
         self.log.debug("Stopping...")
         # Close WS sockets
diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html
index 2cce69cf..b18a69b1 100644
--- a/src/Ui/template/wrapper.html
+++ b/src/Ui/template/wrapper.html
@@ -74,11 +74,11 @@ else if (window.opener && window.opener.location.toString()) {
 
 
 <!-- Site Iframe -->
-<iframe src='about:blank' id='inner-iframe' sandbox="allow-forms allow-scripts allow-top-navigation allow-popups allow-modals allow-presentation allow-pointer-lock allow-popups-to-escape-sandbox allow-same-origin {sandbox_permissions}" allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true" oallowfullscreen="true" msallowfullscreen="true" referrerpolicy="same-origin"></iframe>
+<iframe src='about:blank' id='inner-iframe' sandbox="allow-forms allow-scripts allow-top-navigation allow-popups allow-modals allow-presentation allow-pointer-lock allow-popups-to-escape-sandbox allow-same-origin {sandbox_permissions}" allowfullscreen="true"></iframe>
 
 <!-- Site info -->
 <script id="script_init" nonce="{script_nonce}">
-iframe_src = "{file_url}{query_string}"
+iframe_src = "{site_file_server}{file_url}{query_string}"
 console.log("Changing url from " + document.getElementById("inner-iframe").src + " to " + iframe_src)
 document.getElementById("inner-iframe").src = document.getElementById("inner-iframe").src  // Workaround for Firefox back button bug
 document.getElementById("inner-iframe").src = iframe_src
diff --git a/src/main.py b/src/main.py
index b15f46ec..3c526b85 100644
--- a/src/main.py
+++ b/src/main.py
@@ -230,7 +230,7 @@ class Actions:
 
         import threading
         self.gevent_quit = threading.Event()
-        launched_greenlets = [gevent.spawn(ui_server.start), gevent.spawn(file_server.start)]
+        launched_greenlets = [gevent.spawn(ui_server.start), gevent.spawn(file_server.start), gevent.spawn(ui_server.startSiteServer)]
 
         # if --repl, start ipython thread
         # FIXME: Unfortunately this leads to exceptions on exit so use with care

From f37ab4ee5dfead113f4fcb915771649dab3206c5 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 7 Dec 2023 09:56:01 +0000
Subject: [PATCH 282/333] Continuing previous commit: --ui_site_port

---
 src/Config.py       |  3 +++
 src/Ui/UiRequest.py | 22 +++++++++++++++++-----
 src/Ui/UiServer.py  |  3 ++-
 3 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index 1dba0007..e9cd03a2 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -230,6 +230,7 @@ class Config(object):
         self.parser.add_argument('--language', help='Web interface language', default=language, metavar='language')
         self.parser.add_argument('--ui_ip', help='Web interface bind address', default="127.0.0.1", metavar='ip')
         self.parser.add_argument('--ui_port', help='Web interface bind port', default=43110, type=int, metavar='port')
+        self.parser.add_argument('--ui_site_port', help='Port for serving site content, defaults to ui_port+1', default=None, metavar='port')
         self.parser.add_argument('--ui_restrict', help='Restrict web access', default=False, metavar='ip', nargs='*')
         self.parser.add_argument('--ui_host', help='Allow access using this hosts', metavar='host', nargs='*')
         self.parser.add_argument('--ui_trans_proxy', help='Allow access using a transparent proxy', action='store_true')
@@ -430,6 +431,8 @@ class Config(object):
                 self.arguments = {}
         else:
             self.arguments = self.parser.parse_args(argv[1:])
+        if self.arguments.ui_site_port is None:
+            self.arguments.ui_site_port = self.arguments.ui_port + 1
 
     # Parse config file
     def parseConfig(self, argv):
diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index ced55a00..17e63c9b 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -147,12 +147,11 @@ class UiRequest:
         is_iframe = self.env.get('HTTP_SEC_FETCH_DEST') == 'iframe'
 
         if is_navigate and not is_iframe and self.is_data_request:
-            # remove port from host
-            host = ':'.join(self.env['HTTP_HOST'].split(':')[:-1])
+            host = self.getHostWithoutPort()
             path_info = self.env['PATH_INFO']
             query_string = self.env['QUERY_STRING']
             protocol = self.env['wsgi.url_scheme']
-            return self.actionRedirect(f'{protocol}://{host}:43110{path_info}?{query_string}')
+            return self.actionRedirect(f'{protocol}://{host}:{config.ui_port}{path_info}?{query_string}')
 
         if self.isCrossOriginRequest():
             # we are still exposed by answering on port
@@ -360,7 +359,14 @@ class UiRequest:
         if noscript:
             headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src *; font-src * data:; media-src *; style-src * 'unsafe-inline';"
         elif script_nonce:
-            headers["Content-Security-Policy"] = f"default-src 'none'; script-src 'nonce-{script_nonce}'; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src 'self' blob: http://127.0.0.1:43111"
+            host = self.getHostWithoutPort()
+            port = int(self.env['SERVER_PORT'])
+            if port == config.ui_port:
+                other_port = config.ui_site_port
+            else:
+                other_port = config.ui_port
+            site_server = f'{host}:{other_port}'
+            headers["Content-Security-Policy"] = f"default-src 'none'; script-src 'nonce-{script_nonce}'; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src {site_server}"
 
         if allow_ajax:
             headers["Access-Control-Allow-Origin"] = "null"
@@ -518,6 +524,9 @@ class UiRequest:
             server_url = ""
         return server_url
 
+    def getHostWithoutPort(self):
+        return ':'.join(self.env['HTTP_HOST'].split(':')[:-1])
+
     def processQueryString(self, site, query_string):
         match = re.search("zeronet_peers=(.*?)(&|$)", query_string)
         if match:
@@ -629,9 +638,12 @@ class UiRequest:
             repl.update(html_chars)
             return s.translate(repl)
 
+        scheme = self.env['wsgi.url_scheme']
+        host = self.getHostWithoutPort()
+
         return self.render(
             "src/Ui/template/wrapper.html",
-            site_file_server='http://127.0.0.1:43111',
+            site_file_server=f'{scheme}://{host}:{config.ui_site_port}',
             server_url=server_url,
             inner_path=inner_path,
             file_url=xescape(file_url),
diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py
index 24fd04d7..27727d4c 100644
--- a/src/Ui/UiServer.py
+++ b/src/Ui/UiServer.py
@@ -55,6 +55,7 @@ class UiServer:
     def __init__(self):
         self.ip = config.ui_ip
         self.port = config.ui_port
+        self.site_port = config.ui_site_port
         self.running = False
         if self.ip == "*":
             self.ip = "0.0.0.0"  # Bind all
@@ -164,7 +165,7 @@ class UiServer:
             return ui_request.error500('Error while trying to server site data')
 
     def startSiteServer(self):
-        self.site_server = WSGIServer((self.ip, 43111), self.handleSiteRequest, log=self.log)
+        self.site_server = WSGIServer((self.ip, self.site_port), self.handleSiteRequest, log=self.log)
         self.site_server.serve_forever()
 
     def stop(self):

From 90456768a7281dfeb2e7bd9c4ebd991c80b96c36 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 20 Dec 2023 12:25:54 +0000
Subject: [PATCH 283/333] Update CHANGELOG

---
 CHANGELOG.md | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ecea0d38..172f5f7b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
 ### zeronet-conservancy 0.7.10+
+- disable site-plugins installed for security reasons (@caryoscelus)
+- fix downloading geoip db (@caryoscelus)
+- python <3.6 is officially unsupported
+- SafeRe improvements by @geekless
+- remove and don't update muted files (@caryoscelus)
+- option to disable port checking (@caryoscelus)
+- new install startup improvements (@caryoscelus)
+- fix blank site with UiPassword under certain conditions (reported & sponsored by @bitcoren) (@caryoscelus)
+- fix chromium compatibility (@caryoscelus)
+- better fix of local sites leak (@caryoscelus)
 - ipython-based repl via --repl for debug/interactive development (@caryoscelus)
+- various improvements
 
 ### zeronet-conservancy 0.7.10 (2023-07-26) (18d35d3bed4f0683e99)
 prepared by @caryoscelus

From 0c310005937acdf77a6cb5536c270fe576637d64 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 21 Dec 2023 10:18:57 +0000
Subject: [PATCH 284/333] Use dashes in command line options instead of
 underscore

- using underscore is deprecated but still works
- also don't override commandline --open-browser option
---
 src/Config.py | 169 ++++++++++++++++++++++++++------------------------
 1 file changed, 89 insertions(+), 80 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index e9cd03a2..fee378d0 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -15,7 +15,7 @@ class Config(object):
         self.version = "0.7.10+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5130
+        self.rev = 5140
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None
@@ -58,8 +58,8 @@ class Config(object):
     def getStartDir(self):
         this_file = os.path.abspath(__file__).replace("\\", "/").rstrip("cd")
 
-        if "--start_dir" in self.argv:
-            start_dir = self.argv[self.argv.index("--start_dir") + 1]
+        if "--start-dir" in self.argv:
+            start_dir = self.argv[self.argv.index("--start-dir") + 1]
         elif this_file.endswith("/Contents/Resources/core/src/Config.py"):
             # Running as ZeroNet.app
             if this_file.startswith("/Application") or this_file.startswith("/private") or this_file.startswith(os.path.expanduser("~/Library")):
@@ -108,7 +108,7 @@ class Config(object):
         # SiteCreate
         action = self.subparsers.add_parser("siteCreate", help='Create a new site')
         action.register('type', 'bool', self.strToBool)
-        action.add_argument('--use_master_seed', help="Allow created site's private key to be recovered using the master seed in users.json (default: True)", type="bool", choices=[True, False], default=True)
+        action.add_argument('--use-master_seed', help="Allow created site's private key to be recovered using the master seed in users.json (default: True)", type="bool", choices=[True, False], default=True)
 
         # SiteNeedFile
         action = self.subparsers.add_parser("siteNeedFile", help='Get a file from site')
@@ -123,9 +123,9 @@ class Config(object):
         action = self.subparsers.add_parser("siteSign", help='Update and sign content.json: address [privatekey]')
         action.add_argument('address', help='Site to sign')
         action.add_argument('privatekey', help='Private key (default: ask on execute)', nargs='?')
-        action.add_argument('--inner_path', help='File you want to sign (default: content.json)',
+        action.add_argument('--inner-path', help='File you want to sign (default: content.json)',
                             default="content.json", metavar="inner_path")
-        action.add_argument('--remove_missing_optional', help='Remove optional files that is not present in the directory', action='store_true')
+        action.add_argument('--remove-missing_optional', help='Remove optional files that is not present in the directory', action='store_true')
         action.add_argument('--publish', help='Publish site after the signing', action='store_true')
 
         # SitePublish
@@ -135,10 +135,10 @@ class Config(object):
                             default=None, nargs='?')
         action.add_argument('peer_port', help='Peer port to publish (default: random peer port from tracker)',
                             default=15441, nargs='?')
-        action.add_argument('--inner_path', help='Content.json you want to publish (default: content.json)',
+        action.add_argument('--inner-path', help='Content.json you want to publish (default: content.json)',
                             default="content.json", metavar="inner_path")
         action.add_argument('--recursive', help="Whether to publish all of site's content.json. "
-                            "Overrides --inner_path. (default: false)", action='store_true', dest='recursive')
+                            "Overrides --inner-path. (default: false)", action='store_true', dest='recursive')
 
         # SiteVerify
         action = self.subparsers.add_parser("siteVerify", help='Verify site files using sha512: address')
@@ -211,99 +211,99 @@ class Config(object):
         self.parser.add_argument('--verbose', help='More detailed logging', action='store_true')
         self.parser.add_argument('--debug', help='Debug mode', action='store_true')
         self.parser.add_argument('--silent', help='Only log errors to terminal output', action='store_true')
-        self.parser.add_argument('--debug_socket', help='Debug socket connections', action='store_true')
-        self.parser.add_argument('--merge_media', help='Merge all.js and all.css', action='store_true')
+        self.parser.add_argument('--debug-socket', help='Debug socket connections', action='store_true')
+        self.parser.add_argument('--merge-media', help='Merge all.js and all.css', action='store_true')
 
         self.parser.add_argument('--batch', help="Batch mode (No interactive input for commands)", action='store_true')
 
-        self.parser.add_argument('--start_dir', help='Path of working dir for variable content (data, log, .conf)', default=self.start_dir, metavar="path")
-        self.parser.add_argument('--config_file', help='Path of config file', default=config_file, metavar="path")
-        self.parser.add_argument('--data_dir', help='Path of data directory', default=data_dir, metavar="path")
+        self.parser.add_argument('--start-dir', help='Path of working dir for variable content (data, log, .conf)', default=self.start_dir, metavar="path")
+        self.parser.add_argument('--config-file', help='Path of config file', default=config_file, metavar="path")
+        self.parser.add_argument('--data-dir', help='Path of data directory', default=data_dir, metavar="path")
 
-        self.parser.add_argument('--console_log_level', help='Level of logging to console', default="default", choices=["default", "DEBUG", "INFO", "ERROR", "off"])
+        self.parser.add_argument('--console-log-level', help='Level of logging to console', default="default", choices=["default", "DEBUG", "INFO", "ERROR", "off"])
 
-        self.parser.add_argument('--log_dir', help='Path of logging directory', default=log_dir, metavar="path")
-        self.parser.add_argument('--log_level', help='Level of logging to file', default="DEBUG", choices=["DEBUG", "INFO", "ERROR", "off"])
-        self.parser.add_argument('--log_rotate', help='Log rotate interval', default="daily", choices=["hourly", "daily", "weekly", "off"])
-        self.parser.add_argument('--log_rotate_backup_count', help='Log rotate backup count', default=5, type=int)
+        self.parser.add_argument('--log-dir', help='Path of logging directory', default=log_dir, metavar="path")
+        self.parser.add_argument('--log-level', help='Level of logging to file', default="DEBUG", choices=["DEBUG", "INFO", "ERROR", "off"])
+        self.parser.add_argument('--log-rotate', help='Log rotate interval', default="daily", choices=["hourly", "daily", "weekly", "off"])
+        self.parser.add_argument('--log-rotate-backup-count', help='Log rotate backup count', default=5, type=int)
 
         self.parser.add_argument('--language', help='Web interface language', default=language, metavar='language')
-        self.parser.add_argument('--ui_ip', help='Web interface bind address', default="127.0.0.1", metavar='ip')
-        self.parser.add_argument('--ui_port', help='Web interface bind port', default=43110, type=int, metavar='port')
-        self.parser.add_argument('--ui_site_port', help='Port for serving site content, defaults to ui_port+1', default=None, metavar='port')
-        self.parser.add_argument('--ui_restrict', help='Restrict web access', default=False, metavar='ip', nargs='*')
-        self.parser.add_argument('--ui_host', help='Allow access using this hosts', metavar='host', nargs='*')
-        self.parser.add_argument('--ui_trans_proxy', help='Allow access using a transparent proxy', action='store_true')
+        self.parser.add_argument('--ui-ip', help='Web interface bind address', default="127.0.0.1", metavar='ip')
+        self.parser.add_argument('--ui-port', help='Web interface bind port', default=43110, type=int, metavar='port')
+        self.parser.add_argument('--ui-site-port', help='Port for serving site content, defaults to ui_port+1', default=None, metavar='port')
+        self.parser.add_argument('--ui-restrict', help='Restrict web access', default=False, metavar='ip', nargs='*')
+        self.parser.add_argument('--ui-host', help='Allow access using this hosts', metavar='host', nargs='*')
+        self.parser.add_argument('--ui-trans-proxy', help='Allow access using a transparent proxy', action='store_true')
 
-        self.parser.add_argument('--open_browser', help='Open homepage in web browser automatically',
+        self.parser.add_argument('--open-browser', help='Open homepage in web browser automatically',
                                  nargs='?', const="default_browser", metavar='browser_name')
         self.parser.add_argument('--homepage', help='Web interface Homepage', default='191CazMVNaAcT9Y1zhkxd9ixMBPs59g2um',
                                  metavar='address')
         # self.parser.add_argument('--updatesite', help='Source code update site', default='1uPDaT3uSyWAPdCv1WkMb5hBQjWSNNACf',
                                  # metavar='address')
-        self.parser.add_argument('--admin_pages', help='Pages with admin privileges', default=[], metavar='address', nargs='*')
-        self.parser.add_argument('--dist_type', help='Type of installed distribution', default='source')
+        self.parser.add_argument('--admin-pages', help='Pages with admin privileges', default=[], metavar='address', nargs='*')
+        self.parser.add_argument('--dist-type', help='Type of installed distribution', default='source')
 
-        self.parser.add_argument('--size_limit', help='Default site size limit in MB', default=10, type=int, metavar='limit')
-        self.parser.add_argument('--file_size_limit', help='Maximum per file size limit in MB', default=10, type=int, metavar='limit')
-        self.parser.add_argument('--connected_limit', help='Max connected peer per site', default=8, type=int, metavar='connected_limit')
-        self.parser.add_argument('--global_connected_limit', help='Max connections', default=512, type=int, metavar='global_connected_limit')
+        self.parser.add_argument('--size-limit', help='Default site size limit in MB', default=10, type=int, metavar='limit')
+        self.parser.add_argument('--file-size-limit', help='Maximum per file size limit in MB', default=10, type=int, metavar='limit')
+        self.parser.add_argument('--connected-limit', help='Max connected peer per site', default=8, type=int, metavar='connected_limit')
+        self.parser.add_argument('--global-connected-limit', help='Max connections', default=512, type=int, metavar='global_connected_limit')
         self.parser.add_argument('--workers', help='Download workers per site', default=5, type=int, metavar='workers')
 
-        self.parser.add_argument('--fileserver_ip', help='FileServer bind address', default="*", metavar='ip')
-        self.parser.add_argument('--fileserver_port', help='FileServer bind port (0: randomize)', default=0, type=int, metavar='port')
-        self.parser.add_argument('--fileserver_port_range', help='FileServer randomization range', default="10000-40000", metavar='port')
-        self.parser.add_argument('--fileserver_ip_type', help='FileServer ip type', default="dual", choices=["ipv4", "ipv6", "dual"])
-        self.parser.add_argument('--ip_local', help='My local ips', default=ip_local, type=int, metavar='ip', nargs='*')
-        self.parser.add_argument('--ip_external', help='Set reported external ip (tested on start if None)', metavar='ip', nargs='*')
+        self.parser.add_argument('--fileserver-ip', help='FileServer bind address', default="*", metavar='ip')
+        self.parser.add_argument('--fileserver-port', help='FileServer bind port (0: randomize)', default=0, type=int, metavar='port')
+        self.parser.add_argument('--fileserver-port-range', help='FileServer randomization range', default="10000-40000", metavar='port')
+        self.parser.add_argument('--fileserver-ip-type', help='FileServer ip type', default="dual", choices=["ipv4", "ipv6", "dual"])
+        self.parser.add_argument('--ip-local', help='My local ips', default=ip_local, type=int, metavar='ip', nargs='*')
+        self.parser.add_argument('--ip-external', help='Set reported external ip (tested on start if None)', metavar='ip', nargs='*')
         self.parser.add_argument('--offline', help='Disable network communication', action='store_true')
-        self.parser.add_argument('--disable_port_check', help='Disable checking port', action='store_true')
+        self.parser.add_argument('--disable-port-check', help='Disable checking port', action='store_true')
 
-        self.parser.add_argument('--disable_udp', help='Disable UDP connections', action='store_true')
+        self.parser.add_argument('--disable-udp', help='Disable UDP connections', action='store_true')
         self.parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port')
         self.parser.add_argument('--bind', help='Bind outgoing sockets to this address', metavar='ip')
-        self.parser.add_argument('--bootstrap_url', help='URL of file with link to bootstrap bundle', default='https://raw.githubusercontent.com/zeronet-conservancy/zeronet-conservancy/master/bootstrap.url', type=str)
-        self.parser.add_argument('--disable_bootstrap', help='Disable downloading bootstrap information from clearnet', action='store_true')
+        self.parser.add_argument('--bootstrap-url', help='URL of file with link to bootstrap bundle', default='https://raw.githubusercontent.com/zeronet-conservancy/zeronet-conservancy/master/bootstrap.url', type=str)
+        self.parser.add_argument('--disable-bootstrap', help='Disable downloading bootstrap information from clearnet', action='store_true')
         self.parser.add_argument('--trackers', help='Bootstraping torrent trackers', default=[], metavar='protocol://address', nargs='*')
-        self.parser.add_argument('--trackers_file', help='Load torrent trackers dynamically from a file', default=['{data_dir}/15CEFKBRHFfAP9rmL6hhLmHoXrrgmw4B5o/cache/1/Syncronite.html'], metavar='path', nargs='*')
-        self.parser.add_argument('--trackers_proxy', help='Force use proxy to connect to trackers (disable, tor, ip:port)', default="disable")
-        self.parser.add_argument('--use_libsecp256k1', help='Use Libsecp256k1 liblary for speedup', type='bool', choices=[True, False], default=True)
-        self.parser.add_argument('--use_openssl', help='Use OpenSSL liblary for speedup', type='bool', choices=[True, False], default=True)
-        self.parser.add_argument('--openssl_lib_file', help='Path for OpenSSL library file (default: detect)', default=argparse.SUPPRESS, metavar="path")
-        self.parser.add_argument('--openssl_bin_file', help='Path for OpenSSL binary file (default: detect)', default=argparse.SUPPRESS, metavar="path")
-        self.parser.add_argument('--disable_db', help='Disable database updating', action='store_true')
-        self.parser.add_argument('--disable_encryption', help='Disable connection encryption', action='store_true')
-        self.parser.add_argument('--force_encryption', help="Enforce encryption to all peer connections", action='store_true')
-        self.parser.add_argument('--disable_sslcompression', help='Disable SSL compression to save memory',
+        self.parser.add_argument('--trackers-file', help='Load torrent trackers dynamically from a file (using Syncronite by default)', default=['{data_dir}/15CEFKBRHFfAP9rmL6hhLmHoXrrgmw4B5o/cache/1/Syncronite.html'], metavar='path', nargs='*')
+        self.parser.add_argument('--trackers-proxy', help='Force use proxy to connect to trackers (disable, tor, ip:port)', default="disable")
+        self.parser.add_argument('--use-libsecp256k1', help='Use Libsecp256k1 liblary for speedup', type='bool', choices=[True, False], default=True)
+        self.parser.add_argument('--use-openssl', help='Use OpenSSL liblary for speedup', type='bool', choices=[True, False], default=True)
+        self.parser.add_argument('--openssl-lib-file', help='Path for OpenSSL library file (default: detect)', default=argparse.SUPPRESS, metavar="path")
+        self.parser.add_argument('--openssl-bin-file', help='Path for OpenSSL binary file (default: detect)', default=argparse.SUPPRESS, metavar="path")
+        self.parser.add_argument('--disable-db', help='Disable database updating', action='store_true')
+        self.parser.add_argument('--disable-encryption', help='Disable connection encryption', action='store_true')
+        self.parser.add_argument('--force-encryption', help="Enforce encryption to all peer connections", action='store_true')
+        self.parser.add_argument('--disable-sslcompression', help='Disable SSL compression to save memory',
                                  type='bool', choices=[True, False], default=True)
-        self.parser.add_argument('--keep_ssl_cert', help='Disable new SSL cert generation on startup', action='store_true')
-        self.parser.add_argument('--max_files_opened', help='Change maximum opened files allowed by OS to this value on startup',
+        self.parser.add_argument('--keep-ssl-cert', help='Disable new SSL cert generation on startup', action='store_true')
+        self.parser.add_argument('--max-files-opened', help='Change maximum opened files allowed by OS to this value on startup',
                                  default=2048, type=int, metavar='limit')
-        self.parser.add_argument('--stack_size', help='Change thread stack size', default=None, type=int, metavar='thread_stack_size')
-        self.parser.add_argument('--use_tempfiles', help='Use temporary files when downloading (experimental)',
+        self.parser.add_argument('--stack-size', help='Change thread stack size', default=None, type=int, metavar='thread_stack_size')
+        self.parser.add_argument('--use-tempfiles', help='Use temporary files when downloading (experimental)',
                                  type='bool', choices=[True, False], default=False)
-        self.parser.add_argument('--stream_downloads', help='Stream download directly to files (experimental)',
+        self.parser.add_argument('--stream-downloads', help='Stream download directly to files (experimental)',
                                  type='bool', choices=[True, False], default=False)
-        self.parser.add_argument("--msgpack_purepython", help='Use less memory, but a bit more CPU power',
+        self.parser.add_argument('--msgpack-purepython', help='Use less memory, but a bit more CPU power',
                                  type='bool', choices=[True, False], default=False)
-        self.parser.add_argument("--fix_float_decimals", help='Fix content.json modification date float precision on verification',
+        self.parser.add_argument('--fix-float-decimals', help='Fix content.json modification date float precision on verification',
                                  type='bool', choices=[True, False], default=fix_float_decimals)
-        self.parser.add_argument("--db_mode", choices=["speed", "security"], default="speed")
+        self.parser.add_argument('--db-mode', choices=["speed", "security"], default="speed")
 
-        self.parser.add_argument('--threads_fs_read', help='Number of threads for file read operations', default=1, type=int)
-        self.parser.add_argument('--threads_fs_write', help='Number of threads for file write operations', default=1, type=int)
-        self.parser.add_argument('--threads_crypt', help='Number of threads for cryptographic operations', default=2, type=int)
-        self.parser.add_argument('--threads_db', help='Number of threads for database operations', default=1, type=int)
+        self.parser.add_argument('--threads-fs-read', help='Number of threads for file read operations', default=1, type=int)
+        self.parser.add_argument('--threads-fs-write', help='Number of threads for file write operations', default=1, type=int)
+        self.parser.add_argument('--threads-crypt', help='Number of threads for cryptographic operations', default=2, type=int)
+        self.parser.add_argument('--threads-db', help='Number of threads for database operations', default=1, type=int)
 
-        self.parser.add_argument("--download_optional", choices=["manual", "auto"], default="manual")
+        self.parser.add_argument('--download-optional', choices=["manual", "auto"], default="manual")
 
         self.parser.add_argument('--tor', help='enable: Use only for Tor peers, always: Use Tor for every connection', choices=["disable", "enable", "always"], default='enable')
-        self.parser.add_argument('--tor_controller', help='Tor controller address', metavar='ip:port', default='127.0.0.1:9051')
-        self.parser.add_argument('--tor_proxy', help='Tor proxy address', metavar='ip:port', default='127.0.0.1:9050')
-        self.parser.add_argument('--tor_password', help='Tor controller password', metavar='password')
-        self.parser.add_argument('--tor_use_bridges', help='Use obfuscated bridge relays to avoid Tor block', action='store_true')
-        self.parser.add_argument('--tor_hs_limit', help='Maximum number of hidden services in Tor always mode', metavar='limit', type=int, default=10)
-        self.parser.add_argument('--tor_hs_port', help='Hidden service port in Tor always mode', metavar='limit', type=int, default=15441)
+        self.parser.add_argument('--tor-controller', help='Tor controller address', metavar='ip:port', default='127.0.0.1:9051')
+        self.parser.add_argument('--tor-proxy', help='Tor proxy address', metavar='ip:port', default='127.0.0.1:9050')
+        self.parser.add_argument('--tor-password', help='Tor controller password', metavar='password')
+        self.parser.add_argument('--tor-use-bridges', help='Use obfuscated bridge relays to avoid Tor block', action='store_true')
+        self.parser.add_argument('--tor-hs-limit', help='Maximum number of hidden services in Tor always mode', metavar='limit', type=int, default=10)
+        self.parser.add_argument('--tor-hs-port', help='Hidden service port in Tor always mode', metavar='limit', type=int, default=15441)
 
         self.parser.add_argument('--repl', help='Instead of printing logs in console, drop into REPL after initialization', action='store_true')
         self.parser.add_argument('--version', action='version', version=f'zeronet-conservancy {self.version} r{self.rev}')
@@ -414,8 +414,21 @@ class Config(object):
 
         self.loadTrackersFile()
 
-    # Parse command line arguments
+    def fixArgs(self, args):
+        "Fix old-style flags and issue a warning"
+        res = []
+        for arg in args:
+            if arg.startswith('--') and '_' in arg:
+                farg = arg.replace('_', '-')
+                print(f'WARNING: using deprecated flag in command line: {arg} should be {farg}')
+                print('Support for deprecated flags might be removed in the future')
+            else:
+                farg = arg
+            res.append(farg)
+        return res
+
     def parseCommandline(self, argv, silent=False):
+        argv = self.fixArgs(argv)
         # Find out if action is specificed on start
         action = self.getAction(argv)
         if not action:
@@ -434,11 +447,11 @@ class Config(object):
         if self.arguments.ui_site_port is None:
             self.arguments.ui_site_port = self.arguments.ui_port + 1
 
-    # Parse config file
     def parseConfig(self, argv):
+        argv = self.fixArgs(argv)
         # Find config file path from parameters
-        if "--config_file" in argv:
-            self.config_file = argv[argv.index("--config_file") + 1]
+        if "--config-file" in argv:
+            self.config_file = argv[argv.index("--config-file") + 1]
         # Load config file
         if os.path.isfile(self.config_file):
             config = configparser.RawConfigParser(allow_no_value=True, strict=False)
@@ -449,13 +462,9 @@ class Config(object):
                         val = None
                     if section != "global":  # If not global prefix key with section
                         key = section + "_" + key
+                    key = key.replace('_', '-')
 
-                    if key == "open_browser":  # Prefer config file value over cli argument
-                        while "--%s" % key in argv:
-                            pos = argv.index("--open_browser")
-                            del argv[pos:pos + 2]
-
-                    argv_extend = ["--%s" % key]
+                    argv_extend = [f'--{key}']
                     if val:
                         for line in val.strip().split("\n"):  # Allow multi-line values
                             argv_extend.append(line)

From ee0829d95e4d030a5a5d5a3dd4699c10ec8a8026 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 22 Dec 2023 11:00:58 +0000
Subject: [PATCH 285/333] Minor CryptBitcoin refactor

---
 src/Crypt/CryptBitcoin.py | 33 +++++++++++++++++++++++++--------
 1 file changed, 25 insertions(+), 8 deletions(-)

diff --git a/src/Crypt/CryptBitcoin.py b/src/Crypt/CryptBitcoin.py
index a0807187..25377f5d 100644
--- a/src/Crypt/CryptBitcoin.py
+++ b/src/Crypt/CryptBitcoin.py
@@ -4,6 +4,9 @@ import binascii
 import time
 import hashlib
 
+from collections.abc import Container
+from typing import Optional
+
 from util.Electrum import dbl_format
 from Config import config
 
@@ -69,7 +72,8 @@ def privatekeyToAddress(privatekey):  # Return address from private key
         return False
 
 
-def sign(data, privatekey):  # Return sign to data using private key
+def sign(data: str, privatekey: str) -> str:
+    """Sign data with privatekey, return base64 string signature"""
     if privatekey.startswith("23") and len(privatekey) > 52:
         return None  # Old style private key not supported
     return base64.b64encode(sslcurve.sign(
@@ -79,13 +83,13 @@ def sign(data, privatekey):  # Return sign to data using private key
         hash=dbl_format
     )).decode()
 
-
-def verify(data, valid_address, sign, lib_verify=None):  # Verify data using address and sign
+def get_sign_address_64(data: str, sign: str, lib_verify=None) -> Optional[str]:
+    """Returns pubkey/address of signer if any"""
     if not lib_verify:
         lib_verify = lib_verify_best
 
     if not sign:
-        return False
+        return None
 
     if lib_verify == "libsecp256k1":
         sign_address = libsecp256k1message.recover_address(data.encode("utf8"), sign).decode("utf8")
@@ -95,10 +99,23 @@ def verify(data, valid_address, sign, lib_verify=None):  # Verify data using add
     else:
         raise Exception("No library enabled for signature verification")
 
-    if type(valid_address) is list:  # Any address in the list
-        return sign_address in valid_address
-    else:  # One possible address
-        return sign_address == valid_address
+    return sign_address
+
+def verify(*args, **kwargs):
+    """Default verify, see verify64"""
+    return verify64(*args, **kwargs)
+
+def verify64(data: str, addresses: str | Container[str], sign: str, lib_verify=None) -> bool:
+    """Verify that sign is a valid signature for data by one of addresses
+
+    Expecting signature to be in base64
+    """
+    sign_address = get_sign_address_64(data, sign, lib_verify)
+
+    if isinstance(addresses, str):
+        return sign_address == addresses
+    else:
+        return sign_address in addresses
 
 def isValidAddress(addr):
     '''Check if provided address is valid bitcoin address'''

From 31d94a16b62821931beeb5260c5d489a869c90f3 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 22 Dec 2023 12:59:04 +0000
Subject: [PATCH 286/333] Add option to ignore compromised certificates

This would allow to stop spam attacks without pausing sites that allow
compromised certificates and without action from site owners. However,
without mass adoption of adding individual permissions on sites for
valid users of such certificates, this will also block updates from
such users.

In any case this is more of a temporary measure in case we face such
an attack before the whole user id issue is resolved
---
 src/Config.py                 |  2 ++
 src/Content/ContentManager.py | 43 +++++++++++++++++++++++++++++++----
 src/Ui/UiWebsocket.py         |  3 +++
 3 files changed, 43 insertions(+), 5 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index fee378d0..99619c47 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -297,6 +297,8 @@ class Config(object):
 
         self.parser.add_argument('--download-optional', choices=["manual", "auto"], default="manual")
 
+        self.parser.add_argument('--lax-cert-check', action=argparse.BooleanOptionalAction, default=True, help="Enabling lax cert check allows users getting site writing priviledges by employing compromized (i.e. with leaked private keys) cert issuer. Disable for spam protection")
+
         self.parser.add_argument('--tor', help='enable: Use only for Tor peers, always: Use Tor for every connection', choices=["disable", "enable", "always"], default='enable')
         self.parser.add_argument('--tor-controller', help='Tor controller address', metavar='ip:port', default='127.0.0.1:9051')
         self.parser.add_argument('--tor-proxy', help='Tor proxy address', metavar='ip:port', default='127.0.0.1:9050')
diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index a06ba523..938f3d6f 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -29,7 +29,17 @@ class SignError(Exception):
 
 
 @PluginManager.acceptPlugins
-class ContentManager(object):
+class ContentManager:
+    """Manage site content verifying and other related stuff"""
+
+    def loadBadCerts():
+        try:
+            with open(f'{config.data_dir}/badcerts.json') as f:
+                return set(json.load(f))
+        except FileNotFoundError:
+            return set()
+
+    bad_certs = loadBadCerts()
 
     def __init__(self, site):
         self.site = site
@@ -38,6 +48,13 @@ class ContentManager(object):
         self.hashfield = PeerHashfield()
         self.has_optional_files = False
 
+    def addBadCert(self, sign):
+        addr = CryptBitcoin.get_sign_address_64('compromised', sign)
+        if addr:
+            self.bad_certs.add(addr)
+            with open(f'{config.data_dir}/badcerts.json', 'w') as f:
+                json.dump(list(self.bad_certs), f)
+
     # Load all content.json files
     def loadContents(self):
         if len(self.contents) == 0:
@@ -478,6 +495,9 @@ class ContentManager(object):
                 return self.getUserContentRules(parent_content, inner_path, content)
         return False
 
+    def isGoodCert(self, cert):
+        return cert not in self.bad_certs
+
     # Get rules for a user file
     # Return: The rules of the file or False if not allowed
     def getUserContentRules(self, parent_content, inner_path, content):
@@ -511,7 +531,20 @@ class ContentManager(object):
             banned = False
         if "signers" in rules:
             rules["signers"] = rules["signers"][:]  # Make copy of the signers
-        for permission_pattern, permission_rules in list(user_contents["permission_rules"].items()):  # Regexp rules
+
+        if content is not None:
+            name, domain = content['cert_user_id'].rsplit('@', 1)
+            cert_addresses = parent_content['user_contents']['cert_signers'].get(domain)
+        else:
+            cert_addresses = None
+        # to prevent spam, if cert is compromised, only allow personal rules
+        if config.lax_cert_check or cert_addresses is None or self.isGoodCert(cert_addresses[0]):
+            permission_rules = user_contents.get('permission_rules', {}).items()
+            if not self.isGoodCert(cert_addresses[0]):
+                self.log.warning('Accepting compromised certificate! This may lead to spam attack. Turn off with --no_lax_cert_check. Default behaviour may change in the future.')
+        else:
+            permission_rules = {}
+        for permission_pattern, permission_rules in permission_rules:  # Regexp rules
             if not SafeRe.match(permission_pattern, user_urn):
                 continue  # Rule is not valid for user
             if permission_rules is None:
@@ -892,9 +925,9 @@ class ContentManager(object):
             raise VerifyError("No rules")
 
         # Check include size limit
-        if rules.get("max_size") is not None:  # Include size limit
-            if content_size > rules["max_size"]:
-                raise VerifyError("Include too large %sB > %sB" % (content_size, rules["max_size"]))
+        max_size = rules.get("max_size", 0)
+        if content_size > max_size:
+            raise VerifyError(f'Include too large {content_size}B > {max_size}B')
 
         if rules.get("max_size_optional") is not None:  # Include optional files limit
             if content_size_optional > rules["max_size_optional"]:
diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index e6f2f405..953c63d9 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -405,6 +405,9 @@ class UiWebsocket(object):
     def actionSiteBadFiles(self, to):
         return list(self.site.bad_files.keys())
 
+    def actionBadCert(self, to, sign):
+        self.site.content_manager.addBadCert(sign)
+
     # Join to an event channel
     def actionChannelJoin(self, to, channels):
         if type(channels) != list:

From 2ce87828b0f377afa540112a1452a577a3f409b8 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 22 Dec 2023 17:20:11 +0000
Subject: [PATCH 287/333] Fix py3.9 compatibility

refs #252
---
 src/Worker/Worker.py | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py
index 9adba30d..64d3e2ec 100644
--- a/src/Worker/Worker.py
+++ b/src/Worker/Worker.py
@@ -128,8 +128,11 @@ class Worker(object):
                 "%s: Verify failed: %s, error: %s, failed peers: %s, workers: %s" %
                 (self.key, task["inner_path"], error, len(task["failed"]), task["workers_num"])
             )
-            # traceback.format_
-            self.manager.log.debug(''.join(traceback.format_exception(error)))
+            if sys.version_info.major == 3 and sys.version_info.minor < 10:
+                tbk = traceback.format_exc() # ugh, this could in theory be a different error
+            else:
+                tbk = traceback.format_exception(error)
+            self.manager.log.debug(''.join(tbk))
         task["failed"].append(self.peer)
         self.peer.hash_failed += 1
         if self.peer.hash_failed >= max(len(self.manager.tasks), 3) or self.peer.connection_error > 10:

From 0382aee68f1d5ce36761b8cc659dd7f8c8bca49c Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 26 Dec 2023 15:14:00 +0000
Subject: [PATCH 288/333] CHANGELOG

---
 CHANGELOG.md  | 1 +
 src/Config.py | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 172f5f7b..edfcada8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
 - fix chromium compatibility (@caryoscelus)
 - better fix of local sites leak (@caryoscelus)
 - ipython-based repl via --repl for debug/interactive development (@caryoscelus)
+- optional blocking of compromised id certificates for spam protection (@caryoscelus)
 - various improvements
 
 ### zeronet-conservancy 0.7.10 (2023-07-26) (18d35d3bed4f0683e99)
diff --git a/src/Config.py b/src/Config.py
index 99619c47..a950be09 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -15,7 +15,7 @@ class Config(object):
         self.version = "0.7.10+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
-        self.rev = 5140
+        self.rev = 5141
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None

From dbe283e593a94c4b122007305f1a714554bba3ab Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 26 Dec 2023 19:06:09 +0000
Subject: [PATCH 289/333] Fix import

---
 src/Worker/Worker.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py
index 64d3e2ec..db929d9f 100644
--- a/src/Worker/Worker.py
+++ b/src/Worker/Worker.py
@@ -1,4 +1,5 @@
 import time
+import sys
 
 import gevent
 import gevent.lock

From 1a8c6aaa939fb97591944b04c6ecea48d46f5a72 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 27 Dec 2023 11:27:15 +0000
Subject: [PATCH 290/333] Fetch version info from git or Build file

---
 requirements.txt |  1 +
 src/Config.py    | 18 ++++++++++++--
 src/buildinfo.py | 47 ++++++++++++++++++++++++++++++++++++
 src/util/Git.py  | 62 ++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 126 insertions(+), 2 deletions(-)
 create mode 100755 src/buildinfo.py
 create mode 100644 src/util/Git.py

diff --git a/requirements.txt b/requirements.txt
index b9eb9fc9..c5ecfae2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,3 +16,4 @@ defusedxml>=0.7
 pyaes
 requests
 ipython>=8
+GitPython
diff --git a/src/Config.py b/src/Config.py
index fee378d0..768443e5 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -1,6 +1,7 @@
 import argparse
 import sys
 import os
+import platform
 import locale
 import re
 import configparser
@@ -9,9 +10,21 @@ import logging.handlers
 import stat
 import time
 
-class Config(object):
+class Config:
 
     def __init__(self, argv):
+        try:
+            from . import Build
+        except ImportError:
+            print('cannot find build')
+            from .util import Git
+            self.build_type = 'source'
+            self.branch = Git.branch() or 'unknown'
+            self.commit = Git.commit() or 'unknown'
+        else:
+            self.build_type = Build.build_type
+            self.branch = Build.branch
+            self.commit = Build.commit
         self.version = "0.7.10+"
         self.user_agent = "conservancy"
         # DEPRECATED ; replace with git-generated commit
@@ -306,7 +319,8 @@ class Config(object):
         self.parser.add_argument('--tor-hs-port', help='Hidden service port in Tor always mode', metavar='limit', type=int, default=15441)
 
         self.parser.add_argument('--repl', help='Instead of printing logs in console, drop into REPL after initialization', action='store_true')
-        self.parser.add_argument('--version', action='version', version=f'zeronet-conservancy {self.version} r{self.rev}')
+        self.parser.add_argument('--version', action='version',
+                                 version=f'zeronet-conservancy {self.version} ({self.build_type} from {self.branch}-{self.commit})')
         self.parser.add_argument('--end', help='Stop multi value argument parsing', action='store_true')
 
         return self.parser
diff --git a/src/buildinfo.py b/src/buildinfo.py
new file mode 100755
index 00000000..38de4cae
--- /dev/null
+++ b/src/buildinfo.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+
+##  Copyright (c) 2023 caryoscelus
+##
+##  zeronet-conservancy is free software: you can redistribute it and/or modify it under the
+##  terms of the GNU General Public License as published by the Free Software
+##  Foundation, either version 3 of the License, or (at your option) any later version.
+##
+##  zeronet-conservancy is distributed in the hope that it will be useful, but
+##  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+##  FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+##  details.
+##
+## You should have received a copy of the GNU General Public License along with
+## zeronet-conservancy. If not, see <https://www.gnu.org/licenses/>.
+##
+
+"""A small script to generate build info"""
+
+from util import Git
+
+import sys
+
+def main(argv):
+    if len(argv) < 2:
+        print(f'Useage: {argv[0]} <build-type>')
+        print('Known build types:')
+        sys.exit(1)
+    else:
+        writeBuildInfo(argv[1])
+
+def writeBuildInfo(build_type):
+    bvars = {
+        'build_type': build_type,
+        'branch': Git.branch(),
+        'commit': Git.commit(),
+    }
+    code = '\n'.join(f'{var} = {repr(val)}' for var, val in bvars.items())
+    content = \
+        '# auto-generated by buildinfo.py\n' \
+        '# This file should not persist in git tree\n' + code + '\n'
+    with open('src/Build.py', 'w') as f:
+        f.write(content)
+
+if __name__ == '__main__':
+    from sys import argv
+    main(argv)
diff --git a/src/util/Git.py b/src/util/Git.py
new file mode 100644
index 00000000..7b60d396
--- /dev/null
+++ b/src/util/Git.py
@@ -0,0 +1,62 @@
+##  Copyright (c) 2023 caryoscelus
+##
+##  zeronet-conservancy is free software: you can redistribute it and/or modify it under the
+##  terms of the GNU General Public License as published by the Free Software
+##  Foundation, either version 3 of the License, or (at your option) any later version.
+##
+##  zeronet-conservancy is distributed in the hope that it will be useful, but
+##  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+##  FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+##  details.
+##
+## You should have received a copy of the GNU General Public License along with
+## zeronet-conservancy. If not, see <https://www.gnu.org/licenses/>.
+##
+
+"""Git-related operations
+
+Currently this is only to retrieve git revision for debug purposes, but later on we might
+also want to use it for updates.
+"""
+
+import os
+
+from typing import Optional
+
+global git
+
+try:
+    import git
+except ImportError:
+    git = None
+else:
+    try:
+        global _repo
+        up = os.path.dirname
+        root = up(up(up(__file__)))
+        print(root)
+        _repo = git.Repo(root)
+    except Exception as exc:
+        print("Caught exception while trying to detect git repo.")
+        traceback.print_exc()
+        git = None
+
+def _gitted(f):
+    if git:
+        return f
+    else:
+        return lambda *args, **kwargs: None
+
+@_gitted
+def commit() -> str:
+    """Returns git revision, possibly suffixed with -dirty"""
+    dirty = '-dirty' if _repo.is_dirty() else ''
+    return f'{_repo.head.commit}{dirty}'
+
+@_gitted
+def branch() -> Optional[str]:
+    """Returns current git branch if any"""
+    try:
+        return str(_repo.active_branch)
+    except TypeError:
+        return None

From 15dddc27c6164c83663235b2b98e0ecbc455e06d Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 27 Dec 2023 13:17:59 +0000
Subject: [PATCH 291/333] Get rid of using old-style revision everywhere

---
 plugins/AnnounceLocal/AnnounceLocalPlugin.py | 2 +-
 plugins/Stats/StatsPlugin.py                 | 4 ++--
 src/Config.py                                | 7 +++----
 src/Plugin/PluginManager.py                  | 3 ---
 src/Site/Site.py                             | 1 -
 src/Ui/UiRequest.py                          | 4 ++--
 src/Ui/UiWebsocket.py                        | 6 ++++--
 src/main.py                                  | 2 +-
 8 files changed, 13 insertions(+), 16 deletions(-)

diff --git a/plugins/AnnounceLocal/AnnounceLocalPlugin.py b/plugins/AnnounceLocal/AnnounceLocalPlugin.py
index b9225966..2034a436 100644
--- a/plugins/AnnounceLocal/AnnounceLocalPlugin.py
+++ b/plugins/AnnounceLocal/AnnounceLocalPlugin.py
@@ -31,7 +31,7 @@ class LocalAnnouncer(BroadcastServer.BroadcastServer):
         self.sender_info["peer_id"] = self.server.peer_id
         self.sender_info["port"] = self.server.port
         self.sender_info["broadcast_port"] = listen_port
-        self.sender_info["rev"] = config.rev
+        self.sender_info["rev"] = config.user_agent_rev
 
         self.known_peers = {}
         self.last_discover = 0
diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py
index 32fd06c5..be692ecf 100644
--- a/plugins/Stats/StatsPlugin.py
+++ b/plugins/Stats/StatsPlugin.py
@@ -41,7 +41,7 @@ class UiRequestPlugin(object):
         from Crypt import CryptConnection
 
         # Memory
-        yield "rev%s | " % config.rev
+        yield f'{config.version_full} | '
         yield "%s | " % main.file_server.ip_external_list
         yield "Port: %s | " % main.file_server.port
         yield "Network: %s | " % main.file_server.supported_ip_types
@@ -579,7 +579,7 @@ class ActionsPlugin:
         yield "\n"
 
         yield from self.formatTable(
-            ["ZeroNet version:", "%s rev%s" % (config.version, config.rev)],
+            ["zeronet-conservancy version:", config.version_full],
             ["Python:", "%s" % sys.version],
             ["Platform:", "%s" % sys.platform],
             ["Crypt verify lib:", "%s" % CryptBitcoin.lib_verify_best],
diff --git a/src/Config.py b/src/Config.py
index 768443e5..8a847bf3 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -26,9 +26,9 @@ class Config:
             self.branch = Build.branch
             self.commit = Build.commit
         self.version = "0.7.10+"
+        self.version_full = f'{self.version} ({self.build_type} from {self.branch}-{self.commit})'
         self.user_agent = "conservancy"
-        # DEPRECATED ; replace with git-generated commit
-        self.rev = 5140
+        # for compatibility
         self.user_agent_rev = 8192
         self.argv = argv
         self.action = None
@@ -319,8 +319,7 @@ class Config:
         self.parser.add_argument('--tor-hs-port', help='Hidden service port in Tor always mode', metavar='limit', type=int, default=15441)
 
         self.parser.add_argument('--repl', help='Instead of printing logs in console, drop into REPL after initialization', action='store_true')
-        self.parser.add_argument('--version', action='version',
-                                 version=f'zeronet-conservancy {self.version} ({self.build_type} from {self.branch}-{self.commit})')
+        self.parser.add_argument('--version', action='version', version=f'zeronet-conservancy {self.version_full}')
         self.parser.add_argument('--end', help='Stop multi value argument parsing', action='store_true')
 
         return self.parser
diff --git a/src/Plugin/PluginManager.py b/src/Plugin/PluginManager.py
index 5855c842..ab0940e8 100644
--- a/src/Plugin/PluginManager.py
+++ b/src/Plugin/PluginManager.py
@@ -22,7 +22,6 @@ class PluginManager:
         self.pluggable = {}
         self.plugin_names = []  # Loaded plugin names
         self.plugins_updated = {}  # List of updated plugins since restart
-        self.plugins_rev = {}  # Installed plugins revision numbers
         self.after_load = []   # Execute functions after loaded plugins
         self.function_flags = {}  # Flag function for permissions
         self.reloading = False
@@ -88,7 +87,6 @@ class PluginManager:
             plugin["dir_path"] = dir_path
             plugin["inner_path"] = plugin_name
             plugin["enabled"] = is_enabled
-            plugin["rev"] = config.rev
             plugin["loaded"] = plugin_name in self.plugin_names
             plugins.append(plugin)
 
@@ -101,7 +99,6 @@ class PluginManager:
         for plugin in self.listPlugins():
             self.log.debug("Loading plugin: %s (%s)" % (plugin["name"], plugin["source"]))
             if plugin["source"] != "builtin":
-                self.plugins_rev[plugin["name"]] = plugin["rev"]
                 site_plugin_dir = os.path.dirname(plugin["dir_path"])
                 if site_plugin_dir not in sys.path:
                     sys.path.append(site_plugin_dir)
diff --git a/src/Site/Site.py b/src/Site/Site.py
index ae2be36a..ad0e3ca2 100644
--- a/src/Site/Site.py
+++ b/src/Site/Site.py
@@ -600,7 +600,6 @@ class Site(object):
         num_connected_peers = len(peers)
 
         random.shuffle(peers)
-        peers = sorted(peers, key=lambda peer: peer.connection.handshake.get("rev", 0) < config.rev - 100)  # Prefer newer clients
 
         if len(peers) < limit * 2 and len(self.peers) > len(peers):  # Add more, non-connected peers if necessary
             peers += self.getRecentPeers(limit * 2)
diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 17e63c9b..2b06661d 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -660,7 +660,7 @@ class UiRequest:
             permissions=json.dumps(site.settings["permissions"]),
             show_loadingscreen=json.dumps(show_loadingscreen),
             sandbox_permissions=sandbox_permissions,
-            rev=config.rev,
+            rev=config.commit,
             lang=config.language,
             homepage=homepage,
             themeclass=themeclass,
@@ -1038,7 +1038,7 @@ class UiRequest:
 
         if details and config.debug:
             details = {key: val for key, val in list(self.env.items()) if hasattr(val, "endswith") and "COOKIE" not in key}
-            details["version_zeronet"] = "%s r%s" % (config.version, config.rev)
+            details["version_zeronet"] = config.version_full
             details["version_python"] = sys.version
             details["version_gevent"] = gevent.__version__
             details["plugins"] = PluginManager.plugin_manager.plugin_names
diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index e6f2f405..a21c9885 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -328,13 +328,15 @@ class UiWebsocket(object):
                 'ui_ip' : config.ui_ip,
                 'ui_port' : config.ui_port,
                 'version' : config.version,
-                'rev' : config.rev,
+                # The only place this is used is in dashboard so we shorten it for now
+                'rev' : config.commit[:8],
                 'timecorrection' : file_server.timecorrection,
                 'language' : config.language,
                 'debug' : config.debug,
                 'offline' : config.offline,
                 'plugins' : PluginManager.plugin_manager.plugin_names,
-                'plugins_rev' : PluginManager.plugin_manager.plugins_rev,
+                # For compat only
+                'plugins_rev' : {},
                 'user_settings' : self.user.settings,
                 'lib_verify_best' : CryptBitcoin.lib_verify_best
             }
diff --git a/src/main.py b/src/main.py
index 3c526b85..b7fd6e8b 100644
--- a/src/main.py
+++ b/src/main.py
@@ -196,7 +196,7 @@ elif config.bind:
 @PluginManager.acceptPlugins
 class Actions:
     def call(self, function_name, kwargs):
-        logging.info("Version: %s r%s, Python %s, Gevent: %s" % (config.version, config.rev, sys.version, gevent.__version__))
+        logging.info(f'zeronet-conservancy {config.version_full} on Python {sys.version} Gevent {gevent.__version__}')
 
         func = getattr(self, function_name, None)
         back = func(**kwargs)

From d66dafc1118081f21e7f150b4fc8ae7134ffba48 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 27 Dec 2023 13:58:03 +0000
Subject: [PATCH 292/333] WIP

---
 src/Config.py | 65 +++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 50 insertions(+), 15 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index 8a847bf3..bf482d93 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -44,8 +44,8 @@ class Config:
             "tor", "fileserver_port", "fileserver_ip_type", "threads_fs_read", "threads_fs_write", "threads_crypt", "threads_db"
         ])
         self.start_dir = self.getStartDir()
-
         self.config_file = self.start_dir + "/zeronet.conf"
+        self.private_dir = self.start_dir + '/private'
         self.data_dir = self.start_dir + "/data"
         self.log_dir = self.start_dir + "/log"
         self.openssl_lib_file = None
@@ -69,19 +69,45 @@ class Config:
         return v.lower() in ("yes", "true", "t", "1")
 
     def getStartDir(self):
-        this_file = os.path.abspath(__file__).replace("\\", "/").rstrip("cd")
-
+        """Return directory with config & data"""
         if "--start-dir" in self.argv:
-            start_dir = self.argv[self.argv.index("--start-dir") + 1]
-        elif this_file.endswith("/Contents/Resources/core/src/Config.py"):
-            # Running as ZeroNet.app
-            if this_file.startswith("/Application") or this_file.startswith("/private") or this_file.startswith(os.path.expanduser("~/Library")):
-                # Runnig from non-writeable directory, put data to Application Support
-                start_dir = os.path.expanduser("~/Library/Application Support/ZeroNet")
-            else:
-                # Running from writeable directory put data next to .app
-                start_dir = re.sub("/[^/]+/Contents/Resources/core/src/Config.py", "", this_file)
-        elif this_file.endswith("/core/src/Config.py"):
+            return self.argv[self.argv.index("--start-dir") + 1]
+
+        if '--portable' in self.argv or self.build_type == 'portable':
+            return '.'
+
+        here = os.path.dirname(os.path.abspath(__file__).replace("\\", "/"))
+        if os.path.isdir(f'{here}/data') and not '--no-portable' in self.argv:
+            print('WARNING: found data in current directory')
+            print('  It used to be default behaviour to store data alongside project directory,')
+            print('  but now we default to place data and config in user home directory.')
+            print('  If you want to keep previous behaviour, please use --portable')
+            print('Assuming implicit --portable (use --no-portable to override)')
+            print(self.argv)
+            self.argv.insert(1, '--portable')
+            print(self.argv)
+            return '.'
+
+        home_zn = os.path.expanduser(f'~/ZeroNet')
+        if os.path.isdir(home_zn):
+            print(f'WARNING: found data in {home_zn}')
+            print( '  It is possible that this is from previous version or another installation')
+            print( '  altogether. If you want to use that data directory with zeronet-conservancy')
+            print(f'  you have to run it with --start-dir "{home_zn}" option')
+
+        if platform.system() == 'Linux':
+            # XDG!
+            return os.path.expanduser('~/.local/zeronet-conservancy')
+
+        if platform.system() == 'Darwin':
+            return os.path.expanduser("~/Library/Application Support/zeronet-conservancy")
+
+        if platform.system() == 'Windows':
+            return os.path.expanduser('~/AppData/zeronet-conservancy')
+            
+        elif here.endswith("/Contents/Resources/core/src"):
+            start_dir = os.path.expanduser("~/Library/Application Support/ZeroNet")
+        elif this_file.endswith("/core/src"):
             # Running as exe or source is at Application Support directory, put var files to outside of core dir
             start_dir = this_file.replace("/core/src/Config.py", "")
         elif not os.access(this_file.replace('/src/Config.py', ''), os.R_OK | os.W_OK):
@@ -90,8 +116,6 @@ class Config:
         else:
             start_dir = "."
 
-        return start_dir
-
     # Create command line arguments
     def createArguments(self):
         try:
@@ -409,11 +433,14 @@ class Config:
 
         self.parseCommandline(argv, silent)  # Parse argv
         self.setAttributes()
+        print('Parsed command line once')
+        print(self.arguments)
         if parse_config:
             argv = self.parseConfig(argv)  # Add arguments from config file
 
         self.parseCommandline(argv, silent)  # Parse argv
         self.setAttributes()
+        print('Parsed command line twice')
 
         if not silent:
             if self.fileserver_ip != "*" and self.fileserver_ip not in self.ip_local:
@@ -426,6 +453,7 @@ class Config:
             current_parser.exit = original_exit
 
         self.loadTrackersFile()
+        print('Parse done')
 
     def fixArgs(self, args):
         "Fix old-style flags and issue a warning"
@@ -450,13 +478,20 @@ class Config:
             action = "main"
         argv = self.moveUnknownToEnd(argv, action)
         if silent:
+            # print(argv[1:])
             res = self.parser.parse_known_args(argv[1:])
+            # print(res) # ????
+            # parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+            # parser.add_subparsers(title='Action', dest='action')
+            # parser.parse_args(argv[1:])
             if res:
                 self.arguments = res[0]
             else:
                 self.arguments = {}
         else:
+            print('Parsing again')
             self.arguments = self.parser.parse_args(argv[1:])
+            print('Parsed thrice')
         if self.arguments.ui_site_port is None:
             self.arguments.ui_site_port = self.arguments.ui_port + 1
 

From 25bfc0341ca3f0d8a1bba0f89d77f1724f2ebca8 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 27 Dec 2023 14:24:03 +0000
Subject: [PATCH 293/333] Use integer rev in UiWebSocket for dashboard
 compatibility

---
 src/Ui/UiWebsocket.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index a21c9885..086e4444 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -328,8 +328,8 @@ class UiWebsocket(object):
                 'ui_ip' : config.ui_ip,
                 'ui_port' : config.ui_port,
                 'version' : config.version,
-                # The only place this is used is in dashboard so we shorten it for now
-                'rev' : config.commit[:8],
+                # Some legacy code relies on this being an integer, so lets return dummy one
+                'rev' : config.user_agent_rev,
                 'timecorrection' : file_server.timecorrection,
                 'language' : config.language,
                 'debug' : config.debug,

From f3c57cdc32cb07ff69696af4b973d6a26f262aff Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 17 Jan 2024 21:18:15 +0000
Subject: [PATCH 294/333] Fix command-line argument style (use dashes instead
 of underscores)

---
 plugins/AnnounceLocal/AnnounceLocalPlugin.py     | 2 +-
 plugins/AnnounceShare/AnnounceSharePlugin.py     | 2 +-
 plugins/Benchmark/BenchmarkPlugin.py             | 4 ++--
 plugins/Bigfile/BigfilePlugin.py                 | 4 ++--
 plugins/OptionalManager/OptionalManagerPlugin.py | 4 ++--
 plugins/Zeroname/SiteManagerPlugin.py            | 2 +-
 plugins/disabled-Multiuser/MultiuserPlugin.py    | 4 ++--
 plugins/disabled-UiPassword/UiPasswordPlugin.py  | 2 +-
 8 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/plugins/AnnounceLocal/AnnounceLocalPlugin.py b/plugins/AnnounceLocal/AnnounceLocalPlugin.py
index 2034a436..8b6b490d 100644
--- a/plugins/AnnounceLocal/AnnounceLocalPlugin.py
+++ b/plugins/AnnounceLocal/AnnounceLocalPlugin.py
@@ -142,6 +142,6 @@ class FileServerPlugin(object):
 class ConfigPlugin(object):
     def createArguments(self):
         group = self.parser.add_argument_group("AnnounceLocal plugin")
-        group.add_argument('--broadcast_port', help='UDP broadcasting port for local peer discovery', default=1544, type=int, metavar='port')
+        group.add_argument('--broadcast-port', help='UDP broadcasting port for local peer discovery', default=1544, type=int, metavar='port')
 
         return super(ConfigPlugin, self).createArguments()
diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py
index 057ce55a..b350cf42 100644
--- a/plugins/AnnounceShare/AnnounceSharePlugin.py
+++ b/plugins/AnnounceShare/AnnounceSharePlugin.py
@@ -185,6 +185,6 @@ class FileServerPlugin(object):
 class ConfigPlugin(object):
     def createArguments(self):
         group = self.parser.add_argument_group("AnnounceShare plugin")
-        group.add_argument('--working_shared_trackers_limit', help='Stop discovering new shared trackers after this number of shared trackers reached', default=5, type=int, metavar='limit')
+        group.add_argument('--working-shared-trackers-limit', help='Stop discovering new shared trackers after this number of shared trackers reached', default=5, type=int, metavar='limit')
 
         return super(ConfigPlugin, self).createArguments()
diff --git a/plugins/Benchmark/BenchmarkPlugin.py b/plugins/Benchmark/BenchmarkPlugin.py
index fd6cacf3..193b46c2 100644
--- a/plugins/Benchmark/BenchmarkPlugin.py
+++ b/plugins/Benchmark/BenchmarkPlugin.py
@@ -413,7 +413,7 @@ class ConfigPlugin(object):
         back = super(ConfigPlugin, self).createArguments()
         if self.getCmdlineValue("test") == "benchmark":
             self.test_parser.add_argument(
-                '--num_multipler', help='Benchmark run time multipler',
+                '--num-multipler', help='Benchmark run time multipler',
                 default=1.0, type=float, metavar='num'
             )
             self.test_parser.add_argument(
@@ -422,7 +422,7 @@ class ConfigPlugin(object):
             )
         elif self.getCmdlineValue("test") == "portChecker":
             self.test_parser.add_argument(
-                '--func_name', help='Name of open port checker function',
+                '--func-name', help='Name of open port checker function',
                 default=None, metavar='func_name'
             )
         return back
diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py
index 78a27b05..6fc8f43b 100644
--- a/plugins/Bigfile/BigfilePlugin.py
+++ b/plugins/Bigfile/BigfilePlugin.py
@@ -837,7 +837,7 @@ class SitePlugin(object):
 class ConfigPlugin(object):
     def createArguments(self):
         group = self.parser.add_argument_group("Bigfile plugin")
-        group.add_argument('--autodownload_bigfile_size_limit', help='Also download bigfiles smaller than this limit if help distribute option is checked', default=10, metavar="MB", type=int)
-        group.add_argument('--bigfile_size_limit', help='Maximum size of downloaded big files', default=False, metavar="MB", type=int)
+        group.add_argument('--autodownload-bigfile-size-limit', help='Also download bigfiles smaller than this limit if help distribute option is checked', default=10, metavar="MB", type=int)
+        group.add_argument('--bigfile-size-limit', help='Maximum size of downloaded big files', default=False, metavar="MB", type=int)
 
         return super(ConfigPlugin, self).createArguments()
diff --git a/plugins/OptionalManager/OptionalManagerPlugin.py b/plugins/OptionalManager/OptionalManagerPlugin.py
index f01fab65..420f9e05 100644
--- a/plugins/OptionalManager/OptionalManagerPlugin.py
+++ b/plugins/OptionalManager/OptionalManagerPlugin.py
@@ -247,7 +247,7 @@ class SitePlugin(object):
 class ConfigPlugin(object):
     def createArguments(self):
         group = self.parser.add_argument_group("OptionalManager plugin")
-        group.add_argument('--optional_limit', help='Limit total size of optional files', default="10%", metavar="GB or free space %")
-        group.add_argument('--optional_limit_exclude_minsize', help='Exclude files larger than this limit from optional size limit calculation', default=20, metavar="MB", type=int)
+        group.add_argument('--optional-limit', help='Limit total size of optional files', default="10%", metavar="GB or free space %")
+        group.add_argument('--optional-limit-exclude-minsize', help='Exclude files larger than this limit from optional size limit calculation', default=20, metavar="MB", type=int)
 
         return super(ConfigPlugin, self).createArguments()
diff --git a/plugins/Zeroname/SiteManagerPlugin.py b/plugins/Zeroname/SiteManagerPlugin.py
index c25fafa1..cca79dd8 100644
--- a/plugins/Zeroname/SiteManagerPlugin.py
+++ b/plugins/Zeroname/SiteManagerPlugin.py
@@ -62,7 +62,7 @@ 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",
+            "--bit-resolver", help="ZeroNet site to resolve .bit domains (deprecated)",
             default="1GnACKctkJrGWHTqxk9T9zXo2bLQc2PDnF", metavar="address"
         )
 
diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py
index fd28ead8..a2fd79ae 100644
--- a/plugins/disabled-Multiuser/MultiuserPlugin.py
+++ b/plugins/disabled-Multiuser/MultiuserPlugin.py
@@ -272,7 +272,7 @@ class UiWebsocketPlugin(object):
 class ConfigPlugin(object):
     def createArguments(self):
         group = self.parser.add_argument_group("Multiuser plugin")
-        group.add_argument('--multiuser_local', help="Enable unsafe Ui functions and write users to disk", action='store_true')
-        group.add_argument('--multiuser_no_new_sites', help="Denies adding new sites by normal users", action='store_true')
+        group.add_argument('--multiuser-local', help="Enable unsafe Ui functions and write users to disk", action='store_true')
+        group.add_argument('--multiuser-no-new-sites', help="Denies adding new sites by normal users", action='store_true')
 
         return super(ConfigPlugin, self).createArguments()
diff --git a/plugins/disabled-UiPassword/UiPasswordPlugin.py b/plugins/disabled-UiPassword/UiPasswordPlugin.py
index e8a4e4fe..1123a695 100644
--- a/plugins/disabled-UiPassword/UiPasswordPlugin.py
+++ b/plugins/disabled-UiPassword/UiPasswordPlugin.py
@@ -159,7 +159,7 @@ class UiRequestPlugin(object):
 class ConfigPlugin(object):
     def createArguments(self):
         group = self.parser.add_argument_group("UiPassword plugin")
-        group.add_argument('--ui_password', help='Password to access UiServer', default=None, metavar="password")
+        group.add_argument('--ui-password', help='Password to access UiServer', default=None, metavar="password")
 
         return super(ConfigPlugin, self).createArguments()
 

From 2520024f5683e0864a9fc9e3fa2068a12cc14718 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 3 Feb 2024 11:19:22 +0000
Subject: [PATCH 295/333] Allow cross-site embedding without "cors-" prefix

..As long as CORS read permission is granted. This is done for
compatibility with sites that relied on lack of enforcing of cross-site
isolation in previous ZeroNet versions.

fixes #259
---
 src/Ui/UiRequest.py | 23 +++++++++++++++++++++--
 1 file changed, 21 insertions(+), 2 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 2b06661d..34af96e2 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -100,6 +100,25 @@ class UiRequest:
     def resolveDomain(self, domain):
         return self.server.site_manager.resolveDomainCached(domain)
 
+    def hasCorsPermission(self, referer):
+        """Check if site from referer has CORS permission to read site in current request
+
+        NOTE: this allows embedding WITHOUT prepending "cors-" (as it has already been used
+        for a long time e.g. on ZeroBlog++ based sites) as long as read permission has been
+        granted.
+        """
+        target_path = self.env['PATH_INFO']
+        if referer is None or target_path is None:
+            return False
+        s_parts = self.parsePath(referer)
+        t_parts = self.parsePath(target_path)
+        s_address = s_parts['address']
+        t_address = t_parts['address']
+        if not s_address or not t_address:
+            return False
+        s_site = self.server.sites[s_address]
+        return f'Cors:{t_address}' in s_site.settings['permissions']
+
     def isCrossOriginRequest(self):
         """Prevent detecting sites on this 0net instance
 
@@ -129,7 +148,7 @@ class UiRequest:
             return False
 
         # Deny cross site requests
-        if not self.isSameOrigin(referer, url):
+        if not self.isSameOrigin(referer, url) and not self.hasCorsPermission(referer):
             return True
 
         return False
@@ -731,7 +750,7 @@ class UiRequest:
         if "../" in path or "./" in path:
             raise SecurityError("Invalid path")
 
-        match = re.match(r"/(media/)?(?P<address>[A-Za-z0-9]+[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
+        match = re.match(r"(?P<server>(http[s]{0,1}://(.*?))?)/(media/)?(?P<address>[A-Za-z0-9]+[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
         if match:
             path_parts = match.groupdict()
             addr = path_parts["address"]

From ca5d573908c428571c41c6568917465acf538024 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 9 Feb 2024 15:55:08 +0000
Subject: [PATCH 296/333] Fix typo

---
 src/Ui/UiServer.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py
index 27727d4c..de90f8f1 100644
--- a/src/Ui/UiServer.py
+++ b/src/Ui/UiServer.py
@@ -162,7 +162,7 @@ class UiServer:
             return ui_request.route(path)
         except Exception as err:
             logging.debug(f"UiRequest @ site error: {Debug.formatException(err)}")
-            return ui_request.error500('Error while trying to server site data')
+            return ui_request.error500('Error while trying to serve site data')
 
     def startSiteServer(self):
         self.site_server = WSGIServer((self.ip, self.site_port), self.handleSiteRequest, log=self.log)

From d17f4afdd039c7f2c34f9ce5bfe2efed22ad72a3 Mon Sep 17 00:00:00 2001
From: yggverse <yggverse@bot>
Date: Wed, 27 Mar 2024 20:33:52 +0200
Subject: [PATCH 297/333] fix IPv6 validation #263

---
 src/Ui/UiRequest.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 34af96e2..a61b0439 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -82,7 +82,7 @@ class UiRequest:
             self.learnHost(host)
             return True
 
-        if ":" in host and helper.isIp(host.rsplit(":", 1)[0]):  # Test without port
+        if ":" in host and helper.isIp(host.rsplit(":", 1)[0].lstrip("[").rstrip("]")):  # Test without port
             self.learnHost(host)
             return True
 

From 8a95e9b67de099ec941fc78e1c52dbdbe17174b3 Mon Sep 17 00:00:00 2001
From: yggverse <yggverse@bot>
Date: Wed, 27 Mar 2024 21:27:53 +0200
Subject: [PATCH 298/333] gitignore enabled plugins that disabled by default
 #265

---
 .gitignore | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/.gitignore b/.gitignore
index 2fce8187..0d03e87f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,11 @@ zeronet.conf
 
 # ZeroNet log files
 log/*
+
+# Enabled plugins that disabled by default
+plugins/Bootstrapper
+plugins/DonationMessage
+plugins/Multiuser
+plugins/NoNewSites
+plugins/StemPort
+plugins/UiPassword

From 762c5f24029a1e6f034120065889dcbf22841c71 Mon Sep 17 00:00:00 2001
From: yggverse <yggverse@bot>
Date: Thu, 28 Mar 2024 15:52:19 +0200
Subject: [PATCH 299/333] remove square brackets from IPv6 host #263 #264 #267

---
 src/Ui/UiRequest.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index a61b0439..4b11548f 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -544,7 +544,7 @@ class UiRequest:
         return server_url
 
     def getHostWithoutPort(self):
-        return ':'.join(self.env['HTTP_HOST'].split(':')[:-1])
+        return ':'.join(self.env['HTTP_HOST'].split(':')[:-1]).lstrip("[").rstrip("]")
 
     def processQueryString(self, site, query_string):
         match = re.search("zeronet_peers=(.*?)(&|$)", query_string)

From 658c685a451a95b83bf1df7988e5a21ed1541e63 Mon Sep 17 00:00:00 2001
From: yggverse <yggverse@bot>
Date: Thu, 28 Mar 2024 16:16:02 +0200
Subject: [PATCH 300/333] add configurable ipv6_testip #263

---
 src/File/FileServer.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/File/FileServer.py b/src/File/FileServer.py
index d1de4761..ee14d4ce 100644
--- a/src/File/FileServer.py
+++ b/src/File/FileServer.py
@@ -98,7 +98,10 @@ class FileServer(ConnectionServer):
         if config.tor == "always":
             return True
         # Test if we can connect to ipv6 address
-        ipv6_testip = "fcec:ae97:8902:d810:6c92:ec67:efb2:3ec5"
+        if config.ipv6_testip:
+            ipv6_testip = config.ipv6_testip
+        else:
+            ipv6_testip = "fcec:ae97:8902:d810:6c92:ec67:efb2:3ec5"
         try:
             sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
             sock.connect((ipv6_testip, 80))

From 5858a68d1166ee1b1b783ad702d5e9f3dc4ba680 Mon Sep 17 00:00:00 2001
From: yggverse <yggverse@bot>
Date: Thu, 28 Mar 2024 16:22:44 +0200
Subject: [PATCH 301/333] add ipv6_testip to server info #263

---
 src/Config.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/Config.py b/src/Config.py
index 8a847bf3..8885f576 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -582,6 +582,7 @@ class Config:
             "fileserver_port": self.fileserver_port,
             "ui_ip": self.ui_ip,
             "ui_port": self.ui_port,
+            "ipv6_testip": self.ipv6_testip,
             "version": self.version,
             "rev": self.rev,
             "language": self.language,

From 4030d3a8592219151cf86402ed02a074b22479b1 Mon Sep 17 00:00:00 2001
From: yggverse <yggverse@bot>
Date: Thu, 28 Mar 2024 16:28:58 +0200
Subject: [PATCH 302/333] remove ipv6_testip info as dependent #263

---
 src/Config.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/Config.py b/src/Config.py
index 8885f576..8a847bf3 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -582,7 +582,6 @@ class Config:
             "fileserver_port": self.fileserver_port,
             "ui_ip": self.ui_ip,
             "ui_port": self.ui_port,
-            "ipv6_testip": self.ipv6_testip,
             "version": self.version,
             "rev": self.rev,
             "language": self.language,

From 8fb98991ef9febe38cc83f8b7ea3b7e7041d6da9 Mon Sep 17 00:00:00 2001
From: yggverse <yggverse@bot>
Date: Thu, 28 Mar 2024 16:31:44 +0200
Subject: [PATCH 303/333] fix optional ipv6_testip check #263

---
 src/File/FileServer.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/File/FileServer.py b/src/File/FileServer.py
index ee14d4ce..212cd301 100644
--- a/src/File/FileServer.py
+++ b/src/File/FileServer.py
@@ -98,7 +98,7 @@ class FileServer(ConnectionServer):
         if config.tor == "always":
             return True
         # Test if we can connect to ipv6 address
-        if config.ipv6_testip:
+        if hasattr(myObject, "ipv6_testip"):
             ipv6_testip = config.ipv6_testip
         else:
             ipv6_testip = "fcec:ae97:8902:d810:6c92:ec67:efb2:3ec5"

From 3c24e5d34c6b0101be089be3d5ccde8c142d949c Mon Sep 17 00:00:00 2001
From: yggverse <yggverse@bot>
Date: Thu, 28 Mar 2024 16:32:30 +0200
Subject: [PATCH 304/333] fix object name #263

---
 src/File/FileServer.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/File/FileServer.py b/src/File/FileServer.py
index 212cd301..c74a288d 100644
--- a/src/File/FileServer.py
+++ b/src/File/FileServer.py
@@ -98,7 +98,7 @@ class FileServer(ConnectionServer):
         if config.tor == "always":
             return True
         # Test if we can connect to ipv6 address
-        if hasattr(myObject, "ipv6_testip"):
+        if hasattr(config, "ipv6_testip"):
             ipv6_testip = config.ipv6_testip
         else:
             ipv6_testip = "fcec:ae97:8902:d810:6c92:ec67:efb2:3ec5"

From b928cfc0d5ac8c846045d398075942adf17d63d2 Mon Sep 17 00:00:00 2001
From: yggverse <yggverse@bot>
Date: Thu, 28 Mar 2024 16:48:01 +0200
Subject: [PATCH 305/333] add ipv6 support to openBrowser method #263

---
 src/util/helper.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/util/helper.py b/src/util/helper.py
index af65f727..8c7c6fff 100644
--- a/src/util/helper.py
+++ b/src/util/helper.py
@@ -325,7 +325,10 @@ def openBrowser(agent):
     if agent and agent != "False":
         print(f"Opening browser: {agent}...")
         ui_ip = config.ui_ip if config.ui_ip != "*" else "127.0.0.1"
-        url = f'http://{ui_ip}:{config.ui_port}/{config.homepage}'
+        if ':' in ui_ip: # IPv6
+            url = f'http://[{ui_ip}]:{config.ui_port}/{config.homepage}'
+        else: # IPv4
+            url = f'http://{ui_ip}:{config.ui_port}/{config.homepage}'
         try:
             import subprocess
             return subprocess.Popen([config.open_browser, url])

From f4e52fce5a279a89a8cdebad458c21cdb204df86 Mon Sep 17 00:00:00 2001
From: yggverse <yggverse@bot>
Date: Thu, 28 Mar 2024 17:06:57 +0200
Subject: [PATCH 306/333] rollback ipv6_testip feature as dependent of config
 implementation #263 #269

---
 src/File/FileServer.py | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/File/FileServer.py b/src/File/FileServer.py
index c74a288d..d1de4761 100644
--- a/src/File/FileServer.py
+++ b/src/File/FileServer.py
@@ -98,10 +98,7 @@ class FileServer(ConnectionServer):
         if config.tor == "always":
             return True
         # Test if we can connect to ipv6 address
-        if hasattr(config, "ipv6_testip"):
-            ipv6_testip = config.ipv6_testip
-        else:
-            ipv6_testip = "fcec:ae97:8902:d810:6c92:ec67:efb2:3ec5"
+        ipv6_testip = "fcec:ae97:8902:d810:6c92:ec67:efb2:3ec5"
         try:
             sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
             sock.connect((ipv6_testip, 80))

From 6702f331a3dbdfea6ba463dd370690a6f566c1df Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 30 Mar 2024 14:31:22 +0000
Subject: [PATCH 307/333] IPv6/non-localhost IP fix

- Content-Security-Policy: `frame-src *` for main UiRequest and `self` for
  user-content UiRequest
- revert change in getHostWithoutPort to make ipv6 work
---
 src/Ui/UiRequest.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 4b11548f..482096a3 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -382,10 +382,12 @@ class UiRequest:
             port = int(self.env['SERVER_PORT'])
             if port == config.ui_port:
                 other_port = config.ui_site_port
+                frame_src = '*'
             else:
                 other_port = config.ui_port
-            site_server = f'{host}:{other_port}'
-            headers["Content-Security-Policy"] = f"default-src 'none'; script-src 'nonce-{script_nonce}'; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src {site_server}"
+                frame_src = 'self'
+
+            headers["Content-Security-Policy"] = f"default-src 'none'; script-src 'nonce-{script_nonce}'; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src {frame_src}"
 
         if allow_ajax:
             headers["Access-Control-Allow-Origin"] = "null"
@@ -544,7 +546,7 @@ class UiRequest:
         return server_url
 
     def getHostWithoutPort(self):
-        return ':'.join(self.env['HTTP_HOST'].split(':')[:-1]).lstrip("[").rstrip("]")
+        return ':'.join(self.env['HTTP_HOST'].split(':')[:-1])
 
     def processQueryString(self, site, query_string):
         match = re.search("zeronet_peers=(.*?)(&|$)", query_string)

From 33436f7eb66c850f105d37fddddaf27a92563ad4 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sun, 31 Mar 2024 16:21:52 +0000
Subject: [PATCH 308/333] Remove outdated compatibility code

---
 src/main.py | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/src/main.py b/src/main.py
index b7fd6e8b..4176459b 100644
--- a/src/main.py
+++ b/src/main.py
@@ -12,13 +12,6 @@ def startupError(msg):
 
 # Third party modules
 import gevent
-if gevent.version_info.major <= 1:  # Workaround for random crash when libuv used with threads
-    try:
-        if "libev" not in str(gevent.config.loop):
-            gevent.config.loop = "libev-cext"
-    except Exception as err:
-        startupError("Unable to switch gevent loop to libev: %s" % err)
-
 import gevent.monkey
 gevent.monkey.patch_all(thread=False, subprocess=False)
 

From bbe577310c971c06d360970fd9beef67d9c60332 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 4 Apr 2024 12:37:47 +0000
Subject: [PATCH 309/333] New config/data path management (WIP)

---
 src/Config.py |  5 +++--
 src/main.py   | 26 ++++++++++++--------------
 2 files changed, 15 insertions(+), 16 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index bf482d93..7c13d4d7 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -76,7 +76,7 @@ class Config:
         if '--portable' in self.argv or self.build_type == 'portable':
             return '.'
 
-        here = os.path.dirname(os.path.abspath(__file__).replace("\\", "/"))
+        here = os.path.dirname(os.path.abspath(__file__).replace("\\", "/")).rstrip('/src')
         if os.path.isdir(f'{here}/data') and not '--no-portable' in self.argv:
             print('WARNING: found data in current directory')
             print('  It used to be default behaviour to store data alongside project directory,')
@@ -253,6 +253,7 @@ class Config:
 
         self.parser.add_argument('--batch', help="Batch mode (No interactive input for commands)", action='store_true')
 
+        self.parser.add_argument('--portable', action=argparse.BooleanOptionalAction)
         self.parser.add_argument('--start-dir', help='Path of working dir for variable content (data, log, .conf)', default=self.start_dir, metavar="path")
         self.parser.add_argument('--config-file', help='Path of config file', default=config_file, metavar="path")
         self.parser.add_argument('--data-dir', help='Path of data directory', default=data_dir, metavar="path")
@@ -300,7 +301,7 @@ class Config:
         self.parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port')
         self.parser.add_argument('--bind', help='Bind outgoing sockets to this address', metavar='ip')
         self.parser.add_argument('--bootstrap-url', help='URL of file with link to bootstrap bundle', default='https://raw.githubusercontent.com/zeronet-conservancy/zeronet-conservancy/master/bootstrap.url', type=str)
-        self.parser.add_argument('--disable-bootstrap', help='Disable downloading bootstrap information from clearnet', action='store_true')
+        self.parser.add_argument('--bootstrap', help='Enable downloading bootstrap information from clearnet', action=argparse.BooleanOptionalAction, default=True)
         self.parser.add_argument('--trackers', help='Bootstraping torrent trackers', default=[], metavar='protocol://address', nargs='*')
         self.parser.add_argument('--trackers-file', help='Load torrent trackers dynamically from a file (using Syncronite by default)', default=['{data_dir}/15CEFKBRHFfAP9rmL6hhLmHoXrrgmw4B5o/cache/1/Syncronite.html'], metavar='path', nargs='*')
         self.parser.add_argument('--trackers-proxy', help='Force use proxy to connect to trackers (disable, tor, ip:port)', default="disable")
diff --git a/src/main.py b/src/main.py
index 4176459b..b899fbcc 100644
--- a/src/main.py
+++ b/src/main.py
@@ -4,6 +4,7 @@ import stat
 import time
 import logging
 from util.compat import *
+from pathlib import Path
 
 startup_errors = []
 def startupError(msg):
@@ -51,31 +52,28 @@ def importBundle(bundle):
                                 map(lambda f: removeprefix(f, prefix).split('/')[0], all_files))))
         for d in top_2:
             if isValidAddress(d):
-                logging.info(f'unpack {d} into {config.data_dir}')
+                print(f'Unpacking {d} into {config.data_dir}')
                 for fname in filter(lambda f: f.startswith(prefix+d) and not f.endswith('/'), all_files):
-                    tgt = config.data_dir + '/' + removeprefix(fname, prefix)
-                    logging.info(f'-- {fname} --> {tgt}')
+                    tgt = removeprefix(fname, prefix)
+                    print(f'-- {fname} --> {tgt}')
                     info = zf.getinfo(fname)
                     info.filename = tgt
-                    zf.extract(info)
+                    zf.extract(info, path=config.data_dir)
                 logging.info(f'add site {d}')
                 sites[d] = {}
             else:
-                logging.info(f'Warning: unknown file in a bundle: {prefix+d}')
+                print(f'Warning: unknown file in a bundle: {prefix+d}')
     with open(sites_json_path, 'w') as f:
         json.dump(sites, f)
 
 def init_dirs():
-    data_dir = config.data_dir
-    has_data_dir = os.path.isdir(data_dir)
-    need_bootstrap = not config.disable_bootstrap and (not has_data_dir or not os.path.isfile(f'{data_dir}/sites.json')) and not config.offline
+    data_dir = Path(config.data_dir)
+    need_bootstrap = (config.bootstrap
+                      and not config.offline
+                      and (not data_dir.is_dir() or not (data_dir / 'sites.json').is_file()))
 
-    if not has_data_dir:
-        os.mkdir(data_dir)
-        try:
-            os.chmod(data_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
-        except Exception as err:
-            startupError(f"Can't change permission of {data_dir}: {err}")
+    if not data_dir.is_dir():
+        data_dir.mkdir(parents=True, exist_ok=True)
 
     if need_bootstrap:
         import requests

From b674737f99a1710a4694b2da3bdddc755c6bb35e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 4 Apr 2024 13:08:21 +0000
Subject: [PATCH 310/333] Restructure main, split Actions.py, minor clean-up

---
 src/Actions.py | 521 +++++++++++++++++++++++++++++++++++++++
 src/Config.py  |   8 +-
 src/main.py    | 658 ++++++-------------------------------------------
 3 files changed, 591 insertions(+), 596 deletions(-)
 create mode 100644 src/Actions.py

diff --git a/src/Actions.py b/src/Actions.py
new file mode 100644
index 00000000..7a1fe7d9
--- /dev/null
+++ b/src/Actions.py
@@ -0,0 +1,521 @@
+import logging
+import sys
+import gevent
+from Config import config
+from Plugin import PluginManager
+
+@PluginManager.acceptPlugins
+class Actions:
+    def call(self, function_name, kwargs):
+        logging.info(f'zeronet-conservancy {config.version_full} on Python {sys.version} Gevent {gevent.__version__}')
+
+        func = getattr(self, function_name, None)
+        back = func(**kwargs)
+        if back:
+            print(back)
+
+    def ipythonThread(self):
+        import IPython
+        IPython.embed()
+        self.gevent_quit.set()
+
+    # Default action: Start serving UiServer and FileServer
+    def main(self):
+        import main
+        from File import FileServer
+        from Ui import UiServer
+        logging.info("Creating FileServer....")
+        main.file_server = FileServer()
+        logging.info("Creating UiServer....")
+        main.ui_server = UiServer()
+        main.file_server.ui_server = main.ui_server
+
+        # for startup_error in startup_errors:
+            # logging.error("Startup error: %s" % startup_error)
+
+        logging.info("Removing old SSL certs...")
+        from Crypt import CryptConnection
+        CryptConnection.manager.removeCerts()
+
+        logging.info("Starting servers....")
+
+        import threading
+        self.gevent_quit = threading.Event()
+        launched_greenlets = [gevent.spawn(main.ui_server.start), gevent.spawn(main.file_server.start), gevent.spawn(main.ui_server.startSiteServer)]
+
+        # if --repl, start ipython thread
+        # FIXME: Unfortunately this leads to exceptions on exit so use with care
+        if config.repl:
+            threading.Thread(target=self.ipythonThread).start()
+
+        stopped = 0
+        # Process all greenlets in main thread
+        while not self.gevent_quit.is_set() and stopped < len(launched_greenlets):
+            stopped += len(gevent.joinall(launched_greenlets, timeout=1))
+
+        # Exited due to repl, so must kill greenlets
+        if stopped < len(launched_greenlets):
+            gevent.killall(launched_greenlets, exception=KeyboardInterrupt)
+
+        logging.info("All server stopped")
+
+    # Site commands
+
+    def siteCreate(self, use_master_seed=True):
+        logging.info("Generating new privatekey (use_master_seed: %s)..." % config.use_master_seed)
+        from Crypt import CryptBitcoin
+        if use_master_seed:
+            from User import UserManager
+            user = UserManager.user_manager.get()
+            if not user:
+                user = UserManager.user_manager.create()
+            address, address_index, site_data = user.getNewSiteData()
+            privatekey = site_data["privatekey"]
+            logging.info("Generated using master seed from users.json, site index: %s" % address_index)
+        else:
+            privatekey = CryptBitcoin.newPrivatekey()
+            address = CryptBitcoin.privatekeyToAddress(privatekey)
+        logging.info("----------------------------------------------------------------------")
+        logging.info("Site private key: %s" % privatekey)
+        logging.info("                  !!! ^ Save it now, required to modify the site ^ !!!")
+        logging.info("Site address:     %s" % address)
+        logging.info("----------------------------------------------------------------------")
+
+        while True and not config.batch and not use_master_seed:
+            if input("? Have you secured your private key? (yes, no) > ").lower() == "yes":
+                break
+            else:
+                logging.info("Please, secure it now, you going to need it to modify your site!")
+
+        logging.info("Creating directory structure...")
+        from Site.Site import Site
+        from Site import SiteManager
+        SiteManager.site_manager.load()
+
+        os.mkdir("%s/%s" % (config.data_dir, address))
+        open("%s/%s/index.html" % (config.data_dir, address), "w").write("Hello %s!" % address)
+
+        logging.info("Creating content.json...")
+        site = Site(address)
+        extend = {"postmessage_nonce_security": True}
+        if use_master_seed:
+            extend["address_index"] = address_index
+
+        site.content_manager.sign(privatekey=privatekey, extend=extend)
+        site.settings["own"] = True
+        site.saveSettings()
+
+        logging.info("Site created!")
+
+    def siteSign(self, address, privatekey=None, inner_path="content.json", publish=False, remove_missing_optional=False):
+        from Site.Site import Site
+        from Site import SiteManager
+        from Debug import Debug
+        SiteManager.site_manager.load()
+        logging.info("Signing site: %s..." % address)
+        site = Site(address, allow_create=False)
+
+        if not privatekey:  # If no privatekey defined
+            from User import UserManager
+            user = UserManager.user_manager.get()
+            if user:
+                site_data = user.getSiteData(address)
+                privatekey = site_data.get("privatekey")
+            else:
+                privatekey = None
+            if not privatekey:
+                # Not found in users.json, ask from console
+                import getpass
+                privatekey = getpass.getpass("Private key (input hidden):")
+        # inner_path can be either relative to site directory or absolute/relative path
+        if os.path.isabs(inner_path):
+            full_path = os.path.abspath(inner_path)
+        else:
+            full_path = os.path.abspath(config.working_dir + '/' + inner_path)
+        print(full_path)
+        if os.path.isfile(full_path):
+            if address in full_path:
+                # assuming site address is unique, keep only path after it
+                inner_path = full_path.split(address+'/')[1]
+            else:
+                # oops, file that we found seems to be rogue, so reverting to old behaviour
+                logging.warning(f'using {inner_path} relative to site directory')
+        try:
+            succ = site.content_manager.sign(
+                inner_path=inner_path, privatekey=privatekey,
+                update_changed_files=True, remove_missing_optional=remove_missing_optional
+            )
+        except Exception as err:
+            logging.error("Sign error: %s" % Debug.formatException(err))
+            succ = False
+        if succ and publish:
+            self.sitePublish(address, inner_path=inner_path)
+
+    def siteVerify(self, address):
+        import time
+        from Site.Site import Site
+        from Site import SiteManager
+        SiteManager.site_manager.load()
+
+        s = time.time()
+        logging.info("Verifing site: %s..." % address)
+        site = Site(address)
+        bad_files = []
+
+        for content_inner_path in site.content_manager.contents:
+            s = time.time()
+            logging.info("Verifing %s signature..." % content_inner_path)
+            error = None
+            try:
+                file_correct = site.content_manager.verifyFile(
+                    content_inner_path, site.storage.open(content_inner_path, "rb"), ignore_same=False
+                )
+            except Exception as err:
+                file_correct = False
+                error = err
+
+            if file_correct is True:
+                logging.info("[OK] %s (Done in %.3fs)" % (content_inner_path, time.time() - s))
+            else:
+                logging.error("[ERROR] %s: invalid file: %s!" % (content_inner_path, error))
+                input("Continue?")
+                bad_files += content_inner_path
+
+        logging.info("Verifying site files...")
+        bad_files += site.storage.verifyFiles()["bad_files"]
+        if not bad_files:
+            logging.info("[OK] All file sha512sum matches! (%.3fs)" % (time.time() - s))
+        else:
+            logging.error("[ERROR] Error during verifying site files!")
+
+    def dbRebuild(self, address):
+        from Site.Site import Site
+        from Site import SiteManager
+        SiteManager.site_manager.load()
+
+        logging.info("Rebuilding site sql cache: %s..." % address)
+        site = SiteManager.site_manager.get(address)
+        s = time.time()
+        try:
+            site.storage.rebuildDb()
+            logging.info("Done in %.3fs" % (time.time() - s))
+        except Exception as err:
+            logging.error(err)
+
+    def dbQuery(self, address, query):
+        from Site.Site import Site
+        from Site import SiteManager
+        SiteManager.site_manager.load()
+
+        import json
+        site = Site(address)
+        result = []
+        for row in site.storage.query(query):
+            result.append(dict(row))
+        print(json.dumps(result, indent=4))
+
+    def siteAnnounce(self, address):
+        from Site.Site import Site
+        from Site import SiteManager
+        SiteManager.site_manager.load()
+
+        logging.info("Opening a simple connection server")
+        from File import FileServer
+        main.file_server = FileServer("127.0.0.1", 1234)
+        main.file_server.start()
+
+        logging.info("Announcing site %s to tracker..." % address)
+        site = Site(address)
+
+        s = time.time()
+        site.announce()
+        print("Response time: %.3fs" % (time.time() - s))
+        print(site.peers)
+
+    def siteDownload(self, address):
+        from Site.Site import Site
+        from Site import SiteManager
+        SiteManager.site_manager.load()
+
+        logging.info("Opening a simple connection server")
+        from File import FileServer
+        main.file_server = FileServer("127.0.0.1", 1234)
+        file_server_thread = gevent.spawn(main.file_server.start, check_sites=False)
+
+        site = Site(address)
+
+        on_completed = gevent.event.AsyncResult()
+
+        def onComplete(evt):
+            evt.set(True)
+
+        site.onComplete.once(lambda: onComplete(on_completed))
+        print("Announcing...")
+        site.announce()
+
+        s = time.time()
+        print("Downloading...")
+        site.downloadContent("content.json", check_modifications=True)
+
+        print("Downloaded in %.3fs" % (time.time()-s))
+
+    def siteNeedFile(self, address, inner_path):
+        from Site.Site import Site
+        from Site import SiteManager
+        SiteManager.site_manager.load()
+
+        def checker():
+            while 1:
+                s = time.time()
+                time.sleep(1)
+                print("Switch time:", time.time() - s)
+        gevent.spawn(checker)
+
+        logging.info("Opening a simple connection server")
+        from File import FileServer
+        main.file_server = FileServer("127.0.0.1", 1234)
+        file_server_thread = gevent.spawn(main.file_server.start, check_sites=False)
+
+        site = Site(address)
+        site.announce()
+        print(site.needFile(inner_path, update=True))
+
+    def siteCmd(self, address, cmd, parameters):
+        import json
+        from Site import SiteManager
+
+        site = SiteManager.site_manager.get(address)
+
+        if not site:
+            logging.error("Site not found: %s" % address)
+            return None
+
+        ws = self.getWebsocket(site)
+
+        ws.send(json.dumps({"cmd": cmd, "params": parameters, "id": 1}))
+        res_raw = ws.recv()
+
+        try:
+            res = json.loads(res_raw)
+        except Exception as err:
+            return {"error": "Invalid result: %s" % err, "res_raw": res_raw}
+
+        if "result" in res:
+            return res["result"]
+        else:
+            return res
+
+    def importBundle(self, bundle):
+        import main
+        main.importBundle(bundle)
+
+    def getWebsocket(self, site):
+        import websocket
+
+        ws_address = "ws://%s:%s/Websocket?wrapper_key=%s" % (config.ui_ip, config.ui_port, site.settings["wrapper_key"])
+        logging.info("Connecting to %s" % ws_address)
+        ws = websocket.create_connection(ws_address)
+        return ws
+
+    def sitePublish(self, address, peer_ip=None, peer_port=15441, inner_path="content.json", recursive=False):
+        from Site import SiteManager
+        logging.info("Loading site...")
+        site = SiteManager.site_manager.get(address)
+        site.settings["serving"] = True  # Serving the site even if its disabled
+
+        if not recursive:
+            inner_paths = [inner_path]
+        else:
+            inner_paths = list(site.content_manager.contents.keys())
+
+        try:
+            ws = self.getWebsocket(site)
+
+        except Exception as err:
+            self.sitePublishFallback(site, peer_ip, peer_port, inner_paths, err)
+
+        else:
+            logging.info("Sending siteReload")
+            self.siteCmd(address, "siteReload", inner_path)
+
+            for inner_path in inner_paths:
+                logging.info(f"Sending sitePublish for {inner_path}")
+                self.siteCmd(address, "sitePublish", {"inner_path": inner_path, "sign": False})
+            logging.info("Done.")
+            ws.close()
+
+    def sitePublishFallback(self, site, peer_ip, peer_port, inner_paths, err):
+        if err is not None:
+            logging.info(f"Can't connect to local websocket client: {err}")
+        logging.info("Publish using fallback mechanism. "
+                     "Note that there might be not enough time for peer discovery, "
+                     "but you can specify target peer on command line.")
+        logging.info("Creating FileServer....")
+        file_server_thread = gevent.spawn(main.file_server.start, check_sites=False)  # Dont check every site integrity
+        time.sleep(0.001)
+
+        # Started fileserver
+        main.file_server.portCheck()
+        if peer_ip:  # Announce ip specificed
+            site.addPeer(peer_ip, peer_port)
+        else:  # Just ask the tracker
+            logging.info("Gathering peers from tracker")
+            site.announce()  # Gather peers
+
+        for inner_path in inner_paths:
+            published = site.publish(5, inner_path)  # Push to peers
+
+        if published > 0:
+            time.sleep(3)
+            logging.info("Serving files (max 60s)...")
+            gevent.joinall([file_server_thread], timeout=60)
+            logging.info("Done.")
+        else:
+            logging.info("No peers found, sitePublish command only works if you already have visitors serving your site")
+
+    # Crypto commands
+    def cryptPrivatekeyToAddress(self, privatekey=None):
+        from Crypt import CryptBitcoin
+        if not privatekey:  # If no privatekey in args then ask it now
+            import getpass
+            privatekey = getpass.getpass("Private key (input hidden):")
+
+        print(CryptBitcoin.privatekeyToAddress(privatekey))
+
+    def cryptSign(self, message, privatekey):
+        from Crypt import CryptBitcoin
+        print(CryptBitcoin.sign(message, privatekey))
+
+    def cryptVerify(self, message, sign, address):
+        from Crypt import CryptBitcoin
+        print(CryptBitcoin.verify(message, address, sign))
+
+    def cryptGetPrivatekey(self, master_seed, site_address_index=None):
+        from Crypt import CryptBitcoin
+        if len(master_seed) != 64:
+            logging.error("Error: Invalid master seed length: %s (required: 64)" % len(master_seed))
+            return False
+        privatekey = CryptBitcoin.hdPrivatekey(master_seed, site_address_index)
+        print("Requested private key: %s" % privatekey)
+
+    # Peer
+    def peerPing(self, peer_ip, peer_port=None):
+        if not peer_port:
+            peer_port = 15441
+        logging.info("Opening a simple connection server")
+        from Connection import ConnectionServer
+        main.file_server = ConnectionServer("127.0.0.1", 1234)
+        main.file_server.start(check_connections=False)
+        from Crypt import CryptConnection
+        CryptConnection.manager.loadCerts()
+
+        from Peer import Peer
+        logging.info("Pinging 5 times peer: %s:%s..." % (peer_ip, int(peer_port)))
+        s = time.time()
+        peer = Peer(peer_ip, peer_port)
+        peer.connect()
+
+        if not peer.connection:
+            print("Error: Can't connect to peer (connection error: %s)" % peer.connection_error)
+            return False
+        if "shared_ciphers" in dir(peer.connection.sock):
+            print("Shared ciphers:", peer.connection.sock.shared_ciphers())
+        if "cipher" in dir(peer.connection.sock):
+            print("Cipher:", peer.connection.sock.cipher()[0])
+        if "version" in dir(peer.connection.sock):
+            print("TLS version:", peer.connection.sock.version())
+        print("Connection time: %.3fs  (connection error: %s)" % (time.time() - s, peer.connection_error))
+
+        for i in range(5):
+            ping_delay = peer.ping()
+            print("Response time: %.3fs" % ping_delay)
+            time.sleep(1)
+        peer.remove()
+        print("Reconnect test...")
+        peer = Peer(peer_ip, peer_port)
+        for i in range(5):
+            ping_delay = peer.ping()
+            print("Response time: %.3fs" % ping_delay)
+            time.sleep(1)
+
+    def peerGetFile(self, peer_ip, peer_port, site, filename, benchmark=False):
+        logging.info("Opening a simple connection server")
+        from Connection import ConnectionServer
+        main.file_server = ConnectionServer("127.0.0.1", 1234)
+        main.file_server.start(check_connections=False)
+        from Crypt import CryptConnection
+        CryptConnection.manager.loadCerts()
+
+        from Peer import Peer
+        logging.info("Getting %s/%s from peer: %s:%s..." % (site, filename, peer_ip, peer_port))
+        peer = Peer(peer_ip, peer_port)
+        s = time.time()
+        if benchmark:
+            for i in range(10):
+                peer.getFile(site, filename),
+            print("Response time: %.3fs" % (time.time() - s))
+            input("Check memory")
+        else:
+            print(peer.getFile(site, filename).read())
+
+    def peerCmd(self, peer_ip, peer_port, cmd, parameters):
+        logging.info("Opening a simple connection server")
+        from Connection import ConnectionServer
+        main.file_server = ConnectionServer()
+        main.file_server.start(check_connections=False)
+        from Crypt import CryptConnection
+        CryptConnection.manager.loadCerts()
+
+        from Peer import Peer
+        peer = Peer(peer_ip, peer_port)
+
+        import json
+        if parameters:
+            parameters = json.loads(parameters.replace("'", '"'))
+        else:
+            parameters = {}
+        try:
+            res = peer.request(cmd, parameters)
+            print(json.dumps(res, indent=2, ensure_ascii=False))
+        except Exception as err:
+            print("Unknown response (%s): %s" % (err, res))
+
+    def getConfig(self):
+        import json
+        print(json.dumps(config.getServerInfo(), indent=2, ensure_ascii=False))
+
+    def test(self, test_name, *args, **kwargs):
+        import types
+        def funcToName(func_name):
+            test_name = func_name.replace("test", "")
+            return test_name[0].lower() + test_name[1:]
+
+        test_names = [funcToName(name) for name in dir(self) if name.startswith("test") and name != "test"]
+        if not test_name:
+            # No test specificed, list tests
+            print("\nNo test specified, possible tests:")
+            for test_name in test_names:
+                func_name = "test" + test_name[0].upper() + test_name[1:]
+                func = getattr(self, func_name)
+                if func.__doc__:
+                    print("- %s: %s" % (test_name, func.__doc__.strip()))
+                else:
+                    print("- %s" % test_name)
+            return None
+
+        # Run tests
+        func_name = "test" + test_name[0].upper() + test_name[1:]
+        if hasattr(self, func_name):
+            func = getattr(self, func_name)
+            print("- Running test: %s" % test_name, end="")
+            s = time.time()
+            ret = func(*args, **kwargs)
+            if type(ret) is types.GeneratorType:
+                for progress in ret:
+                    print(progress, end="")
+                    sys.stdout.flush()
+            print("\n* Test %s done in %.3fs" % (test_name, time.time() - s))
+        else:
+            print("Unknown test: %r (choose from: %s)" % (
+                test_name, test_names
+            ))
diff --git a/src/Config.py b/src/Config.py
index 7c13d4d7..57fb3a3c 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -96,7 +96,7 @@ class Config:
             print(f'  you have to run it with --start-dir "{home_zn}" option')
 
         if platform.system() == 'Linux':
-            # XDG!
+            # TODO: XDG!
             return os.path.expanduser('~/.local/zeronet-conservancy')
 
         if platform.system() == 'Darwin':
@@ -434,14 +434,11 @@ class Config:
 
         self.parseCommandline(argv, silent)  # Parse argv
         self.setAttributes()
-        print('Parsed command line once')
-        print(self.arguments)
         if parse_config:
             argv = self.parseConfig(argv)  # Add arguments from config file
 
         self.parseCommandline(argv, silent)  # Parse argv
         self.setAttributes()
-        print('Parsed command line twice')
 
         if not silent:
             if self.fileserver_ip != "*" and self.fileserver_ip not in self.ip_local:
@@ -454,7 +451,6 @@ class Config:
             current_parser.exit = original_exit
 
         self.loadTrackersFile()
-        print('Parse done')
 
     def fixArgs(self, args):
         "Fix old-style flags and issue a warning"
@@ -490,9 +486,7 @@ class Config:
             else:
                 self.arguments = {}
         else:
-            print('Parsing again')
             self.arguments = self.parser.parse_args(argv[1:])
-            print('Parsed thrice')
         if self.arguments.ui_site_port is None:
             self.arguments.ui_site_port = self.arguments.ui_port + 1
 
diff --git a/src/main.py b/src/main.py
index b899fbcc..b25e4a81 100644
--- a/src/main.py
+++ b/src/main.py
@@ -6,6 +6,8 @@ import logging
 from util.compat import *
 from pathlib import Path
 
+from rich import print
+
 startup_errors = []
 def startupError(msg):
     startup_errors.append(msg)
@@ -27,8 +29,6 @@ def load_config():
         # Config parse failed completely, show the help screen and exit
         config.parse()
 
-load_config()
-
 def importBundle(bundle):
     from zipfile import ZipFile
     from Crypt.CryptBitcoin import isValidAddress
@@ -99,617 +99,97 @@ def init_dirs():
         with open(users_json, "w") as f:
             f.write("{}")
 
-# TODO: GET RID OF TOP-LEVEL CODE!!!
-config.initConsoleLogger()
-
-try:
-    init_dirs()
-except:
-    import traceback as tb
-    print(tb.format_exc())
-    # at least make sure to print help if we're otherwise so helpless
-    config.parser.print_help()
-    sys.exit(1)
-
-if config.action == "main":
-    from util import helper
-    try:
-        lock = helper.openLocked(f"{config.data_dir}/lock.pid", "w")
-        lock.write(f"{os.getpid()}")
-    except BlockingIOError as err:
-        startupError(f"Can't open lock file, your 0net client is probably already running, exiting... ({err})")
-        proc = helper.openBrowser(config.open_browser)
-        r = proc.wait()
-        sys.exit(r)
-
-config.initLogging(console_logging=False)
-
-# Debug dependent configuration
-from Debug import DebugHook
-from Plugin import PluginManager
-
 def load_plugins():
+    from Plugin import PluginManager
     PluginManager.plugin_manager.loadPlugins()
     config.loadPlugins()
     config.parse()  # Parse again to add plugin configuration options
 
-load_plugins()
+def init():
+    load_config()
+    config.initConsoleLogger()
 
-# Log current config
-logging.debug("Config: %s" % config)
-
-# Modify stack size on special hardwares
-if config.stack_size:
-    import threading
-    threading.stack_size(config.stack_size)
-
-# Use pure-python implementation of msgpack to save CPU
-if config.msgpack_purepython:
-    os.environ["MSGPACK_PUREPYTHON"] = "True"
-
-# Fix console encoding on Windows
-# TODO: check if this is still required
-if sys.platform.startswith("win"):
-    import subprocess
     try:
-        chcp_res = subprocess.check_output("chcp 65001", shell=True).decode(errors="ignore").strip()
-        logging.debug("Changed console encoding to utf8: %s" % chcp_res)
-    except Exception as err:
-        logging.error("Error changing console encoding to utf8: %s" % err)
+        print(config.start_dir)
+        init_dirs()
+    except:
+        import traceback as tb
+        print(tb.format_exc())
+        # at least make sure to print help if we're otherwise so helpless
+        # config.parser.print_help()
+        sys.exit(1)
 
-# Socket monkey patch
-if config.proxy:
-    from util import SocksProxy
-    import urllib.request
-    logging.info("Patching sockets to socks proxy: %s" % config.proxy)
-    if config.fileserver_ip == "*":
-        config.fileserver_ip = '127.0.0.1'  # Do not accept connections anywhere but localhost
-    config.disable_udp = True  # UDP not supported currently with proxy
-    SocksProxy.monkeyPatch(*config.proxy.split(":"))
-elif config.tor == "always":
-    from util import SocksProxy
-    import urllib.request
-    logging.info("Patching sockets to tor socks proxy: %s" % config.tor_proxy)
-    if config.fileserver_ip == "*":
-        config.fileserver_ip = '127.0.0.1'  # Do not accept connections anywhere but localhost
-    SocksProxy.monkeyPatch(*config.tor_proxy_split())
-    config.disable_udp = True
-elif config.bind:
-    bind = config.bind
-    if ":" not in config.bind:
-        bind += ":0"
-    from util import helper
-    helper.socketBindMonkeyPatch(*bind.split(":"))
+    if config.action == "main":
+        from util import helper
+        try:
+            lock = helper.openLocked(f"{config.data_dir}/lock.pid", "w")
+            lock.write(f"{os.getpid()}")
+        except BlockingIOError as err:
+            startupError(f"Can't open lock file, your 0net client is probably already running, exiting... ({err})")
+            proc = helper.openBrowser(config.open_browser)
+            r = proc.wait()
+            sys.exit(r)
 
-# -- Actions --
+    config.initLogging(console_logging=False)
 
+    # Debug dependent configuration
+    from Debug import DebugHook
 
-@PluginManager.acceptPlugins
-class Actions:
-    def call(self, function_name, kwargs):
-        logging.info(f'zeronet-conservancy {config.version_full} on Python {sys.version} Gevent {gevent.__version__}')
+    load_plugins()
 
-        func = getattr(self, function_name, None)
-        back = func(**kwargs)
-        if back:
-            print(back)
-
-    def ipythonThread(self):
-        import IPython
-        IPython.embed()
-        self.gevent_quit.set()
-
-    # Default action: Start serving UiServer and FileServer
-    def main(self):
-        global ui_server, file_server
-        from File import FileServer
-        from Ui import UiServer
-        logging.info("Creating FileServer....")
-        file_server = FileServer()
-        logging.info("Creating UiServer....")
-        ui_server = UiServer()
-        file_server.ui_server = ui_server
-
-        for startup_error in startup_errors:
-            logging.error("Startup error: %s" % startup_error)
-
-        logging.info("Removing old SSL certs...")
-        from Crypt import CryptConnection
-        CryptConnection.manager.removeCerts()
-
-        logging.info("Starting servers....")
+    # Log current config
+    logging.debug("Config: %s" % config)
 
+    # Modify stack size on special hardwares
+    if config.stack_size:
         import threading
-        self.gevent_quit = threading.Event()
-        launched_greenlets = [gevent.spawn(ui_server.start), gevent.spawn(file_server.start), gevent.spawn(ui_server.startSiteServer)]
+        threading.stack_size(config.stack_size)
 
-        # if --repl, start ipython thread
-        # FIXME: Unfortunately this leads to exceptions on exit so use with care
-        if config.repl:
-            threading.Thread(target=self.ipythonThread).start()
+    # Use pure-python implementation of msgpack to save CPU
+    if config.msgpack_purepython:
+        os.environ["MSGPACK_PUREPYTHON"] = "True"
 
-        stopped = 0
-        # Process all greenlets in main thread
-        while not self.gevent_quit.is_set() and stopped < len(launched_greenlets):
-            stopped += len(gevent.joinall(launched_greenlets, timeout=1))
-
-        # Exited due to repl, so must kill greenlets
-        if stopped < len(launched_greenlets):
-            gevent.killall(launched_greenlets, exception=KeyboardInterrupt)
-
-        logging.info("All server stopped")
-
-    # Site commands
-
-    def siteCreate(self, use_master_seed=True):
-        logging.info("Generating new privatekey (use_master_seed: %s)..." % config.use_master_seed)
-        from Crypt import CryptBitcoin
-        if use_master_seed:
-            from User import UserManager
-            user = UserManager.user_manager.get()
-            if not user:
-                user = UserManager.user_manager.create()
-            address, address_index, site_data = user.getNewSiteData()
-            privatekey = site_data["privatekey"]
-            logging.info("Generated using master seed from users.json, site index: %s" % address_index)
-        else:
-            privatekey = CryptBitcoin.newPrivatekey()
-            address = CryptBitcoin.privatekeyToAddress(privatekey)
-        logging.info("----------------------------------------------------------------------")
-        logging.info("Site private key: %s" % privatekey)
-        logging.info("                  !!! ^ Save it now, required to modify the site ^ !!!")
-        logging.info("Site address:     %s" % address)
-        logging.info("----------------------------------------------------------------------")
-
-        while True and not config.batch and not use_master_seed:
-            if input("? Have you secured your private key? (yes, no) > ").lower() == "yes":
-                break
-            else:
-                logging.info("Please, secure it now, you going to need it to modify your site!")
-
-        logging.info("Creating directory structure...")
-        from Site.Site import Site
-        from Site import SiteManager
-        SiteManager.site_manager.load()
-
-        os.mkdir("%s/%s" % (config.data_dir, address))
-        open("%s/%s/index.html" % (config.data_dir, address), "w").write("Hello %s!" % address)
-
-        logging.info("Creating content.json...")
-        site = Site(address)
-        extend = {"postmessage_nonce_security": True}
-        if use_master_seed:
-            extend["address_index"] = address_index
-
-        site.content_manager.sign(privatekey=privatekey, extend=extend)
-        site.settings["own"] = True
-        site.saveSettings()
-
-        logging.info("Site created!")
-
-    def siteSign(self, address, privatekey=None, inner_path="content.json", publish=False, remove_missing_optional=False):
-        from Site.Site import Site
-        from Site import SiteManager
-        from Debug import Debug
-        SiteManager.site_manager.load()
-        logging.info("Signing site: %s..." % address)
-        site = Site(address, allow_create=False)
-
-        if not privatekey:  # If no privatekey defined
-            from User import UserManager
-            user = UserManager.user_manager.get()
-            if user:
-                site_data = user.getSiteData(address)
-                privatekey = site_data.get("privatekey")
-            else:
-                privatekey = None
-            if not privatekey:
-                # Not found in users.json, ask from console
-                import getpass
-                privatekey = getpass.getpass("Private key (input hidden):")
-        # inner_path can be either relative to site directory or absolute/relative path
-        if os.path.isabs(inner_path):
-            full_path = os.path.abspath(inner_path)
-        else:
-            full_path = os.path.abspath(config.working_dir + '/' + inner_path)
-        print(full_path)
-        if os.path.isfile(full_path):
-            if address in full_path:
-                # assuming site address is unique, keep only path after it
-                inner_path = full_path.split(address+'/')[1]
-            else:
-                # oops, file that we found seems to be rogue, so reverting to old behaviour
-                logging.warning(f'using {inner_path} relative to site directory')
+    # Fix console encoding on Windows
+    # TODO: check if this is still required
+    if sys.platform.startswith("win"):
+        import subprocess
         try:
-            succ = site.content_manager.sign(
-                inner_path=inner_path, privatekey=privatekey,
-                update_changed_files=True, remove_missing_optional=remove_missing_optional
-            )
+            chcp_res = subprocess.check_output("chcp 65001", shell=True).decode(errors="ignore").strip()
+            logging.debug("Changed console encoding to utf8: %s" % chcp_res)
         except Exception as err:
-            logging.error("Sign error: %s" % Debug.formatException(err))
-            succ = False
-        if succ and publish:
-            self.sitePublish(address, inner_path=inner_path)
+            logging.error("Error changing console encoding to utf8: %s" % err)
 
-    def siteVerify(self, address):
-        import time
-        from Site.Site import Site
-        from Site import SiteManager
-        SiteManager.site_manager.load()
+    # Socket monkey patch
+    if config.proxy:
+        from util import SocksProxy
+        import urllib.request
+        logging.info("Patching sockets to socks proxy: %s" % config.proxy)
+        if config.fileserver_ip == "*":
+            config.fileserver_ip = '127.0.0.1'  # Do not accept connections anywhere but localhost
+        config.disable_udp = True  # UDP not supported currently with proxy
+        SocksProxy.monkeyPatch(*config.proxy.split(":"))
+    elif config.tor == "always":
+        from util import SocksProxy
+        import urllib.request
+        logging.info("Patching sockets to tor socks proxy: %s" % config.tor_proxy)
+        if config.fileserver_ip == "*":
+            config.fileserver_ip = '127.0.0.1'  # Do not accept connections anywhere but localhost
+        SocksProxy.monkeyPatch(*config.tor_proxy_split())
+        config.disable_udp = True
+    elif config.bind:
+        bind = config.bind
+        if ":" not in config.bind:
+            bind += ":0"
+        from util import helper
+        helper.socketBindMonkeyPatch(*bind.split(":"))
 
-        s = time.time()
-        logging.info("Verifing site: %s..." % address)
-        site = Site(address)
-        bad_files = []
-
-        for content_inner_path in site.content_manager.contents:
-            s = time.time()
-            logging.info("Verifing %s signature..." % content_inner_path)
-            error = None
-            try:
-                file_correct = site.content_manager.verifyFile(
-                    content_inner_path, site.storage.open(content_inner_path, "rb"), ignore_same=False
-                )
-            except Exception as err:
-                file_correct = False
-                error = err
-
-            if file_correct is True:
-                logging.info("[OK] %s (Done in %.3fs)" % (content_inner_path, time.time() - s))
-            else:
-                logging.error("[ERROR] %s: invalid file: %s!" % (content_inner_path, error))
-                input("Continue?")
-                bad_files += content_inner_path
-
-        logging.info("Verifying site files...")
-        bad_files += site.storage.verifyFiles()["bad_files"]
-        if not bad_files:
-            logging.info("[OK] All file sha512sum matches! (%.3fs)" % (time.time() - s))
-        else:
-            logging.error("[ERROR] Error during verifying site files!")
-
-    def dbRebuild(self, address):
-        from Site.Site import Site
-        from Site import SiteManager
-        SiteManager.site_manager.load()
-
-        logging.info("Rebuilding site sql cache: %s..." % address)
-        site = SiteManager.site_manager.get(address)
-        s = time.time()
-        try:
-            site.storage.rebuildDb()
-            logging.info("Done in %.3fs" % (time.time() - s))
-        except Exception as err:
-            logging.error(err)
-
-    def dbQuery(self, address, query):
-        from Site.Site import Site
-        from Site import SiteManager
-        SiteManager.site_manager.load()
-
-        import json
-        site = Site(address)
-        result = []
-        for row in site.storage.query(query):
-            result.append(dict(row))
-        print(json.dumps(result, indent=4))
-
-    def siteAnnounce(self, address):
-        from Site.Site import Site
-        from Site import SiteManager
-        SiteManager.site_manager.load()
-
-        logging.info("Opening a simple connection server")
-        global file_server
-        from File import FileServer
-        file_server = FileServer("127.0.0.1", 1234)
-        file_server.start()
-
-        logging.info("Announcing site %s to tracker..." % address)
-        site = Site(address)
-
-        s = time.time()
-        site.announce()
-        print("Response time: %.3fs" % (time.time() - s))
-        print(site.peers)
-
-    def siteDownload(self, address):
-        from Site.Site import Site
-        from Site import SiteManager
-        SiteManager.site_manager.load()
-
-        logging.info("Opening a simple connection server")
-        global file_server
-        from File import FileServer
-        file_server = FileServer("127.0.0.1", 1234)
-        file_server_thread = gevent.spawn(file_server.start, check_sites=False)
-
-        site = Site(address)
-
-        on_completed = gevent.event.AsyncResult()
-
-        def onComplete(evt):
-            evt.set(True)
-
-        site.onComplete.once(lambda: onComplete(on_completed))
-        print("Announcing...")
-        site.announce()
-
-        s = time.time()
-        print("Downloading...")
-        site.downloadContent("content.json", check_modifications=True)
-
-        print("Downloaded in %.3fs" % (time.time()-s))
-
-    def siteNeedFile(self, address, inner_path):
-        from Site.Site import Site
-        from Site import SiteManager
-        SiteManager.site_manager.load()
-
-        def checker():
-            while 1:
-                s = time.time()
-                time.sleep(1)
-                print("Switch time:", time.time() - s)
-        gevent.spawn(checker)
-
-        logging.info("Opening a simple connection server")
-        global file_server
-        from File import FileServer
-        file_server = FileServer("127.0.0.1", 1234)
-        file_server_thread = gevent.spawn(file_server.start, check_sites=False)
-
-        site = Site(address)
-        site.announce()
-        print(site.needFile(inner_path, update=True))
-
-    def siteCmd(self, address, cmd, parameters):
-        import json
-        from Site import SiteManager
-
-        site = SiteManager.site_manager.get(address)
-
-        if not site:
-            logging.error("Site not found: %s" % address)
-            return None
-
-        ws = self.getWebsocket(site)
-
-        ws.send(json.dumps({"cmd": cmd, "params": parameters, "id": 1}))
-        res_raw = ws.recv()
-
-        try:
-            res = json.loads(res_raw)
-        except Exception as err:
-            return {"error": "Invalid result: %s" % err, "res_raw": res_raw}
-
-        if "result" in res:
-            return res["result"]
-        else:
-            return res
-
-    def importBundle(self, bundle):
-        importBundle(bundle)
-
-    def getWebsocket(self, site):
-        import websocket
-
-        ws_address = "ws://%s:%s/Websocket?wrapper_key=%s" % (config.ui_ip, config.ui_port, site.settings["wrapper_key"])
-        logging.info("Connecting to %s" % ws_address)
-        ws = websocket.create_connection(ws_address)
-        return ws
-
-    def sitePublish(self, address, peer_ip=None, peer_port=15441, inner_path="content.json", recursive=False):
-        from Site import SiteManager
-        logging.info("Loading site...")
-        site = SiteManager.site_manager.get(address)
-        site.settings["serving"] = True  # Serving the site even if its disabled
-
-        if not recursive:
-            inner_paths = [inner_path]
-        else:
-            inner_paths = list(site.content_manager.contents.keys())
-
-        try:
-            ws = self.getWebsocket(site)
-
-        except Exception as err:
-            self.sitePublishFallback(site, peer_ip, peer_port, inner_paths, err)
-
-        else:
-            logging.info("Sending siteReload")
-            self.siteCmd(address, "siteReload", inner_path)
-
-            for inner_path in inner_paths:
-                logging.info(f"Sending sitePublish for {inner_path}")
-                self.siteCmd(address, "sitePublish", {"inner_path": inner_path, "sign": False})
-            logging.info("Done.")
-            ws.close()
-
-    def sitePublishFallback(self, site, peer_ip, peer_port, inner_paths, err):
-        if err is not None:
-            logging.info(f"Can't connect to local websocket client: {err}")
-        logging.info("Publish using fallback mechanism. "
-                     "Note that there might be not enough time for peer discovery, "
-                     "but you can specify target peer on command line.")
-        logging.info("Creating FileServer....")
-        file_server_thread = gevent.spawn(file_server.start, check_sites=False)  # Dont check every site integrity
-        time.sleep(0.001)
-
-        # Started fileserver
-        file_server.portCheck()
-        if peer_ip:  # Announce ip specificed
-            site.addPeer(peer_ip, peer_port)
-        else:  # Just ask the tracker
-            logging.info("Gathering peers from tracker")
-            site.announce()  # Gather peers
-
-        for inner_path in inner_paths:
-            published = site.publish(5, inner_path)  # Push to peers
-
-        if published > 0:
-            time.sleep(3)
-            logging.info("Serving files (max 60s)...")
-            gevent.joinall([file_server_thread], timeout=60)
-            logging.info("Done.")
-        else:
-            logging.info("No peers found, sitePublish command only works if you already have visitors serving your site")
-
-    # Crypto commands
-    def cryptPrivatekeyToAddress(self, privatekey=None):
-        from Crypt import CryptBitcoin
-        if not privatekey:  # If no privatekey in args then ask it now
-            import getpass
-            privatekey = getpass.getpass("Private key (input hidden):")
-
-        print(CryptBitcoin.privatekeyToAddress(privatekey))
-
-    def cryptSign(self, message, privatekey):
-        from Crypt import CryptBitcoin
-        print(CryptBitcoin.sign(message, privatekey))
-
-    def cryptVerify(self, message, sign, address):
-        from Crypt import CryptBitcoin
-        print(CryptBitcoin.verify(message, address, sign))
-
-    def cryptGetPrivatekey(self, master_seed, site_address_index=None):
-        from Crypt import CryptBitcoin
-        if len(master_seed) != 64:
-            logging.error("Error: Invalid master seed length: %s (required: 64)" % len(master_seed))
-            return False
-        privatekey = CryptBitcoin.hdPrivatekey(master_seed, site_address_index)
-        print("Requested private key: %s" % privatekey)
-
-    # Peer
-    def peerPing(self, peer_ip, peer_port=None):
-        if not peer_port:
-            peer_port = 15441
-        logging.info("Opening a simple connection server")
-        global file_server
-        from Connection import ConnectionServer
-        file_server = ConnectionServer("127.0.0.1", 1234)
-        file_server.start(check_connections=False)
-        from Crypt import CryptConnection
-        CryptConnection.manager.loadCerts()
-
-        from Peer import Peer
-        logging.info("Pinging 5 times peer: %s:%s..." % (peer_ip, int(peer_port)))
-        s = time.time()
-        peer = Peer(peer_ip, peer_port)
-        peer.connect()
-
-        if not peer.connection:
-            print("Error: Can't connect to peer (connection error: %s)" % peer.connection_error)
-            return False
-        if "shared_ciphers" in dir(peer.connection.sock):
-            print("Shared ciphers:", peer.connection.sock.shared_ciphers())
-        if "cipher" in dir(peer.connection.sock):
-            print("Cipher:", peer.connection.sock.cipher()[0])
-        if "version" in dir(peer.connection.sock):
-            print("TLS version:", peer.connection.sock.version())
-        print("Connection time: %.3fs  (connection error: %s)" % (time.time() - s, peer.connection_error))
-
-        for i in range(5):
-            ping_delay = peer.ping()
-            print("Response time: %.3fs" % ping_delay)
-            time.sleep(1)
-        peer.remove()
-        print("Reconnect test...")
-        peer = Peer(peer_ip, peer_port)
-        for i in range(5):
-            ping_delay = peer.ping()
-            print("Response time: %.3fs" % ping_delay)
-            time.sleep(1)
-
-    def peerGetFile(self, peer_ip, peer_port, site, filename, benchmark=False):
-        logging.info("Opening a simple connection server")
-        global file_server
-        from Connection import ConnectionServer
-        file_server = ConnectionServer("127.0.0.1", 1234)
-        file_server.start(check_connections=False)
-        from Crypt import CryptConnection
-        CryptConnection.manager.loadCerts()
-
-        from Peer import Peer
-        logging.info("Getting %s/%s from peer: %s:%s..." % (site, filename, peer_ip, peer_port))
-        peer = Peer(peer_ip, peer_port)
-        s = time.time()
-        if benchmark:
-            for i in range(10):
-                peer.getFile(site, filename),
-            print("Response time: %.3fs" % (time.time() - s))
-            input("Check memory")
-        else:
-            print(peer.getFile(site, filename).read())
-
-    def peerCmd(self, peer_ip, peer_port, cmd, parameters):
-        logging.info("Opening a simple connection server")
-        global file_server
-        from Connection import ConnectionServer
-        file_server = ConnectionServer()
-        file_server.start(check_connections=False)
-        from Crypt import CryptConnection
-        CryptConnection.manager.loadCerts()
-
-        from Peer import Peer
-        peer = Peer(peer_ip, peer_port)
-
-        import json
-        if parameters:
-            parameters = json.loads(parameters.replace("'", '"'))
-        else:
-            parameters = {}
-        try:
-            res = peer.request(cmd, parameters)
-            print(json.dumps(res, indent=2, ensure_ascii=False))
-        except Exception as err:
-            print("Unknown response (%s): %s" % (err, res))
-
-    def getConfig(self):
-        import json
-        print(json.dumps(config.getServerInfo(), indent=2, ensure_ascii=False))
-
-    def test(self, test_name, *args, **kwargs):
-        import types
-        def funcToName(func_name):
-            test_name = func_name.replace("test", "")
-            return test_name[0].lower() + test_name[1:]
-
-        test_names = [funcToName(name) for name in dir(self) if name.startswith("test") and name != "test"]
-        if not test_name:
-            # No test specificed, list tests
-            print("\nNo test specified, possible tests:")
-            for test_name in test_names:
-                func_name = "test" + test_name[0].upper() + test_name[1:]
-                func = getattr(self, func_name)
-                if func.__doc__:
-                    print("- %s: %s" % (test_name, func.__doc__.strip()))
-                else:
-                    print("- %s" % test_name)
-            return None
-
-        # Run tests
-        func_name = "test" + test_name[0].upper() + test_name[1:]
-        if hasattr(self, func_name):
-            func = getattr(self, func_name)
-            print("- Running test: %s" % test_name, end="")
-            s = time.time()
-            ret = func(*args, **kwargs)
-            if type(ret) is types.GeneratorType:
-                for progress in ret:
-                    print(progress, end="")
-                    sys.stdout.flush()
-            print("\n* Test %s done in %.3fs" % (test_name, time.time() - s))
-        else:
-            print("Unknown test: %r (choose from: %s)" % (
-                test_name, test_names
-            ))
+init()
 
+from Actions import Actions
 
 actions = Actions()
+
 # Starts here when running zeronet.py
-
-
 def start():
     # Call function
     action_kwargs = config.getActionArguments()

From 3db5e108824285b715844cd3c438fcecf75d1b2b Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 4 Apr 2024 13:14:34 +0000
Subject: [PATCH 311/333] Better python compatibility detection

---
 zeronet.py | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/zeronet.py b/zeronet.py
index bb53404f..ff752fb6 100755
--- a/zeronet.py
+++ b/zeronet.py
@@ -6,11 +6,18 @@ from src.Config import config
 # fix further imports from src dir
 sys.modules['Config'] = sys.modules['src.Config']
 
+def pyReq():
+    major = sys.version_info.major
+    minor = sys.version_info.minor
+    if major < 3 or (major == 3 and minor < 8):
+        print("Error: Python 3.8+ is required")
+        sys.exit(0)
+    if major == 3 and minor < 11:
+        print(f"Python 3.11+ is recommended (you're running {sys.version})")
+
 def launch():
     '''renamed from main to avoid clashes with main module'''
-    if sys.version_info.major < 3:
-        print("Error: Python 3.x is required")
-        sys.exit(0)
+    pyReq()
 
     if '--silent' not in sys.argv:
         from greet import fancy_greet

From d09e1f8757f7c2dfaa34a4f105b3d6690e678c5d Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 4 Apr 2024 22:15:34 +0000
Subject: [PATCH 312/333] Build script

---
 .gitignore      |  3 +++
 build.py        | 53 +++++++++++++++++++++++++++++++++++++++++++++++++
 src/Config.py   |  6 ++++--
 src/util/Git.py |  2 +-
 4 files changed, 61 insertions(+), 3 deletions(-)
 create mode 100755 build.py

diff --git a/.gitignore b/.gitignore
index 0d03e87f..5a91a419 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,3 +45,6 @@ plugins/Multiuser
 plugins/NoNewSites
 plugins/StemPort
 plugins/UiPassword
+
+# Build files
+src/Build.py
diff --git a/build.py b/build.py
new file mode 100755
index 00000000..7b71daf1
--- /dev/null
+++ b/build.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+
+##  Copyright (c) 2024 caryoscelus
+##
+##  zeronet-conservancy is free software: you can redistribute it and/or modify it under the
+##  terms of the GNU General Public License as published by the Free Software
+##  Foundation, either version 3 of the License, or (at your option) any later version.
+##
+##  zeronet-conservancy is distributed in the hope that it will be useful, but
+##  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+##  FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+##  details.
+##
+## You should have received a copy of the GNU General Public License along with
+## zeronet-conservancy. If not, see <https://www.gnu.org/licenses/>.
+##
+
+"""Simple build/bundle script
+"""
+
+import argparse
+
+def write_to(args, target):
+    branch = args.branch
+    commit = args.commit
+    if branch is None or commit is None:
+        from src.util import Git
+        branch = branch or Git.branch() or 'unknown'
+        commit = commit or Git.commit() or 'unknown'
+    target.write('\n'.join([
+        f"build_type = {args.type!r}",
+        f"branch = {branch!r}",
+        f"commit = {commit!r}",
+        f"version = {args.version!r}",
+    ]))
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--type', default='source')
+    parser.add_argument('--version')
+    parser.add_argument('--branch')
+    parser.add_argument('--commit')
+    parser.add_argument('--stdout', action=argparse.BooleanOptionalAction, default=False)
+    args = parser.parse_args()
+    if args.stdout:
+        import sys
+        target = sys.stdout
+    else:
+        target = open('src/Build.py', 'w')
+    write_to(args, target)
+
+if __name__ == '__main__':
+    main()
diff --git a/src/Config.py b/src/Config.py
index 57fb3a3c..12603d40 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -10,22 +10,24 @@ import logging.handlers
 import stat
 import time
 
+VERSION = "0.7.10+"
+
 class Config:
 
     def __init__(self, argv):
         try:
             from . import Build
         except ImportError:
-            print('cannot find build')
             from .util import Git
             self.build_type = 'source'
             self.branch = Git.branch() or 'unknown'
             self.commit = Git.commit() or 'unknown'
+            self.version = VERSION
         else:
             self.build_type = Build.build_type
             self.branch = Build.branch
             self.commit = Build.commit
-        self.version = "0.7.10+"
+            self.version = Build.version or VERSION
         self.version_full = f'{self.version} ({self.build_type} from {self.branch}-{self.commit})'
         self.user_agent = "conservancy"
         # for compatibility
diff --git a/src/util/Git.py b/src/util/Git.py
index 7b60d396..ef633abe 100644
--- a/src/util/Git.py
+++ b/src/util/Git.py
@@ -48,7 +48,7 @@ def _gitted(f):
         return lambda *args, **kwargs: None
 
 @_gitted
-def commit() -> str:
+def commit() -> Optional[str]:
     """Returns git revision, possibly suffixed with -dirty"""
     dirty = '-dirty' if _repo.is_dirty() else ''
     return f'{_repo.head.commit}{dirty}'

From e3b010175f519280adf8f42f01532cd9df5021bd Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Thu, 4 Apr 2024 23:40:32 +0000
Subject: [PATCH 313/333] --ui-ip-protect option to only apply recent privacy
 protection where it makes sense (i.e. on localhost) by default

refs #263, #270
---
 src/Config.py       | 9 +++++++++
 src/Ui/UiRequest.py | 4 ++--
 2 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index 8a847bf3..ba519a6b 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -241,6 +241,7 @@ class Config:
         self.parser.add_argument('--log-rotate-backup-count', help='Log rotate backup count', default=5, type=int)
 
         self.parser.add_argument('--language', help='Web interface language', default=language, metavar='language')
+        self.parser.add_argument('--ui-ip-protect', help="Protect UI server from being accessed through third-party pages and on unauthorized cross-origin pages (enabled by default when serving on localhost IPs; doesn't work with non-local IPs, need testing with host names)", choices=['always', 'local', 'off'], default='local')
         self.parser.add_argument('--ui-ip', help='Web interface bind address', default="127.0.0.1", metavar='ip')
         self.parser.add_argument('--ui-port', help='Web interface bind port', default=43110, type=int, metavar='port')
         self.parser.add_argument('--ui-site-port', help='Port for serving site content, defaults to ui_port+1', default=None, metavar='port')
@@ -459,6 +460,14 @@ class Config:
             self.arguments = self.parser.parse_args(argv[1:])
         if self.arguments.ui_site_port is None:
             self.arguments.ui_site_port = self.arguments.ui_port + 1
+        if self.arguments.ui_ip_protect == 'always':
+            self.arguments.ui_check_cors = True
+        elif self.arguments.ui_ip_protect == 'off':
+            self.arguments.ui_check_cors = False
+        elif self.arguments.ui_ip_protect == 'local':
+            self.arguments.ui_check_cors = self.arguments.ui_ip == '127.0.0.1' or self.arguments.ui_ip == '::1'
+        else:
+            raise Exception("Wrong argparse result")
 
     def parseConfig(self, argv):
         argv = self.fixArgs(argv)
diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 482096a3..6e1d5e9e 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -148,7 +148,7 @@ class UiRequest:
             return False
 
         # Deny cross site requests
-        if not self.isSameOrigin(referer, url) and not self.hasCorsPermission(referer):
+        if not self.isSameOrigin(referer, url) or not self.hasCorsPermission(referer):
             return True
 
         return False
@@ -172,7 +172,7 @@ class UiRequest:
             protocol = self.env['wsgi.url_scheme']
             return self.actionRedirect(f'{protocol}://{host}:{config.ui_port}{path_info}?{query_string}')
 
-        if self.isCrossOriginRequest():
+        if config.ui_check_cors and self.isCrossOriginRequest():
             # we are still exposed by answering on port
             self.log.warning('Cross-origin request detected. Someone might be trying to analyze your 0net usage')
             return []

From 77720365590f5cfde50335c0c251d3a06012b8d5 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 5 Apr 2024 10:48:22 +0000
Subject: [PATCH 314/333] Fix some CORS/redirectering cases

---
 src/Ui/UiRequest.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 6e1d5e9e..70470dba 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -148,7 +148,7 @@ class UiRequest:
             return False
 
         # Deny cross site requests
-        if not self.isSameOrigin(referer, url) or not self.hasCorsPermission(referer):
+        if not self.isSameOrigin(referer, url) and not self.hasCorsPermission(referer):
             return True
 
         return False
@@ -165,7 +165,7 @@ class UiRequest:
         is_navigate = self.env.get('HTTP_SEC_FETCH_MODE') == 'navigate'
         is_iframe = self.env.get('HTTP_SEC_FETCH_DEST') == 'iframe'
 
-        if is_navigate and not is_iframe and self.is_data_request:
+        if ((is_navigate and not is_iframe) or not config.ui_check_cors) and self.is_data_request:
             host = self.getHostWithoutPort()
             path_info = self.env['PATH_INFO']
             query_string = self.env['QUERY_STRING']

From e8da744744da6eab0aaddb3bb8dd820b15ce4c79 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Mon, 8 Apr 2024 13:30:49 +0000
Subject: [PATCH 315/333] WIP

---
 src/User/User.py        | 5 +++--
 src/User/UserManager.py | 6 +++---
 src/main.py             | 9 ++++++++-
 3 files changed, 14 insertions(+), 6 deletions(-)

diff --git a/src/User/User.py b/src/User/User.py
index dbcfc56f..89571146 100644
--- a/src/User/User.py
+++ b/src/User/User.py
@@ -35,8 +35,9 @@ class User(object):
     # Save to data/users.json
     @util.Noparallel(queue=True, ignore_class=True)
     def save(self):
+        users_json = f'{config.private_dir}/users.json'
         s = time.time()
-        users = json.load(open("%s/users.json" % config.data_dir))
+        users = json.load(open(users_json))
         if self.master_address not in users:
             users[self.master_address] = {}  # Create if not exist
         user_data = users[self.master_address]
@@ -45,7 +46,7 @@ class User(object):
         user_data["sites"] = self.sites
         user_data["certs"] = self.certs
         user_data["settings"] = self.settings
-        helper.atomicWrite("%s/users.json" % config.data_dir, helper.jsonDumps(users).encode("utf8"))
+        helper.atomicWrite(users_json, helper.jsonDumps(users).encode("utf8"))
         self.log.debug("Saved in %.3fs" % (time.time() - s))
         self.delayed_save_thread = None
 
diff --git a/src/User/UserManager.py b/src/User/UserManager.py
index 067734a6..a2afc295 100644
--- a/src/User/UserManager.py
+++ b/src/User/UserManager.py
@@ -15,7 +15,7 @@ class UserManager(object):
         self.users = {}
         self.log = logging.getLogger("UserManager")
 
-    # Load all user from data/users.json
+    # Load all user from users.json
     def load(self):
         if not self.users:
             self.users = {}
@@ -25,7 +25,7 @@ class UserManager(object):
         s = time.time()
         # Load new users
         try:
-            json_path = "%s/users.json" % config.data_dir
+            json_path = f'{config.private_dir}/users.json'
             data = json.load(open(json_path))
         except Exception as err:
             raise Exception("Unable to load %s: %s" % (json_path, err))
@@ -57,7 +57,7 @@ class UserManager(object):
             user.saveDelayed()
         return user
 
-    # List all users from data/users.json
+    # List all users
     # Return: {"usermasteraddr": User}
     def list(self):
         if self.users == {}:  # Not loaded yet
diff --git a/src/main.py b/src/main.py
index b25e4a81..2fff005f 100644
--- a/src/main.py
+++ b/src/main.py
@@ -72,6 +72,13 @@ def init_dirs():
                       and not config.offline
                       and (not data_dir.is_dir() or not (data_dir / 'sites.json').is_file()))
 
+    old_users_json = data_dir / 'users.json'
+    if old_users_json.is_file():
+        print('Migrating existing users.json file to private/')
+    old_sites_json = data_dir / 'sites.json'
+    if old_sites_json.is_file():
+        print('Migrating existing sites.json file to private/')
+
     if not data_dir.is_dir():
         data_dir.mkdir(parents=True, exist_ok=True)
 
@@ -94,7 +101,7 @@ def init_dirs():
     if not os.path.isfile(sites_json):
         with open(sites_json, "w") as f:
             f.write("{}")
-    users_json = f"{data_dir}/users.json"
+    users_json = f"{private_dir_dir}/users.json"
     if not os.path.isfile(users_json):
         with open(users_json, "w") as f:
             f.write("{}")

From bdddf58712abfb03b8ff98abb37351f317ed1ce0 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 7 May 2024 14:03:44 +0000
Subject: [PATCH 316/333] WIP: change default data directories, subdirectories
 and config file

---
 CHANGELOG.md                                  |   5 +-
 build.py                                      |   2 +
 plugins/AnnounceShare/AnnounceSharePlugin.py  |   2 +-
 .../AnnounceShare/Test/TestAnnounceShare.py   |   2 +-
 plugins/Chart/ChartDb.py                      |   2 +-
 plugins/ContentFilter/ContentFilterStorage.py |  10 +-
 plugins/FilePack/FilePackPlugin.py            |   2 +-
 plugins/Sidebar/SidebarPlugin.py              |   2 +-
 .../disabled-Bootstrapper/BootstrapperDb.py   |   2 +-
 plugins/disabled-Multiuser/MultiuserPlugin.py |   2 +-
 .../disabled-Multiuser/Test/TestMultiuser.py  |   5 +-
 src/Actions.py                                |   4 +-
 src/Config.py                                 | 247 +++++++++++++-----
 src/Content/ContentDb.py                      |   2 +-
 src/Crypt/CryptConnection.py                  |  18 +-
 src/Debug/DebugReloader.py                    |   2 +-
 src/Plugin/PluginManager.py                   |   2 +-
 src/Site/Site.py                              |   5 +-
 src/Site/SiteManager.py                       |   6 +-
 src/Site/SiteStorage.py                       |   2 +-
 src/Ui/UiRequest.py                           |   2 +-
 src/User/User.py                              |   2 +-
 src/User/UserManager.py                       |   2 +-
 src/main.py                                   |  24 +-
 src/util/compat.py                            |   6 +
 src/util/helper.py                            |  22 +-
 zeronet.py                                    |   2 +-
 27 files changed, 263 insertions(+), 121 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 172f5f7b..4d9ec20c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
 ### zeronet-conservancy 0.7.10+
 - disable site-plugins installed for security reasons (@caryoscelus)
 - fix downloading geoip db (@caryoscelus)
-- python <3.6 is officially unsupported
+- python <3.6 is officially unsupported (3.8 is more likely minimum requirement)
 - SafeRe improvements by @geekless
 - remove and don't update muted files (@caryoscelus)
 - option to disable port checking (@caryoscelus)
@@ -10,6 +10,9 @@
 - fix chromium compatibility (@caryoscelus)
 - better fix of local sites leak (@caryoscelus)
 - ipython-based repl via --repl for debug/interactive development (@caryoscelus)
+- changes in directory structure (split data and config, use user directories by default)
+- use version information from git if available
+- different build types (portable vs package)
 - various improvements
 
 ### zeronet-conservancy 0.7.10 (2023-07-26) (18d35d3bed4f0683e99)
diff --git a/build.py b/build.py
index 7b71daf1..e142816a 100755
--- a/build.py
+++ b/build.py
@@ -32,6 +32,7 @@ def write_to(args, target):
         f"branch = {branch!r}",
         f"commit = {commit!r}",
         f"version = {args.version!r}",
+        f"platform = {args.platform!r}",
     ]))
 
 def main():
@@ -40,6 +41,7 @@ def main():
     parser.add_argument('--version')
     parser.add_argument('--branch')
     parser.add_argument('--commit')
+    parser.add_argument('--platform', default='source')
     parser.add_argument('--stdout', action=argparse.BooleanOptionalAction, default=False)
     args = parser.parse_args()
     if args.stdout:
diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py
index b350cf42..2a8a3891 100644
--- a/plugins/AnnounceShare/AnnounceSharePlugin.py
+++ b/plugins/AnnounceShare/AnnounceSharePlugin.py
@@ -14,7 +14,7 @@ from util import helper
 class TrackerStorage(object):
     def __init__(self):
         self.log = logging.getLogger("TrackerStorage")
-        self.file_path = "%s/trackers.json" % config.data_dir
+        self.file_path = config.start_dir / 'trackers.json'
         self.load()
         self.time_discover = 0.0
         atexit.register(self.save)
diff --git a/plugins/AnnounceShare/Test/TestAnnounceShare.py b/plugins/AnnounceShare/Test/TestAnnounceShare.py
index 7178eac8..5b820f9b 100644
--- a/plugins/AnnounceShare/Test/TestAnnounceShare.py
+++ b/plugins/AnnounceShare/Test/TestAnnounceShare.py
@@ -9,7 +9,7 @@ from Config import config
 @pytest.mark.usefixtures("resetTempSettings")
 class TestAnnounceShare:
     def testAnnounceList(self, file_server):
-        open("%s/trackers.json" % config.data_dir, "w").write("{}")
+        (config.start_dir / 'trackers.json').open('w').write('{}')
         tracker_storage = AnnounceSharePlugin.tracker_storage
         tracker_storage.load()
         peer = Peer(file_server.ip, 1544, connection_server=file_server)
diff --git a/plugins/Chart/ChartDb.py b/plugins/Chart/ChartDb.py
index 66a22082..3bb449e8 100644
--- a/plugins/Chart/ChartDb.py
+++ b/plugins/Chart/ChartDb.py
@@ -6,7 +6,7 @@ import time
 class ChartDb(Db):
     def __init__(self):
         self.version = 2
-        super(ChartDb, self).__init__(self.getSchema(), "%s/chart.db" % config.data_dir)
+        super(ChartDb, self).__init__(self.getSchema(), config.start_dir / 'chart.db')
         self.foreign_keys = True
         self.checkTables()
         self.sites = self.loadSites()
diff --git a/plugins/ContentFilter/ContentFilterStorage.py b/plugins/ContentFilter/ContentFilterStorage.py
index 2ad378d6..7d62e7e4 100644
--- a/plugins/ContentFilter/ContentFilterStorage.py
+++ b/plugins/ContentFilter/ContentFilterStorage.py
@@ -14,7 +14,7 @@ from util import helper
 class ContentFilterStorage(object):
     def __init__(self, site_manager):
         self.log = logging.getLogger("ContentFilterStorage")
-        self.file_path = "%s/filters.json" % config.data_dir
+        self.file_path = config.config_dir / 'filters.json'
         self.site_manager = site_manager
         self.file_content = self.load()
 
@@ -36,12 +36,12 @@ class ContentFilterStorage(object):
 
     def load(self):
         # Rename previously used mutes.json -> filters.json
-        if os.path.isfile("%s/mutes.json" % config.data_dir):
+        if (config.config_dir / 'mutes.json').is_file():
             self.log.info("Renaming mutes.json to filters.json...")
-            os.rename("%s/mutes.json" % config.data_dir, self.file_path)
-        if os.path.isfile(self.file_path):
+            os.rename(config.config_dir / 'mutes.json', self.file_path)
+        if self.file_path.is_file():
             try:
-                return json.load(open(self.file_path))
+                return json.load(self.file_path.open())
             except Exception as err:
                 self.log.error("Error loading filters.json: %s" % err)
                 return None
diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py
index 1c931316..488ff1a0 100644
--- a/plugins/FilePack/FilePackPlugin.py
+++ b/plugins/FilePack/FilePackPlugin.py
@@ -44,7 +44,7 @@ class UiRequestPlugin(object):
         if ".zip/" in path or ".tar.gz/" in path:
             file_obj = None
             path_parts = self.parsePath(path)
-            file_path = "%s/%s/%s" % (config.data_dir, path_parts["address"], path_parts["inner_path"])
+            file_path = config.data_dir / path_parts["address"] / path_parts["inner_path"]
             match = re.match(r"^(.*\.(?:tar.gz|zip))/(.*)", file_path)
             archive_path, path_within = match.groups()
             if archive_path not in archive_cache:
diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index b8c5f0f3..c24a0e0b 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -686,7 +686,7 @@ class UiWebsocketPlugin(object):
         if sys.platform == "linux":
             sys_db_paths += ['/usr/share/GeoIP/' + db_name]
 
-        data_dir_db_path = os.path.join(config.data_dir, db_name)
+        data_dir_db_path = config.data_dir / db_name
 
         db_paths = sys_db_paths + [data_dir_db_path]
 
diff --git a/plugins/disabled-Bootstrapper/BootstrapperDb.py b/plugins/disabled-Bootstrapper/BootstrapperDb.py
index 0866dc3e..355fac8c 100644
--- a/plugins/disabled-Bootstrapper/BootstrapperDb.py
+++ b/plugins/disabled-Bootstrapper/BootstrapperDb.py
@@ -12,7 +12,7 @@ class BootstrapperDb(Db.Db):
     def __init__(self):
         self.version = 7
         self.hash_ids = {}  # hash -> id cache
-        super(BootstrapperDb, self).__init__({"db_name": "Bootstrapper"}, "%s/bootstrapper.db" % config.data_dir)
+        super(BootstrapperDb, self).__init__({"db_name": "Bootstrapper"}, config.start_dir / 'bootstrapper.db')
         self.foreign_keys = True
         self.checkTables()
         self.updateHashCache()
diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py
index a2fd79ae..342307bf 100644
--- a/plugins/disabled-Multiuser/MultiuserPlugin.py
+++ b/plugins/disabled-Multiuser/MultiuserPlugin.py
@@ -16,7 +16,7 @@ def importPluginnedClasses():
     from User import UserManager
 
 try:
-    local_master_addresses = set(json.load(open("%s/users.json" % config.data_dir)).keys())  # Users in users.json
+    local_master_addresses = set(json.load((config.private_dir / 'users.json').open()).keys())  # Users in users.json
 except Exception as err:
     local_master_addresses = set()
 
diff --git a/plugins/disabled-Multiuser/Test/TestMultiuser.py b/plugins/disabled-Multiuser/Test/TestMultiuser.py
index b8ff4267..fe03833d 100644
--- a/plugins/disabled-Multiuser/Test/TestMultiuser.py
+++ b/plugins/disabled-Multiuser/Test/TestMultiuser.py
@@ -8,7 +8,8 @@ from User import UserManager
 class TestMultiuser:
     def testMemorySave(self, user):
         # It should not write users to disk
-        users_before = open("%s/users.json" % config.data_dir).read()
+        users_json = config.private_dir / 'users.json'
+        users_before = users_json.open().read()
         user = UserManager.user_manager.create()
         user.save()
-        assert open("%s/users.json" % config.data_dir).read() == users_before
+        assert users_json.open().read() == users_before
diff --git a/src/Actions.py b/src/Actions.py
index 7a1fe7d9..72c8b063 100644
--- a/src/Actions.py
+++ b/src/Actions.py
@@ -92,8 +92,8 @@ class Actions:
         from Site import SiteManager
         SiteManager.site_manager.load()
 
-        os.mkdir("%s/%s" % (config.data_dir, address))
-        open("%s/%s/index.html" % (config.data_dir, address), "w").write("Hello %s!" % address)
+        (config.data_dir / address).mkdir()
+        (config.data_dir / address / 'index.html').open('w').write(f"Hello {address}!")
 
         logging.info("Creating content.json...")
         site = Site(address)
diff --git a/src/Config.py b/src/Config.py
index 12603d40..2895f5fd 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -9,10 +9,18 @@ import logging
 import logging.handlers
 import stat
 import time
+from pathlib import Path
 
 VERSION = "0.7.10+"
 
+class StartupError(RuntimeError):
+    pass
+
 class Config:
+    """Class responsible for storing and loading config.
+
+    Used as singleton `config`
+    """
 
     def __init__(self, argv):
         try:
@@ -23,11 +31,13 @@ class Config:
             self.branch = Git.branch() or 'unknown'
             self.commit = Git.commit() or 'unknown'
             self.version = VERSION
+            self.platform = 'source'
         else:
             self.build_type = Build.build_type
             self.branch = Build.branch
             self.commit = Build.commit
             self.version = Build.version or VERSION
+            self.platform = Build.platform
         self.version_full = f'{self.version} ({self.build_type} from {self.branch}-{self.commit})'
         self.user_agent = "conservancy"
         # for compatibility
@@ -45,15 +55,18 @@ class Config:
         self.keys_restart_need = set([
             "tor", "fileserver_port", "fileserver_ip_type", "threads_fs_read", "threads_fs_write", "threads_crypt", "threads_db"
         ])
-        self.start_dir = self.getStartDir()
-        self.config_file = self.start_dir + "/zeronet.conf"
-        self.private_dir = self.start_dir + '/private'
-        self.data_dir = self.start_dir + "/data"
-        self.log_dir = self.start_dir + "/log"
+
+        self.config_file = None
+        self.config_dir = None
+        self.data_dir = None
+        self.private_dir = None
+        self.log_dir = None
+        self.configurePaths(argv)
+
         self.openssl_lib_file = None
         self.openssl_bin_file = None
 
-        self.trackers_file = False
+        self.trackers_file = None
         self.createParser()
         self.createArguments()
 
@@ -70,46 +83,21 @@ class Config:
     def strToBool(self, v):
         return v.lower() in ("yes", "true", "t", "1")
 
-    def getStartDir(self):
-        """Return directory with config & data"""
+    def getStartDirOld(self):
+        """Get directory that would have been used by older versions (pre v0.7.11)"""
+        this_file = os.path.abspath(__file__).replace("\\", "/").rstrip("cd")
+
         if "--start-dir" in self.argv:
-            return self.argv[self.argv.index("--start-dir") + 1]
-
-        if '--portable' in self.argv or self.build_type == 'portable':
-            return '.'
-
-        here = os.path.dirname(os.path.abspath(__file__).replace("\\", "/")).rstrip('/src')
-        if os.path.isdir(f'{here}/data') and not '--no-portable' in self.argv:
-            print('WARNING: found data in current directory')
-            print('  It used to be default behaviour to store data alongside project directory,')
-            print('  but now we default to place data and config in user home directory.')
-            print('  If you want to keep previous behaviour, please use --portable')
-            print('Assuming implicit --portable (use --no-portable to override)')
-            print(self.argv)
-            self.argv.insert(1, '--portable')
-            print(self.argv)
-            return '.'
-
-        home_zn = os.path.expanduser(f'~/ZeroNet')
-        if os.path.isdir(home_zn):
-            print(f'WARNING: found data in {home_zn}')
-            print( '  It is possible that this is from previous version or another installation')
-            print( '  altogether. If you want to use that data directory with zeronet-conservancy')
-            print(f'  you have to run it with --start-dir "{home_zn}" option')
-
-        if platform.system() == 'Linux':
-            # TODO: XDG!
-            return os.path.expanduser('~/.local/zeronet-conservancy')
-
-        if platform.system() == 'Darwin':
-            return os.path.expanduser("~/Library/Application Support/zeronet-conservancy")
-
-        if platform.system() == 'Windows':
-            return os.path.expanduser('~/AppData/zeronet-conservancy')
-            
-        elif here.endswith("/Contents/Resources/core/src"):
-            start_dir = os.path.expanduser("~/Library/Application Support/ZeroNet")
-        elif this_file.endswith("/core/src"):
+            start_dir = self.argv[self.argv.index("--start-dir") + 1]
+        elif this_file.endswith("/Contents/Resources/core/src/Config.py"):
+            # Running as ZeroNet.app
+            if this_file.startswith("/Application") or this_file.startswith("/private") or this_file.startswith(os.path.expanduser("~/Library")):
+                # Runnig from non-writeable directory, put data to Application Support
+                start_dir = os.path.expanduser("~/Library/Application Support/ZeroNet")
+            else:
+                # Running from writeable directory put data next to .app
+                start_dir = re.sub("/[^/]+/Contents/Resources/core/src/Config.py", "", this_file)
+        elif this_file.endswith("/core/src/Config.py"):
             # Running as exe or source is at Application Support directory, put var files to outside of core dir
             start_dir = this_file.replace("/core/src/Config.py", "")
         elif not os.access(this_file.replace('/src/Config.py', ''), os.R_OK | os.W_OK):
@@ -117,6 +105,144 @@ class Config:
             start_dir = os.path.expanduser("~/ZeroNet")
         else:
             start_dir = "."
+        return start_dir
+
+    def migrateOld(self, source):
+        print(f'[bold red]WARNING: found data {source}[/bold red]')
+        print( '  It used to be default behaviour to store data there,')
+        print( '  but now we default to place data and config in user home directory.')
+        print( '')
+
+    def configurePaths(self, argv):
+        if '--config-file' in argv:
+            self.config_file = argv[argv.index('--config-file') + 1]
+        old_dir = Path(self.getStartDirOld())
+        new_dir = Path(self.getStartDir())
+        no_migrate = '--no-migrate' in argv
+        silent_migrate = '--portable' in argv or '--migrate' in argv
+        try:
+            self.start_dir = self.maybeMigrate(old_dir, new_dir, no_migrate, silent_migrate)
+        except Exception as ex:
+            raise ex
+
+        self.updatePaths()
+
+    def updatePaths(self):
+        if self.config_file is None:
+            self.config_file = self.start_dir / 'znc.conf'
+        if self.config_dir is None:
+            self.config_dir = self.start_dir
+        if self.private_dir is None:
+            self.private_dir = self.start_dir / 'private'
+        if self.data_dir is None:
+            self.data_dir = self.start_dir / 'data'
+        if self.log_dir is None:
+            self.log_dir = self.start_dir / 'log'
+
+    def createPaths(self):
+        self.start_dir.mkdir(parents=True, exist_ok=True)
+        self.private_dir.mkdir(parents=True, exist_ok=True)
+        self.data_dir.mkdir(parents=True, exist_ok=True)
+        self.log_dir.mkdir(parents=True, exist_ok=True)
+
+    def checkDir(self, root):
+        return (root / 'znc.conf').is_file()
+
+    def doMigrate(self, old_dir, new_dir):
+        raise RuntimeError('Migration not implemented yet')
+
+    def askMigrate(self, old_dir, new_dir, silent):
+        if not sys.stdin.isatty():
+            raise StartupError('Migration refused: non-interactive shell')
+        while True:
+            r = input(f'You have old data in `{old_dir}`. Migrate to new format to `{new_dir}`? [Y/n]')
+            if r.lower().startswith('n'):
+                raise StartupError('Migration refused')
+            if r.lower().startswith('y'):
+                return self.doMigrate(old_dir, new_dir)
+
+    def createNewConfig(self, new_dir):
+        new_dir.mkdir(parents=True, exist_ok=True)
+        with (new_dir / 'znc.conf').open('w') as f:
+            f.write('# zeronet-conervancy config file')
+
+    def maybeMigrate(self, old_dir, new_dir, no_migrate, silent_migrate):
+        if old_dir.exists() and new_dir.exists():
+            if old_dir == new_dir:
+                if self.checkDir(new_dir):
+                    return new_dir
+                elif no_migrate:
+                    return StartError('Migration refused, but new directory should be migrated')
+                else:
+                    return askMigrate(old_dir, new_dir, silent_migrate)
+            else:
+                if self.checkDir(new_dir):
+                    if not no_migrate:
+                        print("There's an old starting directory")
+                    return new_dir
+                else:
+                    raise StartupError('Bad startup directory')
+        elif old_dir.exists():
+            if no_migrate:
+                self.createNewConfig(new_dir)
+                return new_dir
+            else:
+                return self.askMigrate(old_dir, new_dir, silent_migrate)
+        elif new_dir.exists():
+            if self.checkDir(new_dir):
+                return new_dir
+            else:
+                return StartupError('Bad startup directory')
+        else:
+            self.createNewConfig(new_dir)
+            return new_dir
+
+    def getStartDir(self):
+        """Return directory with config & data"""
+        if "--start-dir" in self.argv:
+            return self.argv[self.argv.index("--start-dir") + 1]
+
+        here = os.path.dirname(os.path.abspath(__file__).replace("\\", "/")).rstrip('/src')
+        if '--portable' in self.argv or self.build_type == 'portable':
+            return here
+
+        # if os.path.isdir(f'{here}/data') and not '--no-portable' in self.argv:
+            # print('WARNING: found data in current directory')
+            # print('  It used to be default behaviour to store data alongside project directory,')
+            # print('  but now we default to place data and config in user home directory.')
+            # print('  If you want to keep previous behaviour, please use --portable')
+            # print('Assuming implicit --portable (use --no-portable to override)')
+            # print(self.argv)
+            # self.argv.insert(1, '--portable')
+            # print(self.argv)
+            # return here
+
+        # home_zn = os.path.expanduser(f'~/ZeroNet')
+        # if os.path.isdir(home_zn):
+            # print(f'WARNING: found data in {home_zn}')
+            # print( '  It is possible that this is from previous version or another installation')
+            # print( '  altogether. If you want to use that data directory with zeronet-conservancy')
+            # print(f'  you have to run it with --start-dir "{home_zn}" option')
+
+        MACOSX_DIR = '~/Library/Application Support/zeronet-conservancy'
+        WINDOWS_DIR = '~/AppData/zeronet-conservancy'
+        LIBREDESKTOP_DIR = '~/.local/share/zeronet-conservancy'
+        if self.platform == 'source':
+            if platform.system() == 'Darwin':
+                path = MACOSX_DIR
+            elif platform.system() == 'Windows':
+                path = WINDOWS_DIR
+            else:
+                path = LIBREDESKTOP_DIR
+        elif self.platform == 'macosx':
+            path = MACOSX_DIR
+        elif self.platform == 'windows':
+            path = WINDOWS_DIR
+        elif self.platform == 'libredesktop':
+            path = LIBREDESKTOP_DIR
+        else:
+            raise RuntimeError(f'UNKNOWN PLATFORM: {self.platform}. Something must have went terribly wrong!')
+        return os.path.expanduser(path)
 
     # Create command line arguments
     def createArguments(self):
@@ -135,9 +261,9 @@ class Config:
         else:
             fix_float_decimals = False
 
-        config_file = self.start_dir + "/zeronet.conf"
-        data_dir = self.start_dir + "/data"
-        log_dir = self.start_dir + "/log"
+        config_file = self.config_file
+        data_dir = self.data_dir
+        log_dir = self.log_dir
 
         ip_local = ["127.0.0.1", "::1"]
 
@@ -256,9 +382,10 @@ class Config:
         self.parser.add_argument('--batch', help="Batch mode (No interactive input for commands)", action='store_true')
 
         self.parser.add_argument('--portable', action=argparse.BooleanOptionalAction)
-        self.parser.add_argument('--start-dir', help='Path of working dir for variable content (data, log, .conf)', default=self.start_dir, metavar="path")
+        self.parser.add_argument('--start-dir', help='Path of working dir for variable content (data, log, config)', default=self.start_dir, metavar="path")
         self.parser.add_argument('--config-file', help='Path of config file', default=config_file, metavar="path")
         self.parser.add_argument('--data-dir', help='Path of data directory', default=data_dir, metavar="path")
+        self.parser.add_argument('--no-migrate', help='Ignore data directories from old 0net versions', action=argparse.BooleanOptionalAction, default=False)
 
         self.parser.add_argument('--console-log-level', help='Level of logging to console', default="default", choices=["default", "DEBUG", "INFO", "ERROR", "off"])
 
@@ -352,7 +479,7 @@ class Config:
         return self.parser
 
     def loadTrackersFile(self):
-        if not self.trackers_file:
+        if self.trackers_file is None:
             return None
 
         self.trackers = self.arguments.trackers[:]
@@ -362,16 +489,19 @@ class Config:
                 if trackers_file.startswith("/"):  # Absolute
                     trackers_file_path = trackers_file
                 elif trackers_file.startswith("{data_dir}"):  # Relative to data_dir
-                    trackers_file_path = trackers_file.replace("{data_dir}", self.data_dir)
-                else:  # Relative to zeronet.py
-                    trackers_file_path = self.start_dir + "/" + trackers_file
+                    trackers_file_path = trackers_file.replace('{data_dir}', str(self.data_dir))
+                else:
+                    # Relative to zeronet.py or something else, unsupported
+                    raise RuntimeError(f'trackers_file should be relative to {{data_dir}} or absolute path (not {trackers_file})')
 
                 for line in open(trackers_file_path):
                     tracker = line.strip()
                     if "://" in tracker and tracker not in self.trackers:
                         self.trackers.append(tracker)
             except Exception as err:
-                print("Error loading trackers file: %s" % err)
+                print(self.trackers_file)
+                print(trackers_file)
+                print(f'Error loading trackers file: {err}')
 
     # Find arguments specified for current action
     def getActionArguments(self):
@@ -436,6 +566,8 @@ class Config:
 
         self.parseCommandline(argv, silent)  # Parse argv
         self.setAttributes()
+        self.updatePaths()
+        self.createPaths()
         if parse_config:
             argv = self.parseConfig(argv)  # Add arguments from config file
 
@@ -460,7 +592,7 @@ class Config:
         for arg in args:
             if arg.startswith('--') and '_' in arg:
                 farg = arg.replace('_', '-')
-                print(f'WARNING: using deprecated flag in command line: {arg} should be {farg}')
+                print(f'[bold red]WARNING: using deprecated flag in command line: {arg} should be {farg}[/bold red]')
                 print('Support for deprecated flags might be removed in the future')
             else:
                 farg = arg
@@ -494,9 +626,6 @@ class Config:
 
     def parseConfig(self, argv):
         argv = self.fixArgs(argv)
-        # Find config file path from parameters
-        if "--config-file" in argv:
-            self.config_file = argv[argv.index("--config-file") + 1]
         # Load config file
         if os.path.isfile(self.config_file):
             config = configparser.RawConfigParser(allow_no_value=True, strict=False)
@@ -539,7 +668,7 @@ class Config:
                     val = val[:]
                 if key in ("data_dir", "log_dir", "start_dir", "openssl_bin_file", "openssl_lib_file"):
                     if val:
-                        val = val.replace("\\", "/")
+                        val = Path(val)
                 setattr(self, key, val)
 
     def loadPlugins(self):
diff --git a/src/Content/ContentDb.py b/src/Content/ContentDb.py
index f284581e..5b63993d 100644
--- a/src/Content/ContentDb.py
+++ b/src/Content/ContentDb.py
@@ -153,7 +153,7 @@ content_dbs = {}
 
 def getContentDb(path=None):
     if not path:
-        path = "%s/content.db" % config.data_dir
+        path = config.data_dir / 'content.db'
     if path not in content_dbs:
         content_dbs[path] = ContentDb(path)
         content_dbs[path].init()
diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py
index ebbc6295..c7f3ea5b 100644
--- a/src/Crypt/CryptConnection.py
+++ b/src/Crypt/CryptConnection.py
@@ -24,20 +24,20 @@ class CryptConnectionManager:
         self.context_server = None
 
         self.openssl_conf_template = "src/lib/openssl/openssl.cnf"
-        self.openssl_conf = config.data_dir + "/openssl.cnf"
+        self.openssl_conf = config.private_dir / "openssl.cnf"
 
         self.openssl_env = {
             "OPENSSL_CONF": self.openssl_conf,
-            "RANDFILE": config.data_dir + "/openssl-rand.tmp"
+            "RANDFILE": config.private_dir / "openssl-rand.tmp"
         }
 
         self.crypt_supported = []  # Supported cryptos
 
-        self.cacert_pem = config.data_dir + "/cacert-rsa.pem"
-        self.cakey_pem = config.data_dir + "/cakey-rsa.pem"
-        self.cert_pem = config.data_dir + "/cert-rsa.pem"
-        self.cert_csr = config.data_dir + "/cert-rsa.csr"
-        self.key_pem = config.data_dir + "/key-rsa.pem"
+        self.cacert_pem = config.private_dir / "cacert-rsa.pem"
+        self.cakey_pem = config.private_dir / "cakey-rsa.pem"
+        self.cert_pem = config.private_dir / "cert-rsa.pem"
+        self.cert_csr = config.private_dir / "cert-rsa.csr"
+        self.key_pem = config.private_dir / "key-rsa.pem"
 
         self.log = logging.getLogger("CryptConnectionManager")
         self.log.debug("Version: %s" % ssl.OPENSSL_VERSION)
@@ -105,8 +105,8 @@ class CryptConnectionManager:
         if config.keep_ssl_cert:
             return False
         for file_name in ["cert-rsa.pem", "key-rsa.pem", "cacert-rsa.pem", "cakey-rsa.pem", "cacert-rsa.srl", "cert-rsa.csr", "openssl-rand.tmp"]:
-            file_path = "%s/%s" % (config.data_dir, file_name)
-            if os.path.isfile(file_path):
+            file_path = config.data_dir / file_name
+            if file_path.is_file():
                 os.unlink(file_path)
 
     # Load and create cert files is necessary
diff --git a/src/Debug/DebugReloader.py b/src/Debug/DebugReloader.py
index 482c7921..be9b4d8c 100644
--- a/src/Debug/DebugReloader.py
+++ b/src/Debug/DebugReloader.py
@@ -21,7 +21,7 @@ else:
 class DebugReloader:
     def __init__(self, paths=None):
         if not paths:
-            paths = ["src", "plugins", config.data_dir + "/__plugins__"]
+            paths = ["src", "plugins"]
         self.log = logging.getLogger("DebugReloader")
         self.last_chaged = 0
         self.callbacks = []
diff --git a/src/Plugin/PluginManager.py b/src/Plugin/PluginManager.py
index ab0940e8..82db4cfd 100644
--- a/src/Plugin/PluginManager.py
+++ b/src/Plugin/PluginManager.py
@@ -25,7 +25,7 @@ class PluginManager:
         self.after_load = []   # Execute functions after loaded plugins
         self.function_flags = {}  # Flag function for permissions
         self.reloading = False
-        self.config_path = config.data_dir + "/plugins.json"
+        self.config_path = config.config_dir / 'plugins.json'
         self.loadConfig()
 
         self.config.setdefault("builtin", {})
diff --git a/src/Site/Site.py b/src/Site/Site.py
index ad0e3ca2..becc7d3e 100644
--- a/src/Site/Site.py
+++ b/src/Site/Site.py
@@ -88,9 +88,10 @@ class Site(object):
     def loadSettings(self, settings=None):
         if not settings:
             try:
-                settings = json.load(open(f'{config.data_dir}/sites.json')).get(self.address)
+                with (config.private_dir / 'sites.json').open() as f:
+                    settings = json.load(f).get(self.address)
             except Exception as err:
-                logging.error(f'Error loading {config.data_dir}/sites.json: {err}')
+                logging.error(f'Error loading {config.private_dir}/sites.json: {err}')
                 settings = {}
         if settings:
             self.settings = settings
diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py
index 78c20f86..0fb3b060 100644
--- a/src/Site/SiteManager.py
+++ b/src/Site/SiteManager.py
@@ -38,7 +38,7 @@ class SiteManager(object):
         load_s = time.time()
         # Load new adresses
         try:
-            json_path = f"{config.data_dir}/sites.json"
+            json_path = config.private_dir / 'sites.json'
             data = json.load(open(json_path))
         except Exception as err:
             self.log.error(f"Unable to load {json_path}: {err}")
@@ -48,7 +48,7 @@ class SiteManager(object):
 
         for address, settings in data.items():
             if address not in self.sites:
-                if os.path.isfile("%s/%s/content.json" % (config.data_dir, address)):
+                if (config.data_dir / address / 'content.json').is_file():
                     # Root content.json exists, try load site
                     s = time.time()
                     try:
@@ -121,7 +121,7 @@ class SiteManager(object):
 
         s = time.time()
         if data:
-            helper.atomicWrite("%s/sites.json" % config.data_dir, helper.jsonDumps(data).encode("utf8"))
+            helper.atomicWrite(config.private_dir / 'sites.json', helper.jsonDumps(data).encode("utf8"))
         else:
             self.log.debug("Save error: No data")
         time_write = time.time() - s
diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py
index be8f88e9..7249ad34 100644
--- a/src/Site/SiteStorage.py
+++ b/src/Site/SiteStorage.py
@@ -29,7 +29,7 @@ thread_pool_fs_batch = ThreadPool.ThreadPool(1, name="FS batch")
 class SiteStorage(object):
     def __init__(self, site, allow_create=True):
         self.site = site
-        self.directory = f'{config.data_dir}/{self.site.address}'  # Site data diretory
+        self.directory = config.data_dir / self.site.address  # Site data diretory
         self.allowed_dir = os.path.abspath(self.directory)  # Only serve file within this dir
         self.log = site.log
         self.db = None  # Db class
diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py
index 34af96e2..967508da 100644
--- a/src/Ui/UiRequest.py
+++ b/src/Ui/UiRequest.py
@@ -784,7 +784,7 @@ class UiRequest:
 
         address = path_parts["address"]
 
-        file_path = "%s/%s/%s" % (config.data_dir, address, path_parts["inner_path"])
+        file_path = config.data_dir / address / path_parts['inner_path']
 
         if (config.debug or config.merge_media) and file_path.split("/")[-1].startswith("all."):
             # If debugging merge *.css to all.css and *.js to all.js
diff --git a/src/User/User.py b/src/User/User.py
index 89571146..bbf18f07 100644
--- a/src/User/User.py
+++ b/src/User/User.py
@@ -35,7 +35,7 @@ class User(object):
     # Save to data/users.json
     @util.Noparallel(queue=True, ignore_class=True)
     def save(self):
-        users_json = f'{config.private_dir}/users.json'
+        users_json = config.private_dir / 'users.json'
         s = time.time()
         users = json.load(open(users_json))
         if self.master_address not in users:
diff --git a/src/User/UserManager.py b/src/User/UserManager.py
index a2afc295..b8e49664 100644
--- a/src/User/UserManager.py
+++ b/src/User/UserManager.py
@@ -25,7 +25,7 @@ class UserManager(object):
         s = time.time()
         # Load new users
         try:
-            json_path = f'{config.private_dir}/users.json'
+            json_path = config.private_dir / 'users.json'
             data = json.load(open(json_path))
         except Exception as err:
             raise Exception("Unable to load %s: %s" % (json_path, err))
diff --git a/src/main.py b/src/main.py
index 2fff005f..a6622432 100644
--- a/src/main.py
+++ b/src/main.py
@@ -34,7 +34,7 @@ def importBundle(bundle):
     from Crypt.CryptBitcoin import isValidAddress
     import json
 
-    sites_json_path = f"{config.data_dir}/sites.json"
+    sites_json_path = config.private_dir / 'sites.json'
     try:
         with open(sites_json_path) as f:
             sites = json.load(f)
@@ -68,16 +68,17 @@ def importBundle(bundle):
 
 def init_dirs():
     data_dir = Path(config.data_dir)
+    private_dir = Path(config.private_dir)
     need_bootstrap = (config.bootstrap
                       and not config.offline
-                      and (not data_dir.is_dir() or not (data_dir / 'sites.json').is_file()))
+                      and (not data_dir.is_dir() or not (private_dir / 'sites.json').is_file()))
 
-    old_users_json = data_dir / 'users.json'
-    if old_users_json.is_file():
-        print('Migrating existing users.json file to private/')
-    old_sites_json = data_dir / 'sites.json'
-    if old_sites_json.is_file():
-        print('Migrating existing sites.json file to private/')
+    # old_users_json = data_dir / 'users.json'
+    # if old_users_json.is_file():
+        # print('Migrating existing users.json file to private/')
+    # old_sites_json = data_dir / 'sites.json'
+    # if old_sites_json.is_file():
+        # print('Migrating existing sites.json file to private/')
 
     if not data_dir.is_dir():
         data_dir.mkdir(parents=True, exist_ok=True)
@@ -97,11 +98,11 @@ def init_dirs():
             startupError(f"Cannot load boostrap bundle (response status: {response.status_code})")
         importBundle(BytesIO(response.content))
 
-    sites_json = f"{data_dir}/sites.json"
+    sites_json = private_dir / 'sites.json'
     if not os.path.isfile(sites_json):
         with open(sites_json, "w") as f:
             f.write("{}")
-    users_json = f"{private_dir_dir}/users.json"
+    users_json = private_dir / 'users.json'
     if not os.path.isfile(users_json):
         with open(users_json, "w") as f:
             f.write("{}")
@@ -117,7 +118,6 @@ def init():
     config.initConsoleLogger()
 
     try:
-        print(config.start_dir)
         init_dirs()
     except:
         import traceback as tb
@@ -129,7 +129,7 @@ def init():
     if config.action == "main":
         from util import helper
         try:
-            lock = helper.openLocked(f"{config.data_dir}/lock.pid", "w")
+            lock = helper.openLocked(config.data_dir / 'lock.pid', "w")
             lock.write(f"{os.getpid()}")
         except BlockingIOError as err:
             startupError(f"Can't open lock file, your 0net client is probably already running, exiting... ({err})")
diff --git a/src/util/compat.py b/src/util/compat.py
index f41e67b2..1867dad0 100644
--- a/src/util/compat.py
+++ b/src/util/compat.py
@@ -14,3 +14,9 @@ else:
         return s.removeprefix(prefix)
     def removesuffix(s, suffix, /):
         return s.removesuffix(suffix)
+
+import argparse
+
+if not hasattr(argparse, 'BooleanOptionalAction'):
+    from .argparseCompat import BooleanOptionalAction
+    argparse.BooleanOptionalAction = BooleanOptionalAction
diff --git a/src/util/helper.py b/src/util/helper.py
index af65f727..62264c89 100644
--- a/src/util/helper.py
+++ b/src/util/helper.py
@@ -16,17 +16,17 @@ from Config import config
 
 def atomicWrite(dest, content, mode="wb"):
     try:
-        with open(dest + "-tmpnew", mode) as f:
+        with open(f'{dest}-tmpnew', mode) as f:
             f.write(content)
             f.flush()
             os.fsync(f.fileno())
-        if os.path.isfile(dest + "-tmpold"):  # Previous incomplete write
-            os.rename(dest + "-tmpold", dest + "-tmpold-%s" % time.time())
+        if os.path.isfile(f'{dest}-tmpold'):  # Previous incomplete write
+            os.rename(f'{dest}-tmpold', f'{dest}-tmpold-{time.time()}')
         if os.path.isfile(dest):  # Rename old file to -tmpold
-            os.rename(dest, dest + "-tmpold")
-        os.rename(dest + "-tmpnew", dest)
-        if os.path.isfile(dest + "-tmpold"):
-            os.unlink(dest + "-tmpold")  # Remove old file
+            os.rename(dest, f'{dest}-tmpold')
+        os.rename(f'{dest}-tmpnew', dest)
+        if os.path.isfile(f'{dest}-tmpold'):
+            os.unlink(f'{dest}-tmpold')  # Remove old file
         return True
     except Exception as err:
         from Debug import Debug
@@ -34,8 +34,8 @@ def atomicWrite(dest, content, mode="wb"):
             "File %s write failed: %s, (%s) reverting..." %
             (dest, Debug.formatException(err), Debug.formatStack())
         )
-        if os.path.isfile(dest + "-tmpold") and not os.path.isfile(dest):
-            os.rename(dest + "-tmpold", dest)
+        if os.path.isfile(f'{dest}-tmpold') and not os.path.isfile(dest):
+            os.rename(f'{dest}-tmpold', dest)
         return False
 
 
@@ -85,7 +85,7 @@ def openLocked(path, mode="wb"):
 def getFreeSpace():
     free_space = -1
     if "statvfs" in dir(os):  # Unix
-        statvfs = os.statvfs(config.data_dir.encode("utf8"))
+        statvfs = os.statvfs(str(config.data_dir).encode("utf8"))
         free_space = statvfs.f_frsize * statvfs.f_bavail
     else:  # Windows
         try:
@@ -111,7 +111,7 @@ def shellquote(*args):
     if len(args) == 1:
         return '"%s"' % args[0].replace('"', "")
     else:
-        return tuple(['"%s"' % arg.replace('"', "") for arg in args])
+        return tuple(['"%s"' % str(arg).replace('"', "") for arg in args])
 
 
 def packPeers(peers):
diff --git a/zeronet.py b/zeronet.py
index ff752fb6..4931b4f7 100755
--- a/zeronet.py
+++ b/zeronet.py
@@ -34,7 +34,7 @@ def launch():
         except Exception as log_err:
             print("Failed to log error:", log_err)
             traceback.print_exc()
-        error_log_path = config.log_dir + "/error.log"
+        error_log_path = config.log_dir / "error.log"
         traceback.print_exc(file=open(error_log_path, "w"))
         print("---")
         print("Please report it: https://github.com/zeronet-conservancy/zeronet-conservancy/issues/new?template=bug-report.md")

From d8b06a28bb705050303e082ba87df174023caee6 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 7 May 2024 20:58:26 +0000
Subject: [PATCH 317/333] Fix directories

---
 plugins/Sidebar/SidebarPlugin.py | 2 +-
 src/Content/ContentDb.py         | 2 +-
 src/main.py                      | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py
index c24a0e0b..78c4f39a 100644
--- a/plugins/Sidebar/SidebarPlugin.py
+++ b/plugins/Sidebar/SidebarPlugin.py
@@ -686,7 +686,7 @@ class UiWebsocketPlugin(object):
         if sys.platform == "linux":
             sys_db_paths += ['/usr/share/GeoIP/' + db_name]
 
-        data_dir_db_path = config.data_dir / db_name
+        data_dir_db_path = config.start_dir / db_name
 
         db_paths = sys_db_paths + [data_dir_db_path]
 
diff --git a/src/Content/ContentDb.py b/src/Content/ContentDb.py
index 5b63993d..0abd3658 100644
--- a/src/Content/ContentDb.py
+++ b/src/Content/ContentDb.py
@@ -153,7 +153,7 @@ content_dbs = {}
 
 def getContentDb(path=None):
     if not path:
-        path = config.data_dir / 'content.db'
+        path = config.start_dir / 'content.db'
     if path not in content_dbs:
         content_dbs[path] = ContentDb(path)
         content_dbs[path].init()
diff --git a/src/main.py b/src/main.py
index a6622432..c9f6b3d0 100644
--- a/src/main.py
+++ b/src/main.py
@@ -129,7 +129,7 @@ def init():
     if config.action == "main":
         from util import helper
         try:
-            lock = helper.openLocked(config.data_dir / 'lock.pid', "w")
+            lock = helper.openLocked(config.start_dir / 'lock.pid', "w")
             lock.write(f"{os.getpid()}")
         except BlockingIOError as err:
             startupError(f"Can't open lock file, your 0net client is probably already running, exiting... ({err})")

From 2191e496b47b3cccfdcd36a08042935520946333 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 8 May 2024 23:50:05 +0000
Subject: [PATCH 318/333] Cleanup

---
 src/Config.py | 25 -------------------------
 1 file changed, 25 deletions(-)

diff --git a/src/Config.py b/src/Config.py
index 2895f5fd..d3e87312 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -206,24 +206,6 @@ class Config:
         if '--portable' in self.argv or self.build_type == 'portable':
             return here
 
-        # if os.path.isdir(f'{here}/data') and not '--no-portable' in self.argv:
-            # print('WARNING: found data in current directory')
-            # print('  It used to be default behaviour to store data alongside project directory,')
-            # print('  but now we default to place data and config in user home directory.')
-            # print('  If you want to keep previous behaviour, please use --portable')
-            # print('Assuming implicit --portable (use --no-portable to override)')
-            # print(self.argv)
-            # self.argv.insert(1, '--portable')
-            # print(self.argv)
-            # return here
-
-        # home_zn = os.path.expanduser(f'~/ZeroNet')
-        # if os.path.isdir(home_zn):
-            # print(f'WARNING: found data in {home_zn}')
-            # print( '  It is possible that this is from previous version or another installation')
-            # print( '  altogether. If you want to use that data directory with zeronet-conservancy')
-            # print(f'  you have to run it with --start-dir "{home_zn}" option')
-
         MACOSX_DIR = '~/Library/Application Support/zeronet-conservancy'
         WINDOWS_DIR = '~/AppData/zeronet-conservancy'
         LIBREDESKTOP_DIR = '~/.local/share/zeronet-conservancy'
@@ -499,8 +481,6 @@ class Config:
                     if "://" in tracker and tracker not in self.trackers:
                         self.trackers.append(tracker)
             except Exception as err:
-                print(self.trackers_file)
-                print(trackers_file)
                 print(f'Error loading trackers file: {err}')
 
     # Find arguments specified for current action
@@ -609,12 +589,7 @@ class Config:
             action = "main"
         argv = self.moveUnknownToEnd(argv, action)
         if silent:
-            # print(argv[1:])
             res = self.parser.parse_known_args(argv[1:])
-            # print(res) # ????
-            # parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
-            # parser.add_subparsers(title='Action', dest='action')
-            # parser.parse_args(argv[1:])
             if res:
                 self.arguments = res[0]
             else:

From f6b970c3f6d1e36995241e1ccae79dd28948337e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 10 May 2024 10:24:21 +0000
Subject: [PATCH 319/333] Add missing argparseCompat

---
 src/util/argparseCompat.py | 114 +++++++++++++++++++++++++++++++++++++
 1 file changed, 114 insertions(+)
 create mode 100644 src/util/argparseCompat.py

diff --git a/src/util/argparseCompat.py b/src/util/argparseCompat.py
new file mode 100644
index 00000000..5d2a7f16
--- /dev/null
+++ b/src/util/argparseCompat.py
@@ -0,0 +1,114 @@
+# This code is taken from CPython Lib/argparse.py and contains BooleanOptionalAction
+# for use in py<3.9
+
+# Author: Steven J. Bethard <steven.bethard@gmail.com>.
+# New maintainer as of 29 August 2019:  Raymond Hettinger <raymond.hettinger@gmail.com>
+
+# Copyright © 2001-2024 Python Software Foundation. All rights reserved.
+# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
+# --------------------------------------------
+#
+# 1. This LICENSE AGREEMENT is between the Python Software Foundation
+# ("PSF"), and the Individual or Organization ("Licensee") accessing and
+# otherwise using this software ("Python") in source or binary form and
+# its associated documentation.
+#
+# 2. Subject to the terms and conditions of this License Agreement, PSF hereby
+# grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
+# analyze, test, perform and/or display publicly, prepare derivative works,
+# distribute, and otherwise use Python alone or in any derivative version,
+# provided, however, that PSF's License Agreement and PSF's notice of copyright,
+# i.e., "Copyright (c) 2001-2024 Python Software Foundation; All Rights Reserved"
+# are retained in Python alone or in any derivative version prepared by Licensee.
+#
+# 3. In the event Licensee prepares a derivative work that is based on
+# or incorporates Python or any part thereof, and wants to make
+# the derivative work available to others as provided herein, then
+# Licensee hereby agrees to include in any such work a brief summary of
+# the changes made to Python.
+#
+# 4. PSF is making Python available to Licensee on an "AS IS"
+# basis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+# IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
+# INFRINGE ANY THIRD PARTY RIGHTS.
+#
+# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
+# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+#
+# 6. This License Agreement will automatically terminate upon a material
+# breach of its terms and conditions.
+#
+# 7. Nothing in this License Agreement shall be deemed to create any
+# relationship of agency, partnership, or joint venture between PSF and
+# Licensee.  This License Agreement does not grant permission to use PSF
+# trademarks or trade name in a trademark sense to endorse or promote
+# products or services of Licensee, or any third party.
+#
+# 8. By copying, installing or otherwise using Python, Licensee
+# agrees to be bound by the terms and conditions of this License
+# Agreement.
+
+from argparse import Action
+
+class BooleanOptionalAction(Action):
+    def __init__(self,
+                 option_strings,
+                 dest,
+                 default=None,
+                 type=_deprecated_default,
+                 choices=_deprecated_default,
+                 required=False,
+                 help=None,
+                 metavar=_deprecated_default,
+                 deprecated=False):
+
+        _option_strings = []
+        for option_string in option_strings:
+            _option_strings.append(option_string)
+
+            if option_string.startswith('--'):
+                option_string = '--no-' + option_string[2:]
+                _option_strings.append(option_string)
+
+        # We need `_deprecated` special value to ban explicit arguments that
+        # match default value. Like:
+        #   parser.add_argument('-f', action=BooleanOptionalAction, type=int)
+        for field_name in ('type', 'choices', 'metavar'):
+            if locals()[field_name] is not _deprecated_default:
+                import warnings
+                warnings._deprecated(
+                    field_name,
+                    "{name!r} is deprecated as of Python 3.12 and will be "
+                    "removed in Python {remove}.",
+                    remove=(3, 14))
+
+        if type is _deprecated_default:
+            type = None
+        if choices is _deprecated_default:
+            choices = None
+        if metavar is _deprecated_default:
+            metavar = None
+
+        super().__init__(
+            option_strings=_option_strings,
+            dest=dest,
+            nargs=0,
+            default=default,
+            type=type,
+            choices=choices,
+            required=required,
+            help=help,
+            metavar=metavar,
+            deprecated=deprecated)
+
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        if option_string in self.option_strings:
+            setattr(namespace, self.dest, not option_string.startswith('--no-'))
+
+    def format_usage(self):
+        return ' | '.join(self.option_strings)

From 2a450445d2a99f66e9a1acb5c81ee53f4988857d Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 10 May 2024 10:29:12 +0000
Subject: [PATCH 320/333] Put correct minimum py version (3.8)

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5407e265..2343a2e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
 ### zeronet-conservancy 0.7.10+
 - disable site-plugins installed for security reasons (@caryoscelus)
 - fix downloading geoip db (@caryoscelus)
-- python <3.6 is officially unsupported (3.8 is more likely minimum requirement)
+- python <3.8 is officially unsupported
 - SafeRe improvements by @geekless
 - remove and don't update muted files (@caryoscelus)
 - option to disable port checking (@caryoscelus)

From fd13b132af53a9379b6a219e9d6f2bb44cd7fd34 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 10 May 2024 19:23:05 +0000
Subject: [PATCH 321/333] Fix path-to-str

---
 src/Site/SiteStorage.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py
index 7249ad34..16861b8b 100644
--- a/src/Site/SiteStorage.py
+++ b/src/Site/SiteStorage.py
@@ -413,7 +413,7 @@ class SiteStorage(object):
         if path == self.directory:
             inner_path = ""
         else:
-            if path.startswith(self.directory):
+            if str(path).startswith(self.directory):
                 inner_path = path[len(self.directory) + 1:]
             else:
                 raise Exception("File not allowed: %s" % path)

From 164238b8b5982d50b95cadef3baa4a52dda4a4b8 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 21 May 2024 11:09:27 +0000
Subject: [PATCH 322/333] Fix checking of included file sizes

---
 src/Content/ContentManager.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index 938f3d6f..2c1915b9 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -925,8 +925,8 @@ class ContentManager:
             raise VerifyError("No rules")
 
         # Check include size limit
-        max_size = rules.get("max_size", 0)
-        if content_size > max_size:
+        max_size = rules.get('max_size')
+        if max_size is not None and content_size > max_size:
             raise VerifyError(f'Include too large {content_size}B > {max_size}B')
 
         if rules.get("max_size_optional") is not None:  # Include optional files limit

From ab7707c429d548656c5b008f545a97474c5c642e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 15 Jun 2024 13:55:09 +0000
Subject: [PATCH 323/333] Minor fix

convert path to str
---
 src/Site/SiteStorage.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py
index 16861b8b..56245314 100644
--- a/src/Site/SiteStorage.py
+++ b/src/Site/SiteStorage.py
@@ -413,8 +413,8 @@ class SiteStorage(object):
         if path == self.directory:
             inner_path = ""
         else:
-            if str(path).startswith(self.directory):
-                inner_path = path[len(self.directory) + 1:]
+            if str(path).startswith(str(self.directory)):
+                inner_path = path[len(str(self.directory)) + 1:]
             else:
                 raise Exception("File not allowed: %s" % path)
         return inner_path

From bd3383e0733e49cc86656506b96534fd0f283b5a Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 15 Jun 2024 14:50:42 +0000
Subject: [PATCH 324/333] Fix support for absolute start_dir path

---
 src/Content/ContentManager.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index 2c1915b9..c1461150 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -91,7 +91,7 @@ class ContentManager:
         content_inner_path = content_inner_path.strip("/")  # Remove / from beginning
         old_content = self.contents.get(content_inner_path)
         content_path = self.site.storage.getPath(content_inner_path)
-        content_dir = helper.getDirname(self.site.storage.getPath(content_inner_path))
+        content_dir = os.path.dirname(content_path)
         content_inner_dir = helper.getDirname(content_inner_path)
 
         if os.path.isfile(content_path):

From 2abd16d51c1d30c7e6e479f757f5f8c8e07e0bf6 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Sat, 15 Jun 2024 16:41:34 +0000
Subject: [PATCH 325/333] Fix in --lax-cert-check

---
 src/Content/ContentManager.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py
index c1461150..bf9411e6 100644
--- a/src/Content/ContentManager.py
+++ b/src/Content/ContentManager.py
@@ -540,8 +540,10 @@ class ContentManager:
         # to prevent spam, if cert is compromised, only allow personal rules
         if config.lax_cert_check or cert_addresses is None or self.isGoodCert(cert_addresses[0]):
             permission_rules = user_contents.get('permission_rules', {}).items()
-            if not self.isGoodCert(cert_addresses[0]):
-                self.log.warning('Accepting compromised certificate! This may lead to spam attack. Turn off with --no_lax_cert_check. Default behaviour may change in the future.')
+            if cert_addresses is None:
+                self.log.warning('Accepting no certificate because you have --lax-cert-check on.')
+            elif not self.isGoodCert(cert_addresses[0]):
+                self.log.warning('Accepting compromised certificate! This may lead to spam attack. Turn off with --no-lax-cert-check. Default behaviour may change in the future.')
         else:
             permission_rules = {}
         for permission_pattern, permission_rules in permission_rules:  # Regexp rules

From 8362a258da7e9f34f4d313fa71c34e1832dbad3e Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Tue, 25 Jun 2024 11:34:46 +0000
Subject: [PATCH 326/333] Minor fix (convert path to str)

---
 src/Site/SiteStorage.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py
index 56245314..2119db24 100644
--- a/src/Site/SiteStorage.py
+++ b/src/Site/SiteStorage.py
@@ -314,7 +314,7 @@ class SiteStorage(object):
         directory = self.getPath(dir_inner_path)
         for root, dirs, files in os.walk(directory):
             root = root.replace("\\", "/")
-            root_relative_path = re.sub("^%s" % re.escape(directory), "", root).lstrip("/")
+            root_relative_path = re.sub(f'^{re.escape(str(directory))}', '', root).lstrip('/')
             for file_name in files:
                 if root_relative_path:  # Not root dir
                     file_relative_path = root_relative_path + "/" + file_name

From 79a209a91650265dab007a66afd5804523848a36 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 26 Jun 2024 16:07:32 +0000
Subject: [PATCH 327/333] Minor improvement

More sensible default for siteList
---
 src/Ui/UiWebsocket.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index a87abd62..aebf6a80 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -942,7 +942,7 @@ class UiWebsocket(object):
 
     # List all site info
     @flag.admin
-    def actionSiteList(self, to, connecting_sites=False):
+    def actionSiteList(self, to, connecting_sites=True):
         ret = []
         for site in list(self.server.sites.values()):
             if not site.content_manager.contents.get("content.json") and not connecting_sites:

From f78b1e3672556d486d48d8d8c5ae52f1a06ed832 Mon Sep 17 00:00:00 2001
From: snyk-bot <snyk-bot@snyk.io>
Date: Mon, 15 Jul 2024 20:46:15 +0000
Subject: [PATCH 328/333] fix: requirements.txt to reduce vulnerabilities

The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-SETUPTOOLS-7448482
---
 requirements.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/requirements.txt b/requirements.txt
index e5cfb71e..2697b086 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 gevent>=1.1.0
 msgpack>=0.4.4
+setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability

From 668b25e972a0cdb39e2e44aa3bbfc8badbe578f9 Mon Sep 17 00:00:00 2001
From: Ikko Eltociear Ashimine <eltociear@gmail.com>
Date: Mon, 9 Sep 2024 12:57:15 +0900
Subject: [PATCH 329/333] docs: add Japanese README

I created Japanese translated README.
---
 README-ja.md    | 274 ++++++++++++++++++++++++++++++++++++++++++++++++
 README-ptbr.md  |   2 +-
 README-ru.md    |   2 +-
 README-zh-cn.md |   2 +-
 README.md       |   2 +-
 5 files changed, 278 insertions(+), 4 deletions(-)
 create mode 100644 README-ja.md

diff --git a/README-ja.md b/README-ja.md
new file mode 100644
index 00000000..2ab57496
--- /dev/null
+++ b/README-ja.md
@@ -0,0 +1,274 @@
+# zeronet-conservancy
+
+[![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
+
+(このファイルの翻訳は通常このファイルの後ろにあります)
+
+[по-русски](README-ru.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md) | 日本語
+
+`zeronet-conservancy`は[ZeroNet](https://github.com/HelloZeroNet/ZeroNet)プロジェクトのフォーク/継続です
+(その作成者によって放棄された)、既存のp2pネットワークを維持し、開発することに専念しています
+分散化と自由の価値観を持ちながら、徐々により良い設計のネットワークに移行していきます
+
+## アクティブなメンテナーの警告なし
+
+このフォークは@caryoscelusによって作成および維持されましたが、興味が薄れたため、
+もう一人のプロジェクトを持つことを避けるために、開発は制限されています。
+
+## なぜフォークするのか?
+
+onion-v3スイッチの危機の間、私たちはonion-v3で動作し、1人または
+2人の人々への信頼に依存しないフォークが必要でした。このフォークはその使命を果たすことから始まりました
+誰でも簡単に監査できる[ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3)ブランチに最小限の変更を加えます。
+フォークの初期リリースを使用してonion-v3を動作させることはまだできますが、このフォークの目標はそれ以来シフトしました
+新しい、完全に透明で監査されたネットワークが準備が整い、このプロジェクトが休止できるまで、より多くの問題を解決し、ユーザーエクスペリエンスとセキュリティを全体的に改善することに専念しています
+
+## なぜ0netなのか?
+
+* 私たちは、オープンで自由で検閲されていないネットワークとコミュニケーションを信じています。
+* 単一障害点がない:少なくとも1つのピアが
+  それを提供しています。
+* ホスティングコストなし:サイトは訪問者によって提供されます。
+* シャットダウンすることは不可能です:どこにもないのでどこにでもあります。
+* 高速でオフラインで動作します:インターネットが利用できない場合でもサイトにアクセスできます。
+
+## 特徴
+ * リアルタイムで更新されるサイト
+ * ワンクリックでウェブサイトをクローン
+ * プライベート/パブリックキーを使用したパスワードレス認証
+ * P2Pデータ同期を備えた組み込みSQLサーバー:より簡単な動的サイト開発を可能にします
+ * 匿名性:.onion隠しサービスを使用したTorネットワークのサポート(onion-v3サポートを含む)
+ * TLS暗号化接続(クリーンネット経由)
+ * 自動uPnPポート開放(オプトインした場合)
+ * マルチユーザー(オープンプロキシ)サポート用のプラグイン
+ * すべてのモダンブラウザ/OSで動作
+ * オフラインで動作し、代替トランスポートを介して同期できます(または接続が戻ったとき)
+
+## どのように機能しますか?
+
+* `zeronet.py`を起動した後、zeronetサイトにアクセスできるようになります
+  `http://127.0.0.1:43110/{zeronet_address}`(例:
+  `http://127.0.0.1:43110/1MCoA8rQHhwu4LY2t2aabqcGSRqrL8uf2X/`)。
+* 新しいzeronetサイトにアクセスすると、BitTorrentを使用してピアを見つけようとします
+  ネットワークは、サイトファイル(html、css、js ...)をそれらからダウンロードできるようにします。
+* 訪問した各サイトもあなたによって提供されます。
+* すべてのサイトには、すべての他のファイルをsha512ハッシュで保持する`content.json`ファイルが含まれています
+  およびサイトの秘密鍵を使用して生成された署名。
+* サイト所有者(サイトアドレスの秘密鍵を持っている人)がサイトを変更した場合
+  サイト、次に彼/彼女は新しい`content.json`に署名し、それをピアに公開します。
+  その後、ピアは`content.json`の整合性を確認します(
+  署名)、変更されたファイルをダウンロードし、新しいコンテンツを
+  他のピア。
+
+次のリンクは、元のZeroNetに関連しています。
+
+- [ZeroNetの暗号化、サイトの更新、マルチユーザーサイトに関するスライドショー»](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
+- [よくある質問»](https://zeronet.io/docs/faq/)
+- [ZeroNet開発者ドキュメント»](https://zeronet.io/docs/site_development/getting_started/)(古くなっています)
+
+## 参加方法
+
+### ディストリビューションリポジトリからインストール
+
+- NixOS:[zeronet-conservancyパッケージ検索](https://search.nixos.org/packages?from=0&size=50&sort=relevance&type=packages&query=zeronet-conservancy)(以下を参照)
+- ArchLinux:[最新リリース](https://aur.archlinux.org/packages/zeronet-conservancy)、[最新のgitバージョン](https://aur.archlinux.org/packages/zeronet-conservancy-git)
+
+### Nixパッケージマネージャーからインストール(LinuxまたはMacOS)
+
+ - nixパッケージマネージャーをインストールして構成します(必要に応じて)
+ - `nix-env -iA nixpkgs.zeronet-conservancy`
+
+または、NixOSを使用している場合は、システム構成に`zeronet-conservancy`を追加します
+
+(パッケージの作成と維持を行ってくれた@fgazに感謝します)
+
+### ソースからインストール
+
+#### システム依存関係
+
+##### 一般的なUnix系(mac os xを含む)
+
+autoconfおよびその他の基本的な開発ツール、python3およびpipをインストールし、「ビルドpython依存関係」に進みます
+(実行に失敗した場合は、欠落している依存関係を報告するか、依存関係リストを修正するためのプルリクエストを作成してください)
+
+##### Aptベース(debian、ubuntuなど)
+ - `sudo apt update`
+ - `sudo apt install git pkg-config libffi-dev python3-pip python3-venv python3-dev build-essential libtool`
+
+##### Red HatおよびFedoraベース
+ - `yum install epel-release -y 2>/dev/null`
+ - `yum install git python3 python3-wheel`
+
+##### Fedoraベースのdandified
+ - `sudo dnf install git python3-pip python3-wheel -y`
+
+##### openSUSE
+ - `sudo zypper install python3-pip python3-setuptools python3-wheel`
+
+##### ArchおよびManjaroベース
+ - `sudo pacman -S git python-pip -v --no-confirm`
+
+##### Android/Termux
+ - [Termux](https://termux.com/)をインストールします(Termuxでは、`pkg install <package-names>`を使用してパッケージをインストールできます)
+ - `pkg update`
+ - `pkg install python automake git binutils libtool`
+ - (古いandroidバージョンでは、`pkg install openssl-tool libcrypt clang`もインストールする必要がある場合があります)
+ - (上記のパッケージをインストールしても起動の問題が発生する場合は、報告してください)
+ - (オプション)`pkg install tor`
+ - (オプション)`tor --ControlPort 9051 --CookieAuthentication 1`コマンドを使用してtorを実行します(右にスワイプして新しいセッションを開くことができます)
+
+#### python依存関係、venvのビルドと実行
+ - このリポジトリをクローンします(注:Android/Termuxでは、仮想環境が`storage/`に存在できないため、Termuxの「ホーム」フォルダにクローンする必要があります)
+ - `python3 -m venv venv`(python仮想環境を作成します。最後の`venv`は名前にすぎません。後のコマンドで置き換える必要がある場合は、別の名前を使用してください)
+ - `source venv/bin/activate`(環境をアクティブ化)
+ - `python3 -m pip install -r requirements.txt`(依存関係をインストール)
+ - `python3 zeronet.py`(**zeronet-conservancyを実行!**)
+ - ブラウザでランディングページを開き、http://127.0.0.1:43110/に移動します
+ - 新しいターミナルから再起動するには、リポジトリディレクトリに移動して次のコマンドを実行する必要があります:
+ - `source venv/bin/activate`
+ - `python3 zeronet.py`
+
+#### (代替)NixOSで
+- このリポジトリをクローン
+- `nix-shell '<nixpkgs>' -A zeronet-conservancy`を実行して、依存関係がインストールされたシェルに入ります
+- `./zeronet.py`
+
+#### (代替)Dockerイメージのビルド
+- 0netイメージをビルド:`docker build -t 0net-conservancy:latest . -f Dockerfile`
+- 統合されたtorを使用して0netイメージをビルド:`docker build -t 0net-conservancy:latest . -f Dockerfile.integrated_tor`
+- 実行:`docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net-conservancy:latest`
+- /path/to/0n/data/directory - すべてのデータが保存されるディレクトリ。秘密の証明書も含まれます。運用モードで実行する場合は、このフォルダを削除しないでください!
+- または、docker-composeを使用して実行できます:`docker compose up -d 0net-conservancy`は、0netとtorの2つのコンテナを個別に起動します。
+- または:`docker compose up -d 0net-tor`を実行して、1つのコンテナで0netとtorを実行します。
+(これらの手順がまだ正確かどうかを確認してください)
+
+#### 代替のワンライナー(@ssdifnskdjfnsdjkによる)(python依存関係をグローバルにインストール)
+
+Githubリポジトリをクローンし、必要なPythonモジュールをインストールします。最初に
+コマンドの先頭にあるzndirパスを、`zeronet-conservancy`を保存するパスに編集します:
+
+`zndir="/home/user/myapps/zeronet" ; if [[ ! -d "$zndir" ]]; then git clone --recursive "https://github.com/zeronet-conservancy/zeronet-conservancy.git" "$zndir" && cd "$zndir"||exit; else cd "$zndir";git pull origin master; fi; cd "$zndir" && pip install -r requirements.txt|grep -v "already satisfied"; echo "Try to run: python3 $(pwd)/zeronet.py"`
+
+(このコマンドは、`zeronet-conservancy`を最新の状態に保つためにも使用できます)
+
+#### 代替スクリプト
+ - 一般的な依存関係をインストールし、リポジトリをクローンした後(上記のように)、
+   `start-venv.sh`を実行して仮想環境を作成し、
+   pythonの要件をインストールします
+ - 便利なスクリプトを近日追加予定
+
+### (非公式)Windows OSビルド
+
+.zipアーカイブをダウンロードして解凍します
+[zeronet-conservancy-0.7.10-unofficial-win64.zip](https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.10/zeronet-conservancy-0.7.10-unofficial-win64.zip)
+
+### Windows OSでのビルド
+
+(これらの手順は進行中の作業です。テストして改善するのに役立ててください!)
+
+- https://www.python.org/downloads/からpythonをインストールします
+- pythonに適したWindowsコンパイラをインストールします。これが私にとって最も難しい部分でした(https://wiki.python.org/moin/WindowsCompilersを参照し、後でさらに参考資料をリンクします)
+- [最新の開発バージョンを取得するためにオプション] https://git-scm.com/downloadsからgitをインストールします
+- [より良い接続性と匿名性のためにtorを使用するためにオプション] https://www.torproject.org/download/からtorブラウザをインストールします
+- git bashコンソールを開きます
+- コマンドラインに`git clone https://github.com/zeronet-conservancy/zeronet-conservancy.git`を入力/コピーします
+- gitが最新の開発バージョンをダウンロードするのを待ち、コンソールで続行します
+- `cd zeronet-conservancy`
+- `python -m venv venv`(仮想python環境を作成します)
+- `venv\Scripts\activate`(これにより環境がアクティブになります)
+- `pip install -r requirements.txt`(python依存関係をインストールします)(一部のユーザーは、このコマンドが依存関係を正常にインストールせず、依存関係を1つずつ手動でインストールする必要があると報告しています)
+- (注:前のステップが失敗した場合、c/c++コンパイラが正常にインストールされていない可能性が高いです)
+- [より良い接続性と匿名性のためにtorを使用するためにオプション] Torブラウザを起動します
+- (注:Windowsは「セキュリティ上の理由から」インターネットへのアクセスをブロックしたというウィンドウを表示する場合があります。アクセスを許可する必要があります)
+- `python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151`(zeronet-conservancyを起動!)
+- [完全なtor匿名性のためにこれを起動] `python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151 --tor always`
+- お気に入りのブラウザでhttp://127.0.0.1:43110に移動します!
+
+.exeをビルドするには
+
+- 上記と同じ手順に従いますが、さらに
+- `pip install pyinstaller`
+- `pyinstaller -p src -p plugins --hidden-import merkletools --hidden-import lib.bencode_open --hidden-import Crypt.Crypt --hidden-import Db.DbQuery --hidden-import lib.subtl --hidden-import lib.subtl.subtl --hidden-import sockshandler --add-data "src;src" --add-data "plugins;plugins" --clean zeronet.py`
+- dist/zeronetには動作するzeronet.exeが含まれているはずです!
+
+## 現在の制限
+
+* ファイルトランザクションは圧縮されません
+* プライベートサイトなし
+* DHTサポートなし
+* I2Pサポートなし
+* zeroidのような集中化された要素(これに取り組んでいます!)
+* 信頼できるスパム保護なし(これにも取り組んでいます)
+* ブラウザから直接動作しません(中期的な優先事項の1つ)
+* データの透明性なし
+* 再現可能なビルドなし
+* オンディスク暗号化なし
+* 再現可能なビルドなし(したがって、特定のGNU/Linuxディストリビューションを超えたビルドはありません)
+
+## ZeroNetサイトを作成するにはどうすればよいですか?
+
+ * [ダッシュボード](http://127.0.0.1:43110/191CazMVNaAcT9Y1zhkxd9ixMBPs59g2um/)の**⋮** > **「新しい空のサイトを作成」**メニュー項目をクリックします。
+ * **リダイレクト**され、あなただけが変更できる完全に新しいサイトに移動します!
+ * **data/[yoursiteaddress]**ディレクトリでサイトのコンテンツを見つけて変更できます
+ * 変更後にサイトを開き、右上の「0」ボタンを左にドラッグしてから、下部の**署名して公開**ボタンを押します
+
+次のステップ:[ZeroNet開発者ドキュメント](https://zeronet.io/docs/site_development/getting_started/)
+
+## このプロジェクトを存続させるために支援する
+
+### メンテナーになる
+
+もっとメンテナーが必要です!今日1つになりましょう!コーディング方法を知る必要はありません、
+他にもたくさんの仕事があります。
+
+### プラットフォーム用のビルドを作成する
+
+主要なプラットフォーム用の再現可能なスタンドアロンビルド、およびさまざまなFLOSSリポジトリへのプレゼンスが必要です。まだパッケージがないLinuxディストリビューションの1つを使用している場合は、パッケージを作成するか(方法がわからない場合は)今すぐメンテナーに依頼してみませんか?
+
+### バグを修正し、機能を追加する
+
+私たちは前進し、完璧なp2pウェブを作成することにしました。したがって、実装を支援するためにさらに多くの支援が必要です。
+
+### サイトを作成する/コンテンツを持ち込む
+
+ドキュメントが不足していることはわかっていますが、できる限りのサポートを提供しようとしています
+移行したい人。遠慮なく質問してください。
+
+### 使用して広める
+
+なぜ0netとこのフォークを特に使用するのかを人々に必ず伝えてください!人々
+彼らの選択肢を知る必要があります。
+
+### メンテナーを財政的に支援する
+
+このフォークは@caryoscelusによって作成および維持されました。あなたは
+https://caryoscelus.github.io/donate/で彼らに寄付する方法を確認してください(または
+githubでこれを読んでいる場合は、他の方法についてはサイドバーを確認してください)。私たちのチームが成長するにつれて、私たちは
+フレンドリーなクラウドファンディングプラットフォームにチームアカウントを作成します。
+
+このプロジェクトへの寄付として寄付が認識されることを確認したい場合は、
+専用のビットコインアドレスもあります:
+1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6。より匿名でプライベートにしたい場合は、Moneroウォレット:
+4AiYUcqVRH4C2CVr9zbBdkhRnJnHiJoypHEsq4N7mQziGUoosPCpPeg8SPr87nvwypaRzDgMHEbWWDekKtq8hm9LBmgcMzC
+
+別の方法で寄付したい場合は、メンテナーに連絡するか、
+問題を作成する
+
+# 抗議の下でGitHubを使用しています
+
+このプロジェクトは現在GitHubでホストされています。これは理想的ではありません。 GitHubは
+フリー/リブレおよびオープンソースソフトウェアではない独自のトレードシークレットシステム
+(FLOSS)。 GitHubのような独自のシステムを使用してFLOSSプロジェクトを開発することについて深く懸念しています。私たちは
+長期的にGitHubから移行するための[オープンイシュー](https://github.com/zeronet-conservancy/zeronet-conservancy/issues/89)があります。 [Give up GitHub](https://GiveUpGitHub.org)キャンペーンについて読むことをお勧めします
+[ソフトウェアフリーダムコンセルバンシー](https://sfconservancy.org)から
+GitHubがFOSSプロジェクトをホストするのに適していない理由のいくつかを理解するため。
+
+すでにGitHubの使用を個人的にやめたコントリビューターの場合は、
+[notabugのミラーからチェックアウト](https://notabug.org/caryoscelus/zeronet-conservancy)
+して、そこで開発するか、gitパッチをプロジェクトメンテナーに直接送信します
+[連絡先チャネル](https://caryoscelus.github.io/contacts/)を好む。
+
+GitHub Copilotによるこのプロジェクトのコードの過去または現在の使用は、
+私たちの許可なしに行われます。私たちは、Copilotでこのプロジェクトのコードを使用することに同意しません。
+
+![GiveUpGitHubキャンペーンのロゴ](https://sfconservancy.org/img/GiveUpGitHub.png)
diff --git a/README-ptbr.md b/README-ptbr.md
index a53bc9c2..09b1c4a6 100644
--- a/README-ptbr.md
+++ b/README-ptbr.md
@@ -1,6 +1,6 @@
 # zeronet-conservancy
 
-[in English](README.md) | [по-русски](README-ru.md) | [简体中文](README-zh-cn.md)
+[in English](README.md) | [по-русски](README-ru.md) | [简体中文](README-zh-cn.md) | [日本語](README-ja.md)
 
 [![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
 
diff --git a/README-ru.md b/README-ru.md
index 1c9df426..adeec693 100644
--- a/README-ru.md
+++ b/README-ru.md
@@ -1,6 +1,6 @@
 # zeronet-conservancy
 
-[in English](README.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md)
+[in English](README.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md) | [日本語](README-ja.md)
 
 [![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
 
diff --git a/README-zh-cn.md b/README-zh-cn.md
index 2a4bb346..f651ecee 100644
--- a/README-zh-cn.md
+++ b/README-zh-cn.md
@@ -4,7 +4,7 @@
 
 [![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
 
-[in English](README.md) | [em português](README-ptbr.md) | [по-русски](README-ru.md)
+[in English](README.md) | [em português](README-ptbr.md) | [по-русски](README-ru.md) | [日本語](README-ja.md)
 
 ## 为什么?
 
diff --git a/README.md b/README.md
index 9f1cdb43..54f99eb6 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 
 (NOTE THAT TRANSLATIONS ARE USUALLY BEHIND THIS FILE)
 
-[по-русски](README-ru.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md)
+[по-русски](README-ru.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md) | [日本語](README-ja.md)
 
 `zeronet-conservancy` is a fork/continuation of [ZeroNet](https://github.com/HelloZeroNet/ZeroNet) project
 (that has been abandoned by its creator) that is dedicated to sustaining existing p2p network and developing

From 09e5f6e2034bc54d7a32ace0bdda577ee129d7ed Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Fri, 27 Sep 2024 11:02:27 +0000
Subject: [PATCH 330/333] State of the fork

---
 README.md | 18 ++++++++----------
 1 file changed, 8 insertions(+), 10 deletions(-)

diff --git a/README.md b/README.md
index 9f1cdb43..3ec8797e 100644
--- a/README.md
+++ b/README.md
@@ -10,19 +10,17 @@
 (that has been abandoned by its creator) that is dedicated to sustaining existing p2p network and developing
 its values of decentralization and freedom, while gradually switching to a better designed network
 
-## No active maintainer warning
-
-This fork was created and maintained by @caryoscelus, but due to vanishing interest and in order to avoid having
-another one-person project, the development is limitted.
-
-## Why fork?
+## State of the fork
 
 During onion-v3 switch crisis, we needed a fork that worked with onion-v3 and didn't depend on trust to one or
 two people. This fork started from fulfilling that mission, implementing minimal changes to
-[ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3) branch which are easy to audit by anyone. While
-you can still use the early releases of the fork to get onion-v3 working, the goal of this fork has since shifted
-and we're dedicated to solving more problems and improving user experience and security all over, until the
-brand new, completely transparent and audited network is ready and this project can be put to rest
+[ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3) branch which are easy to audit by anyone.
+
+Now 0net is in deeper crisis than ever before and this fork seems to
+be the last one standing.  Development is sparse and slow, but some of
+the work is being done behind the scenes. If you're completely new to
+0net, don't have anybody to guide you there and are not a developer,
+we recommend waiting until v0.8 is out.
 
 ## Why 0net?
 

From 899816746fac0760ff63933e3aa233197a078202 Mon Sep 17 00:00:00 2001
From: caryoscelus <caryoscelus@gmx.com>
Date: Wed, 23 Oct 2024 18:45:01 +0000
Subject: [PATCH 331/333] Fix links in README-ja.md

---
 README-ja.md | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/README-ja.md b/README-ja.md
index 2ab57496..08724e7d 100644
--- a/README-ja.md
+++ b/README-ja.md
@@ -123,7 +123,7 @@ autoconfおよびその他の基本的な開発ツール、python3およびpip
  - `source venv/bin/activate`(環境をアクティブ化)
  - `python3 -m pip install -r requirements.txt`(依存関係をインストール)
  - `python3 zeronet.py`(**zeronet-conservancyを実行!**)
- - ブラウザでランディングページを開き、http://127.0.0.1:43110/に移動します
+ - ブラウザでランディングページを開き、http://127.0.0.1:43110/ に移動します
  - 新しいターミナルから再起動するには、リポジトリディレクトリに移動して次のコマンドを実行する必要があります:
  - `source venv/bin/activate`
  - `python3 zeronet.py`
@@ -166,10 +166,10 @@ Githubリポジトリをクローンし、必要なPythonモジュールをイ
 
 (これらの手順は進行中の作業です。テストして改善するのに役立ててください!)
 
-- https://www.python.org/downloads/からpythonをインストールします
-- pythonに適したWindowsコンパイラをインストールします。これが私にとって最も難しい部分でした(https://wiki.python.org/moin/WindowsCompilersを参照し、後でさらに参考資料をリンクします)
-- [最新の開発バージョンを取得するためにオプション] https://git-scm.com/downloadsからgitをインストールします
-- [より良い接続性と匿名性のためにtorを使用するためにオプション] https://www.torproject.org/download/からtorブラウザをインストールします
+- https://www.python.org/downloads/ からpythonをインストールします
+- pythonに適したWindowsコンパイラをインストールします。これが私にとって最も難しい部分でした(https://wiki.python.org/moin/WindowsCompilers を参照し、後でさらに参考資料をリンクします)
+- [最新の開発バージョンを取得するためにオプション] https://git-scm.com/downloads からgitをインストールします
+- [より良い接続性と匿名性のためにtorを使用するためにオプション] https://www.torproject.org/download/ からtorブラウザをインストールします
 - git bashコンソールを開きます
 - コマンドラインに`git clone https://github.com/zeronet-conservancy/zeronet-conservancy.git`を入力/コピーします
 - gitが最新の開発バージョンをダウンロードするのを待ち、コンソールで続行します
@@ -182,7 +182,7 @@ Githubリポジトリをクローンし、必要なPythonモジュールをイ
 - (注:Windowsは「セキュリティ上の理由から」インターネットへのアクセスをブロックしたというウィンドウを表示する場合があります。アクセスを許可する必要があります)
 - `python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151`(zeronet-conservancyを起動!)
 - [完全なtor匿名性のためにこれを起動] `python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151 --tor always`
-- お気に入りのブラウザでhttp://127.0.0.1:43110に移動します!
+- お気に入りのブラウザでhttp://127.0.0.1:43110 に移動します!
 
 .exeをビルドするには
 
@@ -242,7 +242,7 @@ Githubリポジトリをクローンし、必要なPythonモジュールをイ
 ### メンテナーを財政的に支援する
 
 このフォークは@caryoscelusによって作成および維持されました。あなたは
-https://caryoscelus.github.io/donate/で彼らに寄付する方法を確認してください(または
+https://caryoscelus.github.io/donate/ で彼らに寄付する方法を確認してください(または
 githubでこれを読んでいる場合は、他の方法についてはサイドバーを確認してください)。私たちのチームが成長するにつれて、私たちは
 フレンドリーなクラウドファンディングプラットフォームにチームアカウントを作成します。
 

From 97d8d3d3b6e6dab85f939be5e49fef5574e7fe35 Mon Sep 17 00:00:00 2001
From: momordica2020 <287859992@qq.com>
Date: Fri, 25 Oct 2024 21:02:01 +0800
Subject: [PATCH 332/333] Update README-zh-cn.md

translate the latest README.md to Chinese. Not a machine translation because I am a native Chinese speaker.
---
 README-zh-cn.md | 265 ++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 211 insertions(+), 54 deletions(-)

diff --git a/README-zh-cn.md b/README-zh-cn.md
index f651ecee..eee1e863 100644
--- a/README-zh-cn.md
+++ b/README-zh-cn.md
@@ -1,81 +1,238 @@
 # zeronet-conservancy
 
-**警告**:这个翻译已经过时了。请阅读英文版。对此翻译的任何贡献都将受到高度赞赏
-
 [![Packaging status](https://repology.org/badge/vertical-allrepos/zeronet-conservancy.svg)](https://repology.org/project/zeronet-conservancy/versions)
 
-[in English](README.md) | [em português](README-ptbr.md) | [по-русски](README-ru.md) | [日本語](README-ja.md)
+**(注意:本文档的翻译版本通常滞后于最新内容)**
 
-## 为什么?
+[по-русски](README-ru.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md) | [日本語](README-ja.md)
 
-* 我们相信开放,自由,无审查的网络和通讯
-* 不会受单点故障影响:只要有在线的节点,站点就会保持在线
-* 无托管费用:站点由访问者托管
-* 无法关闭:因为节点无处不在
-* 快速并可离线运行:即使没有互联网连接也可以使用
+`zeronet-conservancy` 是 [零网 ZeroNet](https://github.com/HelloZeroNet/ZeroNet) 项目的一个分支/延续(其创建者已放弃维护),致力于维持现有的p2p网络,进一步发扬去中心化与自由的价值观,并逐步得到一个设计得更好的网络。
 
+## 本分支的状况
+
+在onion-v3切换危机期间,我们需要一个能够支持onion-v3,且不仅仅依赖于个别开发者的可信的分支。本分支从实现这一任务开始,对 [ZeroNet/py3](https://github.com/HelloZeroNet/ZeroNet/tree/py3) 分支作了最少的改动,从而让任何人都可以轻松地审计这些改动。
+
+目前,0net正陷入前所未有的危机,而本分支似乎是唯一存活下来的。我们的开发进展缓慢,但其中的部分工作正在幕后进行。如果你完全是个0net的新手,且没有人指导你,而且你也不是开发者的话,我们建议请等待v0.8版本发布。
+
+## 为何选择0net?
+
+* 我们相信开放、自由和不受审查的网络与通讯。
+* 不受单点故障影响:只要至少有一个节点在提供服务,站点就能保持在线。
+* 无需托管费用:站点由访问者们来共同托管。
+* 无法被关停:因为节点无处不在。
+* 快速并且支持离线访问:即使没有互联网,你也可以访问站点。
 
 ## 功能
- * 实时站点更新
- * 安装方便:只需解压并运行
- * 一键克隆存在的站点
- * 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
-   的认证:您的账户被与比特币钱包相同的加密方法保护
- * 内建 SQL 服务器和 P2P 数据同步:让开发更简单并提升加载速度
- * 匿名性:完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过 IPv4 地址连接
- * TLS 加密连接
- * 自动打开 uPnP 端口
- * 多用户(openproxy)支持的插件
- * 适用于任何浏览器 / 操作系统
+* 实时更新站点
+* 一键克隆已有的站点
+* 使用私钥/公钥的免密码认证
+* 内置SQL服务器,支持P2P数据同步,便于动态站点开发
+* 匿名性:支持采用.onion匿踪服务的Tor网络(支持onion-v3)
+* TLS加密连接(会经过明网)
+* 自动开启uPnP端口(可选)
+* 支持多用户插件(openproxy)
+* 兼容任何现代浏览器/操作系统
+* 离线站点可通过其他方式进行同步(或当连接恢复时同步)
 
+## 它是如何工作的?
 
-## 原理
+* 启动`zeronet.py`后,你可以通过以下方式访问zeronet站点: `http://127.0.0.1:43110/{零网站点地址}` (例如  `http://127.0.0.1:43110/1MCoA8rQHhwu4LY2t2aabqcGSRqrL8uf2X/`)
+* 当你访问一个新站点时,它会通过BitTorrent网络来寻找可用节点,从这些节点下载站点所需的文件(如html, css, js...)。
+* 每个你访问过的站点也由你提供托管服务。
+* 每个站点包含一个`content.json`文件,其中包含着由本站所有文件生成的sha512哈希值以及由站点私钥生成的签名。
+* 如果站点拥有者(即持有站点私钥的人)对站点内容进行了修改,则他/她会重新签署`content.json`文件并发布至其他节点。随后,其他节点会验证`content.json`的完整性(通过签名),并下载被修改的文件,随后将更新过的内容继续发布给其他节点。
 
-* 在运行 `zeronet.py` 后,您将可以通过
-  `http://127.0.0.1:43110/{zeronet_address}`(例如:
-  `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`)访问 zeronet 中的站点
-* 在您浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点,从而下载需要的文件(html,css,js...)
-* 您将会储存每一个浏览过的站点
-* 每个站点都包含一个名为 `content.json` 的文件,它储存了其他所有文件的 sha512 散列值以及一个通过站点私钥生成的签名
-* 如果站点的所有者(拥有站点地址的私钥)修改了站点,并且他 / 她签名了新的 `content.json` 然后推送至其他节点,
-  那么这些节点将会在使用签名验证 `content.json` 的真实性后,下载修改后的文件并将新内容推送至另外的节点
-
-####  [关于 ZeroNet 加密,站点更新,多用户站点的幻灯片 »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
-####  [常见问题 »](https://zeronet.io/docs/faq/)
-
-####  [ZeroNet 开发者文档 »](https://zeronet.io/docs/site_development/getting_started/)
-
-
-## 屏幕截图
-
-#### [ZeroNet 文档中的更多屏幕截图 »](https://zeronet.io/docs/using_zeronet/sample_sites/)
+以下链接来自原版ZeroNet:
 
+- [关于ZeroNet加密、站点更新、多用户站点的演示文稿 »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
+- [常见问题解答 »](https://zeronet.io/docs/faq/)
+- [ZeroNet开发者文档 »](https://zeronet.io/docs/site_development/getting_started/)(已过时)
 
 ## 如何加入
 
+### 从发行版仓库安装
+
+- NixOS: [搜索zeronet-conservancy软件包](https://search.nixos.org/packages?from=0&size=50&sort=relevance&type=packages&query=zeronet-conservancy)(并参见下方)
+- ArchLinux: [最新发行版](https://aur.archlinux.org/packages/zeronet-conservancy),[最新git版](https://aur.archlinux.org/packages/zeronet-conservancy-git)
+
+### 从Nix包管理器安装(Linux 或 MacOS)
+
+- 安装并配置nix包管理器(如果需要的话)
+- `nix-env -iA nixpkgs.zeronet-conservancy`
+
+如果您使用的是NixOS系统,将`zeronet-conservancy`添加到系统配置中
+
+(感谢 @fgaz 制作并维护此软件包)
+
+### 从源代码安装
+
+#### 系统依赖项
+
+##### 通用的类Unix系统(包括MacOS X)
+
+安装autoconf和其他基本开发工具,python3和pip,然后看“构建python依赖”
+
+##### 基于Apt的系统(debian、ubuntu等)
+ - `sudo apt update`
+ - `sudo apt install git pkg-config libffi-dev python3-pip python3-venv python3-dev build-essential libtool`
+
+##### 基于Red Hat和Fedora的系统
+ - `yum install epel-release -y 2>/dev/null`
+ - `yum install git python3 python3-wheel`
+
+##### 基于Fedora系统的dandified包管理器
+ - `sudo dnf install git python3-pip python3-wheel -y`
+
+##### openSUSE
+ - `sudo zypper install python3-pip python3-setuptools python3-wheel`
+
+##### 基于Arch和Manjaro的系统
+ - `sudo pacman -S git python-pip -v --no-confirm`
+
+##### Android/Termux
+ - 安装 [Termux](https://termux.com/)(在Termux中可以通过以下命令安装包`pkg install <package-names>`)
+ - `pkg update`
+ - `pkg install python automake git binutils libtool`
+ - (在旧Android系统上可能还需安装这个)`pkg install openssl-tool libcrypt clang`
+ - (若安装了上述包仍在启动时遇到问题,请告诉我们)
+ - (可选)`pkg install tor`
+ - (可选)通过以下命令运行Tor:`tor --ControlPort 9051 --CookieAuthentication 1`(你可以通过右滑来打开新会话)
+
+#### 构建python依赖、虚拟环境、运行
+ - 克隆此repo(注意:在Android/Termux上应将其克隆到Termux的“home”文件夹中,这是因为虚拟环境无法位于`storage/`)
+ - `python3 -m venv venv`(创建python虚拟环境,命令末尾的`venv`只是个名称,若使用其他名称,需在后续命令中替换)
+ - `source venv/bin/activate`(激活环境)
+ - `python3 -m pip install -r requirements.txt`(安装依赖)
+ - `python3 zeronet.py`(**运行zeronet-conservancy!**)
+ - 在浏览器中打开登录页面:http://127.0.0.1:43110/
+ - 需要在新的终端再次启动的话,进入repo目录并执行:
+ - `source venv/bin/activate`
+ - `python3 zeronet.py`
+
+#### (可选用)在NixOS上
+
+- 克隆此repo
+- 进入已经安装过依赖的shell:`nix-shell '<nixpkgs>' -A zeronet-conservancy`
+- `./zeronet.py`
+
+#### (可选用)构建Docker镜像
+- 构建0net镜像:`docker build -t 0net-conservancy:latest . -f Dockerfile`
+- 或构建集成tor的0net镜像:`docker build -t 0net-conservancy:latest . -f Dockerfile.integrated_tor`
+- 运行它:`docker run --rm -it -v </path/to/0n/data/directory>:/app/data -p 43110:43110 -p 26552:26552 0net-conservancy:latest`
+- /path/to/0n/data/directory - 该目录保存所有数据,包括你的私钥证书。如果在生产模式下运行,请千万不要删除这个文件夹!
+- 也可以通过docker-compose运行:`docker compose up -d 0net-conservancy` 来启动两个容器——0net和tor各自运行。
+- 或者在同一个容器中运行0net和tor:`docker compose up -d 0net-tor` 。
+(请检查上述说明是否仍然可行)
+
+#### 可选用单行命令(提供者@ssdifnskdjfnsdjk)(全局安装Python依赖)
+
+克隆Github仓库并安装所需的Python模块。首先编辑命令开头的zndir,将其改为你想要存储`zeronet-conservancy`的路径:
+
+`zndir="/home/user/myapps/zeronet" ; if [[ ! -d "$zndir" ]]; then git clone --recursive "https://github.com/zeronet-conservancy/zeronet-conservancy.git" "$zndir" && cd "$zndir"||exit; else cd "$zndir";git pull origin master; fi; cd "$zndir" && pip install -r requirements.txt|grep -v "already satisfied"; echo "Try to run: python3 $(pwd)/zeronet.py"`
+
+(此命令也可用于保持`zeronet-conservancy`是最新版本)
+
+#### 可选用脚本
+ - 安装通用依赖和克隆repo(如上所述)后,
+   运行`start-venv.sh`,它将为你创建虚拟环境并安装Python依赖
+ - 以后会添加更多便捷脚本
+
+### (非官方的)Windows系统构建版本
+
+下载并解压以下.zip压缩包
+[zeronet-conservancy-0.7.10-unofficial-win64.zip](https://github.com/zeronet-conservancy/zeronet-conservancy/releases/download/v0.7.10/zeronet-conservancy-0.7.10-unofficial-win64.zip)
+
+### 在Windows操作系统下构建
+
+(这些说明正在测试当中,请帮助我们测试并改进它!)
+
+- 安装Python: https://www.python.org/downloads/
+- 安装在Windows下适用于Python的编译器,这对我这种非Windows用户来说是最难的一步(详见 https://wiki.python.org/moin/WindowsCompilers ,之后我会提供更多参考)
+- [可选,如需获取最新开发版本]安装Git:https://git-scm.com/downloads
+- [可选,如需为更好的连接性和匿名性来使用tor]安装tor浏览器:https://www.torproject.org/download/ 
+- 打开git bash控制台
+- 在命令行输入或粘贴`git clone https://github.com/zeronet-conservancy/zeronet-conservancy.git`
+- 等待,直至git下载好最新的开发版本,然后继续在控制台操作
+- `cd zeronet-conservancy`
+- `python -m venv venv`(创建Python虚拟环境)
+- `venv\Scripts\activate`(激活环境)
+- `pip install -r requirements.txt`(安装Python依赖)(有些使用者反馈此命令未能成功安装所有依赖,需手动逐个安装依赖)
+- (注意:如果上一步失败了,可能说明C/C++编译器未能成功安装)
+- [可选,为更好的连接性和匿名性]启动Tor浏览器
+- (注意:Windows可能会弹窗,提示出于“安全原因”阻止了网络访问—请选择允许访问)
+- `python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151`(启动zeronet-conservancy!)
+- [需要完全匿名,要用如下方式启动]`python zeronet.py --tor_proxy 127.0.0.1:9150 --tor_controller 127.0.0.1:9151 --tor always`
+- 在浏览器中访问http://127.0.0.1:43110
+
+构建.exe文件
+
+- 按照上述步骤操作完成后,再执行
+- `pip install pyinstaller`
+- `pyinstaller -p src -p plugins --hidden-import merkletools --hidden-import lib.bencode_open --hidden-import Crypt.Crypt --hidden-import Db.DbQuery --hidden-import lib.subtl --hidden-import lib.subtl.subtl --hidden-import sockshandler --add-data "src;src" --add-data "plugins;plugins" --clean zeronet.py`
+- dist/zeronet 路径下会包含可用的 zeronet.exe!
+
+## 目前的局限性
+
+* 文件传输未压缩
+* 不支持私密站点
+* 不支持DHT
+* 不支持I2P
+* 不支持中心化的模块,比如zeroid(我们正在改进!)
+* 尚无可靠的垃圾信息保护措施(我们也在改进)
+* 无法直接在浏览器中使用(中长期会优先解决此问题)
+* 不支持数据透明
+* 尚无可重现构建
+* 不支持磁盘加密存储
+* 尚无可重现构建(因此在部分GNU/Linux发行版之外无法构建)
 
 
-## 现有限制
+## 我该如何创建一个ZeroNet站点?
 
-* ~~没有类似于 torrent 的文件拆分来支持大文件~~ (已添加大文件支持)
-* ~~没有比 BitTorrent 更好的匿名性~~ (已添加内置的完整 Tor 支持)
-* 传输文件时没有压缩~~和加密~~ (已添加 TLS 支持)
-* 不支持私有站点
+ * 点击[仪表盘](http://127.0.0.1:43110/191CazMVNaAcT9Y1zhkxd9ixMBPs59g2um/)菜单中的 **⋮** > **"Create new, empty site"** 。
+ * 你将被**重定向**到一个只有你可以修改的全新站点!
+ * 你可以在 **data/[你的站点地址]** 目录中找到并修改你的站点内容。
+ * 修改完成后,打开你的站点,向左拖动页面右上角的“0”按钮,然后点击底部的**sign and publish**按钮。
 
+下一步请看:[ZeroNet开发者文档](https://zeronet.io/docs/site_development/getting_started/)
 
-## 如何创建一个 ZeroNet 站点?
+## 帮助此项目持续发展
 
- * 点击 [ZeroHello](http://127.0.0.1:43110/126NXcevn1AUehWFZLTBw7FrX1crEizQdr) 站点的 **⋮** > **「新建空站点」** 菜单项
- * 您将被**重定向**到一个全新的站点,该站点只能由您修改
- * 您可以在 **data/[您的站点地址]** 目录中找到并修改网站的内容
- * 修改后打开您的网站,将右上角的「0」按钮拖到左侧,然后点击底部的**签名**并**发布**按钮
+### 成为维护者
 
-接下来的步骤:[ZeroNet 开发者文档](https://zeronet.io/docs/site_development/getting_started/)
+我们需要更多的维护者!今天就加入吧!你不需要懂编程,这里还有很多其他工作要做。
 
-## 帮助这个项目
+### 为你的平台创建构建包
 
-- Bitcoin: 1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6
+我们需要为各个主流操作系统平台提供独立构建的版本,并存入各种FLOSS仓库。如果你正在使用的Linux发行版还没有得到我们提供的相应构建包,为何不自己创建一个,或者(如果你不知道怎么做)去询问你的系统维护者呢?
 
-#### 感谢您!
+### 修复bug & 添加功能
 
-* 更多信息,帮助,变更记录和 zeronet 站点:https://www.reddit.com/r/zeronet/
+我们决心继续前进,打造一个完美的p2p网络,因此我们需要更多的帮助来实现它。
+
+### 创建你的站点/带来你的内容
+
+我们知道这份文档有所欠缺,但我们尽力支持任何希望迁移站点和内容的用户。请不要犹豫,随时联系我们。
+
+### 使用并传播
+
+告诉大家你为何选择使用0net及我们这个特别的分支!人们需要知道他们的替代方案。
+
+### 资金支持我们
+
+此分支由@caryoscelus创建并维护。你可以在 https://caryoscelus.github.io/donate/ 上查看捐赠方式(如果在github上阅读,也可查看侧边栏了解更多途径)。随着我们团队的不断壮大,我们也会在更友善的众筹平台上创建团队账户。
+
+如果你想确保你的捐赠会被视作对本项目的支持,下面是一个专门的比特币地址:
+1Kjuw3reZvxRVNs27Gen7jPJYCn6LY7Fg6。如需更匿名和私密的方式,以下是Monero钱包地址:
+4AiYUcqVRH4C2CVr9zbBdkhRnJnHiJoypHEsq4N7mQziGUoosPCpPeg8SPr87nvwypaRzDgMHEbWWDekKtq8hm9LBmgcMzC
+
+如果你希望通过其他方式捐赠,请随时联系维护者或创建一个issue。
+
+# 我们在抗议的情况下使用GitHub
+
+该项目当前托管在GitHub。但这并不理想;GitHub是一个专有的、有其商业秘密的系统,不是自由及开放源代码软件(FLOSS)。我们对使用像GitHub这样的专有系统来开发我们的FLOSS项目深感担忧。我们有一个[开放的议题页面](https://github.com/zeronet-conservancy/zeronet-conservancy/issues/89)来追踪从长远来看迁移出GitHub的进展。我们强烈建议你阅读[放弃 GitHub](https://GiveUpGitHub.org)运动以及[软件自由保护协会](https://sfconservancy.org)对其的阐述,以便了解为什么GitHub不适合托管FOSS项目。
+
+如果你是已经停止使用GitHub的贡献者,欢迎[在notabug上查看我们的镜像](https://notabug.org/caryoscelus/zeronet-conservancy),并在那里进行开发,或者通过[联系方式](https://caryoscelus.github.io/contacts/)直接向项目维护者发送git补丁。
+
+无论过去还是现在,未经许可地通过GitHub Copilot使用本项目的代码的行为必须立即停止。我们不允许GitHub将本项目代码用于Copilot。
+
+![GiveUpGitHub运动的标志](https://sfconservancy.org/img/GiveUpGitHub.png)

From d0d766f4dfbb96e16fcf3e57eaf217794bb9420d Mon Sep 17 00:00:00 2001
From: momordica2020 <287859992@qq.com>
Date: Sat, 26 Oct 2024 11:33:16 +0800
Subject: [PATCH 333/333] Update README-zh-cn.md

---
 README-zh-cn.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README-zh-cn.md b/README-zh-cn.md
index eee1e863..34f47a45 100644
--- a/README-zh-cn.md
+++ b/README-zh-cn.md
@@ -4,7 +4,7 @@
 
 **(注意:本文档的翻译版本通常滞后于最新内容)**
 
-[по-русски](README-ru.md) | [em português](README-ptbr.md) | [简体中文](README-zh-cn.md) | [日本語](README-ja.md)
+[in English](README.md) | [em português](README-ptbr.md) | [по-русски](README-ru.md) | [日本語](README-ja.md)
 
 `zeronet-conservancy` 是 [零网 ZeroNet](https://github.com/HelloZeroNet/ZeroNet) 项目的一个分支/延续(其创建者已放弃维护),致力于维持现有的p2p网络,进一步发扬去中心化与自由的价值观,并逐步得到一个设计得更好的网络。