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