new
commit
1d1173320b
|
@ -0,0 +1,2 @@
|
||||||
|
database/scrapyard.db
|
||||||
|
config/config.json
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,333 @@
|
||||||
|
// colors
|
||||||
|
$white-main: #DEDEDF
|
||||||
|
$accent-main: rgb(255, 51, 255)
|
||||||
|
$gray-main: #696969
|
||||||
|
$gray-dull: #141414
|
||||||
|
$card-color: #080808
|
||||||
|
$red-color: rgb(143, 2, 2)
|
||||||
|
|
||||||
|
// sizes
|
||||||
|
// Ширина
|
||||||
|
$main-width: 48vw
|
||||||
|
// Отступ от боков экрана
|
||||||
|
$main-offset: 25vw
|
||||||
|
// Отступы между абзацами
|
||||||
|
$div-offset: 1vw
|
||||||
|
|
||||||
|
// mixins
|
||||||
|
// Ширина обводки, цвет овбодки, радиус
|
||||||
|
$border-border-radius($a, $b, $c)
|
||||||
|
border: $a solid $b
|
||||||
|
border-radius: $c
|
||||||
|
// color, background-color
|
||||||
|
$color($a, $b)
|
||||||
|
color: $a
|
||||||
|
background-color: $b
|
||||||
|
// верх, право, низ, лево
|
||||||
|
$margin($a, $b, $c, $d)
|
||||||
|
margin: $a $b $c $d
|
||||||
|
// ширина, высота
|
||||||
|
$width-height($a, $b)
|
||||||
|
width: $a
|
||||||
|
height: $b
|
||||||
|
// justify, align
|
||||||
|
$flex-justify-align($a, $b)
|
||||||
|
display: flex
|
||||||
|
justify-content: $a
|
||||||
|
align-items: $b
|
||||||
|
// display flex + column dir
|
||||||
|
$flex-column()
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
// изменение цвета бордера при нажатии
|
||||||
|
$focus-input-border()
|
||||||
|
transition: border .3s ease
|
||||||
|
&:focus
|
||||||
|
border: .1vw solid $accent-main
|
||||||
|
outline: none
|
||||||
|
// изменение цвета при наведении
|
||||||
|
$hover-color($color)
|
||||||
|
transition: color .3s ease
|
||||||
|
&:hover
|
||||||
|
color: $color
|
||||||
|
|
||||||
|
.accent
|
||||||
|
color: $accent-main
|
||||||
|
.accent-dull
|
||||||
|
opacity: .8
|
||||||
|
color: $accent-main
|
||||||
|
.gray
|
||||||
|
color: $gray-main
|
||||||
|
.card-color
|
||||||
|
color: $card-color
|
||||||
|
.gray-dull
|
||||||
|
color: $gray-dull
|
||||||
|
.red
|
||||||
|
color: $red-color
|
||||||
|
|
||||||
|
// тут почему то не работает соответствующий миксин
|
||||||
|
::selection
|
||||||
|
color: white
|
||||||
|
background-color: rgba(255, 51, 255, .7)
|
||||||
|
|
||||||
|
// устанавливает отступы для всех вложенный div'ов
|
||||||
|
#div-offset
|
||||||
|
div
|
||||||
|
margin-top: $div-offset
|
||||||
|
|
||||||
|
a
|
||||||
|
cursor: pointer
|
||||||
|
color: $white-main
|
||||||
|
text-decoration: none
|
||||||
|
&:hover
|
||||||
|
text-decoration: none
|
||||||
|
|
||||||
|
input
|
||||||
|
font-size: 1vw
|
||||||
|
width: 10vw
|
||||||
|
$border-border-radius(.1vw, $gray-dull, .3vw)
|
||||||
|
$color($white-main, $card-color)
|
||||||
|
$focus-input-border()
|
||||||
|
&[type="file"]
|
||||||
|
display: none
|
||||||
|
|
||||||
|
textarea
|
||||||
|
$border-border-radius(.1vw, $gray-dull, .3vw)
|
||||||
|
padding: .5vw
|
||||||
|
$color($white-main, $card-color)
|
||||||
|
font-size: 1vw
|
||||||
|
margin-top: 1vw
|
||||||
|
resize: vertical
|
||||||
|
width: 100%
|
||||||
|
$focus-input-border()
|
||||||
|
|
||||||
|
button
|
||||||
|
padding: .2vw
|
||||||
|
$border-border-radius(.1vw, $gray-main, .3vw)
|
||||||
|
font-size: 1vw
|
||||||
|
$color($white-main, $card-color)
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
.button
|
||||||
|
padding: .2vw
|
||||||
|
$border-border-radius(.1vw, $gray-main, .3vw)
|
||||||
|
font-size: 1vw
|
||||||
|
$color($white-main, $card-color)
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
body
|
||||||
|
$color($white-main, #000)
|
||||||
|
font-size: 1vw
|
||||||
|
margin: 0
|
||||||
|
font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif
|
||||||
|
$flex-column()
|
||||||
|
position: relative
|
||||||
|
overflow-x: hidden
|
||||||
|
|
||||||
|
@keyframes blink
|
||||||
|
0%, 100%
|
||||||
|
opacity: 1
|
||||||
|
50%
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
.content
|
||||||
|
position: relative
|
||||||
|
$margin(0, $main-offset, 2vw, $main-offset)
|
||||||
|
width: $main-width
|
||||||
|
flex: 1
|
||||||
|
.main-content
|
||||||
|
position: relative
|
||||||
|
width: 100%
|
||||||
|
margin-right: 0vw
|
||||||
|
box-sizing: border-box
|
||||||
|
.tags
|
||||||
|
color: $gray-main
|
||||||
|
font-weight: 700
|
||||||
|
margin-bottom: 1vw
|
||||||
|
.post
|
||||||
|
position: relative
|
||||||
|
$border-border-radius(.1vw, $gray-dull, 1vw)
|
||||||
|
background-color: $card-color
|
||||||
|
padding: 1vw
|
||||||
|
$margin(1vw, 0, 1vw, 0)
|
||||||
|
text-decoration: none
|
||||||
|
&:hover::after, &:hover::before
|
||||||
|
content: ''
|
||||||
|
opacity: .5
|
||||||
|
position: absolute
|
||||||
|
top: 50%
|
||||||
|
transform: translateY(-50%)
|
||||||
|
border-width: 2vw
|
||||||
|
border-style: solid
|
||||||
|
&:hover::after
|
||||||
|
left: 100%
|
||||||
|
border-color: transparent $accent-main transparent transparent
|
||||||
|
&:hover::before
|
||||||
|
right: 100%
|
||||||
|
border-color: transparent transparent transparent $accent-main
|
||||||
|
.date
|
||||||
|
border-top: 0.17vw dotted $gray-main
|
||||||
|
$margin(.2vw, 0, 1vw, 0)
|
||||||
|
padding-top: 1vw
|
||||||
|
color: $gray-main
|
||||||
|
.title
|
||||||
|
border-bottom: 0.17vw dotted $gray-main
|
||||||
|
padding-bottom: 1vw
|
||||||
|
font-size: 1.7vw
|
||||||
|
font-weight: 700
|
||||||
|
.body
|
||||||
|
margin-bottom: 0
|
||||||
|
.read-other-posts
|
||||||
|
color: $white-main
|
||||||
|
text-align: center
|
||||||
|
margin-top: 2vw
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
#right-border
|
||||||
|
flex-grow: 1
|
||||||
|
border-bottom: 0.1vw solid $white-main
|
||||||
|
margin-right: 1vw
|
||||||
|
#left-border
|
||||||
|
flex-grow: 1
|
||||||
|
border-bottom: 0.1vw solid $white-main
|
||||||
|
margin-left: 1vw
|
||||||
|
.next-post
|
||||||
|
color: $white-main
|
||||||
|
text-align: center
|
||||||
|
margin-top: 2vw
|
||||||
|
$flex-justify-align(center, center)
|
||||||
|
.content-control
|
||||||
|
margin-bottom: 1vw
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.3vw
|
||||||
|
.post-header
|
||||||
|
text-decoration: none
|
||||||
|
overflow-wrap: break-word
|
||||||
|
border: none
|
||||||
|
padding-bottom: 1vw
|
||||||
|
margin-bottom: 0
|
||||||
|
.post.red-border
|
||||||
|
border: .1vw solid $accent-main
|
||||||
|
.mail
|
||||||
|
overflow: hidden
|
||||||
|
$border-border-radius(.1vw, $gray-dull, 1vw)
|
||||||
|
background-color: $card-color
|
||||||
|
display: flex
|
||||||
|
$width-height(100%, 5vw)
|
||||||
|
box-sizing: border-box
|
||||||
|
button
|
||||||
|
padding: 1vw
|
||||||
|
font-size: 2vw
|
||||||
|
color: $white-main
|
||||||
|
border: none
|
||||||
|
$hover-color($gray-main)
|
||||||
|
textarea
|
||||||
|
border: none
|
||||||
|
padding: .7vw
|
||||||
|
$color($white-main, transparent)
|
||||||
|
font-size: .8vw
|
||||||
|
margin-top: 0
|
||||||
|
resize: none
|
||||||
|
$width-height(100%, 100%)
|
||||||
|
form
|
||||||
|
width: 100%
|
||||||
|
display: flex
|
||||||
|
.footer
|
||||||
|
color: $gray-main
|
||||||
|
text-align: center
|
||||||
|
margin-top: 2vw
|
||||||
|
.post-container
|
||||||
|
$border-border-radius(.1vw, $gray-dull, 1vw)
|
||||||
|
background-color: $card-color
|
||||||
|
margin-bottom: 1vw
|
||||||
|
padding: 1vw
|
||||||
|
.pagination
|
||||||
|
$flex-justify-align(center, center)
|
||||||
|
margin-top: 2vw
|
||||||
|
gap: 5vw
|
||||||
|
.page-name
|
||||||
|
box-sizing: border-box
|
||||||
|
width: 100%
|
||||||
|
$border-border-radius(.2vw, $gray-dull, .5vw)
|
||||||
|
overflow-wrap: break-word
|
||||||
|
$margin(1vw, 0, 1vw, 0)
|
||||||
|
padding: 1vw
|
||||||
|
font-size: 1.7vw
|
||||||
|
.cursor
|
||||||
|
display: inline-block
|
||||||
|
animation: blink 2s step-end infinite
|
||||||
|
|
||||||
|
.navbar
|
||||||
|
position: relative
|
||||||
|
$margin(1vw, $main-offset, 0vw, $main-offset)
|
||||||
|
$flex-justify-align(center, center)
|
||||||
|
width: $main-width
|
||||||
|
gap: 1vw
|
||||||
|
.button
|
||||||
|
position: relative
|
||||||
|
$color($gray-main, $card-color)
|
||||||
|
$border-border-radius(.1vw, $gray-dull, .3vw)
|
||||||
|
padding: .7vw
|
||||||
|
$width-height(auto, auto)
|
||||||
|
$hover-color($white-main)
|
||||||
|
&::before
|
||||||
|
content: ''
|
||||||
|
position: absolute
|
||||||
|
top: 50%
|
||||||
|
$width-height($main-width, .1vw)
|
||||||
|
background-color: $gray-dull
|
||||||
|
|
||||||
|
.logo
|
||||||
|
display: flex
|
||||||
|
$margin(2vw, $main-offset, 0vw, $main-offset)
|
||||||
|
width: $main-width
|
||||||
|
gap: 3vw
|
||||||
|
|
||||||
|
.avatar
|
||||||
|
$width-height(6vw, 6vw)
|
||||||
|
border-radius: 1vw
|
||||||
|
opacity: 60%
|
||||||
|
z-index: 99999
|
||||||
|
transition: transform .3s ease
|
||||||
|
transform: scale(1.2) rotate(30deg)
|
||||||
|
&:hover
|
||||||
|
transform: rotate(-30deg)
|
||||||
|
|
||||||
|
.site-name-and-status
|
||||||
|
$flex-column()
|
||||||
|
.site-name
|
||||||
|
font-family: Ubuntu Mono, "Courier New", Courier, monospace
|
||||||
|
font-weight: bold
|
||||||
|
font-size: 3vw
|
||||||
|
|
||||||
|
.last-track
|
||||||
|
position: relative
|
||||||
|
box-sizing: border-box
|
||||||
|
background-color: $card-color
|
||||||
|
$border-border-radius(.1vw, $accent-main, 1vw)
|
||||||
|
$width-height(17vw, 6vw)
|
||||||
|
padding: .5vw
|
||||||
|
gap: .5vw
|
||||||
|
$flex-justify-align(flex-start, center)
|
||||||
|
img
|
||||||
|
box-sizing: border-box
|
||||||
|
$border-border-radius(.1vw, $gray-dull, 1vw)
|
||||||
|
$width-height(5vw, 5vw)
|
||||||
|
.overlay-image
|
||||||
|
background-color: rgba(0, 0, 0, 0.5)
|
||||||
|
display: none
|
||||||
|
position: absolute
|
||||||
|
box-sizing: border-box
|
||||||
|
$width-height(5vw, 5vw)
|
||||||
|
pointer-events: none
|
||||||
|
.descr
|
||||||
|
font-size: .8vw
|
||||||
|
gap: .1vw
|
||||||
|
$flex-column()
|
||||||
|
margin-bottom: auto
|
||||||
|
max-height: 100%
|
||||||
|
overflow-y: scroll
|
||||||
|
scrollbar-width: none
|
||||||
|
#subdescr
|
||||||
|
color: $gray-main
|
|
@ -0,0 +1,27 @@
|
||||||
|
.content {
|
||||||
|
padding: 5vw;
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #999999;
|
||||||
|
font-size: 5vw;
|
||||||
|
font-family: Arial;
|
||||||
|
background-color: black;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
div {
|
||||||
|
margin-bottom: 2.5vw
|
||||||
|
}
|
||||||
|
|
||||||
|
.pseudo-link {
|
||||||
|
color: #FF33FF;
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 346 KiB |
Binary file not shown.
After Width: | Height: | Size: 806 B |
Binary file not shown.
After Width: | Height: | Size: 318 B |
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,17 @@
|
||||||
|
function handleScroll(el, x, y) {
|
||||||
|
if (el.getAttribute('no-scroll') === 'false') { window.scrollTo(x, y); }
|
||||||
|
else { el.setAttribute('no-scroll', 'false'); }
|
||||||
|
}
|
||||||
|
function updateTextarea(el, message) {
|
||||||
|
el.querySelector('textarea').value = '';
|
||||||
|
el.querySelector('textarea').placeholder = message;
|
||||||
|
}
|
||||||
|
function mailBeforeRequest(el) {
|
||||||
|
document.getElementById('main-content').setAttribute('no-scroll', 'true');
|
||||||
|
updateTextarea(el, 'Сообщение успешно отправлено!');
|
||||||
|
}
|
||||||
|
function mailAfterRequest(el) {
|
||||||
|
setTimeout(() => updateTextarea(el,
|
||||||
|
'Почтовый ящик! Здесь можно оставить анонимное сообщение админу'), 5000);
|
||||||
|
}
|
||||||
|
function enterBlock(ev) { if (ev.key=='Enter') {ev.preventDefault();} }
|
|
@ -0,0 +1,41 @@
|
||||||
|
package benchmarks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LOCK_POST_DESCR = "🤖 Пост доступен только для зарегистрированных пользователей. Аккаунты создаются администратором. Превью для приватных постов не предусмотрено..."
|
||||||
|
SMALL_POST_DESCR = "🛸 Прилетело НЛО и украло описание поста..."
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkPostDescription(b *testing.B) {
|
||||||
|
post := GetLargePost()
|
||||||
|
b.ResetTimer() // reset timer before cycle
|
||||||
|
b.Run("PostDescription1", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
post.PostDescription1()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (post Post) PostDescription1() template.HTML {
|
||||||
|
const maxLength = 500
|
||||||
|
|
||||||
|
if post.Lock == 1 {
|
||||||
|
return template.HTML(LOCK_POST_DESCR)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(post.Body) > maxLength {
|
||||||
|
spanIndex := strings.Index(string(post.Body), "<span>")
|
||||||
|
min := min(spanIndex, maxLength)
|
||||||
|
if min != -1 {
|
||||||
|
return template.HTML(post.Body[:min] + "...")
|
||||||
|
} else {
|
||||||
|
return template.HTML(post.Body[:maxLength] + "...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return template.HTML(SMALL_POST_DESCR)
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package benchmarks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkPostsSublist(b *testing.B) {
|
||||||
|
p := generatePostsList()
|
||||||
|
// fmt.Println(p)
|
||||||
|
b.ResetTimer() // reset timer before cycle
|
||||||
|
b.Run("GetPostListBySubstring1", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
p.GetPostListBySubstring1("#Aiogram")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (posts Posts) GetPostListBySubstring1(substring string) Posts {
|
||||||
|
res := make(Posts, 0, len(posts))
|
||||||
|
// Если первый символ - #, то поиск происходит по тегам
|
||||||
|
if substring = substring + " "; substring[0] == '#' {
|
||||||
|
for i := range posts {
|
||||||
|
// добавляю пробел, чтобы не было ситуации включения одного тега в начало другого
|
||||||
|
if strings.Contains(posts[i].Tags+" ", substring) {
|
||||||
|
res = append(res, posts[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package benchmarks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var globalStringList = []string{"#Aiogram", "#Go", "#JS", "#Windows", "#Анонимность",
|
||||||
|
"#Апдейт", "#Ассемблер", "#Веб", "#Идеи", "#Книги", "#Музыка", "#Находки",
|
||||||
|
"#Нейросети", "#ООП", "#Осайте", "#Прога", "#Си", "#Фронтенд"}
|
||||||
|
|
||||||
|
var randGen = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
// Структура поста
|
||||||
|
type Post struct {
|
||||||
|
ID int
|
||||||
|
Author string
|
||||||
|
Title string
|
||||||
|
Body template.HTML
|
||||||
|
PostingTime string
|
||||||
|
UpdateTime string
|
||||||
|
Tags string
|
||||||
|
Lock int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Posts []Post
|
||||||
|
|
||||||
|
// random 5 tags for post
|
||||||
|
func tag5() string {
|
||||||
|
return globalStringList[randGen.Intn(len(globalStringList))] + " " +
|
||||||
|
globalStringList[randGen.Intn(len(globalStringList))] + " " +
|
||||||
|
globalStringList[randGen.Intn(len(globalStringList))] + " " +
|
||||||
|
globalStringList[randGen.Intn(len(globalStringList))] + " " +
|
||||||
|
globalStringList[randGen.Intn(len(globalStringList))]
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatePostsList() Posts {
|
||||||
|
var p Posts
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
p = append(p, GetLargePost())
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate big post
|
||||||
|
func GetLargePost() Post {
|
||||||
|
author := "admin"
|
||||||
|
title := "A Comprehensive Guide to Go Programming Language"
|
||||||
|
body := strings.Repeat("REPEAT ME", 500)
|
||||||
|
postingTime := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
updateTime := postingTime
|
||||||
|
tags := tag5()
|
||||||
|
lock := 0
|
||||||
|
|
||||||
|
return Post{
|
||||||
|
ID: 1,
|
||||||
|
Author: author,
|
||||||
|
Title: title,
|
||||||
|
Body: template.HTML(body),
|
||||||
|
PostingTime: postingTime,
|
||||||
|
UpdateTime: updateTime,
|
||||||
|
Tags: tags,
|
||||||
|
Lock: lock,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Cfg *Config
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ServerIP string `json:"SERVER_IP"`
|
||||||
|
DBPath string `json:"DB_PATH"`
|
||||||
|
TemplatesPath string `json:"TEMPLATES_PATH"`
|
||||||
|
GcssPath string `json:"GCSS_PATH"`
|
||||||
|
TgBotToken string `json:"TG_BOT_TOKEN"`
|
||||||
|
TgChatID string `json:"TG_CHAT_ID"`
|
||||||
|
LastfmAPIKey string `json:"LASTFM_API_KEY"`
|
||||||
|
LastFmUsername string `json:"LASTFM_USERNAME"`
|
||||||
|
CookieCryptKey string `json:"COOKIE_CRYPT_KEY"`
|
||||||
|
CookieHMAC string `json:"COOKIE_HMAC"`
|
||||||
|
SessionKey string `json:"SESSION_KEY"`
|
||||||
|
SessionTime int `json:"SESSION_TIME_HOURS"`
|
||||||
|
TgTickerTime time.Duration `json:"TG_TICKER_TIME_HOURS"`
|
||||||
|
LastFmTickerTime time.Duration `json:"LASTFM_TICKER_TIME_SECONDS"`
|
||||||
|
MaxPostsOnPage int `json:"MAX_POSTS_ON_PAGE"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(filename string) error {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
Cfg = &config
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
folder for database
|
|
@ -0,0 +1,53 @@
|
||||||
|
module main
|
||||||
|
|
||||||
|
go 1.23.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.hikan.ru/serr/candycache v6.0.0+incompatible
|
||||||
|
github.com/gin-contrib/sessions v1.0.2
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/tdewolff/minify v2.3.6+incompatible
|
||||||
|
github.com/yosssi/gcss v0.1.0
|
||||||
|
modernc.org/sqlite v1.34.5
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/context v1.1.2 // indirect
|
||||||
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
|
github.com/gorilla/sessions v1.2.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/tdewolff/parse v2.3.4+incompatible // indirect
|
||||||
|
github.com/tdewolff/test v1.0.10 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.31.0 // indirect
|
||||||
|
golang.org/x/net v0.33.0 // indirect
|
||||||
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
|
golang.org/x/text v0.21.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/libc v1.55.3 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.8.0 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,149 @@
|
||||||
|
git.hikan.ru/serr/candycache v6.0.0+incompatible h1:pgNHHLUURD50k+C5usKFGNgdipYP3bm1ZEqZ7Z5O+Z8=
|
||||||
|
git.hikan.ru/serr/candycache v6.0.0+incompatible/go.mod h1:e5pb4OWEzK43HUKH5iQGSTWujA6TWUjBvljlZFYsvjw=
|
||||||
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
|
||||||
|
github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||||
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||||
|
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||||
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
|
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||||
|
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo=
|
||||||
|
github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs=
|
||||||
|
github.com/tdewolff/parse v2.3.4+incompatible h1:x05/cnGwIMf4ceLuDMBOdQ1qGniMoxpP46ghf0Qzh38=
|
||||||
|
github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ=
|
||||||
|
github.com/tdewolff/test v1.0.10 h1:uWiheaLgLcNFqHcdWveum7PQfMnIUTf9Kl3bFxrIoew=
|
||||||
|
github.com/tdewolff/test v1.0.10/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/yosssi/gcss v0.1.0 h1:jRuino7qq7kqntBIhT+0xSUI5/sBgCA/zCQ1Tuzd6Gg=
|
||||||
|
github.com/yosssi/gcss v0.1.0/go.mod h1:M3mTPOWZWjVROkXKZ2AiDzOBOXu2MqQeDXF/nKO44sI=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||||
|
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||||
|
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||||
|
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
||||||
|
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
|
||||||
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
|
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||||
|
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||||
|
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
||||||
|
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
||||||
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
|
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||||
|
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||||
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
|
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||||
|
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
|
||||||
|
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
|
||||||
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
|
@ -0,0 +1,190 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"main/config"
|
||||||
|
"main/mvc/controllers"
|
||||||
|
"main/mvc/models"
|
||||||
|
"main/tools"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.hikan.ru/serr/candycache"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yosssi/gcss"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
For server start:
|
||||||
|
|
||||||
|
sudo systemctl stop server.service
|
||||||
|
go build main.go
|
||||||
|
sudo systemctl start server.service
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// load config
|
||||||
|
if err := config.LoadConfig("config/config.json"); err != nil {
|
||||||
|
log.Fatal("config.LoadConfig error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inUse, err := tools.IsIPInUse(config.Cfg.ServerIP)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
} else if inUse {
|
||||||
|
// server start
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
startSiteProcess(config.Cfg.ServerIP)
|
||||||
|
} else {
|
||||||
|
// localhost start
|
||||||
|
startSiteProcess("localhost")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startSiteProcess(ip string) {
|
||||||
|
// CSS render
|
||||||
|
_, err := gcss.CompileFile(config.Cfg.GcssPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("gcss.CompileFile error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get empty site ctx
|
||||||
|
s := models.NewSiteCtx()
|
||||||
|
|
||||||
|
// Create connection with db
|
||||||
|
s.DB, err = models.DBConnect(config.Cfg.DBPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// Get posts list from DB
|
||||||
|
s.Posts, err = models.PostsListFromDB(s.DB)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// Get tags map
|
||||||
|
s.Tags = s.Posts.TagsMap()
|
||||||
|
|
||||||
|
// TG worker
|
||||||
|
s.Bot = models.TGNew(config.Cfg.TgBotToken, config.Cfg.TgChatID)
|
||||||
|
// LFM worker
|
||||||
|
s.LFM = models.LFMNew(config.Cfg.LastFmUsername, config.Cfg.LastfmAPIKey)
|
||||||
|
// Get last track LFM
|
||||||
|
if track, err := s.LFM.LastTrack(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
} else {
|
||||||
|
s.LastTrackAjaxBlock = s.LFM.TrackAjax(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create cache storage
|
||||||
|
s.Cache = candycache.Cacher(-1)
|
||||||
|
// Create gin instance
|
||||||
|
s.GinEngine = gin.New()
|
||||||
|
s.GinEngine.Use(controllers.Logger(s))
|
||||||
|
controllers.CreateSessionsStore(s.GinEngine)
|
||||||
|
// Set up routes
|
||||||
|
if err = setupRoutes(s); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// Start subprocesses
|
||||||
|
s.RunSubProcesses()
|
||||||
|
// Start server
|
||||||
|
if err := s.Run(ip + ":80"); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// DB close
|
||||||
|
models.DBClose(s.DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
var funcMap = template.FuncMap{
|
||||||
|
"add": func(number, value int) int { return number + value },
|
||||||
|
"SplitString": strings.Split,
|
||||||
|
"DaysSinceStartSite": tools.DaysSinceStartSite,
|
||||||
|
"getCurTime": tools.GetCurTime,
|
||||||
|
"urlQueryEscape": url.QueryEscape,
|
||||||
|
}
|
||||||
|
|
||||||
|
func initRoutes(s *models.Site) {
|
||||||
|
// Root
|
||||||
|
s.GinEngine.GET("/", func(c *gin.Context) {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1")
|
||||||
|
})
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
s.GinEngine.GET("/api/get_last_track", func(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "text/html")
|
||||||
|
c.String(http.StatusOK, s.LastTrackAjaxBlock)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Static (js, css, pic)
|
||||||
|
s.GinEngine.Static("/assets", "./assets")
|
||||||
|
// No route
|
||||||
|
s.GinEngine.NoRoute(func(c *gin.Context) {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Нет доступа")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Routes without authorize check
|
||||||
|
unauthorized := s.GinEngine.Group("")
|
||||||
|
{
|
||||||
|
controllers.PageByName("mobile", unauthorized, s)
|
||||||
|
controllers.PageByName("login", unauthorized, s)
|
||||||
|
controllers.PageByName("AJAXlogin", unauthorized, s)
|
||||||
|
controllers.Login("login", unauthorized, s)
|
||||||
|
controllers.PageByName("index", unauthorized, s)
|
||||||
|
controllers.PageByName("AJAXindex", unauthorized, s)
|
||||||
|
controllers.PageByName("post", unauthorized, s)
|
||||||
|
controllers.PageByName("AJAXpost", unauthorized, s)
|
||||||
|
controllers.PageByName("tags", unauthorized, s)
|
||||||
|
controllers.PageByName("AJAXtags", unauthorized, s)
|
||||||
|
controllers.PageByName("404", unauthorized, s)
|
||||||
|
controllers.SearchPage("search", unauthorized, s)
|
||||||
|
controllers.SearchPage("AJAXsearch", unauthorized, s)
|
||||||
|
controllers.SendMesssage("send", unauthorized, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need authorize check
|
||||||
|
authorized := s.GinEngine.Group("", controllers.AuthMiddleware())
|
||||||
|
{
|
||||||
|
controllers.PageByName("editpage", authorized, s)
|
||||||
|
controllers.PageByName("AJAXeditpage", authorized, s)
|
||||||
|
controllers.Logout("logout", authorized, s) //
|
||||||
|
controllers.PageByName("logout", authorized, s)
|
||||||
|
controllers.PageByName("AJAXlogout", authorized, s)
|
||||||
|
controllers.PageByName("adm", authorized, s)
|
||||||
|
controllers.PageByName("AJAXadm", authorized, s)
|
||||||
|
controllers.DownloadCache(authorized, s)
|
||||||
|
controllers.CacheClear("cacheclear", authorized, s)
|
||||||
|
controllers.DeleteFromCache("deletefromcache", authorized, s)
|
||||||
|
controllers.UploadCache("uploadcachedump", authorized, s)
|
||||||
|
controllers.EditPost("editpost", authorized, s)
|
||||||
|
controllers.AddPost("addpost", authorized, s)
|
||||||
|
controllers.DeletePost("delpost", authorized, s)
|
||||||
|
controllers.Backup("knock", authorized, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRoutes(s *models.Site) error {
|
||||||
|
// load templates from TemplatesPath
|
||||||
|
tmpl := template.New("").Funcs(funcMap)
|
||||||
|
fn := func(path string, f os.FileInfo, err error) error {
|
||||||
|
if !f.IsDir() && strings.HasSuffix(f.Name(), ".gohtml") {
|
||||||
|
var err error
|
||||||
|
tmpl, err = tmpl.ParseFiles(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := filepath.Walk(config.Cfg.TemplatesPath, fn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Tmpl = tmpl
|
||||||
|
initRoutes(s)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"main/mvc/models"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `form:"username" json:"username"`
|
||||||
|
Password string `form:"password" json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Login(tmplname string, group *gin.RouterGroup, s *models.Site) {
|
||||||
|
group.POST(fmt.Sprintf("/%s", tmplname), func(c *gin.Context) {
|
||||||
|
var requestData LoginRequest
|
||||||
|
|
||||||
|
// Привязка данных формы к структуре
|
||||||
|
if err := c.ShouldBind(&requestData); err != nil {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Ошибка при привязке данных к структуре")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accessLvl, _ := models.AccessCheck(s.DB, requestData.Username, requestData.Password)
|
||||||
|
|
||||||
|
if accessLvl == 2 {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
session.Set("access_lvl", accessLvl)
|
||||||
|
session.Save()
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Вход выполнен")
|
||||||
|
} else {
|
||||||
|
c.Redirect(http.StatusFound, "/login/1?Ошибка входа")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Logout(tmplname string, group *gin.RouterGroup, s *models.Site) {
|
||||||
|
group.POST(fmt.Sprintf("/%s", tmplname), func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
session.Clear()
|
||||||
|
session.Save()
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Выход выполнен")
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"main/config"
|
||||||
|
"main/mvc/models"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Отстукивает в ТГ
|
||||||
|
func Backup(tmplname string, group *gin.RouterGroup, s *models.Site) {
|
||||||
|
group.POST(tmplname, func(c *gin.Context) {
|
||||||
|
go s.Bot.Backup(config.Cfg.DBPath, s.LogEntries, s.Cache)
|
||||||
|
c.Redirect(http.StatusFound, "/adm/1?Отстук инициирован")
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"main/mvc/models"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeleteFromCacheRequest struct {
|
||||||
|
Cachekey string `form:"cachekey" json:"cachekey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFromCache(tmplname string, group *gin.RouterGroup, s *models.Site) {
|
||||||
|
group.POST(fmt.Sprintf("/%s", tmplname), func(c *gin.Context) {
|
||||||
|
var requestData DeleteFromCacheRequest
|
||||||
|
|
||||||
|
// Привязка данных формы к структуре
|
||||||
|
if err := c.ShouldBind(&requestData); err != nil {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Ошибка при привязке данных к структуре")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Cache.Delete(requestData.Cachekey)
|
||||||
|
|
||||||
|
c.Redirect(http.StatusFound, "/adm/1?Запись удалена из кэша")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CacheClear(tmplname string, group *gin.RouterGroup, s *models.Site) {
|
||||||
|
group.POST(tmplname, func(c *gin.Context) {
|
||||||
|
s.Cache.Flush()
|
||||||
|
c.Redirect(http.StatusFound, "/adm/1?Кэш очищен")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadCache(tmplname string, group *gin.RouterGroup, s *models.Site) {
|
||||||
|
group.POST(tmplname, func(c *gin.Context) {
|
||||||
|
|
||||||
|
file, err := c.FormFile("file-upload")
|
||||||
|
if err != nil {
|
||||||
|
c.Redirect(http.StatusFound, "/adm/1?Ошибка при получении файла")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Открываем файл для чтения
|
||||||
|
fileReader, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
c.Redirect(http.StatusFound, "/adm/1?Ошибка при открытии файла")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer fileReader.Close()
|
||||||
|
|
||||||
|
// Загружаем данные в кэш
|
||||||
|
if err := s.Cache.Load(fileReader); err != nil {
|
||||||
|
c.Redirect(http.StatusFound, "/adm/1?Ошибка при загрузке данных в кэш")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusFound, "/adm/1?Дамп успешно загружен")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadCache(group *gin.RouterGroup, s *models.Site) {
|
||||||
|
group.GET("/loadcachedump", func(c *gin.Context) {
|
||||||
|
path := "cache.json"
|
||||||
|
if err := models.WriteCacheDumpToFile(s.Cache, path); err != nil {
|
||||||
|
s.Bot.SendMessage(fmt.Sprintf("🔴 Ошибка записи дампа кэша в файл: %s", err.Error()))
|
||||||
|
} else {
|
||||||
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%d%s", time.Now().Unix(), path))
|
||||||
|
c.Header("Content-Type", "application/octet-stream")
|
||||||
|
c.File(path)
|
||||||
|
}
|
||||||
|
os.Remove(path)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"main/mvc/models"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserSendMesssageRequest struct {
|
||||||
|
Body string `form:"body" json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendMesssage(tmplname string, group *gin.RouterGroup, s *models.Site) {
|
||||||
|
s.GinEngine.POST(tmplname, func(c *gin.Context) {
|
||||||
|
var requestData UserSendMesssageRequest
|
||||||
|
|
||||||
|
if err := c.ShouldBind(&requestData); err != nil {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Ошибка при привязке данных к структуре")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message := fmt.Sprintf("Анонимное сообщение от юзера %s:\n\n%s", c.ClientIP(), requestData.Body)
|
||||||
|
go s.Bot.SendMessage(message)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"main/mvc/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Logger(s *models.Site) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
go func() {
|
||||||
|
logEntry := fmt.Sprintf("%s | %d | %s | %s | %s\n",
|
||||||
|
time.Now().Format("2006/01/02 - 15:04:05"),
|
||||||
|
c.Writer.Status(),
|
||||||
|
c.ClientIP(),
|
||||||
|
c.Request.Method,
|
||||||
|
c.Request.URL.Path,
|
||||||
|
)
|
||||||
|
fmt.Print(logEntry)
|
||||||
|
s.Lock()
|
||||||
|
s.LogEntries = append(s.LogEntries, logEntry)
|
||||||
|
s.Unlock()
|
||||||
|
}()
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if AccessLvl(c) == 0 {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Нет доступа")
|
||||||
|
// гарантирует что цепочка обработчиков оборвется
|
||||||
|
// после выполнения этого обработчика
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация страницы
|
||||||
|
func PageValidationMiddleware(s *models.Site) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
|
||||||
|
tmplname := path[1 : strings.Index(path[1:], "/")+1]
|
||||||
|
pageNumber, err := strconv.Atoi(c.Param("id"))
|
||||||
|
|
||||||
|
// Проверка на ошибки при преобразовании id (переполнение)
|
||||||
|
if err != nil {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Нет доступа")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация pageNumber в зависимости от tmplname
|
||||||
|
switch tmplname {
|
||||||
|
case "post", "AJAXpost", "editpage", "AJAXeditpage":
|
||||||
|
if pageNumber > len(s.Posts) || pageNumber < 1 {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Нет доступа")
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
case "index", "AJAXindex":
|
||||||
|
if pageNumber > s.Posts.GetMaxPageNumber() || pageNumber < 1 {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Нет доступа")
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
case "search", "AJAXsearch":
|
||||||
|
searchSubstring := c.Query("search")
|
||||||
|
if searchSubstring == "" ||
|
||||||
|
pageNumber > s.Posts[0:s.Tags[searchSubstring]].GetMaxPageNumber() ||
|
||||||
|
pageNumber < 1 {
|
||||||
|
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Нет доступа")
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if pageNumber != 1 {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Нет доступа")
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("pageNumber", pageNumber)
|
||||||
|
// Продолжаем обработку запроса
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"main/mvc/models"
|
||||||
|
"main/tools"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Возвращает контекст страницы по списку постов (которые на ней должны быть),
|
||||||
|
// номеру страницы, метадаты
|
||||||
|
func PageCtx(c *gin.Context, s *models.Site,
|
||||||
|
postsSublist models.Posts, pageNumber int, data string) *models.Page {
|
||||||
|
|
||||||
|
accessLvl := AccessLvl(c)
|
||||||
|
return &models.Page{
|
||||||
|
AccessLvl: accessLvl,
|
||||||
|
Title: "hikan.ru",
|
||||||
|
PostsSublist: postsSublist,
|
||||||
|
Number: pageNumber,
|
||||||
|
Data: data,
|
||||||
|
SiteCtx: s,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Задает маршруты для страниц вида tmplname/id.html, где необходим список всех постов
|
||||||
|
func PageByName(tmplname string, group *gin.RouterGroup, s *models.Site) {
|
||||||
|
group.GET(fmt.Sprintf("/%s/:id", tmplname),
|
||||||
|
PageValidationMiddleware(s),
|
||||||
|
func(c *gin.Context) {
|
||||||
|
|
||||||
|
pageNumber, _ := c.Get("pageNumber")
|
||||||
|
pageContext := PageCtx(c, s, s.Posts, pageNumber.(int), "")
|
||||||
|
|
||||||
|
page, err := getPageByName(c, s, tmplname, pageNumber.(int), pageContext)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, "Ошибка рендеринга: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPage(c, page)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вынесенный общий код для получения страницы
|
||||||
|
func getPageByName(c *gin.Context, s *models.Site, tmplname string,
|
||||||
|
pageNumber int, pageContext *models.Page) (string, error) {
|
||||||
|
|
||||||
|
var page string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if AccessLvl(c) != 0 {
|
||||||
|
// Для админа - рендер без кэширования
|
||||||
|
page, err = RenderPage(pageContext, tmplname, s)
|
||||||
|
} else {
|
||||||
|
key := fmt.Sprintf("%s%d", tmplname, pageNumber)
|
||||||
|
// Попытка получить страницу из кэша
|
||||||
|
if cachedPage, err := s.Cache.Get(key); err == nil {
|
||||||
|
page = cachedPage.(string)
|
||||||
|
} else {
|
||||||
|
// Нет в кэше => рендерим
|
||||||
|
page, err = RenderPage(pageContext, tmplname, s)
|
||||||
|
if err == nil {
|
||||||
|
// Сжимаю по возможности и добавляю в кэш
|
||||||
|
go func() {
|
||||||
|
compressedPage, err := tools.MinifyStringHtml(page)
|
||||||
|
if err != nil {
|
||||||
|
s.Cache.Set(key, page, -1)
|
||||||
|
} else {
|
||||||
|
s.Cache.Set(key, compressedPage, -1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return page, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchPage(tmplname string, group *gin.RouterGroup, s *models.Site) {
|
||||||
|
group.GET(fmt.Sprintf("/%s/:id", tmplname),
|
||||||
|
PageValidationMiddleware(s),
|
||||||
|
func(c *gin.Context) {
|
||||||
|
|
||||||
|
searchSubstring := c.Query("search")
|
||||||
|
pageNumber, _ := c.Get("pageNumber")
|
||||||
|
|
||||||
|
// Валидная страничка, рендерим
|
||||||
|
page, err := getSearchPage(c, s, tmplname, pageNumber.(int), searchSubstring)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, "Ошибка рендеринга: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendPage(c, page)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вынесенный общий код
|
||||||
|
func getSearchPage(c *gin.Context, s *models.Site, tmplname string,
|
||||||
|
pageNumber int, searchSubstring string) (string, error) {
|
||||||
|
|
||||||
|
var page string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if AccessLvl(c) != 0 { // кэширую только странички по тегам
|
||||||
|
// Для админа - рендер без кэширования
|
||||||
|
posts := s.Posts.GetPostListBySubstring(searchSubstring) // посты, содержащие искомую подстроку
|
||||||
|
pageContext := PageCtx(c, s, posts, pageNumber, searchSubstring)
|
||||||
|
page, err = RenderPage(pageContext, tmplname, s)
|
||||||
|
} else {
|
||||||
|
key := fmt.Sprintf("%s%d%s", tmplname, pageNumber, searchSubstring)
|
||||||
|
|
||||||
|
if cachedPage, err := s.Cache.Get(key); err == nil {
|
||||||
|
page = cachedPage.(string)
|
||||||
|
} else {
|
||||||
|
// Нет в кэше => рендерим
|
||||||
|
posts := s.Posts.GetPostListBySubstring(searchSubstring)
|
||||||
|
pageContext := PageCtx(c, s, posts, pageNumber, searchSubstring)
|
||||||
|
page, err = RenderPage(pageContext, tmplname, s)
|
||||||
|
// если нет ошибки и список постов не пустой, то надо добавить в кэш
|
||||||
|
if err == nil && len(posts) > 0 {
|
||||||
|
// Сжимаю по возможности и добавляю в кэш
|
||||||
|
go func() {
|
||||||
|
compressedPage, err := tools.MinifyStringHtml(page)
|
||||||
|
if err != nil {
|
||||||
|
s.Cache.Set(key, page, -1)
|
||||||
|
} else {
|
||||||
|
s.Cache.Set(key, compressedPage, -1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return page, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderPage(pageContext *models.Page,
|
||||||
|
tmplname string, s *models.Site) (string, error) {
|
||||||
|
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
if err := s.Tmpl.ExecuteTemplate(
|
||||||
|
&buffer,
|
||||||
|
fmt.Sprintf("%s.gohtml", tmplname),
|
||||||
|
pageContext); err != nil {
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendPage(c *gin.Context, page string) {
|
||||||
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
c.String(http.StatusOK, page)
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"main/mvc/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type СhangePostRequest struct {
|
||||||
|
Postname string `form:"name" json:"name"`
|
||||||
|
Tags string `form:"tags" json:"tags"`
|
||||||
|
Body string `form:"body" json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddPost(tmplname string, group *gin.RouterGroup, s *models.Site) {
|
||||||
|
group.POST(fmt.Sprintf("/%s", tmplname), func(c *gin.Context) {
|
||||||
|
var requestData СhangePostRequest
|
||||||
|
|
||||||
|
// Привязка данных формы к структуре
|
||||||
|
if err := c.ShouldBind(&requestData); err != nil {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Ошибка при привязке данных к структуре")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
models.AddPost(s.DB, "admin", requestData.Postname, requestData.Body, requestData.Tags)
|
||||||
|
// Актуализация данных в ОЗУ
|
||||||
|
s.Posts, _ = models.PostsListFromDB(s.DB)
|
||||||
|
s.Tags = s.Posts.TagsMap()
|
||||||
|
//
|
||||||
|
c.Redirect(http.StatusFound, fmt.Sprintf("/post/%d?Пост создан", len(s.Posts)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func EditPost(tmplname string, group *gin.RouterGroup, s *models.Site) {
|
||||||
|
group.POST(fmt.Sprintf("/%s/:id", tmplname), func(c *gin.Context) {
|
||||||
|
postIndex, err := strconv.Atoi(c.Param("id")) // Получаем параметр из URL
|
||||||
|
if err != nil {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Ошибка при получении номера страницы")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestData СhangePostRequest
|
||||||
|
|
||||||
|
// Привязка данных формы к структуре
|
||||||
|
if err := c.ShouldBind(&requestData); err != nil {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Ошибка при привязке данных к структуре")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
models.UpdatePost(s.DB, postIndex, "admin", requestData.Postname,
|
||||||
|
requestData.Body, requestData.Tags)
|
||||||
|
// Актуализация данных в ОЗУ
|
||||||
|
s.Posts, _ = models.PostsListFromDB(s.DB)
|
||||||
|
s.Tags = s.Posts.TagsMap()
|
||||||
|
//
|
||||||
|
c.Redirect(http.StatusFound, fmt.Sprintf("/post/%d?Пост обновлен", postIndex))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeletePost(tmplname string, group *gin.RouterGroup, s *models.Site) {
|
||||||
|
group.POST(fmt.Sprintf("/%s/:id", tmplname), func(c *gin.Context) {
|
||||||
|
postIndex, err := strconv.Atoi(c.Param("id")) // Получаем параметр из URL
|
||||||
|
if err != nil {
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Ошибка при получении номера страницы")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.DelPost(s.DB, postIndex)
|
||||||
|
// Актуализация данных в ОЗУ
|
||||||
|
s.Posts, _ = models.PostsListFromDB(s.DB)
|
||||||
|
s.Tags = s.Posts.TagsMap()
|
||||||
|
//
|
||||||
|
c.Redirect(http.StatusFound, "/index/1?Пост обновлен")
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"main/config"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-contrib/sessions/cookie"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateSessionsStore(r *gin.Engine) {
|
||||||
|
store := cookie.NewStore([]byte(config.Cfg.CookieCryptKey), []byte(config.Cfg.CookieHMAC))
|
||||||
|
r.Use(sessions.Sessions(config.Cfg.SessionKey, store))
|
||||||
|
store.Options(sessions.Options{
|
||||||
|
Path: "/", // сессия действительна для всех путей
|
||||||
|
MaxAge: config.Cfg.SessionTime * 60 * 60, // время жизни сессий в часах
|
||||||
|
HttpOnly: true, // отключение доступа через js (так надо говорят)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращает уровень доступа из пользовательской сессии
|
||||||
|
func AccessLvl(c *gin.Context) int {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
var accessLvl int
|
||||||
|
if session.Get("access_lvl") != nil {
|
||||||
|
accessLvl = session.Get("access_lvl").(int)
|
||||||
|
} else {
|
||||||
|
accessLvl = 0
|
||||||
|
}
|
||||||
|
return accessLvl
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.hikan.ru/serr/candycache"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WriteCacheDumpToFile(cache *candycache.Cache, filename string) error {
|
||||||
|
file, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
if err = cache.Save(file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"html/template"
|
||||||
|
"main/config"
|
||||||
|
"main/tools"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.hikan.ru/serr/candycache"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Контекст сайта
|
||||||
|
type Site struct {
|
||||||
|
sync.RWMutex
|
||||||
|
Cfg *config.Config
|
||||||
|
Tmpl *template.Template // Хранилище шаблонов
|
||||||
|
Bot *TGBot // Контроллер для отстука в тг бота
|
||||||
|
LFM *LFMWorker // Контроллер ЛАСТ ФМ
|
||||||
|
LogEntries []string // Журнал логов (отправляется в ТГ бота)
|
||||||
|
Posts Posts // Список со всеми постами из базы данных
|
||||||
|
Tags map[string]int // Список всех тегов
|
||||||
|
Cache *candycache.Cache // Домик для кэша
|
||||||
|
LastTrackAjaxBlock string // HTML Аякс блок, соответствующий последнему кэшированному треку с ластфм
|
||||||
|
RestartTime time.Time // Временной момент рестарта серверного приложения
|
||||||
|
Version int64 // Окончание ?v=version для всех кэшируемых файлов
|
||||||
|
DB *sql.DB // Открытое соединение с БД
|
||||||
|
GinEngine *gin.Engine // Движок Gin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция возвращает пустой экземпляр контекста сайта
|
||||||
|
func NewSiteCtx() *Site {
|
||||||
|
return &Site{
|
||||||
|
RestartTime: time.Now(),
|
||||||
|
Version: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск параллельных процессов
|
||||||
|
func (s *Site) RunSubProcesses() {
|
||||||
|
go tools.Ticker(config.Cfg.TgTickerTime*time.Hour, func() {
|
||||||
|
s.Bot.Backup(config.Cfg.DBPath, s.LogEntries, s.Cache)
|
||||||
|
})
|
||||||
|
go tools.Ticker(config.Cfg.LastFmTickerTime*time.Second, func() {
|
||||||
|
if track, err := s.LFM.LastTrack(); err == nil {
|
||||||
|
s.LastTrackAjaxBlock = s.LFM.TrackAjax(track)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Site) Run(ip string) error {
|
||||||
|
if err := s.GinEngine.Run(ip); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"main/tools"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdatePost обновляет существующий пост по переданному ID
|
||||||
|
func UpdatePost(db *sql.DB, id int, author, title, body, tags string) error {
|
||||||
|
updateTime := tools.GetCurTime()
|
||||||
|
_, err := db.Exec("UPDATE posts SET author = ?, title = ?, body = ?, update_time = ?, tags = ? WHERE id = ?",
|
||||||
|
author, title, body, updateTime, tags, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPost добавляет новый пост
|
||||||
|
func AddPost(db *sql.DB, author, title, body, tags string) error {
|
||||||
|
postingTime, updateTime := tools.GetCurTime(), tools.GetCurTime()
|
||||||
|
_, err := db.Exec("INSERT INTO posts (author, title, body, posting_time, update_time, tags) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
author, title, body, postingTime, updateTime, tags)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelPost удаляет пост
|
||||||
|
func DelPost(db *sql.DB, postID int) error {
|
||||||
|
_, err := db.Exec("DELETE FROM posts WHERE id = ?", postID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = db.Exec("UPDATE posts SET id = id - 1 WHERE id > ?", postID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекает список постов из базы данных
|
||||||
|
func PostsListFromDB(db *sql.DB) (Posts, error) {
|
||||||
|
rows, err := db.Query("SELECT * FROM posts")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var posts Posts
|
||||||
|
for rows.Next() {
|
||||||
|
var post Post
|
||||||
|
if err := rows.Scan(&post.ID,
|
||||||
|
&post.Author,
|
||||||
|
&post.Title,
|
||||||
|
&post.Body,
|
||||||
|
&post.PostingTime,
|
||||||
|
&post.UpdateTime,
|
||||||
|
&post.Tags); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
posts = append(posts, post)
|
||||||
|
}
|
||||||
|
slices.Reverse(posts)
|
||||||
|
return posts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessCheck проверяет доступ пользователя
|
||||||
|
func AccessCheck(db *sql.DB, nick, password string) (int, error) {
|
||||||
|
hashed, err := tools.Sha256HashString(password + "chak_chak")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var userLevel int
|
||||||
|
err = db.QueryRow("SELECT LVL FROM users WHERE NICK = ? AND PASS = ?", nick, hashed).Scan(&userLevel)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return 0, nil // пользователь не найден
|
||||||
|
}
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return userLevel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DBConnect(path string) (*sql.DB, error) {
|
||||||
|
db, err := sql.Open("sqlite", "./"+path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DBClose(db *sql.DB) {
|
||||||
|
db.Close()
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
// Простой модуль для взаимодействия с API LastFM
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LFMWorker struct {
|
||||||
|
Username string
|
||||||
|
ApiKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Track struct {
|
||||||
|
URL string
|
||||||
|
Title string
|
||||||
|
Artist string
|
||||||
|
Album string
|
||||||
|
Listening int
|
||||||
|
}
|
||||||
|
|
||||||
|
type LastFMResponse struct {
|
||||||
|
RecentTracks struct {
|
||||||
|
Track []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Artist struct {
|
||||||
|
Name string `json:"#text"`
|
||||||
|
} `json:"artist"`
|
||||||
|
Album struct {
|
||||||
|
Name string `json:"#text"`
|
||||||
|
} `json:"album"`
|
||||||
|
Date struct {
|
||||||
|
Unix string `json:"#text"`
|
||||||
|
} `json:"date"`
|
||||||
|
Image []struct {
|
||||||
|
Size string `json:"size"`
|
||||||
|
URL string `json:"#text"`
|
||||||
|
} `json:"image"`
|
||||||
|
} `json:"track"`
|
||||||
|
} `json:"recenttracks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LFMNew(username, apikey string) *LFMWorker {
|
||||||
|
return &LFMWorker{
|
||||||
|
Username: username,
|
||||||
|
ApiKey: apikey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *LFMWorker) LastTrack() (*Track, error) {
|
||||||
|
url := fmt.Sprintf("https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=%s&api_key=%s&format=json",
|
||||||
|
w.Username, w.ApiKey)
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastFMResponse LastFMResponse
|
||||||
|
var listening int
|
||||||
|
json.Unmarshal(body, &lastFMResponse)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
// Получаем только последний трек (первый в списке)
|
||||||
|
if len(lastFMResponse.RecentTracks.Track) == 0 {
|
||||||
|
return nil, errors.New("no recent tracks")
|
||||||
|
}
|
||||||
|
lastTrack := lastFMResponse.RecentTracks.Track[0]
|
||||||
|
|
||||||
|
var imageURL string
|
||||||
|
for _, img := range lastTrack.Image {
|
||||||
|
if img.Size == "large" {
|
||||||
|
imageURL = img.URL
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastTrack.Date.Unix == "" {
|
||||||
|
listening = 1
|
||||||
|
} else {
|
||||||
|
listening = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Track{
|
||||||
|
URL: imageURL,
|
||||||
|
Title: lastTrack.Name,
|
||||||
|
Artist: lastTrack.Artist.Name,
|
||||||
|
Album: lastTrack.Album.Name,
|
||||||
|
Listening: listening}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *LFMWorker) TrackAjax(trackData *Track) string {
|
||||||
|
style, subdescr := "display: none;", "Последнее, что слушал админ:"
|
||||||
|
if trackData.Listening == 1 {
|
||||||
|
style, subdescr = "display: block;", "Админ сейчас слушает:"
|
||||||
|
}
|
||||||
|
|
||||||
|
if trackData.URL == "" {
|
||||||
|
trackData.URL = "/assets/pic/noimage.webp"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
<img src="%s">
|
||||||
|
<img src="/assets/pic/playing.gif" alt="overlay" class="overlay-image" style="%s">
|
||||||
|
<div class="descr" title="Это можно скроллить если описание слишком большое">
|
||||||
|
<div id="subdescr">%s</div>
|
||||||
|
<div id="artist">%s</div>
|
||||||
|
<div id="title">%s</div>
|
||||||
|
</div>`,
|
||||||
|
trackData.URL,
|
||||||
|
style,
|
||||||
|
subdescr,
|
||||||
|
trackData.Artist,
|
||||||
|
trackData.Title,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
// Контекст страницы
|
||||||
|
type Page struct {
|
||||||
|
AccessLvl int //уровень доступа юзера
|
||||||
|
Title string // тайтл странциы
|
||||||
|
PostsSublist Posts
|
||||||
|
Number int // номер страницы
|
||||||
|
Data string // строка с данными (например искомая строка для поиска)
|
||||||
|
SiteCtx *Site
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"main/config"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Структура поста
|
||||||
|
type Post struct {
|
||||||
|
ID int
|
||||||
|
Author string
|
||||||
|
Title string
|
||||||
|
Body template.HTML
|
||||||
|
PostingTime string
|
||||||
|
UpdateTime string
|
||||||
|
Tags string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Posts []Post
|
||||||
|
|
||||||
|
const (
|
||||||
|
LOCK_POST_DESCR = "🤖 Пост доступен только для зарегистрированных пользователей. Аккаунты создаются администратором. Превью для приватных постов не предусмотрено..."
|
||||||
|
SMALL_POST_DESCR = "🛸 Прилетело НЛО и украло описание поста..."
|
||||||
|
)
|
||||||
|
|
||||||
|
func (posts Posts) GetPostById(id int) Post {
|
||||||
|
if id < 1 || id > len(posts) {
|
||||||
|
return posts[0]
|
||||||
|
}
|
||||||
|
return posts[len(posts)-id]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (posts Posts) GetPrevPageNumber(pageNumber int) int {
|
||||||
|
if 1 > pageNumber-1 {
|
||||||
|
return pageNumber
|
||||||
|
} else {
|
||||||
|
return pageNumber - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (posts Posts) GetNextPageNumber(pageNumber int) int {
|
||||||
|
if posts.GetMaxPageNumber() < pageNumber+1 {
|
||||||
|
return pageNumber
|
||||||
|
} else {
|
||||||
|
return pageNumber + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (posts Posts) GetMaxPageNumber() int {
|
||||||
|
if len(posts) == 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
pages := len(posts) / config.Cfg.MaxPostsOnPage
|
||||||
|
if len(posts)%config.Cfg.MaxPostsOnPage != 0 {
|
||||||
|
pages++
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Posts) TagsMap() map[string]int {
|
||||||
|
tagsCount := make(map[string]int)
|
||||||
|
|
||||||
|
for _, post := range p {
|
||||||
|
for _, tag := range strings.Fields(post.Tags) {
|
||||||
|
if tag != "" {
|
||||||
|
tagsCount[tag]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подсписок постов по номеру страницы
|
||||||
|
func (posts Posts) GetSublistByPageNumber(pageNumber int) Posts {
|
||||||
|
// Рассчитываем индекс начала и конца среза
|
||||||
|
if pageNumber < 1 {
|
||||||
|
return posts[0:config.Cfg.MaxPostsOnPage]
|
||||||
|
}
|
||||||
|
startIndex := (pageNumber - 1) * config.Cfg.MaxPostsOnPage
|
||||||
|
endIndex := startIndex + config.Cfg.MaxPostsOnPage
|
||||||
|
// Проверяем, не превышает ли конец доступный размер
|
||||||
|
if startIndex >= len(posts) {
|
||||||
|
// Возвращаем пустой срез, если номер страницы вне допустимого диапазона
|
||||||
|
return Posts{}
|
||||||
|
}
|
||||||
|
if endIndex > len(posts) {
|
||||||
|
// Если конец превышает длину списка, устанавливаем его на длину списка
|
||||||
|
endIndex = len(posts)
|
||||||
|
}
|
||||||
|
// Возвращаем подсписок постов
|
||||||
|
return posts[startIndex:endIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поиск постов по тэгам
|
||||||
|
func (posts Posts) GetPostListBySubstring(substring string) Posts {
|
||||||
|
res := make(Posts, 0, len(posts))
|
||||||
|
// Если первый символ - #, то поиск происходит по тегам
|
||||||
|
if substring = substring + " "; substring[0] == '#' {
|
||||||
|
for i := range posts {
|
||||||
|
// добавляю пробел, чтобы не было ситуации включения одного тега в начало другого
|
||||||
|
if strings.Contains(posts[i].Tags+" ", substring) {
|
||||||
|
res = append(res, posts[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение превью поста
|
||||||
|
func (post Post) PostDescription() template.HTML {
|
||||||
|
const maxLength = 500
|
||||||
|
|
||||||
|
if len(post.Body) > maxLength {
|
||||||
|
spanIndex := strings.Index(string(post.Body), "<span>")
|
||||||
|
min := min(spanIndex, maxLength)
|
||||||
|
if min != -1 {
|
||||||
|
return template.HTML(post.Body[:min] + "...")
|
||||||
|
} else {
|
||||||
|
return template.HTML(post.Body[:maxLength] + "...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return template.HTML(SMALL_POST_DESCR)
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"main/tools"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.hikan.ru/serr/candycache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TGBot struct {
|
||||||
|
Token string // tg bot token
|
||||||
|
ID string // tg chat id
|
||||||
|
}
|
||||||
|
|
||||||
|
func TGNew(token, id string) *TGBot {
|
||||||
|
return &TGBot{
|
||||||
|
Token: token,
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *TGBot) SendMessage(text string) {
|
||||||
|
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", b.Token)
|
||||||
|
|
||||||
|
data := url.Values{}
|
||||||
|
data.Set("chat_id", b.ID)
|
||||||
|
data.Set("text", text)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", apiURL, strings.NewReader(data.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *TGBot) SendFile(filePath, caption string) {
|
||||||
|
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendDocument", b.Token)
|
||||||
|
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
part, err := writer.CreateFormFile("document", filepath.Base(file.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(part, file)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteField("chat_id", b.ID)
|
||||||
|
writer.WriteField("caption", caption)
|
||||||
|
|
||||||
|
err = writer.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", apiURL, body)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Telegram backup. Takes database path, query log, cache
|
||||||
|
func (b *TGBot) Backup(dbpath string, log []string, cache *candycache.Cache) {
|
||||||
|
// Send db
|
||||||
|
b.SendFile(dbpath, "📦 Бэкап базы данных")
|
||||||
|
// Create and send log file
|
||||||
|
if err := tools.StringSliceToFile(log, "log.txt"); err != nil {
|
||||||
|
b.SendMessage(fmt.Sprintf("🔴 Ошибка записи логов в файл: %s", err.Error()))
|
||||||
|
} else {
|
||||||
|
b.SendFile("log.txt", "📃 Лог запросов за последний час")
|
||||||
|
}
|
||||||
|
// Create and send cache dump
|
||||||
|
if err := WriteCacheDumpToFile(cache, "cache.json"); err != nil {
|
||||||
|
b.SendMessage(fmt.Sprintf("🔴 Ошибка записи дампа кэша в файл: %s", err.Error()))
|
||||||
|
} else {
|
||||||
|
b.SendFile("cache.json", "🗄 Дамп кэша")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
for i := range log {
|
||||||
|
log[i] = ""
|
||||||
|
}
|
||||||
|
os.Remove("log.txt")
|
||||||
|
os.Remove("cache.json")
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
{{ template "AJAXadm" . }}
|
||||||
|
|
||||||
|
{{ define "AJAXadm" }}
|
||||||
|
<div class="page-name">
|
||||||
|
C:\<a class="accent"
|
||||||
|
hx-get="/AJAXindex/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/index/1">blog</a>\<a
|
||||||
|
|
||||||
|
class="accent"
|
||||||
|
hx-get="/AJAXadm/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/adm/1">adm</a>\> <span class="cursor">|</span>
|
||||||
|
</div>
|
||||||
|
<div id="div-offset">
|
||||||
|
<span class="gray">Служебная информация:</span>
|
||||||
|
<div>
|
||||||
|
> Твой уровень доступа:
|
||||||
|
<span class="gray">{{ .AccessLvl }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form
|
||||||
|
action="/knock"
|
||||||
|
method="POST"
|
||||||
|
style="display: inline-block;">
|
||||||
|
<button type="submit">
|
||||||
|
Отстучать в тг
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: .5vw; align-items: center;">
|
||||||
|
> Работа с дампами:
|
||||||
|
<form action="/uploadcachedump" method="post" enctype="multipart/form-data">
|
||||||
|
<label class="button" for="file-upload">Загрузить дамп кэша</label>
|
||||||
|
<input type="file" name="file-upload" id="file-upload" accept=".json" onchange="this.form.submit()" />
|
||||||
|
</form>
|
||||||
|
<form
|
||||||
|
action="/loadcachedump"
|
||||||
|
method="GET">
|
||||||
|
<button type="submit">Скачать дамп кэша</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
> Количество кэшированных страниц:
|
||||||
|
<span class="gray">{{ .SiteCtx.Cache.Count }} ({{ (.SiteCtx.Cache.Size) }} bytes)</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form action="/deletefromcache" method="POST" style="display: inline-block;">
|
||||||
|
<input type="text" id="cachekey" name="cachekey" required>
|
||||||
|
<label for="cachekey">
|
||||||
|
← Введи сюда ключ страницы, которую хочешь удалить из кэша
|
||||||
|
</label>
|
||||||
|
<button type="submit">
|
||||||
|
Удалить
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
> Вручную удалить все содержимое кэша:
|
||||||
|
<form
|
||||||
|
action="/cacheclear"
|
||||||
|
method="POST"
|
||||||
|
style="display: inline-block;">
|
||||||
|
<button type="submit">
|
||||||
|
Удалить
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
> Количество постов на сайте:
|
||||||
|
<span class="gray">{{ len .PostsSublist }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
> Время существования сайта:
|
||||||
|
<span class="gray">{{ DaysSinceStartSite }} дней</span> (с 27.05.24)
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Добавление поста:
|
||||||
|
<form action="/addpost" method="POST">
|
||||||
|
<div>
|
||||||
|
<input type="text" id="name" name="name" required>
|
||||||
|
<label for="name">← Название</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="text" id="tags" name="tags" required>
|
||||||
|
<label for="tags">
|
||||||
|
← Теги в формате
|
||||||
|
<span class="gray">#Text1 #Text2... #TextN</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="body">Содержание ↓</label>
|
||||||
|
<textarea id="body" name="body" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit">Добавить</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ template "footer" . }}
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{ template "base_head" . }}
|
||||||
|
{{ template "AJAXadm" . }}
|
||||||
|
{{ template "base_tail" . }}
|
|
@ -0,0 +1,132 @@
|
||||||
|
{{ define "base_head" }}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script>
|
||||||
|
if (window.innerWidth < 1200) { window.location.href = "/mobile/1"; }
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/assets/css/desktop/styles.css?v={{ .SiteCtx.Version }}">
|
||||||
|
<link rel="shortcut icon" href="/assets/pic/favicon.webp?v={{ .SiteCtx.Version }}" type="image/x-icon">
|
||||||
|
<script src="/assets/scripts/htmx.js?v={{ .SiteCtx.Version }}"></script>
|
||||||
|
<script src="/assets/scripts/scripts.js?v={{ .SiteCtx.Version }}"></script>
|
||||||
|
<title>{{ .Title }}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="logo">
|
||||||
|
{{ template "Logo" . }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar">
|
||||||
|
{{ template "Navbar" . }}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div no-scroll="false"
|
||||||
|
hx-on::after-swap="handleScroll(this,0,0);"
|
||||||
|
class="main-content" id="main-content">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "Logo" }}
|
||||||
|
<div class="site-name-and-status">
|
||||||
|
<a hx-get="/AJAXindex/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/index/1">
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="site-name"
|
||||||
|
title="Домен удачно куплен по цене банки пива">
|
||||||
|
<span class="gray">
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
hikan<span class="accent">.</span>ru
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gray">
|
||||||
|
<span class="gray-dull">
|
||||||
|
>>
|
||||||
|
</span>
|
||||||
|
average SA-MP enjoyer
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gray">
|
||||||
|
<span class="gray-dull">
|
||||||
|
>>
|
||||||
|
</span>
|
||||||
|
мобильную версию делать впадлу...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="last-track"
|
||||||
|
hx-get="/api/get_last_track"
|
||||||
|
hx-trigger="load, every 10s"
|
||||||
|
hx-target=".last-track"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
<img src="/assets/pic/noimage.webp?v={{ .SiteCtx.Version }}">
|
||||||
|
<img src="/assets/pic/playing.gif?v={{ .SiteCtx.Version }}" alt="overlay" class="overlay-image">
|
||||||
|
|
||||||
|
<div class="descr" title="Это можно скроллить если описание слишком большое">
|
||||||
|
<div id="subdescr">Загрузка данных...</div>
|
||||||
|
<div id="artist">Загрузка данных...</div>
|
||||||
|
<div id="title"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="/assets/pic/cat_with_siga.webp?v={{ .SiteCtx.Version }}"
|
||||||
|
alt="logo"
|
||||||
|
class="avatar"
|
||||||
|
title="Фото админа">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "Navbar" }}
|
||||||
|
<a class="button"
|
||||||
|
hx-get="/AJAXindex/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/index/1">Лента</a>
|
||||||
|
|
||||||
|
<a class="button"
|
||||||
|
hx-get="/AJAXpost/2"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/post/2">Об авторе</a>
|
||||||
|
|
||||||
|
<a class="button"
|
||||||
|
hx-get="/AJAXtags/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/tags/1">Теги</a>
|
||||||
|
|
||||||
|
<a class="button"
|
||||||
|
hx-get="/AJAXpost/20"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/post/20">Идеи</a>
|
||||||
|
|
||||||
|
<a class="button" target="_blank" href="https://git.hikan.ru/">git.hikan.ru</a>
|
||||||
|
{{ if ne .AccessLvl 0 }}
|
||||||
|
<a class="button"
|
||||||
|
hx-get="/AJAXadm/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/adm/1">Админка</a>
|
||||||
|
|
||||||
|
<a class="button"
|
||||||
|
hx-get="/AJAXlogout/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/logout/1">Выйти</a>
|
||||||
|
{{ else }}
|
||||||
|
<a class="button"
|
||||||
|
hx-get="/AJAXlogin/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/login/1">Войти</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{{ define "base_tail" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: none;" id="meta">{{ .AccessLvl }}</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,42 @@
|
||||||
|
{{ template "AJAXeditpage" . }}
|
||||||
|
|
||||||
|
{{ define "AJAXeditpage" }}
|
||||||
|
<div class="page-name">
|
||||||
|
Редактирование выбранного поста
|
||||||
|
</div>
|
||||||
|
<div id="div-offset">
|
||||||
|
{{ $post := .PostsSublist.GetPostById .Number }}
|
||||||
|
Для редактирования выбран пост
|
||||||
|
<a class="accent"
|
||||||
|
hx-get="/AJAXpost/{{ .Number }}"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/post/{{ .Number }}">
|
||||||
|
{{ $post.Title }}
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<form action="/editpost/{{ .Number }}" method="POST">
|
||||||
|
<div>
|
||||||
|
<input type="text" id="name" name="name" value="{{ $post.Title }}" required>
|
||||||
|
<label for="name">← Название</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="text" id="tags" name="tags" value="{{ $post.Tags }}" required>
|
||||||
|
<label for="tags">← Теги в формате
|
||||||
|
<span class="gray">
|
||||||
|
#Text1 #Text2... #TextN
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="body">Содержание ↓</label>
|
||||||
|
<textarea id="body" name="body" required>{{ $post.Body }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit">Добавить</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ template "footer" . }}
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{ template "base_head" . }}
|
||||||
|
{{ template "AJAXeditpage" . }}
|
||||||
|
{{ template "base_tail" . }}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{{ template "AJAX404" . }}
|
||||||
|
|
||||||
|
{{ define "AJAX404" }}
|
||||||
|
<div class="page-name">
|
||||||
|
C:\<a class="accent"
|
||||||
|
hx-get="/AJAXindex/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/index/1">blog</a>\>
|
||||||
|
<span class="cursor">|</span>
|
||||||
|
</div>
|
||||||
|
<div class="gray">
|
||||||
|
К сожалению, запрашиваемая страничка не найдена (или нет доступа к ней)
|
||||||
|
</div>
|
||||||
|
{{ template "footer" . }}
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{ template "base_head" . }}
|
||||||
|
{{ template "AJAX404" . }}
|
||||||
|
{{ template "base_tail" . }}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{{ define "footer" }}
|
||||||
|
<div class="footer">
|
||||||
|
<span class="gray">
|
||||||
|
Кэшировано {{ getCurTime }} UTC с использованием
|
||||||
|
<a
|
||||||
|
class="accent-dull"
|
||||||
|
target="_blank"
|
||||||
|
href="https://git.hikan.ru/serr/simplecache">
|
||||||
|
candycache
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,29 @@
|
||||||
|
{{ define "renderPost" }}
|
||||||
|
<div class="post">
|
||||||
|
<div class="title">
|
||||||
|
{{ .Title }}
|
||||||
|
</div>
|
||||||
|
<div class="date">
|
||||||
|
Опубликовано: {{ .PostingTime }} -- Обновлено: {{ .UpdateTime }}
|
||||||
|
</div>
|
||||||
|
{{ if ne .Tags "" }}
|
||||||
|
<div class="tags">
|
||||||
|
{{ range SplitString .Tags " " }}{{ . }} {{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
<div class="body">
|
||||||
|
{{ .PostDescription }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "getposts" }}
|
||||||
|
{{ range .PostsSublist.GetSublistByPageNumber .Number }}
|
||||||
|
<a hx-get="/AJAXpost/{{ .ID }}"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/post/{{ .ID }}">
|
||||||
|
{{ template "renderPost" . }}
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,30 @@
|
||||||
|
{{ template "AJAXindex" . }}
|
||||||
|
|
||||||
|
{{ define "AJAXindex" }}
|
||||||
|
<div class="page-name">
|
||||||
|
C:\<a class="accent"
|
||||||
|
hx-get="/AJAXindex/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/index/1"
|
||||||
|
>blog</a>\> <span class="cursor">|</span>
|
||||||
|
</div>
|
||||||
|
{{ template "getposts" . }}
|
||||||
|
<div class="pagination">
|
||||||
|
{{ if ne .Number 1 }}
|
||||||
|
<a class="accent"
|
||||||
|
hx-get="/AJAXindex/{{ .PostsSublist.GetPrevPageNumber .Number}}"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/index/{{ .PostsSublist.GetPrevPageNumber .Number}}">← Новые посты</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ if ne .Number .PostsSublist.GetMaxPageNumber }}
|
||||||
|
<a class="accent"
|
||||||
|
hx-get="/AJAXindex/{{ .PostsSublist.GetNextPageNumber .Number}}"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/index/{{ .PostsSublist.GetNextPageNumber .Number}}">Старые посты →</a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ template "footer" . }}
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{ template "base_head" . }}
|
||||||
|
{{ template "AJAXindex" . }}
|
||||||
|
{{ template "base_tail" . }}
|
|
@ -0,0 +1,37 @@
|
||||||
|
{{ template "AJAXlogin" . }}
|
||||||
|
|
||||||
|
{{ define "AJAXlogin" }}
|
||||||
|
<div class="page-name">
|
||||||
|
C:\<a class="accent"
|
||||||
|
hx-get="/AJAXindex/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/index/1"
|
||||||
|
>blog</a>\<a
|
||||||
|
class="accent"
|
||||||
|
hx-get="/AJAXlogin/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/login/1">login</a>\>
|
||||||
|
<span class="cursor">|</span>
|
||||||
|
</div>
|
||||||
|
<div id="div-offset">
|
||||||
|
<span class="gray">
|
||||||
|
Возможности регистрации нет, аккаунты создаются администратором
|
||||||
|
</span>
|
||||||
|
<form action="/login" method="POST">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="gray">Логин:</label>
|
||||||
|
<input type="text" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="gray">Пароль:</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit">Войти</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{ template "footer" . }}
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{ template "base_head" . }}
|
||||||
|
{{ template "AJAXlogin" . }}
|
||||||
|
{{ template "base_tail" . }}
|
|
@ -0,0 +1,28 @@
|
||||||
|
{{ template "AJAXlogout" . }}
|
||||||
|
|
||||||
|
{{ define "AJAXlogout" }}
|
||||||
|
<div class="page-name">
|
||||||
|
C:\<a class="accent"
|
||||||
|
hx-get="/AJAXindex/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/index/1">blog</a>\<a
|
||||||
|
class="accent"
|
||||||
|
hx-get="/AJAXlogout/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/logout/1">logout</a>\>
|
||||||
|
<span class="cursor">|</span>
|
||||||
|
</div>
|
||||||
|
<div id="div-offset">
|
||||||
|
<span class="gray">
|
||||||
|
Возможности регистрации нет, аккаунты создаются администратором
|
||||||
|
</span>
|
||||||
|
<form action="/logout" method="POST">
|
||||||
|
<div>
|
||||||
|
<button type="submit">Выйти</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{ template "footer" . }}
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{ template "base_head" . }}
|
||||||
|
{{ template "AJAXlogout" . }}
|
||||||
|
{{ template "base_tail" . }}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{{ define "mail" }}
|
||||||
|
<div class="mail">
|
||||||
|
<div style="display: none;" id="response"></div>
|
||||||
|
<form hx-post="/send" hx-target="#response"
|
||||||
|
hx-on::before-request="mailBeforeRequest(this);"
|
||||||
|
hx-on::after-request="mailAfterRequest(this);">
|
||||||
|
<textarea
|
||||||
|
name="body" maxlength="200" required
|
||||||
|
placeholder="Почтовый ящик! Здесь можно оставить анонимное сообщение админу"
|
||||||
|
hx-on:keydown="enterBlock(event);"></textarea>
|
||||||
|
<button type="submit">➤</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="shortcut icon" href="/assets/favicon.ico">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/assets/css/mobile/mobile.css?v=1.7">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="content">
|
||||||
|
<div>
|
||||||
|
Доступ к <span class="pseudo-link">hikan.ru</span> с мобильных устройств временно закрыт!
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Попробуй зайти с компа - скорее всего получится 💻
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,80 @@
|
||||||
|
{{ template "AJAXpost" . }}
|
||||||
|
|
||||||
|
{{ define "AJAXpost" }}
|
||||||
|
{{ $post := .PostsSublist.GetPostById .Number }}
|
||||||
|
<div class="page-name">
|
||||||
|
C:\<a class="accent"
|
||||||
|
hx-get="/AJAXindex/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/index/1">blog</a>\<a
|
||||||
|
class="accent"
|
||||||
|
hx-get="/AJAXpost/{{ .Number }}"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/post/{{ .Number }}">{{ $post.Title }}</a>\>
|
||||||
|
<span class="cursor">|</span>
|
||||||
|
</div>
|
||||||
|
<div class="post-container">
|
||||||
|
<div class="post-header">
|
||||||
|
{{ if ne .AccessLvl 0 }}
|
||||||
|
<div class="content-control">
|
||||||
|
{{ template "ContentControl" . }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if ne $post.Tags "" }}
|
||||||
|
<div class="tags">
|
||||||
|
{{ range SplitString $post.Tags " " }}{{ . }} {{ end }} |
|
||||||
|
Опубликовано: {{ $post.PostingTime }} -- Обновлено: {{ $post.UpdateTime }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: -1vw;">
|
||||||
|
{{ $post.Body }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ template "mail" . }}
|
||||||
|
<div class="read-other-posts">
|
||||||
|
<div id="right-border"></div>
|
||||||
|
<span>READ OTHER POSTS</span>
|
||||||
|
<div id="left-border"></div>
|
||||||
|
</div>
|
||||||
|
{{ if ne .Number 1 }}
|
||||||
|
{{ $nextPost := .PostsSublist.GetPostById (add .Number -1) }}
|
||||||
|
<div class="next-post">
|
||||||
|
<a
|
||||||
|
hx-get="/AJAXpost/{{ add .Number -1 }}"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/post/{{ add .Number -1 }}">
|
||||||
|
{{ $nextPost.Title }} →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ template "footer" . }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "ContentControl" }}
|
||||||
|
{{ $post := .PostsSublist.GetPostById .Number }}
|
||||||
|
<span class="gray">Управление контентом: </span>
|
||||||
|
<form
|
||||||
|
action="/delpost/{{ .Number }}"
|
||||||
|
method="POST">
|
||||||
|
<button
|
||||||
|
id="del-submit"
|
||||||
|
type="submit"
|
||||||
|
style="display: none;"
|
||||||
|
class="submit-button">
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
style="cursor: pointer;"
|
||||||
|
onclick="document.getElementById('del-submit').click();">
|
||||||
|
🗑️
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
<a
|
||||||
|
hx-get="/AJAXeditpage/{{ .Number }}"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/editpage/{{ .Number }}">🖍️</a>
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{ template "base_head" . }}
|
||||||
|
{{ template "AJAXpost" . }}
|
||||||
|
{{ template "base_tail" . }}
|
|
@ -0,0 +1,49 @@
|
||||||
|
{{ template "AJAXsearch" . }}
|
||||||
|
|
||||||
|
{{ define "AJAXsearch" }}
|
||||||
|
<div class="page-name">
|
||||||
|
C:\<a class="accent"
|
||||||
|
hx-get="/AJAXindex/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/index/1">blog</a>\<a
|
||||||
|
class="accent"
|
||||||
|
hx-get="/AJAXtags/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/tags/1">tags</a>\<a
|
||||||
|
class="accent"
|
||||||
|
hx-get="/AJAXsearch/1?search={{ urlQueryEscape .Data }}"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/search/1?search={{ .Data }}">{{ .Data }}</a>\>
|
||||||
|
<span class="cursor"> |</span>
|
||||||
|
</div>
|
||||||
|
<div style="overflow-wrap: break-word;">
|
||||||
|
<span class="gray">Посты, отмеченные тегом {{ .Data }}</span>
|
||||||
|
</div>
|
||||||
|
{{ template "getposts" . }}
|
||||||
|
{{ if or (ne .Number 1) (ne .Number .PostsSublist.GetMaxPageNumber) }}
|
||||||
|
<div class="pagination">
|
||||||
|
{{ if ne .Number 1 }}
|
||||||
|
<a class="accent"
|
||||||
|
hx-get="/AJAXsearch/{{ .PostsSublist.GetPrevPageNumber .Number }}/?search={{ urlQueryEscape .Data }}"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/search/{{ .PostsSublist.GetPrevPageNumber .Number }}/?search={{ .Data }}">
|
||||||
|
← Новые посты
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ if ne .Number .PostsSublist.GetMaxPageNumber }}
|
||||||
|
<a class="accent"
|
||||||
|
hx-get="/AJAXsearch/{{ .PostsSublist.GetNextPageNumber .Number }}/?search={{ urlQueryEscape .Data }}"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/search/{{ .PostsSublist.GetNextPageNumber .Number }}/?search={{ .Data }}">
|
||||||
|
Старые посты →
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ template "footer" . }}
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{ template "base_head" . }}
|
||||||
|
{{ template "AJAXsearch" . }}
|
||||||
|
{{ template "base_tail" . }}
|
|
@ -0,0 +1,37 @@
|
||||||
|
{{ template "AJAXtags" . }}
|
||||||
|
|
||||||
|
{{ define "AJAXtags" }}
|
||||||
|
<div class="page-name">
|
||||||
|
C:\<a class="accent"
|
||||||
|
hx-get="/AJAXindex/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/index/1">blog</a>\<a
|
||||||
|
class="accent"
|
||||||
|
hx-get="/AJAXtags/1"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/tags/1">tags</a>\>
|
||||||
|
<span class="cursor">|</span>
|
||||||
|
</div>
|
||||||
|
<div class="gray">
|
||||||
|
Все теги, используемые на сайте в данный момент:
|
||||||
|
</div>
|
||||||
|
<div id="div-offset">
|
||||||
|
<div>
|
||||||
|
{{ range $key, $value := .SiteCtx.Tags }}
|
||||||
|
<div>
|
||||||
|
~ <a
|
||||||
|
class="accent"
|
||||||
|
hx-get="/AJAXsearch/1?search={{ urlQueryEscape $key }}"
|
||||||
|
hx-target=".main-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="/search/1?search={{ $key }}">
|
||||||
|
{{ $key }} ({{ $value }})
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ template "footer" . }}
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{ template "base_head" . }}
|
||||||
|
{{ template "AJAXtags" . }}
|
||||||
|
{{ template "base_tail" . }}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tdewolff/minify"
|
||||||
|
"github.com/tdewolff/minify/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Сжимает html страницу переданную как строку
|
||||||
|
func MinifyStringHtml(page string) (string, error) {
|
||||||
|
m := minify.New()
|
||||||
|
m.AddFunc("text/html", html.Minify)
|
||||||
|
compressedPage, err := m.String("text/html", page)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return compressedPage, nil
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsIPInUse(targetIP string) (bool, error) {
|
||||||
|
// Получаем список всех сетевых интерфейсов
|
||||||
|
interfaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, iface := range interfaces {
|
||||||
|
// Получаем адреса для каждого интерфейса
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if strings.Split(addr.String(), "/")[0] == targetIP {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil // Адрес не найден
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func StringSliceToFile(stringSlice []string, filename string) error {
|
||||||
|
file, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if len(stringSlice) == 0 {
|
||||||
|
if _, err := file.WriteString("None"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, entry := range stringSlice {
|
||||||
|
if _, err := file.WriteString(entry); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Sha256HashString(password string) (string, error) {
|
||||||
|
hasher := sha256.New()
|
||||||
|
hasher.Write([]byte(password))
|
||||||
|
hashed := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
return hashed, nil
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Принимает значение интервала и выполняемю функцию
|
||||||
|
func Ticker(duration time.Duration, f func()) {
|
||||||
|
ticker := time.NewTicker(duration)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func GetCurTime() string {
|
||||||
|
return time.Now().UTC().Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Количество дней от создания блога до сегодняшнего дня
|
||||||
|
func DaysSinceStartSite() int {
|
||||||
|
targetDate := time.Date(2024, time.May, 27, 0, 0, 0, 0, time.UTC)
|
||||||
|
return int(time.Since(targetDate).Hours() / 24)
|
||||||
|
}
|
Loading…
Reference in New Issue