Moved plugin web files to be in the plugin folder to reduce staticfiles blueprint coupling

This commit is contained in:
Kevin Froman 2020-09-01 22:41:59 +00:00
parent 14f2d03ebf
commit 37e5dbab4b
23 changed files with 37 additions and 40 deletions

View file

@ -8,6 +8,7 @@ import os
import ujson as json
from flask import Response, Blueprint
from flask import send_from_directory
from deadsimplekv import DeadSimpleKV
from utils import identifyhome
@ -28,6 +29,9 @@ from utils import identifyhome
flask_blueprint = Blueprint('circles', __name__)
root = os.path.dirname(os.path.realpath(__file__))
with open(
os.path.dirname(
os.path.realpath(__file__)) + '/info.json', 'r') as info_file:
@ -41,6 +45,15 @@ read_only_cache = DeadSimpleKV(
flush_on_exit=False,
refresh_seconds=30)
@flask_blueprint.route('/board/<path:path>', endpoint='circlesstatic')
def load_mail(path):
return send_from_directory(root + '/web/', path)
@flask_blueprint.route('/board/', endpoint='circlesindex')
def load_mail_index():
return send_from_directory(root + '/web/', 'index.html')
@flask_blueprint.route('/circles/getpostsbyboard/<board>')
def get_post_by_board(board):

View file

@ -36,7 +36,7 @@ import flowapi # noqa
"""
flask_blueprint = flowapi.flask_blueprint
security_whitelist = ['staticfiles.boardContent', 'staticfiles.board']
security_whitelist = ['circles.circlesstatic', 'circles.circlesindex']
plugin_name = 'circles'
PLUGIN_VERSION = '0.1.0'

View file

@ -0,0 +1,44 @@
/*
Onionr - Private P2P Communication
Auto refresh board posts
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
var checkbox = document.getElementById('refreshCheckbox')
function autoRefresh(){
if (! checkbox.checked || document.hidden){return}
getBlocks()
}
function setupInterval(){
if (checkbox.checked){
refreshInterval = setInterval(autoRefresh, 3000)
autoRefresh()
return
}
clearInterval(refreshInterval)
}
var refreshInterval = setInterval(autoRefresh, 3000)
setupInterval()
checkbox.onchange = function(){setupInterval}
document.addEventListener("visibilitychange", function() {
if (document.visibilityState === 'visible') {
autoRefresh()
}
})

View file

@ -0,0 +1,260 @@
/*
Onionr - Private P2P Communication
This file handles the boards/circles interface
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
requested = []
newPostForm = document.getElementById('addMsg')
firstLoad = true
lastLoadedBoard = 'global'
loadingMessage = document.getElementById('loadingBoard')
loadedAny = false
loadingTimeout = 8000
let toggleLoadingMessage = function(){
switch (loadingMessage.style.display){
case "inline-block":
loadingMessage.style.display = "none"
break;
default:
loadingMessage.style.display = "initial"
break;
}
}
fetch('/circles/version', {
method: 'GET',
headers: {
"token": webpass
}})
.then((ver) => ver.text())
.then(function(ver) {
document.getElementById('circlesVersion').innerText = ver
})
function appendMessages(msg, blockHash, beforeHash, channel) {
if (channel !== document.getElementById('feedIDInput').value) return // ignore if channel name isn't matching
if (msg.length == 0) return // ignore empty messages
var humanDate = new Date(0)
var msgDate = msg['meta']['time']
var feed = document.getElementById("feed")
var beforeEl = null
if (msgDate === undefined){
msgDate = 'unknown'
} else {
humanDate.setUTCSeconds(msgDate)
msgDate = humanDate.toLocaleString("en-US", {timeZone: "Etc/GMT"})
}
var el = document.createElement('div')
el.innerText = msg['content']
if (beforeHash !== null) {
for (i = 0; i < feed.children.length; i++) {
if (feed.children[i].getAttribute('data-bl') === beforeHash) {
beforeEl = feed.children[i]
}
}
}
/* Template Test */
// Test to see if the browser supports the HTML template element by checking
// for the presence of the template element's content attribute.
if ('content' in document.createElement('template')) {
// Instantiate the table with the existing HTML tbody
// and the row with the template
var template = document.getElementById('cMsgTemplate')
// Clone the new row and insert it into the table
var clone = document.importNode(template.content, true)
var div = clone.querySelectorAll("div")
var identicon = clone.querySelectorAll("img")
div[0].classList.add('entry')
div[0].setAttribute('timestamp', msg['meta']['time'])
div[0].setAttribute('data-bl', blockHash)
div[2].textContent = msg['content']
if (typeof msg['meta']['signer'] != 'undefined' && msg['meta']['signer'].length > 0){
div[3].textContent = msg['meta']['signer'].substr(0, 5)
setHumanReadableIDOnPost(div[3], msg['meta']['signer'])
div[3].title = msg['meta']['signer']
userIcon(msg['meta']['signer']).then(function(data){
identicon[0].src = "data:image/svg+xml;base64," + data
})
}
else{
identicon[0].remove()
}
div[4].textContent = msgDate
loadingMessage.style.display = "none"
loadedAny = true
if (firstLoad){
//feed.appendChild(clone)
feed.prepend(clone)
firstLoad = false
}
else{
if (beforeEl === null){
feed.prepend(clone)
}
else{
beforeEl.insertAdjacentElement("beforebegin", clone.children[0])
}
}
}
}
function getBlocks(){
var feed = document.getElementById("feed")
var ch = document.getElementById('feedIDInput').value
if (lastLoadedBoard !== ch){
requested = []
toggleLoadingMessage()
loadedAny = false
while (feed.firstChild) feed.removeChild(feed.firstChild); // remove all messages from feed
setTimeout(function(){
if (! loadedAny && ch == document.getElementById('feedIDInput').value){
PNotify.notice("There are no posts for " + ch + ". You can be the first!")
}
}, loadingTimeout)
}
lastLoadedBoard = ch
if (document.getElementById('none') !== null){
document.getElementById('none').remove();
}
fetch('/circles/getpostsbyboard/' + ch, {
method: 'GET',
headers: {
"token": webpass
}})
.then((resp) => resp.text())
.then(function(feedText) {
var blockList = feedText.split(',')
for (i = 0; i < blockList.length; i++){
blockList[i] = "0".repeat(64 - blockList[i].length) + blockList[i] // pad hash with zeroes
if (! requested.includes(blockList[i])){
if (blockList[i].length == 0) continue
else requested.push(blockList[i])
loadMessage(blockList[i], blockList, i, ch);
}
}
sortEntries()
})
}
function loadMessage(blockHash, blockList, count, channel){
if (blockHash == '0000000000000000000000000000000000000000000000000000000000000000'){
return
}
fetch('/getblockdata/' + blockHash, {
method: 'GET',
headers: {
"token": webpass
}}).then(function(response) {
if (!response.ok) {
let on404 = function() {
if (response.status == 404){
fetch('/circles/removefromcache/' + channel + '/' + blockHash, {
method: 'POST',
headers: {
"content-type": "application/json",
"token": webpass
}
})
}
else{
console.log(error)
}
}()
return
}
response.json().then(function(data){
let before = blockList[count - 1]
let delay = 2000
if (typeof before == "undefined"){
before = null
} else {
let existing = document.getElementsByClassName('cMsgBox')
for (x = 0; x < existing.length; x++){
if (existing[x].getAttribute('data-bl') === before){
delay = 0
}
}
}
setTimeout(function(){appendMessages(data, blockHash, before, channel)}, delay)
})
return response;
})
}
document.getElementById('refreshFeed').onclick = function() {
getBlocks()
}
newPostForm.onsubmit = function(){
var message = document.getElementById('newMsgText').value
var channel = document.getElementById('feedIDInput').value
var meta = {'ch': channel}
let doSign = document.getElementById('postAnon').checked
var postData = {'message': message, 'sign': doSign, 'type': 'brd', 'encrypt': false, 'meta': JSON.stringify(meta)}
postData = JSON.stringify(postData)
newPostForm.style.display = 'none'
fetch('/insertblock', {
method: 'POST',
body: postData,
headers: {
"content-type": "application/json",
"token": webpass
}
})
.then((resp) => resp.text())
.then(function(data) {
newPostForm.style.display = 'block'
if (data == 'failure due to duplicate insert'){
PNotify.error({
text: "This message is already queued"
})
return
}
PNotify.success({
text: "Message queued for posting"
})
setTimeout(function(){getBlocks()}, 500)
})
return false
}
resetCirclePickers = function(){
document.getElementById('recommendedBoards').value = ""
document.getElementById('popularBoards').value = ""
}
document.getElementById('feedIDInput').onchange = resetCirclePickers

View file

@ -0,0 +1,26 @@
/*
Onionr - Private P2P Communication
Handle default board picker
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
recommendedIDs = document.getElementById('recommendedBoards')
recommendedIDs.onchange = function(){
document.getElementById('feedIDInput').value = recommendedIDs.value
getBlocks()
resetCirclePickers()
}

View file

@ -0,0 +1,35 @@
/*
Onionr - Private P2P Communication
detect for Circles if plaintext insert/storage is enabled
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
plaintext_enabled = null
fetch('/config/get/general.store_plaintext_blocks', {
method: 'GET',
headers: {
"token": webpass
}})
.then((resp) => resp.text())
.then(function(data) {
plaintext_enabled = true
if (data == "false"){
plaintext_enabled = false
PNotify.error({
text: "Plaintext storage is disabled. You will not be able to see new posts or make posts yourself"
})
}
})

View file

@ -0,0 +1,196 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset='utf-8'>
<!--Mobile responsive-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
Circles
</title>
<link rel="shortcut icon" type="image/ico" href="/shared/images/favicon.ico">
<link rel="stylesheet" href="/shared/main/PNotifyBrightTheme.css">
<link rel="stylesheet" href="/shared/fontawesome-free-5.10.2/css/all.min.css">
<link rel="stylesheet" href="/gettheme">
<link rel="stylesheet" href="theme.css">
<script defer src="/shared/base32.js"></script>
<script defer src="/shared/identicon.js"></script>
<script defer src="/shared/node_modules/pnotify/dist/iife/PNotify.js"></script>
<script defer src="/shared/node_modules/pnotify/dist/iife/PNotifyButtons.js"></script>
<script defer src="/shared/useridenticons.js"></script>
<script defer src="/shared/misc.js"></script>
<script defer src="/shared/navbar.js"></script>
<script defer src="/shared/main/apicheck.js"></script>
<script defer src="detect-plaintext-storage.js"></script>
<script defer src="sethumanreadable.js"></script>
<script defer src="default-circle-picker.js"></script>
<script defer src="sort-posts.js"></script>
<script defer src="board.js"></script>
<script defer src="autorefresh.js"></script>
<script defer src="popular.js"></script>
</head>
<body>
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item idLink" href="/">
<img src="/shared/images/favicon.ico" class="navbarLogo">
</a>
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false"
data-target="navbarBasic">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasic" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item idLink" href="/mail/">Mail</a>
<a class="navbar-item idLink" href="/friends/">Friends</a>
<a class="navbar-item idLink" href="/board/">Circles</a>
</div>
</div>
</nav>
<!--Hero (Dark Section)-->
<section class="hero is-small is-dark">
<div class="hero-body">
<div class="container">
<div class="columns">
<div class="column">
<h1 class="title">
Circles <span class="is-pulled-right">v<span id='circlesVersion'></span></span>
</h1>
<h2 class="subtitle">
Anonymous message boards
</h2>
</div>
</div>
</div>
</section>
<br>
<!--Start Content-->
<div class="container">
<div class="columns">
<!--Add Friend-->
<div class="column is-one-third">
<div class="card">
<form method='POST' action='/' id='addMsg'>
<header class="card-header">
<p class="card-header-title">
Post message
</p>
</header>
<div class="card-content">
<div class="content">
<textarea id='newMsgText' class="textarea" name='newMsgText' rows=10 cols=50 required
minlength="2"></textarea>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item">
<input class='button is-primary' type='submit' value='Post'>
</a>
</footer>
</form>
</div>
</div>
<!--Feed-->
<div class="column">
<div class="card">
<header class="card-header">
<p class="card-header-title">
Feed
</p>
</header>
<div class="card-content">
<div class="content">
<div class="field">
<div class="field has-addons">
<p class="control">
<a class="button is-static">Circle Name</a>
</p>
<p class="control is-expanded">
<input id="feedIDInput" class="input" placeholder="Board name" value="global">
</p>
<p class="control">
<a class="button is-success" id="refreshFeed">Refresh Feed</a>
</p>
</div>
<div class="field">
<div class="columns">
<div class="column is-2">
<div class="control">
<label for="recommendedBoards" class="label">Default Circles:</label>
<div class="select">
<select id="recommendedBoards">
<option value=""></option>
<option value="global">Global</option>
<option value="onionr">Onionr</option>
<option value="games">Games</option>
<option value="politics">Politics</option>
<option value="tech">Tech</option>
<option value="random">Random</option>
<option value="privacy">Privacy</option>
</select>
</div>
</div>
</div>
<div class="column is-2">
<div class="control">
<label for="popularBoards" class="label">Popular Circles:</label>
<div class="select">
<select id="popularBoards">
<option value="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</option>
</select>
</div>
</div>
</div>
</div>
</div>
<p class="control">
<br>
Note: All posts in Circles are publicly accessible.
</p>
<input type="checkbox" class="checkbox" id="refreshCheckbox" checked>
<label for="refreshCheckbox">Auto refresh feed</label>
<br>
<input type="checkbox" class="checkbox" id="postAnon" checked>
<label for="postAnon">Sign posts</label>
</div>
</div>
<div class="content">
<span id='loadingBoard'><i class="fas fa-yin-yang fa-spin"></i></span>
<div id='feed'>
<span id='none'>None yet, try refreshing 😃</span>
<!--Message Items are appended here based on template-->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!--Template markup for Circle message-->
<template id="cMsgTemplate">
<div class="box cMsgBox">
<div class="columns">
<div class="column cMsg">
Message
</div>
<div class="column cAuthor is-narrow"></div>
<img class="identicon image is-48x48" alt="user icon" src="/shared/images/anon.svg">
<div class="column is-narrow cMsgDate">
Date
</div>
</div>
</div>
</template>
</body>
</html>

View file

@ -0,0 +1,45 @@
/*
Onionr - Private P2P Communication
Load popular boards and show them in the UI. Handle selections of popular boards.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
fetch('/circles/getpopular/8', {
method: 'GET',
headers: {
"token": webpass
}})
.then((popular) => popular.text())
.then(function(popular) {
var popularSelect = document.getElementById('popularBoards')
let boards = popular.split(',')
for (board of boards){
let newOption = document.createElement('option')
if (board == ""){continue}
newOption.value = board
newOption.innerText = board.charAt(0).toUpperCase() + board.slice(1)
console.debug(board)
popularSelect.appendChild(newOption)
}
})
document.getElementById('popularBoards').onchange = function(){
document.getElementById('feedIDInput').value = document.getElementById('popularBoards').value
getBlocks()
resetCirclePickers()
}

View file

@ -0,0 +1,39 @@
/*
Onionr - Private P2P Communication
Set human readable public keys onto post author elements
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
humanReadableKeys = {}
function setHumanReadableIDOnPost(el, key){
if (typeof humanReadableKeys[key] == "undefined"){
fetch('/getHumanReadable/' + key, {
method: 'GET',
headers: {
"token": webpass
}})
.then((resp) => resp.text()) // Transform the data into json
.then(function(data) {
if (data.includes('HTML')){
return
}
humanReadableKeys[key] = data
setHumanReadableIDOnPost(el, key)
})
return
}
el.innerText = humanReadableKeys[key].split('-').slice(0, 3).join(' ')
}

View file

@ -0,0 +1,30 @@
/*
Onionr - Private P2P Communication
Sort post entries
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function sortEntries() {
var entries = document.getElementsByClassName('entry')
if (entries.length > 1) {
const sortBy = 'timestamp'
const parent = entries[0].parentNode
const sorted = Array.from(entries).sort((a, b) => b.getAttribute(sortBy) - a.getAttribute(sortBy))
sorted.forEach(element => parent.appendChild(element))
}
}

View file

@ -0,0 +1,11 @@
.cMsg{
word-wrap:break-word;
word-break:break-word;
white-space: pre-wrap;
}
body{
background-color: #212224;
color: white;
}