Add *
After Width: | Height: | Size: 4.4 MiB |
After Width: | Height: | Size: 3.4 MiB |
After Width: | Height: | Size: 2.9 MiB |
After Width: | Height: | Size: 3.4 MiB |
After Width: | Height: | Size: 3.6 MiB |
After Width: | Height: | Size: 867 KiB |
After Width: | Height: | Size: 3.0 MiB |
After Width: | Height: | Size: 2.2 MiB |
After Width: | Height: | Size: 2.4 MiB |
After Width: | Height: | Size: 4.7 MiB |
After Width: | Height: | Size: 6.7 MiB |
After Width: | Height: | Size: 4.1 MiB |
After Width: | Height: | Size: 3.2 MiB |
After Width: | Height: | Size: 4.0 MiB |
After Width: | Height: | Size: 2.5 MiB |
After Width: | Height: | Size: 2.3 MiB |
After Width: | Height: | Size: 2.5 MiB |
After Width: | Height: | Size: 77 KiB |
After Width: | Height: | Size: 3.5 MiB |
After Width: | Height: | Size: 3.1 MiB |
After Width: | Height: | Size: 3.4 MiB |
After Width: | Height: | Size: 2.3 MiB |
After Width: | Height: | Size: 740 KiB |
After Width: | Height: | Size: 2.5 MiB |
After Width: | Height: | Size: 3.3 MiB |
After Width: | Height: | Size: 633 KiB |
|
@ -0,0 +1,33 @@
|
|||
from bottle import Bottle
|
||||
from bottle.ext import sqlite
|
||||
import os
|
||||
|
||||
root = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
default_user = "Anonymous"
|
||||
invalid_nicknames = [default_user.lower(), "admin"]
|
||||
secret = "fnord"
|
||||
limit = 10
|
||||
|
||||
boards = {
|
||||
"technology":"My printer don't work can you help me?",
|
||||
"programming":"Hello World!",
|
||||
"games":"Wait A Second Dingus",
|
||||
"media":"General Media Sharing",
|
||||
"library":"Hidden Boards Book Collection",
|
||||
"gallery":"Hidden Boards Visual Media Collection",
|
||||
"random":f"{limit} Random Threads",
|
||||
"nsfw":"Nylon Sheets Feel Weird",
|
||||
"requests":"Sugestions and Complaints"
|
||||
}
|
||||
|
||||
host = '0.0.0.0'
|
||||
port = 8080
|
||||
|
||||
db_path = f"{root}/.hidden/hiddenboards.db"
|
||||
cert_path = f"{root}/.hidden/cert.pem"
|
||||
key_path = f"{root}/.hidden/key.pem"
|
||||
banner_dir = f"{root}/banners"
|
||||
|
||||
app = Bottle()
|
||||
app.install(sqlite.Plugin(dbfile=db_path))
|
|
@ -0,0 +1,315 @@
|
|||
@font-face {
|
||||
font-family:hiddenfont;
|
||||
src:url(/s/font) format('woff');
|
||||
font-weight:lighter;
|
||||
font-style:normal;
|
||||
font-size: x-large;
|
||||
}
|
||||
|
||||
* {
|
||||
border-radius: 2px;
|
||||
}
|
||||
.comment{
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.thread h1{
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
|
||||
.thread img {
|
||||
max-width:100%;
|
||||
}
|
||||
|
||||
.nav{
|
||||
float:left;
|
||||
/* background:black; */
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
padding-top:0;
|
||||
padding-bottom: 0;
|
||||
margin: 5px;
|
||||
margin-top: 0;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.block {
|
||||
padding:10px;
|
||||
margin:5px;
|
||||
background:black;
|
||||
}
|
||||
|
||||
.file {
|
||||
float:left;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
.file img {
|
||||
/* width: 100%; */
|
||||
/* max-height:500px; */
|
||||
}
|
||||
|
||||
.files {
|
||||
width:100%;
|
||||
float:left;
|
||||
}
|
||||
|
||||
.box{
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.thumb{
|
||||
width:33%;
|
||||
height:270px;
|
||||
margin-bottom:1px;
|
||||
margin-top:1px;
|
||||
margin-right: 1px;
|
||||
margin-left: 1px;
|
||||
padding: 5px;
|
||||
|
||||
}
|
||||
|
||||
.thumb img{
|
||||
max-height:200px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.thumb a {
|
||||
line-break:anywhere;
|
||||
}
|
||||
|
||||
.toggle{
|
||||
position: absolute;
|
||||
left: -100vw;
|
||||
}
|
||||
|
||||
:checked ~ .thumb{
|
||||
width:100%;
|
||||
height: 100%;
|
||||
max-height:100%;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
:checked ~ .thumb p{
|
||||
visibility: hidden;
|
||||
height:0px;
|
||||
margin:0px;
|
||||
}
|
||||
|
||||
:checked ~ .file img{
|
||||
max-width: 100%;
|
||||
max-height:100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.codetable pre{
|
||||
padding-left: 10px;
|
||||
color:black;
|
||||
font-family:hiddenfont;
|
||||
}
|
||||
|
||||
.codetable {
|
||||
width:100%;
|
||||
height: auto;
|
||||
overflow:auto;
|
||||
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family:hiddenfont;
|
||||
}
|
||||
|
||||
body {
|
||||
color:#ACA4F6;
|
||||
font-family:hiddenfont;
|
||||
margin:0;
|
||||
margin-top:50px;
|
||||
background:#0C0C0C;
|
||||
}
|
||||
|
||||
.titlebar h2{
|
||||
font-family:hiddenfont;
|
||||
color:#7352E6;
|
||||
margin-bottom: 0;
|
||||
margin-top:0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-family:hiddenfont;
|
||||
border-left: 2px solid gray;
|
||||
padding:5px;
|
||||
}
|
||||
input,textarea {
|
||||
font-family:hiddenfont;
|
||||
width:100%;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
input[type=submit] {
|
||||
font-family:hiddenfont;
|
||||
width:100%;
|
||||
background: #0C0C0C;
|
||||
}
|
||||
input[type=submit]:hover {
|
||||
font-family:hiddenfont;
|
||||
background: #080808;
|
||||
color: #FFE620;
|
||||
}
|
||||
table {
|
||||
font-family:hiddenfont;
|
||||
border:none;
|
||||
width:600px;
|
||||
margin:0 auto;
|
||||
border-radius:5px;
|
||||
background:#000000;
|
||||
}
|
||||
i {
|
||||
font-family:hiddenfont;
|
||||
font-style:normal;
|
||||
margin-left:2.5px;
|
||||
border-radius:10px;
|
||||
padding:2px;
|
||||
}
|
||||
small {
|
||||
font-family:hiddenfont;
|
||||
padding-left:2.5px;
|
||||
}
|
||||
form {
|
||||
color:#ACA4F6;
|
||||
font-family:hiddenfont;
|
||||
margin:0 0 30px 0;
|
||||
}
|
||||
strong {
|
||||
font-family:hiddenfont;
|
||||
}
|
||||
#replytext {
|
||||
font-family:hiddenfont;
|
||||
margin-top:-20px;
|
||||
font-size:10pt;
|
||||
margin-bottom:15px;
|
||||
}
|
||||
textarea {
|
||||
color:#ACA4F6;
|
||||
font-family:hiddenfont;
|
||||
height:100px;
|
||||
border: none;
|
||||
background: #0C0C0C;
|
||||
}
|
||||
h1 {
|
||||
font-family:hiddenfont;
|
||||
text-align:center;
|
||||
margin:10px 0;
|
||||
color:#7352E6;
|
||||
}
|
||||
#banner {
|
||||
font-family:hiddenfont;
|
||||
display:block;
|
||||
height:150px;
|
||||
margin:10px auto;
|
||||
border:1px solid #FFE620;
|
||||
}
|
||||
.thread {
|
||||
font-family: hiddenfont;
|
||||
display:inline-block;
|
||||
margin:15px 3px 3px 3px;
|
||||
padding:10px;
|
||||
/* width:calc(100% - 32px); */
|
||||
max-width:calc(100% - 32px);
|
||||
float:left;
|
||||
clear:left;
|
||||
text-align:left;
|
||||
font-family:hiddenfont;
|
||||
margin-top:1px;
|
||||
width:100%;
|
||||
margin:5px;
|
||||
background:black;
|
||||
}
|
||||
|
||||
|
||||
/* .thread img {
|
||||
float:left;
|
||||
max-height:250px;
|
||||
max-width:250px;
|
||||
margin-right:10px;
|
||||
} */
|
||||
|
||||
/* .thread div {
|
||||
width:100%;
|
||||
word-wrap:break-word;
|
||||
} */
|
||||
|
||||
hr {
|
||||
font-family:hiddenfont;
|
||||
border:1px solid #FFE620;
|
||||
}
|
||||
|
||||
header {
|
||||
font-family: hiddenfont;
|
||||
top:0;
|
||||
width:100%;
|
||||
position:absolute;
|
||||
text-align:center;
|
||||
padding:5px 0;
|
||||
z-index:23;
|
||||
color:#ACA4F6;
|
||||
background:#000000;
|
||||
border-bottom:1px solid #FFE620;
|
||||
}
|
||||
|
||||
footer {
|
||||
font-family: hiddenfont;
|
||||
bottom:0;
|
||||
width:100%;
|
||||
position:absolute;
|
||||
text-align:center;
|
||||
padding:5px 0;
|
||||
z-index:23;
|
||||
color:#ACA4F6;
|
||||
background:#000000;
|
||||
border-top:1px solid #FFE620;
|
||||
}
|
||||
|
||||
.home-info-box {
|
||||
font-family:hiddenfont;
|
||||
width:800px;
|
||||
text-align:center;
|
||||
margin:0 auto;
|
||||
}
|
||||
|
||||
input {
|
||||
color:#ACA4F6;
|
||||
font-family:hiddenfont;
|
||||
background: #0C0C0C;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.spoiler {
|
||||
padding-left:10px;
|
||||
font-family:hiddenfont;
|
||||
background:grey;
|
||||
color:grey;
|
||||
}
|
||||
.spoiler:hover {
|
||||
color:white;
|
||||
}
|
||||
.coincidence {
|
||||
font-family:hiddenfont;
|
||||
background:#000263;
|
||||
color:#00D7FF;
|
||||
}
|
||||
b {
|
||||
font-family:hiddenfont;
|
||||
color:#7352E6;
|
||||
}
|
||||
|
||||
a {
|
||||
font-family:hiddenfont;
|
||||
color:#FFE620;
|
||||
outline:none;
|
||||
text-decoration:none;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="robots" content="noindex,nofollow"/>
|
||||
<title>{{!title}}</title>
|
||||
<style>
|
||||
{{!style}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a style="float:left;margin-left:5px;" href="/">HIDDEN BOARDS</a>
|
||||
<span style="position: absolute;top: 50%;left: 50%;margin-right: -50%;transform: translate(-50%, -50%)">
|
||||
{{!header}}
|
||||
</span>
|
||||
<b style="float:right;margin-right:5px;">{{!login}}</b>
|
||||
</header>
|
||||
<div class="home-info-box">
|
||||
<a href="/"><img id="banner" src="/s/banner"></a>
|
||||
<a href="/.{{!board}}"><h1 style="margin-top:20px;text-transform:uppercase;">{{!board}}</h1></a>
|
||||
<h3 style="margin-bottom:20px;color:#ACA4F6;">{{description}}</h3>
|
||||
<form method="post" enctype="multipart/form-data" autocomplete="off">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Name:</td>
|
||||
<td><input type="text" name="name" readonly placeholder="{{!user}}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subject:</td>
|
||||
<td><input name="title" required /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Comment:</td>
|
||||
<td><textarea name="content" required ></textarea></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Files:</td>
|
||||
<td><input type="file" name="files" multiple ></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" colspan="2"><input type="submit"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<div id="feed">
|
||||
{{!posts}}
|
||||
</div>
|
||||
{{!nav}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="robots" content="noindex,nofollow"/>
|
||||
<title>{{!title}}</title>
|
||||
<style>
|
||||
{{!style}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a style="float:left;margin-left:5px;" href="/">HIDDEN BOARDS</a>
|
||||
<span style="position: absolute;top: 50%;left: 50%;margin-right: -50%;transform: translate(-50%, -50%)">
|
||||
{{!header}}
|
||||
</span>
|
||||
<b style="float:right;margin-right:5px;">{{!login}}</b>
|
||||
</header>
|
||||
<div class="home-info-box">
|
||||
<a href="/"><img id="banner" src="/s/banner"></a>
|
||||
<a href="/.{{!board}}"><h1 style="margin-top:20px;text-transform:uppercase;">{{!board}}</h1></a>
|
||||
<h3 style="margin-bottom:20px;color:#ACA4F6;">{{!description}}</h3>
|
||||
<div class="thread">
|
||||
{{!posts}}
|
||||
</div>
|
||||
{{!nav}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="robots" content="noindex,nofollow"/>
|
||||
<title>{{!error}}</title>
|
||||
<style>
|
||||
{{!style}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{{!header}}
|
||||
</header>
|
||||
<a href="/"><img id="banner" src="/s/banner"></a>
|
||||
<div class="home-info-box">
|
||||
<h1>Error {{!error}}!</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="robots" content="noindex,nofollow"/>
|
||||
<title>{{!title}}</title>
|
||||
<style>
|
||||
{{!style}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a style="float:left;margin-left:5px;" href="/">HIDDEN BOARDS</a>
|
||||
<span style="position: absolute;top: 50%;left: 50%;margin-right: -50%;transform: translate(-50%, -50%)">
|
||||
{{!header}}
|
||||
</span>
|
||||
<b style="float:right;margin-right:5px;">{{!login}}</b>
|
||||
</header>
|
||||
<div class="home-info-box">
|
||||
<a href="/"><img id="banner" src="/s/banner"></a>
|
||||
<h1 style="margin-top:20px;text-transform:uppercase;">{{!title}}</h1>
|
||||
<h3 style="margin-bottom:20px;color:#ACA4F6;">{{!motd}}</h3>
|
||||
<div class="block titlebar"><h2>Active Boards</h2></div>
|
||||
<div id="feed" style="text-align:left;">
|
||||
{{!boards}}
|
||||
</div>
|
||||
<!-- <div class="titlebar">Rules</div>
|
||||
<div style="text-align:center;margin-left:10px;padding:10px 0;">
|
||||
1. We don't talk about HIDDEN BOARDS<br/>
|
||||
2. No spam, no flood and no unrelated ads<br/>
|
||||
3. No child pornography<br/>
|
||||
4. No doxxing<br/>
|
||||
5. Everything should follow the law of fives
|
||||
</div> -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,28 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="robots" content="noindex,nofollow"/>
|
||||
<title>{{!title}}</title>
|
||||
<style>
|
||||
{{!style}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a style="float:left;margin-left:5px;" href="/">HIDDEN BOARDS</a>
|
||||
<span style="position: absolute;top: 50%;left: 50%;margin-right: -50%;transform: translate(-50%, -50%)">
|
||||
{{!header}}
|
||||
</span>
|
||||
<b style="float:right;margin-right:5px;">{{!login}}</b>
|
||||
</header>
|
||||
<div class="home-info-box">
|
||||
<a href="/"><img id="banner" src="/s/banner"></a>
|
||||
<a href="/.{{!board}}"><h1 style="margin-top:20px;text-transform:uppercase;">{{!board}}</h1></a>
|
||||
<h3 style="margin-bottom:20px;color:#ACA4F6;">{{description}}</h3>
|
||||
<div id="feed">
|
||||
{{!posts}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="robots" content="noindex,nofollow"/>
|
||||
<title>LOGIN</title>
|
||||
<style>
|
||||
{{!style}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a style="float:left;margin-left:5px;" href="/">HIDDEN BOARDS</a>
|
||||
<span style="position: absolute;top: 50%;left: 50%;margin-right: -50%;transform: translate(-50%, -50%)">
|
||||
{{!header}}
|
||||
</span>
|
||||
<b style="float:right;margin-right:5px;">New account? <a href="/signup">SIGNUP</a></b>
|
||||
</header>
|
||||
|
||||
<div class="home-info-box">
|
||||
<a href="/"><img id="banner" src="/s/banner"></a>
|
||||
<h1>SIGNIN</h1>
|
||||
|
||||
<form method="post" autocomplete="off">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Nickname:</td>
|
||||
<td><input type="text" name="user" value="{{!nickname}}" pattern="[a-zA-Z][a-zA-Z0-9-_\.]{1,23}" required ></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Password:</td>
|
||||
<td><input type="password" name="password" required /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" colspan="2"><input type="submit"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<p>{{!error}}</p>
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,45 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="robots" content="noindex,nofollow"/>
|
||||
<title>SIGNUP</title>
|
||||
<style>
|
||||
{{!style}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a style="float:left;margin-left:5px;" href="/">HIDDEN BOARDS</a>
|
||||
<span style="position: absolute;top: 50%;left: 50%;margin-right: -50%;transform: translate(-50%, -50%)">
|
||||
{{!header}}
|
||||
</span>
|
||||
<b style="float:right;margin-right:5px;">Have an account? <a href="/login">LOGIN</a></b>
|
||||
</header>
|
||||
|
||||
<div class="home-info-box">
|
||||
<a href="/"><img id="banner" src="/s/banner"></a>
|
||||
<h1>SIGNUP</h1>
|
||||
<form method="post" autocomplete="off">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Nickname:</td>
|
||||
<td><input type="text" name="user" value="{{!nickname}}" pattern="[a-zA-Z][a-zA-Z0-9-_\.]{1,23}" required ></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Password:</td>
|
||||
<td><input type="password" name="password" required /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Confirm Password:</td>
|
||||
<td><input type="password" name="confirmation" required /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" colspan="2"><input type="submit"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<p>{{!error}}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,51 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="robots" content="noindex,nofollow"/>
|
||||
<title>{{!title}}</title>
|
||||
<style>
|
||||
{{!style}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a style="float:left;margin-left:5px;" href="/">HIDDEN BOARDS</a>
|
||||
<span style="position: absolute;top: 50%;left: 50%;margin-right: -50%;transform: translate(-50%, -50%)">
|
||||
{{!header}}
|
||||
</span>
|
||||
<b style="float:right;margin-right:5px;">{{!login}}</b>
|
||||
</header>
|
||||
|
||||
<div class="home-info-box">
|
||||
<a href="/"><img id="banner" src="/s/banner"></a>
|
||||
<a href="/.{{!board}}"><h1 style="margin-top:20px;text-transform:uppercase;">{{!board}}</h1></a>
|
||||
<h3 style="margin-bottom:20px;color:#ACA4F6;">{{description}}</h3>
|
||||
<div class="titlebar" style="background:none;"><h1 style="align:center;"><a href="/.{{!board}}/thread/{{!id}}">{{title}}</a></h1></div>
|
||||
|
||||
<div id="feed">
|
||||
{{!posts}}
|
||||
</div>
|
||||
{{!nav}}
|
||||
<form method="post" enctype="multipart/form-data" autocomplete="off">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Name:</td>
|
||||
<td><input type="text" name="name" readonly placeholder={{!user}}></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Comment:</td>
|
||||
<td><textarea name="content" required ></textarea></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Files:</td>
|
||||
<td><input type="file" name="files" multiple ></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" colspan="2"><input type="submit"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="robots" content="noindex,nofollow"/>
|
||||
<title>{{!title}}</title>
|
||||
<style>
|
||||
{{!style}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a style="float:left;margin-left:5px;" href="/">HIDDEN BOARDS</a>
|
||||
<span style="position: absolute;top: 50%;left: 50%;margin-right: -50%;transform: translate(-50%, -50%)">
|
||||
{{!header}}
|
||||
</span>
|
||||
<b style="float:right;margin-right:5px;">{{!login}}</b>
|
||||
</header>
|
||||
|
||||
<div class="home-info-box">
|
||||
<a href="/"><img id="banner" src="/s/banner"></a>
|
||||
<a href="/u/{{!user}}"><h1 style="margin-top:20px;">{{!title}}</h1></a>
|
||||
{{!page}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,6 @@
|
|||
from config import app, host, port
|
||||
from util import SSLServer
|
||||
|
||||
import routes
|
||||
|
||||
app.run(host=host, port=port, server=SSLServer)
|
|
@ -0,0 +1,483 @@
|
|||
from bottle import request, redirect, static_file, response, abort
|
||||
from pdf2image import convert_from_bytes
|
||||
from hashlib import md5
|
||||
import datetime
|
||||
import random
|
||||
import uuid
|
||||
import os
|
||||
import io
|
||||
|
||||
from templates import index_page, board_page, thread_page, user_page ,error_page
|
||||
from templates import user_content, user_form, default_content
|
||||
from templates import collection_page, random_page
|
||||
from templates import signin_page, signup_page
|
||||
from templates import account, signout_link
|
||||
|
||||
from config import secret, default_user, limit, invalid_nicknames
|
||||
from config import root, banner_dir
|
||||
from config import app
|
||||
|
||||
from util import render, setup, header, motd, get_login, check_user
|
||||
from util import get_boards, get_posts, get_random, get_nav, get_files
|
||||
from util import get_description, get_style, get_title, get_user
|
||||
|
||||
|
||||
# INDEX
|
||||
@app.route("/", "GET")
|
||||
def index(db):
|
||||
setup(db)
|
||||
user = get_user(db)
|
||||
boards=get_boards(db)
|
||||
|
||||
format = {}
|
||||
format['title'] = "Hidden Boards"
|
||||
format['style'] = get_style()
|
||||
format['header'] = header(db)
|
||||
format['login'] = get_login(user)
|
||||
format['boards'] = boards
|
||||
format['motd'] = motd()
|
||||
return index_page.render(**format)
|
||||
|
||||
# LIBRARY
|
||||
@app.route("/.library/page/<page:int>/", "GET")
|
||||
@app.route("/.library/page/<page:int>", "GET")
|
||||
@app.route("/.library/", "GET")
|
||||
@app.route("/.library", "GET")
|
||||
def library_index(db,page=1):
|
||||
setup(db)
|
||||
user = get_user(db)
|
||||
board = "library"
|
||||
t = ["application/pdf"]
|
||||
files = get_files(db, t,page=page)
|
||||
if not files and page != 1:
|
||||
abort(404)
|
||||
format = {}
|
||||
format['title'] = "LIBRARY"
|
||||
format['style'] = get_style()
|
||||
format['header'] = header(db)
|
||||
format['description'] = get_description(db,board)
|
||||
format['user'] = user or default_user
|
||||
format['login'] = get_login(user)
|
||||
format['board'] = board
|
||||
format['posts'] = files
|
||||
format['nav'] = get_nav(db,board,t=t)
|
||||
return collection_page.render(**format)
|
||||
|
||||
# LIBRARY POST
|
||||
@app.route("/.library/page/<page:int>/", "POST")
|
||||
@app.route("/.library/page/<page:int>", "POST")
|
||||
@app.route("/.library/", "POST")
|
||||
@app.route("/.library", "POST")
|
||||
def library_post(db):
|
||||
abort(406)
|
||||
|
||||
# GALLERY
|
||||
@app.route("/.gallery/page/<page:int>/", "GET")
|
||||
@app.route("/.gallery/page/<page:int>", "GET")
|
||||
@app.route("/.gallery/", "GET")
|
||||
@app.route("/.gallery", "GET")
|
||||
def gallery_get(db,page=1):
|
||||
setup(db)
|
||||
user = get_user(db)
|
||||
board = "gallery"
|
||||
t = ["image/gif","image/png","image/jpeg"]
|
||||
files = get_files(db, t,page=page)
|
||||
if not files and page != 1:
|
||||
abort(404)
|
||||
format = {}
|
||||
format['title'] = "GALLERY"
|
||||
format['style'] = get_style()
|
||||
format['header'] = header(db)
|
||||
format['description'] = get_description(db,board)
|
||||
format['user'] = user or default_user
|
||||
format['login'] = get_login(user)
|
||||
format['board'] = board
|
||||
format['posts'] = files
|
||||
format['nav'] = get_nav(db,board,t=t)
|
||||
return collection_page.render(**format)
|
||||
|
||||
# GALLERY POST
|
||||
@app.route("/.gallery/page/<page:int>/", "POST")
|
||||
@app.route("/.gallery/page/<page:int>", "POST")
|
||||
@app.route("/.gallery/", "POST")
|
||||
@app.route("/.gallery", "POST")
|
||||
def gallery_post(db):
|
||||
abort(406)
|
||||
|
||||
# RANDOM
|
||||
@app.route("/.random/page/<page:int>/", "POST")
|
||||
@app.route("/.random/page/<page:int>", "POST")
|
||||
@app.route("/.random/", "POST" )
|
||||
@app.route("/.random", "POST")
|
||||
def random_post(page=None):
|
||||
abort(406)
|
||||
|
||||
@app.route("/.random/page/<page:int>/", "GET")
|
||||
@app.route("/.random/page/<page:int>", "GET")
|
||||
def random_pages(page):
|
||||
abort(404)
|
||||
|
||||
|
||||
@app.route("/.random/", "GET" )
|
||||
@app.route("/.random", "GET")
|
||||
def random_get(db):
|
||||
board="random"
|
||||
setup(db,board)
|
||||
user = get_user(db)
|
||||
posts = get_random(db)
|
||||
format = {}
|
||||
format['title'] = board.upper()
|
||||
format['style'] = get_style()
|
||||
format['header'] = header(db)
|
||||
format['description'] = get_description(db,board)
|
||||
format['user'] = user or default_user
|
||||
format['login'] = get_login(user)
|
||||
format['board'] = board
|
||||
format['posts'] = posts
|
||||
return random_page.render(**format)
|
||||
|
||||
|
||||
# BOARD
|
||||
@app.route("/.<board:re:[a-z-]+>/page/<page:int>/", "GET")
|
||||
@app.route("/.<board:re:[a-z-]+>/page/<page:int>", "GET")
|
||||
@app.route("/.<board:re:[a-z-]+>/", "GET")
|
||||
@app.route("/.<board:re:[a-z-]+>", "GET")
|
||||
def board_index(db,board,page=1):
|
||||
setup(db,board)
|
||||
user = get_user(db)
|
||||
posts = get_posts(db,board,page)
|
||||
if page != 1 and not posts:
|
||||
abort(404)
|
||||
format = {}
|
||||
format['title'] = board.upper()
|
||||
format['style'] = get_style()
|
||||
format['header'] = header(db)
|
||||
format['description'] = get_description(db,board)
|
||||
format['user'] = user or default_user
|
||||
format['login'] = get_login(user)
|
||||
format['board'] = board
|
||||
format['posts'] = posts
|
||||
format['nav'] = get_nav(db,board)
|
||||
return board_page.render(**format)
|
||||
|
||||
@app.route("/.<board:re:[a-z]+>/page/<page:int>/", "POST")
|
||||
@app.route("/.<board:re:[a-z]+>/page/<page:int>", "POST")
|
||||
@app.route("/.<board:re:[a-z]+>/", "POST")
|
||||
@app.route("/.<board:re:[a-z]+>", "POST")
|
||||
def post_board(db,board,page=None):
|
||||
setup(db,board)
|
||||
user = get_user(db)
|
||||
content = request.forms.get("content") or ""
|
||||
title = request.forms.get("title")
|
||||
author = user or default_user
|
||||
files = request.files.getall("files")
|
||||
time = datetime.datetime.now() + datetime.timedelta(hours=3)
|
||||
time = time.timestamp()
|
||||
c = db.execute(f"INSERT INTO POSTS (SUBJECT,AUTHOR,COMMENT,TIME,LAST,BOARD) values (?,?,?,?,?,?);", [title,author,content,time,time,board])
|
||||
db.execute("UPDATE BOARDS SET LAST = ? WHERE NAME = ?;",[time, board])
|
||||
db.execute(f"UPDATE POSTS SET POST = {c.lastrowid} WHERE ID = {c.lastrowid};")
|
||||
if db.execute("SELECT HIDDEN FROM BOARDS WHERE NAME = ?;",[board]).fetchone()[0]:
|
||||
hide = 1
|
||||
else:
|
||||
hide = 0
|
||||
for file in files:
|
||||
ext = "." + file.filename.split(".")[-1]
|
||||
if ext == "."+file.filename:
|
||||
ext = ""
|
||||
f = file.file.read()
|
||||
fname = f"{md5(f).hexdigest()}{ext}"
|
||||
|
||||
db.execute("INSERT INTO FILES (NAME,POST,TYPE,FILE,HIDDEN) VALUES (?,?,?,?,?);",[fname,c.lastrowid,file.content_type,f,hide])
|
||||
redirect(f"/.{board}/thread/{c.lastrowid}")
|
||||
|
||||
# THREAD
|
||||
@app.route("/.<board:re:[a-z]+>/thread/<id:int>/page/<page:int>/", "GET")
|
||||
@app.route("/.<board:re:[a-z]+>/thread/<id:int>/page/<page:int>", "GET")
|
||||
@app.route("/.<board:re:[a-z]+>/thread/<id:int>/", "GET")
|
||||
@app.route("/.<board:re:[a-z]+>/thread/<id:int>", "GET")
|
||||
def thread_get(db,board,id,page=1):
|
||||
setup(db,board)
|
||||
user = get_user(db)
|
||||
posts = get_posts(db,id=id,page=page)
|
||||
if not posts:
|
||||
abort(404)
|
||||
format = {}
|
||||
format['login'] = get_login(user)
|
||||
format['title'] = get_title(db,id)
|
||||
format['style'] = get_style()
|
||||
format['header'] = header(db)
|
||||
format['description'] = get_description(db,board)
|
||||
format['user'] = user or default_user
|
||||
format['id'] = id
|
||||
format['board'] = board
|
||||
format['posts'] = posts
|
||||
format['nav'] = get_nav(db,board,id=id)
|
||||
return thread_page.render(**format)
|
||||
|
||||
@app.route("/.<board:re:[a-z]+>/thread/<id:int>/page/<page:int>/", "POST")
|
||||
@app.route("/.<board:re:[a-z]+>/thread/<id:int>/page/<page:int>", "POST")
|
||||
@app.route("/.<board:re:[a-z]+>/thread/<id:int>/", "POST")
|
||||
@app.route("/.<board:re:[a-z]+>/thread/<id:int>", "POST")
|
||||
def thread_page_post(db,board,id,page=1):
|
||||
setup(db,board)
|
||||
user = get_user(db)
|
||||
author = user or default_user
|
||||
content = request.forms.get("content") or ""
|
||||
files = request.files.getall("files")
|
||||
time = datetime.datetime.now() + datetime.timedelta(hours=3)
|
||||
time = time.timestamp()
|
||||
c = db.execute(f"INSERT INTO POSTS (AUTHOR,COMMENT,TIME,BOARD,POST) values (?,?,?,?,?);", [author,content,time,board,id])
|
||||
db.execute("UPDATE BOARDS SET LAST = ? WHERE NAME = ?;",[time, board])
|
||||
db.execute("UPDATE POSTS SET LAST = ? WHERE ID = ?;",[time,id])
|
||||
if db.execute("SELECT HIDDEN FROM BOARDS WHERE NAME = ?;",[board]).fetchone()[0]:
|
||||
hide = 1
|
||||
else:
|
||||
hide = 0
|
||||
for file in files:
|
||||
ext = "." + file.filename.split(".")[-1]
|
||||
if ext == "."+file.filename:
|
||||
ext = ""
|
||||
f = file.file.read()
|
||||
fname = f"{md5(f).hexdigest()}{ext}"
|
||||
db.execute("INSERT INTO FILES (NAME,POST,TYPE,FILE,HIDDEN) VALUES (?,?,?,?,?);",[fname,c.lastrowid,file.content_type,f,hide])
|
||||
count = db.execute("SELECT COUNT(*) FROM POSTS WHERE POST = ?",[id]).fetchone()[0]
|
||||
if count < limit:
|
||||
redirect(f"/.{board}/thread/{id}#{c.lastrowid}")
|
||||
else:
|
||||
m = (count % limit)
|
||||
p = (count - m) / limit
|
||||
p = int(p) + (1 if m != 0 else 0)
|
||||
redirect(f"/.{board}/thread/{id}/page/{p}#{c.lastrowid}")
|
||||
|
||||
# USER
|
||||
@app.route("/u/<username:re:[a-zA-Z][a-zA-Z0-9-_\.]{1,23}>/", "GET")
|
||||
@app.route("/u/<username:re:[a-zA-Z][a-zA-Z0-9-_\.]{1,23}>", "GET")
|
||||
def user_index(db,username):
|
||||
setup(db)
|
||||
user = get_user(db)
|
||||
u = db.execute("SELECT * FROM USERS WHERE USERNAME LIKE ?;",[username]).fetchone()
|
||||
if not u:
|
||||
abort(404)
|
||||
page = render(u['PAGE'] or "")
|
||||
owner = user == u["USERNAME"]
|
||||
format = {}
|
||||
if owner:
|
||||
format['login'] = signout_link
|
||||
else:
|
||||
format['login'] = get_login(user)
|
||||
format['header'] = header(db)
|
||||
format['page'] = user_content.render(page=page or default_content)
|
||||
if owner:
|
||||
format['page'] += user_form.render(page=u["PAGE"] or "")
|
||||
format['style'] = get_style()
|
||||
format['user'] = user or default_user
|
||||
format['title'] = u["USERNAME"]
|
||||
return user_page.render(**format)
|
||||
|
||||
@app.route("/u/<username:re:[a-zA-Z][a-zA-Z0-9-_\.]{1,23}>/", "POST")
|
||||
@app.route("/u/<username:re:[a-zA-Z][a-zA-Z0-9-_\.]{1,23}>", "POST")
|
||||
def user_index(db,username):
|
||||
setup(db)
|
||||
user = get_user(db)
|
||||
u = db.execute("SELECT * FROM USERS WHERE USERNAME LIKE ?;",[username]).fetchone()
|
||||
if not u:
|
||||
abort(404)
|
||||
owner = user == u["USERNAME"]
|
||||
if not owner:
|
||||
abort(401)
|
||||
content = request.forms.getunicode("content")
|
||||
db.execute("UPDATE USERS SET PAGE = ? WHERE USERNAME LIKE ?;", [content, user])
|
||||
redirect(f"/u/{user}")
|
||||
|
||||
|
||||
@app.route("/u/Anonymous/")
|
||||
@app.route("/u/Anonymous")
|
||||
def anonymous():
|
||||
abort(406)
|
||||
|
||||
# LOGIN
|
||||
|
||||
@app.route("/login", "GET")
|
||||
def login_get(db):
|
||||
setup(db)
|
||||
user = get_user(db)
|
||||
format = {}
|
||||
format['nickname'] = ""
|
||||
format['header'] = header(db)
|
||||
format['style'] = get_style()
|
||||
format['user'] = user or default_user
|
||||
format['error'] = ""
|
||||
return signin_page.render(**format)
|
||||
|
||||
@app.route("/login", "POST")
|
||||
def login_post(db):
|
||||
setup(db)
|
||||
user = get_user(db)
|
||||
nickname = request.forms.getunicode("user")
|
||||
password = request.forms.getunicode("password")
|
||||
|
||||
if not check_user.match(nickname):
|
||||
format = {}
|
||||
format['nickname'] = nickname
|
||||
format['header'] = header(db)
|
||||
format['style'] = get_style()
|
||||
format['user'] = user or default_user
|
||||
format['error'] = "Invalid Nickname!"
|
||||
return signin_page.render(**format)
|
||||
|
||||
u = db.execute("SELECT * FROM USERS WHERE USERNAME LIKE ?;",[nickname]).fetchone()
|
||||
|
||||
if not u:
|
||||
format = {}
|
||||
format['nickname'] = nickname
|
||||
format['header'] = header(db)
|
||||
format['style'] = get_style()
|
||||
format['user'] = user or default_user
|
||||
format['error'] = "Nickname doesn't exists!"
|
||||
return signin_page.render(**format)
|
||||
|
||||
#time = datetime.datetime.fromtimestamp(u['time'])
|
||||
hash = md5((password+str(u['TIME'])).encode()).hexdigest()
|
||||
if hash == u['PASSWORD']:
|
||||
session_id = str(uuid.uuid4())
|
||||
time = datetime.datetime.now() + datetime.timedelta(hours=3)
|
||||
response.set_cookie("session_id", session_id, secret=secret)
|
||||
db.execute("INSERT INTO SESSIONS(USERNAME, TIME, KEY) VALUES (?,?,?) ON CONFLICT (USERNAME) DO UPDATE SET TIME = ?, KEY = ? ;",[nickname, time.timestamp(), session_id,time.timestamp(), session_id])
|
||||
redirect(f"/u/{nickname}")
|
||||
else:
|
||||
format = {}
|
||||
format['nickname'] = nickname
|
||||
format['header'] = header(db)
|
||||
format['style'] = get_style()
|
||||
format['user'] = user or default_user
|
||||
format['error'] = "Wrong password!"
|
||||
return signin_page.render(**format)
|
||||
|
||||
# LOGOUT
|
||||
|
||||
@app.route("/logout", "GET")
|
||||
def logout_get(db):
|
||||
setup(db)
|
||||
user = get_user(db)
|
||||
if not user:
|
||||
abort(401)
|
||||
db.execute("DELETE FROM SESSIONS WHERE USERNAME = ?",[user])
|
||||
redirect("/")
|
||||
|
||||
|
||||
# SIGNUP
|
||||
|
||||
@app.route("/signup", "GET")
|
||||
def signup(db):
|
||||
setup(db)
|
||||
user = get_user(db)
|
||||
|
||||
format = {}
|
||||
format['nickname'] = ""
|
||||
format['header'] = header(db)
|
||||
format['style'] = get_style()
|
||||
format['user'] = user or default_user
|
||||
format['error'] = ""
|
||||
return signup_page.render(**format)
|
||||
|
||||
@app.route("/signup", "POST")
|
||||
def signup(db):
|
||||
setup(db)
|
||||
user = get_user(db)
|
||||
|
||||
nickname = request.forms.getunicode("user")
|
||||
password = request.forms.getunicode("password")
|
||||
confirmation = request.forms.getunicode("confirmation")
|
||||
|
||||
format = {}
|
||||
format['nickname'] = nickname
|
||||
format['header'] = header(db)
|
||||
format['style'] = get_style()
|
||||
format['user'] = user or default_user
|
||||
|
||||
if not check_user.match(nickname):
|
||||
format['error'] = "Invalid Nickname!"
|
||||
return signup_page.render(**format)
|
||||
|
||||
if db.execute("SELECT * FROM USERS WHERE USERNAME LIKE ?;",[nickname]).fetchone() or nickname.lower() in invalid_nicknames:
|
||||
format['error'] = "Nickname already in use!"
|
||||
return signup_page.render(**format)
|
||||
|
||||
if password != confirmation:
|
||||
format['error'] = "Passwords don't match!"
|
||||
return signup_page.render(**format)
|
||||
|
||||
time = datetime.datetime.now() + datetime.timedelta(hours=3)
|
||||
hash = md5((password+str(time.timestamp())).encode()).hexdigest()
|
||||
db.execute("INSERT INTO USERS (USERNAME, TIME, PASSWORD) VALUES (?,?,?);",[nickname,time.timestamp(),hash])
|
||||
session_id = str(uuid.uuid4())
|
||||
response.set_cookie("session_id", session_id, secret=secret)
|
||||
db.execute("INSERT INTO SESSIONS(USERNAME, TIME, KEY) VALUES (?,?,?) ON CONFLICT (USERNAME) DO UPDATE SET TIME = ?, KEY = ? ;",[nickname, time.timestamp(), session_id,time.timestamp(), session_id])
|
||||
redirect(f"/u/{nickname}")
|
||||
|
||||
# ERROR
|
||||
|
||||
@app.error(401)
|
||||
@app.error(404)
|
||||
@app.error(405)
|
||||
@app.error(406)
|
||||
@app.error(500)
|
||||
def error_redirect(error):
|
||||
return error_page.render(style=get_style(),error=error.status_code,header=motd())
|
||||
|
||||
|
||||
# FILES
|
||||
|
||||
@app.route("/files/<filename>/")
|
||||
@app.route("/files/<filename>")
|
||||
def files(db,filename):
|
||||
file = db.execute("SELECT * FROM FILES WHERE NAME = ?;",[filename]).fetchone()
|
||||
if not file:
|
||||
abort(404)
|
||||
response.set_header("content-type", file["TYPE"])
|
||||
return file["FILE"]
|
||||
|
||||
# THUMBS
|
||||
|
||||
@app.route("/thumbs/<filename>/")
|
||||
@app.route("/thumbs/<filename>")
|
||||
def thumbs(db,filename):
|
||||
file = db.execute("SELECT * FROM THUMBS WHERE NAME = ?;",[filename]).fetchone()
|
||||
if file:
|
||||
response.set_header("content-type", file["TYPE"])
|
||||
return file["FILE"]
|
||||
file = db.execute("SELECT * FROM FILES WHERE NAME = ?;",[filename]).fetchone()
|
||||
if not file:
|
||||
abort(404)
|
||||
if file["TYPE"] == "application/pdf":
|
||||
image = convert_from_bytes(file["FILE"], first_page=1,last_page=1,fmt="jpg")
|
||||
image = image[0]
|
||||
file = io.BytesIO()
|
||||
response.set_header("content-type", "image/jpeg")
|
||||
image.save(file,format='JPEG')
|
||||
db.execute("INSERT INTO THUMBS (NAME,TYPE,FILE) VALUES (?,?,?);",[filename,"image/jpeg",file.getvalue()])
|
||||
return file.getvalue()
|
||||
else:
|
||||
return static_file("thumbs/file.png",root=root)
|
||||
|
||||
# STATIC
|
||||
|
||||
# BANNER
|
||||
@app.route("/s/banner/", "GET")
|
||||
@app.route("/s/banner", "GET")
|
||||
def banner():
|
||||
b = random.choice(os.listdir(banner_dir))
|
||||
response = static_file(b,root=banner_dir)
|
||||
response.set_header('cache-control','no-cache, no-store, must-revalidate')
|
||||
return response
|
||||
|
||||
# ICON
|
||||
@app.route("/favicon.ico", "GET")
|
||||
def favicon():
|
||||
return static_file("static/favicon.ico",root=root)
|
||||
|
||||
# FONT
|
||||
@app.route("/s/font/", "GET")
|
||||
@app.route("/s/font", "GET")
|
||||
def font():
|
||||
return static_file('static/ForcedSquare.woff',root=root)
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,116 @@
|
|||
from bottle import SimpleTemplate
|
||||
from config import root
|
||||
|
||||
index_page = SimpleTemplate(open(f"{root}/html/index.html").read())
|
||||
board_page = SimpleTemplate(open(f"{root}/html/board.html").read())
|
||||
random_page = SimpleTemplate(open(f"{root}/html/random.html").read())
|
||||
collection_page = SimpleTemplate(open(f"{root}/html/collection.html").read())
|
||||
thread_page = SimpleTemplate(open(f"{root}/html/thread.html").read())
|
||||
user_page = SimpleTemplate(open(f"{root}/html/user.html").read())
|
||||
signin_page = SimpleTemplate(open(f"{root}/html/signin.html").read())
|
||||
signup_page = SimpleTemplate(open(f"{root}/html/signup.html").read())
|
||||
error_page = SimpleTemplate(open(f"{root}/html/error.html").read())
|
||||
|
||||
account = SimpleTemplate("""<a href="/u/{{!user}}"><b>{{!user}}</b></a>""")
|
||||
|
||||
board_box = SimpleTemplate("""
|
||||
<div class="block">
|
||||
<a href="/.{{board}}">{{board.upper()}}</a>
|
||||
<span style="float:right;"> |