first commit

This commit is contained in:
sML 2022-12-27 10:07:20 +01:00
commit be5bb8e131
10 changed files with 722 additions and 0 deletions

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# Intro
babil0nia-server (bserver) is the web gui for [bkli](https://code.lacashita.com/sml/bkli).
It can be used to save/organize your favourite urls, URLs are saved into the sqlite3 db and then you can do some
searchs, filtering or download the saved url to a file (jpg).
babil0nia-cli(bkli) and babil0nia-server(bserver) uses the same sqlite3 database, so if you are using both you can
share/sync the db file between them.
Check changelog.txt to see the current status.
# How it looks
![babil0nia-image](babil0nia.png)
# To know
Feel free to change the code in order to adapt the program to your needs.
During the first run the program:
* Creates ~/babil0nia.db file which is the database.
* Creates ~/.babil0niafiles which is the folder to store the screenshots.
* Creates a link from ~/.babil0niafiles to screenshots/ in the same folder where is the executable.
* The server uses the port 8002.
Check that you have permissions to run the program in the port 8001 and also write permissions in screenshots folder in order to
download the screenshots if needed.
Once started, you can access at http://127.0.0.1:8002.
# Usage
| Description | Command |
| ----------- | ----------- |
| Start the web server (without allowing to archive) | <kbd>bserver</kbd>|
| Start the web server (allowing to archive) | <kbd>bserver -a</kbd>|
# Installation
```sh
git clone https://code.lacashita.com/sml/bserver
go build bserver.go
./bserver.go -a
```
# Upgrade
Just clone the repo with the last version, compile and replace with the latest binary.

BIN
babil0nia.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
bserver Executable file

Binary file not shown.

406
bserver.go Executable file
View File

@ -0,0 +1,406 @@
package main
import (
"database/sql"
"fmt"
"html/template"
"log"
"net/http"
"os"
"os/exec"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
"context"
"io/ioutil"
"github.com/chromedp/chromedp"
"crypto/md5"
"encoding/hex"
)
// Start Web PART
type urlinfo struct {
Date string
Url string
Description string
Tags string
Screenshot string
Separated []string
Duplicated []string
}
type Message struct {
Msj string
}
func main() {
var filedb string
var homepath = os.Getenv("HOME")
if os.Getenv("babil0nia_DB") == "" {
filedb = homepath+"/babil0nia.db"
} else {
filedb = os.Getenv("babil0nia_DB")
}
// Create ~/.babil0niafiles folder to save screenshots
if _, err := os.Stat(homepath+"/.babil0niafiles"); os.IsNotExist(err) {
err = os.Mkdir(homepath+"/.babil0niafiles", 0755)
if err != nil {
log.Fatal(err)
}
}
// Link ~/.babil0niafiles to screenshots/ folder.
if _, err := os.Stat("screenshots"); os.IsNotExist(err) {
cmd := exec.Command("/usr/bin/ln", "-s", homepath+"/.babil0niafiles", "screenshots")
_, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
}
checkDB(filedb)
sqliteDatabase, _ := sql.Open("sqlite3", filedb)
defer sqliteDatabase.Close()
startServer(sqliteDatabase)
}
// Func to check if DB exists, if not create it.
func checkDB(filedb string) {
_, err := os.Stat(filedb)
//If file htask.db doesnt exist.
if err != nil {
fmt.Println("[+]Creating Database\n")
file, err := os.Create(filedb)
if err != nil {
log.Fatal(err.Error())
}
file.Close()
sqlDB, _ := sql.Open("sqlite3",filedb)
defer sqlDB.Close()
createTable(sqlDB)
}
}
// Func to create DB.
func createTable(db *sql.DB) {
createbabil0niatable := `CREATE TABLE babil0nia (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"date" TEXT,
"url" TEXT,
"description" TEXT,
"tags" TEXT,
"screenshot" TEXT
);`
statement, err := db.Prepare(createbabil0niatable)
if err != nil {
log.Fatal(err.Error())
}
statement.Exec()
log.Println("[+] babil0nia table created")
}
func startServer(db *sql.DB) {
// All requests to /screenshots/ will serve the static content of screenshots/
fs := http.FileServer(http.Dir("./screenshots"))
http.Handle("/screenshots/", http.StripPrefix("/screenshots/", fs))
http.HandleFunc("/", babil0niaHandler(db))
// All requests to /styles/css will serve static/css/bootstrap.min.css.
http.HandleFunc("/styles/css", func(response http.ResponseWriter, request *http.Request) {
http.ServeFile(response, request, "static/css/bootstrap.min.css")
})
http.HandleFunc("/add", func(response http.ResponseWriter, request *http.Request) {
http.ServeFile(response, request, "static/html/add.html")
})
http.ListenAndServe("127.0.0.1:8002", nil)
}
func babil0niaHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
enabled := len(os.Args)
var repetido int
tagtose := r.URL.Query()["tags"]
tagtosearch := strings.Join(tagtose ," ")
wordtose := r.URL.Query()["wordtosearch"]
wordtosearch := strings.Join(wordtose," ")
urltoa := r.URL.Query()["url"]
urltoadd := strings.Join(urltoa, " ")
descript := r.URL.Query()["description"]
descriptoinsert := strings.Join(descript," ")
repetido = dupURL(db,urltoadd)
urltodel := r.URL.Query()["delete"]
urltodelete := strings.Join(urltodel ," ")
urltoar := r.URL.Query()["archive"]
urltoarchive := strings.Join(urltoar ," ")
existeurl := dupURL(db,urltoarchive)
existdownload := checkDownload(db,urltoarchive)
if urltoarchive != "" && enabled > 1 && os.Args[1] == "-a" && existeurl != 0 && existdownload == ""{
var homepath = os.Getenv("HOME")
filename := getFilename(urltoarchive)
screenshotname := filename+".jpg"
screenshotfullpath := homepath+"/.babil0niafiles/"+filename+".jpg"
err, filename := archiveURL(db,urltoarchive,screenshotfullpath,w)
if err != nil {
fmt.Printf("ERROR UPDATING")
} else {
updateUrl(db,screenshotname,urltoarchive)
}
}
if urltoarchive != "" && enabled < 2 || urltoarchive != "" && enabled > 1 && os.Args[1] != "-a" {
m := Message{"Archive is disabled."}
printMessage(w,m)
}
if urltodelete != "" {
imageURLname := getImageName(db,urltodelete)
filetodelete := "screenshots/"+imageURLname
os.Remove(filetodelete)
deleteUrl(db,urltodelete)
}
if urltoadd == "" && descriptoinsert == "" {
displaybabil0nia(db, w,tagtosearch,wordtosearch)
} else {
if repetido != 0 {
m := Message{"URL is duplicated."}
printMessage(w,m)
} else {
insertUrl(db,urltoadd,descriptoinsert,tagtosearch)
m := Message{"URL added successfully."}
printMessage(w,m)
}
}
}
}
func displaybabil0nia(db *sql.DB, w http.ResponseWriter,tagtosearch string, wordtosearch string) {
var Arrayurlinfo []urlinfo
var Repetidos []string
tmpl := template.Must(template.ParseFiles("static/html/main.html"))
p := urlinfo{}
if tagtosearch == "" && wordtosearch == "" {
row, err := db.Query("SELECT date,url,description,tags,screenshot FROM babil0nia")
if err != nil {
log.Fatal(err)
}
defer row.Close()
for row.Next() {
var date string
var url string
var description string
var tags string
var screenshot string
row.Scan( &date, &url, &description, &tags, &screenshot)
// s is []string where tags are separated by ,
s := strings.Split(tags, ",")
//All s elementes are appended to Repetidos []string
for _,element := range s {
Repetidos = append(Repetidos,element)
}
p = urlinfo{date,url,description,tags,screenshot,s,nil}
Arrayurlinfo = append(Arrayurlinfo, p)
}
}
if tagtosearch != "" && wordtosearch == "" {
row, err := db.Query("SELECT date,url,description,tags,screenshot FROM babil0nia where tags like ?","%"+tagtosearch+"%")
if err != nil {
log.Fatal(err)
}
defer row.Close()
for row.Next() {
var date string
var url string
var description string
var tags string
var screenshot string
row.Scan( &date, &url, &description, &tags, &screenshot)
var s []string
s = strings.Split(tags, ",")
for _,element := range s {
Repetidos = append(Repetidos,element)
}
p = urlinfo{date,url,description,tags,screenshot,s,nil}
Arrayurlinfo = append(Arrayurlinfo, p)
}
}
if tagtosearch == "" && wordtosearch != "" {
row, err := db.Query("SELECT date,url,description,tags,screenshot FROM babil0nia where url like ? or description like ?","%"+wordtosearch+"%","%"+wordtosearch+"%")
if err != nil {
log.Fatal(err)
}
defer row.Close()
for row.Next() {
var date string
var url string
var description string
var tags string
var screenshot string
row.Scan( &date, &url, &description, &tags, &screenshot)
var s []string
s = strings.Split(tags, ",")
for _,element := range s {
Repetidos = append(Repetidos,element)
}
p = urlinfo{date,url,description,tags,screenshot,s,nil}
Arrayurlinfo = append(Arrayurlinfo, p)
}
}
//Repetidos store now []string without tags duplicated.
Repetidos = removeDuplicates(Repetidos)
// Creates the last urlinfo struct with Repetidos.
lastp := urlinfo{"","","","","",nil,Repetidos}
Arrayurlinfo = append(Arrayurlinfo,lastp)
tmpl.Execute(w,Arrayurlinfo)
}
// Func to Print Messages through Web.
func printMessage(w http.ResponseWriter, msg Message) {
tmpl := template.Must(template.ParseFiles("static/html/message.html"))
tmpl.Execute(w, msg)
}
// Inserting URL into SQL.
func insertUrl(db *sql.DB, url string, descriptoinsert string, tagstoinsert string) {
currentTime := time.Now()
date := currentTime.Format("01-02-2006")
insertURLSQL := `INSERT INTO babil0nia(date,url,description,tags) VALUES (?,?,?,?)`
statement, err := db.Prepare(insertURLSQL)
if err != nil {
log.Fatalln(err.Error())
}
_, err = statement.Exec(date, url,descriptoinsert, tagstoinsert)
if err != nil {
log.Fatalln(err.Error())
}
}
// Delete URL.
func deleteUrl(db *sql.DB, url string) {
deleteURLSQL := `DELETE FROM babil0nia WHERE url = ?`
statement, err := db.Prepare(deleteURLSQL)
if err != nil {
log.Fatalln(err.Error())
}
_, err = statement.Exec(url)
if err != nil {
log.Fatalln(err.Error())
}
}
// Func to update an URL to add the filename of the screenshot.
func updateUrl(db *sql.DB, filename string, url string) {
updateURLSQL := `UPDATE babil0nia set screenshot = ? WHERE url = ?`
statement, err := db.Prepare(updateURLSQL)
if err != nil {
log.Fatalln(err.Error())
}
_, err = statement.Exec(filename,url)
if err != nil {
log.Fatalln(err.Error())
}
}
// Func to check if URL is duplicated, it returns an int with the number of times that URL is repeated.
func dupURL(db *sql.DB, url string) int{
var total int
row, err := db.Query("SELECT count(*) as total from babil0nia where url = ?",url)
if err != nil {
log.Fatal(err)
}
defer row.Close()
for row.Next() {
var count int
row.Scan( &count)
total = count
}
return total
}
// Func to check if URL has the file downloaded.
func checkDownload(db *sql.DB, url string) string{
var total string
row, err := db.Query("SELECT screenshot from babil0nia where url = ?",url)
if err != nil {
log.Fatal(err)
}
defer row.Close()
for row.Next() {
var screenshot string
row.Scan( &screenshot)
total = screenshot
}
return total
}
// Func to get the "id" that will be used as imagename from an given URL, it returns the id.
func getImageName(db *sql.DB, url string) string{
var imagename string
row, err := db.Query("SELECT screenshot from babil0nia where url = ?",url)
if err != nil {
log.Fatal(err)
}
defer row.Close()
for row.Next() {
var screenshot string
row.Scan( &screenshot)
imagename = screenshot
}
return imagename
}
// Func to remove duplicated strings from an slice.
func removeDuplicates(strSlice []string) []string {
allKeys := make(map[string]bool)
list := []string{}
for _, item := range strSlice {
if _, value := allKeys[item]; !value {
allKeys[item] = true
list = append(list, item)
}
}
return list
}
// Func to create a filename using the md5 hash of the url name.
func getFilename(text string) string {
hash := md5.Sum([]byte(text))
return hex.EncodeToString(hash[:])
}
// Func to take one screenshot from the given URL and save it to a file. It returns error and the name of the saved (screenshot) file.
func archiveURL(db *sql.DB,urltoarchive string, filename string,w http.ResponseWriter) (error,string){
var fail error
ctx, cancel := chromedp.NewContext(
context.Background(),
)
defer cancel()
var buf []byte
if err := chromedp.Run(ctx, fullScreenshot(urltoarchive, 100, &buf)); err != nil {
m := Message{"Error downloading."}
printMessage(w,m)
fail = err
}
if fail != nil {
fmt.Println("Fallo la descarga de la imagen asi que nos e guarda")
} else {
if err := ioutil.WriteFile(filename, buf, 0o644); err != nil {
m := Message{"Error saving the image."}
printMessage(w,m)
fail = err
}
}
return fail, filename
}
func fullScreenshot(urlstr string, quality int, res *[]byte) chromedp.Tasks {
return chromedp.Tasks{
chromedp.Navigate(urlstr),
chromedp.FullScreenshot(res, quality),
}
}

16
go.mod Executable file
View File

@ -0,0 +1,16 @@
module spiderimage
go 1.18
require (
github.com/chromedp/cdproto v0.0.0-20220725225757-5988d9195a6c // indirect
github.com/chromedp/chromedp v0.8.3 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-sqlite3 v1.14.8 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
)

21
go.sum Executable file
View File

@ -0,0 +1,21 @@
github.com/chromedp/cdproto v0.0.0-20220725225757-5988d9195a6c h1:Gm+DujZPVAtQNTLhbg5PExjRNfhdTCSMLvJ/pFfY4aY=
github.com/chromedp/cdproto v0.0.0-20220725225757-5988d9195a6c/go.mod h1:5Y4sD/eXpwrChIuxhSr/G20n9CdbCmoerOHnuAf0Zr0=
github.com/chromedp/chromedp v0.8.3 h1:UwOY+fhC5Vv3uKgRpnvilCbWs/QPz8ciFwRB0q6pH8k=
github.com/chromedp/chromedp v0.8.3/go.mod h1:9YfKSJnBNeP77vKecv+DNx2/Tcb+6Gli0d1aZPw/xbk=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

7
static/css/bootstrap.min.css vendored Executable file

File diff suppressed because one or more lines are too long

61
static/html/add.html Executable file
View File

@ -0,0 +1,61 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="styles/css">
<title>babil0nia</title>
<style>
body {
background-color: #FFFDFA;
}
input[type="text"], textarea {
background-color : #FFFDFA;
}
.form-control-sm {
font-family: inherit;
width: 100%;
border: 0;
border-bottom: 2px solid black;
outline: 0;
font-size: 1.3rem;
padding: 7px 0;
background: transparent;
transition: border-color 0.2s;
}
</style>
</head>
<body>
<div class="container">
<div class="d-flex flex-column min-vh-100 justify-content-center align-items-center">
<h1 class="py-5">babil0nia</h1>
<form action="/">
<div class="row">
<div class="col-7">
<input class="form-control-sm" type="text" autocomplete="off" id="url" name="url" placeholder="URL"><br><br>
</div>
<div class="col-5">
<input class="form-control-sm" type="text" autocomplete="off" id="tags" name="tags" placeholder="tags"><br><br>
</div>
</div>
<div class="row py-3">
<textarea class="form-control-sm" rows="1" id="description" name="description" placeholder="Description"></textarea><br><br>
<div class="py-3">
<input type="submit" class="btn btn-outline-dark" value="Submit">
</div>
</div>
</form>
</div>
</div>
</body>
</html>

134
static/html/main.html Executable file
View File

@ -0,0 +1,134 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="styles/css">
<title>babil0nia</title>
<style>
body {
background-color: #FFFDFA;
padding-top: 100px;
font-family: "Ubuntu Mono", monospace;
}
input[type="text"] {
background-color : #FFFDFA;
}
.fixed-top {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 1030;
}
.card-group {
margin-right:5%;
}
.form-control-sm {
font-family: inherit;
width: 100%;
border: 0;
border-bottom: 2px solid black;
outline: 0;
font-size: 1.3rem;
padding: 7px 0;
background: transparent;
transition: border-color 0.2s;
}
</style>
</head>
<body>
<nav class="navbar navbar-light fixed-top navbar-masthead">
<div class="container-fluid">
<a class="navbar-brand">
<form action="/add">
<input type="submit" class="btn btn-outline-dark" value="+ Add" />
</form>
</a>
<h1>babil0nia</h1>
</div>
</nav>
<div class="d-flex align-items-center">
<div class="container py-5">
<div class="row">
<div class="col">
<form class="d-flex" action="/">
<input class="form-control-sm me-2" autocomplete="off" type="text" name="wordtosearch" id="wordtosearch" placeholder="Search" aria-label="Search">
</form>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-center">
<div class="container">
<div class="row">
<div class="col-10">
{{range .}}
<div>
<span class="fw-light">{{.Date}}</span> <a target="_blank" href="{{.Url}}">{{.Url}}</a>
{{range .Separated}}
<span class="badge bg-light"><a href="?tags={{.}}">{{.}}</a></span>
{{end}}
{{ if eq .Url "" }}
{{ else }}
<a href="/?delete={{.Url}}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg></a>
{{ if eq .Screenshot "" }}
<a href="/?archive={{.Url}}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-arrow-down" viewBox="0 0 16 16">
<path d="M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293V6.5z"/>
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
</svg></a>
{{ else }}
<a target="_blank" href="screenshots/{{.Screenshot}}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-card-image" viewBox="0 0 16 16">
<path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
<path d="M1.5 2A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-13zm13 1a.5.5 0 0 1 .5.5v6l-3.775-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12v.54A.505.505 0 0 1 1 12.5v-9a.5.5 0 0 1 .5-.5h13z"/>
</svg></a>
{{end}}
{{end}}
{{ if eq .Description "" }}
{{ else }}
<p class="py-2">{{.Description}}</p>
<hr>
{{end}}
</div>
{{end}}
</div>
<div class="col">
<h6>Tags</h6>
{{range .}}
{{range .Duplicated}}
<a href="?tags={{.}}"><span class="badge bg-dark">{{.}}</a></span>
{{end}}
{{end}}
</div>
</div>
</div>
</div>
</body>
</html>

31
static/html/message.html Executable file
View File

@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="styles/css">
<title>babil0nia Message</title>
<style>
body {
background-color: #FFFDFA;
}
</style>
</head>
<body>
<div class="d-flex align-items-center hv-100 vh-100">
<div class="container text-center">
<div>{{.Msj}}</div>
<a class="my-3 badge rounded-pill bg-dark" href="/">Back</a>
</div>
</div>
</body>
</html>