Merge branch 'onionrui' into 'master'
onionrui -> master See merge request beardog/Onionr!10
This commit is contained in:
commit
1939dd4427
1
Makefile
1
Makefile
@ -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:
|
||||
|
@ -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/**
|
@ -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'}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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():
|
||||
|
@ -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)
|
||||
|
234
onionr/onionr.py
234
onionr/onionr.py
@ -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__":
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -46,4 +46,5 @@ class OnionrChat:
|
||||
self.communicator.socketClient.sendData(peer, "lol")
|
||||
except:
|
||||
pass
|
||||
|
||||
time.sleep(2)
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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())):
|
||||
|
@ -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=''):
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 :::::::
|
||||
|
||||
|
1
onionr/static-data/www/ui/common/default-icon.html
Normal file
1
onionr/static-data/www/ui/common/default-icon.html
Normal file
File diff suppressed because one or more lines are too long
@ -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">×</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>
|
||||
|
31
onionr/static-data/www/ui/common/onionr-reply-creator.html
Normal file
31
onionr/static-data/www/ui/common/onionr-reply-creator.html
Normal 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 -->
|
@ -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>
|
||||
|
@ -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 -->
|
31
onionr/static-data/www/ui/common/onionr-timeline-reply.html
Normal file
31
onionr/static-data/www/ui/common/onionr-timeline-reply.html
Normal 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 -->
|
@ -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)
|
||||
|
43
onionr/static-data/www/ui/dist/css/main.css
vendored
43
onionr/static-data/www/ui/dist/css/main.css
vendored
@ -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%;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
144
onionr/static-data/www/ui/dist/index.html
vendored
144
onionr/static-data/www/ui/dist/index.html
vendored
@ -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">×</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">×</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>
|
||||
|
||||
|
354
onionr/static-data/www/ui/dist/js/main.js
vendored
354
onionr/static-data/www/ui/dist/js/main.js
vendored
File diff suppressed because one or more lines are too long
474
onionr/static-data/www/ui/dist/js/timeline.js
vendored
474
onionr/static-data/www/ui/dist/js/timeline.js
vendored
@ -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();
|
||||
|
@ -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" : {
|
||||
|
@ -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%;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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">×</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>
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user