Merge branch 'node-profiling' of gitlab.com:beardog/Onionr into node-profiling

This commit is contained in:
Kevin Froman 2018-07-30 02:38:17 -05:00
commit dda5c2885d
26 changed files with 1043 additions and 16 deletions

View File

@ -2,6 +2,7 @@
setup: setup:
sudo pip3 install -r requirements.txt sudo pip3 install -r requirements.txt
-@cd onionr/static-data/ui/; ./compile.py
install: install:
sudo rm -rf /usr/share/onionr/ sudo rm -rf /usr/share/onionr/

View File

@ -16,9 +16,9 @@ To prevent censorship or loss of information, these measures must be in place:
* Anonymization of users by default * Anonymization of users by default
* The Inability to violently coerce human users (personal threats/"doxxing", or totalitarian regime censorship) * 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 # Onionr Design Goals

View File

@ -30,6 +30,9 @@ class API:
''' '''
Main HTTP API (Flask) Main HTTP API (Flask)
''' '''
callbacks = {'public' : {}, 'private' : {}, 'ui' : {}}
def validateToken(self, token): def validateToken(self, token):
''' '''
Validate that the client token matches the given 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 not hmac.compare_digest(timingToken, self.timeBypassToken):
if elapsed < self._privateDelayTime: if elapsed < self._privateDelayTime:
time.sleep(self._privateDelayTime - elapsed) 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/') @app.route('/client/')
def private_handler(): def private_handler():
@ -164,6 +167,44 @@ class API:
self.mimeType = 'text/html' self.mimeType = 'text/html'
response = siteData.split(b'-', 2)[-1] response = siteData.split(b'-', 2)[-1]
resp = Response(response) 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: else:
resp = Response('(O_o) Dude what? (invalid command)') resp = Response('(O_o) Dude what? (invalid command)')
endTime = math.floor(time.time()) endTime = math.floor(time.time())
@ -258,6 +299,8 @@ class API:
peers = self._core.listPeers(getPow=True) peers = self._core.listPeers(getPow=True)
response = ','.join(peers) response = ','.join(peers)
resp = Response(response) resp = Response(response)
elif action in callbacks['public']:
resp = Response(str(getCallback(action, scope = 'public')(request)))
else: else:
resp = Response("") resp = Response("")
@ -328,3 +371,31 @@ class API:
# we exit rather than abort to avoid fingerprinting # we exit rather than abort to avoid fingerprinting
logger.debug('Avoiding fingerprinting, exiting...') logger.debug('Avoiding fingerprinting, exiting...')
sys.exit(1) 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

View File

@ -91,8 +91,6 @@ class Onionr:
self.onionrCore = core.Core() self.onionrCore = core.Core()
self.onionrUtils = OnionrUtils(self.onionrCore) self.onionrUtils = OnionrUtils(self.onionrCore)
self.userOS = platform.system()
# Handle commands # Handle commands
self.debug = False # Whole application debugging self.debug = False # Whole application debugging
@ -542,7 +540,7 @@ class Onionr:
subprocess.Popen([communicatorDaemon, "run", str(net.socksPort)]) subprocess.Popen([communicatorDaemon, "run", str(net.socksPort)])
logger.debug('Started communicator') logger.debug('Started communicator')
events.event('daemon_start', onionr = self) events.event('daemon_start', onionr = self)
api.API(self.debug) self.api = api.API(self.debug)
return return

View File

@ -38,7 +38,6 @@ class Block:
self.btype = type self.btype = type
self.bcontent = content self.bcontent = content
# initialize variables # initialize variables
self.valid = True self.valid = True
self.raw = None self.raw = None
@ -71,8 +70,10 @@ class Block:
# logic # logic
def decrypt(self, anonymous=True, encodedData=True): 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: if self.decrypted:
return True return True
retData = False retData = False
@ -102,7 +103,9 @@ class Block:
return retData return retData
def verifySig(self): 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() core = self.getCore()
if core._crypto.edVerify(data=self.signedData, key=self.signer, sig=self.signature, encodedData=True): if core._crypto.edVerify(data=self.signedData, key=self.signer, sig=self.signature, encodedData=True):
@ -227,11 +230,13 @@ class Block:
else: else:
self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign) self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign)
self.update() self.update()
return self.getHash() return self.getHash()
else: else:
logger.warn('Not writing block; it is invalid.') logger.warn('Not writing block; it is invalid.')
except Exception as e: except Exception as e:
logger.error('Failed to save block.', error = e, timestamp = False) logger.error('Failed to save block.', error = e, timestamp = False)
return False return False
# getters # getters

View File

@ -130,6 +130,22 @@ class CommandAPI:
def get_commands(self): def get_commands(self):
return self.pluginapi.get_onionr().getCommands() 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: class pluginapi:
def __init__(self, onionr, data): def __init__(self, onionr, data):
self.onionr = onionr self.onionr = onionr
@ -142,6 +158,7 @@ class pluginapi:
self.daemon = DaemonAPI(self) self.daemon = DaemonAPI(self)
self.plugins = PluginAPI(self) self.plugins = PluginAPI(self)
self.commands = CommandAPI(self) self.commands = CommandAPI(self)
self.web = WebAPI(self)
def get_onionr(self): def get_onionr(self):
return self.onionr return self.onionr
@ -167,5 +184,8 @@ class pluginapi:
def get_commandapi(self): def get_commandapi(self):
return self.commands return self.commands
def get_webapi(self):
return self.web
def is_development_mode(self): def is_development_mode(self):
return self.get_onionr()._developmentMode return self.get_onionr()._developmentMode

View File

@ -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>

View File

@ -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>

View File

@ -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 -->

123
onionr/static-data/ui/compile.py Executable file
View File

@ -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)

View File

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

74
onionr/static-data/ui/dist/css/main.css vendored Normal file
View File

@ -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;
}

View File

@ -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

77
onionr/static-data/ui/dist/index.html vendored Normal file
View File

@ -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>

170
onionr/static-data/ui/dist/js/main.js vendored Normal file
View File

@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* 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);
}

View File

@ -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));
}

View File

@ -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" : "趋势"
}
}

View File

@ -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;
}

View File

@ -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

View File

@ -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>

View File

@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* 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;
}
}

View File

@ -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));
}