merge contacts

master
Kevin Froman 2019-02-17 15:47:06 -06:00
commit fc6dc0caf7
112 changed files with 1567 additions and 3821 deletions

0
.dockerignore Normal file → Executable file
View File

0
.gitignore vendored Normal file → Executable file
View File

7
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,7 @@
test:
image: ubuntu:bionic
script:
- apt-get update -qy
- apt-get install -y python3-pip tor
- pip3 install -r requirements.txt
- make test

0
.gitmodules vendored Normal file → Executable file
View File

2
CODE_OF_CONDUCT.md Normal file → Executable file
View File

@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beardog@firemail.cc. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beardog at mailbox.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

0
CONTRIBUTING.md Normal file → Executable file
View File

0
Dockerfile Normal file → Executable file
View File

0
ISSUE_TEMPLATE.md Normal file → Executable file
View File

0
LICENSE.txt Normal file → Executable file
View File

14
Makefile Normal file → Executable file
View File

@ -18,26 +18,20 @@ uninstall:
rm -f $(DESTDIR)$(PREFIX)/bin/onionr
test:
@./onionr.sh stop
@sleep 1
@rm -rf onionr/data-backup
@mv onionr/data onionr/data-backup | true > /dev/null 2>&1
-@cd onionr; ./tests.py;
@rm -rf onionr/data
@mv onionr/data-backup onionr/data | true > /dev/null 2>&1
./run_tests.sh
soft-reset:
@echo "Soft-resetting Onionr..."
rm -f onionr/data/blocks/*.dat onionr/data/*.db onionr/data/block-nonces.dat | true > /dev/null 2>&1
rm -f onionr/$(ONIONR_HOME)/blocks/*.dat onionr/data/*.db onionr/$(ONIONR_HOME)/block-nonces.dat | true > /dev/null 2>&1
@./onionr.sh version | grep -v "Failed" --color=always
reset:
@echo "Hard-resetting Onionr..."
rm -rf onionr/data/ | true > /dev/null 2>&1
rm -rf onionr/$(ONIONR_HOME)/ | true > /dev/null 2>&1
cd onionr/static-data/www/ui/; rm -rf ./dist; python compile.py
#@./onionr.sh.sh version | grep -v "Failed" --color=always
plugins-reset:
@echo "Resetting plugins..."
rm -rf onionr/data/plugins/ | true > /dev/null 2>&1
rm -rf onionr/$(ONIONR_HOME)/plugins/ | true > /dev/null 2>&1
@./onionr.sh version | grep -v "Failed" --color=always

2
README.md Normal file → Executable file
View File

@ -11,6 +11,7 @@
(***pre-alpha & experimental, not well tested or easy to use yet***)
[![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/)
<img src='https://gitlab.com/beardog/Onionr/badges/master/build.svg'>
<hr>
@ -60,6 +61,7 @@ Everyone is welcome to help out. Help is wanted for the following:
* Testing
* Running stable nodes
* Security review/audit
* Automatic I2P setup
Bitcoin: [1onion55FXzm6h8KQw3zFw2igpHcV7LPq](bitcoin:1onion55FXzm6h8KQw3zFw2igpHcV7LPq)
USD: [Ko-Fi](https://www.ko-fi.com/beardogkf)

0
docs/api.md Normal file → Executable file
View File

0
docs/onionr-logo.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

0
docs/onionr-web.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

0
docs/whitepaper.md Normal file → Executable file
View File

0
onionr/__init__.py Normal file
View File

View File

@ -19,9 +19,7 @@
'''
from gevent.pywsgi import WSGIServer, WSGIHandler
from gevent import Timeout
#import gevent.monkey
#gevent.monkey.patch_socket()
import flask, cgi
import flask, cgi, uuid
from flask import request, Response, abort, send_from_directory
import sys, random, threading, hmac, hashlib, base64, time, math, os, json, socket
import core
@ -29,34 +27,15 @@ from onionrblockapi import Block
import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionr
class FDSafeHandler(WSGIHandler):
'''Our WSGI handler. Doesn't do much non-default except timeouts'''
def handle(self):
timeout = Timeout(60, exception=Exception)
timeout.start()
#timeout = gevent.Timeout.start_new(3)
try:
WSGIHandler.handle(self)
except Timeout as ex:
raise
def guessMime(path):
'''
Guesses the mime type of a file from the input filename
'''
mimetypes = {
'html' : 'text/html',
'js' : 'application/javascript',
'css' : 'text/css',
'png' : 'image/png',
'jpg' : 'image/jpeg'
}
for mimetype in mimetypes:
if path.endswith('.%s' % mimetype):
return mimetypes[mimetype]
return 'text/plain'
def setBindIP(filePath):
'''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)'''
hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))]
@ -67,6 +46,7 @@ def setBindIP(filePath):
try:
s.bind((data, 0))
except OSError:
# if mac/non-bindable, show warning and default to 127.0.0.1
logger.warn('Your platform appears to not support random local host addresses 127.x.x.x. Falling back to 127.0.0.1.')
data = '127.0.0.1'
s.close()
@ -89,33 +69,42 @@ class PublicAPI:
self.torAdder = clientAPI._core.hsAddress
self.i2pAdder = clientAPI._core.i2pAddress
self.bindPort = config.get('client.public.port')
self.lastRequest = 0
logger.info('Running public api on %s:%s' % (self.host, self.bindPort))
@app.before_request
def validateRequest():
'''Validate request has the correct hostname'''
# If high security level, deny requests to public
# If high security level, deny requests to public (HS should be disabled anyway for Tor, but might not be for I2P)
if config.get('general.security_level', default=0) > 0:
abort(403)
if type(self.torAdder) is None and type(self.i2pAdder) is None:
# abort if our hs addresses are not known
abort(403)
if request.host not in (self.i2pAdder, self.torAdder):
# Disallow connection if wrong HTTP hostname, in order to prevent DNS rebinding attacks
abort(403)
@app.after_request
def sendHeaders(resp):
'''Send api, access control headers'''
resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch.
resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch, since we can't fully remove the header.
# CSP to prevent XSS. Mainly for client side attacks (if hostname protection could somehow be bypassed)
resp.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'none'; object-src 'none'; style-src data: 'unsafe-inline'; img-src data:; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'none'"
# Prevent click jacking
resp.headers['X-Frame-Options'] = 'deny'
# No sniff is possibly not needed
resp.headers['X-Content-Type-Options'] = "nosniff"
# Network API version
resp.headers['X-API'] = onionr.API_VERSION
# Close connections to limit FD use
resp.headers['Connection'] = "close"
self.lastRequest = clientAPI._core._utils.getRoundedEpoch(roundS=5)
return resp
@app.route('/')
def banner():
# Display a bit of information to people who visit a node address in their browser
try:
with open('static-data/index.html', 'r') as html:
resp = Response(html.read(), mimetype='text/html')
@ -125,41 +114,48 @@ class PublicAPI:
@app.route('/getblocklist')
def getBlockList():
# Provide a list of our blocks, with a date offset
dateAdjust = request.args.get('date')
bList = clientAPI._core.getBlockList(dateRec=dateAdjust)
for b in self.hideBlocks:
if b in bList:
# Don't share blocks we created if they haven't been *uploaded* yet, makes it harder to find who created a block
bList.remove(b)
return Response('\n'.join(bList))
@app.route('/getdata/<name>')
def getBlockData(name):
# Share data for a block if we have it
resp = ''
data = name
if clientAPI._utils.validateHash(data):
if data not in self.hideBlocks:
if data in clientAPI._core.getBlockList():
block = clientAPI.getBlockData(data, raw=True).encode()
resp = base64.b64encode(block).decode()
block = clientAPI.getBlockData(data, raw=True)
try:
block = block.encode()
except AttributeError:
abort(404)
block = clientAPI._core._utils.strToBytes(block)
resp = block
#resp = base64.b64encode(block).decode()
if len(resp) == 0:
abort(404)
resp = ""
return Response(resp)
return Response(resp, mimetype='application/octet-stream')
@app.route('/www/<path:path>')
def wwwPublic(path):
# A way to share files directly over your .onion
if not config.get("www.public.run", True):
abort(403)
return send_from_directory(config.get('www.public.path', 'static-data/www/public/'), path)
@app.route('/ping')
def ping():
# Endpoint to test if nodes are up
return Response("pong!")
@app.route('/getdbhash')
def getDBHash():
return Response(clientAPI._utils.getBlockDBHash())
@app.route('/pex')
def peerExchange():
response = ','.join(clientAPI._core.listAdders(recent=3600))
@ -194,11 +190,9 @@ class PublicAPI:
except AttributeError:
pass
if powHash.startswith('0000'):
try:
newNode = newNode.decode()
except AttributeError:
pass
if clientAPI._core.addAddress(newNode):
newNode = clientAPI._core._utils.bytesToStr(newNode)
if clientAPI._core._utils.validateID(newNode) and not newNode in clientAPI._core.onionrInst.communicatorInst.newPeers:
clientAPI._core.onionrInst.communicatorInst.newPeers.append(newNode)
resp = 'Success'
else:
logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash)
@ -207,6 +201,9 @@ class PublicAPI:
@app.route('/upload', methods=['post'])
def upload():
'''Accept file uploads. In the future this will be done more often than on creation
to speed up block sync
'''
resp = 'failure'
try:
data = request.form['block']
@ -228,6 +225,7 @@ class PublicAPI:
resp = Response(resp)
return resp
# Set instances, then startup our public api server
clientAPI.setPublicAPIInstance(self)
while self.torAdder == '':
clientAPI._core.refreshFirstStartVars()
@ -255,7 +253,6 @@ class API:
onionr.Onionr.setupConfig('data/', self = self)
self.debug = debug
self._privateDelayTime = 3
self._core = onionrInst.onionrCore
self.startTime = self._core._utils.getEpoch()
self._crypto = onionrcrypto.OnionrCrypto(self._core)
@ -264,7 +261,7 @@ class API:
bindPort = int(config.get('client.client.port', 59496))
self.bindPort = bindPort
# Be extremely mindful of this
# Be extremely mindful of this. These are endpoints available without a password
self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent', 'sharedContent', 'mail', 'mailindex')
self.clientToken = config.get('client.webpassword')
@ -276,12 +273,14 @@ class API:
logger.info('Running api on %s:%s' % (self.host, self.bindPort))
self.httpServer = ''
self.pluginResponses = {} # Responses for plugin endpoints
self.queueResponse = {}
onionrInst.setClientAPIInst(self)
@app.before_request
def validateRequest():
'''Validate request has set password and is the correct hostname'''
# For the purpose of preventing DNS rebinding attacks
if request.host != '%s:%s' % (self.host, self.bindPort):
abort(403)
if request.endpoint in self.whitelistEndpoints:
@ -294,11 +293,13 @@ class API:
@app.after_request
def afterReq(resp):
#resp.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'none'; object-src 'none'; style-src data: 'unsafe-inline'; img-src data:; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'none'"
# Security headers
if request.endpoint == 'site':
resp.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:"
else:
resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'"
resp.headers['X-Frame-Options'] = 'deny'
resp.headers['X-Content-Type-Options'] = "nosniff"
resp.headers['X-API'] = onionr.API_VERSION
resp.headers['Server'] = ''
resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch.
resp.headers['Connection'] = "close"
@ -330,11 +331,13 @@ class API:
@app.route('/queueResponseAdd/<name>', methods=['post'])
def queueResponseAdd(name):
# Responses from the daemon. TODO: change to direct var access instead of http endpoint
self.queueResponse[name] = request.form['data']
return Response('success')
@app.route('/queueResponse/<name>')
def queueResponse(name):
# Fetch a daemon queue response
resp = 'failure'
try:
resp = self.queueResponse[name]
@ -346,10 +349,12 @@ class API:
@app.route('/ping')
def ping():
# Used to check if client api is working
return Response("pong!")
@app.route('/', endpoint='onionrhome')
def hello():
# ui home
return send_from_directory('static-data/www/private/', 'index.html')
@app.route('/getblocksbytype/<name>')
@ -357,12 +362,13 @@ class API:
blocks = self._core.getBlocksByType(name)
return Response(','.join(blocks))
@app.route('/gethtmlsafeblockdata/<name>')
def getSafeData(name):
@app.route('/getblockbody/<name>')
def getBlockBodyData(name):
resp = ''
if self._core._utils.validateHash(name):
try:
resp = cgi.escape(Block(name).bcontent, quote=True)
resp = Block(name, decrypt=True).bcontent
#resp = cgi.escape(Block(name, decrypt=True).bcontent, quote=True)
except TypeError:
pass
else:
@ -384,6 +390,15 @@ class API:
abort(404)
return Response(resp)
@app.route('/getblockheader/<name>')
def getBlockHeader(name):
resp = self.getBlockData(name, decrypt=True, headerOnly=True)
return Response(resp)
@app.route('/lastconnect')
def lastConnect():
return Response(str(self.publicAPI.lastRequest))
@app.route('/site/<name>', endpoint='site')
def site(name):
bHash = name
@ -402,7 +417,8 @@ class API:
return Response(resp)
@app.route('/waitforshare/<name>', methods=['post'])
def waitforshare():
def waitforshare(name):
'''Used to prevent the **public** api from sharing blocks we just created'''
assert name.isalnum()
if name in self.publicAPI.hideBlocks:
self.publicAPI.hideBlocks.remove(name)
@ -428,6 +444,7 @@ class API:
@app.route('/getstats')
def getStats():
# returns node stats
#return Response("disabled")
while True:
try:
@ -439,6 +456,78 @@ class API:
def showUptime():
return Response(str(self.getUptime()))
@app.route('/getActivePubkey')
def getActivePubkey():
return Response(self._core._crypto.pubKey)
@app.route('/getHumanReadable/<name>')
def getHumanReadable(name):
return Response(self._core._utils.getHumanReadableID(name))
@app.route('/insertblock', methods=['POST'])
def insertBlock():
encrypt = False
bData = request.get_json(force=True)
message = bData['message']
subject = 'temp'
encryptType = ''
sign = True
meta = {}
to = ''
try:
if bData['encrypt']:
to = bData['to']
encrypt = True
encryptType = 'asym'
except KeyError:
pass
try:
if not bData['sign']:
sign = False
except KeyError:
pass
try:
bType = bData['type']
except KeyError:
bType = 'bin'
try:
meta = json.loads(bData['meta'])
except KeyError:
pass
threading.Thread(target=self._core.insertBlock, args=(message,), kwargs={'header': bType, 'encryptType': encryptType, 'sign':sign, 'asymPeer': to, 'meta': meta}).start()
return Response('success')
@app.route('/apipoints/<path:subpath>', methods=['POST', 'GET'])
def pluginEndpoints(subpath=''):
'''Send data to plugins'''
# TODO have a variable for the plugin to set data to that we can use for the response
pluginResponseCode = str(uuid.uuid4())
resp = 'success'
responseTimeout = 20
startTime = self._core._utils.getEpoch()
postData = {}
if request.method == 'POST':
postData = request.form['postData']
if len(subpath) > 1:
data = subpath.split('/')
if len(data) > 1:
plName = data[0]
events.event('pluginRequest', {'name': plName, 'path': subpath, 'pluginResponse': pluginResponseCode, 'postData': postData}, onionr=onionrInst)
while True:
try:
resp = self.pluginResponses[pluginResponseCode]
except KeyError:
time.sleep(0.2)
if self._core._utils.getEpoch() - startTime > responseTimeout:
abort(504)
break
else:
break
else:
abort(404)
return Response(resp)
self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler)
self.httpServer.serve_forever()
@ -448,7 +537,7 @@ class API:
def validateToken(self, token):
'''
Validate that the client token matches the given token
Validate that the client token matches the given token. Used to prevent CSRF and data exfiltration
'''
if len(self.clientToken) == 0:
logger.error("client password needs to be set")
@ -469,7 +558,8 @@ class API:
# Don't error on race condition with startup
pass
def getBlockData(self, bHash, decrypt=False, raw=False):
def getBlockData(self, bHash, decrypt=False, raw=False, headerOnly=False):
assert self._core._utils.validateHash(bHash)
bl = Block(bHash, core=self._core)
if decrypt:
bl.decrypt()
@ -477,12 +567,22 @@ class API:
raise ValueError
if not raw:
if not headerOnly:
retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent}
for x in list(retData.keys()):
try:
retData[x] = retData[x].decode()
except AttributeError:
pass
else:
validSig = False
signer = self._core._utils.bytesToStr(bl.signer)
#print(signer, bl.isSigned(), self._core._utils.validatePubKey(signer), bl.isSigner(signer))
if bl.isSigned() and self._core._utils.validatePubKey(signer) and bl.isSigner(signer):
validSig = True
bl.bheader['validSig'] = validSig
bl.bheader['meta'] = ''
retData = {'meta': bl.bheader, 'metadata': bl.bmetadata}
return json.dumps(retData)
else:
return bl.raw

View File

@ -1,75 +0,0 @@
'''
Onionr - P2P Anonymous Storage Network
Handles api data exchange, interfaced by both public and client http api
'''
'''
This program 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.
This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
'''
import config, apipublic, apiprivate, core, socket, random, threading, time
config.reload()
PRIVATE_API_VERSION = 0
PUBLIC_API_VERSION = 1
DEV_MODE = config.get('general.dev_mode')
def getOpenPort():
'''Get a random open port'''
p = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
p.bind(("127.0.0.1",0))
p.listen(1)
port = p.getsockname()[1]
p.close()
return port
def getRandomLocalIP():
'''Get a random local ip address'''
hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))]
host = '.'.join(hostOctets)
return host
class APIManager:
def __init__(self, coreInst):
assert isinstance(coreInst, core.Core)
self.core = coreInst
self.utils = coreInst._utils
self.crypto = coreInst._crypto
# if this gets set to true, both the public and private apis will shutdown
self.shutdown = False
publicIP = '127.0.0.1'
privateIP = '127.0.0.1'
if DEV_MODE:
# set private and local api servers bind IPs to random localhost (127.x.x.x), make sure not the same
privateIP = getRandomLocalIP()
while True:
publicIP = getRandomLocalIP()
if publicIP != privateIP:
break
# Make official the IPs and Ports
self.publicIP = publicIP
self.privateIP = privateIP
self.publicPort = config.get('client.port', 59496)
self.privatePort = config.get('client.port', 59496)
# Run the API servers in new threads
self.publicAPI = apipublic.APIPublic(self)
self.privateAPI = apiprivate.APIPrivate(self)
threading.Thread(target=self.publicAPI.run).start()
threading.Thread(target=self.privateAPI.run).start()
while not self.shutdown:
time.sleep(1)

View File

@ -1,41 +0,0 @@
'''
Onionr - P2P Anonymous Storage Network
Handle incoming commands from other Onionr nodes, over HTTP
'''
'''
This program 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.
This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
'''
import flask, apimanager
from flask import request, Response, abort, send_from_directory
from gevent.pywsgi import WSGIServer
class APIPublic:
def __init__(self, managerInst):
assert isinstance(managerInst, apimanager.APIManager)
app = flask.Flask(__name__)
@app.route('/')
def banner():
try:
with open('static-data/index.html', 'r') as html:
resp = Response(html.read(), mimetype='text/html')
except FileNotFoundError:
resp = Response("")
return resp
self.httpServer = WSGIServer((managerInst.publicIP, managerInst.publicPort), app)
def run(self):
self.httpServer.serve_forever()
return

0
onionr/blockimporter.py Normal file → Executable file
View File

View File

@ -21,10 +21,12 @@
'''
import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid
import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block
import onionrdaemontools, onionrsockets, onionr, onionrproofs, proofofmemory
import onionrdaemontools, onionrsockets, onionr, onionrproofs
import binascii
from dependencies import secrets
from defusedxml import minidom
from utils import networkmerger
config.reload()
class OnionrCommunicatorDaemon:
def __init__(self, onionrInst, proxyPort, developmentMode=config.get('general.dev_mode', False)):
@ -38,11 +40,11 @@ class OnionrCommunicatorDaemon:
# list of timer instances
self.timers = []
# initalize core with Tor socks port being 3rd argument
# initialize core with Tor socks port being 3rd argument
self.proxyPort = proxyPort
self._core = onionrInst.onionrCore
# intalize NIST beacon salt and time
# initialize NIST beacon salt and time
self.nistSaltTimestamp = 0
self.powSalt = 0
@ -57,11 +59,12 @@ class OnionrCommunicatorDaemon:
self.cooldownPeer = {}
self.connectTimes = {}
self.peerProfiles = [] # list of peer's profiles (onionrpeers.PeerProfile instances)
self.newPeers = [] # Peers merged to us. Don't add to db until we know they're reachable
# amount of threads running by name, used to prevent too many
self.threadCounts = {}
# set true when shutdown command recieved
# set true when shutdown command received
self.shutdown = False
# list of new blocks to download, added to when new block lists are fetched from peers
@ -130,7 +133,6 @@ class OnionrCommunicatorDaemon:
self.socketServer.start()
self.socketClient = onionrsockets.OnionrSocketClient(self._core)
# Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking
try:
while not self.shutdown:
@ -155,11 +157,27 @@ class OnionrCommunicatorDaemon:
'''Lookup new peer addresses'''
logger.info('Looking up new addresses...')
tryAmount = 1
newPeers = []
for i in range(tryAmount):
# Download new peer address list from random online peers
if len(newPeers) > 10000:
# Dont get new peers if we have too many queued up
break
peer = self.pickOnlinePeer()
newAdders = self.peerAction(peer, action='pex')
self._core._utils.mergeAdders(newAdders)
try:
newPeers = newAdders.split(',')
except AttributeError:
pass
else:
# Validate new peers are good format and not already in queue
invalid = []
for x in newPeers:
if not self._core._utils.validateID(x) or x in self.newPeers:
invalid.append(x)
for x in invalid:
newPeers.remove(x)
self.newPeers.extend(newPeers)
self.decrementThreadCount('lookupAdders')
def lookupBlocks(self):
@ -188,12 +206,8 @@ class OnionrCommunicatorDaemon:
break
else:
continue
newDBHash = self.peerAction(peer, 'getdbhash') # get their db hash
if newDBHash == False or not self._core._utils.validateHash(newDBHash):
continue # if request failed, restart loop (peer is added to offline peers automatically)
triedPeers.append(peer)
if newDBHash != self._core.getAddressInfo(peer, 'DBHash'):
self._core.setAddressInfo(peer, 'DBHash', newDBHash)
# Get the last time we looked up a peer's stamp to only fetch blocks since then.
# Saved in memory only for privacy reasons
try:
@ -222,6 +236,7 @@ class OnionrCommunicatorDaemon:
self.blockQueue[i] = [peer] # add blocks to download queue
else:
if peer not in self.blockQueue[i]:
if len(self.blockQueue[i]) < 10:
self.blockQueue[i].append(peer)
self.decrementThreadCount('lookupBlocks')
return
@ -240,10 +255,10 @@ class OnionrCommunicatorDaemon:
break
# Do not download blocks being downloaded or that are already saved (edge cases)
if blockHash in self.currentDownloading:
logger.debug('Already downloading block %s...' % blockHash)
#logger.debug('Already downloading block %s...' % blockHash)
continue
if blockHash in self._core.getBlockList():
logger.debug('Block %s is already saved.' % (blockHash,))
#logger.debug('Block %s is already saved.' % (blockHash,))
try:
del self.blockQueue[blockHash]
except KeyError:
@ -268,10 +283,7 @@ class OnionrCommunicatorDaemon:
content = content.encode()
except AttributeError:
pass
try:
content = base64.b64decode(content) # content is base64 encoded in transport
except binascii.Error:
pass
realHash = self._core._crypto.sha3Hash(content)
try:
realHash = realHash.decode() # bytes on some versions for some reason
@ -402,8 +414,18 @@ class OnionrCommunicatorDaemon:
else:
peerList = self._core.listAdders()
mainPeerList = self._core.listAdders()
peerList = onionrpeers.getScoreSortedPeerList(self._core)
if len(peerList) < 8 or secrets.randbelow(4) == 3:
tryingNew = []
for x in self.newPeers:
if x not in peerList:
peerList.append(x)
tryingNew.append(x)
for i in tryingNew:
self.newPeers.remove(i)
if len(peerList) == 0 or useBootstrap:
# Avoid duplicating bootstrap addresses in peerList
self.addBootstrapListToPeerList(peerList)
@ -418,6 +440,8 @@ class OnionrCommunicatorDaemon:
if self.peerAction(address, 'ping') == 'pong!':
logger.info('Connected to ' + address)
time.sleep(0.1)
if address not in mainPeerList:
networkmerger.mergeAdders(address, self._core)
if address not in self.onlinePeers:
self.onlinePeers.append(address)
self.connectTimes[address] = self._core._utils.getEpoch()
@ -588,7 +612,7 @@ class OnionrCommunicatorDaemon:
proxyType = 'i2p'
logger.info("Uploading block to " + peer)
if not self._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False:
self._core._utils.localCommand('waitforshare/' + bl)
self._core._utils.localCommand('waitforshare/' + bl, post=True)
finishedUploads.append(bl)
for x in finishedUploads:
try:
@ -605,8 +629,8 @@ class OnionrCommunicatorDaemon:
def detectAPICrash(self):
'''exit if the api server crashes/stops'''
if self._core._utils.localCommand('ping', silent=False) not in ('pong', 'pong!'):
for i in range(5):
if self._core._utils.localCommand('ping') in ('pong', 'pong!'):
for i in range(8):
if self._core._utils.localCommand('ping') in ('pong', 'pong!') or self.shutdown:
break # break for loop
time.sleep(1)
else:

0
onionr/config.py Normal file → Executable file
View File

84
onionr/core.py Normal file → Executable file
View File

@ -21,7 +21,8 @@ import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcon
from onionrblockapi import Block
import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions
import onionrblacklist, onionrusers
import onionrblacklist
from onionrusers import onionrusers
import dbcreator, onionrstorage, serializeddata
from etc import onionrvalues
@ -56,7 +57,7 @@ class Core:
self.privateApiHostFile = self.dataDir + 'private-host.txt'
self.addressDB = self.dataDir + 'address.db'
self.hsAddress = ''
self.i2pAddress = config.get('i2p.ownAddr', None)
self.i2pAddress = config.get('i2p.own_addr', None)
self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt'
self.bootstrapList = []
self.requirements = onionrvalues.OnionrValues()
@ -85,6 +86,10 @@ class Core:
self.createBlockDB()
if not os.path.exists(self.forwardKeysFile):
self.dbCreate.createForwardKeyDB()
if not os.path.exists(self.peerDB):
self.createPeerDB()
if not os.path.exists(self.addressDB):
self.createAddressDB()
if os.path.exists(self.dataDir + '/hs/hostname'):
with open(self.dataDir + '/hs/hostname', 'r') as hs:
@ -125,6 +130,7 @@ class Core:
'''
Adds a public key to the key database (misleading function name)
'''
assert peerID not in self.listPeers()
# This function simply adds a peer to the DB
if not self._utils.validatePubKey(peerID):
@ -221,18 +227,8 @@ class Core:
c.execute('Delete from hashes where hash=?;', t)
conn.commit()
conn.close()
blockFile = self.dataDir + '/blocks/%s.dat' % block
dataSize = 0
try:
''' Get size of data when loaded as an object/var, rather than on disk,
to avoid conflict with getsizeof when saving blocks
'''
with open(blockFile, 'r') as data:
dataSize = sys.getsizeof(data.read())
dataSize = sys.getsizeof(onionrstorage.getData(self, block))
self._utils.storageCounter.removeBytes(dataSize)
os.remove(blockFile)
except FileNotFoundError:
pass
def createAddressDB(self):
'''
@ -282,15 +278,6 @@ class Core:
Simply return the data associated to a hash
'''
'''
try:
# logger.debug('Opening %s' % (str(self.blockDataLocation) + str(hash) + '.dat'))
dataFile = open(self.blockDataLocation + hash + '.dat', 'rb')
data = dataFile.read()
dataFile.close()
except FileNotFoundError:
data = False
'''
data = onionrstorage.getData(self, hash)
return data
@ -316,9 +303,6 @@ class Core:
#raise Exception("Data is already set for " + dataHash)
else:
if self._utils.storageCounter.addBytes(dataSize) != False:
#blockFile = open(blockFileName, 'wb')
#blockFile.write(data)
#blockFile.close()
onionrstorage.store(self, data, blockHash=dataHash)
conn = sqlite3.connect(self.blockDB, timeout=30)
c = conn.cursor()
@ -557,19 +541,18 @@ class Core:
knownPeer text, 2
speed int, 3
success int, 4
DBHash text, 5
powValue 6
failure int 7
lastConnect 8
trust 9
introduced 10
powValue 5
failure int 6
lastConnect 7
trust 8
introduced 9
'''
conn = sqlite3.connect(self.addressDB, timeout=30)
c = conn.cursor()
command = (address,)
infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'DBHash': 5, 'powValue': 6, 'failure': 7, 'lastConnect': 8, 'trust': 9, 'introduced': 10}
infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'powValue': 5, 'failure': 6, 'lastConnect': 7, 'trust': 8, 'introduced': 9}
info = infoNumbers[info]
iterCount = 0
retVal = ''
@ -595,7 +578,7 @@ class Core:
command = (data, address)
if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'):
if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'):
raise Exception("Got invalid database key when setting address info")
else:
c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command)
@ -680,19 +663,6 @@ class Core:
conn.close()
return rows
def setBlockType(self, hash, blockType):
'''
Sets the type of block
'''
conn = sqlite3.connect(self.blockDB, timeout=30)
c = conn.cursor()
c.execute("UPDATE hashes SET dataType = ? WHERE hash = ?;", (blockType, hash))
conn.commit()
conn.close()
return
def updateBlockInfo(self, hash, key, data):
'''
sets info associated with a block
@ -731,6 +701,7 @@ class Core:
logger.error(allocationReachedMessage)
return False
retData = False
# check nonce
dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data))
try:
@ -746,6 +717,12 @@ class Core:
if type(data) is bytes:
data = data.decode()
data = str(data)
plaintext = data
plaintextMeta = {}
# Convert asym peer human readable key to base32 if set
if ' ' in asymPeer.strip():
asymPeer = self._utils.convertHumanReadableID(asymPeer)
retData = ''
signature = ''
@ -768,7 +745,7 @@ class Core:
pass
if encryptType == 'asym':
if not disableForward and asymPeer != self._crypto.pubKey:
if not disableForward and sign and asymPeer != self._crypto.pubKey:
try:
forwardEncrypted = onionrusers.OnionrUser(self, asymPeer).forwardEncrypt(data)
data = forwardEncrypted[0]
@ -780,6 +757,7 @@ class Core:
#fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys().reverse()
meta['newFSKey'] = fsKey
jsonMeta = json.dumps(meta)
plaintextMeta = jsonMeta
if sign:
signature = self._crypto.edSign(jsonMeta.encode() + data, key=self._crypto.privKey, encodeResult=True)
signer = self._crypto.pubKey
@ -802,10 +780,10 @@ class Core:
if self._utils.validatePubKey(asymPeer):
# Encrypt block data with forward secrecy key first, but not meta
jsonMeta = json.dumps(meta)
jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True, anonymous=True).decode()
data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True, anonymous=True).decode()
signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True, anonymous=True).decode()
signer = self._crypto.pubKeyEncrypt(signer, asymPeer, encodedData=True, anonymous=True).decode()
jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True).decode()
data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True).decode()
signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True).decode()
signer = self._crypto.pubKeyEncrypt(signer, asymPeer, encodedData=True).decode()
onionrusers.OnionrUser(self, asymPeer, saveUser=True)
else:
raise onionrexceptions.InvalidPubkey(asymPeer + ' is not a valid base32 encoded ed25519 key')
@ -832,14 +810,14 @@ class Core:
retData = False
else:
# Tell the api server through localCommand to wait for the daemon to upload this block to make stastical analysis more difficult
self._utils.localCommand('waitforshare/' + retData)
self._utils.localCommand('/waitforshare/' + retData, post=True)
self.addToBlockDB(retData, selfInsert=True, dataSaved=True)
#self.setBlockType(retData, meta['type'])
self._utils.processBlockMetadata(retData)
self.daemonQueueAdd('uploadBlock', retData)
if retData != False:
events.event('insertBlock', onionr = None, threaded = False)
events.event('insertblock', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = True)
return retData
def introduceNode(self):

View File

@ -1,35 +0,0 @@
#!/usr/bin/env python3
'''
Onionr - P2P Microblogging Platform & Social network
This program 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.
This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
'''
import unittest, sys, os, core, onionrcrypto, logger
class OnionrCryptoTests(unittest.TestCase):
def testSymmetric(self):
dataString = "this is a secret message"
dataBytes = dataString.encode()
myCore = core.Core()
crypto = onionrcrypto.OnionrCrypto(myCore)
key = key = b"tttttttttttttttttttttttttttttttt"
logger.info("Encrypting: " + dataString, timestamp=True)
encrypted = crypto.symmetricEncrypt(dataString, key, returnEncoded=True)
logger.info(encrypted, timestamp=True)
logger.info('Decrypting encrypted string:', timestamp=True)
decrypted = crypto.symmetricDecrypt(encrypted, key, encodedMessage=True)
logger.info(decrypted, timestamp=True)
self.assertTrue(True)
if __name__ == "__main__":
unittest.main()

1
onionr/dbcreator.py Normal file → Executable file
View File

@ -39,7 +39,6 @@ class DBCreator:
knownPeer text,
speed int,
success int,
DBHash text,
powValue text,
failure int,
lastConnect int,

0
onionr/dependencies/secrets.py Normal file → Executable file
View File

0
onionr/etc/onionrvalues.py Normal file → Executable file
View File

56
onionr/etc/pgpwords.py Normal file → Executable file
View File

@ -1,7 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- (because 0xFF, even : "Yucatán")
import os, re, sys
'''This file is adapted from https://github.com/thblt/pgp-words by github user 'thblt' ('Thibault Polge), GPL v3 license'''
import os, re, sys, binascii
_words = [
["aardvark", "adroitness"],
@ -259,7 +261,7 @@ _words = [
["wayside", "Wilmington"],
["willow", "Wyoming"],
["woodlark", "yesteryear"],
["Zulu", "Yucatán"]]
["Zulu", "Yucatan"]]
hexre = re.compile("[a-fA-F0-9]+")
@ -275,41 +277,21 @@ def wordify(seq):
ret = []
for i in range(0, len(seq), 2):
ret.append(_words[int(seq[i:i+2], 16)][(i//2)%2])
ret.append(_words[int(seq[i:i+2], 16)][(i//2)%2].lower())
return ret
def usage():
print("Usage:")
print(" {0} [fingerprint...]".format(os.path.basename(sys.argv[0])))
print("")
print("If called with multiple arguments, they will be concatenated")
print("and treated as a single fingerprint.")
print("")
print("If called with no arguments, input is read from stdin,")
print("and each line is treated as a single fingerprint. In this")
print("mode, invalid values are silently ignored.")
exit(1)
if __name__ == '__main__':
if 1 == len(sys.argv):
fps = sys.stdin.readlines()
else:
fps = [" ".join(sys.argv[1:])]
for fp in fps:
def hexify(seq, delim=' '):
ret = b''
sentence = seq
try:
words = wordify(fp)
print("\n{0}: ".format(fp.strip()))
sys.stdout.write("\t")
for i in range(0, len(words)):
sys.stdout.write(words[i] + " ")
if (not (i+1) % 4) and not i == len(words)-1:
sys.stdout.write("\n\t")
print("")
except Exception as e:
if len(fps) == 1:
print (e)
usage()
print("")
sentence = seq.split(delim)
except AttributeError:
pass
count = 0
for word in sentence:
count = 0
for wordPair in _words:
if word in wordPair:
ret += bytes([(count)])
count += 1
return binascii.hexlify(ret)

0
onionr/keymanager.py Normal file → Executable file
View File

5
onionr/logger.py Normal file → Executable file
View File

@ -18,7 +18,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import re, sys, time, traceback
import re, sys, time, traceback, os
class colors:
'''
@ -64,6 +64,9 @@ class colors:
'''
Use the bitwise operators to merge these settings
'''
if os.name == 'nt':
USE_ANSI = 0b000
else:
USE_ANSI = 0b100
OUTPUT_TO_CONSOLE = 0b010
OUTPUT_TO_FILE = 0b001

0
onionr/netcontroller.py Normal file → Executable file
View File

View File

@ -25,7 +25,7 @@ MIN_PY_VERSION = 6
if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION:
print('Error, Onionr requires Python 3.%s+' % (MIN_PY_VERSION,))
sys.exit(1)
import os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re, json, getpass, sqlite3
import os, base64, random, getpass, shutil, time, platform, datetime, re, json, getpass, sqlite3
import webbrowser, uuid, signal
from threading import Thread
import api, core, config, logger, onionrplugins as plugins, onionrevents as events
@ -33,17 +33,18 @@ import onionrutils
import netcontroller, onionrstorage
from netcontroller import NetController
from onionrblockapi import Block
import onionrproofs, onionrexceptions, onionrusers, communicator
import onionrproofs, onionrexceptions, communicator
from onionrusers import onionrusers
try:
from urllib3.contrib.socks import SOCKSProxyManager
except ImportError:
raise Exception("You need the PySocks module (for use with socks5 proxy to use Tor)")
ONIONR_TAGLINE = 'Anonymous P2P Platform - GPLv3 - https://Onionr.VoidNet.Tech'
ONIONR_TAGLINE = 'Anonymous P2P Platform - GPLv3 - https://Onionr.net'
ONIONR_VERSION = '0.5.0' # for debugging and stuff
ONIONR_VERSION_TUPLE = tuple(ONIONR_VERSION.split('.')) # (MAJOR, MINOR, VERSION)
API_VERSION = '5' # increments of 1; only change when something fundemental about how the API works changes. This way other nodes know how to communicate without learning too much information about you.
API_VERSION = '5' # increments of 1; only change when something fundamental about how the API works changes. This way other nodes know how to communicate without learning too much information about you.
class Onionr:
def __init__(self):
@ -110,12 +111,6 @@ class Onionr:
except:
plugins.disable(name, onionr = self, stop_event = False)
if not os.path.exists(self.onionrCore.peerDB):
self.onionrCore.createPeerDB()
pass
if not os.path.exists(self.onionrCore.addressDB):
self.onionrCore.createAddressDB()
# Get configuration
if type(config.get('client.webpassword')) is type(None):
config.set('client.webpassword', base64.b16encode(os.urandom(32)).decode('utf-8'), savefile=True)
@ -124,6 +119,7 @@ class Onionr:
config.set('client.client.port', randomPort, savefile=True)
if type(config.get('client.public.port')) is type(None):
randomPort = netcontroller.getOpenPort()
print(randomPort)
config.set('client.public.port', randomPort, savefile=True)
if type(config.get('client.participate')) is type(None):
config.set('client.participate', True, savefile=True)
@ -206,9 +202,6 @@ class Onionr:
'connect': self.addAddress,
'pex': self.doPEX,
'ui' : self.openUI,
'gui' : self.openUI,
'getpassword': self.printWebPassword,
'get-password': self.printWebPassword,
'getpwd': self.printWebPassword,
@ -297,8 +290,6 @@ class Onionr:
with open('%s/%s.dat' % (exportDir, bHash), 'wb') as exportFile:
exportFile.write(data)
def showDetails(self):
details = {
'Node Address' : self.get_hostname(),
@ -562,24 +553,12 @@ class Onionr:
if self.onionrUtils.hasKey(newPeer):
logger.info('We already have that key')
return
if not '-' in newPeer:
logger.info('Since no POW token was supplied for that key, one is being generated')
proof = onionrproofs.DataPOW(newPeer)
while True:
result = proof.getResult()
if result == False:
time.sleep(0.5)
else:
break
newPeer += '-' + base64.b64encode(result[1]).decode()
logger.info(newPeer)
logger.info("Adding peer: " + logger.colors.underline + newPeer)
if self.onionrUtils.mergeKeys(newPeer):
try:
if self.onionrCore.addPeer(newPeer):
logger.info('Successfully added key')
else:
except AssertionError:
logger.error('Failed to add key')
return
def addAddress(self):
@ -598,7 +577,6 @@ class Onionr:
logger.info("Successfully added address.")
else:
logger.warn("Unable to add address.")
return
def addMessage(self, header="txt"):
@ -774,7 +752,7 @@ class Onionr:
Onionr.setupConfig('data/', self = self)
if self._developmentMode:
logger.warn('DEVELOPMENT MODE ENABLED (LESS SECURE)', timestamp = False)
logger.warn('DEVELOPMENT MODE ENABLED (NOT RECOMMENDED)', timestamp = False)
net = NetController(config.get('client.public.port', 59497), apiServerIP=apiHost)
logger.debug('Tor is starting...')
if not net.startTor():
@ -985,7 +963,7 @@ class Onionr:
'''
self.addFile(singleBlock=True, blockType='html')
def addFile(self, singleBlock=False, blockType='txt'):
def addFile(self, singleBlock=False, blockType='bin'):
'''
Adds a file to the onionr network
'''
@ -1001,6 +979,7 @@ class Onionr:
try:
with open(filename, 'rb') as singleFile:
blockhash = self.onionrCore.insertBlock(base64.b64encode(singleFile.read()), header=blockType)
if len(blockhash) > 0:
logger.info('File %s saved in block %s' % (filename, blockhash))
except:
logger.error('Failed to save file in block.', timestamp = False)
@ -1030,7 +1009,6 @@ class Onionr:
settings = settings | logger.OUTPUT_TO_CONSOLE
if config.get('log.file.output', True):
settings = settings | logger.OUTPUT_TO_FILE
logger.set_file(config.get('log.file.path', '/tmp/onionr.log').replace('data/', dataDir))
logger.set_settings(settings)
if not self is None:
@ -1073,12 +1051,6 @@ class Onionr:
return data_exists
def openUI(self):
url = 'http://127.0.0.1:%s/ui/index.html?timingToken=%s' % (config.get('client.port', 59496), self.onionrUtils.getTimeBypassToken())
logger.info('Opening %s ...' % url)
webbrowser.open(url, new = 1, autoraise = True)
def header(self, message = logger.colors.fg.pink + logger.colors.bold + 'Onionr' + logger.colors.reset + logger.colors.fg.pink + ' has started.'):
if os.path.exists('static-data/header.txt') and logger.get_level() <= logger.LEVEL_INFO:
with open('static-data/header.txt', 'rb') as file:

3
onionr/onionrblacklist.py Normal file → Executable file
View File

@ -116,4 +116,7 @@ class OnionrBlackList:
return
insert = (hashed,)
blacklistDate = self._core._utils.getEpoch()
try:
self._dbExecute("INSERT INTO blacklist (hash, dataType, blacklistDate, expire) VALUES(?, ?, ?, ?);", (str(hashed), dataType, blacklistDate, expire))
except sqlite3.IntegrityError:
pass

16
onionr/onionrblockapi.py Normal file → Executable file
View File

@ -18,8 +18,9 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions, onionrusers
import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions
import json, os, sys, datetime, base64, onionrstorage
from onionrusers import onionrusers
class Block:
blockCacheOrder = list() # NEVER write your own code that writes to this!
@ -59,7 +60,7 @@ class Block:
self.update()
def decrypt(self, anonymous = True, encodedData = True):
def decrypt(self, encodedData = True):
'''
Decrypt a block, loading decrypted data into their vars
'''
@ -71,16 +72,16 @@ class Block:
# decrypt data
if self.getHeader('encryptType') == 'asym':
try:
self.bcontent = core._crypto.pubKeyDecrypt(self.bcontent, anonymous=anonymous, encodedData=encodedData)
bmeta = core._crypto.pubKeyDecrypt(self.bmetadata, anonymous=anonymous, encodedData=encodedData)
self.bcontent = core._crypto.pubKeyDecrypt(self.bcontent, encodedData=encodedData)
bmeta = core._crypto.pubKeyDecrypt(self.bmetadata, encodedData=encodedData)
try:
bmeta = bmeta.decode()
except AttributeError:
# yet another bytes fix
pass
self.bmetadata = json.loads(bmeta)
self.signature = core._crypto.pubKeyDecrypt(self.signature, anonymous=anonymous, encodedData=encodedData)
self.signer = core._crypto.pubKeyDecrypt(self.signer, anonymous=anonymous, encodedData=encodedData)
self.signature = core._crypto.pubKeyDecrypt(self.signature, encodedData=encodedData)
self.signer = core._crypto.pubKeyDecrypt(self.signer, encodedData=encodedData)
self.bheader['signer'] = self.signer.decode()
self.signedData = json.dumps(self.bmetadata) + self.bcontent.decode()
try:
@ -94,7 +95,8 @@ class Block:
logger.error(str(e))
pass
except nacl.exceptions.CryptoError:
logger.debug('Could not decrypt block. Either invalid key or corrupted data')
pass
#logger.debug('Could not decrypt block. Either invalid key or corrupted data')
else:
retData = True
self.decrypted = True

54
onionr/onionrcrypto.py Normal file → Executable file
View File

@ -18,7 +18,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.pwhash, nacl.utils, nacl.secret, os, binascii, base64, hashlib, logger, onionrproofs, time, math, sys, hmac
import onionrexceptions, keymanager
import onionrexceptions, keymanager, core
# secrets module was added into standard lib in 3.6+
if sys.version_info[0] == 3 and sys.version_info[1] < 6:
from dependencies import secrets
@ -94,46 +94,35 @@ class OnionrCrypto:
retData = key.sign(data).signature
return retData
def pubKeyEncrypt(self, data, pubkey, anonymous=True, encodedData=False):
def pubKeyEncrypt(self, data, pubkey, encodedData=False):
'''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)'''
retVal = ''
try:
pubkey = pubkey.encode()
except AttributeError:
pass
box = None
data = self._core._utils.strToBytes(data)
pubkey = nacl.signing.VerifyKey(pubkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_public_key()
if encodedData:
encoding = nacl.encoding.Base64Encoder
else:
encoding = nacl.encoding.RawEncoder
if self.privKey != None and not anonymous:
ownKey = nacl.signing.SigningKey(seed=self.privKey, encoder=nacl.encoding.Base32Encoder).to_curve25519_private_key()
key = nacl.signing.VerifyKey(key=pubkey, encoder=nacl.encoding.Base32Encoder).to_curve25519_public_key()
ourBox = nacl.public.Box(ownKey, key)
retVal = ourBox.encrypt(data.encode(), encoder=encoding)
elif anonymous:
key = nacl.signing.VerifyKey(key=pubkey, encoder=nacl.encoding.Base32Encoder).to_curve25519_public_key()
anonBox = nacl.public.SealedBox(key)
try:
data = data.encode()
except AttributeError:
pass
retVal = anonBox.encrypt(data, encoder=encoding)
box = nacl.public.SealedBox(pubkey)
retVal = box.encrypt(data, encoder=encoding)
return retVal
def pubKeyDecrypt(self, data, pubkey='', privkey='', anonymous=False, encodedData=False):
def pubKeyDecrypt(self, data, pubkey='', privkey='', encodedData=False):
'''pubkey decrypt (Curve25519, taken from Ed25519 pubkey)'''
decrypted = False
if encodedData:
encoding = nacl.encoding.Base64Encoder
else:
encoding = nacl.encoding.RawEncoder
ownKey = nacl.signing.SigningKey(seed=self.privKey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key()
if self.privKey != None and not anonymous:
ourBox = nacl.public.Box(ownKey, pubkey)
decrypted = ourBox.decrypt(data, encoder=encoding)
elif anonymous:
if privkey == '':
privkey = self.privKey
ownKey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key()
if self._core._utils.validatePubKey(privkey):
privkey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key()
anonBox = nacl.public.SealedBox(privkey)
@ -180,12 +169,6 @@ class OnionrCrypto:
decrypted = base64.b64encode(decrypted)
return decrypted
def generateSymmetricPeer(self, peer):
'''Generate symmetric key for a peer and save it to the peer database'''
key = self.generateSymmetric()
self._core.setPeerInfo(peer, 'forwardKey', key)
return
def generateSymmetric(self):
'''Generate a symmetric key (bytes) and return it'''
return binascii.hexlify(nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE))
@ -283,6 +266,15 @@ class OnionrCrypto:
@staticmethod
def safeCompare(one, two):
# Do encode here to avoid spawning core
try:
one = one.encode()
except AttributeError:
pass
try:
two = two.encode()
except AttributeError:
pass
return hmac.compare_digest(one, two)
@staticmethod

17
onionr/onionrdaemontools.py Normal file → Executable file
View File

@ -18,9 +18,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import onionrexceptions, onionrpeers, onionrproofs, logger, onionrusers
import onionrexceptions, onionrpeers, onionrproofs, logger
import base64, sqlite3, os
from dependencies import secrets
from utils import netutils
from onionrusers import onionrusers
class DaemonTools:
'''
@ -43,12 +45,18 @@ class DaemonTools:
else:
peer = self.daemon.pickOnlinePeer()
for x in range(1):
if x == 1 and self.daemon._core.config.get('i2p.host'):
ourID = self.daemon._core.config.get('i2p.own_addr').strip()
else:
ourID = self.daemon._core.hsAddress.strip()
url = 'http://' + peer + '/announce'
data = {'node': ourID}
combinedNodes = ourID + peer
if ourID != 1:
#TODO: Extend existingRand for i2p
existingRand = self.daemon._core.getAddressInfo(peer, 'powValue')
if type(existingRand) is type(None):
existingRand = ''
@ -80,7 +88,7 @@ class DaemonTools:
def netCheck(self):
'''Check if we are connected to the internet or not when we can't connect to any peers'''
if len(self.daemon.onlinePeers) == 0:
if not self.daemon._core._utils.checkNetwork(torPort=self.daemon.proxyPort):
if not netutils.checkNetwork(self.daemon._core._utils, torPort=self.daemon.proxyPort):
logger.warn('Network check failed, are you connected to the internet?')
self.daemon.isOnline = False
else:
@ -186,10 +194,11 @@ class DaemonTools:
def insertDeniableBlock(self):
'''Insert a fake block in order to make it more difficult to track real blocks'''
fakePeer = self.daemon._core._crypto.generatePubKey()[0]
fakePeer = ''
chance = 10
if secrets.randbelow(chance) == (chance - 1):
fakePeer = self.daemon._core._crypto.generatePubKey()[0]
data = secrets.token_hex(secrets.randbelow(500) + 1)
self.daemon._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer)
self.daemon._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, meta={'subject': 'foo'})
self.daemon.decrementThreadCount('insertDeniableBlock')
return

3
onionr/onionrevents.py Normal file → Executable file
View File

@ -61,10 +61,9 @@ def call(plugin, event_name, data = None, pluginapi = None):
try:
attribute = 'on_' + str(event_name).lower()
# TODO: Use multithreading perhaps?
if hasattr(plugin, attribute):
#logger.debug('Calling event ' + str(event_name))
getattr(plugin, attribute)(pluginapi)
getattr(plugin, attribute)(pluginapi, data)
return True
except Exception as e:

9
onionr/onionrexceptions.py Normal file → Executable file
View File

@ -44,6 +44,10 @@ class PasswordStrengthError(Exception):
pass
# block exceptions
class DifficultyTooLarge(Exception):
pass
class InvalidMetadata(Exception):
pass
@ -83,3 +87,8 @@ class DiskAllocationReached(Exception):
class MissingAddress(Exception):
pass
# Contact exceptions
class ContactDeleted(Exception):
pass

0
onionr/onionrpeers.py Normal file → Executable file
View File

0
onionr/onionrpluginapi.py Normal file → Executable file
View File

0
onionr/onionrplugins.py Normal file → Executable file
View File

18
onionr/onionrproofs.py Normal file → Executable file
View File

@ -64,11 +64,12 @@ def getDifficultyForNewBlock(data, ourBlock=True):
else:
raise ValueError('not Block, str, or int')
if ourBlock:
minDifficulty = config.get('general.minimum_send_pow')
minDifficulty = config.get('general.minimum_send_pow', 4)
else:
minDifficulty = config.get('general.minimum_block_pow')
minDifficulty = config.get('general.minimum_block_pow', 4)
retData = max(minDifficulty, math.floor(dataSize / 100000)) + getDifficultyModifier()
retData = max(minDifficulty, math.floor(dataSize / 1000000)) + getDifficultyModifier()
return retData
def getHashDifficulty(h):
@ -192,7 +193,7 @@ class DataPOW:
if not self.hashing:
break
else:
time.sleep(2)
time.sleep(1)
except KeyboardInterrupt:
self.shutdown()
logger.warn('Got keyboard interrupt while waiting for POW result, stopping')
@ -243,13 +244,11 @@ class POW:
self.reporting = reporting
iFound = False # if current thread is the one that found the answer
answer = ''
heartbeat = 200000
hbCount = 0
nonce = int(binascii.hexlify(nacl.utils.random(2)), 16)
while self.hashing:
rand = nacl.utils.random()
#token = nacl.hash.blake2b(rand + self.data).decode()
self.metadata['powRandomToken'] = base64.b64encode(rand).decode()
self.metadata['powRandomToken'] = nonce
payload = json.dumps(self.metadata).encode() + b'\n' + self.data
token = myCore._crypto.sha3Hash(payload)
try:
@ -262,6 +261,7 @@ class POW:
iFound = True
self.result = payload
break
nonce += 1
if iFound:
endTime = math.floor(time.time())
@ -299,7 +299,7 @@ class POW:
if not self.hashing:
break
else:
time.sleep(2)
time.sleep(1)
except KeyboardInterrupt:
self.shutdown()
logger.warn('Got keyboard interrupt while waiting for POW result, stopping')

0
onionr/onionrsockets.py Normal file → Executable file
View File

View File

@ -55,6 +55,21 @@ def _dbFetch(coreInst, blockHash):
conn.close()
return None
def deleteBlock(coreInst, blockHash):
# You should call core.removeBlock if you automatically want to remove storage byte count
assert isinstance(coreInst, core.Core)
if os.path.exists('%s/%s.dat' % (coreInst.blockDataLocation, blockHash)):
os.remove('%s/%s.dat' % (coreInst.blockDataLocation, blockHash))
return True
dbCreate(coreInst)
conn = sqlite3.connect(coreInst.blockDataDB, timeout=10)
c = conn.cursor()
data = (blockHash,)
c.execute('DELETE FROM blockData where hash = ?', data)
conn.commit()
conn.close()
return True
def store(coreInst, data, blockHash=''):
assert isinstance(coreInst, core.Core)
assert coreInst._utils.validateHash(blockHash)

View File

@ -0,0 +1,72 @@
'''
Onionr - P2P Anonymous Storage Network
Sets more abstract information related to a peer. Can be thought of as traditional 'contact' system
'''
'''
This program 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.
This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
'''
import os, json, onionrexceptions
from onionrusers import onionrusers
class ContactManager(onionrusers.OnionrUser):
def __init__(self, coreInst, publicKey, saveUser=False, recordExpireSeconds=5):
super(ContactManager, self).__init__(coreInst, publicKey, saveUser=saveUser)
self.dataDir = coreInst.dataDir + '/contacts/'
self.dataFile = '%s/contacts/%s.json' % (coreInst.dataDir, publicKey)
self.lastRead = 0
self.recordExpire = recordExpireSeconds
self.data = self._loadData()
self.deleted = False
if not os.path.exists(self.dataDir):
os.mkdir(self.dataDir)
def _writeData(self):
data = json.dumps(self.data)
with open(self.dataFile, 'w') as dataFile:
dataFile.write(data)
def _loadData(self):
self.lastRead = self._core._utils.getEpoch()
retData = {}
if os.path.exists(self.dataFile):
with open(self.dataFile, 'r') as dataFile:
retData = json.loads(dataFile.read())
return retData
def set_info(self, key, value, autoWrite=True):
if self.deleted:
raise onionrexceptions.ContactDeleted
self.data[key] = value
if autoWrite:
self._writeData()
return
def get_info(self, key, forceReload=False):
if self.deleted:
raise onionrexceptions.ContactDeleted
if (self._core._utils.getEpoch() - self.lastRead >= self.recordExpire) or forceReload:
self.data = self._loadData()
try:
return self.data[key]
except KeyError:
return None
def delete_contact(self):
self.deleted = True
if os.path.exists(self.dataFile):
os.remove(self.dataFile)

View File

@ -40,12 +40,18 @@ class OnionrUser:
Takes an instance of onionr core, a base32 encoded ed25519 public key, and a bool saveUser
saveUser determines if we should add a user to our peer database or not.
'''
if ' ' in coreInst._utils.bytesToStr(publicKey).strip():
publicKey = coreInst._utils.convertHumanReadableID(publicKey)
self.trust = 0
self._core = coreInst
self.publicKey = publicKey
if saveUser:
try:
self._core.addPeer(publicKey)
except AssertionError:
pass
self.trust = self._core.getPeerInfo(self.publicKey, 'trust')
return
@ -73,7 +79,7 @@ class OnionrUser:
encrypted = coreInst._crypto.pubKeyEncrypt(data, self.publicKey, encodedData=True)
return encrypted
def decrypt(self, data, anonymous=True):
def decrypt(self, data):
decrypted = coreInst._crypto.pubKeyDecrypt(data, self.publicKey, encodedData=True)
return decrypted
@ -81,7 +87,7 @@ class OnionrUser:
retData = ''
forwardKey = self._getLatestForwardKey()
if self._core._utils.validatePubKey(forwardKey):
retData = self._core._crypto.pubKeyEncrypt(data, forwardKey, encodedData=True, anonymous=True)
retData = self._core._crypto.pubKeyEncrypt(data, forwardKey, encodedData=True)
else:
raise onionrexceptions.InvalidPubkey("No valid forward secrecy key available for this user")
#self.generateForwardKey()
@ -91,7 +97,7 @@ class OnionrUser:
retData = ""
for key in self.getGeneratedForwardKeys(False):
try:
retData = self._core._crypto.pubKeyDecrypt(encrypted, privkey=key[1], anonymous=True, encodedData=True)
retData = self._core._crypto.pubKeyDecrypt(encrypted, privkey=key[1], encodedData=True)
except nacl.exceptions.CryptoError:
retData = False
else:

244
onionr/onionrutils.py Normal file → Executable file
View File

@ -18,14 +18,15 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
# Misc functions that do not fit in the main api, but are useful
import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config, binascii, time, base64, json, glob, shutil, math, json, re, urllib.parse
import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config, binascii, time, base64, json, glob, shutil, math, json, re, urllib.parse, string
import nacl.signing, nacl.encoding
from onionrblockapi import Block
import onionrexceptions
from onionr import API_VERSION
import onionrevents
import onionrusers, storagecounter
import storagecounter
from etc import pgpwords
from onionrusers import onionrusers
if sys.version_info < (3, 6):
try:
import sha3
@ -68,90 +69,6 @@ class OnionrUtils:
epoch = self.getEpoch()
return epoch - (epoch % roundS)
def mergeKeys(self, newKeyList):
'''
Merge ed25519 key list to our database, comma seperated string
'''
try:
retVal = False
if newKeyList != False:
for key in newKeyList.split(','):
key = key.split('-')
# Test if key is valid
try:
if len(key[0]) > 60 or len(key[1]) > 1000:
logger.warn('%s or its pow value is too large.' % key[0])
continue
except IndexError:
logger.warn('No pow token')
continue
try:
value = base64.b64decode(key[1])
except binascii.Error:
continue
# Load the pow token
hashedKey = self._core._crypto.blake2bHash(key[0])
powHash = self._core._crypto.blake2bHash(value + hashedKey)
try:
powHash = powHash.encode()
except AttributeError:
pass
# if POW meets required difficulty, TODO make configurable/dynamic
if powHash.startswith(b'0000'):
# if we don't already have the key and its not our key, add it.
if not key[0] in self._core.listPeers(randomOrder=False) and type(key) != None and key[0] != self._core._crypto.pubKey:
if self._core.addPeer(key[0], key[1]):
# Check if the peer has a set username already
onionrusers.OnionrUser(self._core, key[0]).findAndSetID()
retVal = True
else:
logger.warn("Failed to add key")
else:
pass
#logger.debug('%s pow failed' % key[0])
return retVal
except Exception as error:
logger.error('Failed to merge keys.', error=error)
return False
def mergeAdders(self, newAdderList):
'''
Merge peer adders list to our database
'''
try:
retVal = False
if newAdderList != False:
for adder in newAdderList.split(','):
adder = adder.strip()
if not adder in self._core.listAdders(randomOrder = False) and adder != self.getMyAddress() and not self._core._blacklist.inBlacklist(adder):
if not config.get('tor.v3onions') and len(adder) == 62:
continue
if self._core.addAddress(adder):
# Check if we have the maxmium amount of allowed stored peers
if config.get('peers.max_stored_peers') > len(self._core.listAdders()):
logger.info('Added %s to db.' % adder, timestamp = True)
retVal = True
else:
logger.warn('Reached the maximum amount of peers in the net database as allowed by your config.')
else:
pass
#logger.debug('%s is either our address or already in our DB' % adder)
return retVal
except Exception as error:
logger.error('Failed to merge adders.', error = error)
return False
def getMyAddress(self):
try:
with open('./' + self._core.dataDir + 'hs/hostname', 'r') as hostname:
return hostname.read().strip()
except FileNotFoundError:
return ""
except Exception as error:
logger.error('Failed to read my address.', error = error)
return None
def getClientAPIServer(self):
retData = ''
try:
@ -163,7 +80,7 @@ class OnionrUtils:
retData += '%s:%s' % (hostname, config.get('client.client.port'))
return retData
def localCommand(self, command, data='', silent = True, post=False, postData = {}, maxWait=10):
def localCommand(self, command, data='', silent = True, post=False, postData = {}, maxWait=20):
'''
Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers.
'''
@ -185,9 +102,9 @@ class OnionrUtils:
payload = 'http://%s/%s%s' % (hostname, command, data)
try:
if post:
retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, 30)).text
retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text
else:
retData = requests.get(payload, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, 30)).text
retData = requests.get(payload, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text
except Exception as error:
if not silent:
logger.error('Failed to make local request (command: %s):%s' % (command, error))
@ -195,38 +112,23 @@ class OnionrUtils:
return retData
def getPassword(self, message='Enter password: ', confirm = True):
'''
Get a password without showing the users typing and confirm the input
'''
# Get a password safely with confirmation and return it
while True:
print(message)
pass1 = getpass.getpass()
if confirm:
print('Confirm password: ')
pass2 = getpass.getpass()
if pass1 != pass2:
logger.error("Passwords do not match.")
logger.readline()
else:
break
else:
break
return pass1
def getHumanReadableID(self, pub=''):
'''gets a human readable ID from a public key'''
if pub == '':
pub = self._core._crypto.pubKey
pub = base64.b16encode(base64.b32decode(pub)).decode()
return '-'.join(pgpwords.wordify(pub))
return ' '.join(pgpwords.wordify(pub))
def convertHumanReadableID(self, pub):
'''Convert a human readable pubkey id to base32'''
pub = pub.lower()
return self.bytesToStr(base64.b32encode(binascii.unhexlify(pgpwords.hexify(pub.strip()))))
def getBlockMetadataFromData(self, blockData):
'''
accepts block contents as string, returns a tuple of metadata, meta (meta being internal metadata, which will be returned as an encrypted base64 string if it is encrypted, dict if not).
accepts block contents as string, returns a tuple of
metadata, meta (meta being internal metadata, which will be
returned as an encrypted base64 string if it is encrypted, dict if not).
'''
meta = {}
metadata = {}
@ -251,34 +153,6 @@ class OnionrUtils:
meta = metadata['meta']
return (metadata, meta, data)
def checkPort(self, port, host=''):
'''
Checks if a port is available, returns bool
'''
# inspired by https://www.reddit.com/r/learnpython/comments/2i4qrj/how_to_write_a_python_script_that_checks_to_see/ckzarux/
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
retVal = False
try:
sock.bind((host, port))
except OSError as e:
if e.errno is 98:
retVal = True
finally:
sock.close()
return retVal
def checkIsIP(self, ip):
'''
Check if a string is a valid IPv4 address
'''
try:
socket.inet_aton(ip)
except:
return False
else:
return True
def processBlockMetadata(self, blockHash):
'''
Read metadata from a block and cache it to the block database
@ -309,7 +183,8 @@ class OnionrUtils:
else:
self._core.updateBlockInfo(blockHash, 'expire', expireTime)
else:
logger.debug('Not processing metadata on encrypted block we cannot decrypt.')
pass
#logger.debug('Not processing metadata on encrypted block we cannot decrypt.')
def escapeAnsi(self, line):
'''
@ -320,21 +195,6 @@ class OnionrUtils:
ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]')
return ansi_escape.sub('', line)
def getBlockDBHash(self):
'''
Return a sha3_256 hash of the blocks DB
'''
try:
with open(self._core.blockDB, 'rb') as data:
data = data.read()
hasher = hashlib.sha3_256()
hasher.update(data)
dataHash = hasher.hexdigest()
return dataHash
except Exception as error:
logger.error('Failed to get block DB hash.', error=error)
def hasBlock(self, hash):
'''
Check for new block in the list
@ -361,7 +221,7 @@ class OnionrUtils:
def validateHash(self, data, length=64):
'''
Validate if a string is a valid hex formatted hash
Validate if a string is a valid hash hex digest (does not compare, just checks length and charset)
'''
retVal = True
if data == False or data == True:
@ -450,6 +310,8 @@ class OnionrUtils:
Validate if a string is a valid base32 encoded Ed25519 key
'''
retVal = False
if type(key) is type(None):
return False
try:
nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder)
except nacl.exceptions.ValueError:
@ -460,15 +322,6 @@ class OnionrUtils:
retVal = True
return retVal
def isIntegerString(self, data):
'''Check if a string is a valid base10 integer (also returns true if already an int)'''
try:
int(data)
except ValueError:
return False
else:
return True
def validateID(self, id):
'''
Validate if an address is a valid tor or i2p hidden service
@ -513,36 +366,22 @@ class OnionrUtils:
retVal = False
# Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32
try:
base64.b32decode(idNoDomain.upper().encode())
except binascii.Error:
retVal = False
# Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32
try:
base64.b32decode(idNoDomain.upper().encode())
except binascii.Error:
for x in idNoDomain.upper():
if x not in string.ascii_uppercase and x not in '234567':
retVal = False
return retVal
except:
return False
def getPeerByHashId(self, hash):
'''
Return the pubkey of the user if known from the hash
'''
if self._core._crypto.pubKeyHashID() == hash:
retData = self._core._crypto.pubKey
return retData
conn = sqlite3.connect(self._core.peerDB)
c = conn.cursor()
command = (hash,)
retData = ''
for row in c.execute('SELECT id FROM peers WHERE hashID = ?', command):
if row[0] != '':
retData = row[0]
return retData
def isIntegerString(self, data):
'''Check if a string is a valid base10 integer (also returns true if already an int)'''
try:
int(data)
except (ValueError, TypeError) as e:
return False
else:
return True
def isCommunicatorRunning(self, timeout = 5, interval = 0.1):
try:
@ -564,13 +403,6 @@ class OnionrUtils:
except:
return False
def token(self, size = 32):
'''
Generates a secure random hex encoded token
'''
return binascii.hexlify(os.urandom(size))
def importNewBlocks(self, scanDir=''):
'''
This function is intended to scan for new blocks ON THE DISK and import them
@ -692,22 +524,6 @@ class OnionrUtils:
pass
return data
def checkNetwork(self, torPort=0):
'''Check if we are connected to the internet (through Tor)'''
retData = False
connectURLs = []
try:
with open('static-data/connect-check.txt', 'r') as connectTest:
connectURLs = connectTest.read().split(',')
for url in connectURLs:
if self.doGetRequest(url, port=torPort, ignoreAPI=True) != False:
retData = True
break
except FileNotFoundError:
pass
return retData
def size(path='.'):
'''
Returns the size of a folder's contents in bytes

0
onionr/static-data/bootstrap-nodes.txt Normal file → Executable file
View File

2
onionr/static-data/connect-check.txt Normal file → Executable file
View File

@ -1 +1 @@
https://3g2upl4pq6kufc4m.onion/robots.txt,http://expyuzz4wqqyqhjn.onion/robots.txt,https://onionr.voidnet.tech/
https://3g2upl4pq6kufc4m.onion/robots.txt,http://expyuzz4wqqyqhjn.onion/robots.txt,http://archivecaslytosk.onion/robots.txt

0
onionr/static-data/default-plugins/cliui/info.json Normal file → Executable file
View File

0
onionr/static-data/default-plugins/cliui/main.py Normal file → Executable file
View File

0
onionr/static-data/default-plugins/encrypt/info.json Normal file → Executable file
View File

4
onionr/static-data/default-plugins/encrypt/main.py Normal file → Executable file
View File

@ -69,7 +69,7 @@ class PlainEncryption:
data['data'] = plaintext
data = json.dumps(data)
plaintext = data
encrypted = self.api.get_core()._crypto.pubKeyEncrypt(plaintext, pubkey, anonymous=True, encodedData=True)
encrypted = self.api.get_core()._crypto.pubKeyEncrypt(plaintext, pubkey, encodedData=True)
encrypted = self.api.get_core()._utils.bytesToStr(encrypted)
logger.info('Encrypted Message: \n\nONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,))
@ -88,7 +88,7 @@ class PlainEncryption:
return
encrypted = data.replace('ONIONR ENCRYPTED DATA ', '').replace('END ENCRYPTED DATA', '')
myPub = self.api.get_core()._crypto.pubKey
decrypted = self.api.get_core()._crypto.pubKeyDecrypt(encrypted, privkey=self.api.get_core()._crypto.privKey, anonymous=True, encodedData=True)
decrypted = self.api.get_core()._crypto.pubKeyDecrypt(encrypted, privkey=self.api.get_core()._crypto.privKey, encodedData=True)
if decrypted == False:
logger.error("Decryption failed")
else:

0
onionr/static-data/default-plugins/flow/info.json Normal file → Executable file
View File

0
onionr/static-data/default-plugins/flow/main.py Normal file → Executable file
View File

View File

View File

@ -41,7 +41,7 @@ def _processForwardKey(api, myBlock):
else:
raise onionrexceptions.InvalidPubkey("%s is nota valid pubkey key" % (key,))
def on_processblocks(api):
def on_processblocks(api, data=None):
# Generally fired by utils.
myBlock = api.data['block']
blockType = api.data['type']

0
onionr/static-data/default-plugins/pluginmanager/.gitignore vendored Normal file → Executable file
View File

View File

View File

View File

View File

0
onionr/static-data/default-plugins/pms/info.json Normal file → Executable file
View File

91
onionr/static-data/default-plugins/pms/main.py Normal file → Executable file
View File

@ -21,8 +21,9 @@
# Imports some useful libraries
import logger, config, threading, time, readline, datetime
from onionrblockapi import Block
import onionrexceptions, onionrusers
import locale, sys, os
import onionrexceptions
from onionrusers import onionrusers
import locale, sys, os, json
locale.setlocale(locale.LC_ALL, '')
@ -48,14 +49,14 @@ class MailStrings:
self.mailInstance = mailInstance
self.programTag = 'OnionrMail v%s' % (PLUGIN_VERSION)
choices = ['view inbox', 'view sentbox', 'send message', 'quit']
choices = ['view inbox', 'view sentbox', 'send message', 'toggle pseudonymity', 'quit']
self.mainMenuChoices = choices
self.mainMenu = '''\n
-----------------
self.mainMenu = '''-----------------
1. %s
2. %s
3. %s
4. %s''' % (choices[0], choices[1], choices[2], choices[3])
4. %s
5. %s''' % (choices[0], choices[1], choices[2], choices[3], choices[4])
class OnionrMail:
def __init__(self, pluginapi):
@ -65,6 +66,7 @@ class OnionrMail:
self.sentboxTools = sentboxdb.SentBox(self.myCore)
self.sentboxList = []
self.sentMessages = {}
self.doSigs = True
return
def inbox(self):
@ -133,18 +135,30 @@ class OnionrMail:
else:
cancel = ''
readBlock.verifySig()
logger.info('Message recieved from %s' % (self.myCore._utils.bytesToStr(readBlock.signer,)))
senderDisplay = self.myCore._utils.bytesToStr(readBlock.signer)
if len(senderDisplay.strip()) == 0:
senderDisplay = 'Anonymous'
logger.info('Message received from %s' % (senderDisplay,))
logger.info('Valid signature: %s' % readBlock.validSig)
if not readBlock.validSig:
logger.warn('This message has an INVALID signature. ANYONE could have sent this message.')
logger.warn('This message has an INVALID/NO signature. ANYONE could have sent this message.')
cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).')
print('')
if cancel != '-q':
try:
print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip())))
except ValueError:
logger.warn('Error presenting message. This is usually due to a malformed or blank message.')
pass
if readBlock.validSig:
reply = logger.readline("Press enter to continue, or enter %s to reply" % ("-r",))
print('')
if reply == "-r":
self.draftMessage(self.myCore._utils.bytesToStr(readBlock.signer,))
self.draft_message(self.myCore._utils.bytesToStr(readBlock.signer,))
else:
logger.readline("Press enter to continue")
print('')
return
def sentbox(self):
@ -153,7 +167,7 @@ class OnionrMail:
'''
entering = True
while entering:
self.getSentList()
self.get_sent_list()
logger.info('Enter a block number or -q to return')
try:
choice = input('>')
@ -180,18 +194,19 @@ class OnionrMail:
return
def getSentList(self):
def get_sent_list(self, display=True):
count = 1
self.sentboxList = []
self.sentMessages = {}
for i in self.sentboxTools.listSent():
self.sentboxList.append(i['hash'])
self.sentMessages[i['hash']] = (i['message'], i['peer'])
logger.info('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date']))
self.sentMessages[i['hash']] = (self.myCore._utils.bytesToStr(i['message']), i['peer'], i['subject'])
if display:
logger.info('%s. %s - %s - (%s) - %s' % (count, i['hash'], i['peer'][:12], i['subject'], i['date']))
count += 1
return json.dumps(self.sentMessages)
def draftMessage(self, recip=''):
def draft_message(self, recip=''):
message = ''
newLine = ''
subject = ''
@ -237,14 +252,26 @@ class OnionrMail:
if not cancelEnter:
logger.info('Inserting encrypted message as Onionr block....')
blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True, meta={'subject': subject})
self.sentboxTools.addToSent(blockID, recip, message)
blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=self.doSigs, meta={'subject': subject})
def toggle_signing(self):
self.doSigs = not self.doSigs
def menu(self):
choice = ''
while True:
sigMsg = 'Message Signing: %s'
logger.info(self.strings.programTag + '\n\nOur ID: ' + self.myCore._crypto.pubKey + self.strings.mainMenu.title()) # print out main menu
logger.info(self.strings.programTag + '\n\nUser ID: ' + self.myCore._crypto.pubKey)
if self.doSigs:
sigMsg = sigMsg % ('enabled',)
else:
sigMsg = sigMsg % ('disabled (Your messages cannot be trusted)',)
if self.doSigs:
logger.info(sigMsg)
else:
logger.warn(sigMsg)
logger.info(self.strings.mainMenu.title()) # print out main menu
try:
choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip()
except (KeyboardInterrupt, EOFError):
@ -255,8 +282,10 @@ class OnionrMail:
elif choice in (self.strings.mainMenuChoices[1], '2'):
self.sentbox()
elif choice in (self.strings.mainMenuChoices[2], '3'):
self.draftMessage()
self.draft_message()
elif choice in (self.strings.mainMenuChoices[3], '4'):
self.toggle_signing()
elif choice in (self.strings.mainMenuChoices[4], '5'):
logger.info('Goodbye.')
break
elif choice == '':
@ -265,6 +294,26 @@ class OnionrMail:
logger.warn('Invalid choice.')
return
def on_insertblock(api, data={}):
sentboxTools = sentboxdb.SentBox(api.get_core())
meta = json.loads(data['meta'])
sentboxTools.addToSent(data['hash'], data['peer'], data['content'], meta['subject'])
def on_pluginrequest(api, data=None):
resp = ''
subject = ''
recip = ''
message = ''
postData = {}
blockID = ''
sentboxTools = sentboxdb.SentBox(api.get_core())
if data['name'] == 'mail':
path = data['path']
cmd = path.split('/')[1]
if cmd == 'sentbox':
resp = OnionrMail(api).get_sent_list(display=False)
if resp != '':
api.get_onionr().clientAPIInst.pluginResponses[data['pluginResponse']] = resp
def on_init(api, data = None):
'''

9
onionr/static-data/default-plugins/pms/sentboxdb.py Normal file → Executable file
View File

@ -37,6 +37,7 @@ class SentBox:
hash id not null,
peer text not null,
message text not null,
subject text not null,
date int not null
);
''')
@ -46,12 +47,12 @@ class SentBox:
def listSent(self):
retData = []
for entry in self.cursor.execute('SELECT * FROM sent;'):
retData.append({'hash': entry[0], 'peer': entry[1], 'message': entry[2], 'date': entry[3]})
retData.append({'hash': entry[0], 'peer': entry[1], 'message': entry[2], 'subject': entry[3], 'date': entry[4]})
return retData
def addToSent(self, blockID, peer, message):
args = (blockID, peer, message, self.core._utils.getEpoch())
self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?)', args)
def addToSent(self, blockID, peer, message, subject=''):
args = (blockID, peer, message, subject, self.core._utils.getEpoch())
self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?, ?)', args)
self.conn.commit()
return

7
onionr/static-data/default_config.json Normal file → Executable file
View File

@ -2,8 +2,8 @@
"general" : {
"dev_mode" : true,
"display_header" : false,
"minimum_block_pow": 1,
"minimum_send_pow": 1,
"minimum_block_pow": 4,
"minimum_send_pow": 4,
"socket_servers": false,
"security_level": 0,
"max_block_age": 2678400,
@ -21,8 +21,7 @@
"private" : {
"run" : true,
"path" : "static-data/www/private/",
"guess_mime" : true,
"timing_protection" : true
"guess_mime" : true
},
"ui" : {

0
onionr/static-data/default_plugin.py Normal file → Executable file
View File

0
onionr/static-data/header.txt Normal file → Executable file
View File

0
onionr/static-data/index.html Normal file → Executable file
View File

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,101 @@
padding-top: 1em;
}
.threads div span{
<<<<<<< HEAD
padding-left: 0.5em;
padding-right: 0.5em;
=======
padding-left: 0.2em;
padding-right: 0.2em;
}
#threadPlaceholder{
display: none;
margin-top: 1em;
font-size: 2em;
}
input{
background-color: white;
color: black;
}
.btn-group button {
border: 1px solid black;
padding: 10px 24px; /* Some padding */
cursor: pointer; /* Pointer/hand icon */
float: left; /* Float the buttons side by side */
}
.btn-group button:hover {
background-color: darkgray;
}
.btn-group {
margin-bottom: 2em;
}
#tabBtns{
margin-bottom: 3em;
display: block;
}
.activeTab{
color: black;
background-color: gray;
}
.overlayContent{
background-color: lightgray;
border: 3px solid black;
border-radius: 3px;
color: black;
font-family: Verdana, Geneva, Tahoma, sans-serif;
min-height: 100%;
padding: 1em;
margin: 1em;
}
.danger{
color: red;
}
.warn{
color: orange;
}
.good{
color: greenyellow;
}
.pre{
padding-top: 1em;
word-wrap: break-word;
font-family: monospace;
white-space: pre;
}
.messageContent{
font-size: 1.5em;
}
#draftText{
margin-top: 1em;
margin-bottom: 1em;
display: block;
width: 50%;
height: 75%;
min-width: 2%;
min-height: 5%;
background: white;
color: black;
}
.successBtn{
background-color: #28a745;
border-radius: 3px;
padding: 5px;
color: black;
font-size: 1.5em;
width: 10%;
>>>>>>> contacts
}

View File

@ -1,3 +1,4 @@
<<<<<<< HEAD
pms = ''
threadPart = document.getElementById('threads')
function getInbox(){
@ -38,6 +39,183 @@ function getInbox(){
}.bind([pms, i]))
}
=======
/*
Onionr - P2P Anonymous Storage Network
This file handles the mail interface
This program 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.
This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
pms = ''
sentbox = ''
threadPart = document.getElementById('threads')
threadPlaceholder = document.getElementById('threadPlaceholder')
tabBtns = document.getElementById('tabBtns')
threadContent = {}
myPub = httpGet('/getActivePubkey')
function openThread(bHash, sender, date, sigBool){
var messageDisplay = document.getElementById('threadDisplay')
var blockContent = httpGet('/getblockbody/' + bHash)
document.getElementById('fromUser').value = sender
messageDisplay.innerText = blockContent
var sigEl = document.getElementById('sigValid')
var sigMsg = 'signature'
if (sigBool){
sigMsg = 'Good ' + sigMsg
sigEl.classList.remove('danger')
}
else{
sigMsg = 'Bad/no ' + sigMsg + ' (message could be fake)'
sigEl.classList.add('danger')
}
sigEl.innerText = sigMsg
overlay('messageDisplay')
}
function setActiveTab(tabName){
threadPart.innerHTML = ""
switch(tabName){
case 'inbox':
getInbox()
break
case 'sentbox':
getSentbox()
break
case 'send message':
overlay('sendMessage')
break
}
}
function loadInboxEntrys(bHash){
fetch('/getblockheader/' + bHash, {
headers: {
"token": webpass
}})
.then((resp) => resp.json()) // Transform the data into json
.then(function(resp) {
//console.log(resp)
var entry = document.createElement('div')
var bHashDisplay = document.createElement('span')
var senderInput = document.createElement('input')
var subjectLine = document.createElement('span')
var dateStr = document.createElement('span')
var validSig = document.createElement('span')
var humanDate = new Date(0)
var metadata = resp['metadata']
humanDate.setUTCSeconds(resp['meta']['time'])
if (resp['meta']['signer'] != ''){
senderInput.value = httpGet('/getHumanReadable/' + resp['meta']['signer'])
}
if (resp['meta']['validSig']){
validSig.innerText = 'Signature Validity: Good'
}
else{
validSig.innerText = 'Signature Validity: Bad'
validSig.style.color = 'red'
}
if (senderInput.value == ''){
senderInput.value = 'Anonymous'
}
bHashDisplay.innerText = bHash.substring(0, 10)
entry.setAttribute('hash', bHash)
senderInput.readOnly = true
dateStr.innerText = humanDate.toString()
if (metadata['subject'] === undefined || metadata['subject'] === null) {
subjectLine.innerText = '()'
}
else{
subjectLine.innerText = '(' + metadata['subject'] + ')'
}
//entry.innerHTML = 'sender ' + resp['meta']['signer'] + ' - ' + resp['meta']['time']
threadPart.appendChild(entry)
entry.appendChild(bHashDisplay)
entry.appendChild(senderInput)
entry.appendChild(validSig)
entry.appendChild(subjectLine)
entry.appendChild(dateStr)
entry.classList.add('threadEntry')
entry.onclick = function(){
openThread(entry.getAttribute('hash'), senderInput.value, dateStr.innerText, resp['meta']['validSig'])
}
}.bind(bHash))
}
function getInbox(){
var showed = false
var requested = ''
for(var i = 0; i < pms.length; i++) {
if (pms[i].trim().length == 0){
continue
}
else{
threadPlaceholder.style.display = 'none'
showed = true
}
loadInboxEntrys(pms[i])
}
if (! showed){
threadPlaceholder.style.display = 'block'
}
}
function getSentbox(){
fetch('/apipoints/mail/sentbox', {
headers: {
"token": webpass
}})
.then((resp) => resp.json()) // Transform the data into json
.then(function(resp) {
var keys = [];
var entry = document.createElement('div')
var entryUsed;
for(var k in resp) keys.push(k);
for (var i = 0; i < keys.length; i++){
var entry = document.createElement('div')
var obj = resp[i];
var toLabel = document.createElement('span')
toLabel.innerText = 'To: '
var toEl = document.createElement('input')
var preview = document.createElement('span')
toEl.readOnly = true
toEl.value = resp[keys[i]][1]
preview.innerText = '(' + resp[keys[i]][2] + ')'
entry.appendChild(toLabel)
entry.appendChild(toEl)
entry.appendChild(preview)
entryUsed = resp[keys[i]]
entry.onclick = function(){
console.log(resp)
showSentboxWindow(toEl.value, entryUsed[0])
}
threadPart.appendChild(entry)
}
threadPart.appendChild(entry)
}.bind(threadPart))
}
function showSentboxWindow(to, content){
document.getElementById('toID').value = to
document.getElementById('sentboxDisplayText').innerText = content
overlay('sentboxDisplay')
>>>>>>> contacts
}
fetch('/getblocksbytype/pm', {
@ -47,6 +225,43 @@ fetch('/getblocksbytype/pm', {
.then((resp) => resp.text()) // Transform the data into json
.then(function(data) {
pms = data.split(',')
<<<<<<< HEAD
getInbox(pms)
})
=======
setActiveTab('inbox')
})
tabBtns.onclick = function(event){
var children = tabBtns.children
for (var i = 0; i < children.length; i++) {
var btn = children[i]
btn.classList.remove('activeTab')
}
event.target.classList.add('activeTab')
setActiveTab(event.target.innerText.toLowerCase())
}
var idStrings = document.getElementsByClassName('myPub')
var myHumanReadable = httpGet('/getHumanReadable/' + myPub)
for (var i = 0; i < idStrings.length; i++){
if (idStrings[i].tagName.toLowerCase() == 'input'){
idStrings[i].value = myHumanReadable
}
else{
idStrings[i].innerText = myHumanReadable
}
}
for (var i = 0; i < document.getElementsByClassName('refresh').length; i++){
document.getElementsByClassName('refresh')[i].style.float = 'right'
}
for (var i = 0; i < document.getElementsByClassName('closeOverlay').length; i++){
document.getElementsByClassName('closeOverlay')[i].onclick = function(e){
document.getElementById(e.target.getAttribute('overlay')).style.visibility = 'hidden'
}
}
>>>>>>> contacts

View File

@ -0,0 +1,45 @@
/*
Onionr - P2P Anonymous Storage Network
This file handles the mail interface
This program 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.
This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
var sendbutton = document.getElementById('sendMail')
function sendMail(to, message, subject){
//postData = {"postData": '{"to": "' + to + '", "message": "' + message + '"}'} // galaxy brain
postData = {'message': message, 'to': to, 'type': 'pm', 'encrypt': true, 'meta': JSON.stringify({'subject': subject})}
postData = JSON.stringify(postData)
fetch('/insertblock', {
method: 'POST',
body: postData,
headers: {
"content-type": "application/json",
"token": webpass
}})
.then((resp) => resp.text()) // Transform the data into json
.then(function(data) {
})
}
sendForm.onsubmit = function(){
var messageContent = document.getElementById('draftText')
var to = document.getElementById('draftID')
var subject = document.getElementById('draftSubject')
sendMail(to.value, messageContent.value, subject.value)
return false;
}

View File

@ -21,6 +21,7 @@
<br><br><a class='idLink' href='/mail/'>Mail</a>
<h2>Stats</h2>
<p>Uptime: <span id='uptime'></span></p>
<p>Last Received Connection: <span id='lastIncoming'>Unknown</span></p>
<p>Stored Blocks: <span id='storedBlocks'></span></p>
<p>Blocks in queue: <span id='blockQueue'></span></p>
<p>Connected nodes:</p>

View File

@ -21,6 +21,10 @@ uptimeDisplay = document.getElementById('uptime')
connectedDisplay = document.getElementById('connectedNodes')
storedBlockDisplay = document.getElementById('storedBlocks')
queuedBlockDisplay = document.getElementById('blockQueue')
<<<<<<< HEAD
=======
lastIncoming = document.getElementById('lastIncoming')
>>>>>>> contacts
function getStats(){
stats = JSON.parse(httpGet('getstats', webpass))
@ -28,5 +32,18 @@ function getStats(){
connectedDisplay.innerText = stats['connectedNodes']
storedBlockDisplay.innerText = stats['blockCount']
queuedBlockDisplay.innerText = stats['blockQueueCount']
<<<<<<< HEAD
=======
var lastConnect = httpGet('/lastconnect')
if (lastConnect > 0){
var humanDate = new Date(0)
humanDate.setUTCSeconds(httpGet('/lastconnect'))
lastConnect = humanDate.toString()
}
else{
lastConnect = 'Unknown'
}
lastIncoming.innerText = lastConnect
>>>>>>> contacts
}
getStats()

View File

@ -37,7 +37,11 @@ body{
align-items:center;
}
.logo{
<<<<<<< HEAD
max-width: 25%;
=======
max-width: 20%;
>>>>>>> contacts
vertical-align: middle;
}
.logoText{
@ -132,9 +136,31 @@ body{
left: 0px;
top: 0px;
width:100%;
<<<<<<< HEAD
opacity: 0.9;
height:100%;
text-align:center;
z-index: 1000;
background-color: black;
}
=======
height:100%;
text-align:left;
z-index: 1000;
background-color: #2c2b3f;
color: white;
}
.closeOverlay{
background-color: white;
color: black;
border: 1px solid red;
border-radius: 5px;
float: right;
font-family: sans-serif;
}
.closeOverlay:after{
content: '❌';
padding: 5px;
}
>>>>>>> contacts

View File

@ -1,5 +1,30 @@
<<<<<<< HEAD
webpass = document.location.hash.replace('#', '')
nowebpass = false
=======
/*
Onionr - P2P Anonymous Storage Network
This file handles the mail interface
This program 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.
This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
webpass = document.location.hash.replace('#', '')
nowebpass = false
>>>>>>> contacts
if (typeof webpass == "undefined"){
webpass = localStorage['webpass']
}
@ -12,6 +37,13 @@ if (typeof webpass == "undefined" || webpass == ""){
nowebpass = true
}
<<<<<<< HEAD
=======
function arrayContains(needle, arrhaystack) {
return (arrhaystack.indexOf(needle) > -1);
}
>>>>>>> contacts
function httpGet(theUrl) {
var xmlHttp = new XMLHttpRequest()
xmlHttp.open( "GET", theUrl, false ) // false for synchronous request
@ -27,6 +59,10 @@ function httpGet(theUrl) {
function overlay(overlayID) {
el = document.getElementById(overlayID)
el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible"
<<<<<<< HEAD
=======
scroll(0,0)
>>>>>>> contacts
}
var passLinks = document.getElementsByClassName("idLink")

View File

@ -1,44 +0,0 @@
# Onionr UI
## About
The default GUI for Onionr
## Setup
To compile the application, simply execute the following:
```
python3 compile.py
```
If you are wanting to compile Onionr UI for another language, execute the following, replacing `[lang]` with the target language (supported languages include `eng` for English, `spa` para español, and `zho`为中国人):
```
python3 compile.py [lang]
```
## FAQ
### Why "compile" anyway?
This web application is compiled for a few reasons:
1. To make it easier to update; this way, we do not have to update the header in every file if we want to change something about it.
2. To make the application smaller in size; there is less duplicated code when the code like the header and footer can be stored in an individual file rather than every file.
3. For multi-language support; with the Python "tags" feature, we can reference strings by variable name, and based on a language file, they can be dynamically inserted into the page on compilation.
4. For compile-time customizations.
### What exactly happens when you compile?
Upon compilation, files from the `src/` directory will be copied to `dist/` directory, header and footers will be injected in the proper places, and Python "tags" will be interpreted.
### How do Python "tags" work?
There are two types of Python "tags":
1. Logic tags (`<$ logic $>`): These tags allow you to perform logic at compile time. Example: `<$ import datetime; lastUpdate = datetime.datetime.now() $>`: This gets the current time while compiling, then stores it in `lastUpdate`.
2. Data tags (`<$= data $>`): These tags take whatever the return value of the statement in the tags is, and write it directly to the page. Example: `<$= 'This application was compiled at %s.' % lastUpdate $>`: This will write the message in the string in the tags to the page.
**Note:** Logic tags take a higher priority and will always be interpreted first.
### How does the language feature work?
When you use a data tag to write a string to the page (e.g. `<$= LANG.HELLO_WORLD $>`), the language feature simply takes dictionary of the language that is currently being used from the language map file (`lang.json`), then searches for the key (being the variable name after the characters `LANG.` in the data tag, like `HELLO_WORLD` from the example before). It then writes that string to the page. Language variables are always prefixed with `LANG.` and should always be uppercase (as they are a constant).
### I changed a few things in the application and tried to view the updates in my browser, but nothing changed!
You most likely forgot to compile. Try running `python3 compile.py` and check again. If you are still having issues, [open up an issue](https://gitlab.com/beardog/Onionr/issues/new?issue[title]=Onionr UI not updating after compiling).

File diff suppressed because one or more lines are too long

View File

@ -1,19 +0,0 @@
<!-- Modal -->
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modal-title"><$= LANG.MODAL_TITLE $></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" id="modal-content"><$= LANG.MODAL_MESSAGE $></div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
<script src="js/main.js"></script>

View File

@ -1,30 +0,0 @@
<title><$= LANG.ONIONR_TITLE $></title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous" />
<link rel="stylesheet" type="text/css" href="css/main.css" />
<link rel="stylesheet" type="text/css" href="css/themes/dark.css" />
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<a class="navbar-brand" href="#">Onionr</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="index.html"><$= LANG.TIMELINE $></a>
</li>
<li class="nav-item">
<a class="nav-link" href="notifications.html"><$= LANG.NOTIFICATIONS $></a>
</li>
<li class="nav-item">
<a class="nav-link" href="messages.html"><$= LANG.MESSAGES $></a>
</li>
</ul>
</div>
</nav>

View File

@ -1,31 +0,0 @@
<!-- POST REPLIES -->
<div class="onionr-post-creator">
<div class="row">
<div class="onionr-reply-creator container">
<div class="row">
<div class="col-3">
<img class="onionr-post-creator-user-icon" id="onionr-reply-creator-user-icon">
</div>
<div class="col-9">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-creator-user-name" id="onionr-reply-creator-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')"></a>
<a class="onionr-post-creator-user-id" id="onionr-reply-creator-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id"><$= LANG.REPLY_CREATOR_YOU $></a>
</div>
</div>
<textarea class="onionr-post-creator-content" id="onionr-reply-creator-content" oninput="replyCreatorChange()"></textarea>
<div class="onionr-post-creator-content-message" id="onionr-reply-creator-content-message"></div>
<input type="button" onclick="makeReply()" title="<$= LANG.REPLY_CREATOR_CREATE $>" value="<$= LANG.REPLY_CREATOR_CREATE $>" id="onionr-reply-creator-create" class="onionr-post-creator-create" />
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div id="onionr-replies"></div>
</div>
<!-- END POST REPLIES -->

View File

@ -1,32 +0,0 @@
<!-- POST -->
<div class="col-12">
<div class="onionr-post" id="onionr-post-$post-hash" onclick="focusPost('$post-hash', 'user-id-url', 'user-name-url', '')">
<div class="row">
<div class="col-2">
<img class="onionr-post-user-icon" src="$user-image">
</div>
<div class="col-10">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-user-name" id="onionr-post-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')">$user-name</a>
<a class="onionr-post-user-id" id="onionr-post-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id">$user-id-truncated</a>
</div>
<div class="col col-auto text-right ml-auto pl-0">
<div class="onionr-post-date text-right" data-placement="top" data-toggle="tooltip" title="$date">$date-relative</div>
</div>
</div>
<div class="onionr-post-content">
$content
</div>
<div class="onionr-post-controls pt-2">
<a href="#!" onclick="toggleLike('$post-hash')" class="glyphicon glyphicon-heart mr-2">$liked</a>
<a href="#!" onclick="reply('$post-hash')" class="glyphicon glyphicon-comment mr-2"><$= LANG.POST_REPLY $></a>
</div>
</div>
</div>
</div>
</div>
<!-- END POST -->

View File

@ -1,30 +0,0 @@
<!-- POST FOCUS REPLIES -->
<div class="col-12">
<div class="row">
<div class="onionr-post-focus-reply-creator">
<div class="row">
<div class="col-1"></div>
<div class="col-2">
<img class="onionr-post-creator-user-icon" id="onionr-post-focus-reply-creator-user-icon">
</div>
<div class="col-9">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-creator-user-name" id="onionr-post-focus-reply-creator-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')"></a>
<a class="onionr-post-creator-user-id" id="onionr-post-focus-reply-creator-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id"><$= LANG.REPLY_CREATOR_YOU $></a>
</div>
</div>
<textarea class="onionr-post-creator-content" id="onionr-post-focus-reply-creator-content" oninput="focusReplyCreatorChange()"></textarea>
<div class="onionr-post-creator-content-message" id="onionr-post-focus-reply-creator-content-message"></div>
<input type="button" onclick="makeFocusReply()" title="<$= LANG.REPLY_CREATOR_CREATE $>" value="<$= LANG.REPLY_CREATOR_CREATE $>" id="onionr-post-focus-reply-creator-create" class="onionr-post-creator-create" />
</div>
</div>
</div>
<div class="onionr-post-focus-replies"></div>
</div>
</div>
<!-- END POST FOCUS REPLIES -->

View File

@ -1,31 +0,0 @@
<!-- POST -->
<div class="col-12">
<div class="onionr-post" id="onionr-post-$post-hash" onclick="focusPost('$post-hash', 'user-id-url', 'user-name-url', '')">
<div class="row">
<div class="col-3">
<img class="onionr-post-user-icon" src="$user-image">
</div>
<div class="col-9">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-user-name" id="onionr-post-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')">$user-name</a>
</div>
<div class="col col-auto text-right ml-auto pl-0">
<div class="onionr-post-date text-right" data-placement="top" data-toggle="tooltip" title="$date">$date-relative-truncated</div>
</div>
</div>
<div class="onionr-post-content">
$content
</div>
<div class="onionr-post-controls pt-2">
<a href="#!" onclick="toggleLike('$post-hash')" class="glyphicon glyphicon-heart mr-2">$liked</a>
<a href="#!" onclick="reply('$post-hash')" class="glyphicon glyphicon-comment mr-2"><$= LANG.POST_REPLY $></a>
</div>
</div>
</div>
</div>
</div>
<!-- END POST -->

View File

@ -1,130 +0,0 @@
#!/usr/bin/python3
import shutil, os, re, json, traceback
# get user's config
settings = {}
with open('config.json', 'r') as file:
settings = json.loads(file.read())
# "hardcoded" config, not for user to mess with
HEADER_FILE = 'common/header.html'
FOOTER_FILE = 'common/footer.html'
SRC_DIR = 'src/'
DST_DIR = 'dist/'
HEADER_STRING = '<header />'
FOOTER_STRING = '<footer />'
# remove dst folder
shutil.rmtree(DST_DIR, ignore_errors=True)
# taken from https://stackoverflow.com/questions/1868714/how-do-i-copy-an-entire-directory-of-files-into-an-existing-directory-using-pyth
def copytree(src, dst, symlinks=False, ignore=None):
for item in os.listdir(src):
s = os.path.join(src, item)
d = os.path.join(dst, item)
if os.path.isdir(s):
shutil.copytree(s, d, symlinks, ignore)
else:
shutil.copy2(s, d)
# copy src to dst
copytree(SRC_DIR, DST_DIR, False)
# load in lang map
langmap = {}
with open('lang.json', 'r') as file:
langmap = json.loads(file.read())[settings['language']]
LANG = type('LANG', (), langmap)
# templating
class Template:
def jsTemplate(template, filename = ''):
with open('common/%s.html' % template, 'r') as file:
return Template.parseTags(file.read().replace('\\', '\\\\').replace('\'', '\\\'').replace('\n', "\\\n"), filename)
def htmlTemplate(template, filename = ''):
with open('common/%s.html' % template, 'r') as file:
return Template.parseTags(file.read(), filename)
# tag parser
def parseTags(contents, filename = ''):
# <$ logic $>
for match in re.findall(r'(<\$(?!=)(.*?)\$>)', contents):
try:
out = exec(match[1].strip())
contents = contents.replace(match[0], '' if out is None else str(out))
except Exception as e:
print('Error: Failed to execute python tag (%s): %s\n' % (filename, match[1]))
traceback.print_exc()
print('\nIgnoring this error, continuing to compile...\n')
# <$= data $>
for match in re.findall(r'(<\$=(.*?)\$>)', contents):
try:
out = eval(match[1].strip())
contents = contents.replace(match[0], '' if out is None else str(out))
except (NameError, AttributeError) as e:
name = match[1].strip()
print('Warning: %s does not exist, treating as an str' % name)
contents = contents.replace(match[0], name)
except Exception as e:
print('Error: Failed to execute python tag (%s): %s\n' % (filename, match[1]))
traceback.print_exc()
print('\nIgnoring this error, continuing to compile...\n')
return contents
def jsTemplate(contents):
return Template.jsTemplate(contents)
def htmlTemplate(contents):
return Template.htmlTemplate(contents)
# get header file
with open(HEADER_FILE, 'r') as file:
HEADER_FILE = file.read()
if settings['python_tags']:
HEADER_FILE = Template.parseTags(HEADER_FILE)
# get footer file
with open(FOOTER_FILE, 'r') as file:
FOOTER_FILE = file.read()
if settings['python_tags']:
FOOTER_FILE = Template.parseTags(FOOTER_FILE)
# iterate dst, replace files
def iterate(directory):
for filename in os.listdir(directory):
if filename.split('.')[-1].lower() in ['htm', 'html', 'css', 'js']:
try:
path = os.path.join(directory, filename)
if os.path.isdir(path):
iterate(path)
else:
contents = ''
with open(path, 'r') as file:
# get file contents
contents = file.read()
os.remove(path)
with open(path, 'w') as file:
# set the header & footer
contents = contents.replace(HEADER_STRING, HEADER_FILE)
contents = contents.replace(FOOTER_STRING, FOOTER_FILE)
# do python tags
if settings['python_tags']:
contents = Template.parseTags(contents, filename)
# write file
file.write(contents)
except Exception as e:
print('Error: Failed to parse file: %s\n' % filename)
traceback.print_exc()
print('\nIgnoring this error, continuing to compile...\n')
iterate(DST_DIR)

View File

@ -1,4 +0,0 @@
{
"language" : "eng",
"python_tags" : true
}

View File

@ -1,122 +0,0 @@
/* general formatting */
@media (min-width: 768px) {
.container-small {
width: 300px;
}
.container-large {
width: 970px;
}
}
@media (min-width: 992px) {
.container-small {
width: 500px;
}
.container-large {
width: 1170px;
}
}
@media (min-width: 1200px) {
.container-small {
width: 700px;
}
.container-large {
width: 1500px;
}
}
.container-small, .container-large {
max-width: 100%;
}
/* navbar */
body {
margin-top: 5rem;
}
/* timeline */
.onionr-post-focus-separator {
width: 100%;
padding: 1rem;
padding-left: 0;
padding-right: 0;
}
.onionr-post {
padding: 1rem;
margin-bottom: 1rem;
cursor: pointer;
width: 100%;
}
.onionr-post-user-name {
display: inline;
}
.onionr-post-user-id:before { content: "("; }
.onionr-post-user-id:after { content: ")"; }
.onionr-post-content {
word-wrap: break-word;
}
.onionr-post-user-icon {
border-radius: 100%;
width: 100%;
}
.onionr-post-creator {
padding: 1rem;
margin-bottom: 1rem;
width: 100%;
}
.onionr-post-creator-user-name {
display: inline;
}
.onionr-post-creator-user-id:before { content: "("; }
.onionr-post-creator-user-id:after { content: ")"; }
.onionr-post-creator-content {
word-wrap: break-word;
width: 100%;
}
.onionr-post-creator-user-icon {
border-radius: 100%;
width: 100%;
}
.onionr-post-creator-create {
width: 100%;
text-align: center;
}
.h-divider {
margin: 5px 15px;
height: 1px;
width: 100%;
}
/* profile */
.onionr-profile-user-icon {
border-radius: 100%;
width: 100%;
margin-bottom: 1rem;
}
.onionr-profile-username {
text-align: center;
}
.onionr-profile-save {
width: 100%;
}

View File

@ -1,76 +0,0 @@
body {
background-color: #96928f;
color: #25383C;
}
/* timeline */
.onionr-post-focus-separator {
border-color: black;
}
.modal-content {
border: 1px solid black;
border-radius: 1rem;
background-color: lightgray;
}
.onionr-post {
border: 1px solid black;
border-radius: 1rem;
background-color: lightgray;
}
.onionr-post-user-name {
color: green;
font-weight: bold;
}
.onionr-post-user-id {
color: gray;
}
.onionr-post-date {
color: gray;
}
.onionr-post-content {
font-family: sans-serif, serif;
border-top: 1px solid black;
font-size: 15pt;
}
.onionr-post-creator {
border: 1px solid black;
border-radius: 1rem;
background-color: lightgray;
}
.onionr-post-creator-user-name {
color: green;
font-weight: bold;
}
.onionr-post-creator-user-id {
color: gray;
}
.onionr-post-creator-date {
color: gray;
}
.onionr-post-creator-content {
font-family: sans-serif, serif;
border-top: 1px solid black;
font-size: 15pt;
background-color: lightgray;
color: black;
border-width: 0px;
}
.h-divider {
border-top:1px solid gray;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -1,215 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Onionr UI</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous" />
<link rel="stylesheet" type="text/css" href="css/main.css" />
<link rel="stylesheet" type="text/css" href="css/themes/dark.css" />
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<a class="navbar-brand" href="#">Onionr</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="index.html">Timeline</a>
</li>
<li class="nav-item">
<a class="nav-link" href="notifications.html">Notifications</a>
</li>
<li class="nav-item">
<a class="nav-link" href="messages.html">Messages</a>
</li>
</ul>
</div>
</nav>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-12 col-lg-3">
<div class="onionr-profile">
<div class="row">
<div class="col-4 col-lg-12">
<img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="">
</div>
<div class="col-8 col-lg-12">
<h2 maxlength="25" id="onionr-profile-username" class="onionr-profile-username text-left text-lg-center text-sm-left" data-placement="top" data-toggle="tooltip" title="unknown" data-editable></h2>
</div>
<div class="col-12">
<p maxlength="128" id="onionr-profile-description" class="onionr-profile-description" data-editable></p>
</div>
<div class="col-12 onionr-profile-edit" id="onionr-profile-edit" style="display: none">
<div class="row">
<div class="col-sm-6 col-lg-12">
<input type="button" onclick="updateUser()" class="onionr-profile-save text-center" id="onionr-profile-save" value="Save" />
</div>
<div class="col-sm-6 col-lg-12">
<input type="button" onclick="cancelUpdate()" class="onionr-profile-save text-center" id="onionr-profile-cancel" value="Cancel" />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="h-divider pb-3 d-block d-lg-none"></div>
<div class="col-sm-12 col-lg-6">
<div class="row" id="onionr-timeline-post-creator">
<div class="col-12">
<div class="onionr-timeline">
<h2>Timeline</h2>
</div>
</div>
<!-- POST CREATOR -->
<div class="col-12">
<div class="onionr-post-creator">
<div class="row">
<div class="col-2">
<img class="onionr-post-creator-user-icon" id="onionr-post-creator-user-icon">
</div>
<div class="col-10">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-creator-user-name" id="onionr-post-creator-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')"></a>
<a class="onionr-post-creator-user-id" id="onionr-post-creator-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id">you</a>
</div>
</div>
<textarea class="onionr-post-creator-content" id="onionr-post-creator-content" oninput="postCreatorChange()"></textarea>
<div class="onionr-post-creator-content-message" id="onionr-post-creator-content-message"></div>
<input type="button" onclick="makePost()" title="Create post" value="Create post" id="onionr-post-creator-create" class="onionr-post-creator-create" />
</div>
</div>
</div>
</div>
<!-- END POST CREATOR -->
</div>
<div class="row" id="onionr-timeline-posts">
</div>
</div>
<div class="d-none d-lg-block col-lg-3">
<div class="row">
<div class="col-12">
<div class="onionr-replies">
<h2 id="onionr-replies-title"></h2>
</div>
</div>
<div id="onionr-reply-creator-panel">
</div>
</div>
<div class="row">
</div>
</div>
</div>
</div>
<!-- POST FOCUS DIALOG -->
<div class="modal fade" id="onionr-post-focus" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="row p-3">
<div class="col-2">
<img src="" id="onionr-post-focus-user-icon" class="onionr-post-user-icon">
</div>
<div class="col-10">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-user-name" id="onionr-post-focus-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url'); jQuery('#onionr-post-focus').modal('hide');">$user-name</a>
<a class="onionr-post-user-id" id="onionr-post-focus-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url'); jQuery('#onionr-post-focus').modal('hide');" data-placement="top" data-toggle="tooltip" title="$user-id">$user-id-truncated</a>
</div>
<div class="col col-auto text-right ml-auto pl-0">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
<div class="onionr-post-content" id="onionr-post-focus-content">
</div>
</div>
<hr class="col-12 onionr-post-focus-separator" />
<!-- POST FOCUS REPLIES -->
<div class="col-12">
<div class="row">
<div class="onionr-post-focus-reply-creator">
<div class="row">
<div class="col-1"></div>
<div class="col-2">
<img class="onionr-post-creator-user-icon" id="onionr-post-focus-reply-creator-user-icon">
</div>
<div class="col-9">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-creator-user-name" id="onionr-post-focus-reply-creator-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')"></a>
<a class="onionr-post-creator-user-id" id="onionr-post-focus-reply-creator-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id">you</a>
</div>
</div>
<textarea class="onionr-post-creator-content" id="onionr-post-focus-reply-creator-content" oninput="focusReplyCreatorChange()"></textarea>
<div class="onionr-post-creator-content-message" id="onionr-post-focus-reply-creator-content-message"></div>
<input type="button" onclick="makeFocusReply()" title="Reply" value="Reply" id="onionr-post-focus-reply-creator-create" class="onionr-post-creator-create" />
</div>
</div>
</div>
<div class="onionr-post-focus-replies"></div>
</div>
</div>
<!-- END POST FOCUS REPLIES -->
</div>
</div>
</div>
</div>
<!-- END POST FOCUS DIALOG -->
<!-- Modal -->
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modal-title">Loading...</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" id="modal-content">Onionr has begun performing a CPU-intensive operation. If this operation does not complete in the next 10 seconds, try reloading the page.</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
<script src="js/main.js"></script>
<script src="js/timeline.js"></script>
</body>
</html>

View File

@ -1,491 +0,0 @@
/* just for testing rn */
Block.getBlocks({'type' : 'onionr-post', 'signed' : true, 'reverse' : true}, function(data) {
for(var i = 0; i < data.length; i++) {
try {
var block = data[i];
var finished = false;
User.getUser(new String(block.getHeader('signer', 'unknown')), function(user) {
var post = new Post();
var blockContent = JSON.parse(block.getContent());
// just ignore anything shorter than 280 characters
if(String(blockContent['content']).length <= 280 && block.getParent() === null) {
post.setContent(blockContent['content']);
post.setPostDate(block.getDate());
post.setUser(user);
post.setHash(block.getHash());
document.getElementById('onionr-timeline-posts').innerHTML += post.getHTML();
}
finished = true;
});
while(!finished);
} catch(e) {
console.log('Troublemaker block: ' + data[i].getHash());
console.log(e);
}
}
});
function toggleLike(hash) {
var post = getPostMap(hash);
if(post === null || !getPostMap()[hash]['liked']) {
console.log('Liking ' + hash + '...');
if(post === null)
getPostMap()[hash] = {};
getPostMap()[hash]['liked'] = true;
set('postmap', JSON.stringify(getPostMap()));
var block = new Block();
block.setType('onionr-post-like');
block.setContent(JSON.stringify({'hash' : hash}));
block.save(true, function(hash) {});
} else {
console.log('Unliking ' + hash + '...');
}
}
function postCreatorChange() {
var content = document.getElementById('onionr-post-creator-content').value;
var message = '';
var maxlength = 280;
var disable = true;
var warn = false;
if(content.length !== 0) {
if(content.length - content.replaceAll('\n', '').length > 16) {
// 16 max newlines
message = 'Please use less than 16 newlines';
} else if(content.length <= maxlength) {
// 280 max characters
message = '%s characters remaining'.replaceAll('%s', (280 - content.length));
disable = false;
if(maxlength - content.length < maxlength / 4) {
warn = true;
}
} else {
message = '%s characters over maximum'.replaceAll('%s', (content.length - maxlength));
}
}
var element = document.getElementById('onionr-post-creator-content-message');
var button = document.getElementById("onionr-post-creator-create");
if(message === '')
element.style.visibility = 'hidden';
else {
element.style.visibility = 'visible';
element.innerHTML = message;
if(disable)
element.style.color = 'red';
else if(warn)
element.style.color = '#FF8C00';
else
element.style.color = 'gray';
}
if(disable)
button.disabled = true;
else
button.disabled = false;
}
function replyCreatorChange() {
var content = document.getElementById('onionr-reply-creator-content').value;
var message = '';
var maxlength = 280;
var disable = true;
var warn = false;
if(content.length !== 0) {
if(content.length - content.replaceAll('\n', '').length > 16) {
// 16 max newlines
message = 'Please use less than 16 newlines';
} else if(content.length <= maxlength) {
// 280 max characters
message = '%s characters remaining'.replaceAll('%s', (280 - content.length));
disable = false;
if(maxlength - content.length < maxlength / 4) {
warn = true;
}
} else {
message = '%s characters over maximum'.replaceAll('%s', (content.length - maxlength));
}
}
var element = document.getElementById('onionr-reply-creator-content-message');
var button = document.getElementById("onionr-reply-creator-create");
if(message === '')
element.style.visibility = 'hidden';
else {
element.style.visibility = 'visible';
element.innerHTML = message;
if(disable)
element.style.color = 'red';
else if(warn)
element.style.color = '#FF8C00';
else
element.style.color = 'gray';
}
if(disable)
button.disabled = true;
else
button.disabled = false;
}
function focusReplyCreatorChange() {
var content = document.getElementById('onionr-post-focus-reply-creator-content').value;
var message = '';
var maxlength = 280;
var disable = true;
var warn = false;
if(content.length !== 0) {
if(content.length - content.replaceAll('\n', '').length > 16) {
// 16 max newlines
message = 'Please use less than 16 newlines';
} else if(content.length <= maxlength) {
// 280 max characters
message = '%s characters remaining'.replaceAll('%s', (280 - content.length));
disable = false;
if(maxlength - content.length < maxlength / 4) {
warn = true;
}
} else {
message = '%s characters over maximum'.replaceAll('%s', (content.length - maxlength));
}
}
var element = document.getElementById('onionr-post-focus-reply-creator-content-message');
var button = document.getElementById("onionr-post-focus-reply-creator-create");
if(message === '')
element.style.visibility = 'hidden';
else {
element.style.visibility = 'visible';
element.innerHTML = message;
if(disable)
element.style.color = 'red';
else if(warn)
element.style.color = '#FF8C00';
else
element.style.color = 'gray';
}
if(disable)
button.disabled = true;
else
button.disabled = false;
}
function viewProfile(id, name) {
id = decodeURIComponent(id);
document.getElementById("onionr-profile-username").innerHTML = Sanitize.html(decodeURIComponent(name));
User.getUser(id, function(data) {
if(data !== null) {
document.getElementById("onionr-profile-user-icon").src = "data:image/jpeg;base64," + Sanitize.html(data.getIcon());
document.getElementById("onionr-profile-user-icon").b64 = Sanitize.html(data.getIcon());
document.getElementById("onionr-profile-username").innerHTML = Sanitize.html(Sanitize.username(data.getName()));
document.getElementById("onionr-profile-username").title = Sanitize.html(data.getID());
document.getElementById("onionr-profile-description").innerHTML = Sanitize.html(Sanitize.description(data.getDescription()));
}
});
}
function updateUser() {
toggleSaveButton(false);
// jQuery('#modal').modal('show');
var name = jQuery('#onionr-profile-username').text();
var id = document.getElementById("onionr-profile-username").title;
var icon = document.getElementById("onionr-profile-user-icon").b64;
var description = jQuery("#onionr-profile-description").text();
var user = new User();
user.setName(name);
user.setID(id);
user.setIcon(icon);
user.setDescription(Sanitize.description(description));
user.remember();
user.save(function() {
setCurrentUser(user);
window.location.reload();
});
}
function cancelUpdate() {
toggleSaveButton(false);
var name = jQuery('#onionr-profile-username').text();
var id = document.getElementById("onionr-profile-username").title;
viewProfile(id, name);
}
function toggleSaveButton(show) {
document.getElementById("onionr-profile-edit").style.display = (show ? 'block' : 'none');
}
function makePost() {
var content = document.getElementById("onionr-post-creator-content").value;
if(content.trim() !== '') {
var post = new Post();
post.setUser(getCurrentUser());
post.setContent(content);
post.setPostDate(new Date());
post.save(function(data) {}); // async, but no function
document.getElementById('onionr-timeline-posts').innerHTML = post.getHTML() + document.getElementById('onionr-timeline-posts').innerHTML;
document.getElementById("onionr-post-creator-content").value = "";
document.getElementById("onionr-post-creator-content").focus();
postCreatorChange();
} else {
console.log('Not making empty post.');
}
}
function getReplies(id, callback) {
Block.getBlocks({'type' : 'onionr-post', 'parent' : id, 'signed' : true, 'reverse' : true}, callback);
}
function focusPost(id) {
viewReplies(id);
}
function viewRepliesMobile(id) {
var post = document.getElementById('onionr-post-' + id);
var user_name = '';
var user_id = '';
var user_id_trunc = '';
var user_icon = '';
var post_content = '';
if(post !== null && post !== undefined) {
// if the post is in the timeline, get the data from it
user_name = post.getElementsByClassName('onionr-post-user-name')[0].innerHTML;
user_id = post.getElementsByClassName('onionr-post-user-id')[0].title;
user_id_trunc = post.getElementsByClassName('onionr-post-user-id')[0].innerHTML;
user_icon = post.getElementsByClassName('onionr-post-user-icon')[0].src;
post_content = post.getElementsByClassName('onionr-post-content')[0].innerHTML;
} else {
// otherwise, fetch the data
}
document.getElementById('onionr-post-focus-user-icon').src = user_icon;
document.getElementById('onionr-post-focus-user-name').innerHTML = user_name;
document.getElementById('onionr-post-focus-user-id').innerHTML = user_id_trunc;
document.getElementById('onionr-post-focus-user-id').title = user_id;
document.getElementById('onionr-post-focus-content').innerHTML = post_content;
document.getElementById('onionr-post-focus-reply-creator-user-name').innerHTML = Sanitize.html(Sanitize.username(getCurrentUser().getName()));
document.getElementById('onionr-post-focus-reply-creator-user-icon').src = "data:image/jpeg;base64," + Sanitize.html(getCurrentUser().getIcon());
document.getElementById('onionr-post-focus-reply-creator-content').value = '';
document.getElementById('onionr-post-focus-reply-creator-content-message').value = '';
jQuery('#onionr-post-focus').modal('show');
}
function viewReplies(id) {
document.getElementById('onionr-replies-title').innerHTML = 'Replies';
document.getElementById('onionr-reply-creator-panel').originalPost = id;
document.getElementById('onionr-reply-creator-panel').innerHTML = '<!-- POST REPLIES -->\
<div class="onionr-post-creator">\
<div class="row">\
<div class="onionr-reply-creator container">\
<div class="row">\
<div class="col-3">\
<img class="onionr-post-creator-user-icon" id="onionr-reply-creator-user-icon">\
</div>\
<div class="col-9">\
<div class="row">\
<div class="col col-auto">\
<a class="onionr-post-creator-user-name" id="onionr-reply-creator-user-name" href="#!" onclick="viewProfile(\'$user-id-url\', \'$user-name-url\')"></a>\
<a class="onionr-post-creator-user-id" id="onionr-reply-creator-user-id" href="#!" onclick="viewProfile(\'$user-id-url\', \'$user-name-url\')" data-placement="top" data-toggle="tooltip" title="$user-id">you</a>\
</div>\
</div>\
\
<textarea class="onionr-post-creator-content" id="onionr-reply-creator-content" oninput="replyCreatorChange()"></textarea>\
\
<div class="onionr-post-creator-content-message" id="onionr-reply-creator-content-message"></div>\
\
<input type="button" onclick="makeReply()" title="Reply" value="Reply" id="onionr-reply-creator-create" class="onionr-post-creator-create" />\
</div>\
</div>\
</div>\
</div>\
</div>\
\
<div class="row">\
<div id="onionr-replies"></div>\
</div>\
<!-- END POST REPLIES -->\
';
document.getElementById('onionr-reply-creator-content').innerHTML = '';
document.getElementById("onionr-reply-creator-content").placeholder = "Enter a message here...";
document.getElementById('onionr-reply-creator-user-name').innerHTML = Sanitize.html(Sanitize.username(getCurrentUser().getName()));
document.getElementById('onionr-reply-creator-user-icon').src = "data:image/jpeg;base64," + Sanitize.html(getCurrentUser().getIcon());
document.getElementById('onionr-replies').innerHTML = '';
getReplies(id, function(data) {
var replies = document.getElementById('onionr-replies');
replies.innerHTML = '';
for(var i = 0; i < data.length; i++) {
try {
var block = data[i];
var finished = false;
User.getUser(new String(block.getHeader('signer', 'unknown')), function(user) {
var post = new Post();
var blockContent = JSON.parse(block.getContent());
// just ignore anything shorter than 280 characters
if(String(blockContent['content']).length <= 280) {
post.setContent(blockContent['content']);
post.setPostDate(block.getDate());
post.setUser(user);
post.setHash(block.getHash());
replies.innerHTML += post.getHTML('reply');
}
finished = true;
});
while(!finished);
} catch(e) {
console.log('Troublemaker block: ' + data[i].getHash());
console.log(e);
}
}
});
}
function makeReply() {
var content = document.getElementById("onionr-reply-creator-content").value;
if(content.trim() !== '') {
var post = new Post();
var originalPost = document.getElementById('onionr-reply-creator-panel').originalPost;
console.log('Original post hash: ' + originalPost);
post.setUser(getCurrentUser());
post.setParent(originalPost);
post.setContent(content);
post.setPostDate(new Date());
post.save(function(data) {}); // async, but no function
document.getElementById('onionr-replies').innerHTML = post.getHTML('reply') + document.getElementById('onionr-replies').innerHTML;
document.getElementById("onionr-reply-creator-content").value = "";
document.getElementById("onionr-reply-creator-content").focus();
replyCreatorChange();
} else {
console.log('Not making empty reply.');
}
}
jQuery('body').on('click', '[data-editable]', function() {
var el = jQuery(this);
var txt = el.text();
var maxlength = el.attr("maxlength");
var input = jQuery('<input/>').val(txt);
input.attr('maxlength', maxlength);
el.replaceWith(input);
var save = function() {
var newTxt = input.val();
if(el.attr('id') === 'onionr-profile-username')
newTxt = Sanitize.username(newTxt);
if(el.attr('id') === 'onionr-profile-description')
newTxt = Sanitize.description(newTxt);
var p = el.text(newTxt);
input.replaceWith(p);
if(newTxt !== txt)
toggleSaveButton(true);
};
var saveEnter = function(event) {
console.log(event);
console.log(event.keyCode);
if (event.keyCode === 13)
save();
};
input.one('blur', save).bind('keyup', saveEnter).focus();
});
//viewProfile('$user-id-url', '$user-name-url')
// jQuery('#onionr-post-user-id').on('click', function(e) { alert(3);});
//jQuery('#onionr-post *').on('click', function(e) { e.stopPropagation(); });
// jQuery('#onionr-post').click(function(e) { alert(1); });
currentUser = getCurrentUser();
if(currentUser !== undefined && currentUser !== null) {
document.getElementById("onionr-post-creator-user-name").innerHTML = Sanitize.html(currentUser.getName());
document.getElementById("onionr-post-creator-user-id").innerHTML = "you";
document.getElementById("onionr-post-creator-user-icon").src = "data:image/jpeg;base64," + Sanitize.html(currentUser.getIcon());
document.getElementById("onionr-post-creator-user-id").title = currentUser.getID();
document.getElementById("onionr-post-creator-content").placeholder = "Enter a message here...";
document.getElementById("onionr-post-focus-reply-creator-content").placeholder = "Enter a message here...";
document.getElementById("onionr-post-focus-reply-creator-user-id").innerHTML = "you";
}
viewCurrentProfile = function() {
viewProfile(encodeURIComponent(currentUser.getID()), encodeURIComponent(currentUser.getName()));
}
document.getElementById("onionr-post-creator-user-id").onclick = viewCurrentProfile;
document.getElementById("onionr-post-creator-user-name").onclick = viewCurrentProfile;
// on some browsers it saves the user input on reload. So, it should also recheck the input.
postCreatorChange();

View File

@ -1,65 +0,0 @@
{
"eng" : {
"ONIONR_TITLE" : "Onionr UI",
"TIMELINE" : "Timeline",
"NOTIFICATIONS" : "Notifications",
"MESSAGES" : "Messages",
"LATEST" : "Latest...",
"TRENDING" : "Trending",
"REPLIES" : "Replies",
"MODAL_TITLE" : "Loading...",
"MODAL_MESSAGE" : "Onionr has begun performing a CPU-intensive operation. If this operation does not complete in the next 10 seconds, try reloading the page.",
"POST_LIKE" : "like",
"POST_UNLIKE" : "unlike",
"POST_REPLY" : "reply",
"POST_CREATOR_YOU" : "you",
"POST_CREATOR_PLACEHOLDER" : "Enter a message here...",
"POST_CREATOR_CREATE" : "Create post",
"REPLY_CREATOR_YOU" : "you",
"REPLY_CREATOR_PLACEHOLDER" : "Enter reply here...",
"REPLY_CREATOR_CREATE" : "Reply",
"POST_CREATOR_MESSAGE_MAXIMUM_NEWLINES" : "Please use less than 16 newlines",
"POST_CREATOR_MESSAGE_REMAINING" : "%s characters remaining",
"POST_CREATOR_MESSAGE_OVER" : "%s characters over maximum",
"REPLY_CREATOR_MESSAGE_MAXIMUM_NEWLINES" : "Please use less than 16 newlines",
"REPLY_CREATOR_MESSAGE_REMAINING" : "%s characters remaining",
"REPLY_CREATOR_MESSAGE_OVER" : "%s characters over maximum",
"PROFILE_EDIT_SAVE" : "Save",
"PROFILE_EDIT_CANCEL" : "Cancel"
},
"spa" : {
"ONIONR_TITLE" : "Onionr UI",
"TIMELINE" : "Linea de Tiempo",
"NOTIFICATIONS" : "Notificaciones",
"MESSAGES" : "Mensaje",
"TRENDING" : "Trending",
"POST_LIKE" : "me gusta",
"POST_REPLY" : "comentario"
},
"zho" : {
"ONIONR_TITLE" : "洋葱 用户界面",
"TIMELINE" : "时间线",
"NOTIFICATIONS" : "通知",
"MESSAGES" : "消息",
"TRENDING" : "趋势",
"POST_LIKE" : "喜欢",
"POST_REPLY" : "回复"
}
}

View File

@ -1,122 +0,0 @@
/* general formatting */
@media (min-width: 768px) {
.container-small {
width: 300px;
}
.container-large {
width: 970px;
}
}
@media (min-width: 992px) {
.container-small {
width: 500px;
}
.container-large {
width: 1170px;
}
}
@media (min-width: 1200px) {
.container-small {
width: 700px;
}
.container-large {
width: 1500px;
}
}
.container-small, .container-large {
max-width: 100%;
}
/* navbar */
body {
margin-top: 5rem;
}
/* timeline */
.onionr-post-focus-separator {
width: 100%;
padding: 1rem;
padding-left: 0;
padding-right: 0;
}
.onionr-post {
padding: 1rem;
margin-bottom: 1rem;
cursor: pointer;
width: 100%;
}
.onionr-post-user-name {
display: inline;
}
.onionr-post-user-id:before { content: "("; }
.onionr-post-user-id:after { content: ")"; }
.onionr-post-content {
word-wrap: break-word;
}
.onionr-post-user-icon {
border-radius: 100%;
width: 100%;
}
.onionr-post-creator {
padding: 1rem;
margin-bottom: 1rem;
width: 100%;
}
.onionr-post-creator-user-name {
display: inline;
}
.onionr-post-creator-user-id:before { content: "("; }
.onionr-post-creator-user-id:after { content: ")"; }
.onionr-post-creator-content {
word-wrap: break-word;
width: 100%;
}
.onionr-post-creator-user-icon {
border-radius: 100%;
width: 100%;
}
.onionr-post-creator-create {
width: 100%;
text-align: center;
}
.h-divider {
margin: 5px 15px;
height: 1px;
width: 100%;
}
/* profile */
.onionr-profile-user-icon {
border-radius: 100%;
width: 100%;
margin-bottom: 1rem;
}
.onionr-profile-username {
text-align: center;
}
.onionr-profile-save {
width: 100%;
}

View File

@ -1,76 +0,0 @@
body {
background-color: #96928f;
color: #25383C;
}
/* timeline */
.onionr-post-focus-separator {
border-color: black;
}
.modal-content {
border: 1px solid black;
border-radius: 1rem;
background-color: lightgray;
}
.onionr-post {
border: 1px solid black;
border-radius: 1rem;
background-color: lightgray;
}
.onionr-post-user-name {
color: green;
font-weight: bold;
}
.onionr-post-user-id {
color: gray;
}
.onionr-post-date {
color: gray;
}
.onionr-post-content {
font-family: sans-serif, serif;
border-top: 1px solid black;
font-size: 15pt;
}
.onionr-post-creator {
border: 1px solid black;
border-radius: 1rem;
background-color: lightgray;
}
.onionr-post-creator-user-name {
color: green;
font-weight: bold;
}
.onionr-post-creator-user-id {
color: gray;
}
.onionr-post-creator-date {
color: gray;
}
.onionr-post-creator-content {
font-family: sans-serif, serif;
border-top: 1px solid black;
font-size: 15pt;
background-color: lightgray;
color: black;
border-width: 0px;
}
.h-divider {
border-top:1px solid gray;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -1,136 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<header />
</head>
<body>
<div class="container">
<div class="row">
<div class="col-12 col-lg-3">
<div class="onionr-profile">
<div class="row">
<div class="col-4 col-lg-12">
<img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="">
</div>
<div class="col-8 col-lg-12">
<h2 maxlength="25" id="onionr-profile-username" class="onionr-profile-username text-left text-lg-center text-sm-left" data-placement="top" data-toggle="tooltip" title="unknown" data-editable></h2>
</div>
<div class="col-12">
<p maxlength="128" id="onionr-profile-description" class="onionr-profile-description" data-editable></p>
</div>
<div class="col-12 onionr-profile-edit" id="onionr-profile-edit" style="display: none">
<div class="row">
<div class="col-sm-6 col-lg-12">
<input type="button" onclick="updateUser()" class="onionr-profile-save text-center" id="onionr-profile-save" value="<$= LANG.PROFILE_EDIT_SAVE $>" />
</div>
<div class="col-sm-6 col-lg-12">
<input type="button" onclick="cancelUpdate()" class="onionr-profile-save text-center" id="onionr-profile-cancel" value="<$= LANG.PROFILE_EDIT_CANCEL $>" />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="h-divider pb-3 d-block d-lg-none"></div>
<div class="col-sm-12 col-lg-6">
<div class="row" id="onionr-timeline-post-creator">
<div class="col-12">
<div class="onionr-timeline">
<h2><$= LANG.TIMELINE $></h2>
</div>
</div>
<!-- POST CREATOR -->
<div class="col-12">
<div class="onionr-post-creator">
<div class="row">
<div class="col-2">
<img class="onionr-post-creator-user-icon" id="onionr-post-creator-user-icon">
</div>
<div class="col-10">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-creator-user-name" id="onionr-post-creator-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')"></a>
<a class="onionr-post-creator-user-id" id="onionr-post-creator-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id"><$= LANG.POST_CREATOR_YOU $></a>
</div>
</div>
<textarea class="onionr-post-creator-content" id="onionr-post-creator-content" oninput="postCreatorChange()"></textarea>
<div class="onionr-post-creator-content-message" id="onionr-post-creator-content-message"></div>
<input type="button" onclick="makePost()" title="<$= LANG.POST_CREATOR_CREATE $>" value="<$= LANG.POST_CREATOR_CREATE $>" id="onionr-post-creator-create" class="onionr-post-creator-create" />
</div>
</div>
</div>
</div>
<!-- END POST CREATOR -->
</div>
<div class="row" id="onionr-timeline-posts">
</div>
</div>
<div class="d-none d-lg-block col-lg-3">
<div class="row">
<div class="col-12">
<div class="onionr-replies">
<h2 id="onionr-replies-title"></h2>
</div>
</div>
<div id="onionr-reply-creator-panel">
</div>
</div>
<div class="row">
</div>
</div>
</div>
</div>
<!-- POST FOCUS DIALOG -->
<div class="modal fade" id="onionr-post-focus" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="row p-3">
<div class="col-2">
<img src="" id="onionr-post-focus-user-icon" class="onionr-post-user-icon">
</div>
<div class="col-10">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-user-name" id="onionr-post-focus-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url'); jQuery('#onionr-post-focus').modal('hide');">$user-name</a>
<a class="onionr-post-user-id" id="onionr-post-focus-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url'); jQuery('#onionr-post-focus').modal('hide');" data-placement="top" data-toggle="tooltip" title="$user-id">$user-id-truncated</a>
</div>
<div class="col col-auto text-right ml-auto pl-0">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
<div class="onionr-post-content" id="onionr-post-focus-content">
</div>
</div>
<hr class="col-12 onionr-post-focus-separator" />
<$= htmlTemplate('onionr-timeline-reply-creator') $>
</div>
</div>
</div>
</div>
<!-- END POST FOCUS DIALOG -->
<footer />
<script src="js/timeline.js"></script>
</body>
</html>

View File

@ -1,689 +0,0 @@
/* handy localstorage functions for quick usage */
function set(key, val) {
return localStorage.setItem(key, val);
}
function get(key, df) { // df is default
value = localStorage.getItem(key);
if(value == null)
value = df;
return value;
}
function remove(key) {
return localStorage.removeItem(key);
}
function getParameter(name) {
var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}
/* usermap localStorage stuff */
var usermap = JSON.parse(get('usermap', '{}'));
var postmap = JSON.parse(get('postmap', '{}'))
function getUserMap() {
return usermap;
}
function getPostMap(hash) {
if(hash !== undefined) {
if(hash in postmap)
return postmap[hash];
return null;
}
return postmap;
}
function deserializeUser(id) {
if(!(id in getUserMap()))
return null;
var serialized = getUserMap()[id]
var user = new User();
user.setName(serialized['name']);
user.setID(serialized['id']);
user.setIcon(serialized['icon']);
user.setDescription(serialized['description']);
return user;
}
function getCurrentUser() {
var user = get('currentUser', null);
if(user === null)
return null;
return User.getUser(user, function() {});
}
function setCurrentUser(user) {
set('currentUser', user.getID());
}
/* returns a relative date format, e.g. "5 minutes" */
function timeSince(date, size) {
// taken from https://stackoverflow.com/a/3177838/3678023
var seconds = Math.floor((new Date() - date) / 1000);
var interval = Math.floor(seconds / 31536000);
if (size === null)
size = 'desktop';
var dates = {
'mobile' : {
'yr' : 'yrs',
'mo' : 'mo',
'd' : 'd',
'hr' : 'h',
'min' : 'm',
'secs' : 's',
'sec' : 's',
},
'desktop' : {
'yr' : ' years',
'mo' : ' months',
'd' : ' days',
'hr' : ' hours',
'min' : ' minutes',
'secs' : ' seconds',
'sec' : ' second',
},
};
if (interval > 1)
return interval + dates[size]['yr'];
interval = Math.floor(seconds / 2592000);
if (interval > 1)
return interval + dates[size]['mo'];
interval = Math.floor(seconds / 86400);
if (interval > 1)
return interval + dates[size]['d'];
interval = Math.floor(seconds / 3600);
if (interval > 1)
return interval + dates[size]['hr'];
interval = Math.floor(seconds / 60);
if (interval > 1)
return interval + dates[size]['min'];
if(Math.floor(seconds) !== 1)
return Math.floor(seconds) + dates[size]['secs'];
return '1' + dates[size]['sec'];
}
/* replace all instances of string */
String.prototype.replaceAll = function(search, replacement, limit) {
// taken from https://stackoverflow.com/a/17606289/3678023
var target = this;
return target.split(search, limit).join(replacement);
};
/* useful functions to sanitize data */
class Sanitize {
/* sanitizes HTML in a string */
static html(html) {
return String(html).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* URL encodes a string */
static url(url) {
return encodeURIComponent(url);
}
/* usernames */
static username(username) {
return String(username).replace(/[\W_]+/g, " ").substring(0, 25);
}
/* profile descriptions */
static description(description) {
return String(description).substring(0, 128);
}
}
/* config stuff */
function getWebPassword() {
return get("web-password", null);
}
function setWebPassword(password) {
return set("web-password", password);
}
function getTimingToken() {
return get("timing-token", null);
}
function setTimingToken(token) {
return set("timing-token", token);
}
/* user class */
class User {
constructor() {
this.name = 'Unknown';
this.id = 'unknown';
this.image = 'img/default.png';
}
setName(name) {
this.name = name;
}
getName() {
return this.name;
}
setID(id) {
this.id = id;
}
getID() {
return this.id;
}
setIcon(image) {
this.image = image;
}
getIcon() {
return this.image;
}
setDescription(description) {
this.description = description;
}
getDescription() {
return this.description;
}
serialize() {
return {
'name' : this.getName(),
'id' : this.getID(),
'icon' : this.getIcon(),
'description' : this.getDescription()
};
}
/* save in usermap */
remember() {
usermap[this.getID()] = this.serialize();
set('usermap', JSON.stringify(usermap));
}
/* save as a block */
save(callback) {
var block = new Block();
block.setType('onionr-user');
block.setContent(JSON.stringify(this.serialize()));
return block.save(true, callback);
}
static getUser(id, callback) {
// console.log(callback);
var user = deserializeUser(id);
if(user === null) {
Block.getBlocks({'type' : 'onionr-user-info', 'signed' : true, 'reverse' : true}, function(data) {
if(data.length !== 0) {
try {
user = new User();
var userInfo = JSON.parse(data[0].getContent());
if(userInfo['id'] === id) {
user.setName(userInfo['name']);
user.setIcon(userInfo['icon']);
user.setDescription(userInfo['description']);
user.setID(id);
user.remember();
// console.log(callback);
callback(user);
return user;
}
} catch(e) {
console.log(e);
callback(null);
return null;
}
} else {
callback(null);
return null;
}
});
} else {
// console.log(callback);
callback(user);
return user;
}
}
}
/* post class */
class Post {
/* returns the html content of a post */
getHTML(type) {
var replyTemplate = '<$= jsTemplate('onionr-timeline-reply') $>';
var postTemplate = '<$= jsTemplate('onionr-timeline-post') $>';
var template = '';
if(type !== undefined && type !== null && type == 'reply')
template = replyTemplate;
else
template = postTemplate;
var device = (jQuery(document).width() < 768 ? 'mobile' : 'desktop');
template = template.replaceAll('$user-name-url', Sanitize.html(Sanitize.url(this.getUser().getName())));
template = template.replaceAll('$user-name', Sanitize.html(this.getUser().getName()));
template = template.replaceAll('$user-id-url', Sanitize.html(Sanitize.url(this.getUser().getID())));
template = template.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().substring(0, 12) + '...'));
// template = template.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().split('-').slice(0, 4).join('-')));
template = template.replaceAll('$user-id', Sanitize.html(this.getUser().getID()));
template = template.replaceAll('$user-image', "data:image/jpeg;base64," + Sanitize.html(this.getUser().getIcon()));
template = template.replaceAll('$content', Sanitize.html(this.getContent()).replaceAll('\n', '<br />', 16)); // Maximum of 16 lines
template = template.replaceAll('$post-hash', this.getHash());
template = template.replaceAll('$date-relative-truncated', timeSince(this.getPostDate(), 'mobile'));
template = template.replaceAll('$date-relative', timeSince(this.getPostDate(), device) + (device === 'desktop' ? ' ago' : ''));
template = template.replaceAll('$date', this.getPostDate().toLocaleString());
if(this.getHash() in getPostMap() && getPostMap()[this.getHash()]['liked']) {
template = template.replaceAll('$liked', '<$= LANG.POST_UNLIKE $>');
} else {
template = template.replaceAll('$liked', '<$= LANG.POST_LIKE $>');
}
return template;
}
setUser(user) {
this.user = user;
}
getUser() {
return this.user;
}
setContent(content) {
this.content = content;
}
getContent() {
return this.content;
}
setParent(parent) {
this.parent = parent;
}
getParent() {
return this.parent;
}
setPostDate(date) { // unix timestamp input
if(date instanceof Date)
this.date = date;
else
this.date = new Date(date * 1000);
}
getPostDate() {
return this.date;
}
setHash(hash) {
this.hash = hash;
}
getHash() {
return this.hash;
}
save(callback) {
var args = {'type' : 'onionr-post', 'sign' : true, 'content' : JSON.stringify({'content' : this.getContent()})};
if(this.getParent() !== undefined && this.getParent() !== null)
args['parent'] = (this.getParent() instanceof Post ? this.getParent().getHash() : (this.getParent() instanceof Block ? this.getParent().getHash() : this.getParent()));
var url = '/client/?action=insertBlock&data=' + Sanitize.url(JSON.stringify(args)) + '&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken());
console.log(url);
var http = new XMLHttpRequest();
if(callback !== undefined) {
// async
var thisObject = this;
http.addEventListener('load', function() {
thisObject.setHash(Block.parseBlockArray(JSON.parse(http.responseText)['hash']));
callback(thisObject.getHash());
}, false);
http.open('GET', url, true);
http.timeout = 5000;
http.send(null);
} else {
// sync
http.open('GET', url, false);
http.send(null);
this.setHash(Block.parseBlockArray(JSON.parse(http.responseText)['hash']));
return this.getHash();
}
}
}
/* block class */
class Block {
constructor(type, content) {
this.type = type;
this.content = content;
}
// returns the block hash, if any
getHash() {
return this.hash;
}
// returns the block type
getType() {
return this.type;
}
// returns the block header
getHeader(key, df) { // df is default
if(key !== undefined) {
if(this.getHeader().hasOwnProperty(key))
return this.getHeader()[key];
else
return (df === undefined ? null : df);
} else
return this.header;
}
// returns the block metadata
getMetadata(key, df) { // df is default
if(key !== undefined) {
if(this.getMetadata().hasOwnProperty(key))
return this.getMetadata()[key];
else
return (df === undefined ? null : df);
} else
return this.metadata;
}
// returns the block content
getContent() {
return this.content;
}
// returns the parent block's hash (not Block object, for performance)
getParent() {
// console.log(this.parent);
// TODO: Create a function to fetch the block contents and parse it from the server; right now it is only possible to search for types of blocks (see Block.getBlocks), so it is impossible to return a Block object here
// if(!(this.parent instanceof Block) && this.parent !== undefined && this.parent !== null)
// this.parent = Block.openBlock(this.parent); // convert hash to Block object
return this.parent;
}
// returns the date that the block was received
getDate() {
return this.date;
}
// returns a boolean that indicates whether or not the block is valid
isValid() {
return this.valid;
}
// returns a boolean thati ndicates whether or not the block is signed
isSigned() {
return this.signed;
}
// returns the block signature
getSignature() {
return this.signature;
}
// returns the block type
setType(type) {
this.type = type;
return this;
}
// sets block metadata by key
setMetadata(key, val) {
this.metadata[key] = val;
return this;
}
// sets block content
setContent(content) {
this.content = content;
return this;
}
// sets the block parent by hash or Block object
setParent(parent) {
this.parent = parent;
return this;
}
// indicates if the Block exists or not
exists() {
return !(this.hash === null || this.hash === undefined);
}
// saves the block, returns the hash
save(sign, callback) {
var type = this.getType();
var content = this.getContent();
var parent = this.getParent();
if(content !== undefined && content !== null && type !== '') {
var args = {'content' : content};
if(type !== undefined && type !== null && type !== '')
args['type'] = type;
if(parent !== undefined && parent !== null && parent.getHash() !== undefined && parent.getHash() !== null && parent.getHash() !== '')
args['parent'] = parent.getHash();
if(sign !== undefined && sign !== null)
args['sign'] = String(sign) !== 'false'
var url = '/client/?action=insertBlock&data=' + Sanitize.url(JSON.stringify(args)) + '&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken());
console.log(url);
var http = new XMLHttpRequest();
if(callback !== undefined) {
// async
http.addEventListener('load', function() {
callback(Block.parseBlockArray(JSON.parse(http.responseText)['hash']));
}, false);
http.open('GET', url, true);
http.timeout = 5000;
http.send(null);
} else {
// sync
http.open('GET', url, false);
http.send(null);
return Block.parseBlockArray(JSON.parse(http.responseText)['hash']);
}
}
return false;
}
/* static functions */
// recreates a block by hash
static openBlock(hash) {
return Block.parseBlock(hash);
}
// converts an associative array to a Block
static parseBlock(val) {
var block = new Block();
block.type = val['type'];
block.content = val['content'];
block.header = val['header'];
block.metadata = val['metadata'];
block.date = new Date(val['date'] * 1000);
block.hash = val['hash'];
block.signature = val['signature'];
block.signed = val['signed'];
block.valid = val['valid'];
block.parent = val['parent'];
if(block.getParent() !== null) {
// if the block data is already in the associative array
/*
if (blocks.hasOwnProperty(block.getParent()))
block.setParent(Block.parseAssociativeArray({blocks[block.getParent()]})[0]);
*/
}
return block;
}
// converts an array of associative arrays to an array of Blocks
static parseBlockArray(blocks) {
var outputBlocks = [];
for(var key in blocks) {
if(blocks.hasOwnProperty(key)) {
var val = blocks[key];
var block = Block.parseBlock(val);
outputBlocks.push(block);
}
}
return outputBlocks;
}
static getBlocks(args, callback) { // callback is optional
args = args || {}
var url = '/client/?action=searchBlocks&data=' + Sanitize.url(JSON.stringify(args)) + '&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken());
console.log(url);
var http = new XMLHttpRequest();
if(callback !== undefined) {
// async
http.addEventListener('load', function() {
callback(Block.parseBlockArray(JSON.parse(http.responseText)['blocks']));
}, false);
http.open('GET', url, true);
http.timeout = 5000;
http.send(null);
} else {
// sync
http.open('GET', url, false);
http.send(null);
return Block.parseBlockArray(JSON.parse(http.responseText)['blocks']);
}
}
}
/* temporary code */
var tt = getParameter("timingToken");
if(tt !== null && tt !== undefined) {
setTimingToken(tt);
}
if(getWebPassword() === null) {
var password = "";
while(password.length != 64) {
password = prompt("Please enter the web password (run `./RUN-LINUX.sh --details`)");
}
setWebPassword(password);
}
if(getCurrentUser() === null) {
jQuery('#modal').modal('show');
var url = '/client/?action=info&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken());
console.log(url);
var http = new XMLHttpRequest();
// sync
http.addEventListener('load', function() {
var id = JSON.parse(http.responseText)['pubkey'];
User.getUser(id, function(data) {
if(data === null || data === undefined) {
var user = new User();
user.setName('New User');
user.setID(id);
user.setIcon('<$= Template.jsTemplate("default-icon") $>');
user.setDescription('A new OnionrUI user');
user.remember();
user.save();
setCurrentUser(user);
} else {
setCurrentUser(data);
}
window.location.reload();
});
}, false);
http.open('GET', url, true);
http.send(null);
}
currentUser = getCurrentUser();

View File

@ -1,460 +0,0 @@
/* just for testing rn */
Block.getBlocks({'type' : 'onionr-post', 'signed' : true, 'reverse' : true}, function(data) {
for(var i = 0; i < data.length; i++) {
try {
var block = data[i];
var finished = false;
User.getUser(new String(block.getHeader('signer', 'unknown')), function(user) {
var post = new Post();
var blockContent = JSON.parse(block.getContent());
// just ignore anything shorter than 280 characters
if(String(blockContent['content']).length <= 280 && block.getParent() === null) {
post.setContent(blockContent['content']);
post.setPostDate(block.getDate());
post.setUser(user);
post.setHash(block.getHash());
document.getElementById('onionr-timeline-posts').innerHTML += post.getHTML();
}
finished = true;
});
while(!finished);
} catch(e) {
console.log('Troublemaker block: ' + data[i].getHash());
console.log(e);
}
}
});
function toggleLike(hash) {
var post = getPostMap(hash);
if(post === null || !getPostMap()[hash]['liked']) {
console.log('Liking ' + hash + '...');
if(post === null)
getPostMap()[hash] = {};
getPostMap()[hash]['liked'] = true;
set('postmap', JSON.stringify(getPostMap()));
var block = new Block();
block.setType('onionr-post-like');
block.setContent(JSON.stringify({'hash' : hash}));
block.save(true, function(hash) {});
} else {
console.log('Unliking ' + hash + '...');
}
}
function postCreatorChange() {
var content = document.getElementById('onionr-post-creator-content').value;
var message = '';
var maxlength = 280;
var disable = true;
var warn = false;
if(content.length !== 0) {
if(content.length - content.replaceAll('\n', '').length > 16) {
// 16 max newlines
message = '<$= LANG.POST_CREATOR_MESSAGE_MAXIMUM_NEWLINES $>';
} else if(content.length <= maxlength) {
// 280 max characters
message = '<$= LANG.POST_CREATOR_MESSAGE_REMAINING $>'.replaceAll('%s', (280 - content.length));
disable = false;
if(maxlength - content.length < maxlength / 4) {
warn = true;
}
} else {
message = '<$= LANG.POST_CREATOR_MESSAGE_OVER $>'.replaceAll('%s', (content.length - maxlength));
}
}
var element = document.getElementById('onionr-post-creator-content-message');
var button = document.getElementById("onionr-post-creator-create");
if(message === '')
element.style.visibility = 'hidden';
else {
element.style.visibility = 'visible';
element.innerHTML = message;
if(disable)
element.style.color = 'red';
else if(warn)
element.style.color = '#FF8C00';
else
element.style.color = 'gray';
}
if(disable)
button.disabled = true;
else
button.disabled = false;
}
function replyCreatorChange() {
var content = document.getElementById('onionr-reply-creator-content').value;
var message = '';
var maxlength = 280;
var disable = true;
var warn = false;
if(content.length !== 0) {
if(content.length - content.replaceAll('\n', '').length > 16) {
// 16 max newlines
message = '<$= LANG.POST_CREATOR_MESSAGE_MAXIMUM_NEWLINES $>';
} else if(content.length <= maxlength) {
// 280 max characters
message = '<$= LANG.POST_CREATOR_MESSAGE_REMAINING $>'.replaceAll('%s', (280 - content.length));
disable = false;
if(maxlength - content.length < maxlength / 4) {
warn = true;
}
} else {
message = '<$= LANG.POST_CREATOR_MESSAGE_OVER $>'.replaceAll('%s', (content.length - maxlength));
}
}
var element = document.getElementById('onionr-reply-creator-content-message');
var button = document.getElementById("onionr-reply-creator-create");
if(message === '')
element.style.visibility = 'hidden';
else {
element.style.visibility = 'visible';
element.innerHTML = message;
if(disable)
element.style.color = 'red';
else if(warn)
element.style.color = '#FF8C00';
else
element.style.color = 'gray';
}
if(disable)
button.disabled = true;
else
button.disabled = false;
}
function focusReplyCreatorChange() {
var content = document.getElementById('onionr-post-focus-reply-creator-content').value;
var message = '';
var maxlength = 280;
var disable = true;
var warn = false;
if(content.length !== 0) {
if(content.length - content.replaceAll('\n', '').length > 16) {
// 16 max newlines
message = '<$= LANG.POST_CREATOR_MESSAGE_MAXIMUM_NEWLINES $>';
} else if(content.length <= maxlength) {
// 280 max characters
message = '<$= LANG.POST_CREATOR_MESSAGE_REMAINING $>'.replaceAll('%s', (280 - content.length));
disable = false;
if(maxlength - content.length < maxlength / 4) {
warn = true;
}
} else {
message = '<$= LANG.POST_CREATOR_MESSAGE_OVER $>'.replaceAll('%s', (content.length - maxlength));
}
}
var element = document.getElementById('onionr-post-focus-reply-creator-content-message');
var button = document.getElementById("onionr-post-focus-reply-creator-create");
if(message === '')
element.style.visibility = 'hidden';
else {
element.style.visibility = 'visible';
element.innerHTML = message;
if(disable)
element.style.color = 'red';
else if(warn)
element.style.color = '#FF8C00';
else
element.style.color = 'gray';
}
if(disable)
button.disabled = true;
else
button.disabled = false;
}
function viewProfile(id, name) {
id = decodeURIComponent(id);
document.getElementById("onionr-profile-username").innerHTML = Sanitize.html(decodeURIComponent(name));
User.getUser(id, function(data) {
if(data !== null) {
document.getElementById("onionr-profile-user-icon").src = "data:image/jpeg;base64," + Sanitize.html(data.getIcon());
document.getElementById("onionr-profile-user-icon").b64 = Sanitize.html(data.getIcon());
document.getElementById("onionr-profile-username").innerHTML = Sanitize.html(Sanitize.username(data.getName()));
document.getElementById("onionr-profile-username").title = Sanitize.html(data.getID());
document.getElementById("onionr-profile-description").innerHTML = Sanitize.html(Sanitize.description(data.getDescription()));
}
});
}
function updateUser() {
toggleSaveButton(false);
// jQuery('#modal').modal('show');
var name = jQuery('#onionr-profile-username').text();
var id = document.getElementById("onionr-profile-username").title;
var icon = document.getElementById("onionr-profile-user-icon").b64;
var description = jQuery("#onionr-profile-description").text();
var user = new User();
user.setName(name);
user.setID(id);
user.setIcon(icon);
user.setDescription(Sanitize.description(description));
user.remember();
user.save(function() {
setCurrentUser(user);
window.location.reload();
});
}
function cancelUpdate() {
toggleSaveButton(false);
var name = jQuery('#onionr-profile-username').text();
var id = document.getElementById("onionr-profile-username").title;
viewProfile(id, name);
}
function toggleSaveButton(show) {
document.getElementById("onionr-profile-edit").style.display = (show ? 'block' : 'none');
}
function makePost() {
var content = document.getElementById("onionr-post-creator-content").value;
if(content.trim() !== '') {
var post = new Post();
post.setUser(getCurrentUser());
post.setContent(content);
post.setPostDate(new Date());
post.save(function(data) {}); // async, but no function
document.getElementById('onionr-timeline-posts').innerHTML = post.getHTML() + document.getElementById('onionr-timeline-posts').innerHTML;
document.getElementById("onionr-post-creator-content").value = "";
document.getElementById("onionr-post-creator-content").focus();
postCreatorChange();
} else {
console.log('Not making empty post.');
}
}
function getReplies(id, callback) {
Block.getBlocks({'type' : 'onionr-post', 'parent' : id, 'signed' : true, 'reverse' : true}, callback);
}
function focusPost(id) {
viewReplies(id);
}
function viewRepliesMobile(id) {
var post = document.getElementById('onionr-post-' + id);
var user_name = '';
var user_id = '';
var user_id_trunc = '';
var user_icon = '';
var post_content = '';
if(post !== null && post !== undefined) {
// if the post is in the timeline, get the data from it
user_name = post.getElementsByClassName('onionr-post-user-name')[0].innerHTML;
user_id = post.getElementsByClassName('onionr-post-user-id')[0].title;
user_id_trunc = post.getElementsByClassName('onionr-post-user-id')[0].innerHTML;
user_icon = post.getElementsByClassName('onionr-post-user-icon')[0].src;
post_content = post.getElementsByClassName('onionr-post-content')[0].innerHTML;
} else {
// otherwise, fetch the data
}
document.getElementById('onionr-post-focus-user-icon').src = user_icon;
document.getElementById('onionr-post-focus-user-name').innerHTML = user_name;
document.getElementById('onionr-post-focus-user-id').innerHTML = user_id_trunc;
document.getElementById('onionr-post-focus-user-id').title = user_id;
document.getElementById('onionr-post-focus-content').innerHTML = post_content;
document.getElementById('onionr-post-focus-reply-creator-user-name').innerHTML = Sanitize.html(Sanitize.username(getCurrentUser().getName()));
document.getElementById('onionr-post-focus-reply-creator-user-icon').src = "data:image/jpeg;base64," + Sanitize.html(getCurrentUser().getIcon());
document.getElementById('onionr-post-focus-reply-creator-content').value = '';
document.getElementById('onionr-post-focus-reply-creator-content-message').value = '';
jQuery('#onionr-post-focus').modal('show');
}
function viewReplies(id) {
document.getElementById('onionr-replies-title').innerHTML = '<$= LANG.REPLIES $>';
document.getElementById('onionr-reply-creator-panel').originalPost = id;
document.getElementById('onionr-reply-creator-panel').innerHTML = '<$= jsTemplate('onionr-reply-creator') $>';
document.getElementById('onionr-reply-creator-content').innerHTML = '';
document.getElementById("onionr-reply-creator-content").placeholder = "<$= LANG.POST_CREATOR_PLACEHOLDER $>";
document.getElementById('onionr-reply-creator-user-name').innerHTML = Sanitize.html(Sanitize.username(getCurrentUser().getName()));
document.getElementById('onionr-reply-creator-user-icon').src = "data:image/jpeg;base64," + Sanitize.html(getCurrentUser().getIcon());
document.getElementById('onionr-replies').innerHTML = '';
getReplies(id, function(data) {
var replies = document.getElementById('onionr-replies');
replies.innerHTML = '';
for(var i = 0; i < data.length; i++) {
try {
var block = data[i];
var finished = false;
User.getUser(new String(block.getHeader('signer', 'unknown')), function(user) {
var post = new Post();
var blockContent = JSON.parse(block.getContent());
// just ignore anything shorter than 280 characters
if(String(blockContent['content']).length <= 280) {
post.setContent(blockContent['content']);
post.setPostDate(block.getDate());
post.setUser(user);
post.setHash(block.getHash());
replies.innerHTML += post.getHTML('reply');
}
finished = true;
});
while(!finished);
} catch(e) {
console.log('Troublemaker block: ' + data[i].getHash());
console.log(e);
}
}
});
}
function makeReply() {
var content = document.getElementById("onionr-reply-creator-content").value;
if(content.trim() !== '') {
var post = new Post();
var originalPost = document.getElementById('onionr-reply-creator-panel').originalPost;
console.log('Original post hash: ' + originalPost);
post.setUser(getCurrentUser());
post.setParent(originalPost);
post.setContent(content);
post.setPostDate(new Date());
post.save(function(data) {}); // async, but no function
document.getElementById('onionr-replies').innerHTML = post.getHTML('reply') + document.getElementById('onionr-replies').innerHTML;
document.getElementById("onionr-reply-creator-content").value = "";
document.getElementById("onionr-reply-creator-content").focus();
replyCreatorChange();
} else {
console.log('Not making empty reply.');
}
}
jQuery('body').on('click', '[data-editable]', function() {
var el = jQuery(this);
var txt = el.text();
var maxlength = el.attr("maxlength");
var input = jQuery('<input/>').val(txt);
input.attr('maxlength', maxlength);
el.replaceWith(input);
var save = function() {
var newTxt = input.val();
if(el.attr('id') === 'onionr-profile-username')
newTxt = Sanitize.username(newTxt);
if(el.attr('id') === 'onionr-profile-description')
newTxt = Sanitize.description(newTxt);
var p = el.text(newTxt);
input.replaceWith(p);
if(newTxt !== txt)
toggleSaveButton(true);
};
var saveEnter = function(event) {
console.log(event);
console.log(event.keyCode);
if (event.keyCode === 13)
save();
};
input.one('blur', save).bind('keyup', saveEnter).focus();
});
//viewProfile('$user-id-url', '$user-name-url')
// jQuery('#onionr-post-user-id').on('click', function(e) { alert(3);});
//jQuery('#onionr-post *').on('click', function(e) { e.stopPropagation(); });
// jQuery('#onionr-post').click(function(e) { alert(1); });
currentUser = getCurrentUser();
if(currentUser !== undefined && currentUser !== null) {
document.getElementById("onionr-post-creator-user-name").innerHTML = Sanitize.html(currentUser.getName());
document.getElementById("onionr-post-creator-user-id").innerHTML = "<$= LANG.POST_CREATOR_YOU $>";
document.getElementById("onionr-post-creator-user-icon").src = "data:image/jpeg;base64," + Sanitize.html(currentUser.getIcon());
document.getElementById("onionr-post-creator-user-id").title = currentUser.getID();
document.getElementById("onionr-post-creator-content").placeholder = "<$= LANG.POST_CREATOR_PLACEHOLDER $>";
document.getElementById("onionr-post-focus-reply-creator-content").placeholder = "<$= LANG.POST_CREATOR_PLACEHOLDER $>";
document.getElementById("onionr-post-focus-reply-creator-user-id").innerHTML = "<$= LANG.POST_CREATOR_YOU $>";
}
viewCurrentProfile = function() {
viewProfile(encodeURIComponent(currentUser.getID()), encodeURIComponent(currentUser.getName()));
}
document.getElementById("onionr-post-creator-user-id").onclick = viewCurrentProfile;
document.getElementById("onionr-post-creator-user-name").onclick = viewCurrentProfile;
// on some browsers it saves the user input on reload. So, it should also recheck the input.
postCreatorChange();

Some files were not shown because too many files have changed in this diff Show More