This commit is contained in:
2026-10-01 16:42:39 +07:00
parent 5638349623
commit ee15d149bd
31 changed files with 37 additions and 88 deletions
+2 -5
View File
@@ -5,11 +5,8 @@ import "context"
type Message struct {
To string
Subject string
// Body — текстовая версия письма (text/plain).
Body string
// HTML — версия письма для почтовых клиентов (text/html).
// Если пусто, письмо уходит только в text/plain.
HTML string
Body string
HTML string
}
type IEmailSender interface {
+1 -1
View File
@@ -9,7 +9,7 @@ func ButtonHTML(link, label string) string {
</td></tr></table>`
}
// WrapHTML is the shared light/dark email layout used by all service emails.
// WrapHTML возвращает общую светлую/тёмную HTML-обёртку для писем сервиса.
func WrapHTML(content, baseURL string) string {
fontURL := ""
if baseURL != "" {
+2 -2
View File
@@ -95,9 +95,9 @@ func mimeFor(f *File) string {
return mimeFromData(f.Data, f.Name)
}
// mimeFromData — Content-Type по содержимому (первые 512 байт) с fallback
// mimeFromData — Content-Type по содержимому (первые 512 байт) с запасным вариантом
// на расширение. text/* (например, HTML) трактуется как octet-stream и
// уходит в fallback: при публичном скачивании не должно отдаваться как
// уходит в запасной вариант: при публичном скачивании не должно отдаваться как
// html с доверенного домена.
func mimeFromData(data []byte, filename string) string {
detected := http.DetectContentType(data)
+1 -1
View File
@@ -2,7 +2,7 @@ package network_utils
import "net"
// GetLocalIPv4Addresses returns non-loopback IPv4 addresses of the host.
// GetLocalIPv4Addresses возвращает IPv4-адреса хоста, кроме loopback-адресов.
func GetLocalIPv4Addresses() ([]string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
+1 -1
View File
@@ -9,7 +9,7 @@ func ContextWithClaims(ctx context.Context, claims *JWTClaims) context.Context {
return context.WithValue(ctx, claimsContextKey{}, claims)
}
// ClaimsFromContext returns authenticated claims stored in ctx.
// ClaimsFromContext возвращает данные аутентификации, сохранённые в ctx.
func ClaimsFromContext(ctx context.Context) (*JWTClaims, bool) {
claims, ok := ctx.Value(claimsContextKey{}).(*JWTClaims)
return claims, ok && claims != nil
-2
View File
@@ -7,8 +7,6 @@ type subject struct {
roles []string
}
// NewSubject создаёт субъект с заданными ролями (например, для тестов
// авторизации или in-memory проверок прав).
func NewSubject(roles ...string) IHasRole {
return &subject{roles: roles}
}
@@ -16,10 +16,8 @@ import (
const testDomain = "http://storage.test/api/files/"
// testArchive — экземпляр модуля для тестов.
var testArchive = NewScenarioArchive()
// buildScenario собирает сценарий с историей и ссылками на изображения.
func buildScenario() *repos.Scenario {
description := "Детективная история"
image := "cover.png"
@@ -35,7 +33,6 @@ func buildScenario() *repos.Scenario {
}
}
// memoryFiles — замена хранилища для Pack: имя → содержимое.
type memoryFiles map[string][]byte
func (m memoryFiles) get(_ context.Context, name string) ([]byte, error) {
@@ -183,7 +180,7 @@ func TestPackExternalURLsKeptAndNotFetched(t *testing.T) {
}
func TestPackLegacyFullURLImage(t *testing.T) {
// Legacy-данные: полный URL хранилища срезается до базового имени файла.
// Устаревшие данные: полный URL хранилища срезается до базового имени файла.
description := "desc"
scenario := &repos.Scenario{
ID: 1,
@@ -258,7 +255,6 @@ func TestPackImageNameCollision(t *testing.T) {
}
}
// writeZip собирает архив с заданными записями (для негативных тестов Unpack).
func writeZip(t *testing.T, entries map[string][]byte) []byte {
t.Helper()
var buf bytes.Buffer
@@ -278,7 +274,6 @@ func writeZip(t *testing.T, entries map[string][]byte) []byte {
return buf.Bytes()
}
// validScenarioJSON — минимальный корректный scenario.json.
func validScenarioJSON() []byte {
b, _ := json.Marshal(ScenarioJSON{
Version: Version,
@@ -23,7 +23,6 @@ type ScenarioJSON struct {
Story *storytelling.Story `json:"story"`
}
// Bundle — результат разбора входящего архива.
type Bundle struct {
Scenario *ScenarioJSON
Files map[string][]byte
@@ -8,13 +8,11 @@ type IStory interface {
type Story struct {
Background string `json:"background,omitempty"`
// Введение - то, что игрок получает в начале игры
Introduction *Introduction `json:"introduction,omitempty"`
// Закрывашка доступна в редакторе сценария, игрокам не передаётся.
Conclusion *Conclusion `json:"conclusion,omitempty"`
// Список точек
Places []*Place `json:"places"`
// Вопросы дела - список вопросов, на которые команда должна ответить
@@ -24,11 +22,8 @@ type Story struct {
// Вопрос дела - вопрос, который автор сценария добавляет вместе с
// правильным ответом; команда отвечает на вопросы в конце игры
type Question struct {
// Код - идентификатор вопроса, используется при отправке ответов
Code string `json:"code"`
// Текст вопроса
Text string `json:"text"`
// Правильный ответ - виден только автору и организаторам,
@@ -44,11 +39,8 @@ type Conclusion struct {
// Введение - текст и аудио, которые игрок получает в начале игры.
type Introduction struct {
// Текст введения
Text string `json:"text"`
// Аудио введения
Audio string `json:"audio,omitempty"`
}
@@ -57,23 +49,17 @@ type Introduction struct {
// Это может быть персонаж - пострадавший, продавец, полицейский...
// Это может быть описание действия игрока - ответ на вопрос, реакция на выбитую дверь...
type Place struct {
// Код - то что вбивает игрок
Code string `json:"code"`
// Название места, имя персонажа, заданный игроком вопрос...
Name string `json:"name"`
// Текст - получаемая текстовая информация
Text string `json:"text"`
// Картинка - получаемая визуальная информация
Image string `json:"image"`
// Признак скрытой карточки - некоторые места изначально не доступны
Hidden bool `json:"hidden"`
// Приложения - получаемые улики
Applications []*Application `json:"applications,omitempty"`
// Двери к другим точкам - действия или диалоги, открывающие другие точки
@@ -86,11 +72,8 @@ type Place struct {
// Приложение - улика
// Это может быть оружие, отпечатки, фотография...
type Application struct {
// Название улики
Name string `json:"name"`
// Картинка
Image string `json:"image"`
// Тип файла улики: image | pdf | audio.
@@ -99,11 +82,8 @@ type Application struct {
// Дверь - действие или диалог
type Door struct {
// Код
Code string `json:"code"`
// Название - заданный вопрос, проделанное действие...
Name string `json:"name"`
// Некоторые двери изначально не доступны
@@ -114,7 +94,5 @@ type Door struct {
// Аналог карт допуска в "Тайны Аркхема"
type Key struct {
// Название
Name string `json:"name"`
}
-1
View File
@@ -67,7 +67,6 @@ func (s *story) GetStory(
}
}
}
// Собираем ключи
if currentPlace != nil {
for _, key := range currentPlace.Keys {
keys[key.Name] = struct{}{}
@@ -48,18 +48,14 @@ var (
// заменяет пробелы на _, удаляет все остальные символы, кроме цифр (знаки препинания и т.д.)
func Transliterate(text string) string {
// Приводим к нижнему регистру и транслитерируем
result := replacer.Replace(strings.ToLower(text))
// Удаляем все символы, кроме букв латиницы, подчеркивания и цифр
reg := regexp.MustCompile(`[^a-z0-9_]`)
result = reg.ReplaceAllString(result, "")
// Удаляем множественные подчеркивания (опционально)
regMulti := regexp.MustCompile(`_+`)
result = regMulti.ReplaceAllString(result, "_")
// Удаляем подчеркивания в начале и конце строки (опционально)
result = strings.Trim(result, "_")
return result
+1 -1
View File
@@ -57,7 +57,7 @@ func Generate(cards []Card) ([]byte, error) {
}
x := margin + float64(i%3)*width
y := margin + float64(i%12/3)*height
// Bitmap включает свободное поле QR; векторные квадраты сохраняют
// Растровый режим включает свободное поле QR; векторные квадраты сохраняют
// чёткость при печати без растрового масштабирования.
bitmap := qr.Bitmap()
pixel := qrSize / float64(len(bitmap))