Feat : Streamer watch list
This commit is contained in:
@@ -330,11 +330,38 @@ export interface LogEntry {
|
||||
ts: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Profil surveillé par le serveur : on colle l'URL d'un streamer, le serveur
|
||||
* sonde son statut et signale les passages en direct.
|
||||
*/
|
||||
export interface WatchTarget {
|
||||
id: string;
|
||||
provider: WatchProvider;
|
||||
username: string;
|
||||
/** Nom lisible, à défaut le pseudo. */
|
||||
label: string | null;
|
||||
url: string;
|
||||
/** Agent qui enregistrera ce streamer, s'il est assigné. */
|
||||
agentId: string | null;
|
||||
notify: boolean;
|
||||
state: StreamState;
|
||||
rawStatus: string | null;
|
||||
/** Depuis quand l'état est stable. */
|
||||
stateSince: number;
|
||||
lastCheckedAt: number | null;
|
||||
lastError: string | null;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export type ServerToDashboard =
|
||||
| { type: 'snapshot'; agents: AgentView[]; logs: LogEntry[] }
|
||||
| { type: 'snapshot'; agents: AgentView[]; logs: LogEntry[]; targets: WatchTarget[] }
|
||||
| { type: 'agent'; agent: AgentView }
|
||||
| { type: 'agent.removed'; agentId: string }
|
||||
| { type: 'log'; entry: LogEntry };
|
||||
| { type: 'log'; entry: LogEntry }
|
||||
| { type: 'target'; target: WatchTarget }
|
||||
| { type: 'target.removed'; targetId: string }
|
||||
/** Transition vers le direct : c'est ce qui déclenche la notification. */
|
||||
| { type: 'target.live'; target: WatchTarget };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -401,6 +428,77 @@ export function normalizeWatchSettings(raw: unknown): WatchSettings {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait le pseudo d'un profil Stripchat à partir d'une URL complète, d'une URL
|
||||
* sans schéma, ou d'un pseudo saisi seul.
|
||||
*/
|
||||
export function parseStripchatUsername(input: string): string | null {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const looksLikeUrl = trimmed.includes('/') || trimmed.includes('.');
|
||||
if (looksLikeUrl) {
|
||||
try {
|
||||
const url = new URL(trimmed.includes('://') ? trimmed : `https://${trimmed}`);
|
||||
if (!/(^|\.)stripchat\.com$/i.test(url.hostname)) return null;
|
||||
const first = url.pathname.split('/').filter(Boolean)[0];
|
||||
return first && USERNAME_PATTERN.test(first) ? first : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return USERNAME_PATTERN.test(trimmed) ? trimmed : null;
|
||||
}
|
||||
|
||||
const USERNAME_PATTERN = /^[A-Za-z0-9_.-]{2,64}$/;
|
||||
|
||||
export function stripchatProfileUrl(username: string): string {
|
||||
return `https://fr.stripchat.com/${username}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interroge le statut d'un modèle Stripchat.
|
||||
*
|
||||
* Le champ autoritatif est `user.user.status` sur
|
||||
* `/api/front/v2/models/username/{pseudo}/cam`. Valeurs relevées en production :
|
||||
* `public`, `private`, `p2p`, `groupShow`, `idle`.
|
||||
*
|
||||
* Mutualisé entre l'agent (pause automatique) et le serveur (veille) : une seule
|
||||
* définition de l'endpoint et du chemin du champ à maintenir.
|
||||
*/
|
||||
export async function fetchStripchatStatus(
|
||||
username: string,
|
||||
privateStatuses: string[],
|
||||
timeoutMs = 8000,
|
||||
): Promise<{ raw: string; state: StreamState }> {
|
||||
const url = `https://fr.stripchat.com/api/front/v2/models/username/${encodeURIComponent(
|
||||
username,
|
||||
)}/cam`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
|
||||
accept: 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
|
||||
if (response.status === 404) return { raw: 'notFound', state: 'offline' };
|
||||
if (!response.ok) throw new Error(`API Stripchat : HTTP ${response.status}`);
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
user?: { user?: { status?: string } };
|
||||
};
|
||||
const raw = payload?.user?.user?.status;
|
||||
if (typeof raw !== 'string') {
|
||||
throw new Error('Réponse Stripchat inattendue : user.user.status absent');
|
||||
}
|
||||
|
||||
return { raw, state: mapStreamStatus(raw, privateStatuses) };
|
||||
}
|
||||
|
||||
/** Traduit un statut brut de l'API en état normalisé. */
|
||||
export function mapStreamStatus(raw: string | undefined, privateStatuses: string[]): StreamState {
|
||||
if (!raw) return 'unknown';
|
||||
|
||||
Reference in New Issue
Block a user