Merge branch 'node-profiling' of gitlab.com:beardog/Onionr into node-profiling
commit
dda5c2885d
1
Makefile
1
Makefile
|
@ -2,6 +2,7 @@
|
|||
|
||||
setup:
|
||||
sudo pip3 install -r requirements.txt
|
||||
-@cd onionr/static-data/ui/; ./compile.py
|
||||
|
||||
install:
|
||||
sudo rm -rf /usr/share/onionr/
|
||||
|
|
|
@ -16,9 +16,9 @@ To prevent censorship or loss of information, these measures must be in place:
|
|||
* Anonymization of users by default
|
||||
* The Inability to violently coerce human users (personal threats/"doxxing", or totalitarian regime censorship)
|
||||
|
||||
* Economic availability. A system should not rely on a single device to be constantly online, and should not be overtly expensive to use. The majority of people in the world own cell phones, but comparatively few own personal computers, particularly in developing countries.
|
||||
* Economic availability. A system should not rely on a single device to be constantly online, and should not be overly expensive to use. The majority of people in the world own cell phones, but comparatively few own personal computers, particularly in developing countries.
|
||||
|
||||
There are many great projects that tackle decentralization and privacy issues, but there are none which tackle all of the above issue. Some of the existing networks have also not worked well in practice, or are more complicated than they need to be.
|
||||
There are many great projects that tackle decentralization and privacy issues, but there are none which tackle all of the above issues. Some of the existing networks have also not worked well in practice, or are more complicated than they need to be.
|
||||
|
||||
# Onionr Design Goals
|
||||
|
||||
|
|
|
@ -30,6 +30,9 @@ class API:
|
|||
'''
|
||||
Main HTTP API (Flask)
|
||||
'''
|
||||
|
||||
callbacks = {'public' : {}, 'private' : {}, 'ui' : {}}
|
||||
|
||||
def validateToken(self, token):
|
||||
'''
|
||||
Validate that the client token matches the given token
|
||||
|
@ -126,7 +129,7 @@ class API:
|
|||
if not hmac.compare_digest(timingToken, self.timeBypassToken):
|
||||
if elapsed < self._privateDelayTime:
|
||||
time.sleep(self._privateDelayTime - elapsed)
|
||||
return send_from_directory('static-data/ui/', path)
|
||||
return send_from_directory('static-data/ui/dist/', path)
|
||||
|
||||
@app.route('/client/')
|
||||
def private_handler():
|
||||
|
@ -164,6 +167,44 @@ class API:
|
|||
self.mimeType = 'text/html'
|
||||
response = siteData.split(b'-', 2)[-1]
|
||||
resp = Response(response)
|
||||
elif action == "insertBlock":
|
||||
response = {'success' : False, 'reason' : 'An unknown error occurred'}
|
||||
|
||||
try:
|
||||
decoded = json.loads(data)
|
||||
|
||||
block = Block()
|
||||
|
||||
sign = False
|
||||
|
||||
for key in decoded:
|
||||
val = decoded[key]
|
||||
|
||||
key = key.lower()
|
||||
|
||||
if key == 'type':
|
||||
block.setType(val)
|
||||
elif key in ['body', 'content']:
|
||||
block.setContent(val)
|
||||
elif key == 'parent':
|
||||
block.setParent(val)
|
||||
elif key == 'sign':
|
||||
sign = (str(val).lower() == 'true')
|
||||
|
||||
hash = block.save(sign = sign)
|
||||
|
||||
if not hash is False:
|
||||
response['success'] = true
|
||||
response['hash'] = hash
|
||||
response['reason'] = 'Successfully wrote block to file'
|
||||
else:
|
||||
response['reason'] = 'Faield to save the block'
|
||||
except Exception as e:
|
||||
logger.debug('insertBlock api request failed', error = e)
|
||||
|
||||
resp = Response(json.dumps(response))
|
||||
elif action in callbacks['private']:
|
||||
resp = Response(str(getCallback(action, scope = 'private')(request)))
|
||||
else:
|
||||
resp = Response('(O_o) Dude what? (invalid command)')
|
||||
endTime = math.floor(time.time())
|
||||
|
@ -258,6 +299,8 @@ class API:
|
|||
peers = self._core.listPeers(getPow=True)
|
||||
response = ','.join(peers)
|
||||
resp = Response(response)
|
||||
elif action in callbacks['public']:
|
||||
resp = Response(str(getCallback(action, scope = 'public')(request)))
|
||||
else:
|
||||
resp = Response("")
|
||||
|
||||
|
@ -328,3 +371,31 @@ class API:
|
|||
# we exit rather than abort to avoid fingerprinting
|
||||
logger.debug('Avoiding fingerprinting, exiting...')
|
||||
sys.exit(1)
|
||||
|
||||
def setCallback(action, callback, scope = 'public'):
|
||||
if not scope in callbacks:
|
||||
return False
|
||||
|
||||
callbacks[scope][action] = callback
|
||||
|
||||
return True
|
||||
|
||||
def removeCallback(action, scope = 'public'):
|
||||
if (not scope in callbacks) or (not action in callbacks[scope]):
|
||||
return False
|
||||
|
||||
del callbacks[scope][action]
|
||||
|
||||
return True
|
||||
|
||||
def getCallback(action, scope = 'public'):
|
||||
if (not scope in callbacks) or (not action in callbacks[scope]):
|
||||
return None
|
||||
|
||||
return callbacks[scope][action]
|
||||
|
||||
def getCallbacks(scope = None):
|
||||
if (not scope is None) and (scope in callbacks):
|
||||
return callbacks[scope]
|
||||
|
||||
return callbacks
|
||||
|
|
|
@ -91,8 +91,6 @@ class Onionr:
|
|||
self.onionrCore = core.Core()
|
||||
self.onionrUtils = OnionrUtils(self.onionrCore)
|
||||
|
||||
self.userOS = platform.system()
|
||||
|
||||
# Handle commands
|
||||
|
||||
self.debug = False # Whole application debugging
|
||||
|
@ -542,7 +540,7 @@ class Onionr:
|
|||
subprocess.Popen([communicatorDaemon, "run", str(net.socksPort)])
|
||||
logger.debug('Started communicator')
|
||||
events.event('daemon_start', onionr = self)
|
||||
api.API(self.debug)
|
||||
self.api = api.API(self.debug)
|
||||
|
||||
return
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ class Block:
|
|||
self.btype = type
|
||||
self.bcontent = content
|
||||
|
||||
|
||||
# initialize variables
|
||||
self.valid = True
|
||||
self.raw = None
|
||||
|
@ -72,7 +71,9 @@ class Block:
|
|||
# logic
|
||||
|
||||
def decrypt(self, anonymous = True, encodedData = True):
|
||||
'''Decrypt a block, loading decrypted data into their vars'''
|
||||
'''
|
||||
Decrypt a block, loading decrypted data into their vars
|
||||
'''
|
||||
if self.decrypted:
|
||||
return True
|
||||
retData = False
|
||||
|
@ -102,7 +103,9 @@ class Block:
|
|||
return retData
|
||||
|
||||
def verifySig(self):
|
||||
'''Verify if a block's signature is signed by its claimed signer'''
|
||||
'''
|
||||
Verify if a block's signature is signed by its claimed signer
|
||||
'''
|
||||
core = self.getCore()
|
||||
|
||||
if core._crypto.edVerify(data=self.signedData, key=self.signer, sig=self.signature, encodedData=True):
|
||||
|
@ -227,11 +230,13 @@ class Block:
|
|||
else:
|
||||
self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign)
|
||||
self.update()
|
||||
|
||||
return self.getHash()
|
||||
else:
|
||||
logger.warn('Not writing block; it is invalid.')
|
||||
except Exception as e:
|
||||
logger.error('Failed to save block.', error = e, timestamp = False)
|
||||
|
||||
return False
|
||||
|
||||
# getters
|
||||
|
|
|
@ -130,6 +130,22 @@ class CommandAPI:
|
|||
def get_commands(self):
|
||||
return self.pluginapi.get_onionr().getCommands()
|
||||
|
||||
class WebAPI:
|
||||
def __init__(self, pluginapi):
|
||||
self.pluginapi = pluginapi
|
||||
|
||||
def register_callback(self, action, callback, scope = 'public'):
|
||||
return self.pluginapi.get_onionr().api.setCallback(action, callback, scope = scope)
|
||||
|
||||
def unregister_callback(self, action, scope = 'public'):
|
||||
return self.pluginapi.get_onionr().api.removeCallback(action, scope = scope)
|
||||
|
||||
def get_callback(self, action, scope = 'public'):
|
||||
return self.pluginapi.get_onionr().api.getCallback(action, scope= scope)
|
||||
|
||||
def get_callbacks(self, scope = None):
|
||||
return self.pluginapi.get_onionr().api.getCallbacks(scope = scope)
|
||||
|
||||
class pluginapi:
|
||||
def __init__(self, onionr, data):
|
||||
self.onionr = onionr
|
||||
|
@ -142,6 +158,7 @@ class pluginapi:
|
|||
self.daemon = DaemonAPI(self)
|
||||
self.plugins = PluginAPI(self)
|
||||
self.commands = CommandAPI(self)
|
||||
self.web = WebAPI(self)
|
||||
|
||||
def get_onionr(self):
|
||||
return self.onionr
|
||||
|
@ -167,5 +184,8 @@ class pluginapi:
|
|||
def get_commandapi(self):
|
||||
return self.commands
|
||||
|
||||
def get_webapi(self):
|
||||
return self.web
|
||||
|
||||
def is_development_mode(self):
|
||||
return self.get_onionr()._developmentMode
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<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>
|
|
@ -0,0 +1,30 @@
|
|||
<title><$= LANG.ONIONR_TITLE $></title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css" />
|
||||
<link rel="stylesheet" type="text/css" href="css/themes/dark.css" />
|
||||
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||
<a class="navbar-brand" href="#">Onionr</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="index.html"><$= LANG.TIMELINE $></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="notifications.html"><$= LANG.NOTIFICATIONS $></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="messages.html"><$= LANG.MESSAGES $></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
|
@ -0,0 +1,31 @@
|
|||
<!-- POST -->
|
||||
<div class="col-12">
|
||||
<div class="onionr-post">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<img class="onionr-post-user-icon" src="$user-image">
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<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>
|
||||
</div>
|
||||
<div class="text-right pl-3 pr-3">
|
||||
<div class="onionr-post-date" data-placement="top" data-toggle="tooltip" title="$date">$date-relative</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="onionr-post-content">
|
||||
$content
|
||||
</div>
|
||||
|
||||
<div class="onionr-post-controls pt-2">
|
||||
<a href="#!" class="glyphicon glyphicon-heart mr-2">like</a>
|
||||
<a href="#!" class="glyphicon glyphicon-comment mr-2">comment</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END POST -->
|
|
@ -0,0 +1,123 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import shutil, os, re, json, traceback
|
||||
|
||||
# get user's config
|
||||
settings = {}
|
||||
with open('config.json', 'r') as file:
|
||||
settings = json.loads(file.read())
|
||||
|
||||
# "hardcoded" config, not for user to mess with
|
||||
HEADER_FILE = 'common/header.html'
|
||||
FOOTER_FILE = 'common/footer.html'
|
||||
SRC_DIR = 'src/'
|
||||
DST_DIR = 'dist/'
|
||||
HEADER_STRING = '<header />'
|
||||
FOOTER_STRING = '<footer />'
|
||||
|
||||
# remove dst folder
|
||||
shutil.rmtree(DST_DIR, ignore_errors=True)
|
||||
|
||||
# taken from https://stackoverflow.com/questions/1868714/how-do-i-copy-an-entire-directory-of-files-into-an-existing-directory-using-pyth
|
||||
def copytree(src, dst, symlinks=False, ignore=None):
|
||||
for item in os.listdir(src):
|
||||
s = os.path.join(src, item)
|
||||
d = os.path.join(dst, item)
|
||||
if os.path.isdir(s):
|
||||
shutil.copytree(s, d, symlinks, ignore)
|
||||
else:
|
||||
shutil.copy2(s, d)
|
||||
|
||||
# copy src to dst
|
||||
copytree(SRC_DIR, DST_DIR, False)
|
||||
|
||||
# load in lang map
|
||||
langmap = {}
|
||||
|
||||
with open('lang.json', 'r') as file:
|
||||
langmap = json.loads(file.read())[settings['language']]
|
||||
|
||||
LANG = type('LANG', (), langmap)
|
||||
|
||||
# templating
|
||||
def jsTemplate(template):
|
||||
with open('common/%s.html' % template, 'r') as file:
|
||||
return file.read().replace('\\', '\\\\').replace('\'', '\\\'').replace('\n', "\\\n")
|
||||
|
||||
def htmlTemplate(template):
|
||||
with open('common/%s.html' % template, 'r') as file:
|
||||
return file.read()
|
||||
|
||||
# tag parser
|
||||
def parseTags(contents):
|
||||
# <$ logic $>
|
||||
for match in re.findall(r'(<\$(?!=)(.*?)\$>)', contents):
|
||||
try:
|
||||
out = exec(match[1].strip())
|
||||
contents = contents.replace(match[0], '' if out is None else str(out))
|
||||
except Exception as e:
|
||||
print('Error: Failed to execute python tag (%s): %s\n' % (filename, match[1]))
|
||||
traceback.print_exc()
|
||||
print('\nIgnoring this error, continuing to compile...\n')
|
||||
|
||||
# <$= data $>
|
||||
for match in re.findall(r'(<\$=(.*?)\$>)', contents):
|
||||
try:
|
||||
out = eval(match[1].strip())
|
||||
contents = contents.replace(match[0], '' if out is None else str(out))
|
||||
except NameError as e:
|
||||
name = match[1].strip()
|
||||
print('Warning: %s does not exist, treating as an str' % name)
|
||||
contents = contents.replace(match[0], name)
|
||||
except Exception as e:
|
||||
print('Error: Failed to execute python tag (%s): %s\n' % (filename, match[1]))
|
||||
traceback.print_exc()
|
||||
print('\nIgnoring this error, continuing to compile...\n')
|
||||
|
||||
return contents
|
||||
|
||||
# get header file
|
||||
with open(HEADER_FILE, 'r') as file:
|
||||
HEADER_FILE = file.read()
|
||||
if settings['python_tags']:
|
||||
HEADER_FILE = parseTags(HEADER_FILE)
|
||||
|
||||
# get footer file
|
||||
with open(FOOTER_FILE, 'r') as file:
|
||||
FOOTER_FILE = file.read()
|
||||
if settings['python_tags']:
|
||||
FOOTER_FILE = parseTags(FOOTER_FILE)
|
||||
|
||||
# iterate dst, replace files
|
||||
def iterate(directory):
|
||||
for filename in os.listdir(directory):
|
||||
if filename.split('.')[-1].lower() in ['htm', 'html', 'css', 'js']:
|
||||
try:
|
||||
path = os.path.join(directory, filename)
|
||||
if os.path.isdir(path):
|
||||
iterate(path)
|
||||
else:
|
||||
contents = ''
|
||||
with open(path, 'r') as file:
|
||||
# get file contents
|
||||
contents = file.read()
|
||||
|
||||
os.remove(path)
|
||||
|
||||
with open(path, 'w') as file:
|
||||
# set the header & footer
|
||||
contents = contents.replace(HEADER_STRING, HEADER_FILE)
|
||||
contents = contents.replace(FOOTER_STRING, FOOTER_FILE)
|
||||
|
||||
# do python tags
|
||||
if settings['python_tags']:
|
||||
contents = parseTags(contents)
|
||||
|
||||
# write file
|
||||
file.write(contents)
|
||||
except Exception as e:
|
||||
print('Error: Failed to parse file: %s\n' % filename)
|
||||
traceback.print_exc()
|
||||
print('\nIgnoring this error, continuing to compile...\n')
|
||||
|
||||
iterate(DST_DIR)
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"language" : "eng",
|
||||
"python_tags" : true
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/* general formatting */
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container-small {
|
||||
width: 300px;
|
||||
}
|
||||
.container-large {
|
||||
width: 970px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container-small {
|
||||
width: 500px;
|
||||
}
|
||||
.container-large {
|
||||
width: 1170px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container-small {
|
||||
width: 700px;
|
||||
}
|
||||
.container-large {
|
||||
width: 1500px;
|
||||
}
|
||||
}
|
||||
|
||||
.container-small, .container-large {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* navbar */
|
||||
|
||||
body {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
/* timeline */
|
||||
|
||||
.onionr-post {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.onionr-post-user-name {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.onionr-post-user-id:before { content: "("; }
|
||||
.onionr-post-user-id:after { content: ")"; }
|
||||
|
||||
.onionr-post-content {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.onionr-post-user-icon {
|
||||
border-radius: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* profile */
|
||||
|
||||
.onionr-profile-user-icon {
|
||||
border-radius: 100%;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.onionr-profile-username {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
body {
|
||||
background-color: #96928f;
|
||||
color: #25383C;
|
||||
}
|
||||
|
||||
/* timeline */
|
||||
|
||||
.onionr-post {
|
||||
border: 1px solid black;
|
||||
border-radius: 1rem;
|
||||
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
.onionr-post-user-name {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.onionr-post-user-id {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.onionr-post-date {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.onionr-post-content {
|
||||
font-family: sans-serif, serif;
|
||||
border-top: 1px solid black;
|
||||
font-size: 15pt;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
|
@ -0,0 +1,77 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Onionr UI</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css" />
|
||||
<link rel="stylesheet" type="text/css" href="css/themes/dark.css" />
|
||||
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||
<a class="navbar-brand" href="#">Onionr</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="index.html">Timeline</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="notifications.html">Notifications</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="messages.html">Messages</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="d-none d-lg-block col-lg-3">
|
||||
<div class="onionr-profile">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="img/default.png">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<h2 id="onionr-profile-username" class="onionr-profile-username">arinerron</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-lg-6">
|
||||
<div class="row" id="onionr-timeline-posts">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-lg-block col-lg-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="onionr-trending">
|
||||
<h2>Trending</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
<script src="js/timeline.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,170 @@
|
|||
/* returns a relative date format, e.g. "5 minutes" */
|
||||
function timeSince(date) {
|
||||
// taken from https://stackoverflow.com/a/3177838/3678023
|
||||
|
||||
var seconds = Math.floor((new Date() - date) / 1000);
|
||||
var interval = Math.floor(seconds / 31536000);
|
||||
|
||||
if (interval > 1)
|
||||
return interval + " years";
|
||||
interval = Math.floor(seconds / 2592000);
|
||||
if (interval > 1)
|
||||
return interval + " months";
|
||||
interval = Math.floor(seconds / 86400);
|
||||
if (interval > 1)
|
||||
return interval + " days";
|
||||
interval = Math.floor(seconds / 3600);
|
||||
if (interval > 1)
|
||||
return interval + " hours";
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval > 1)
|
||||
return interval + " minutes";
|
||||
|
||||
return Math.floor(seconds) + " seconds";
|
||||
}
|
||||
|
||||
/* replace all instances of string */
|
||||
String.prototype.replaceAll = function(search, replacement) {
|
||||
// taken from https://stackoverflow.com/a/17606289/3678023
|
||||
var target = this;
|
||||
return target.split(search).join(replacement);
|
||||
};
|
||||
|
||||
/* sanitizes HTML in a string */
|
||||
function encodeHTML(html) {
|
||||
return String(html).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
/* URL encodes a string */
|
||||
function encodeURL(url) {
|
||||
return encodeURIComponent(url);
|
||||
}
|
||||
|
||||
/* user class */
|
||||
class User {
|
||||
constructor() {
|
||||
this.name = 'Unknown';
|
||||
this.id = 'unknown';
|
||||
this.image = 'img/default.png';
|
||||
}
|
||||
|
||||
setName(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
setID(id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
getID() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
setIcon(image) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return this.image;
|
||||
}
|
||||
}
|
||||
|
||||
/* post class */
|
||||
class Post {
|
||||
/* returns the html content of a post */
|
||||
getHTML() {
|
||||
var postTemplate = '<!-- POST -->\
|
||||
<div class="col-12">\
|
||||
<div class="onionr-post">\
|
||||
<div class="row">\
|
||||
<div class="col-2">\
|
||||
<img class="onionr-post-user-icon" src="$user-image">\
|
||||
</div>\
|
||||
<div class="col-10">\
|
||||
<div class="row">\
|
||||
<div class="col">\
|
||||
<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>\
|
||||
</div>\
|
||||
<div class="text-right pl-3 pr-3">\
|
||||
<div class="onionr-post-date" data-placement="top" data-toggle="tooltip" title="$date">$date-relative</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
\
|
||||
<div class="onionr-post-content">\
|
||||
$content\
|
||||
</div>\
|
||||
\
|
||||
<div class="onionr-post-controls pt-2">\
|
||||
<a href="#!" class="glyphicon glyphicon-heart mr-2">like</a>\
|
||||
<a href="#!" class="glyphicon glyphicon-comment mr-2">comment</a>\
|
||||
</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
<!-- END POST -->\
|
||||
';
|
||||
|
||||
postTemplate = postTemplate.replaceAll('$user-name-url', encodeHTML(encodeURL(this.getUser().getName())));
|
||||
postTemplate = postTemplate.replaceAll('$user-name', encodeHTML(this.getUser().getName()));
|
||||
postTemplate = postTemplate.replaceAll('$user-id-url', encodeHTML(encodeURL(this.getUser().getID())));
|
||||
postTemplate = postTemplate.replaceAll('$user-id-truncated', encodeHTML(this.getUser().getID().split('-').slice(0, 4).join('-')));
|
||||
postTemplate = postTemplate.replaceAll('$user-id', encodeHTML(this.getUser().getID()));
|
||||
postTemplate = postTemplate.replaceAll('$user-image', encodeHTML(this.getUser().getIcon()));
|
||||
postTemplate = postTemplate.replaceAll('$content', encodeHTML(this.getContent()));
|
||||
postTemplate = postTemplate.replaceAll('$date-relative', timeSince(this.getPostDate()) + ' ago');
|
||||
postTemplate = postTemplate.replaceAll('$date', this.getPostDate().toLocaleString());
|
||||
|
||||
return postTemplate;
|
||||
}
|
||||
|
||||
setUser(user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
setContent(content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
setPostDate(date) { // unix timestamp input
|
||||
if(date instanceof Date)
|
||||
this.date = date;
|
||||
else
|
||||
this.date = new Date(date * 1000);
|
||||
}
|
||||
|
||||
getPostDate() {
|
||||
return this.date;
|
||||
}
|
||||
}
|
||||
|
||||
/* handy localstorage functions for quick usage */
|
||||
|
||||
function set(key, val) {
|
||||
return localStorage.setItem(key, val);
|
||||
}
|
||||
|
||||
function get(key, df) { // df is default
|
||||
value = localStorage.getItem(key);
|
||||
if(value == null)
|
||||
value = df;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function remove(key) {
|
||||
return localStorage.removeItem(key);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/* write a random post to the page, for testing */
|
||||
function addRandomPost() {
|
||||
var post = new Post();
|
||||
var user = new User();
|
||||
var items = ['arinerron', 'beardog108', 'samyk', 'snowden', 'aaronswartz'];
|
||||
user.setName(items[Math.floor(Math.random()*items.length)]);
|
||||
user.setID('i-eat-waffles-often-its-actually-crazy-like-i-dont-know-wow');
|
||||
post.setContent('spammm ' + btoa(Math.random() + ' wow'));
|
||||
post.setUser(user);
|
||||
post.setPostDate(new Date(new Date() - (Math.random() * 1000000)));
|
||||
|
||||
document.getElementById('onionr-timeline-posts').innerHTML += post.getHTML();
|
||||
}
|
||||
|
||||
for(var i = 0; i < Math.round(50 * Math.random()); i++)
|
||||
addRandomPost();
|
||||
|
||||
function viewProfile(id, name) {
|
||||
document.getElementById("onionr-profile-username").innerHTML = encodeHTML(decodeURIComponent(name));
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"eng" : {
|
||||
"ONIONR_TITLE" : "Onionr UI",
|
||||
|
||||
"TIMELINE" : "Timeline",
|
||||
"NOTIFICATIONS" : "Notifications",
|
||||
"MESSAGES" : "Messages",
|
||||
|
||||
"TRENDING" : "Trending"
|
||||
},
|
||||
|
||||
"spa" : {
|
||||
"ONIONR_TITLE" : "Onionr UI",
|
||||
|
||||
"TIMELINE" : "Linea de Tiempo",
|
||||
"NOTIFICATIONS" : "Notificaciones",
|
||||
"MESSAGES" : "Mensaje",
|
||||
|
||||
"TRENDING" : "Trending"
|
||||
},
|
||||
|
||||
"zho" : {
|
||||
"ONIONR_TITLE" : "洋葱 用户界面",
|
||||
|
||||
"TIMELINE" : "时间线",
|
||||
"NOTIFICATIONS" : "通知",
|
||||
"MESSAGES" : "消息",
|
||||
|
||||
"TRENDING" : "趋势"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/* general formatting */
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container-small {
|
||||
width: 300px;
|
||||
}
|
||||
.container-large {
|
||||
width: 970px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container-small {
|
||||
width: 500px;
|
||||
}
|
||||
.container-large {
|
||||
width: 1170px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container-small {
|
||||
width: 700px;
|
||||
}
|
||||
.container-large {
|
||||
width: 1500px;
|
||||
}
|
||||
}
|
||||
|
||||
.container-small, .container-large {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* navbar */
|
||||
|
||||
body {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
/* timeline */
|
||||
|
||||
.onionr-post {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.onionr-post-user-name {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.onionr-post-user-id:before { content: "("; }
|
||||
.onionr-post-user-id:after { content: ")"; }
|
||||
|
||||
.onionr-post-content {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.onionr-post-user-icon {
|
||||
border-radius: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* profile */
|
||||
|
||||
.onionr-profile-user-icon {
|
||||
border-radius: 100%;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.onionr-profile-username {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
body {
|
||||
background-color: #96928f;
|
||||
color: #25383C;
|
||||
}
|
||||
|
||||
/* timeline */
|
||||
|
||||
.onionr-post {
|
||||
border: 1px solid black;
|
||||
border-radius: 1rem;
|
||||
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
.onionr-post-user-name {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.onionr-post-user-id {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.onionr-post-date {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.onionr-post-content {
|
||||
font-family: sans-serif, serif;
|
||||
border-top: 1px solid black;
|
||||
font-size: 15pt;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<header />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="d-none d-lg-block col-lg-3">
|
||||
<div class="onionr-profile">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="img/default.png">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<h2 id="onionr-profile-username" class="onionr-profile-username">arinerron</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-lg-6">
|
||||
<div class="row" id="onionr-timeline-posts">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-lg-block col-lg-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="onionr-trending">
|
||||
<h2><$= LANG.TRENDING $></h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer />
|
||||
<script src="js/timeline.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,167 @@
|
|||
|
||||
/* handy localstorage functions for quick usage */
|
||||
|
||||
function set(key, val) {
|
||||
return localStorage.setItem(key, val);
|
||||
}
|
||||
|
||||
function get(key, df) { // df is default
|
||||
value = localStorage.getItem(key);
|
||||
if(value == null)
|
||||
value = df;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function remove(key) {
|
||||
return localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
var usermap = JSON.parse(get('usermap', '{}'));
|
||||
|
||||
function getUserMap() {
|
||||
return usermap;
|
||||
}
|
||||
|
||||
function deserializeUser(id) {
|
||||
var serialized = getUserMap()[id]
|
||||
var user = new User();
|
||||
user.setName(serialized['name']);
|
||||
user.setID(serialized['id']);
|
||||
user.setIcon(serialized['icon']);
|
||||
}
|
||||
|
||||
/* returns a relative date format, e.g. "5 minutes" */
|
||||
function timeSince(date) {
|
||||
// taken from https://stackoverflow.com/a/3177838/3678023
|
||||
|
||||
var seconds = Math.floor((new Date() - date) / 1000);
|
||||
var interval = Math.floor(seconds / 31536000);
|
||||
|
||||
if (interval > 1)
|
||||
return interval + " years";
|
||||
interval = Math.floor(seconds / 2592000);
|
||||
if (interval > 1)
|
||||
return interval + " months";
|
||||
interval = Math.floor(seconds / 86400);
|
||||
if (interval > 1)
|
||||
return interval + " days";
|
||||
interval = Math.floor(seconds / 3600);
|
||||
if (interval > 1)
|
||||
return interval + " hours";
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval > 1)
|
||||
return interval + " minutes";
|
||||
|
||||
return Math.floor(seconds) + " seconds";
|
||||
}
|
||||
|
||||
/* replace all instances of string */
|
||||
String.prototype.replaceAll = function(search, replacement) {
|
||||
// taken from https://stackoverflow.com/a/17606289/3678023
|
||||
var target = this;
|
||||
return target.split(search).join(replacement);
|
||||
};
|
||||
|
||||
/* sanitizes HTML in a string */
|
||||
function encodeHTML(html) {
|
||||
return String(html).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
/* URL encodes a string */
|
||||
function encodeURL(url) {
|
||||
return encodeURIComponent(url);
|
||||
}
|
||||
|
||||
/* user class */
|
||||
class User {
|
||||
constructor() {
|
||||
this.name = 'Unknown';
|
||||
this.id = 'unknown';
|
||||
this.image = 'img/default.png';
|
||||
}
|
||||
|
||||
setName(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
setID(id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
getID() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
setIcon(image) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return this.image;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
'name' : this.getName(),
|
||||
'id' : this.getID(),
|
||||
'icon' : this.getIcon()
|
||||
};
|
||||
}
|
||||
|
||||
remember() {
|
||||
usermap[this.getID()] = this.serialize();
|
||||
set('usermap', JSON.stringify(usermap));
|
||||
}
|
||||
}
|
||||
|
||||
/* post class */
|
||||
class Post {
|
||||
/* returns the html content of a post */
|
||||
getHTML() {
|
||||
var postTemplate = '<$= jsTemplate('onionr-timeline-post') $>';
|
||||
|
||||
postTemplate = postTemplate.replaceAll('$user-name-url', encodeHTML(encodeURL(this.getUser().getName())));
|
||||
postTemplate = postTemplate.replaceAll('$user-name', encodeHTML(this.getUser().getName()));
|
||||
postTemplate = postTemplate.replaceAll('$user-id-url', encodeHTML(encodeURL(this.getUser().getID())));
|
||||
postTemplate = postTemplate.replaceAll('$user-id-truncated', encodeHTML(this.getUser().getID().split('-').slice(0, 4).join('-')));
|
||||
postTemplate = postTemplate.replaceAll('$user-id', encodeHTML(this.getUser().getID()));
|
||||
postTemplate = postTemplate.replaceAll('$user-image', encodeHTML(this.getUser().getIcon()));
|
||||
postTemplate = postTemplate.replaceAll('$content', encodeHTML(this.getContent()));
|
||||
postTemplate = postTemplate.replaceAll('$date-relative', timeSince(this.getPostDate()) + ' ago');
|
||||
postTemplate = postTemplate.replaceAll('$date', this.getPostDate().toLocaleString());
|
||||
|
||||
return postTemplate;
|
||||
}
|
||||
|
||||
setUser(user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
setContent(content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
setPostDate(date) { // unix timestamp input
|
||||
if(date instanceof Date)
|
||||
this.date = date;
|
||||
else
|
||||
this.date = new Date(date * 1000);
|
||||
}
|
||||
|
||||
getPostDate() {
|
||||
return this.date;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/* write a random post to the page, for testing */
|
||||
function addRandomPost() {
|
||||
var post = new Post();
|
||||
var user = new User();
|
||||
var items = ['arinerron', 'beardog108', 'samyk', 'snowden', 'aaronswartz'];
|
||||
user.setName(items[Math.floor(Math.random()*items.length)]);
|
||||
user.setID('i-eat-waffles-often-its-actually-crazy-like-i-dont-know-wow');
|
||||
post.setContent('spammm ' + btoa(Math.random() + ' wow'));
|
||||
post.setUser(user);
|
||||
post.setPostDate(new Date(new Date() - (Math.random() * 1000000)));
|
||||
|
||||
document.getElementById('onionr-timeline-posts').innerHTML += post.getHTML();
|
||||
}
|
||||
|
||||
for(var i = 0; i < Math.round(50 * Math.random()); i++)
|
||||
addRandomPost();
|
||||
|
||||
function viewProfile(id, name) {
|
||||
document.getElementById("onionr-profile-username").innerHTML = encodeHTML(decodeURIComponent(name));
|
||||
}
|
Loading…
Reference in New Issue