113 lines
6.3 KiB
C#
113 lines
6.3 KiB
C#
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();
|
||
}
|
||
}
|