Files
crm-calls/calls/backend/Calls.Api/Recording/CallsRepo.cs

113 lines
6.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.Data.Sqlite;
namespace Calls.Api.Recording;
public sealed record Participant(string Name, bool Guest);
public sealed record CallRow(
string Id, string Room, string WorkspaceId, string Status, long StartedAt, long LastSeenAt, long? EndedAt,
long? RecordStartedAt, string? SessionId, List<Participant> Participants, int Attempts, long NextAttemptAt,
string? Error, string? StoragePath, string? AudioUrl, string? PageId);
/// <summary>Таблица calls: звонок, его запись и шаги обработки (чтобы повторы не делали работу дважды).</summary>
public sealed class CallsRepo(Db db, TimeProvider? time = null)
{
private readonly TimeProvider _time = time ?? TimeProvider.System;
public long Now => _time.GetUtcNow().ToUnixTimeSeconds();
public static readonly string[] OpenStatuses = ["active", "recording"];
public static readonly string[] PendingStatuses = ["ended", "processing", "waiting", "error"];
public CallRow? Open(string room) =>
Query("SELECT * FROM calls WHERE room=$r AND status IN ('active','recording') ORDER BY started_at DESC LIMIT 1", ("$r", room)).FirstOrDefault();
public List<CallRow> AllOpen() => Query("SELECT * FROM calls WHERE status IN ('active','recording')");
public List<CallRow> DuePending(long now) =>
Query("SELECT * FROM calls WHERE status IN ('ended','processing','waiting','error') AND next_attempt_at <= $t ORDER BY started_at", ("$t", now));
public List<CallRow> Recent(int n) => Query("SELECT * FROM calls ORDER BY started_at DESC LIMIT $n", ("$n", n));
public CallRow? Get(string id) => Query("SELECT * FROM calls WHERE id=$i", ("$i", id)).FirstOrDefault();
public CallRow Create(string room, string workspaceId)
{
var id = "c" + Convert.ToHexString(RandomNumberGenerator.GetBytes(10)).ToLowerInvariant();
Exec("""
INSERT INTO calls(id, room, workspace_id, status, started_at, last_seen_at, updated_at)
VALUES ($i,$r,$w,'active',$t,$t,$t)
""", ("$i", id), ("$r", room), ("$w", workspaceId), ("$t", Now));
return Get(id)!;
}
public void Seen(string id, List<Participant> participants) =>
Exec("UPDATE calls SET last_seen_at=$t, participants=$p, updated_at=$t WHERE id=$i",
("$i", id), ("$t", Now), ("$p", JsonSerializer.Serialize(participants)));
public void RecordingStarted(string id, string sessionId) =>
Exec("UPDATE calls SET status='recording', session_id=$s, record_started_at=$t, updated_at=$t WHERE id=$i",
("$i", id), ("$s", sessionId), ("$t", Now));
/// <summary>Конец звонка: с записью — ждём обработку, без записи — нечего обрабатывать.</summary>
public void Ended(string id, long endedAt, bool hadRecording) =>
Exec("UPDATE calls SET status=$st, ended_at=$e, next_attempt_at=0, updated_at=$t WHERE id=$i",
("$i", id), ("$st", hadRecording ? "ended" : "skipped"), ("$e", endedAt), ("$t", Now));
public void SetStatus(string id, string status, string? error = null) =>
Exec("UPDATE calls SET status=$s, error=$er, updated_at=$t WHERE id=$i",
("$i", id), ("$s", status), ("$er", (object?)error ?? DBNull.Value), ("$t", Now));
/// <summary>Ошибка шага: файлы остаются, следующий повтор через растущую паузу (1, 2, 5, 10, 30, 60 мин).</summary>
public void Failed(string id, string error, string status = "error")
{
var row = Get(id);
var attempts = (row?.Attempts ?? 0) + 1;
int[] mins = [1, 2, 5, 10, 30, 60];
var delay = mins[Math.Min(attempts - 1, mins.Length - 1)] * 60;
Exec("UPDATE calls SET status=$s, error=$er, attempts=$a, next_attempt_at=$n, updated_at=$t WHERE id=$i",
("$i", id), ("$s", status), ("$er", error), ("$a", attempts), ("$n", Now + delay), ("$t", Now));
}
/// <summary>Ждём (файлы ещё пишутся, нет настроек): без счётчика ошибок.</summary>
public void Later(string id, string status, string? note, int seconds) =>
Exec("UPDATE calls SET status=$s, error=$er, next_attempt_at=$n, updated_at=$t WHERE id=$i",
("$i", id), ("$s", status), ("$er", (object?)note ?? DBNull.Value), ("$n", Now + seconds), ("$t", Now));
public void SetStoragePath(string id, string path) => Exec("UPDATE calls SET storage_path=$p WHERE id=$i", ("$i", id), ("$p", path));
public void SetAudioUrl(string id, string url) => Exec("UPDATE calls SET audio_url=$u WHERE id=$i", ("$i", id), ("$u", url));
public void SetPageId(string id, string pageId) => Exec("UPDATE calls SET page_id=$p WHERE id=$i", ("$i", id), ("$p", pageId));
public void Done(string id) =>
Exec("UPDATE calls SET status='done', error=NULL, updated_at=$t WHERE id=$i", ("$i", id), ("$t", Now));
private List<CallRow> Query(string sql, params (string, object)[] ps)
{
using var c = db.Open();
using var cmd = c.CreateCommand();
cmd.CommandText = sql;
foreach (var (k, v) in ps) cmd.Parameters.AddWithValue(k, v);
using var r = cmd.ExecuteReader();
var list = new List<CallRow>();
while (r.Read())
{
string? S(string col) => r.IsDBNull(r.GetOrdinal(col)) ? null : r.GetString(r.GetOrdinal(col));
long? L(string col) => r.IsDBNull(r.GetOrdinal(col)) ? null : r.GetInt64(r.GetOrdinal(col));
list.Add(new CallRow(S("id")!, S("room")!, S("workspace_id")!, S("status")!, L("started_at")!.Value, L("last_seen_at")!.Value,
L("ended_at"), L("record_started_at"), S("session_id"),
JsonSerializer.Deserialize<List<Participant>>(S("participants") ?? "[]") ?? [],
(int)L("attempts")!.Value, L("next_attempt_at")!.Value, S("error"), S("storage_path"), S("audio_url"), S("page_id")));
}
return list;
}
private void Exec(string sql, params (string, object)[] ps)
{
using var c = db.Open();
using var cmd = c.CreateCommand();
cmd.CommandText = sql;
foreach (var (k, v) in ps) cmd.Parameters.AddWithValue(k, v);
cmd.ExecuteNonQuery();
}
}