Merge branch 'onionrui' into 'master'

onionrui -> master

See merge request beardog/Onionr!10
This commit is contained in:
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:
@echo "Hard-resetting Onionr..."
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
plugins-reset:

View File

@ -1,12 +1,12 @@
![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/)
Anonymous P2P platform, using Tor & I2P.
***Experimental, not safe or easy to use yet***
<hr>
**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 core
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:
'''
@ -62,14 +62,9 @@ class API:
}
for mimetype in mimetypes:
logger.debug(path + ' endswith .' + mimetype + '?')
if path.endswith('.%s' % mimetype):
logger.debug('- True!')
return mimetypes[mimetype]
else:
logger.debug('- no')
logger.debug('%s not in %s' % (path, mimetypes))
return 'text/plain'
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
'''
config.reload()
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)
# configure logger and stuff
onionr.Onionr.setupConfig('data/', self = self)
self.debug = debug
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['X-Frame-Options'] = 'deny'
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
self.mimeType = 'text/plain'
@ -160,14 +149,17 @@ class API:
self.validateHost('private')
if config.get('www.public.guess_mime', True):
self.mimeType = API.guessMime(path)
endTime = math.floor(time.time())
elapsed = endTime - startTime
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)
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>')
def www_public(path):
@ -176,7 +168,10 @@ class API:
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>')
def ui_private(path):
@ -206,11 +201,11 @@ class API:
time.sleep(self._privateDelayTime - elapsed)
'''
logger.debug('Serving %s' % path)
self.mimeType = API.guessMime(path)
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))
@app.route('/client/')
@ -253,6 +248,16 @@ class API:
resp = Response('Goodbye')
elif action == 'ping':
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":
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 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 defusedxml import minidom
class OnionrCommunicatorDaemon:
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
@ -87,9 +89,8 @@ class OnionrCommunicatorDaemon:
# Set timers, function reference, seconds
# 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)
OnionrCommunicatorTimers(self, self.runCheck, 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.clearOfflinePeer, 58)
@ -134,7 +135,7 @@ class OnionrCommunicatorDaemon:
def lookupAdders(self):
'''Lookup new peer addresses'''
logger.info('LOOKING UP NEW ADDRESSES')
logger.info('Looking up new addresses...')
tryAmount = 1
for i in range(tryAmount):
# Download new peer address list from random online peers
@ -145,7 +146,7 @@ class OnionrCommunicatorDaemon:
def lookupBlocks(self):
'''Lookup new blocks & add them to download queue'''
logger.info('LOOKING UP NEW BLOCKS')
logger.info('Looking up new blocks...')
tryAmount = 2
newBlocks = ''
existingBlocks = self._core.getBlockList()
@ -176,7 +177,7 @@ class OnionrCommunicatorDaemon:
try:
newBlocks = self.peerAction(peer, 'getBlockHashes') # get list of new block hashes
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
if newBlocks != False:
# if request was a success
@ -200,10 +201,10 @@ class OnionrCommunicatorDaemon:
break
# Do not download blocks being downloaded or that are already saved (edge cases)
if blockHash in self.currentDownloading:
logger.debug('ALREADY DOWNLOADING ' + blockHash)
logger.debug('Already downloading block %s...' % blockHash)
continue
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)
continue
if self._core._blacklist.inBlacklist(blockHash):
@ -232,22 +233,22 @@ class OnionrCommunicatorDaemon:
#meta = metas[1]
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
logger.info('Block passed proof, attempting save.')
logger.info('Attempting to save block %s...' % blockHash)
try:
self._core.setData(content)
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
else:
self._core.addToBlockDB(blockHash, dataSaved=True)
self._core._utils.processBlockMetadata(blockHash) # caches block metadata values to block database
else:
logger.warn('POW failed for block ' + blockHash)
logger.warn('POW failed for block %s.' % blockHash)
else:
if self._core._blacklist.inBlacklist(realHash):
logger.warn('%s is blacklisted' % (realHash,))
logger.warn('Block %s is blacklisted.' % (realHash,))
else:
logger.warn('Metadata for ' + blockHash + ' is invalid.')
logger.warn('Metadata for block %s is invalid.' % blockHash)
self._core._blacklist.addToDB(blockHash)
else:
# if block didn't meet expected hash
@ -303,10 +304,12 @@ class OnionrCommunicatorDaemon:
self.decrementThreadCount('clearOfflinePeer')
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.')
maxPeers = int(config.get('peers.maxConnect'))
logger.debug('Refreshing peer pool...')
maxPeers = int(config.get('peers.max_connect', 10))
needed = maxPeers - len(self.onlinePeers)
for i in range(needed):
@ -318,13 +321,15 @@ class OnionrCommunicatorDaemon:
break
else:
if len(self.onlinePeers) == 0:
logger.warn('Could not connect to any peer.')
logger.debug('Couldn\'t connect to any peers.')
self.decrementThreadCount('getOnlinePeers')
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:
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)
self._core.addAddress(i)
@ -347,7 +352,7 @@ class OnionrCommunicatorDaemon:
self.addBootstrapListToPeerList(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
if len(address) == 0 or address in tried or address in self.onlinePeers or address in self.cooldownPeer:
continue
@ -440,11 +445,13 @@ class OnionrCommunicatorDaemon:
def heartbeat(self):
'''Show a heartbeat debug message'''
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')
def daemonCommands(self):
'''process daemon commands from daemonQueue'''
'''
Process daemon commands from daemonQueue
'''
cmd = self._core.daemonQueue()
if cmd is not False:
@ -457,7 +464,7 @@ class OnionrCommunicatorDaemon:
self.announce(cmd[1])
else:
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.')
open(self._core.dataDir + '.runcheck', 'w+').close()
elif cmd[0] == 'connectedPeers':
@ -538,6 +545,12 @@ class OnionrCommunicatorDaemon:
self.shutdown = True
self.decrementThreadCount('detectAPICrash')
def runCheck(self):
if self.daemonTools.runCheck():
logger.debug('Status check; looks good.')
self.decrementThreadCount('runCheck')
class OnionrCommunicatorTimers:
def __init__(self, daemonInstance, timerFunction, frequency, makeThread=True, threadAmount=1, maxThreads=5, requiresPeer=False):
self.timerFunction = timerFunction
@ -571,7 +584,7 @@ class OnionrCommunicatorTimers:
if self.makeThread:
for i in range(self.threadAmount):
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:
self.daemonInstance.threadCounts[self.timerFunction.__name__] += 1
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 onionrblacklist, onionrchat, onionrusers
import dbcreator
if sys.version_info < (3, 6):
try:
import sha3
@ -104,7 +105,10 @@ class Core:
return
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'):
with open(self.dataDir + '/hs/hostname', 'r') as hs:
self.hsAddress = hs.read().strip()
@ -113,6 +117,7 @@ class Core:
'''
Adds a public key to the key database (misleading function name)
'''
# This function simply adds a peer to the DB
if not self._utils.validatePubKey(peerID):
return False
@ -127,7 +132,7 @@ class Core:
c = conn.cursor()
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:
if i[0] == peerID:
conn.close()
@ -146,8 +151,8 @@ class Core:
'''
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
if self._utils.validateID(address):
conn = sqlite3.connect(self.addressDB, timeout=10)
@ -155,7 +160,7 @@ class Core:
# 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
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:
if i[0] == address:
conn.close()
@ -174,13 +179,14 @@ class Core:
return True
else:
logger.debug('Invalid ID')
logger.debug('Invalid ID: %s' % address)
return False
def removeAddress(self, address):
'''
Remove an address from the address database
'''
if self._utils.validateID(address):
conn = sqlite3.connect(self.addressDB, timeout=10)
c = conn.cursor()
@ -200,6 +206,7 @@ class Core:
**You may want blacklist.addToDB(blockHash)
'''
if self._utils.validateHash(block):
conn = sqlite3.connect(self.blockDB, timeout=10)
c = conn.cursor()
@ -207,7 +214,7 @@ class Core:
c.execute('Delete from hashes where hash=?;', t)
conn.commit()
conn.close()
blockFile = self.dataDir + '/blocks/' + block + '.dat'
blockFile = self.dataDir + '/blocks/%s.dat' % block
dataSize = 0
try:
''' Get size of data when loaded as an object/var, rather than on disk,
@ -224,18 +231,21 @@ class Core:
'''
Generate the address database
'''
self.dbCreate.createAddressDB()
def createPeerDB(self):
'''
Generate the peer sqlite3 database and populate it with the peers table.
'''
self.dbCreate.createPeerDB()
def createBlockDB(self):
'''
Create a database for blocks
'''
self.dbCreate.createBlockDB()
def addToBlockDB(self, newHash, selfInsert=False, dataSaved=False):
@ -244,6 +254,7 @@ class Core:
Should be in hex format!
'''
if not os.path.exists(self.blockDB):
raise Exception('Block db does not exist')
if self._utils.hasBlock(newHash):
@ -266,6 +277,7 @@ class Core:
'''
Simply return the data associated to a hash
'''
try:
# logger.debug('Opening %s' % (str(self.blockDataLocation) + str(hash) + '.dat'))
dataFile = open(self.blockDataLocation + hash + '.dat', 'rb')
@ -280,6 +292,7 @@ class Core:
'''
Set the data assciated with a hash
'''
data = data
dataSize = sys.getsizeof(data)
@ -301,7 +314,7 @@ class Core:
blockFile.close()
conn = sqlite3.connect(self.blockDB, timeout=10)
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.close()
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.
'''
retData = False
if not os.path.exists(self.queueDB):
self.dbCreate.createDaemonDB()
@ -343,12 +357,15 @@ class Core:
'''
Add a command to the daemon queue, used by the communication daemon (communicator.py)
'''
retData = True
# Intended to be used by the web server
date = self._utils.getEpoch()
conn = sqlite3.connect(self.queueDB, timeout=10)
c = conn.cursor()
t = (command, data, date)
try:
c.execute('INSERT INTO commands (command, data, date) VALUES(?, ?, ?)', t)
conn.commit()
@ -366,11 +383,13 @@ class Core:
'''
conn = sqlite3.connect(self.queueDB, timeout=10)
c = conn.cursor()
try:
c.execute('DELETE FROM commands;')
conn.commit()
except:
pass
conn.close()
events.event('queue_clear', onionr = None)
@ -401,16 +420,21 @@ class Core:
'''
conn = sqlite3.connect(self.peerDB, timeout=10)
c = conn.cursor()
payload = ""
payload = ''
if trust not in (0, 1, 2):
logger.error('Tried to select invalid trust.')
return
if randomOrder:
payload = 'SELECT * FROM peers where trust >= %s ORDER BY RANDOM();' % (trust,)
payload = 'SELECT * FROM peers WHERE trust >= ? ORDER BY RANDOM();'
else:
payload = 'SELECT * FROM peers where trust >= %s;' % (trust,)
payload = 'SELECT * FROM peers WHERE trust >= ?;'
peerList = []
for i in c.execute(payload):
for i in c.execute(payload, (trust,)):
try:
if len(i[0]) != 0:
if getPow:
@ -419,6 +443,7 @@ class Core:
peerList.append(i[0])
except TypeError:
pass
if getPow:
try:
peerList.append(self._crypto.pubKey + '-' + self._crypto.pubKeyPowToken)
@ -426,7 +451,9 @@ class Core:
pass
else:
peerList.append(self._crypto.pubKey)
conn.close()
return peerList
def getPeerInfo(self, peer, info):
@ -445,18 +472,22 @@ class Core:
'''
conn = sqlite3.connect(self.peerDB, timeout=10)
c = conn.cursor()
command = (peer,)
infoNumbers = {'id': 0, 'name': 1, 'adders': 2, 'dateSeen': 3, 'bytesStored': 4, 'trust': 5, 'pubkeyExchanged': 6, 'hashID': 7}
info = infoNumbers[info]
iterCount = 0
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:
if iterCount == info:
retVal = i
break
else:
iterCount += 1
conn.close()
return retVal
@ -465,15 +496,20 @@ class Core:
'''
Update a peer for a key
'''
conn = sqlite3.connect(self.peerDB, timeout=10)
c = conn.cursor()
command = (data, peer)
# TODO: validate key on whitelist
if key not in ('id', 'name', 'pubkey', 'blockDBHash', 'forwardKey', 'dateSeen', 'bytesStored', 'trust'):
raise Exception("Got invalid database key when setting peer info")
c.execute('UPDATE peers SET ' + key + ' = ? WHERE id=?', command)
conn.commit()
conn.close()
return
def getAddressInfo(self, address, info):
@ -489,14 +525,17 @@ class Core:
failure int 6
lastConnect 7
'''
conn = sqlite3.connect(self.addressDB, timeout=10)
c = conn.cursor()
command = (address,)
infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'DBHash': 5, 'failure': 6, 'lastConnect': 7}
info = infoNumbers[info]
iterCount = 0
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:
if iterCount == info:
retVal = i
@ -504,15 +543,19 @@ class Core:
else:
iterCount += 1
conn.close()
return retVal
def setAddressInfo(self, address, key, data):
'''
Update an address for a key
'''
conn = sqlite3.connect(self.addressDB, timeout=10)
c = conn.cursor()
command = (data, address)
# TODO: validate key on whitelist
if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', 'failure', 'lastConnect', 'lastConnectAttempt'):
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)
conn.commit()
conn.close()
return
def getBlockList(self, unsaved = False): # TODO: Use unsaved??
'''
Get list of our blocks
'''
conn = sqlite3.connect(self.blockDB, timeout=10)
c = conn.cursor()
if unsaved:
execute = 'SELECT hash FROM hashes WHERE dataSaved != 1 ORDER BY RANDOM();'
else:
execute = 'SELECT hash FROM hashes ORDER BY dateReceived ASC;'
rows = list()
for row in c.execute(execute):
for i in row:
@ -543,8 +590,10 @@ class Core:
'''
Returns the date a block was received
'''
conn = sqlite3.connect(self.blockDB, timeout=10)
c = conn.cursor()
execute = 'SELECT dateReceived FROM hashes WHERE hash=?;'
args = (blockHash,)
for row in c.execute(execute, args):
@ -557,17 +606,22 @@ class Core:
'''
Returns a list of blocks by the type
'''
conn = sqlite3.connect(self.blockDB, timeout=10)
c = conn.cursor()
if orderDate:
execute = 'SELECT hash FROM hashes WHERE dataType=? ORDER BY dateReceived;'
else:
execute = 'SELECT hash FROM hashes WHERE dataType=?;'
args = (blockType,)
rows = list()
for row in c.execute(execute, args):
for i in row:
rows.append(i)
return rows
def getExpiredBlocks(self):
@ -591,9 +645,10 @@ class Core:
conn = sqlite3.connect(self.blockDB, timeout=10)
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.close()
return
def updateBlockInfo(self, hash, key, data):
@ -621,6 +676,7 @@ class Core:
c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args)
conn.commit()
conn.close()
return True
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
encryptType must be specified to encrypt a block
'''
retData = False
# check nonce
@ -642,9 +699,6 @@ class Core:
with open(self.dataNonceFile, 'a') as nonceFile:
nonceFile.write(dataNonce + '\n')
if meta is None:
meta = dict()
if type(data) is bytes:
data = data.decode()
data = str(data)

View File

@ -73,6 +73,7 @@ LEVEL_INFO = 2
LEVEL_WARN = 3
LEVEL_ERROR = 4
LEVEL_FATAL = 5
LEVEL_IMPORTANT = 6
_type = OUTPUT_TO_CONSOLE | USE_ANSI # the default settings for logging
_level = LEVEL_DEBUG # the lowest level to log
@ -123,18 +124,18 @@ def get_file():
return _outputfile
def raw(data, fd = sys.stdout):
def raw(data, fd = sys.stdout, sensitive = False):
'''
Outputs raw data to console without formatting
'''
if get_settings() & OUTPUT_TO_CONSOLE:
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:
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
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:
output = colors.filter(output)
raw(output, fd = fd)
raw(output, fd = fd, sensitive = sensitive)
def readline(message = ''):
'''
@ -201,37 +202,37 @@ def confirm(default = 'y', message = 'Are you sure %s? '):
return default == 'y'
# debug: when there is info that could be useful for debugging purposes only
def debug(data, error = None, timestamp = True, prompt = True):
if get_level() <= LEVEL_DEBUG:
log('/', data, timestamp=timestamp, prompt = prompt)
def debug(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_DEBUG):
if get_level() <= level:
log('/', data, timestamp = timestamp, prompt = prompt, sensitive = sensitive)
if not error is None:
debug('Error: ' + str(error) + parse_error())
# info: when there is something to notify the user of, such as the success of a process
def info(data, timestamp = False, prompt = True):
if get_level() <= LEVEL_INFO:
log('+', data, colors.fg.green, timestamp = timestamp, prompt = prompt)
def info(data, timestamp = False, prompt = True, sensitive = False, level = LEVEL_INFO):
if get_level() <= level:
log('+', data, colors.fg.green, timestamp = timestamp, prompt = prompt, sensitive = sensitive)
# 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:
debug('Error: ' + str(error) + parse_error())
if get_level() <= LEVEL_WARN:
log('!', data, colors.fg.orange, timestamp = timestamp, prompt = prompt)
if get_level() <= level:
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
def error(data, error = None, timestamp = True, prompt = True):
if get_level() <= LEVEL_ERROR:
log('-', data, colors.fg.red, timestamp = timestamp, fd = sys.stderr, prompt = prompt)
def error(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_ERROR):
if get_level() <= level:
log('-', data, colors.fg.red, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive)
if not error is None:
debug('Error: ' + str(error) + parse_error())
# 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:
debug('Error: ' + str(error) + parse_error())
if get_level() <= LEVEL_FATAL:
log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp=timestamp, fd = sys.stderr, prompt = prompt)
debug('Error: ' + str(error) + parse_error(), sensitive = sensitive)
if get_level() <= level:
log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive)
# returns a formatted error message
def parse_error():

View File

@ -65,7 +65,7 @@ class NetController:
Generate a torrc file for our tor instance
'''
hsVer = '# v2 onions'
if config.get('tor.v3onions'):
if config.get('tor.v3_onions'):
hsVer = 'HiddenServiceVersion 3'
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?')
return False
except KeyboardInterrupt:
logger.fatal("Got keyboard interrupt.")
logger.fatal('Got keyboard interrupt.', timestamp = false, level = logger.LEVEL_IMPORTANT)
return False
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)")
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)
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/'
# Load global configuration data
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)
data_exists = Onionr.setupConfig(self.dataDir, self = self)
self.onionrCore = core.Core()
self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore)
@ -156,6 +126,16 @@ class Onionr:
'status': self.showStats,
'statistics': 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,
'enplugin': self.enablePlugin,
@ -204,6 +184,7 @@ class Onionr:
'ui' : self.openUI,
'gui' : self.openUI,
'chat': self.startChat,
'getpassword': self.printWebPassword,
'get-password': self.printWebPassword,
@ -223,14 +204,18 @@ class Onionr:
'help': 'Displays this Onionr help menu',
'version': 'Displays the Onionr version',
'config': 'Configures something and adds it to the file',
'start': 'Starts the Onionr daemon',
'stop': 'Stops the Onionr daemon',
'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',
'disable-plugin': 'Disables and stops a plugin',
'reload-plugin': 'Reloads a plugin',
'create-plugin': 'Creates directory structure for a plugin',
'add-peer': 'Adds a peer to database',
'list-peers': 'Displays a list of peers',
'add-file': 'Create an Onionr block from a file',
@ -261,6 +246,17 @@ class Onionr:
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):
try:
data = json.dumps({'peer': sys.argv[2], 'reason': 'chat'})
@ -314,6 +310,48 @@ class Onionr:
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):
try:
ban = sys.argv[2]
@ -346,7 +384,7 @@ class Onionr:
return config.get('client.hmac')
def printWebPassword(self):
print(self.getWebPassword())
logger.info(self.getWebPassword(), sensitive = True)
def getHelp(self):
return self.cmdhelp
@ -399,16 +437,16 @@ class Onionr:
THIS SECTION DEFINES THE COMMANDS
'''
def version(self, verbosity=5):
def version(self, verbosity = 5, function = logger.info):
'''
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:
logger.info(ONIONR_TAGLINE)
function(ONIONR_TAGLINE)
if verbosity >= 2:
logger.info('Running on %s %s' % (platform.platform(), platform.release()))
function('Running on %s %s' % (platform.platform(), platform.release()))
return
@ -427,9 +465,7 @@ class Onionr:
Displays a list of keys (used to be called peers) (?)
'''
logger.info('Public keys in database:\n')
for i in self.onionrCore.listPeers():
logger.info(i)
logger.info('%sPublic keys in database: \n%s%s' % (logger.colors.fg.lightgreen, logger.colors.fg.green, '\n'.join(self.onionrCore.listPeers())))
def addPeer(self):
'''
@ -618,37 +654,56 @@ class Onionr:
'''
Starts the Onionr communication daemon
'''
communicatorDaemon = './communicator2.py'
apiThread = Thread(target=api.API, args=(self.debug,API_VERSION))
# 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.start()
try:
time.sleep(3)
except KeyboardInterrupt:
logger.info('Got keyboard interrupt')
logger.debug('Got keyboard interrupt, shutting down...')
time.sleep(1)
self.onionrUtils.localCommand('shutdown')
else:
if apiThread.isAlive():
# configure logger and stuff
Onionr.setupConfig('data/', self = self)
if self._developmentMode:
logger.warn('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)', timestamp = False)
net = NetController(config.get('client.port', 59496))
logger.info('Tor is starting...')
logger.debug('Tor is starting...')
if not net.startTor():
sys.exit(1)
logger.info('Started .onion service: ' + logger.colors.underline + net.myID)
logger.info('Our Public key: ' + self.onionrCore._crypto.pubKey)
logger.debug('Started .onion service: %s' % (logger.colors.underline + net.myID))
logger.debug('Using public key: %s' % (logger.colors.underline + self.onionrCore._crypto.pubKey))
time.sleep(1)
#TODO make runable on windows
communicatorProc = subprocess.Popen([communicatorDaemon, "run", str(net.socksPort)])
# Print nice header thing :)
# TODO: make runable on windows
communicatorProc = subprocess.Popen([communicatorDaemon, 'run', str(net.socksPort)])
# print nice header thing :)
if config.get('general.display_header', True):
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)
try:
while True:
time.sleep(5)
# Break if communicator process ends, so we don't have left over processes
if communicatorProc.poll() is not None:
break
@ -689,9 +744,6 @@ class Onionr:
messages = {
# info about local client
'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
'div1' : True, # this creates a solid line across the screen, a div
@ -798,7 +850,8 @@ class Onionr:
except IndexError:
logger.error("Syntax %s %s" % (sys.argv[0], '/path/to/filename <blockhash>'))
else:
print(fileName)
logger.info(fileName)
contents = None
if os.path.exists(fileName):
logger.error("File already exists")
@ -806,6 +859,7 @@ class Onionr:
if not self.onionrUtils.validateHash(bHash):
logger.error('Block hash is invalid')
return
Block.mergeChain(bHash, fileName)
return
@ -830,17 +884,83 @@ class Onionr:
else:
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):
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)
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:
# 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')
if __name__ == "__main__":

View File

@ -30,19 +30,22 @@ class OnionrBlackList:
def inBlacklist(self, data):
hashed = self._core._utils.bytesToStr(self._core._crypto.sha3Hash(data))
retData = False
if not hashed.isalnum():
raise Exception("Hashed data is not alpha numeric")
if len(hashed) > 64:
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
break
return retData
def _dbExecute(self, toExec):
def _dbExecute(self, toExec, params = ()):
conn = sqlite3.connect(self.blacklistDB)
c = conn.cursor()
retData = c.execute(toExec)
retData = c.execute(toExec, params)
conn.commit()
return retData
@ -60,13 +63,13 @@ class OnionrBlackList:
except AttributeError:
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 (curTime - i[2]) >= i[3]:
deleteList.append(i[0])
for thing in deleteList:
self._dbExecute("delete from blacklist where hash='%s'" % (thing,))
self._dbExecute("DELETE FROM blacklist WHERE hash = ?", (thing,))
def generateDB(self):
self._dbExecute('''CREATE TABLE blacklist(
@ -79,10 +82,10 @@ class OnionrBlackList:
return
def clearDB(self):
self._dbExecute('''delete from blacklist;);''')
self._dbExecute('''DELETE FROM blacklist;);''')
def getList(self):
data = self._dbExecute('select * from blacklist')
data = self._dbExecute('SELECT * FROM blacklist')
myList = []
for i in data:
myList.append(i[0])
@ -113,4 +116,4 @@ class OnionrBlackList:
return
insert = (hashed,)
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
'''
if self.decrypted:
return True
retData = False
@ -114,6 +115,7 @@ class Block:
'''
Verify if a block's signature is signed by its claimed signer
'''
core = self.getCore()
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.bheader = json.loads(self.getRaw()[:self.getRaw().index('\n')])
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.isEncrypted = True
else:
@ -199,7 +201,13 @@ class Block:
return True
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
return False
@ -214,7 +222,7 @@ class Block:
if self.exists():
os.remove(self.getBlockFile())
removeBlock(self.getHash())
self.getCore().removeBlock(self.getHash())
return True
return False
@ -235,9 +243,9 @@ class Block:
if (not self.getBlockFile() is None) and (recreate is True):
with open(self.getBlockFile(), 'wb') as blockFile:
blockFile.write(self.getRaw().encode())
self.update()
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()
return self.getHash()
@ -782,7 +790,7 @@ class Block:
return False
# 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)]
# cache block content

View File

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

View File

@ -17,8 +17,11 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import onionrexceptions, onionrpeers, onionrproofs, base64, logger, onionrusers, sqlite3
import onionrexceptions, onionrpeers, onionrproofs, logger, onionrusers
import base64, sqlite3, os
from dependencies import secrets
class DaemonTools:
def __init__(self, daemon):
self.daemon = daemon
@ -73,10 +76,12 @@ class DaemonTools:
self.daemon._core._blacklist.addToDB(oldest)
self.daemon._core.removeBlock(oldest)
logger.info('Deleted block: %s' % (oldest,))
# Delete expired blocks
for bHash in self.daemon._core.getExpiredBlocks():
self.daemon._core._blacklist.addToDB(bHash)
self.daemon._core.removeBlock(bHash)
self.daemon.decrementThreadCount('cleanOldBlocks')
def cleanKeys(self):
@ -85,7 +90,8 @@ class DaemonTools:
c = conn.cursor()
time = self.daemon._core._utils.getEpoch()
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])
deleteKeys.append(entry[1])
@ -114,8 +120,9 @@ class DaemonTools:
del self.daemon.cooldownPeer[peer]
# 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
while finding:
try:
toCool = min(tempConnectTimes, key=tempConnectTimes.get)
@ -128,4 +135,32 @@ class DaemonTools:
else:
self.daemon.removeOnlinePeer(toCool)
self.daemon.cooldownPeer[toCool] = self.daemon._core._utils.getEpoch()
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...')
config.reload()
minScore = int(config.get('peers.minimumScore'))
maxPeers = int(config.get('peers.maxStoredPeers'))
minScore = int(config.get('peers.minimum_score', -100))
maxPeers = int(config.get('peers.max_stored', 5000))
adders = getScoreSortedPeerList(coreInst)
adders.reverse()

View File

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

View File

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

View File

@ -101,19 +101,21 @@ class OnionrUser:
conn = sqlite3.connect(self._core.peerDB, timeout=10)
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]
break
conn.commit()
conn.close()
return key
def _getForwardKeys(self):
conn = sqlite3.connect(self._core.peerDB, timeout=10)
c = conn.cursor()
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]
keyList.append(key)
@ -150,8 +152,10 @@ class OnionrUser:
pubkey = self._core._utils.bytesToStr(pubkey)
command = (pubkey,)
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]))
if len(keyList) == 0:
if genNew:
self.generateForwardKey()

View File

@ -125,11 +125,11 @@ class OnionrUtils:
for adder in newAdderList.split(','):
adder = adder.strip()
if not adder in self._core.listAdders(randomOrder = False) and adder != self.getMyAddress() and not self._core._blacklist.inBlacklist(adder):
if not config.get('tor.v3onions') and len(adder) == 62:
if not config.get('tor.v3_onions') and len(adder) == 62:
continue
if self._core.addAddress(adder):
# Check if we have the maxmium amount of allowed stored peers
if config.get('peers.maxStoredPeers') > len(self._core.listAdders()):
if config.get('peers.max_stored') > len(self._core.listAdders()):
logger.info('Added %s to db.' % adder, timestamp = True)
retVal = True
else:
@ -276,7 +276,7 @@ class OnionrUtils:
if myBlock.getMetadata('newFSKey') is not None:
onionrusers.OnionrUser(self._core, signer).addForwardKey(myBlock.getMetadata('newFSKey'))
else:
logger.debug('FS not used for this block')
logger.warn('FS not used for this encrypted block')
logger.info(myBlock.bmetadata)
try:
@ -330,7 +330,7 @@ class OnionrUtils:
c = conn.cursor()
if not self.validateHash(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:
conn.commit()
conn.close()
@ -497,6 +497,12 @@ class OnionrUtils:
except binascii.Error:
retVal = False
# Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32
try:
base64.b32decode(idNoDomain.upper().encode())
except binascii.Error:
retVal = False
return retVal
except:
return False
@ -512,7 +518,7 @@ class OnionrUtils:
c = conn.cursor()
command = (hash,)
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] != '':
retData = row[0]
return retData
@ -521,18 +527,16 @@ class OnionrUtils:
try:
runcheck_file = self._core.dataDir + '.runcheck'
if os.path.isfile(runcheck_file):
os.remove(runcheck_file)
logger.debug('%s file appears to have existed before the run check.' % runcheck_file, timestamp = False)
if not os.path.isfile(runcheck_file):
open(runcheck_file, 'w+').close()
self._core.daemonQueueAdd('runCheck')
# self._core.daemonQueueAdd('runCheck') # deprecated
starttime = time.time()
while True:
time.sleep(interval)
if os.path.isfile(runcheck_file):
os.remove(runcheck_file)
if not os.path.isfile(runcheck_file):
return True
elif time.time() - starttime >= timeout:
return False
@ -543,6 +547,7 @@ class OnionrUtils:
'''
Generates a secure random hex encoded token
'''
return binascii.hexlify(os.urandom(size))
def importNewBlocks(self, scanDir=''):
@ -625,12 +630,14 @@ class OnionrUtils:
else:
return
headers = {'user-agent': 'PyOnionr'}
response_headers = dict()
try:
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))
# Check server is using same API version as us
try:
if r.headers['api'] != str(API_VERSION):
response_headers = r.headers
if r.headers['X-API'] != str(API_VERSION):
raise onionrexceptions.InvalidAPIVersion
except KeyError:
raise onionrexceptions.InvalidAPIVersion
@ -638,9 +645,12 @@ class OnionrUtils:
except KeyboardInterrupt:
raise KeyboardInterrupt
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:
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:
if not 'ConnectTimeoutError' in str(e) and not 'Request rejected or failed' in str(e):
logger.debug('Error: %s' % str(e))
@ -659,12 +669,12 @@ class OnionrUtils:
retData = ''
curTime = self.getRoundedEpoch(rounding)
self.nistSaltTimestamp = curTime
data = self.doGetRequest('https://beacon.nist.gov/rest/record/' + str(curTime), port=torPort)
dataXML = minidom.parseString(data, forbid_dtd=True, forbid_entities=True, forbid_external=True)
data = self.doGetRequest('https://beacon.nist.gov/rest/record/' + str(curTime), port = torPort)
dataXML = minidom.parseString(data, forbid_dtd = True, forbid_entities = True, forbid_external = True)
try:
retData = dataXML.getElementsByTagName('outputValue')[0].childNodes[0].data
except ValueError:
logger.warn('Could not get NIST beacon value')
logger.warn('Failed to get the NIST beacon value.')
else:
self.powSalt = retData
return retData

View File

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

View File

@ -71,7 +71,8 @@ class PlainEncryption:
plaintext = data
encrypted = self.api.get_core()._crypto.pubKeyEncrypt(plaintext, pubkey, anonymous=True, encodedData=True)
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):
plaintext = ""
data = ""
@ -89,10 +90,10 @@ class PlainEncryption:
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)
if decrypted == False:
print("Decryption failed")
logger.error("Decryption failed")
else:
data = json.loads(decrypted)
print(data['data'])
logger.info('Decrypted Message: \n\n%s' % data['data'])
try:
logger.info("Signing public key: %s" % (data['signer'],))
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.")
return
def on_init(api, data = None):
'''
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)
api.commands.register(['encrypt'], encrypt.encrypt)
api.commands.register(['decrypt'], encrypt.decrypt)
return

View File

@ -585,7 +585,6 @@ def commandCreateRepository():
return True
blockhash = createRepository(plugins)
print(blockhash)
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))
else:

View File

@ -102,7 +102,7 @@ class OnionrMail:
displayList.append('%s. %s - %s: %s' % (blockCount, blockDate, senderDisplay[:12], blockHash))
#displayList.reverse()
for i in displayList:
print(i)
logger.info(i)
try:
choice = logger.readline('Enter a block number, -r to refresh, or -q to stop: ').strip().lower()
except (EOFError, KeyboardInterrupt):
@ -129,14 +129,16 @@ class OnionrMail:
else:
cancel = ''
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:
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).')
if cancel != '-q':
print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip())))
input("Press enter to continue")
logger.readline("Press enter to continue")
return
def sentbox(self):
@ -146,7 +148,7 @@ class OnionrMail:
entering = True
while entering:
self.getSentList()
print('Enter block number or -q to return')
logger.info('Enter block number or -q to return')
try:
choice = input('>')
except (EOFError, KeyboardInterrupt) as e:
@ -158,11 +160,11 @@ class OnionrMail:
try:
self.sentboxList[int(choice) - 1]
except IndexError:
print('Invalid block')
logger.warn('Invalid block.')
else:
logger.info('Sent to: ' + self.sentMessages[self.sentboxList[int(choice) - 1]][1])
# 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...')
return
@ -172,7 +174,8 @@ class OnionrMail:
for i in self.sentboxTools.listSent():
self.sentboxList.append(i['hash'])
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
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
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':
try:
newLine = input()
@ -209,7 +212,7 @@ class OnionrMail:
newLine += '\n'
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)
self.sentboxTools.addToSent(blockID, recip, message)
@ -217,7 +220,7 @@ class OnionrMail:
choice = ''
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:
choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip()

View File

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

View File

@ -3,9 +3,9 @@ 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 :::::::::::::::
@ -20,6 +20,7 @@ P :::: ::::: ::::: ::: W :::: :: :: :: ::::: :: :: :: ::
P :::: :::::: :::::: ::::
P :::: :::::::::::: :::: GvPBV
P ::::: :::::::: ::::
P ::::: ::::::
P ::::: :::::
P ::::::::::::::::
P :::::::

File diff suppressed because one or more lines are too long

View File

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

View File

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

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
class Template:
def jsTemplate(template):
def jsTemplate(template, filename = ''):
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:
return Template.parseTags(file.read())
return Template.parseTags(file.read(), filename)
# tag parser
def parseTags(contents):
def parseTags(contents, filename = ''):
# <$ logic $>
for match in re.findall(r'(<\$(?!=)(.*?)\$>)', contents):
try:
@ -66,7 +66,7 @@ class Template:
try:
out = eval(match[1].strip())
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()
print('Warning: %s does not exist, treating as an str' % name)
contents = contents.replace(match[0], name)
@ -118,7 +118,7 @@ def iterate(directory):
# do python tags
if settings['python_tags']:
contents = Template.parseTags(contents)
contents = Template.parseTags(contents, filename)
# write file
file.write(contents)

View File

@ -37,10 +37,20 @@ body {
/* timeline */
.onionr-post-focus-separator {
width: 100%;
padding: 1rem;
padding-left: 0;
padding-right: 0;
}
.onionr-post {
padding: 1rem;
margin-bottom: 1rem;
cursor: pointer;
width: 100%;
}
@ -60,6 +70,35 @@ body {
width: 100%;
}
.onionr-post-creator {
padding: 1rem;
margin-bottom: 1rem;
width: 100%;
}
.onionr-post-creator-user-name {
display: inline;
}
.onionr-post-creator-user-id:before { content: "("; }
.onionr-post-creator-user-id:after { content: ")"; }
.onionr-post-creator-content {
word-wrap: break-word;
width: 100%;
}
.onionr-post-creator-user-icon {
border-radius: 100%;
width: 100%;
}
.onionr-post-creator-create {
width: 100%;
text-align: center;
}
.h-divider {
margin: 5px 15px;
height: 1px;
@ -77,3 +116,7 @@ body {
.onionr-profile-username {
text-align: center;
}
.onionr-profile-save {
width: 100%;
}

View File

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

View File

@ -40,10 +40,24 @@
<div class="onionr-profile">
<div class="row">
<div class="col-4 col-lg-12">
<img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="img/default.png">
<img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="">
</div>
<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>
@ -52,6 +66,40 @@
<div class="h-divider pb-3 d-block d-lg-none"></div>
<div class="col-sm-12 col-lg-6">
<div class="row" id="onionr-timeline-post-creator">
<div class="col-12">
<div class="onionr-timeline">
<h2>Timeline</h2>
</div>
</div>
<!-- POST CREATOR -->
<div class="col-12">
<div class="onionr-post-creator">
<div class="row">
<div class="col-2">
<img class="onionr-post-creator-user-icon" id="onionr-post-creator-user-icon">
</div>
<div class="col-10">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-creator-user-name" id="onionr-post-creator-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')"></a>
<a class="onionr-post-creator-user-id" id="onionr-post-creator-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id">you</a>
</div>
</div>
<textarea class="onionr-post-creator-content" id="onionr-post-creator-content" oninput="postCreatorChange()"></textarea>
<div class="onionr-post-creator-content-message" id="onionr-post-creator-content-message"></div>
<input type="button" onclick="makePost()" title="Create post" value="Create post" id="onionr-post-creator-create" class="onionr-post-creator-create" />
</div>
</div>
</div>
</div>
<!-- END POST CREATOR -->
</div>
<div class="row" id="onionr-timeline-posts">
</div>
@ -60,12 +108,100 @@
<div class="d-none d-lg-block col-lg-3">
<div class="row">
<div class="col-12">
<div class="onionr-trending">
<h2>Trending</h2>
<div class="onionr-replies">
<h2 id="onionr-replies-title"></h2>
</div>
</div>
<div id="onionr-reply-creator-panel">
</div>
</div>
<div class="row">
</div>
</div>
</div>
</div>
<!-- POST FOCUS DIALOG -->
<div class="modal fade" id="onionr-post-focus" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="row p-3">
<div class="col-2">
<img src="" id="onionr-post-focus-user-icon" class="onionr-post-user-icon">
</div>
<div class="col-10">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-user-name" id="onionr-post-focus-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url'); jQuery('#onionr-post-focus').modal('hide');">$user-name</a>
<a class="onionr-post-user-id" id="onionr-post-focus-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url'); jQuery('#onionr-post-focus').modal('hide');" data-placement="top" data-toggle="tooltip" title="$user-id">$user-id-truncated</a>
</div>
<div class="col col-auto text-right ml-auto pl-0">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
<div class="onionr-post-content" id="onionr-post-focus-content">
</div>
</div>
<hr class="col-12 onionr-post-focus-separator" />
<!-- POST FOCUS REPLIES -->
<div class="col-12">
<div class="row">
<div class="onionr-post-focus-reply-creator">
<div class="row">
<div class="col-1"></div>
<div class="col-2">
<img class="onionr-post-creator-user-icon" id="onionr-post-focus-reply-creator-user-icon">
</div>
<div class="col-9">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-creator-user-name" id="onionr-post-focus-reply-creator-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')"></a>
<a class="onionr-post-creator-user-id" id="onionr-post-focus-reply-creator-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id">you</a>
</div>
</div>
<textarea class="onionr-post-creator-content" id="onionr-post-focus-reply-creator-content" oninput="focusReplyCreatorChange()"></textarea>
<div class="onionr-post-creator-content-message" id="onionr-post-focus-reply-creator-content-message"></div>
<input type="button" onclick="makeFocusReply()" title="Reply" value="Reply" id="onionr-post-focus-reply-creator-create" class="onionr-post-creator-create" />
</div>
</div>
</div>
<div class="onionr-post-focus-replies"></div>
</div>
</div>
<!-- END POST FOCUS REPLIES -->
</div>
</div>
</div>
</div>
<!-- END POST FOCUS DIALOG -->
<!-- Modal -->
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modal-title">Loading...</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" id="modal-content">Onionr has begun performing a CPU-intensive operation. If this operation does not complete in the next 10 seconds, try reloading the page.</div>
</div>
</div>
</div>

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

View File

@ -6,10 +6,35 @@
"NOTIFICATIONS" : "Notifications",
"MESSAGES" : "Messages",
"LATEST" : "Latest...",
"TRENDING" : "Trending",
"REPLIES" : "Replies",
"MODAL_TITLE" : "Loading...",
"MODAL_MESSAGE" : "Onionr has begun performing a CPU-intensive operation. If this operation does not complete in the next 10 seconds, try reloading the page.",
"POST_LIKE" : "like",
"POST_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" : {

View File

@ -37,10 +37,20 @@ body {
/* timeline */
.onionr-post-focus-separator {
width: 100%;
padding: 1rem;
padding-left: 0;
padding-right: 0;
}
.onionr-post {
padding: 1rem;
margin-bottom: 1rem;
cursor: pointer;
width: 100%;
}
@ -60,6 +70,35 @@ body {
width: 100%;
}
.onionr-post-creator {
padding: 1rem;
margin-bottom: 1rem;
width: 100%;
}
.onionr-post-creator-user-name {
display: inline;
}
.onionr-post-creator-user-id:before { content: "("; }
.onionr-post-creator-user-id:after { content: ")"; }
.onionr-post-creator-content {
word-wrap: break-word;
width: 100%;
}
.onionr-post-creator-user-icon {
border-radius: 100%;
width: 100%;
}
.onionr-post-creator-create {
width: 100%;
text-align: center;
}
.h-divider {
margin: 5px 15px;
height: 1px;
@ -77,3 +116,7 @@ body {
.onionr-profile-username {
text-align: center;
}
.onionr-profile-save {
width: 100%;
}

View File

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

View File

@ -10,10 +10,24 @@
<div class="onionr-profile">
<div class="row">
<div class="col-4 col-lg-12">
<img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="img/default.png">
<img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="">
</div>
<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>
@ -22,6 +36,40 @@
<div class="h-divider pb-3 d-block d-lg-none"></div>
<div class="col-sm-12 col-lg-6">
<div class="row" id="onionr-timeline-post-creator">
<div class="col-12">
<div class="onionr-timeline">
<h2><$= LANG.TIMELINE $></h2>
</div>
</div>
<!-- POST CREATOR -->
<div class="col-12">
<div class="onionr-post-creator">
<div class="row">
<div class="col-2">
<img class="onionr-post-creator-user-icon" id="onionr-post-creator-user-icon">
</div>
<div class="col-10">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-creator-user-name" id="onionr-post-creator-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')"></a>
<a class="onionr-post-creator-user-id" id="onionr-post-creator-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id"><$= LANG.POST_CREATOR_YOU $></a>
</div>
</div>
<textarea class="onionr-post-creator-content" id="onionr-post-creator-content" oninput="postCreatorChange()"></textarea>
<div class="onionr-post-creator-content-message" id="onionr-post-creator-content-message"></div>
<input type="button" onclick="makePost()" title="<$= LANG.POST_CREATOR_CREATE $>" value="<$= LANG.POST_CREATOR_CREATE $>" id="onionr-post-creator-create" class="onionr-post-creator-create" />
</div>
</div>
</div>
</div>
<!-- END POST CREATOR -->
</div>
<div class="row" id="onionr-timeline-posts">
</div>
@ -30,15 +78,58 @@
<div class="d-none d-lg-block col-lg-3">
<div class="row">
<div class="col-12">
<div class="onionr-trending">
<h2><$= LANG.TRENDING $></h2>
<div class="onionr-replies">
<h2 id="onionr-replies-title"></h2>
</div>
</div>
<div id="onionr-reply-creator-panel">
</div>
</div>
<div class="row">
</div>
</div>
</div>
</div>
<!-- POST FOCUS DIALOG -->
<div class="modal fade" id="onionr-post-focus" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="row p-3">
<div class="col-2">
<img src="" id="onionr-post-focus-user-icon" class="onionr-post-user-icon">
</div>
<div class="col-10">
<div class="row">
<div class="col col-auto">
<a class="onionr-post-user-name" id="onionr-post-focus-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url'); jQuery('#onionr-post-focus').modal('hide');">$user-name</a>
<a class="onionr-post-user-id" id="onionr-post-focus-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url'); jQuery('#onionr-post-focus').modal('hide');" data-placement="top" data-toggle="tooltip" title="$user-id">$user-id-truncated</a>
</div>
<div class="col col-auto text-right ml-auto pl-0">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
<div class="onionr-post-content" id="onionr-post-focus-content">
</div>
</div>
<hr class="col-12 onionr-post-focus-separator" />
<$= htmlTemplate('onionr-timeline-reply-creator') $>
</div>
</div>
</div>
</div>
<!-- END POST FOCUS DIALOG -->
<footer />
<script src="js/timeline.js"></script>
</body>

View File

@ -17,18 +17,56 @@ function remove(key) {
return localStorage.removeItem(key);
}
function getParameter(name) {
var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}
/* usermap localStorage stuff */
var usermap = JSON.parse(get('usermap', '{}'));
var postmap = JSON.parse(get('postmap', '{}'))
function getUserMap() {
return usermap;
}
function getPostMap(hash) {
if(hash !== undefined) {
if(hash in postmap)
return postmap[hash];
return null;
}
return postmap;
}
function deserializeUser(id) {
if(!(id in getUserMap()))
return null;
var serialized = getUserMap()[id]
var user = new User();
user.setName(serialized['name']);
user.setID(serialized['id']);
user.setIcon(serialized['icon']);
user.setDescription(serialized['description']);
return user;
}
function getCurrentUser() {
var user = get('currentUser', null);
if(user === null)
return null;
return User.getUser(user, function() {});
}
function setCurrentUser(user) {
set('currentUser', user.getID());
}
/* returns a relative date format, e.g. "5 minutes" */
@ -89,10 +127,10 @@ function timeSince(date, size) {
}
/* 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
var target = this;
return target.split(search).join(replacement);
return target.split(search, limit).join(replacement);
};
/* useful functions to sanitize data */
@ -106,6 +144,16 @@ class Sanitize {
static url(url) {
return encodeURIComponent(url);
}
/* usernames */
static username(username) {
return String(username).replace(/[\W_]+/g, " ").substring(0, 25);
}
/* profile descriptions */
static description(description) {
return String(description).substring(0, 128);
}
}
/* config stuff */
@ -157,42 +205,118 @@ class User {
return this.image;
}
setDescription(description) {
this.description = description;
}
getDescription() {
return this.description;
}
serialize() {
return {
'name' : this.getName(),
'id' : this.getID(),
'icon' : this.getIcon()
'icon' : this.getIcon(),
'description' : this.getDescription()
};
}
/* save in usermap */
remember() {
usermap[this.getID()] = this.serialize();
set('usermap', JSON.stringify(usermap));
}
/* save as a block */
save(callback) {
var block = new Block();
block.setType('onionr-user');
block.setContent(JSON.stringify(this.serialize()));
return block.save(true, callback);
}
static getUser(id, callback) {
// console.log(callback);
var user = deserializeUser(id);
if(user === null) {
Block.getBlocks({'type' : 'onionr-user-info', 'signed' : true, 'reverse' : true}, function(data) {
if(data.length !== 0) {
try {
user = new User();
var userInfo = JSON.parse(data[0].getContent());
if(userInfo['id'] === id) {
user.setName(userInfo['name']);
user.setIcon(userInfo['icon']);
user.setDescription(userInfo['description']);
user.setID(id);
user.remember();
// console.log(callback);
callback(user);
return user;
}
} catch(e) {
console.log(e);
callback(null);
return null;
}
} else {
callback(null);
return null;
}
});
} else {
// console.log(callback);
callback(user);
return user;
}
}
}
/* post class */
class Post {
/* returns the html content of a post */
getHTML() {
getHTML(type) {
var replyTemplate = '<$= jsTemplate('onionr-timeline-reply') $>';
var postTemplate = '<$= jsTemplate('onionr-timeline-post') $>';
var template = '';
if(type !== undefined && type !== null && type == 'reply')
template = replyTemplate;
else
template = postTemplate;
var device = (jQuery(document).width() < 768 ? 'mobile' : 'desktop');
postTemplate = postTemplate.replaceAll('$user-name-url', Sanitize.html(Sanitize.url(this.getUser().getName())));
postTemplate = postTemplate.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-name-url', Sanitize.html(Sanitize.url(this.getUser().getName())));
template = template.replaceAll('$user-name', Sanitize.html(this.getUser().getName()));
template = template.replaceAll('$user-id-url', Sanitize.html(Sanitize.url(this.getUser().getID())));
postTemplate = postTemplate.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().substring(0, 12) + '...'));
// 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()));
postTemplate = postTemplate.replaceAll('$user-image', Sanitize.html(this.getUser().getIcon()));
postTemplate = postTemplate.replaceAll('$content', Sanitize.html(this.getContent()));
postTemplate = postTemplate.replaceAll('$date-relative', timeSince(this.getPostDate(), device) + (device === 'desktop' ? ' ago' : ''));
postTemplate = postTemplate.replaceAll('$date', this.getPostDate().toLocaleString());
template = template.replaceAll('$user-id', Sanitize.html(this.getUser().getID()));
template = template.replaceAll('$user-image', "data:image/jpeg;base64," + Sanitize.html(this.getUser().getIcon()));
template = template.replaceAll('$content', Sanitize.html(this.getContent()).replaceAll('\n', '<br />', 16)); // Maximum of 16 lines
template = template.replaceAll('$post-hash', this.getHash());
template = template.replaceAll('$date-relative-truncated', timeSince(this.getPostDate(), 'mobile'));
template = template.replaceAll('$date-relative', timeSince(this.getPostDate(), device) + (device === 'desktop' ? ' ago' : ''));
template = template.replaceAll('$date', this.getPostDate().toLocaleString());
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) {
@ -211,6 +335,14 @@ class Post {
return this.content;
}
setParent(parent) {
this.parent = parent;
}
getParent() {
return this.parent;
}
setPostDate(date) { // unix timestamp input
if(date instanceof Date)
this.date = date;
@ -221,6 +353,51 @@ class Post {
getPostDate() {
return this.date;
}
setHash(hash) {
this.hash = hash;
}
getHash() {
return this.hash;
}
save(callback) {
var args = {'type' : 'onionr-post', 'sign' : true, 'content' : JSON.stringify({'content' : this.getContent()})};
if(this.getParent() !== undefined && this.getParent() !== null)
args['parent'] = (this.getParent() instanceof Post ? this.getParent().getHash() : (this.getParent() instanceof Block ? this.getParent().getHash() : this.getParent()));
var url = '/client/?action=insertBlock&data=' + Sanitize.url(JSON.stringify(args)) + '&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken());
console.log(url);
var http = new XMLHttpRequest();
if(callback !== undefined) {
// async
var thisObject = this;
http.addEventListener('load', function() {
thisObject.setHash(Block.parseBlockArray(JSON.parse(http.responseText)['hash']));
callback(thisObject.getHash());
}, false);
http.open('GET', url, true);
http.timeout = 5000;
http.send(null);
} else {
// sync
http.open('GET', url, false);
http.send(null);
this.setHash(Block.parseBlockArray(JSON.parse(http.responseText)['hash']));
return this.getHash();
}
}
}
/* block class */
@ -269,8 +446,12 @@ class Block {
// returns the parent block's hash (not Block object, for performance)
getParent() {
if(!(this.parent instanceof Block) && this.parent !== undefined && this.parent !== null)
this.parent = Block.openBlock(this.parent); // convert hash to Block object
// console.log(this.parent);
// TODO: Create a function to fetch the block contents and parse it from the server; right now it is only possible to search for types of blocks (see Block.getBlocks), so it is impossible to return a Block object here
// if(!(this.parent instanceof Block) && this.parent !== undefined && this.parent !== null)
// this.parent = Block.openBlock(this.parent); // convert hash to Block object
return this.parent;
}
@ -323,11 +504,57 @@ class Block {
return !(this.hash === null || this.hash === undefined);
}
// saves the block, returns the hash
save(sign, callback) {
var type = this.getType();
var content = this.getContent();
var parent = this.getParent();
if(content !== undefined && content !== null && type !== '') {
var args = {'content' : content};
if(type !== undefined && type !== null && type !== '')
args['type'] = type;
if(parent !== undefined && parent !== null && parent.getHash() !== undefined && parent.getHash() !== null && parent.getHash() !== '')
args['parent'] = parent.getHash();
if(sign !== undefined && sign !== null)
args['sign'] = String(sign) !== 'false'
var url = '/client/?action=insertBlock&data=' + Sanitize.url(JSON.stringify(args)) + '&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken());
console.log(url);
var http = new XMLHttpRequest();
if(callback !== undefined) {
// async
http.addEventListener('load', function() {
callback(Block.parseBlockArray(JSON.parse(http.responseText)['hash']));
}, false);
http.open('GET', url, true);
http.timeout = 5000;
http.send(null);
} else {
// sync
http.open('GET', url, false);
http.send(null);
return Block.parseBlockArray(JSON.parse(http.responseText)['hash']);
}
}
return false;
}
/* static functions */
// recreates a block by hash
static openBlock(hash) {
return parseBlock(response);
return Block.parseBlock(hash);
}
// converts an associative array to a Block
@ -406,14 +633,57 @@ class Block {
/* temporary code */
var tt = getParameter("timingToken");
if(tt !== null && tt !== undefined) {
setTimingToken(tt);
}
if(getWebPassword() === null) {
var password = "";
while(password.length != 64) {
password = prompt("Please enter the web password (run `./RUN-LINUX.sh --get-password`)");
password = prompt("Please enter the web password (run `./RUN-LINUX.sh --details`)");
}
setTimingToken(prompt("Please enter the timing token (optional)"));
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 */
Block.getBlocks({'type' : 'onionr-post', 'signed' : true, 'reverse' : true}, function(data) {
for(var i = 0; i < data.length; i++) {
try {
var block = data[i];
var finished = false;
User.getUser(new String(block.getHeader('signer', 'unknown')), function(user) {
var post = new Post();
var user = new User();
var blockContent = JSON.parse(block.getContent());
user.setName('unknown');
user.setID(new String(block.getHeader('signer', 'unknown')));
// just ignore anything shorter than 280 characters
if(String(blockContent['content']).length <= 280 && block.getParent() === null) {
post.setContent(blockContent['content']);
post.setPostDate(block.getDate());
post.setUser(user);
post.setHash(block.getHash());
document.getElementById('onionr-timeline-posts').innerHTML += post.getHTML();
}
finished = true;
});
while(!finished);
} catch(e) {
console.log('Troublemaker block: ' + data[i].getHash());
console.log(e);
}
}
});
function viewProfile(id, name) {
document.getElementById("onionr-profile-username").innerHTML = Sanitize.html(decodeURIComponent(name));
function toggleLike(hash) {
var post = getPostMap(hash);
if(post === null || !getPostMap()[hash]['liked']) {
console.log('Liking ' + hash + '...');
if(post === null)
getPostMap()[hash] = {};
getPostMap()[hash]['liked'] = true;
set('postmap', JSON.stringify(getPostMap()));
var block = new Block();
block.setType('onionr-post-like');
block.setContent(JSON.stringify({'hash' : hash}));
block.save(true, function(hash) {});
} else {
console.log('Unliking ' + hash + '...');
}
}
function postCreatorChange() {
var content = document.getElementById('onionr-post-creator-content').value;
var message = '';
var maxlength = 280;
var disable = true;
var warn = false;
if(content.length !== 0) {
if(content.length - content.replaceAll('\n', '').length > 16) {
// 16 max newlines
message = '<$= LANG.POST_CREATOR_MESSAGE_MAXIMUM_NEWLINES $>';
} else if(content.length <= maxlength) {
// 280 max characters
message = '<$= LANG.POST_CREATOR_MESSAGE_REMAINING $>'.replaceAll('%s', (280 - content.length));
disable = false;
if(maxlength - content.length < maxlength / 4) {
warn = true;
}
} else {
message = '<$= LANG.POST_CREATOR_MESSAGE_OVER $>'.replaceAll('%s', (content.length - maxlength));
}
}
var element = document.getElementById('onionr-post-creator-content-message');
var button = document.getElementById("onionr-post-creator-create");
if(message === '')
element.style.visibility = 'hidden';
else {
element.style.visibility = 'visible';
element.innerHTML = message;
if(disable)
element.style.color = 'red';
else if(warn)
element.style.color = '#FF8C00';
else
element.style.color = 'gray';
}
if(disable)
button.disabled = true;
else
button.disabled = false;
}
function replyCreatorChange() {
var content = document.getElementById('onionr-reply-creator-content').value;
var message = '';
var maxlength = 280;
var disable = true;
var warn = false;
if(content.length !== 0) {
if(content.length - content.replaceAll('\n', '').length > 16) {
// 16 max newlines
message = '<$= LANG.POST_CREATOR_MESSAGE_MAXIMUM_NEWLINES $>';
} else if(content.length <= maxlength) {
// 280 max characters
message = '<$= LANG.POST_CREATOR_MESSAGE_REMAINING $>'.replaceAll('%s', (280 - content.length));
disable = false;
if(maxlength - content.length < maxlength / 4) {
warn = true;
}
} else {
message = '<$= LANG.POST_CREATOR_MESSAGE_OVER $>'.replaceAll('%s', (content.length - maxlength));
}
}
var element = document.getElementById('onionr-reply-creator-content-message');
var button = document.getElementById("onionr-reply-creator-create");
if(message === '')
element.style.visibility = 'hidden';
else {
element.style.visibility = 'visible';
element.innerHTML = message;
if(disable)
element.style.color = 'red';
else if(warn)
element.style.color = '#FF8C00';
else
element.style.color = 'gray';
}
if(disable)
button.disabled = true;
else
button.disabled = false;
}
function focusReplyCreatorChange() {
var content = document.getElementById('onionr-post-focus-reply-creator-content').value;
var message = '';
var maxlength = 280;
var disable = true;
var warn = false;
if(content.length !== 0) {
if(content.length - content.replaceAll('\n', '').length > 16) {
// 16 max newlines
message = '<$= LANG.POST_CREATOR_MESSAGE_MAXIMUM_NEWLINES $>';
} else if(content.length <= maxlength) {
// 280 max characters
message = '<$= LANG.POST_CREATOR_MESSAGE_REMAINING $>'.replaceAll('%s', (280 - content.length));
disable = false;
if(maxlength - content.length < maxlength / 4) {
warn = true;
}
} else {
message = '<$= LANG.POST_CREATOR_MESSAGE_OVER $>'.replaceAll('%s', (content.length - maxlength));
}
}
var element = document.getElementById('onionr-post-focus-reply-creator-content-message');
var button = document.getElementById("onionr-post-focus-reply-creator-create");
if(message === '')
element.style.visibility = 'hidden';
else {
element.style.visibility = 'visible';
element.innerHTML = message;
if(disable)
element.style.color = 'red';
else if(warn)
element.style.color = '#FF8C00';
else
element.style.color = 'gray';
}
if(disable)
button.disabled = true;
else
button.disabled = false;
}
function viewProfile(id, name) {
id = decodeURIComponent(id);
document.getElementById("onionr-profile-username").innerHTML = Sanitize.html(decodeURIComponent(name));
User.getUser(id, function(data) {
if(data !== null) {
document.getElementById("onionr-profile-user-icon").src = "data:image/jpeg;base64," + Sanitize.html(data.getIcon());
document.getElementById("onionr-profile-user-icon").b64 = Sanitize.html(data.getIcon());
document.getElementById("onionr-profile-username").innerHTML = Sanitize.html(Sanitize.username(data.getName()));
document.getElementById("onionr-profile-username").title = Sanitize.html(data.getID());
document.getElementById("onionr-profile-description").innerHTML = Sanitize.html(Sanitize.description(data.getDescription()));
}
});
}
function updateUser() {
toggleSaveButton(false);
// jQuery('#modal').modal('show');
var name = jQuery('#onionr-profile-username').text();
var id = document.getElementById("onionr-profile-username").title;
var icon = document.getElementById("onionr-profile-user-icon").b64;
var description = jQuery("#onionr-profile-description").text();
var user = new User();
user.setName(name);
user.setID(id);
user.setIcon(icon);
user.setDescription(Sanitize.description(description));
user.remember();
user.save(function() {
setCurrentUser(user);
window.location.reload();
});
}
function cancelUpdate() {
toggleSaveButton(false);
var name = jQuery('#onionr-profile-username').text();
var id = document.getElementById("onionr-profile-username").title;
viewProfile(id, name);
}
function toggleSaveButton(show) {
document.getElementById("onionr-profile-edit").style.display = (show ? 'block' : 'none');
}
function makePost() {
var content = document.getElementById("onionr-post-creator-content").value;
if(content.trim() !== '') {
var post = new Post();
post.setUser(getCurrentUser());
post.setContent(content);
post.setPostDate(new Date());
post.save(function(data) {}); // async, but no function
document.getElementById('onionr-timeline-posts').innerHTML = post.getHTML() + document.getElementById('onionr-timeline-posts').innerHTML;
document.getElementById("onionr-post-creator-content").value = "";
document.getElementById("onionr-post-creator-content").focus();
postCreatorChange();
} else {
console.log('Not making empty post.');
}
}
function getReplies(id, callback) {
Block.getBlocks({'type' : 'onionr-post', 'parent' : id, 'signed' : true, 'reverse' : true}, callback);
}
function focusPost(id) {
viewReplies(id);
}
function viewRepliesMobile(id) {
var post = document.getElementById('onionr-post-' + id);
var user_name = '';
var user_id = '';
var user_id_trunc = '';
var user_icon = '';
var post_content = '';
if(post !== null && post !== undefined) {
// if the post is in the timeline, get the data from it
user_name = post.getElementsByClassName('onionr-post-user-name')[0].innerHTML;
user_id = post.getElementsByClassName('onionr-post-user-id')[0].title;
user_id_trunc = post.getElementsByClassName('onionr-post-user-id')[0].innerHTML;
user_icon = post.getElementsByClassName('onionr-post-user-icon')[0].src;
post_content = post.getElementsByClassName('onionr-post-content')[0].innerHTML;
} else {
// otherwise, fetch the data
}
document.getElementById('onionr-post-focus-user-icon').src = user_icon;
document.getElementById('onionr-post-focus-user-name').innerHTML = user_name;
document.getElementById('onionr-post-focus-user-id').innerHTML = user_id_trunc;
document.getElementById('onionr-post-focus-user-id').title = user_id;
document.getElementById('onionr-post-focus-content').innerHTML = post_content;
document.getElementById('onionr-post-focus-reply-creator-user-name').innerHTML = Sanitize.html(Sanitize.username(getCurrentUser().getName()));
document.getElementById('onionr-post-focus-reply-creator-user-icon').src = "data:image/jpeg;base64," + Sanitize.html(getCurrentUser().getIcon());
document.getElementById('onionr-post-focus-reply-creator-content').value = '';
document.getElementById('onionr-post-focus-reply-creator-content-message').value = '';
jQuery('#onionr-post-focus').modal('show');
}
function viewReplies(id) {
document.getElementById('onionr-replies-title').innerHTML = '<$= LANG.REPLIES $>';
document.getElementById('onionr-reply-creator-panel').originalPost = id;
document.getElementById('onionr-reply-creator-panel').innerHTML = '<$= jsTemplate('onionr-reply-creator') $>';
document.getElementById('onionr-reply-creator-content').innerHTML = '';
document.getElementById("onionr-reply-creator-content").placeholder = "<$= LANG.POST_CREATOR_PLACEHOLDER $>";
document.getElementById('onionr-reply-creator-user-name').innerHTML = Sanitize.html(Sanitize.username(getCurrentUser().getName()));
document.getElementById('onionr-reply-creator-user-icon').src = "data:image/jpeg;base64," + Sanitize.html(getCurrentUser().getIcon());
document.getElementById('onionr-replies').innerHTML = '';
getReplies(id, function(data) {
var replies = document.getElementById('onionr-replies');
replies.innerHTML = '';
for(var i = 0; i < data.length; i++) {
try {
var block = data[i];
var finished = false;
User.getUser(new String(block.getHeader('signer', 'unknown')), function(user) {
var post = new Post();
var blockContent = JSON.parse(block.getContent());
// just ignore anything shorter than 280 characters
if(String(blockContent['content']).length <= 280) {
post.setContent(blockContent['content']);
post.setPostDate(block.getDate());
post.setUser(user);
post.setHash(block.getHash());
replies.innerHTML += post.getHTML('reply');
}
finished = true;
});
while(!finished);
} catch(e) {
console.log('Troublemaker block: ' + data[i].getHash());
console.log(e);
}
}
});
}
function makeReply() {
var content = document.getElementById("onionr-reply-creator-content").value;
if(content.trim() !== '') {
var post = new Post();
var originalPost = document.getElementById('onionr-reply-creator-panel').originalPost;
console.log('Original post hash: ' + originalPost);
post.setUser(getCurrentUser());
post.setParent(originalPost);
post.setContent(content);
post.setPostDate(new Date());
post.save(function(data) {}); // async, but no function
document.getElementById('onionr-replies').innerHTML = post.getHTML('reply') + document.getElementById('onionr-replies').innerHTML;
document.getElementById("onionr-reply-creator-content").value = "";
document.getElementById("onionr-reply-creator-content").focus();
replyCreatorChange();
} else {
console.log('Not making empty reply.');
}
}
jQuery('body').on('click', '[data-editable]', function() {
var el = jQuery(this);
var txt = el.text();
var maxlength = el.attr("maxlength");
var input = jQuery('<input/>').val(txt);
input.attr('maxlength', maxlength);
el.replaceWith(input);
var save = function() {
var newTxt = input.val();
if(el.attr('id') === 'onionr-profile-username')
newTxt = Sanitize.username(newTxt);
if(el.attr('id') === 'onionr-profile-description')
newTxt = Sanitize.description(newTxt);
var p = el.text(newTxt);
input.replaceWith(p);
if(newTxt !== txt)
toggleSaveButton(true);
};
var saveEnter = function(event) {
console.log(event);
console.log(event.keyCode);
if (event.keyCode === 13)
save();
};
input.one('blur', save).bind('keyup', saveEnter).focus();
});
//viewProfile('$user-id-url', '$user-name-url')
// jQuery('#onionr-post-user-id').on('click', function(e) { alert(3);});
//jQuery('#onionr-post *').on('click', function(e) { e.stopPropagation(); });
// jQuery('#onionr-post').click(function(e) { alert(1); });
currentUser = getCurrentUser();
if(currentUser !== undefined && currentUser !== null) {
document.getElementById("onionr-post-creator-user-name").innerHTML = Sanitize.html(currentUser.getName());
document.getElementById("onionr-post-creator-user-id").innerHTML = "<$= LANG.POST_CREATOR_YOU $>";
document.getElementById("onionr-post-creator-user-icon").src = "data:image/jpeg;base64," + Sanitize.html(currentUser.getIcon());
document.getElementById("onionr-post-creator-user-id").title = currentUser.getID();
document.getElementById("onionr-post-creator-content").placeholder = "<$= LANG.POST_CREATOR_PLACEHOLDER $>";
document.getElementById("onionr-post-focus-reply-creator-content").placeholder = "<$= LANG.POST_CREATOR_PLACEHOLDER $>";
document.getElementById("onionr-post-focus-reply-creator-user-id").innerHTML = "<$= LANG.POST_CREATOR_YOU $>";
}
viewCurrentProfile = function() {
viewProfile(encodeURIComponent(currentUser.getID()), encodeURIComponent(currentUser.getName()));
}
document.getElementById("onionr-post-creator-user-id").onclick = viewCurrentProfile;
document.getElementById("onionr-post-creator-user-name").onclick = viewCurrentProfile;
// on some browsers it saves the user input on reload. So, it should also recheck the input.
postCreatorChange();