master
serr 2025-02-02 16:43:55 +03:00
commit 1d1173320b
64 changed files with 2814 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
database/scrapyard.db
config/config.json

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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

BIN
assets/pic/closed.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

BIN
assets/pic/favicon.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

BIN
assets/pic/noimage.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

BIN
assets/pic/playing.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

1
assets/scripts/htmx.js Normal file

File diff suppressed because one or more lines are too long

17
assets/scripts/scripts.js Normal file
View File

@ -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();} }

View File

@ -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)
}

View File

@ -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
}

67
benchmarks/stuff.go Normal file
View File

@ -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,
}
}

49
config/config.go Normal file
View File

@ -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
}

1
database/usage.txt Normal file
View File

@ -0,0 +1 @@
folder for database

53
go.mod Normal file
View File

@ -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
)

149
go.sum Normal file
View File

@ -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=

190
main.go Normal file
View File

@ -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
}

47
mvc/controllers/auth.go Normal file
View File

@ -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?Выход выполнен")
})
}

17
mvc/controllers/backup.go Normal file
View File

@ -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?Отстук инициирован")
})
}

79
mvc/controllers/cache.go Normal file
View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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()
}
}

161
mvc/controllers/page.go Normal file
View File

@ -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)
}

76
mvc/controllers/post.go Normal file
View File

@ -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?Пост обновлен")
})
}

View File

@ -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
}

19
mvc/models/cache.go Normal file
View File

@ -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
}

58
mvc/models/context.go Normal file
View File

@ -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
}

91
mvc/models/db.go Normal file
View File

@ -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()
}

123
mvc/models/lastfm.go Normal file
View File

@ -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,
)
}

11
mvc/models/page.go Normal file
View File

@ -0,0 +1,11 @@
package models
// Контекст страницы
type Page struct {
AccessLvl int //уровень доступа юзера
Title string // тайтл странциы
PostsSublist Posts
Number int // номер страницы
Data string // строка с данными (например искомая строка для поиска)
SiteCtx *Site
}

125
mvc/models/post.go Normal file
View File

@ -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)
}

120
mvc/models/telegram.go Normal file
View File

@ -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")
}

View File

@ -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 }}

3
mvc/views/adm/adm.gohtml Normal file
View File

@ -0,0 +1,3 @@
{{ template "base_head" . }}
{{ template "AJAXadm" . }}
{{ template "base_tail" . }}

132
mvc/views/base_head.gohtml Normal file
View File

@ -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 }}

View File

@ -0,0 +1,7 @@
{{ define "base_tail" }}
</div>
</div>
<div style="display: none;" id="meta">{{ .AccessLvl }}</div>
</body>
</html>
{{ end }}

View File

@ -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 }}

View File

@ -0,0 +1,3 @@
{{ template "base_head" . }}
{{ template "AJAXeditpage" . }}
{{ template "base_tail" . }}

View File

@ -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 }}

View File

@ -0,0 +1,3 @@
{{ template "base_head" . }}
{{ template "AJAX404" . }}
{{ template "base_tail" . }}

13
mvc/views/footer.gohtml Normal file
View File

@ -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 }}

29
mvc/views/getposts.gohtml Normal file
View File

@ -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 }}

View File

@ -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 }}

View File

@ -0,0 +1,3 @@
{{ template "base_head" . }}
{{ template "AJAXindex" . }}
{{ template "base_tail" . }}

View File

@ -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 }}

View File

@ -0,0 +1,3 @@
{{ template "base_head" . }}
{{ template "AJAXlogin" . }}
{{ template "base_tail" . }}

View File

@ -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 }}

View File

@ -0,0 +1,3 @@
{{ template "base_head" . }}
{{ template "AJAXlogout" . }}
{{ template "base_tail" . }}

14
mvc/views/mail.gohtml Normal file
View File

@ -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 }}

19
mvc/views/mobile.gohtml Normal file
View File

@ -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>

View File

@ -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 }}

View File

@ -0,0 +1,3 @@
{{ template "base_head" . }}
{{ template "AJAXpost" . }}
{{ template "base_tail" . }}

View File

@ -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 }}

View File

@ -0,0 +1,3 @@
{{ template "base_head" . }}
{{ template "AJAXsearch" . }}
{{ template "base_tail" . }}

View File

@ -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 }}

View File

@ -0,0 +1,3 @@
{{ template "base_head" . }}
{{ template "AJAXtags" . }}
{{ template "base_tail" . }}

17
tools/html.go Normal file
View File

@ -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
}

30
tools/other.go Normal file
View File

@ -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 // Адрес не найден
}

24
tools/slices.go Normal file
View File

@ -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
}

13
tools/string.go Normal file
View File

@ -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
}

12
tools/ticker.go Normal file
View File

@ -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()
}
}

13
tools/time.go Normal file
View File

@ -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)
}