Merge branch 'onionrui' into 'master'

onionrui -> master

See merge request beardog/Onionr!10
master
Kevin 2018-11-17 15:58:45 +00:00
commit 1939dd4427
41 changed files with 2680 additions and 406 deletions

View File

@ -34,6 +34,7 @@ soft-reset:
reset: reset:
@echo "Hard-resetting Onionr..." @echo "Hard-resetting Onionr..."
rm -rf onionr/data/ | true > /dev/null 2>&1 rm -rf onionr/data/ | true > /dev/null 2>&1
cd onionr/static-data/www/ui/; rm -rf ./dist; python compile.py
#@./RUN-LINUX.sh version | grep -v "Failed" --color=always #@./RUN-LINUX.sh version | grep -v "Failed" --color=always
plugins-reset: plugins-reset:

View File

@ -1,12 +1,12 @@
![Onionr logo](./docs/onionr-logo.png) ![Onionr logo](./docs/onionr-logo.png)
v0.3.0 (***experimental, not safe 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/) [![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/)
Anonymous P2P platform, using Tor & I2P. Anonymous P2P platform, using Tor & I2P.
***Experimental, not safe or easy to use yet***
<hr> <hr>
**The main repo for this software is at https://gitlab.com/beardog/Onionr/** **The main repo for this software is at https://gitlab.com/beardog/Onionr/**

View File

@ -24,7 +24,7 @@ from gevent.pywsgi import WSGIServer
import sys, random, threading, hmac, hashlib, base64, time, math, os, json import sys, random, threading, hmac, hashlib, base64, time, math, os, json
import core import core
from onionrblockapi import Block from onionrblockapi import Block
import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionr
class API: class API:
''' '''
@ -62,14 +62,9 @@ class API:
} }
for mimetype in mimetypes: for mimetype in mimetypes:
logger.debug(path + ' endswith .' + mimetype + '?')
if path.endswith('.%s' % mimetype): if path.endswith('.%s' % mimetype):
logger.debug('- True!')
return mimetypes[mimetype] return mimetypes[mimetype]
else:
logger.debug('- no')
logger.debug('%s not in %s' % (path, mimetypes))
return 'text/plain' return 'text/plain'
def __init__(self, debug, API_VERSION): def __init__(self, debug, API_VERSION):
@ -80,14 +75,8 @@ class API:
This also saves the used host (random localhost IP address) to the data folder in host.txt This also saves the used host (random localhost IP address) to the data folder in host.txt
''' '''
config.reload() # configure logger and stuff
onionr.Onionr.setupConfig('data/', self = self)
if config.get('dev_mode', True):
self._developmentMode = True
logger.set_level(logger.LEVEL_DEBUG)
else:
self._developmentMode = False
logger.set_level(logger.LEVEL_INFO)
self.debug = debug self.debug = debug
self._privateDelayTime = 3 self._privateDelayTime = 3
@ -138,7 +127,7 @@ class API:
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'" 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'"
resp.headers['X-Frame-Options'] = 'deny' resp.headers['X-Frame-Options'] = 'deny'
resp.headers['X-Content-Type-Options'] = "nosniff" resp.headers['X-Content-Type-Options'] = "nosniff"
resp.headers['api'] = API_VERSION resp.headers['X-API'] = API_VERSION
# reset to text/plain to help prevent browser attacks # reset to text/plain to help prevent browser attacks
self.mimeType = 'text/plain' self.mimeType = 'text/plain'
@ -160,14 +149,17 @@ class API:
self.validateHost('private') self.validateHost('private')
if config.get('www.public.guess_mime', True):
self.mimeType = API.guessMime(path)
endTime = math.floor(time.time()) endTime = math.floor(time.time())
elapsed = endTime - startTime elapsed = endTime - startTime
if not hmac.compare_digest(timingToken, self.timeBypassToken): if not hmac.compare_digest(timingToken, self.timeBypassToken):
if elapsed < self._privateDelayTime: if (elapsed < self._privateDelayTime) and config.get('www.private.timing_protection', True):
time.sleep(self._privateDelayTime - elapsed) time.sleep(self._privateDelayTime - elapsed)
return send_from_directory('static-data/www/private/', path) return send_from_directory(config.get('www.private.path', 'static-data/www/private/'), path)
@app.route('/www/public/<path:path>') @app.route('/www/public/<path:path>')
def www_public(path): def www_public(path):
@ -176,7 +168,10 @@ class API:
self.validateHost('public') self.validateHost('public')
return send_from_directory('static-data/www/public/', path) if config.get('www.public.guess_mime', True):
self.mimeType = API.guessMime(path)
return send_from_directory(config.get('www.public.path', 'static-data/www/public/'), path)
@app.route('/ui/<path:path>') @app.route('/ui/<path:path>')
def ui_private(path): def ui_private(path):
@ -206,11 +201,11 @@ class API:
time.sleep(self._privateDelayTime - elapsed) time.sleep(self._privateDelayTime - elapsed)
''' '''
logger.debug('Serving %s' % path)
self.mimeType = API.guessMime(path) self.mimeType = API.guessMime(path)
self.overrideCSP = True self.overrideCSP = True
logger.debug('Serving %s (mime: %s)' % (path, self.mimeType))
return send_from_directory('static-data/www/ui/dist/', path, mimetype = API.guessMime(path)) return send_from_directory('static-data/www/ui/dist/', path, mimetype = API.guessMime(path))
@app.route('/client/') @app.route('/client/')
@ -253,6 +248,16 @@ class API:
resp = Response('Goodbye') resp = Response('Goodbye')
elif action == 'ping': elif action == 'ping':
resp = Response('pong') resp = Response('pong')
elif action == 'site':
block = data
siteData = self._core.getData(data)
response = 'not found'
if siteData != '' and siteData != False:
self.mimeType = 'text/html'
response = siteData.split(b'-', 2)[-1]
resp = Response(response)
elif action == 'info':
resp = Response(json.dumps({'pubkey' : self._core._crypto.pubKey, 'host' : self._core.hsAddress}))
elif action == "insertBlock": elif action == "insertBlock":
response = {'success' : False, 'reason' : 'An unknown error occurred'} response = {'success' : False, 'reason' : 'An unknown error occurred'}

View File

@ -21,12 +21,14 @@
''' '''
import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid 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 onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block
import onionrdaemontools, onionrsockets, onionrchat import onionrdaemontools, onionrsockets, onionrchat, onionr
from dependencies import secrets from dependencies import secrets
from defusedxml import minidom from defusedxml import minidom
class OnionrCommunicatorDaemon: class OnionrCommunicatorDaemon:
def __init__(self, debug, developmentMode): def __init__(self, debug, developmentMode):
# configure logger and stuff
onionr.Onionr.setupConfig('data/', self = self)
self.isOnline = True # Assume we're connected to the internet self.isOnline = True # Assume we're connected to the internet
@ -87,9 +89,8 @@ class OnionrCommunicatorDaemon:
# Set timers, function reference, seconds # Set timers, function reference, seconds
# requiresPeer True means the timer function won't fire if we have no connected peers # requiresPeer True means the timer function won't fire if we have no connected peers
OnionrCommunicatorTimers(self, self.daemonCommands, 5)
OnionrCommunicatorTimers(self, self.detectAPICrash, 5)
peerPoolTimer = OnionrCommunicatorTimers(self, self.getOnlinePeers, 60, maxThreads=1) peerPoolTimer = OnionrCommunicatorTimers(self, self.getOnlinePeers, 60, maxThreads=1)
OnionrCommunicatorTimers(self, self.runCheck, 1)
OnionrCommunicatorTimers(self, self.lookupBlocks, self._core.config.get('timers.lookupBlocks'), requiresPeer=True, maxThreads=1) OnionrCommunicatorTimers(self, self.lookupBlocks, self._core.config.get('timers.lookupBlocks'), requiresPeer=True, maxThreads=1)
OnionrCommunicatorTimers(self, self.getBlocks, self._core.config.get('timers.getBlocks'), requiresPeer=True) OnionrCommunicatorTimers(self, self.getBlocks, self._core.config.get('timers.getBlocks'), requiresPeer=True)
OnionrCommunicatorTimers(self, self.clearOfflinePeer, 58) OnionrCommunicatorTimers(self, self.clearOfflinePeer, 58)
@ -134,7 +135,7 @@ class OnionrCommunicatorDaemon:
def lookupAdders(self): def lookupAdders(self):
'''Lookup new peer addresses''' '''Lookup new peer addresses'''
logger.info('LOOKING UP NEW ADDRESSES') logger.info('Looking up new addresses...')
tryAmount = 1 tryAmount = 1
for i in range(tryAmount): for i in range(tryAmount):
# Download new peer address list from random online peers # Download new peer address list from random online peers
@ -145,7 +146,7 @@ class OnionrCommunicatorDaemon:
def lookupBlocks(self): def lookupBlocks(self):
'''Lookup new blocks & add them to download queue''' '''Lookup new blocks & add them to download queue'''
logger.info('LOOKING UP NEW BLOCKS') logger.info('Looking up new blocks...')
tryAmount = 2 tryAmount = 2
newBlocks = '' newBlocks = ''
existingBlocks = self._core.getBlockList() existingBlocks = self._core.getBlockList()
@ -176,7 +177,7 @@ class OnionrCommunicatorDaemon:
try: try:
newBlocks = self.peerAction(peer, 'getBlockHashes') # get list of new block hashes newBlocks = self.peerAction(peer, 'getBlockHashes') # get list of new block hashes
except Exception as error: except Exception as error:
logger.warn("could not get new blocks with " + peer, error=error) logger.warn('Could not get new blocks from %s.' % peer, error = error)
newBlocks = False newBlocks = False
if newBlocks != False: if newBlocks != False:
# if request was a success # if request was a success
@ -200,10 +201,10 @@ class OnionrCommunicatorDaemon:
break break
# Do not download blocks being downloaded or that are already saved (edge cases) # Do not download blocks being downloaded or that are already saved (edge cases)
if blockHash in self.currentDownloading: if blockHash in self.currentDownloading:
logger.debug('ALREADY DOWNLOADING ' + blockHash) logger.debug('Already downloading block %s...' % blockHash)
continue continue
if blockHash in self._core.getBlockList(): if blockHash in self._core.getBlockList():
logger.debug('%s is already saved' % (blockHash,)) logger.debug('Block %s is already saved.' % (blockHash,))
self.blockQueue.remove(blockHash) self.blockQueue.remove(blockHash)
continue continue
if self._core._blacklist.inBlacklist(blockHash): if self._core._blacklist.inBlacklist(blockHash):
@ -232,22 +233,22 @@ class OnionrCommunicatorDaemon:
#meta = metas[1] #meta = metas[1]
if self._core._utils.validateMetadata(metadata, metas[2]): # check if metadata is valid, and verify nonce if self._core._utils.validateMetadata(metadata, metas[2]): # check if metadata is valid, and verify nonce
if self._core._crypto.verifyPow(content): # check if POW is enough/correct if self._core._crypto.verifyPow(content): # check if POW is enough/correct
logger.info('Block passed proof, attempting save.') logger.info('Attempting to save block %s...' % blockHash)
try: try:
self._core.setData(content) self._core.setData(content)
except onionrexceptions.DiskAllocationReached: except onionrexceptions.DiskAllocationReached:
logger.error("Reached disk allocation allowance, cannot save this block.") logger.error('Reached disk allocation allowance, cannot save block %s.' % blockHash)
removeFromQueue = False removeFromQueue = False
else: else:
self._core.addToBlockDB(blockHash, dataSaved=True) self._core.addToBlockDB(blockHash, dataSaved=True)
self._core._utils.processBlockMetadata(blockHash) # caches block metadata values to block database self._core._utils.processBlockMetadata(blockHash) # caches block metadata values to block database
else: else:
logger.warn('POW failed for block ' + blockHash) logger.warn('POW failed for block %s.' % blockHash)
else: else:
if self._core._blacklist.inBlacklist(realHash): if self._core._blacklist.inBlacklist(realHash):
logger.warn('%s is blacklisted' % (realHash,)) logger.warn('Block %s is blacklisted.' % (realHash,))
else: else:
logger.warn('Metadata for ' + blockHash + ' is invalid.') logger.warn('Metadata for block %s is invalid.' % blockHash)
self._core._blacklist.addToDB(blockHash) self._core._blacklist.addToDB(blockHash)
else: else:
# if block didn't meet expected hash # if block didn't meet expected hash
@ -303,10 +304,12 @@ class OnionrCommunicatorDaemon:
self.decrementThreadCount('clearOfflinePeer') self.decrementThreadCount('clearOfflinePeer')
def getOnlinePeers(self): def getOnlinePeers(self):
'''Manages the self.onlinePeers attribute list, connects to more peers if we have none connected''' '''
Manages the self.onlinePeers attribute list, connects to more peers if we have none connected
'''
logger.info('Refreshing peer pool.') logger.debug('Refreshing peer pool...')
maxPeers = int(config.get('peers.maxConnect')) maxPeers = int(config.get('peers.max_connect', 10))
needed = maxPeers - len(self.onlinePeers) needed = maxPeers - len(self.onlinePeers)
for i in range(needed): for i in range(needed):
@ -318,13 +321,15 @@ class OnionrCommunicatorDaemon:
break break
else: else:
if len(self.onlinePeers) == 0: if len(self.onlinePeers) == 0:
logger.warn('Could not connect to any peer.') logger.debug('Couldn\'t connect to any peers.')
self.decrementThreadCount('getOnlinePeers') self.decrementThreadCount('getOnlinePeers')
def addBootstrapListToPeerList(self, peerList): def addBootstrapListToPeerList(self, peerList):
'''Add the bootstrap list to the peer list (no duplicates)''' '''
Add the bootstrap list to the peer list (no duplicates)
'''
for i in self._core.bootstrapList: for i in self._core.bootstrapList:
if i not in peerList and i not in self.offlinePeers and i != self._core.hsAddress: if i not in peerList and i not in self.offlinePeers and i != self._core.hsAddress and len(str(i).strip()) > 0:
peerList.append(i) peerList.append(i)
self._core.addAddress(i) self._core.addAddress(i)
@ -347,7 +352,7 @@ class OnionrCommunicatorDaemon:
self.addBootstrapListToPeerList(peerList) self.addBootstrapListToPeerList(peerList)
for address in peerList: for address in peerList:
if not config.get('tor.v3onions') and len(address) == 62: if not config.get('tor.v3_onions') and len(address) == 62:
continue continue
if len(address) == 0 or address in tried or address in self.onlinePeers or address in self.cooldownPeer: if len(address) == 0 or address in tried or address in self.onlinePeers or address in self.cooldownPeer:
continue continue
@ -440,11 +445,13 @@ class OnionrCommunicatorDaemon:
def heartbeat(self): def heartbeat(self):
'''Show a heartbeat debug message''' '''Show a heartbeat debug message'''
currentTime = self._core._utils.getEpoch() - self.startTime currentTime = self._core._utils.getEpoch() - self.startTime
logger.debug('heartbeat, running seconds: ' + str(currentTime)) logger.debug('Heartbeat. Node online for %s.' % self.daemonTools.humanReadableTime(currentTime))
self.decrementThreadCount('heartbeat') self.decrementThreadCount('heartbeat')
def daemonCommands(self): def daemonCommands(self):
'''process daemon commands from daemonQueue''' '''
Process daemon commands from daemonQueue
'''
cmd = self._core.daemonQueue() cmd = self._core.daemonQueue()
if cmd is not False: if cmd is not False:
@ -457,7 +464,7 @@ class OnionrCommunicatorDaemon:
self.announce(cmd[1]) self.announce(cmd[1])
else: else:
logger.warn("Not introducing, since I have no connected nodes.") logger.warn("Not introducing, since I have no connected nodes.")
elif cmd[0] == 'runCheck': elif cmd[0] == 'runCheck': # deprecated
logger.debug('Status check; looks good.') logger.debug('Status check; looks good.')
open(self._core.dataDir + '.runcheck', 'w+').close() open(self._core.dataDir + '.runcheck', 'w+').close()
elif cmd[0] == 'connectedPeers': elif cmd[0] == 'connectedPeers':
@ -538,6 +545,12 @@ class OnionrCommunicatorDaemon:
self.shutdown = True self.shutdown = True
self.decrementThreadCount('detectAPICrash') self.decrementThreadCount('detectAPICrash')
def runCheck(self):
if self.daemonTools.runCheck():
logger.debug('Status check; looks good.')
self.decrementThreadCount('runCheck')
class OnionrCommunicatorTimers: class OnionrCommunicatorTimers:
def __init__(self, daemonInstance, timerFunction, frequency, makeThread=True, threadAmount=1, maxThreads=5, requiresPeer=False): def __init__(self, daemonInstance, timerFunction, frequency, makeThread=True, threadAmount=1, maxThreads=5, requiresPeer=False):
self.timerFunction = timerFunction self.timerFunction = timerFunction
@ -571,7 +584,7 @@ class OnionrCommunicatorTimers:
if self.makeThread: if self.makeThread:
for i in range(self.threadAmount): for i in range(self.threadAmount):
if self.daemonInstance.threadCounts[self.timerFunction.__name__] >= self.maxThreads: if self.daemonInstance.threadCounts[self.timerFunction.__name__] >= self.maxThreads:
logger.warn(self.timerFunction.__name__ + ' has too many current threads to start anymore.') logger.warn('%s is currently using the maximum number of threads, not starting another.' % self.timerFunction.__name__)
else: else:
self.daemonInstance.threadCounts[self.timerFunction.__name__] += 1 self.daemonInstance.threadCounts[self.timerFunction.__name__] += 1
newThread = threading.Thread(target=self.timerFunction) newThread = threading.Thread(target=self.timerFunction)

View File

@ -23,6 +23,7 @@ from onionrblockapi import Block
import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions, onionrvalues import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions, onionrvalues
import onionrblacklist, onionrchat, onionrusers import onionrblacklist, onionrchat, onionrusers
import dbcreator import dbcreator
if sys.version_info < (3, 6): if sys.version_info < (3, 6):
try: try:
import sha3 import sha3
@ -104,7 +105,10 @@ class Core:
return return
def refreshFirstStartVars(self): def refreshFirstStartVars(self):
'''Hack to refresh some vars which may not be set on first start''' '''
Hack to refresh some vars which may not be set on first start
'''
if os.path.exists(self.dataDir + '/hs/hostname'): if os.path.exists(self.dataDir + '/hs/hostname'):
with open(self.dataDir + '/hs/hostname', 'r') as hs: with open(self.dataDir + '/hs/hostname', 'r') as hs:
self.hsAddress = hs.read().strip() self.hsAddress = hs.read().strip()
@ -113,6 +117,7 @@ class Core:
''' '''
Adds a public key to the key database (misleading function name) Adds a public key to the key database (misleading function name)
''' '''
# This function simply adds a peer to the DB # This function simply adds a peer to the DB
if not self._utils.validatePubKey(peerID): if not self._utils.validatePubKey(peerID):
return False return False
@ -127,7 +132,7 @@ class Core:
c = conn.cursor() c = conn.cursor()
t = (peerID, name, 'unknown', hashID, powID, 0) t = (peerID, name, 'unknown', hashID, powID, 0)
for i in c.execute("SELECT * FROM PEERS where id = '" + peerID + "';"): for i in c.execute("SELECT * FROM peers WHERE id = ?;", (peerID,)):
try: try:
if i[0] == peerID: if i[0] == peerID:
conn.close() conn.close()
@ -146,8 +151,8 @@ class Core:
''' '''
Add an address to the address database (only tor currently) Add an address to the address database (only tor currently)
''' '''
if address == config.get('i2p.ownAddr', None) or address == self.hsAddress:
if address == config.get('i2p.ownAddr', None) or address == self.hsAddress:
return False return False
if self._utils.validateID(address): if self._utils.validateID(address):
conn = sqlite3.connect(self.addressDB, timeout=10) conn = sqlite3.connect(self.addressDB, timeout=10)
@ -155,7 +160,7 @@ class Core:
# check if address is in database # check if address is in database
# this is safe to do because the address is validated above, but we strip some chars here too just in case # this is safe to do because the address is validated above, but we strip some chars here too just in case
address = address.replace('\'', '').replace(';', '').replace('"', '').replace('\\', '') address = address.replace('\'', '').replace(';', '').replace('"', '').replace('\\', '')
for i in c.execute("SELECT * FROM adders where address = '" + address + "';"): for i in c.execute("SELECT * FROM adders WHERE address = ?;", (address,)):
try: try:
if i[0] == address: if i[0] == address:
conn.close() conn.close()
@ -174,13 +179,14 @@ class Core:
return True return True
else: else:
logger.debug('Invalid ID') logger.debug('Invalid ID: %s' % address)
return False return False
def removeAddress(self, address): def removeAddress(self, address):
''' '''
Remove an address from the address database Remove an address from the address database
''' '''
if self._utils.validateID(address): if self._utils.validateID(address):
conn = sqlite3.connect(self.addressDB, timeout=10) conn = sqlite3.connect(self.addressDB, timeout=10)
c = conn.cursor() c = conn.cursor()
@ -200,6 +206,7 @@ class Core:
**You may want blacklist.addToDB(blockHash) **You may want blacklist.addToDB(blockHash)
''' '''
if self._utils.validateHash(block): if self._utils.validateHash(block):
conn = sqlite3.connect(self.blockDB, timeout=10) conn = sqlite3.connect(self.blockDB, timeout=10)
c = conn.cursor() c = conn.cursor()
@ -207,7 +214,7 @@ class Core:
c.execute('Delete from hashes where hash=?;', t) c.execute('Delete from hashes where hash=?;', t)
conn.commit() conn.commit()
conn.close() conn.close()
blockFile = self.dataDir + '/blocks/' + block + '.dat' blockFile = self.dataDir + '/blocks/%s.dat' % block
dataSize = 0 dataSize = 0
try: try:
''' Get size of data when loaded as an object/var, rather than on disk, ''' Get size of data when loaded as an object/var, rather than on disk,
@ -224,18 +231,21 @@ class Core:
''' '''
Generate the address database Generate the address database
''' '''
self.dbCreate.createAddressDB() self.dbCreate.createAddressDB()
def createPeerDB(self): def createPeerDB(self):
''' '''
Generate the peer sqlite3 database and populate it with the peers table. Generate the peer sqlite3 database and populate it with the peers table.
''' '''
self.dbCreate.createPeerDB() self.dbCreate.createPeerDB()
def createBlockDB(self): def createBlockDB(self):
''' '''
Create a database for blocks Create a database for blocks
''' '''
self.dbCreate.createBlockDB() self.dbCreate.createBlockDB()
def addToBlockDB(self, newHash, selfInsert=False, dataSaved=False): def addToBlockDB(self, newHash, selfInsert=False, dataSaved=False):
@ -244,6 +254,7 @@ class Core:
Should be in hex format! Should be in hex format!
''' '''
if not os.path.exists(self.blockDB): if not os.path.exists(self.blockDB):
raise Exception('Block db does not exist') raise Exception('Block db does not exist')
if self._utils.hasBlock(newHash): if self._utils.hasBlock(newHash):
@ -266,6 +277,7 @@ class Core:
''' '''
Simply return the data associated to a hash Simply return the data associated to a hash
''' '''
try: try:
# logger.debug('Opening %s' % (str(self.blockDataLocation) + str(hash) + '.dat')) # logger.debug('Opening %s' % (str(self.blockDataLocation) + str(hash) + '.dat'))
dataFile = open(self.blockDataLocation + hash + '.dat', 'rb') dataFile = open(self.blockDataLocation + hash + '.dat', 'rb')
@ -280,6 +292,7 @@ class Core:
''' '''
Set the data assciated with a hash Set the data assciated with a hash
''' '''
data = data data = data
dataSize = sys.getsizeof(data) dataSize = sys.getsizeof(data)
@ -301,7 +314,7 @@ class Core:
blockFile.close() blockFile.close()
conn = sqlite3.connect(self.blockDB, timeout=10) conn = sqlite3.connect(self.blockDB, timeout=10)
c = conn.cursor() c = conn.cursor()
c.execute("UPDATE hashes SET dataSaved=1 WHERE hash = '" + dataHash + "';") c.execute("UPDATE hashes SET dataSaved=1 WHERE hash = ?;", (dataHash,))
conn.commit() conn.commit()
conn.close() conn.close()
with open(self.dataNonceFile, 'a') as nonceFile: with open(self.dataNonceFile, 'a') as nonceFile:
@ -317,6 +330,7 @@ class Core:
This function intended to be used by the client. Queue to exchange data between "client" and server. This function intended to be used by the client. Queue to exchange data between "client" and server.
''' '''
retData = False retData = False
if not os.path.exists(self.queueDB): if not os.path.exists(self.queueDB):
self.dbCreate.createDaemonDB() self.dbCreate.createDaemonDB()
@ -343,12 +357,15 @@ class Core:
''' '''
Add a command to the daemon queue, used by the communication daemon (communicator.py) Add a command to the daemon queue, used by the communication daemon (communicator.py)
''' '''
retData = True retData = True
# Intended to be used by the web server # Intended to be used by the web server
date = self._utils.getEpoch() date = self._utils.getEpoch()
conn = sqlite3.connect(self.queueDB, timeout=10) conn = sqlite3.connect(self.queueDB, timeout=10)
c = conn.cursor() c = conn.cursor()
t = (command, data, date) t = (command, data, date)
try: try:
c.execute('INSERT INTO commands (command, data, date) VALUES(?, ?, ?)', t) c.execute('INSERT INTO commands (command, data, date) VALUES(?, ?, ?)', t)
conn.commit() conn.commit()
@ -366,11 +383,13 @@ class Core:
''' '''
conn = sqlite3.connect(self.queueDB, timeout=10) conn = sqlite3.connect(self.queueDB, timeout=10)
c = conn.cursor() c = conn.cursor()
try: try:
c.execute('DELETE FROM commands;') c.execute('DELETE FROM commands;')
conn.commit() conn.commit()
except: except:
pass pass
conn.close() conn.close()
events.event('queue_clear', onionr = None) events.event('queue_clear', onionr = None)
@ -401,16 +420,21 @@ class Core:
''' '''
conn = sqlite3.connect(self.peerDB, timeout=10) conn = sqlite3.connect(self.peerDB, timeout=10)
c = conn.cursor() c = conn.cursor()
payload = ""
payload = ''
if trust not in (0, 1, 2): if trust not in (0, 1, 2):
logger.error('Tried to select invalid trust.') logger.error('Tried to select invalid trust.')
return return
if randomOrder: if randomOrder:
payload = 'SELECT * FROM peers where trust >= %s ORDER BY RANDOM();' % (trust,) payload = 'SELECT * FROM peers WHERE trust >= ? ORDER BY RANDOM();'
else: else:
payload = 'SELECT * FROM peers where trust >= %s;' % (trust,) payload = 'SELECT * FROM peers WHERE trust >= ?;'
peerList = [] peerList = []
for i in c.execute(payload):
for i in c.execute(payload, (trust,)):
try: try:
if len(i[0]) != 0: if len(i[0]) != 0:
if getPow: if getPow:
@ -419,6 +443,7 @@ class Core:
peerList.append(i[0]) peerList.append(i[0])
except TypeError: except TypeError:
pass pass
if getPow: if getPow:
try: try:
peerList.append(self._crypto.pubKey + '-' + self._crypto.pubKeyPowToken) peerList.append(self._crypto.pubKey + '-' + self._crypto.pubKeyPowToken)
@ -426,7 +451,9 @@ class Core:
pass pass
else: else:
peerList.append(self._crypto.pubKey) peerList.append(self._crypto.pubKey)
conn.close() conn.close()
return peerList return peerList
def getPeerInfo(self, peer, info): def getPeerInfo(self, peer, info):
@ -445,18 +472,22 @@ class Core:
''' '''
conn = sqlite3.connect(self.peerDB, timeout=10) conn = sqlite3.connect(self.peerDB, timeout=10)
c = conn.cursor() c = conn.cursor()
command = (peer,) command = (peer,)
infoNumbers = {'id': 0, 'name': 1, 'adders': 2, 'dateSeen': 3, 'bytesStored': 4, 'trust': 5, 'pubkeyExchanged': 6, 'hashID': 7} infoNumbers = {'id': 0, 'name': 1, 'adders': 2, 'dateSeen': 3, 'bytesStored': 4, 'trust': 5, 'pubkeyExchanged': 6, 'hashID': 7}
info = infoNumbers[info] info = infoNumbers[info]
iterCount = 0 iterCount = 0
retVal = '' retVal = ''
for row in c.execute('SELECT * from peers where id=?;', command):
for row in c.execute('SELECT * FROM peers WHERE id=?;', command):
for i in row: for i in row:
if iterCount == info: if iterCount == info:
retVal = i retVal = i
break break
else: else:
iterCount += 1 iterCount += 1
conn.close() conn.close()
return retVal return retVal
@ -465,15 +496,20 @@ class Core:
''' '''
Update a peer for a key Update a peer for a key
''' '''
conn = sqlite3.connect(self.peerDB, timeout=10) conn = sqlite3.connect(self.peerDB, timeout=10)
c = conn.cursor() c = conn.cursor()
command = (data, peer) command = (data, peer)
# TODO: validate key on whitelist # TODO: validate key on whitelist
if key not in ('id', 'name', 'pubkey', 'blockDBHash', 'forwardKey', 'dateSeen', 'bytesStored', 'trust'): if key not in ('id', 'name', 'pubkey', 'blockDBHash', 'forwardKey', 'dateSeen', 'bytesStored', 'trust'):
raise Exception("Got invalid database key when setting peer info") raise Exception("Got invalid database key when setting peer info")
c.execute('UPDATE peers SET ' + key + ' = ? WHERE id=?', command) c.execute('UPDATE peers SET ' + key + ' = ? WHERE id=?', command)
conn.commit() conn.commit()
conn.close() conn.close()
return return
def getAddressInfo(self, address, info): def getAddressInfo(self, address, info):
@ -489,14 +525,17 @@ class Core:
failure int 6 failure int 6
lastConnect 7 lastConnect 7
''' '''
conn = sqlite3.connect(self.addressDB, timeout=10) conn = sqlite3.connect(self.addressDB, timeout=10)
c = conn.cursor() c = conn.cursor()
command = (address,) command = (address,)
infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'DBHash': 5, 'failure': 6, 'lastConnect': 7} infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'DBHash': 5, 'failure': 6, 'lastConnect': 7}
info = infoNumbers[info] info = infoNumbers[info]
iterCount = 0 iterCount = 0
retVal = '' retVal = ''
for row in c.execute('SELECT * from adders where address=?;', command):
for row in c.execute('SELECT * FROM adders WHERE address=?;', command):
for i in row: for i in row:
if iterCount == info: if iterCount == info:
retVal = i retVal = i
@ -504,15 +543,19 @@ class Core:
else: else:
iterCount += 1 iterCount += 1
conn.close() conn.close()
return retVal return retVal
def setAddressInfo(self, address, key, data): def setAddressInfo(self, address, key, data):
''' '''
Update an address for a key Update an address for a key
''' '''
conn = sqlite3.connect(self.addressDB, timeout=10) conn = sqlite3.connect(self.addressDB, timeout=10)
c = conn.cursor() c = conn.cursor()
command = (data, address) command = (data, address)
# TODO: validate key on whitelist # TODO: validate key on whitelist
if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', 'failure', 'lastConnect', 'lastConnectAttempt'): if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', 'failure', 'lastConnect', 'lastConnectAttempt'):
raise Exception("Got invalid database key when setting address info") raise Exception("Got invalid database key when setting address info")
@ -520,18 +563,22 @@ class Core:
c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command) c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command)
conn.commit() conn.commit()
conn.close() conn.close()
return return
def getBlockList(self, unsaved = False): # TODO: Use unsaved?? def getBlockList(self, unsaved = False): # TODO: Use unsaved??
''' '''
Get list of our blocks Get list of our blocks
''' '''
conn = sqlite3.connect(self.blockDB, timeout=10) conn = sqlite3.connect(self.blockDB, timeout=10)
c = conn.cursor() c = conn.cursor()
if unsaved: if unsaved:
execute = 'SELECT hash FROM hashes WHERE dataSaved != 1 ORDER BY RANDOM();' execute = 'SELECT hash FROM hashes WHERE dataSaved != 1 ORDER BY RANDOM();'
else: else:
execute = 'SELECT hash FROM hashes ORDER BY dateReceived ASC;' execute = 'SELECT hash FROM hashes ORDER BY dateReceived ASC;'
rows = list() rows = list()
for row in c.execute(execute): for row in c.execute(execute):
for i in row: for i in row:
@ -543,8 +590,10 @@ class Core:
''' '''
Returns the date a block was received Returns the date a block was received
''' '''
conn = sqlite3.connect(self.blockDB, timeout=10) conn = sqlite3.connect(self.blockDB, timeout=10)
c = conn.cursor() c = conn.cursor()
execute = 'SELECT dateReceived FROM hashes WHERE hash=?;' execute = 'SELECT dateReceived FROM hashes WHERE hash=?;'
args = (blockHash,) args = (blockHash,)
for row in c.execute(execute, args): for row in c.execute(execute, args):
@ -557,17 +606,22 @@ class Core:
''' '''
Returns a list of blocks by the type Returns a list of blocks by the type
''' '''
conn = sqlite3.connect(self.blockDB, timeout=10) conn = sqlite3.connect(self.blockDB, timeout=10)
c = conn.cursor() c = conn.cursor()
if orderDate: if orderDate:
execute = 'SELECT hash FROM hashes WHERE dataType=? ORDER BY dateReceived;' execute = 'SELECT hash FROM hashes WHERE dataType=? ORDER BY dateReceived;'
else: else:
execute = 'SELECT hash FROM hashes WHERE dataType=?;' execute = 'SELECT hash FROM hashes WHERE dataType=?;'
args = (blockType,) args = (blockType,)
rows = list() rows = list()
for row in c.execute(execute, args): for row in c.execute(execute, args):
for i in row: for i in row:
rows.append(i) rows.append(i)
return rows return rows
def getExpiredBlocks(self): def getExpiredBlocks(self):
@ -591,9 +645,10 @@ class Core:
conn = sqlite3.connect(self.blockDB, timeout=10) conn = sqlite3.connect(self.blockDB, timeout=10)
c = conn.cursor() c = conn.cursor()
c.execute("UPDATE hashes SET dataType='" + blockType + "' WHERE hash = '" + hash + "';") c.execute("UPDATE hashes SET dataType = ? WHERE hash = ?;", (blockType, hash))
conn.commit() conn.commit()
conn.close() conn.close()
return return
def updateBlockInfo(self, hash, key, data): def updateBlockInfo(self, hash, key, data):
@ -621,6 +676,7 @@ class Core:
c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args) c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args)
conn.commit() conn.commit()
conn.close() conn.close()
return True return True
def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = None, expire=None): def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = None, expire=None):
@ -628,6 +684,7 @@ class Core:
Inserts a block into the network Inserts a block into the network
encryptType must be specified to encrypt a block encryptType must be specified to encrypt a block
''' '''
retData = False retData = False
# check nonce # check nonce
@ -642,9 +699,6 @@ class Core:
with open(self.dataNonceFile, 'a') as nonceFile: with open(self.dataNonceFile, 'a') as nonceFile:
nonceFile.write(dataNonce + '\n') nonceFile.write(dataNonce + '\n')
if meta is None:
meta = dict()
if type(data) is bytes: if type(data) is bytes:
data = data.decode() data = data.decode()
data = str(data) data = str(data)

View File

@ -73,6 +73,7 @@ LEVEL_INFO = 2
LEVEL_WARN = 3 LEVEL_WARN = 3
LEVEL_ERROR = 4 LEVEL_ERROR = 4
LEVEL_FATAL = 5 LEVEL_FATAL = 5
LEVEL_IMPORTANT = 6
_type = OUTPUT_TO_CONSOLE | USE_ANSI # the default settings for logging _type = OUTPUT_TO_CONSOLE | USE_ANSI # the default settings for logging
_level = LEVEL_DEBUG # the lowest level to log _level = LEVEL_DEBUG # the lowest level to log
@ -123,18 +124,18 @@ def get_file():
return _outputfile return _outputfile
def raw(data, fd = sys.stdout): def raw(data, fd = sys.stdout, sensitive = False):
''' '''
Outputs raw data to console without formatting Outputs raw data to console without formatting
''' '''
if get_settings() & OUTPUT_TO_CONSOLE: if get_settings() & OUTPUT_TO_CONSOLE:
ts = fd.write('%s\n' % data) ts = fd.write('%s\n' % data)
if get_settings() & OUTPUT_TO_FILE: if get_settings() & OUTPUT_TO_FILE and not sensitive:
with open(_outputfile, "a+") as f: with open(_outputfile, "a+") as f:
f.write(colors.filter(data) + '\n') f.write(colors.filter(data) + '\n')
def log(prefix, data, color = '', timestamp=True, fd = sys.stdout, prompt = True): def log(prefix, data, color = '', timestamp=True, fd = sys.stdout, prompt = True, sensitive = False):
''' '''
Logs the data Logs the data
prefix : The prefix to the output prefix : The prefix to the output
@ -149,7 +150,7 @@ def log(prefix, data, color = '', timestamp=True, fd = sys.stdout, prompt = True
if not get_settings() & USE_ANSI: if not get_settings() & USE_ANSI:
output = colors.filter(output) output = colors.filter(output)
raw(output, fd = fd) raw(output, fd = fd, sensitive = sensitive)
def readline(message = ''): def readline(message = ''):
''' '''
@ -201,37 +202,37 @@ def confirm(default = 'y', message = 'Are you sure %s? '):
return default == 'y' return default == 'y'
# debug: when there is info that could be useful for debugging purposes only # debug: when there is info that could be useful for debugging purposes only
def debug(data, error = None, timestamp = True, prompt = True): def debug(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_DEBUG):
if get_level() <= LEVEL_DEBUG: if get_level() <= level:
log('/', data, timestamp=timestamp, prompt = prompt) log('/', data, timestamp = timestamp, prompt = prompt, sensitive = sensitive)
if not error is None: if not error is None:
debug('Error: ' + str(error) + parse_error()) debug('Error: ' + str(error) + parse_error())
# info: when there is something to notify the user of, such as the success of a process # info: when there is something to notify the user of, such as the success of a process
def info(data, timestamp = False, prompt = True): def info(data, timestamp = False, prompt = True, sensitive = False, level = LEVEL_INFO):
if get_level() <= LEVEL_INFO: if get_level() <= level:
log('+', data, colors.fg.green, timestamp = timestamp, prompt = prompt) log('+', data, colors.fg.green, timestamp = timestamp, prompt = prompt, sensitive = sensitive)
# warn: when there is a potential for something bad to happen # warn: when there is a potential for something bad to happen
def warn(data, error = None, timestamp = True, prompt = True): def warn(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_WARN):
if not error is None: if not error is None:
debug('Error: ' + str(error) + parse_error()) debug('Error: ' + str(error) + parse_error())
if get_level() <= LEVEL_WARN: if get_level() <= level:
log('!', data, colors.fg.orange, timestamp = timestamp, prompt = prompt) log('!', data, colors.fg.orange, timestamp = timestamp, prompt = prompt, sensitive = sensitive)
# error: when only one function, module, or process of the program encountered a problem and must stop # error: when only one function, module, or process of the program encountered a problem and must stop
def error(data, error = None, timestamp = True, prompt = True): def error(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_ERROR):
if get_level() <= LEVEL_ERROR: if get_level() <= level:
log('-', data, colors.fg.red, timestamp = timestamp, fd = sys.stderr, prompt = prompt) log('-', data, colors.fg.red, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive)
if not error is None: if not error is None:
debug('Error: ' + str(error) + parse_error()) debug('Error: ' + str(error) + parse_error())
# fatal: when the something so bad has happened that the program must stop # fatal: when the something so bad has happened that the program must stop
def fatal(data, error = None, timestamp=True, prompt = True): def fatal(data, error = None, timestamp=True, prompt = True, sensitive = False, level = LEVEL_FATAL):
if not error is None: if not error is None:
debug('Error: ' + str(error) + parse_error()) debug('Error: ' + str(error) + parse_error(), sensitive = sensitive)
if get_level() <= LEVEL_FATAL: if get_level() <= level:
log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp=timestamp, fd = sys.stderr, prompt = prompt) log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive)
# returns a formatted error message # returns a formatted error message
def parse_error(): def parse_error():

View File

@ -65,7 +65,7 @@ class NetController:
Generate a torrc file for our tor instance Generate a torrc file for our tor instance
''' '''
hsVer = '# v2 onions' hsVer = '# v2 onions'
if config.get('tor.v3onions'): if config.get('tor.v3_onions'):
hsVer = 'HiddenServiceVersion 3' hsVer = 'HiddenServiceVersion 3'
logger.info('Using v3 onions :)') logger.info('Using v3 onions :)')
@ -141,7 +141,7 @@ HashedControlPassword ''' + str(password) + '''
logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running?') logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running?')
return False return False
except KeyboardInterrupt: except KeyboardInterrupt:
logger.fatal("Got keyboard interrupt.") logger.fatal('Got keyboard interrupt.', timestamp = false, level = logger.LEVEL_IMPORTANT)
return False return False
logger.debug('Finished starting Tor.', timestamp=True) logger.debug('Finished starting Tor.', timestamp=True)

View File

@ -40,7 +40,7 @@ except ImportError:
raise Exception("You need the PySocks module (for use with socks5 proxy to use Tor)") 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.VoidNet.Tech'
ONIONR_VERSION = '0.3.1' # for debugging and stuff ONIONR_VERSION = '0.3.2' # for debugging and stuff
ONIONR_VERSION_TUPLE = tuple(ONIONR_VERSION.split('.')) # (MAJOR, MINOR, VERSION) 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 fundemental about how the API works changes. This way other nodes know how to communicate without learning too much information about you.
@ -64,37 +64,7 @@ class Onionr:
self.dataDir = 'data/' self.dataDir = 'data/'
# Load global configuration data # Load global configuration data
data_exists = Onionr.setupConfig(self.dataDir, self = self)
data_exists = os.path.exists(self.dataDir)
if not data_exists:
os.mkdir(self.dataDir)
if os.path.exists('static-data/default_config.json'):
config.set_config(json.loads(open('static-data/default_config.json').read())) # this is the default config, it will be overwritten if a config file already exists. Else, it saves it
else:
# the default config file doesn't exist, try hardcoded config
config.set_config({'dev_mode': True, 'log': {'file': {'output': True, 'path': self.dataDir + 'output.log'}, 'console': {'output': True, 'color': True}}})
if not data_exists:
config.save()
config.reload() # this will read the configuration file into memory
settings = 0b000
if config.get('log.console.color', True):
settings = settings | logger.USE_ANSI
if config.get('log.console.output', True):
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/', self.dataDir))
logger.set_settings(settings)
if str(config.get('general.dev_mode', True)).lower() == 'true':
self._developmentMode = True
logger.set_level(logger.LEVEL_DEBUG)
else:
self._developmentMode = False
logger.set_level(logger.LEVEL_INFO)
self.onionrCore = core.Core() self.onionrCore = core.Core()
self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore) self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore)
@ -156,6 +126,16 @@ class Onionr:
'status': self.showStats, 'status': self.showStats,
'statistics': self.showStats, 'statistics': self.showStats,
'stats': self.showStats, 'stats': self.showStats,
'details' : self.showDetails,
'detail' : self.showDetails,
'show-details' : self.showDetails,
'show-detail' : self.showDetails,
'showdetails' : self.showDetails,
'showdetail' : self.showDetails,
'get-details' : self.showDetails,
'get-detail' : self.showDetails,
'getdetails' : self.showDetails,
'getdetail' : self.showDetails,
'enable-plugin': self.enablePlugin, 'enable-plugin': self.enablePlugin,
'enplugin': self.enablePlugin, 'enplugin': self.enablePlugin,
@ -204,6 +184,7 @@ class Onionr:
'ui' : self.openUI, 'ui' : self.openUI,
'gui' : self.openUI, 'gui' : self.openUI,
'chat': self.startChat,
'getpassword': self.printWebPassword, 'getpassword': self.printWebPassword,
'get-password': self.printWebPassword, 'get-password': self.printWebPassword,
@ -223,14 +204,18 @@ class Onionr:
'help': 'Displays this Onionr help menu', 'help': 'Displays this Onionr help menu',
'version': 'Displays the Onionr version', 'version': 'Displays the Onionr version',
'config': 'Configures something and adds it to the file', 'config': 'Configures something and adds it to the file',
'start': 'Starts the Onionr daemon', 'start': 'Starts the Onionr daemon',
'stop': 'Stops the Onionr daemon', 'stop': 'Stops the Onionr daemon',
'stats': 'Displays node statistics', 'stats': 'Displays node statistics',
'get-password': 'Displays the web password', 'details': 'Displays the web password, public key, and human readable public key',
'enable-plugin': 'Enables and starts a plugin', 'enable-plugin': 'Enables and starts a plugin',
'disable-plugin': 'Disables and stops a plugin', 'disable-plugin': 'Disables and stops a plugin',
'reload-plugin': 'Reloads a plugin', 'reload-plugin': 'Reloads a plugin',
'create-plugin': 'Creates directory structure for a plugin', 'create-plugin': 'Creates directory structure for a plugin',
'add-peer': 'Adds a peer to database', 'add-peer': 'Adds a peer to database',
'list-peers': 'Displays a list of peers', 'list-peers': 'Displays a list of peers',
'add-file': 'Create an Onionr block from a file', 'add-file': 'Create an Onionr block from a file',
@ -261,6 +246,17 @@ class Onionr:
THIS SECTION HANDLES THE COMMANDS THIS SECTION HANDLES THE COMMANDS
''' '''
def showDetails(self):
details = {
'Node Address' : self.get_hostname(),
'Web Password' : self.getWebPassword(),
'Public Key' : self.onionrCore._crypto.pubKey,
'Human-readable Public Key' : self.onionrCore._utils.getHumanReadableID()
}
for detail in details:
logger.info('%s%s: \n%s%s\n' % (logger.colors.fg.lightgreen, detail, logger.colors.fg.green, details[detail]), sensitive = True)
def startChat(self): def startChat(self):
try: try:
data = json.dumps({'peer': sys.argv[2], 'reason': 'chat'}) data = json.dumps({'peer': sys.argv[2], 'reason': 'chat'})
@ -314,6 +310,48 @@ class Onionr:
logger.info('Syntax: friend add/remove/list [address]') logger.info('Syntax: friend add/remove/list [address]')
def friendCmd(self):
'''List, add, or remove friend(s)
Changes their peer DB entry.
'''
friend = ''
try:
# Get the friend command
action = sys.argv[2]
except IndexError:
logger.info('Syntax: friend add/remove/list [address]')
else:
action = action.lower()
if action == 'list':
# List out peers marked as our friend
for friend in self.onionrCore.listPeers(randomOrder=False, trust=1):
if friend == self.onionrCore._crypto.pubKey: # do not list our key
continue
friendProfile = onionrusers.OnionrUser(self.onionrCore, friend)
logger.info(friend + ' - ' + friendProfile.getName())
elif action in ('add', 'remove'):
try:
friend = sys.argv[3]
if not self.onionrUtils.validatePubKey(friend):
raise onionrexceptions.InvalidPubkey('Public key is invalid')
if friend not in self.onionrCore.listPeers():
raise onionrexceptions.KeyNotKnown
friend = onionrusers.OnionrUser(self.onionrCore, friend)
except IndexError:
logger.error('Friend ID is required.')
except onionrexceptions.KeyNotKnown:
logger.error('That peer is not in our database')
else:
if action == 'add':
friend.setTrust(1)
logger.info('Added %s as friend.' % (friend.publicKey,))
else:
friend.setTrust(0)
logger.info('Removed %s as friend.' % (friend.publicKey,))
else:
logger.info('Syntax: friend add/remove/list [address]')
def banBlock(self): def banBlock(self):
try: try:
ban = sys.argv[2] ban = sys.argv[2]
@ -346,7 +384,7 @@ class Onionr:
return config.get('client.hmac') return config.get('client.hmac')
def printWebPassword(self): def printWebPassword(self):
print(self.getWebPassword()) logger.info(self.getWebPassword(), sensitive = True)
def getHelp(self): def getHelp(self):
return self.cmdhelp return self.cmdhelp
@ -399,16 +437,16 @@ class Onionr:
THIS SECTION DEFINES THE COMMANDS THIS SECTION DEFINES THE COMMANDS
''' '''
def version(self, verbosity=5): def version(self, verbosity = 5, function = logger.info):
''' '''
Displays the Onionr version Displays the Onionr version
''' '''
logger.info('Onionr %s (%s) - API v%s' % (ONIONR_VERSION, platform.machine(), API_VERSION)) function('Onionr v%s (%s) (API v%s)' % (ONIONR_VERSION, platform.machine(), API_VERSION))
if verbosity >= 1: if verbosity >= 1:
logger.info(ONIONR_TAGLINE) function(ONIONR_TAGLINE)
if verbosity >= 2: if verbosity >= 2:
logger.info('Running on %s %s' % (platform.platform(), platform.release())) function('Running on %s %s' % (platform.platform(), platform.release()))
return return
@ -427,9 +465,7 @@ class Onionr:
Displays a list of keys (used to be called peers) (?) Displays a list of keys (used to be called peers) (?)
''' '''
logger.info('Public keys in database:\n') logger.info('%sPublic keys in database: \n%s%s' % (logger.colors.fg.lightgreen, logger.colors.fg.green, '\n'.join(self.onionrCore.listPeers())))
for i in self.onionrCore.listPeers():
logger.info(i)
def addPeer(self): def addPeer(self):
''' '''
@ -618,37 +654,56 @@ class Onionr:
''' '''
Starts the Onionr communication daemon Starts the Onionr communication daemon
''' '''
communicatorDaemon = './communicator2.py' communicatorDaemon = './communicator2.py'
# remove runcheck if it exists
if os.path.isfile('data/.runcheck'):
logger.debug('Runcheck file found on daemon start, deleting in advance.')
os.remove('data/.runcheck')
apiThread = Thread(target = api.API, args = (self.debug, API_VERSION)) apiThread = Thread(target = api.API, args = (self.debug, API_VERSION))
apiThread.start() apiThread.start()
try: try:
time.sleep(3) time.sleep(3)
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info('Got keyboard interrupt') logger.debug('Got keyboard interrupt, shutting down...')
time.sleep(1) time.sleep(1)
self.onionrUtils.localCommand('shutdown') self.onionrUtils.localCommand('shutdown')
else: else:
if apiThread.isAlive(): if apiThread.isAlive():
# configure logger and stuff
Onionr.setupConfig('data/', self = self)
if self._developmentMode: if self._developmentMode:
logger.warn('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)', timestamp = False) logger.warn('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)', timestamp = False)
net = NetController(config.get('client.port', 59496)) net = NetController(config.get('client.port', 59496))
logger.info('Tor is starting...') logger.debug('Tor is starting...')
if not net.startTor(): if not net.startTor():
sys.exit(1) sys.exit(1)
logger.info('Started .onion service: ' + logger.colors.underline + net.myID) logger.debug('Started .onion service: %s' % (logger.colors.underline + net.myID))
logger.info('Our Public key: ' + self.onionrCore._crypto.pubKey) logger.debug('Using public key: %s' % (logger.colors.underline + self.onionrCore._crypto.pubKey))
time.sleep(1) time.sleep(1)
#TODO make runable on windows
communicatorProc = subprocess.Popen([communicatorDaemon, "run", str(net.socksPort)]) # TODO: make runable on windows
# Print nice header thing :) communicatorProc = subprocess.Popen([communicatorDaemon, 'run', str(net.socksPort)])
# print nice header thing :)
if config.get('general.display_header', True): if config.get('general.display_header', True):
self.header() self.header()
logger.debug('Started communicator')
# print out debug info
self.version(verbosity = 5, function = logger.debug)
logger.debug('Python version %s' % platform.python_version())
logger.debug('Started communicator.')
events.event('daemon_start', onionr = self) events.event('daemon_start', onionr = self)
try: try:
while True: while True:
time.sleep(5) time.sleep(5)
# Break if communicator process ends, so we don't have left over processes # Break if communicator process ends, so we don't have left over processes
if communicatorProc.poll() is not None: if communicatorProc.poll() is not None:
break break
@ -689,9 +744,6 @@ class Onionr:
messages = { messages = {
# info about local client # info about local client
'Onionr Daemon Status' : ((logger.colors.fg.green + 'Online') if self.onionrUtils.isCommunicatorRunning(timeout = 9) else logger.colors.fg.red + 'Offline'), 'Onionr Daemon Status' : ((logger.colors.fg.green + 'Online') if self.onionrUtils.isCommunicatorRunning(timeout = 9) else logger.colors.fg.red + 'Offline'),
'Public Key' : self.onionrCore._crypto.pubKey,
'Human readable public key' : self.onionrCore._utils.getHumanReadableID(),
'Node Address' : self.get_hostname(),
# file and folder size stats # file and folder size stats
'div1' : True, # this creates a solid line across the screen, a div 'div1' : True, # this creates a solid line across the screen, a div
@ -798,7 +850,8 @@ class Onionr:
except IndexError: except IndexError:
logger.error("Syntax %s %s" % (sys.argv[0], '/path/to/filename <blockhash>')) logger.error("Syntax %s %s" % (sys.argv[0], '/path/to/filename <blockhash>'))
else: else:
print(fileName) logger.info(fileName)
contents = None contents = None
if os.path.exists(fileName): if os.path.exists(fileName):
logger.error("File already exists") logger.error("File already exists")
@ -806,6 +859,7 @@ class Onionr:
if not self.onionrUtils.validateHash(bHash): if not self.onionrUtils.validateHash(bHash):
logger.error('Block hash is invalid') logger.error('Block hash is invalid')
return return
Block.mergeChain(bHash, fileName) Block.mergeChain(bHash, fileName)
return return
@ -830,17 +884,83 @@ class Onionr:
else: else:
logger.error('%s add-file <filename>' % sys.argv[0], timestamp = False) logger.error('%s add-file <filename>' % sys.argv[0], timestamp = False)
def setupConfig(dataDir, self = None):
data_exists = os.path.exists(dataDir)
if not data_exists:
os.mkdir(dataDir)
if os.path.exists('static-data/default_config.json'):
config.set_config(json.loads(open('static-data/default_config.json').read())) # this is the default config, it will be overwritten if a config file already exists. Else, it saves it
else:
# the default config file doesn't exist, try hardcoded config
logger.warn('Default configuration file does not exist, switching to hardcoded fallback configuration!')
config.set_config({'dev_mode': True, 'log': {'file': {'output': True, 'path': dataDir + 'output.log'}, 'console': {'output': True, 'color': True}}})
if not data_exists:
config.save()
config.reload() # this will read the configuration file into memory
settings = 0b000
if config.get('log.console.color', True):
settings = settings | logger.USE_ANSI
if config.get('log.console.output', True):
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:
if str(config.get('general.dev_mode', True)).lower() == 'true':
self._developmentMode = True
logger.set_level(logger.LEVEL_DEBUG)
else:
self._developmentMode = False
logger.set_level(logger.LEVEL_INFO)
verbosity = str(config.get('log.verbosity', 'default')).lower().strip()
if not verbosity in ['default', 'null', 'none', 'nil']:
map = {
str(logger.LEVEL_DEBUG) : logger.LEVEL_DEBUG,
'verbose' : logger.LEVEL_DEBUG,
'debug' : logger.LEVEL_DEBUG,
str(logger.LEVEL_INFO) : logger.LEVEL_INFO,
'info' : logger.LEVEL_INFO,
'information' : logger.LEVEL_INFO,
str(logger.LEVEL_WARN) : logger.LEVEL_WARN,
'warn' : logger.LEVEL_WARN,
'warning' : logger.LEVEL_WARN,
'warnings' : logger.LEVEL_WARN,
str(logger.LEVEL_ERROR) : logger.LEVEL_ERROR,
'err' : logger.LEVEL_ERROR,
'error' : logger.LEVEL_ERROR,
'errors' : logger.LEVEL_ERROR,
str(logger.LEVEL_FATAL) : logger.LEVEL_FATAL,
'fatal' : logger.LEVEL_FATAL,
str(logger.LEVEL_IMPORTANT) : logger.LEVEL_IMPORTANT,
'silent' : logger.LEVEL_IMPORTANT,
'quiet' : logger.LEVEL_IMPORTANT,
'important' : logger.LEVEL_IMPORTANT
}
if verbosity in map:
logger.set_level(map[verbosity])
else:
logger.warn('Verbosity level %s is not valid, using default verbosity.' % verbosity)
return data_exists
def openUI(self): def openUI(self):
url = 'http://127.0.0.1:%s/ui/index.html?timingToken=%s' % (config.get('client.port', 59496), self.onionrUtils.getTimeBypassToken()) url = 'http://127.0.0.1:%s/ui/index.html?timingToken=%s' % (config.get('client.port', 59496), self.onionrUtils.getTimeBypassToken())
print('Opening %s ...' % url) logger.info('Opening %s ...' % url)
webbrowser.open(url, new = 1, autoraise = True) 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.'): 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'): if os.path.exists('static-data/header.txt') and logger.get_level() <= logger.LEVEL_INFO:
with open('static-data/header.txt', 'rb') as file: with open('static-data/header.txt', 'rb') as file:
# only to stdout, not file or log or anything # only to stdout, not file or log or anything
sys.stderr.write(file.read().decode().replace('P', logger.colors.fg.pink).replace('W', logger.colors.reset + logger.colors.bold).replace('G', logger.colors.fg.green).replace('\n', logger.colors.reset + '\n').replace('B', logger.colors.bold).replace('V', ONIONR_VERSION)) sys.stderr.write(file.read().decode().replace('P', logger.colors.fg.pink).replace('W', logger.colors.reset + logger.colors.bold).replace('G', logger.colors.fg.green).replace('\n', logger.colors.reset + '\n').replace('B', logger.colors.bold).replace('A', '%s' % API_VERSION).replace('V', ONIONR_VERSION))
logger.info(logger.colors.fg.lightgreen + '-> ' + str(message) + logger.colors.reset + logger.colors.fg.lightgreen + ' <-\n') logger.info(logger.colors.fg.lightgreen + '-> ' + str(message) + logger.colors.reset + logger.colors.fg.lightgreen + ' <-\n')
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -30,19 +30,22 @@ class OnionrBlackList:
def inBlacklist(self, data): def inBlacklist(self, data):
hashed = self._core._utils.bytesToStr(self._core._crypto.sha3Hash(data)) hashed = self._core._utils.bytesToStr(self._core._crypto.sha3Hash(data))
retData = False retData = False
if not hashed.isalnum(): if not hashed.isalnum():
raise Exception("Hashed data is not alpha numeric") raise Exception("Hashed data is not alpha numeric")
if len(hashed) > 64: if len(hashed) > 64:
raise Exception("Hashed data is too large") raise Exception("Hashed data is too large")
for i in self._dbExecute("select * from blacklist where hash='%s'" % (hashed,)):
for i in self._dbExecute("SELECT * FROM blacklist WHERE hash = ?", (hashed,)):
retData = True # this only executes if an entry is present by that hash retData = True # this only executes if an entry is present by that hash
break break
return retData return retData
def _dbExecute(self, toExec): def _dbExecute(self, toExec, params = ()):
conn = sqlite3.connect(self.blacklistDB) conn = sqlite3.connect(self.blacklistDB)
c = conn.cursor() c = conn.cursor()
retData = c.execute(toExec) retData = c.execute(toExec, params)
conn.commit() conn.commit()
return retData return retData
@ -60,13 +63,13 @@ class OnionrBlackList:
except AttributeError: except AttributeError:
raise TypeError("dataType must be int") raise TypeError("dataType must be int")
for i in self._dbExecute('select * from blacklist where dataType=%s' % (dataType,)): for i in self._dbExecute('SELECT * FROM blacklist WHERE dataType = ?', (dataType,)):
if i[1] == dataType: if i[1] == dataType:
if (curTime - i[2]) >= i[3]: if (curTime - i[2]) >= i[3]:
deleteList.append(i[0]) deleteList.append(i[0])
for thing in deleteList: for thing in deleteList:
self._dbExecute("delete from blacklist where hash='%s'" % (thing,)) self._dbExecute("DELETE FROM blacklist WHERE hash = ?", (thing,))
def generateDB(self): def generateDB(self):
self._dbExecute('''CREATE TABLE blacklist( self._dbExecute('''CREATE TABLE blacklist(
@ -79,10 +82,10 @@ class OnionrBlackList:
return return
def clearDB(self): def clearDB(self):
self._dbExecute('''delete from blacklist;);''') self._dbExecute('''DELETE FROM blacklist;);''')
def getList(self): def getList(self):
data = self._dbExecute('select * from blacklist') data = self._dbExecute('SELECT * FROM blacklist')
myList = [] myList = []
for i in data: for i in data:
myList.append(i[0]) myList.append(i[0])
@ -113,4 +116,4 @@ class OnionrBlackList:
return return
insert = (hashed,) insert = (hashed,)
blacklistDate = self._core._utils.getEpoch() blacklistDate = self._core._utils.getEpoch()
self._dbExecute("insert into blacklist (hash, dataType, blacklistDate, expire) VALUES('%s', %s, %s, %s);" % (hashed, dataType, blacklistDate, expire)) self._dbExecute("INSERT INTO blacklist (hash, dataType, blacklistDate, expire) VALUES(?, ?, ?, ?);", (str(hashed), dataType, blacklistDate, expire))

View File

@ -73,6 +73,7 @@ class Block:
''' '''
Decrypt a block, loading decrypted data into their vars Decrypt a block, loading decrypted data into their vars
''' '''
if self.decrypted: if self.decrypted:
return True return True
retData = False retData = False
@ -114,6 +115,7 @@ class Block:
''' '''
Verify if a block's signature is signed by its claimed signer Verify if a block's signature is signed by its claimed signer
''' '''
core = self.getCore() core = self.getCore()
if core._crypto.edVerify(data=self.signedData, key=self.signer, sig=self.signature, encodedData=True): if core._crypto.edVerify(data=self.signedData, key=self.signer, sig=self.signature, encodedData=True):
@ -173,7 +175,7 @@ class Block:
self.raw = str(blockdata) self.raw = str(blockdata)
self.bheader = json.loads(self.getRaw()[:self.getRaw().index('\n')]) self.bheader = json.loads(self.getRaw()[:self.getRaw().index('\n')])
self.bcontent = self.getRaw()[self.getRaw().index('\n') + 1:] self.bcontent = self.getRaw()[self.getRaw().index('\n') + 1:]
if self.bheader['encryptType'] in ('asym', 'sym'): if ('encryptType' in self.bheader) and (self.bheader['encryptType'] in ('asym', 'sym')):
self.bmetadata = self.getHeader('meta', None) self.bmetadata = self.getHeader('meta', None)
self.isEncrypted = True self.isEncrypted = True
else: else:
@ -199,7 +201,13 @@ class Block:
return True return True
except Exception as e: except Exception as e:
logger.error('Failed to update block data.', error = e, timestamp = False) logger.error('Failed to parse block %s.' % self.getHash(), error = e, timestamp = False)
# if block can't be parsed, it's a waste of precious space. Throw it away.
if not self.delete():
logger.error('Failed to delete invalid block %s.' % self.getHash(), error = e)
else:
logger.debug('Deleted invalid block %s.' % self.getHash(), timestamp = False)
self.valid = False self.valid = False
return False return False
@ -214,7 +222,7 @@ class Block:
if self.exists(): if self.exists():
os.remove(self.getBlockFile()) os.remove(self.getBlockFile())
removeBlock(self.getHash()) self.getCore().removeBlock(self.getHash())
return True return True
return False return False
@ -235,9 +243,9 @@ class Block:
if (not self.getBlockFile() is None) and (recreate is True): if (not self.getBlockFile() is None) and (recreate is True):
with open(self.getBlockFile(), 'wb') as blockFile: with open(self.getBlockFile(), 'wb') as blockFile:
blockFile.write(self.getRaw().encode()) blockFile.write(self.getRaw().encode())
self.update()
else: else:
self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign, expire=self.getExpire()) self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign, meta = self.getMetadata(), expire = self.getExpire())
self.update() self.update()
return self.getHash() return self.getHash()
@ -782,7 +790,7 @@ class Block:
return False return False
# dump old cached blocks if the size exeeds the maximum # dump old cached blocks if the size exeeds the maximum
if sys.getsizeof(Block.blockCacheOrder) >= config.get('allocations.blockCacheTotal', 50000000): # 50MB default cache size if sys.getsizeof(Block.blockCacheOrder) >= config.get('allocations.block_cache_total', 50000000): # 50MB default cache size
del Block.blockCache[blockCacheOrder.pop(0)] del Block.blockCache[blockCacheOrder.pop(0)]
# cache block content # cache block content

View File

@ -46,4 +46,5 @@ class OnionrChat:
self.communicator.socketClient.sendData(peer, "lol") self.communicator.socketClient.sendData(peer, "lol")
except: except:
pass pass
time.sleep(2) time.sleep(2)

View File

@ -17,8 +17,11 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
import onionrexceptions, onionrpeers, onionrproofs, base64, logger, onionrusers, sqlite3
import onionrexceptions, onionrpeers, onionrproofs, logger, onionrusers
import base64, sqlite3, os
from dependencies import secrets from dependencies import secrets
class DaemonTools: class DaemonTools:
def __init__(self, daemon): def __init__(self, daemon):
self.daemon = daemon self.daemon = daemon
@ -73,10 +76,12 @@ class DaemonTools:
self.daemon._core._blacklist.addToDB(oldest) self.daemon._core._blacklist.addToDB(oldest)
self.daemon._core.removeBlock(oldest) self.daemon._core.removeBlock(oldest)
logger.info('Deleted block: %s' % (oldest,)) logger.info('Deleted block: %s' % (oldest,))
# Delete expired blocks # Delete expired blocks
for bHash in self.daemon._core.getExpiredBlocks(): for bHash in self.daemon._core.getExpiredBlocks():
self.daemon._core._blacklist.addToDB(bHash) self.daemon._core._blacklist.addToDB(bHash)
self.daemon._core.removeBlock(bHash) self.daemon._core.removeBlock(bHash)
self.daemon.decrementThreadCount('cleanOldBlocks') self.daemon.decrementThreadCount('cleanOldBlocks')
def cleanKeys(self): def cleanKeys(self):
@ -85,7 +90,8 @@ class DaemonTools:
c = conn.cursor() c = conn.cursor()
time = self.daemon._core._utils.getEpoch() time = self.daemon._core._utils.getEpoch()
deleteKeys = [] deleteKeys = []
for entry in c.execute("SELECT * FROM forwardKeys where expire <= ?", (time,)):
for entry in c.execute("SELECT * FROM forwardKeys WHERE expire <= ?", (time,)):
logger.info(entry[1]) logger.info(entry[1])
deleteKeys.append(entry[1]) deleteKeys.append(entry[1])
@ -114,8 +120,9 @@ class DaemonTools:
del self.daemon.cooldownPeer[peer] del self.daemon.cooldownPeer[peer]
# Cool down a peer, if we have max connections alive for long enough # Cool down a peer, if we have max connections alive for long enough
if onlinePeerAmount >= self.daemon._core.config.get('peers.maxConnect'): if onlinePeerAmount >= self.daemon._core.config.get('peers.max_connect', 10):
finding = True finding = True
while finding: while finding:
try: try:
toCool = min(tempConnectTimes, key=tempConnectTimes.get) toCool = min(tempConnectTimes, key=tempConnectTimes.get)
@ -128,4 +135,32 @@ class DaemonTools:
else: else:
self.daemon.removeOnlinePeer(toCool) self.daemon.removeOnlinePeer(toCool)
self.daemon.cooldownPeer[toCool] = self.daemon._core._utils.getEpoch() self.daemon.cooldownPeer[toCool] = self.daemon._core._utils.getEpoch()
self.daemon.decrementThreadCount('cooldownPeer') self.daemon.decrementThreadCount('cooldownPeer')
def runCheck(self):
if os.path.isfile('data/.runcheck'):
os.remove('data/.runcheck')
return True
return False
def humanReadableTime(self, seconds):
build = ''
units = {
'year' : 31557600,
'month' : (31557600 / 12),
'day' : 86400,
'hour' : 3600,
'minute' : 60,
'second' : 1
}
for unit in units:
amnt_unit = int(seconds / units[unit])
if amnt_unit >= 1:
seconds -= amnt_unit * units[unit]
build += '%s %s' % (amnt_unit, unit) + ('s' if amnt_unit != 1 else '') + ' '
return build.strip()

View File

@ -79,8 +79,8 @@ def peerCleanup(coreInst):
logger.info('Cleaning peers...') logger.info('Cleaning peers...')
config.reload() config.reload()
minScore = int(config.get('peers.minimumScore')) minScore = int(config.get('peers.minimum_score', -100))
maxPeers = int(config.get('peers.maxStoredPeers')) maxPeers = int(config.get('peers.max_stored', 5000))
adders = getScoreSortedPeerList(coreInst) adders = getScoreSortedPeerList(coreInst)
adders.reverse() adders.reverse()

View File

@ -234,7 +234,7 @@ def check():
config.reload() config.reload()
if not config.is_set('plugins'): if not config.is_set('plugins'):
logger.debug('Generating plugin config data...') logger.debug('Generating plugin configuration data...')
config.set('plugins', {'enabled': []}, True) config.set('plugins', {'enabled': []}, True)
if not os.path.exists(os.path.dirname(get_plugins_folder())): if not os.path.exists(os.path.dirname(get_plugins_folder())):

View File

@ -87,7 +87,8 @@ class OnionrSocketServer:
def detectShutdown(self): def detectShutdown(self):
while not self._core.killSockets: while not self._core.killSockets:
time.sleep(5) time.sleep(5)
logger.info('Killing socket server')
logger.debug('Killing socket server...')
self.http_server.stop() self.http_server.stop()
def addSocket(self, peer, reason=''): def addSocket(self, peer, reason=''):

View File

@ -101,19 +101,21 @@ class OnionrUser:
conn = sqlite3.connect(self._core.peerDB, timeout=10) conn = sqlite3.connect(self._core.peerDB, timeout=10)
c = conn.cursor() c = conn.cursor()
for row in c.execute("SELECT forwardKey FROM forwardKeys WHERE peerKey = ? order by date desc", (self.publicKey,)): for row in c.execute("SELECT forwardKey FROM forwardKeys WHERE peerKey = ? ORDER BY date DESC", (self.publicKey,)):
key = row[0] key = row[0]
break break
conn.commit() conn.commit()
conn.close() conn.close()
return key return key
def _getForwardKeys(self): def _getForwardKeys(self):
conn = sqlite3.connect(self._core.peerDB, timeout=10) conn = sqlite3.connect(self._core.peerDB, timeout=10)
c = conn.cursor() c = conn.cursor()
keyList = [] keyList = []
for row in c.execute("SELECT forwardKey FROM forwardKeys WHERE peerKey = ? order by date desc", (self.publicKey,)):
for row in c.execute("SELECT forwardKey FROM forwardKeys WHERE peerKey = ? ORDER BY date DESC", (self.publicKey,)):
key = row[0] key = row[0]
keyList.append(key) keyList.append(key)
@ -150,8 +152,10 @@ class OnionrUser:
pubkey = self._core._utils.bytesToStr(pubkey) pubkey = self._core._utils.bytesToStr(pubkey)
command = (pubkey,) command = (pubkey,)
keyList = [] # list of tuples containing pub, private for peer keyList = [] # list of tuples containing pub, private for peer
for result in c.execute("SELECT * FROM myForwardKeys where peer=?", command):
for result in c.execute("SELECT * FROM myForwardKeys WHERE peer = ?", command):
keyList.append((result[1], result[2])) keyList.append((result[1], result[2]))
if len(keyList) == 0: if len(keyList) == 0:
if genNew: if genNew:
self.generateForwardKey() self.generateForwardKey()

View File

@ -125,11 +125,11 @@ class OnionrUtils:
for adder in newAdderList.split(','): for adder in newAdderList.split(','):
adder = adder.strip() 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 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: if not config.get('tor.v3_onions') and len(adder) == 62:
continue continue
if self._core.addAddress(adder): if self._core.addAddress(adder):
# Check if we have the maxmium amount of allowed stored peers # Check if we have the maxmium amount of allowed stored peers
if config.get('peers.maxStoredPeers') > len(self._core.listAdders()): if config.get('peers.max_stored') > len(self._core.listAdders()):
logger.info('Added %s to db.' % adder, timestamp = True) logger.info('Added %s to db.' % adder, timestamp = True)
retVal = True retVal = True
else: else:
@ -276,7 +276,7 @@ class OnionrUtils:
if myBlock.getMetadata('newFSKey') is not None: if myBlock.getMetadata('newFSKey') is not None:
onionrusers.OnionrUser(self._core, signer).addForwardKey(myBlock.getMetadata('newFSKey')) onionrusers.OnionrUser(self._core, signer).addForwardKey(myBlock.getMetadata('newFSKey'))
else: else:
logger.debug('FS not used for this block') logger.warn('FS not used for this encrypted block')
logger.info(myBlock.bmetadata) logger.info(myBlock.bmetadata)
try: try:
@ -330,7 +330,7 @@ class OnionrUtils:
c = conn.cursor() c = conn.cursor()
if not self.validateHash(hash): if not self.validateHash(hash):
raise Exception("Invalid hash") raise Exception("Invalid hash")
for result in c.execute("SELECT COUNT() FROM hashes where hash='" + hash + "'"): for result in c.execute("SELECT COUNT() FROM hashes WHERE hash = ?", (hash,)):
if result[0] >= 1: if result[0] >= 1:
conn.commit() conn.commit()
conn.close() conn.close()
@ -497,6 +497,12 @@ class OnionrUtils:
except binascii.Error: except binascii.Error:
retVal = False 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
return retVal return retVal
except: except:
return False return False
@ -512,7 +518,7 @@ class OnionrUtils:
c = conn.cursor() c = conn.cursor()
command = (hash,) command = (hash,)
retData = '' retData = ''
for row in c.execute('SELECT ID FROM peers where hashID=?', command): for row in c.execute('SELECT id FROM peers WHERE hashID = ?', command):
if row[0] != '': if row[0] != '':
retData = row[0] retData = row[0]
return retData return retData
@ -521,18 +527,16 @@ class OnionrUtils:
try: try:
runcheck_file = self._core.dataDir + '.runcheck' runcheck_file = self._core.dataDir + '.runcheck'
if os.path.isfile(runcheck_file): if not os.path.isfile(runcheck_file):
os.remove(runcheck_file) open(runcheck_file, 'w+').close()
logger.debug('%s file appears to have existed before the run check.' % runcheck_file, timestamp = False)
self._core.daemonQueueAdd('runCheck') # self._core.daemonQueueAdd('runCheck') # deprecated
starttime = time.time() starttime = time.time()
while True: while True:
time.sleep(interval) time.sleep(interval)
if os.path.isfile(runcheck_file):
os.remove(runcheck_file)
if not os.path.isfile(runcheck_file):
return True return True
elif time.time() - starttime >= timeout: elif time.time() - starttime >= timeout:
return False return False
@ -543,6 +547,7 @@ class OnionrUtils:
''' '''
Generates a secure random hex encoded token Generates a secure random hex encoded token
''' '''
return binascii.hexlify(os.urandom(size)) return binascii.hexlify(os.urandom(size))
def importNewBlocks(self, scanDir=''): def importNewBlocks(self, scanDir=''):
@ -625,12 +630,14 @@ class OnionrUtils:
else: else:
return return
headers = {'user-agent': 'PyOnionr'} headers = {'user-agent': 'PyOnionr'}
response_headers = dict()
try: try:
proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)}
r = requests.get(url, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30)) r = requests.get(url, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30))
# Check server is using same API version as us # Check server is using same API version as us
try: try:
if r.headers['api'] != str(API_VERSION): response_headers = r.headers
if r.headers['X-API'] != str(API_VERSION):
raise onionrexceptions.InvalidAPIVersion raise onionrexceptions.InvalidAPIVersion
except KeyError: except KeyError:
raise onionrexceptions.InvalidAPIVersion raise onionrexceptions.InvalidAPIVersion
@ -638,9 +645,12 @@ class OnionrUtils:
except KeyboardInterrupt: except KeyboardInterrupt:
raise KeyboardInterrupt raise KeyboardInterrupt
except ValueError as e: except ValueError as e:
logger.debug('Failed to make request', error = e) logger.debug('Failed to make GET request to %s' % url, error = e, sensitive = True)
except onionrexceptions.InvalidAPIVersion: except onionrexceptions.InvalidAPIVersion:
logger.debug("Node is using different API version :(") if 'X-API' in response_headers:
logger.debug('Using API version %s. Cannot communicate with node\'s API version of %s.' % (API_VERSION, response_headers['X-API']))
else:
logger.debug('Using API version %s. API version was not sent with the request.' % API_VERSION)
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
if not 'ConnectTimeoutError' in str(e) and not 'Request rejected or failed' in str(e): if not 'ConnectTimeoutError' in str(e) and not 'Request rejected or failed' in str(e):
logger.debug('Error: %s' % str(e)) logger.debug('Error: %s' % str(e))
@ -664,7 +674,7 @@ class OnionrUtils:
try: try:
retData = dataXML.getElementsByTagName('outputValue')[0].childNodes[0].data retData = dataXML.getElementsByTagName('outputValue')[0].childNodes[0].data
except ValueError: except ValueError:
logger.warn('Could not get NIST beacon value') logger.warn('Failed to get the NIST beacon value.')
else: else:
self.powSalt = retData self.powSalt = retData
return retData return retData

View File

@ -38,13 +38,12 @@ class OnionrCLIUI:
pass pass
def refresh(self): def refresh(self):
for i in range(100): print('\n' * 80 + logger.colors.reset)
print('')
def start(self): def start(self):
'''Main CLI UI interface menu''' '''Main CLI UI interface menu'''
showMenu = True showMenu = True
isOnline = "No" isOnline = 'No'
firstRun = True firstRun = True
choice = '' choice = ''
@ -53,7 +52,7 @@ class OnionrCLIUI:
while showMenu: while showMenu:
if firstRun: if firstRun:
logger.info("please wait while Onionr starts...") logger.info('Please wait while Onionr starts...')
daemon = subprocess.Popen(["./onionr.py", "start"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL) daemon = subprocess.Popen(["./onionr.py", "start"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL)
time.sleep(30) time.sleep(30)
firstRun = False firstRun = False
@ -63,9 +62,7 @@ class OnionrCLIUI:
else: else:
isOnline = "No" isOnline = "No"
print(''' logger.info('''Daemon Running: ''' + isOnline + '''
Daemon Running: ''' + isOnline + '''
1. Flow (Anonymous public chat, use at your own risk) 1. Flow (Anonymous public chat, use at your own risk)
2. Mail (Secure email-like service) 2. Mail (Secure email-like service)
3. File Sharing 3. File Sharing
@ -83,7 +80,7 @@ Daemon Running: ''' + isOnline + '''
elif choice in ("2", "mail"): elif choice in ("2", "mail"):
self.subCommand("mail") self.subCommand("mail")
elif choice in ("3", "file sharing", "file"): elif choice in ("3", "file sharing", "file"):
print("Not supported yet") logger.warn("Not supported yet")
elif choice in ("4", "user settings", "settings"): elif choice in ("4", "user settings", "settings"):
try: try:
self.setName() self.setName()
@ -91,21 +88,22 @@ Daemon Running: ''' + isOnline + '''
pass pass
elif choice in ("5", "daemon"): elif choice in ("5", "daemon"):
if isOnline == "Yes": if isOnline == "Yes":
print("Onionr daemon will shutdown...") logger.info("Onionr daemon will shutdown...")
self.myCore.daemonQueueAdd('shutdown') self.myCore.daemonQueueAdd('shutdown')
try: try:
daemon.kill() daemon.kill()
except UnboundLocalError: except UnboundLocalError:
pass pass
else: else:
print("Starting Daemon...") logger.info("Starting Daemon...")
daemon = subprocess.Popen(["./onionr.py", "start"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) daemon = subprocess.Popen(["./onionr.py", "start"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
elif choice in ("6", "quit"): elif choice in ("6", "quit"):
showMenu = False showMenu = False
elif choice == "": elif choice == "":
pass pass
else: else:
print("Invalid choice") logger.error("Invalid choice")
return return
def setName(self): def setName(self):

View File

@ -71,7 +71,8 @@ class PlainEncryption:
plaintext = 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, anonymous=True, encodedData=True)
encrypted = self.api.get_core()._utils.bytesToStr(encrypted) encrypted = self.api.get_core()._utils.bytesToStr(encrypted)
print('ONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,)) logger.info('Encrypted Message: \n\nONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,))
def decrypt(self): def decrypt(self):
plaintext = "" plaintext = ""
data = "" data = ""
@ -89,10 +90,10 @@ class PlainEncryption:
myPub = self.api.get_core()._crypto.pubKey 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, anonymous=True, encodedData=True)
if decrypted == False: if decrypted == False:
print("Decryption failed") logger.error("Decryption failed")
else: else:
data = json.loads(decrypted) data = json.loads(decrypted)
print(data['data']) logger.info('Decrypted Message: \n\n%s' % data['data'])
try: try:
logger.info("Signing public key: %s" % (data['signer'],)) logger.info("Signing public key: %s" % (data['signer'],))
assert self.api.get_core()._crypto.edVerify(data['data'], data['signer'], data['sig']) != False assert self.api.get_core()._crypto.edVerify(data['data'], data['signer'], data['sig']) != False
@ -102,7 +103,6 @@ class PlainEncryption:
logger.info("Message has good signature.") logger.info("Message has good signature.")
return return
def on_init(api, data = None): def on_init(api, data = None):
''' '''
This event is called after Onionr is initialized, but before the command This event is called after Onionr is initialized, but before the command
@ -114,4 +114,5 @@ def on_init(api, data = None):
encrypt = PlainEncryption(pluginapi) encrypt = PlainEncryption(pluginapi)
api.commands.register(['encrypt'], encrypt.encrypt) api.commands.register(['encrypt'], encrypt.encrypt)
api.commands.register(['decrypt'], encrypt.decrypt) api.commands.register(['decrypt'], encrypt.decrypt)
return return

View File

@ -585,7 +585,6 @@ def commandCreateRepository():
return True return True
blockhash = createRepository(plugins) blockhash = createRepository(plugins)
print(blockhash)
if not blockhash is None: if not blockhash is None:
logger.info('Successfully created repository. Execute the following command to add the repository:\n ' + logger.colors.underline + '%s --add-repository %s' % (script, blockhash)) logger.info('Successfully created repository. Execute the following command to add the repository:\n ' + logger.colors.underline + '%s --add-repository %s' % (script, blockhash))
else: else:

View File

@ -102,7 +102,7 @@ class OnionrMail:
displayList.append('%s. %s - %s: %s' % (blockCount, blockDate, senderDisplay[:12], blockHash)) displayList.append('%s. %s - %s: %s' % (blockCount, blockDate, senderDisplay[:12], blockHash))
#displayList.reverse() #displayList.reverse()
for i in displayList: for i in displayList:
print(i) logger.info(i)
try: try:
choice = logger.readline('Enter a block number, -r to refresh, or -q to stop: ').strip().lower() choice = logger.readline('Enter a block number, -r to refresh, or -q to stop: ').strip().lower()
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
@ -129,14 +129,16 @@ class OnionrMail:
else: else:
cancel = '' cancel = ''
readBlock.verifySig() readBlock.verifySig()
print('Message recieved from %s' % (self.myCore._utils.bytesToStr(readBlock.signer,)))
print('Valid signature:', readBlock.validSig) logger.info('Message recieved from %s' % (self.myCore._utils.bytesToStr(readBlock.signer,)))
logger.info('Valid signature: %s' % readBlock.validSig)
if not 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 signature. ANYONE could have sent this message.')
cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).') cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).')
if cancel != '-q': if cancel != '-q':
print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip()))) print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip())))
input("Press enter to continue") logger.readline("Press enter to continue")
return return
def sentbox(self): def sentbox(self):
@ -146,7 +148,7 @@ class OnionrMail:
entering = True entering = True
while entering: while entering:
self.getSentList() self.getSentList()
print('Enter block number or -q to return') logger.info('Enter block number or -q to return')
try: try:
choice = input('>') choice = input('>')
except (EOFError, KeyboardInterrupt) as e: except (EOFError, KeyboardInterrupt) as e:
@ -158,11 +160,11 @@ class OnionrMail:
try: try:
self.sentboxList[int(choice) - 1] self.sentboxList[int(choice) - 1]
except IndexError: except IndexError:
print('Invalid block') logger.warn('Invalid block.')
else: else:
logger.info('Sent to: ' + self.sentMessages[self.sentboxList[int(choice) - 1]][1]) logger.info('Sent to: ' + self.sentMessages[self.sentboxList[int(choice) - 1]][1])
# Print ansi escaped sent message # Print ansi escaped sent message
print(self.myCore._utils.escapeAnsi(self.sentMessages[self.sentboxList[int(choice) - 1]][0])) logger.info(self.myCore._utils.escapeAnsi(self.sentMessages[self.sentboxList[int(choice) - 1]][0]))
input('Press enter to continue...') input('Press enter to continue...')
return return
@ -172,7 +174,8 @@ class OnionrMail:
for i in self.sentboxTools.listSent(): for i in self.sentboxTools.listSent():
self.sentboxList.append(i['hash']) self.sentboxList.append(i['hash'])
self.sentMessages[i['hash']] = (i['message'], i['peer']) self.sentMessages[i['hash']] = (i['message'], i['peer'])
print('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date']))
logger.info('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date']))
count += 1 count += 1
def draftMessage(self): def draftMessage(self):
@ -198,7 +201,7 @@ class OnionrMail:
# if -q or ctrl-c/d, exit function here, otherwise we successfully got the public key # if -q or ctrl-c/d, exit function here, otherwise we successfully got the public key
return return
print('Enter your message, stop by entering -q on a new line.') logger.info('Enter your message, stop by entering -q on a new line.')
while newLine != '-q': while newLine != '-q':
try: try:
newLine = input() newLine = input()
@ -209,7 +212,7 @@ class OnionrMail:
newLine += '\n' newLine += '\n'
message += newLine message += newLine
print('Inserting encrypted message as Onionr block....') logger.info('Inserting encrypted message as Onionr block....')
blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True) blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True)
self.sentboxTools.addToSent(blockID, recip, message) self.sentboxTools.addToSent(blockID, recip, message)
@ -217,7 +220,7 @@ class OnionrMail:
choice = '' choice = ''
while True: while True:
print(self.strings.programTag + '\n\nOur ID: ' + self.myCore._crypto.pubKey + self.strings.mainMenu.title()) # print out main menu logger.info(self.strings.programTag + '\n\nOur ID: ' + self.myCore._crypto.pubKey + self.strings.mainMenu.title()) # print out main menu
try: try:
choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip() choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip()

View File

@ -5,6 +5,9 @@
"minimum_block_pow": 5, "minimum_block_pow": 5,
"minimum_send_pow": 5, "minimum_send_pow": 5,
"minimum_block_pow": 5,
"minimum_send_pow": 5,
"direct_connect" : { "direct_connect" : {
"respond" : true, "respond" : true,
"execute_callbacks" : true "execute_callbacks" : true
@ -13,11 +16,16 @@
"www" : { "www" : {
"public" : { "public" : {
"run" : true "run" : true,
"path" : "static-data/www/public/",
"guess_mime" : true
}, },
"private" : { "private" : {
"run" : true "run" : true,
"path" : "static-data/www/private/",
"guess_mime" : true,
"timing_protection" : true
}, },
"ui" : { "ui" : {
@ -30,7 +38,19 @@
}, },
"plugins" : {
"enabled" : {
},
"disabled" : {
}
},
"log" : { "log" : {
"verbosity" : "default",
"file": { "file": {
"output": false, "output": false,
"path": "data/output.log" "path": "data/output.log"
@ -49,22 +69,24 @@
"i2p" : { "i2p" : {
"host" : false, "host" : false,
"connect" : true, "connect" : true,
"ownAddr": "" "own_addr" : ""
}, },
"allocations" : { "allocations" : {
"disk" : 10000000000, "disk" : 10000000000,
"netTotal": 1000000000, "net_total" : 1000000000,
"blockCache" : 5000000, "blockCache" : 5000000,
"blockCacheTotal" : 50000000 "blockCacheTotal" : 50000000
}, },
"peers" : { "peers" : {
"minimumScore": -100, "minimum_score" : -100,
"maxStoredPeers": 5000, "max_stored_peers" : 5000,
"maxConnect": 10 "max_connect" : 10
}, },
"timers" : { "timers" : {
"lookupBlocks": 25, "lookup_blocks" : 25,
"getBlocks": 30 "get_blocks" : 30
} }
} }

View File

@ -3,9 +3,9 @@ P G'
P G'' P G''
P G'' ' P G'' '
P G'''''' P G''''''
P :G;'''''P: P :G''''''P:
P ::G;'''P:: P ::G''''P::
P :::G;;P::: P :::G''P:::
P :::::::: P ::::::::
P :::::::::::: P ::::::::::::
P ::::::::::::::: P :::::::::::::::
@ -20,6 +20,7 @@ P :::: ::::: ::::: ::: W :::: :: :: :: ::::: :: :: :: ::
P :::: :::::: :::::: :::: P :::: :::::: :::::: ::::
P :::: :::::::::::: :::: GvPBV P :::: :::::::::::: :::: GvPBV
P ::::: :::::::: :::: P ::::: :::::::: ::::
P ::::: :::::: P ::::: :::::
P :::::::::::::::: P ::::::::::::::::
P ::::::: P :::::::

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,18 @@
<!-- 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://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://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="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>

View File

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

View File

@ -0,0 +1,30 @@
<!-- 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

@ -0,0 +1,31 @@
<!-- 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

@ -41,16 +41,16 @@ LANG = type('LANG', (), langmap)
# templating # templating
class Template: class Template:
def jsTemplate(template): def jsTemplate(template, filename = ''):
with open('common/%s.html' % template, 'r') as file: with open('common/%s.html' % template, 'r') as file:
return Template.parseTags(file.read().replace('\\', '\\\\').replace('\'', '\\\'').replace('\n', "\\\n")) return Template.parseTags(file.read().replace('\\', '\\\\').replace('\'', '\\\'').replace('\n', "\\\n"), filename)
def htmlTemplate(template): def htmlTemplate(template, filename = ''):
with open('common/%s.html' % template, 'r') as file: with open('common/%s.html' % template, 'r') as file:
return Template.parseTags(file.read()) return Template.parseTags(file.read(), filename)
# tag parser # tag parser
def parseTags(contents): def parseTags(contents, filename = ''):
# <$ logic $> # <$ logic $>
for match in re.findall(r'(<\$(?!=)(.*?)\$>)', contents): for match in re.findall(r'(<\$(?!=)(.*?)\$>)', contents):
try: try:
@ -66,7 +66,7 @@ class Template:
try: try:
out = eval(match[1].strip()) out = eval(match[1].strip())
contents = contents.replace(match[0], '' if out is None else str(out)) contents = contents.replace(match[0], '' if out is None else str(out))
except NameError as e: except (NameError, AttributeError) as e:
name = match[1].strip() name = match[1].strip()
print('Warning: %s does not exist, treating as an str' % name) print('Warning: %s does not exist, treating as an str' % name)
contents = contents.replace(match[0], name) contents = contents.replace(match[0], name)
@ -118,7 +118,7 @@ def iterate(directory):
# do python tags # do python tags
if settings['python_tags']: if settings['python_tags']:
contents = Template.parseTags(contents) contents = Template.parseTags(contents, filename)
# write file # write file
file.write(contents) file.write(contents)

View File

@ -37,10 +37,20 @@ body {
/* timeline */ /* timeline */
.onionr-post-focus-separator {
width: 100%;
padding: 1rem;
padding-left: 0;
padding-right: 0;
}
.onionr-post { .onionr-post {
padding: 1rem; padding: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
cursor: pointer;
width: 100%; width: 100%;
} }
@ -60,6 +70,35 @@ body {
width: 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 { .h-divider {
margin: 5px 15px; margin: 5px 15px;
height: 1px; height: 1px;
@ -77,3 +116,7 @@ body {
.onionr-profile-username { .onionr-profile-username {
text-align: center; text-align: center;
} }
.onionr-profile-save {
width: 100%;
}

View File

@ -5,6 +5,17 @@ body {
/* timeline */ /* timeline */
.onionr-post-focus-separator {
border-color: black;
}
.modal-content {
border: 1px solid black;
border-radius: 1rem;
background-color: lightgray;
}
.onionr-post { .onionr-post {
border: 1px solid black; border: 1px solid black;
border-radius: 1rem; border-radius: 1rem;
@ -31,6 +42,35 @@ body {
font-size: 15pt; 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 { .h-divider {
border-top:1px solid gray; border-top:1px solid gray;
} }

View File

@ -40,10 +40,24 @@
<div class="onionr-profile"> <div class="onionr-profile">
<div class="row"> <div class="row">
<div class="col-4 col-lg-12"> <div class="col-4 col-lg-12">
<img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="img/default.png"> <img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="">
</div> </div>
<div class="col-8 col-lg-12"> <div class="col-8 col-lg-12">
<h2 id="onionr-profile-username" class="onionr-profile-username text-left text-lg-center text-sm-left">arinerron</h2> <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> </div>
@ -52,6 +66,40 @@
<div class="h-divider pb-3 d-block d-lg-none"></div> <div class="h-divider pb-3 d-block d-lg-none"></div>
<div class="col-sm-12 col-lg-6"> <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 class="row" id="onionr-timeline-posts">
</div> </div>
@ -60,12 +108,100 @@
<div class="d-none d-lg-block col-lg-3"> <div class="d-none d-lg-block col-lg-3">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="onionr-trending"> <div class="onionr-replies">
<h2>Trending</h2> <h2 id="onionr-replies-title"></h2>
</div>
</div>
<div id="onionr-reply-creator-panel">
</div>
</div>
<div class="row">
</div> </div>
</div> </div>
</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>
</div> </div>

File diff suppressed because one or more lines are too long

View File

@ -4,24 +4,488 @@ Block.getBlocks({'type' : 'onionr-post', 'signed' : true, 'reverse' : true}, fun
try { try {
var block = data[i]; var block = data[i];
var finished = false;
User.getUser(new String(block.getHeader('signer', 'unknown')), function(user) {
var post = new Post(); var post = new Post();
var user = new User();
var blockContent = JSON.parse(block.getContent()); var blockContent = JSON.parse(block.getContent());
user.setName('unknown'); // just ignore anything shorter than 280 characters
user.setID(new String(block.getHeader('signer', 'unknown'))); if(String(blockContent['content']).length <= 280 && block.getParent() === null) {
post.setContent(blockContent['content']); post.setContent(blockContent['content']);
post.setPostDate(block.getDate()); post.setPostDate(block.getDate());
post.setUser(user); post.setUser(user);
post.setHash(block.getHash());
document.getElementById('onionr-timeline-posts').innerHTML += post.getHTML(); document.getElementById('onionr-timeline-posts').innerHTML += post.getHTML();
}
finished = true;
});
while(!finished);
} catch(e) { } catch(e) {
console.log('Troublemaker block: ' + data[i].getHash());
console.log(e); console.log(e);
} }
} }
}); });
function viewProfile(id, name) { function toggleLike(hash) {
document.getElementById("onionr-profile-username").innerHTML = Sanitize.html(decodeURIComponent(name)); 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

@ -6,10 +6,35 @@
"NOTIFICATIONS" : "Notifications", "NOTIFICATIONS" : "Notifications",
"MESSAGES" : "Messages", "MESSAGES" : "Messages",
"LATEST" : "Latest...",
"TRENDING" : "Trending", "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_LIKE" : "like",
"POST_REPLY" : "reply" "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" : { "spa" : {

View File

@ -37,10 +37,20 @@ body {
/* timeline */ /* timeline */
.onionr-post-focus-separator {
width: 100%;
padding: 1rem;
padding-left: 0;
padding-right: 0;
}
.onionr-post { .onionr-post {
padding: 1rem; padding: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
cursor: pointer;
width: 100%; width: 100%;
} }
@ -60,6 +70,35 @@ body {
width: 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 { .h-divider {
margin: 5px 15px; margin: 5px 15px;
height: 1px; height: 1px;
@ -77,3 +116,7 @@ body {
.onionr-profile-username { .onionr-profile-username {
text-align: center; text-align: center;
} }
.onionr-profile-save {
width: 100%;
}

View File

@ -5,6 +5,17 @@ body {
/* timeline */ /* timeline */
.onionr-post-focus-separator {
border-color: black;
}
.modal-content {
border: 1px solid black;
border-radius: 1rem;
background-color: lightgray;
}
.onionr-post { .onionr-post {
border: 1px solid black; border: 1px solid black;
border-radius: 1rem; border-radius: 1rem;
@ -31,6 +42,35 @@ body {
font-size: 15pt; 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 { .h-divider {
border-top:1px solid gray; border-top:1px solid gray;
} }

View File

@ -10,10 +10,24 @@
<div class="onionr-profile"> <div class="onionr-profile">
<div class="row"> <div class="row">
<div class="col-4 col-lg-12"> <div class="col-4 col-lg-12">
<img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="img/default.png"> <img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="">
</div> </div>
<div class="col-8 col-lg-12"> <div class="col-8 col-lg-12">
<h2 id="onionr-profile-username" class="onionr-profile-username text-left text-lg-center text-sm-left">arinerron</h2> <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> </div>
@ -22,6 +36,40 @@
<div class="h-divider pb-3 d-block d-lg-none"></div> <div class="h-divider pb-3 d-block d-lg-none"></div>
<div class="col-sm-12 col-lg-6"> <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 class="row" id="onionr-timeline-posts">
</div> </div>
@ -30,15 +78,58 @@
<div class="d-none d-lg-block col-lg-3"> <div class="d-none d-lg-block col-lg-3">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="onionr-trending"> <div class="onionr-replies">
<h2><$= LANG.TRENDING $></h2> <h2 id="onionr-replies-title"></h2>
</div> </div>
</div> </div>
<div id="onionr-reply-creator-panel">
</div>
</div>
<div class="row">
</div> </div>
</div> </div>
</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 /> <footer />
<script src="js/timeline.js"></script> <script src="js/timeline.js"></script>
</body> </body>

View File

@ -17,18 +17,56 @@ function remove(key) {
return localStorage.removeItem(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 usermap = JSON.parse(get('usermap', '{}'));
var postmap = JSON.parse(get('postmap', '{}'))
function getUserMap() { function getUserMap() {
return usermap; return usermap;
} }
function getPostMap(hash) {
if(hash !== undefined) {
if(hash in postmap)
return postmap[hash];
return null;
}
return postmap;
}
function deserializeUser(id) { function deserializeUser(id) {
if(!(id in getUserMap()))
return null;
var serialized = getUserMap()[id] var serialized = getUserMap()[id]
var user = new User(); var user = new User();
user.setName(serialized['name']); user.setName(serialized['name']);
user.setID(serialized['id']); user.setID(serialized['id']);
user.setIcon(serialized['icon']); 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" */ /* returns a relative date format, e.g. "5 minutes" */
@ -89,10 +127,10 @@ function timeSince(date, size) {
} }
/* replace all instances of string */ /* replace all instances of string */
String.prototype.replaceAll = function(search, replacement) { String.prototype.replaceAll = function(search, replacement, limit) {
// taken from https://stackoverflow.com/a/17606289/3678023 // taken from https://stackoverflow.com/a/17606289/3678023
var target = this; var target = this;
return target.split(search).join(replacement); return target.split(search, limit).join(replacement);
}; };
/* useful functions to sanitize data */ /* useful functions to sanitize data */
@ -106,6 +144,16 @@ class Sanitize {
static url(url) { static url(url) {
return encodeURIComponent(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 */ /* config stuff */
@ -157,42 +205,118 @@ class User {
return this.image; return this.image;
} }
setDescription(description) {
this.description = description;
}
getDescription() {
return this.description;
}
serialize() { serialize() {
return { return {
'name' : this.getName(), 'name' : this.getName(),
'id' : this.getID(), 'id' : this.getID(),
'icon' : this.getIcon() 'icon' : this.getIcon(),
'description' : this.getDescription()
}; };
} }
/* save in usermap */
remember() { remember() {
usermap[this.getID()] = this.serialize(); usermap[this.getID()] = this.serialize();
set('usermap', JSON.stringify(usermap)); 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 */ /* post class */
class Post { class Post {
/* returns the html content of a post */ /* returns the html content of a post */
getHTML() { getHTML(type) {
var replyTemplate = '<$= jsTemplate('onionr-timeline-reply') $>';
var postTemplate = '<$= jsTemplate('onionr-timeline-post') $>'; 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'); var device = (jQuery(document).width() < 768 ? 'mobile' : 'desktop');
postTemplate = postTemplate.replaceAll('$user-name-url', Sanitize.html(Sanitize.url(this.getUser().getName()))); template = template.replaceAll('$user-name-url', Sanitize.html(Sanitize.url(this.getUser().getName())));
postTemplate = postTemplate.replaceAll('$user-name', Sanitize.html(this.getUser().getName())); template = template.replaceAll('$user-name', Sanitize.html(this.getUser().getName()));
postTemplate = postTemplate.replaceAll('$user-id-url', Sanitize.html(Sanitize.url(this.getUser().getID()))); template = template.replaceAll('$user-id-url', Sanitize.html(Sanitize.url(this.getUser().getID())));
postTemplate = postTemplate.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().substring(0, 12) + '...')); template = template.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().substring(0, 12) + '...'));
// postTemplate = postTemplate.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().split('-').slice(0, 4).join('-'))); // template = template.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().split('-').slice(0, 4).join('-')));
postTemplate = postTemplate.replaceAll('$user-id', Sanitize.html(this.getUser().getID())); template = template.replaceAll('$user-id', Sanitize.html(this.getUser().getID()));
postTemplate = postTemplate.replaceAll('$user-image', Sanitize.html(this.getUser().getIcon())); template = template.replaceAll('$user-image', "data:image/jpeg;base64," + Sanitize.html(this.getUser().getIcon()));
postTemplate = postTemplate.replaceAll('$content', Sanitize.html(this.getContent())); template = template.replaceAll('$content', Sanitize.html(this.getContent()).replaceAll('\n', '<br />', 16)); // Maximum of 16 lines
postTemplate = postTemplate.replaceAll('$date-relative', timeSince(this.getPostDate(), device) + (device === 'desktop' ? ' ago' : '')); template = template.replaceAll('$post-hash', this.getHash());
postTemplate = postTemplate.replaceAll('$date', this.getPostDate().toLocaleString()); 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());
return postTemplate; 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) { setUser(user) {
@ -211,6 +335,14 @@ class Post {
return this.content; return this.content;
} }
setParent(parent) {
this.parent = parent;
}
getParent() {
return this.parent;
}
setPostDate(date) { // unix timestamp input setPostDate(date) { // unix timestamp input
if(date instanceof Date) if(date instanceof Date)
this.date = date; this.date = date;
@ -221,6 +353,51 @@ class Post {
getPostDate() { getPostDate() {
return this.date; 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 */ /* block class */
@ -269,8 +446,12 @@ class Block {
// returns the parent block's hash (not Block object, for performance) // returns the parent block's hash (not Block object, for performance)
getParent() { getParent() {
if(!(this.parent instanceof Block) && this.parent !== undefined && this.parent !== null) // console.log(this.parent);
this.parent = Block.openBlock(this.parent); // convert hash to Block object
// 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; return this.parent;
} }
@ -323,11 +504,57 @@ class Block {
return !(this.hash === null || this.hash === undefined); 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 */ /* static functions */
// recreates a block by hash // recreates a block by hash
static openBlock(hash) { static openBlock(hash) {
return parseBlock(response); return Block.parseBlock(hash);
} }
// converts an associative array to a Block // converts an associative array to a Block
@ -406,14 +633,57 @@ class Block {
/* temporary code */ /* temporary code */
var tt = getParameter("timingToken");
if(tt !== null && tt !== undefined) {
setTimingToken(tt);
}
if(getWebPassword() === null) { if(getWebPassword() === null) {
var password = ""; var password = "";
while(password.length != 64) { while(password.length != 64) {
password = prompt("Please enter the web password (run `./RUN-LINUX.sh --get-password`)"); password = prompt("Please enter the web password (run `./RUN-LINUX.sh --details`)");
} }
setTimingToken(prompt("Please enter the timing token (optional)"));
setWebPassword(password); setWebPassword(password);
window.location.reload(true);
} }
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,28 +1,460 @@
/* just for testing rn */ /* just for testing rn */
Block.getBlocks({'type' : 'onionr-post', 'signed' : true, 'reverse' : true}, function(data) { Block.getBlocks({'type' : 'onionr-post', 'signed' : true, 'reverse' : true}, function(data) {
for(var i = 0; i < data.length; i++) { for(var i = 0; i < data.length; i++) {
try { try {
var block = data[i]; var block = data[i];
var finished = false;
User.getUser(new String(block.getHeader('signer', 'unknown')), function(user) {
var post = new Post(); var post = new Post();
var user = new User();
var blockContent = JSON.parse(block.getContent()); var blockContent = JSON.parse(block.getContent());
user.setName('unknown'); // just ignore anything shorter than 280 characters
user.setID(new String(block.getHeader('signer', 'unknown'))); if(String(blockContent['content']).length <= 280 && block.getParent() === null) {
post.setContent(blockContent['content']); post.setContent(blockContent['content']);
post.setPostDate(block.getDate()); post.setPostDate(block.getDate());
post.setUser(user); post.setUser(user);
post.setHash(block.getHash());
document.getElementById('onionr-timeline-posts').innerHTML += post.getHTML(); document.getElementById('onionr-timeline-posts').innerHTML += post.getHTML();
}
finished = true;
});
while(!finished);
} catch(e) { } catch(e) {
console.log('Troublemaker block: ' + data[i].getHash());
console.log(e); console.log(e);
} }
} }
}); });
function viewProfile(id, name) { function toggleLike(hash) {
document.getElementById("onionr-profile-username").innerHTML = Sanitize.html(decodeURIComponent(name)); 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();