- убраны клиент бота, транспорт обновлений, отправитель сообщений, сборка входа при старте, секция настроек и зависимость go-telegram-bot-api; из конвейера ушла доставка ответа отправителю — исход виден опросом готовности. Колонки адресата и значение источника остались в схеме: применённые шаги не переписываются - шаг 202608140003 запрещает пустого владельца у аудиозаписи и у файла; существующие строки он не проверяет, и это принято сознательно — искать их надо запросом до выкладки - ревью нашло два пред-существующих дефекта, оба закрыты: пустой второй ответ распознавателя стирал сохранённую расшифровку, а пустая расшифровка перестала быть заметной вместе с убранной доставкой. Попутно поднят golang.org/x/image до v0.45.0 — красный шаг vulns, воспроизводился и на чистом master
171 lines
6.7 KiB
Go
171 lines
6.7 KiB
Go
package pocketbase
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/pocketbase/dbx"
|
|
"github.com/pocketbase/pocketbase/core"
|
|
|
|
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
|
"git.vakhrushev.me/av/transcriber/internal/entity"
|
|
)
|
|
|
|
type TextRepository struct {
|
|
app core.App
|
|
}
|
|
|
|
func NewTextRepository(app core.App) *TextRepository {
|
|
return &TextRepository{app: app}
|
|
}
|
|
|
|
// Put кладёт текст записи, заменяя прежний того же вида.
|
|
//
|
|
// Замена, а не вставка: пара «запись и вид» уникальна, и повтор прерванного шага
|
|
// иначе завёл бы второй комплект строк — тогда вопрос «какой текст отдавать
|
|
// человеку» стал бы вопросом порядка записи, а не состояния.
|
|
//
|
|
// **Пустое не кладётся поверх непустого**, и это не осторожность, а защита
|
|
// архива. Повторный опрос той же операции — обычное дело: держатель захвата
|
|
// умер, сохранение рубежа отказало, человек снял остановку в панели. Провайдер
|
|
// при этом вправе ответить пустым потоком, отказом это не считается, и
|
|
// безусловная замена стирала бы сохранённую расшифровку живого человека без
|
|
// следа и без возврата. Та же защита стоит у сырого ответа провайдера
|
|
// (`RecognitionRepository.Finish`), и разное правило у двух хранителей одного
|
|
// результата читалось бы как недосмотр.
|
|
func (repo *TextRepository) Put(recordID, kind, contents string) (*entity.Text, error) {
|
|
collection, err := findCollection(repo.app, migrations.TextsCollection)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
record, err := repo.app.FindFirstRecordByFilter(
|
|
migrations.TextsCollection,
|
|
"record = {:record} && kind = {:kind}",
|
|
dbx.Params{"record": recordID, "kind": kind},
|
|
)
|
|
switch {
|
|
case err == nil:
|
|
// Строка есть — заменяем содержимое.
|
|
case errors.Is(err, sql.ErrNoRows):
|
|
record = core.NewRecord(collection)
|
|
record.Set("record", recordID)
|
|
record.Set("kind", kind)
|
|
default:
|
|
// Отказ хранилища «строкой нет» не является, и подменять его вставкой
|
|
// нельзя: она упрётся в уникальный индекс, и наверх уедет жалоба на
|
|
// запись вместо правды о недоступной базе.
|
|
return nil, fmt.Errorf("failed to look up text of kind %s for record %s: %w", kind, recordID, err)
|
|
}
|
|
// Прежнее непустое содержимое пустым не заменяется: строка остаётся как
|
|
// есть, и вызывающий получает её обратно.
|
|
if contents == "" && record.GetString("contents") != "" {
|
|
return textFromRecord(record), nil
|
|
}
|
|
record.Set("contents", contents)
|
|
|
|
if err := repo.app.Save(record); err != nil {
|
|
// Текст расшифровки наружу не выходит даже отказом: цепочка `%w` от
|
|
// хранилища несёт значение поля.
|
|
return nil, fmt.Errorf("failed to store text of kind %s for record %s", kind, recordID)
|
|
}
|
|
|
|
return textFromRecord(record), nil
|
|
}
|
|
|
|
func (repo *TextRepository) GetByID(id string) (*entity.Text, error) {
|
|
record, err := repo.app.FindRecordById(migrations.TextsCollection, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get text %s: %w", id, err)
|
|
}
|
|
return textFromRecord(record), nil
|
|
}
|
|
|
|
func textFromRecord(record *core.Record) *entity.Text {
|
|
return &entity.Text{
|
|
Id: record.Id,
|
|
RecordID: record.GetString("record"),
|
|
Kind: record.GetString("kind"),
|
|
Contents: record.GetString("contents"),
|
|
}
|
|
}
|
|
|
|
type StructureRepository struct {
|
|
app core.App
|
|
}
|
|
|
|
func NewStructureRepository(app core.App) *StructureRepository {
|
|
return &StructureRepository{app: app}
|
|
}
|
|
|
|
// Put кладёт структуру реплик, заменяя прежнюю той же версии разбора. Довод тот
|
|
// же, что и у текста: повтор шага не должен заводить второй строки.
|
|
func (repo *StructureRepository) Put(recordID string, version int, replicas []entity.Replica) (*entity.Structure, error) {
|
|
collection, err := findCollection(repo.app, migrations.StructuresCollection)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
contents, err := json.Marshal(replicas)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encode structure of record %s", recordID)
|
|
}
|
|
|
|
record, err := repo.app.FindFirstRecordByFilter(
|
|
migrations.StructuresCollection,
|
|
"record = {:record} && version = {:version}",
|
|
dbx.Params{"record": recordID, "version": version},
|
|
)
|
|
switch {
|
|
case err == nil:
|
|
// Строка есть — заменяем содержимое. Пустой перечень реплик поверх
|
|
// непустого не кладётся по тому же доводу, что и у текста: повторный
|
|
// опрос с пустым ответом провайдера стирал бы разбор живой записи.
|
|
if len(replicas) == 0 && len(record.GetString("contents")) > len("[]") {
|
|
return repo.GetByID(record.Id)
|
|
}
|
|
case errors.Is(err, sql.ErrNoRows):
|
|
record = core.NewRecord(collection)
|
|
record.Set("record", recordID)
|
|
record.Set("version", version)
|
|
default:
|
|
return nil, fmt.Errorf("failed to look up structure of record %s: %w", recordID, err)
|
|
}
|
|
record.Set("contents", string(contents))
|
|
|
|
if err := repo.app.Save(record); err != nil {
|
|
return nil, fmt.Errorf("failed to store structure of record %s", recordID)
|
|
}
|
|
|
|
return &entity.Structure{
|
|
Id: record.Id,
|
|
RecordID: recordID,
|
|
Version: version,
|
|
Replicas: replicas,
|
|
}, nil
|
|
}
|
|
|
|
func (repo *StructureRepository) GetByID(id string) (*entity.Structure, error) {
|
|
record, err := repo.app.FindRecordById(migrations.StructuresCollection, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get structure %s: %w", id, err)
|
|
}
|
|
|
|
var replicas []entity.Replica
|
|
raw := record.GetString("contents")
|
|
if raw != "" {
|
|
if err := json.Unmarshal([]byte(raw), &replicas); err != nil {
|
|
return nil, fmt.Errorf("failed to decode structure %s", id)
|
|
}
|
|
}
|
|
|
|
return &entity.Structure{
|
|
Id: record.Id,
|
|
RecordID: record.GetString("record"),
|
|
Version: record.GetInt("version"),
|
|
Replicas: replicas,
|
|
}, nil
|
|
}
|