feat: configurable call timeout & username normalization
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -11,11 +11,7 @@ data/*.lock
|
|||||||
data/*.signal
|
data/*.signal
|
||||||
|
|
||||||
# Runtime Data
|
# Runtime Data
|
||||||
data/*.madeline
|
data/
|
||||||
data/*.madeline.*
|
|
||||||
data/*.mp3
|
|
||||||
data/*.wav
|
|
||||||
data/*.log
|
|
||||||
redis_data/
|
redis_data/
|
||||||
|
|
||||||
# Dependencies (if installed locally)
|
# Dependencies (if installed locally)
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -7,9 +7,10 @@ A dockerized application that converts text to audio (using Piper Neural TTS) an
|
|||||||
* **High Quality TTS**: Uses [Piper](https://github.com/rhasspy/piper) with `es_ES-sharvard-medium` model for human-like speech.
|
* **High Quality TTS**: Uses [Piper](https://github.com/rhasspy/piper) with `es_ES-sharvard-medium` model for human-like speech.
|
||||||
* **Web Dashboard**: Manage queue, view history, and test voices via browser.
|
* **Web Dashboard**: Manage queue, view history, and test voices via browser.
|
||||||
* **Web-based Login**: Log in to your Telegram account directly from the UI (supports 2FA).
|
* **Web-based Login**: Log in to your Telegram account directly from the UI (supports 2FA).
|
||||||
* **Web-based Configuration**: Set `API_ID` and `API_HASH` without touching config files.
|
* **Web-based Configuration**: Set `API_ID`, `API_HASH`, and `Call Timeout` via the UI.
|
||||||
|
* **Flexible Inputs**: Auto-handles usernames with or without `@` prefix.
|
||||||
* **Real-time Queue**: Redis-backed queue with history and error reporting.
|
* **Real-time Queue**: Redis-backed queue with history and error reporting.
|
||||||
* **Smart Dialing**: Waits for the user to answer before playing audio.
|
* **Smart Dialing**: Configurable timeout (default 15s), detects "Rejected" vs "No Answer", and watches for user hang-ups during playback.
|
||||||
* **Legacy API Support**: Compatible with simple GET requests.
|
* **Legacy API Support**: Compatible with simple GET requests.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
@@ -39,26 +40,28 @@ A dockerized application that converts text to audio (using Piper Neural TTS) an
|
|||||||
|
|
||||||
### 1. Initial Setup
|
### 1. Initial Setup
|
||||||
1. Open your browser and navigate to `http://localhost:8777`.
|
1. Open your browser and navigate to `http://localhost:8777`.
|
||||||
2. You will be prompted to enter your **Telegram API ID** and **API Hash**.
|
2. You will be prompted to enter your **Telegram API ID**, **API Hash**, **Phone Number**, and **Call Timeout**.
|
||||||
* Get these from [my.telegram.org](https://my.telegram.org).
|
* Get API keys from [my.telegram.org](https://my.telegram.org).
|
||||||
|
* **Call Timeout**: Seconds to wait for the user to answer before cancelling (default 15s).
|
||||||
3. Click **Save Configuration**. The worker will restart.
|
3. Click **Save Configuration**. The worker will restart.
|
||||||
|
|
||||||
### 2. Login
|
### 2. Login
|
||||||
1. After configuration, the dashboard will show a **Login Now** button.
|
1. After configuration, the dashboard will verify calls.
|
||||||
2. Enter your **Phone Number** (international format, e.g., `+34...`).
|
2. Follow the simple 2-step process (Config -> Login) if not auto-logged in.
|
||||||
3. Scan the **QR Code** using your Telegram Mobile App (Settings > Devices > Link Desktop Device).
|
3. Enter the **Login Code** sent to your Telegram app.
|
||||||
* *Note: If the QR code expires or fails, check `docker logs telemovris_app` for details.*
|
4. If you have 2FA enabled, you will be prompted for your password.
|
||||||
|
|
||||||
### 3. Sending Messages
|
### 3. Sending Messages
|
||||||
* **Via Dashboard**: Use the "Queue New Task" form.
|
* **Via Dashboard**: Use the "Queue New Task" form.
|
||||||
|
* Username can be `@username` or just `username`.
|
||||||
* **Via Legacy API**:
|
* **Via Legacy API**:
|
||||||
```
|
```
|
||||||
GET http://localhost:8777/sendmessage?user=@username&message=Hello+World&audio=yes
|
GET http://localhost:8777/sendmessage?user=username&message=Hello+World&audio=yes
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
* **Reset Config**: If you need to change accounts or keys, click the "Reset Config" button in the dashboard (when disconnected).
|
* **Reset Config**: If you need to change accounts, keys, or timeout settings, click the "Reset Config" button in the dashboard.
|
||||||
* **Logs**: Check worker logs for debugging:
|
* **Logs**: Check worker logs for debugging:
|
||||||
```bash
|
```bash
|
||||||
docker logs -f telemovris_app
|
docker logs -f telemovris_app
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -32,11 +32,50 @@
|
|||||||
<label>App api_hash</label>
|
<label>App api_hash</label>
|
||||||
<input type="text" name="api_hash" class="form-control" placeholder="abcdef123456..." required>
|
<input type="text" name="api_hash" class="form-control" placeholder="abcdef123456..." required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Phone Number (with +)</label>
|
||||||
|
<input type="text" name="phone" class="form-control" placeholder="+1234567890" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Call Timeout (seconds)</label>
|
||||||
|
<input type="number" name="call_timeout" class="form-control" value="15" required>
|
||||||
|
</div>
|
||||||
<button type="submit" class="btn btn-warning">Save Configuration</button>
|
<button type="submit" class="btn btn-warning">Save Configuration</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Login Modal/Form -->
|
||||||
|
<div id="loginForm" class="card mt-4" style="display:none;">
|
||||||
|
<div class="card-header bg-primary text-white">Login Required</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="loginStep1">
|
||||||
|
<p>Logging in with phone number from config...</p>
|
||||||
|
<div class="spinner-border text-primary" role="status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loginStep2" style="display:none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Enter Login Code</label>
|
||||||
|
<input type="text" id="loginCode" class="form-control" placeholder="12345">
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button onclick="submitLoginCode()" class="btn btn-primary">Verify Code</button>
|
||||||
|
<button onclick="startLogin()" class="btn btn-outline-secondary">Resend Code (Retry)</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loginStep3" style="display:none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Two-Factor Password</label>
|
||||||
|
<input type="password" id="loginPassword" class="form-control">
|
||||||
|
</div>
|
||||||
|
<button onclick="submitLoginPassword()" class="btn btn-primary">Submit Password</button>
|
||||||
|
</div>
|
||||||
|
<div id="loginMsg" class="mt-2 text-danger"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="mainDashboard" class="card mt-4" style="display:none;">
|
<div id="mainDashboard" class="card mt-4" style="display:none;">
|
||||||
<div class="card-header">Queue New Task</div>
|
<div class="card-header">Queue New Task</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -111,9 +150,15 @@
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const banner = document.getElementById('statusBanner');
|
const banner = document.getElementById('statusBanner');
|
||||||
|
|
||||||
|
const loginForm = document.getElementById('loginForm');
|
||||||
const mainDashboard = document.getElementById('mainDashboard');
|
const mainDashboard = document.getElementById('mainDashboard');
|
||||||
const setupForm = document.getElementById('setupForm');
|
const setupForm = document.getElementById('setupForm');
|
||||||
|
|
||||||
|
// If user is currently logging in (modal visible), don't disrupt them unless we detect a hard state change like logged_in
|
||||||
|
if (loginForm.style.display === 'block' && data.status !== 'logged_in') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.status === 'logged_in') {
|
if (data.status === 'logged_in') {
|
||||||
const name = data.me.first_name || 'User';
|
const name = data.me.first_name || 'User';
|
||||||
banner.className = 'alert alert-success d-flex justify-content-between align-items-center';
|
banner.className = 'alert alert-success d-flex justify-content-between align-items-center';
|
||||||
@@ -126,30 +171,95 @@
|
|||||||
`;
|
`;
|
||||||
mainDashboard.style.display = 'block';
|
mainDashboard.style.display = 'block';
|
||||||
setupForm.style.display = 'none';
|
setupForm.style.display = 'none';
|
||||||
|
loginForm.style.display = 'none'; // Close modal if success
|
||||||
|
|
||||||
} else if (data.status === 'waiting_for_config') {
|
} else if (data.status === 'waiting_for_config') {
|
||||||
banner.className = 'alert alert-danger d-flex justify-content-between align-items-center';
|
banner.className = 'alert alert-danger d-flex justify-content-between align-items-center';
|
||||||
banner.innerText = 'Configuration Missing';
|
banner.innerText = 'Configuration Missing';
|
||||||
mainDashboard.style.display = 'none';
|
mainDashboard.style.display = 'none';
|
||||||
setupForm.style.display = 'block';
|
setupForm.style.display = 'block';
|
||||||
|
loginForm.style.display = 'none';
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
banner.className = 'alert alert-warning d-flex justify-content-between align-items-center';
|
banner.className = 'alert alert-warning d-flex justify-content-between align-items-center';
|
||||||
banner.innerHTML = `
|
banner.innerHTML = `
|
||||||
<span><strong>Disconnected:</strong> Worker is not logged in.</span>
|
<span><strong>Disconnected:</strong> Worker is not logged in.</span>
|
||||||
<div>
|
<div>
|
||||||
<a href="/login" class="btn btn-sm btn-dark">Login Now</a>
|
<button onclick="startLogin()" class="btn btn-sm btn-primary">Login Now</button>
|
||||||
<button onclick="doReset()" class="btn btn-sm btn-outline-danger ms-2">Reset Config</button>
|
<button onclick="doReset()" class="btn btn-sm btn-outline-danger ms-2">Reset Config</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
mainDashboard.style.display = 'block';
|
mainDashboard.style.display = 'block';
|
||||||
setupForm.style.display = 'none';
|
setupForm.style.display = 'none';
|
||||||
|
// Don't auto-hide login form here, let the 'if' above handle it or user close it?
|
||||||
|
// Actually if we are here, it means we are disconnected.
|
||||||
|
// If the user hasn't opened the modal (handled above), ensure it's hidden (default state)
|
||||||
|
if (loginForm.style.display !== 'block') {
|
||||||
|
loginForm.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error("Status check failed", e);
|
console.error("Status check failed", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function startLogin() {
|
||||||
|
document.getElementById('statusBanner').style.display = 'none';
|
||||||
|
document.getElementById('mainDashboard').style.display = 'none';
|
||||||
|
document.getElementById('loginForm').style.display = 'block';
|
||||||
|
document.getElementById('loginStep1').style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Empty body triggers use of config phone
|
||||||
|
const res = await fetch('/login', { method: 'POST' });
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if(json.status === 'code_requested') {
|
||||||
|
document.getElementById('loginStep1').style.display = 'none';
|
||||||
|
document.getElementById('loginStep2').style.display = 'block';
|
||||||
|
} else if (json.status === 'error') {
|
||||||
|
document.getElementById('loginMsg').innerText = json.message;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
document.getElementById('loginMsg').innerText = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitLoginCode() {
|
||||||
|
const code = document.getElementById('loginCode').value;
|
||||||
|
const res = await fetch('/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: 'code=' + encodeURIComponent(code)
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.status === 'success') {
|
||||||
|
location.reload();
|
||||||
|
} else if (json.message && json.message.includes('password')) {
|
||||||
|
document.getElementById('loginStep2').style.display = 'none';
|
||||||
|
document.getElementById('loginStep3').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
document.getElementById('loginMsg').innerText = json.message || 'Error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitLoginPassword() {
|
||||||
|
const password = document.getElementById('loginPassword').value;
|
||||||
|
const res = await fetch('/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: 'password=' + encodeURIComponent(password)
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.status === 'success') {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
document.getElementById('loginMsg').innerText = json.message || 'Error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function doReset() {
|
async function doReset() {
|
||||||
if(!confirm("Are you sure? This will delete the API ID/Hash and require setup again.")) return;
|
if(!confirm("Are you sure? This will delete the API ID/Hash and require setup again.")) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ if ($method === 'POST' && $uri === '/api/queue') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize Username (Add @ if missing and not numeric/phone)
|
||||||
|
$username = trim($data['username']);
|
||||||
|
if (!empty($username) && !is_numeric($username) && $username[0] !== '+' && $username[0] !== '@') {
|
||||||
|
$username = '@' . $username;
|
||||||
|
}
|
||||||
|
$data['username'] = $username;
|
||||||
|
|
||||||
$jobId = uniqid('job_');
|
$jobId = uniqid('job_');
|
||||||
$data['id'] = $jobId;
|
$data['id'] = $jobId;
|
||||||
$data['status'] = 'queued';
|
$data['status'] = 'queued';
|
||||||
@@ -31,7 +38,7 @@ if ($method === 'POST' && $uri === '/api/queue') {
|
|||||||
// Push to history list (for display)
|
// Push to history list (for display)
|
||||||
$redis->lpush('job_history', $jobId);
|
$redis->lpush('job_history', $jobId);
|
||||||
|
|
||||||
echo json_encode(['job_id' => $jobId, 'status' => 'queued']);
|
echo json_encode(['job_id' => $jobId, 'status' => 'queued', 'normalized_username' => $username]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +114,12 @@ if ($method === 'GET' && $uri === '/sendmessage') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize Username (Add @ if missing and not numeric/phone)
|
||||||
|
$user = trim($user);
|
||||||
|
if (!empty($user) && !is_numeric($user) && $user[0] !== '+' && $user[0] !== '@') {
|
||||||
|
$user = '@' . $user;
|
||||||
|
}
|
||||||
|
|
||||||
$audio = 0;
|
$audio = 0;
|
||||||
if ($audioParam && strtolower($audioParam) === 'yes') {
|
if ($audioParam && strtolower($audioParam) === 'yes') {
|
||||||
$audio = 1;
|
$audio = 1;
|
||||||
@@ -144,15 +157,17 @@ if ($method === 'POST' && $uri === '/api/logout') {
|
|||||||
// API: Save Config
|
// API: Save Config
|
||||||
if ($method === 'POST' && $uri === '/api/config') {
|
if ($method === 'POST' && $uri === '/api/config') {
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
if (!isset($data['api_id'], $data['api_hash'])) {
|
if (!isset($data['api_id'], $data['api_hash'], $data['phone'])) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['error' => 'Missing api_id or api_hash']);
|
echo json_encode(['error' => 'Missing api_id, api_hash, or phone']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
file_put_contents('/app/data/config.json', json_encode([
|
file_put_contents('/app/data/config.json', json_encode([
|
||||||
'api_id' => trim($data['api_id']),
|
'api_id' => trim($data['api_id']),
|
||||||
'api_hash' => trim($data['api_hash'])
|
'api_hash' => trim($data['api_hash']),
|
||||||
|
'phone' => trim($data['phone']),
|
||||||
|
'call_timeout' => isset($data['call_timeout']) ? (int)$data['call_timeout'] : 15
|
||||||
]));
|
]));
|
||||||
|
|
||||||
// Force worker restart to pick up new config
|
// Force worker restart to pick up new config
|
||||||
@@ -193,20 +208,27 @@ if ($method === 'POST' && $uri === '/login') {
|
|||||||
// We will use a dedicated session API wrapper or just basic calls if possible.
|
// We will use a dedicated session API wrapper or just basic calls if possible.
|
||||||
// For simplicity, we try to Instantiate MP and check state.
|
// For simplicity, we try to Instantiate MP and check state.
|
||||||
|
|
||||||
|
// Load Config
|
||||||
|
if (!file_exists('/app/data/config.json')) {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Config missing']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$config = json_decode(file_get_contents('/app/data/config.json'), true);
|
||||||
|
|
||||||
$settings = new \danog\MadelineProto\Settings();
|
$settings = new \danog\MadelineProto\Settings();
|
||||||
$settings->setAppInfo((new \danog\MadelineProto\Settings\AppInfo())
|
$settings->setAppInfo((new \danog\MadelineProto\Settings\AppInfo())
|
||||||
->setApiId((int)getenv('API_ID'))
|
->setApiId((int)$config['api_id'])
|
||||||
->setApiHash(getenv('API_HASH'))
|
->setApiHash($config['api_hash'])
|
||||||
);
|
);
|
||||||
|
|
||||||
$MadelineProto = new API('/app/data/session.madeline', $settings);
|
$MadelineProto = new API('/app/data/session.madeline', $settings);
|
||||||
|
|
||||||
$phone = $_POST['phone'] ?? null;
|
$phone = $_POST['phone'] ?? $config['phone'] ?? null;
|
||||||
$code = $_POST['code'] ?? null;
|
$code = $_POST['code'] ?? null;
|
||||||
$password = $_POST['password'] ?? null;
|
$password = $_POST['password'] ?? null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($phone) {
|
if ($phone && !$code && !$password) {
|
||||||
// Create lock file to pause worker
|
// Create lock file to pause worker
|
||||||
touch('/app/data/login.lock');
|
touch('/app/data/login.lock');
|
||||||
|
|
||||||
@@ -227,11 +249,10 @@ if ($method === 'POST' && $uri === '/login') {
|
|||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid Request']);
|
echo json_encode(['status' => 'error', 'message' => 'Invalid Request']);
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Don't remove lock on error immediately, user might retry code?
|
// Remove lock on error so worker can resume or user can retry
|
||||||
// Actually if phoneLogin failed, we should probably remove it?
|
if (file_exists('/app/data/login.lock')) {
|
||||||
// For now, let's keep it simple. If it's a fatal error or user gives up, they might need to restart.
|
unlink('/app/data/login.lock');
|
||||||
// Or we can provide a 'reset' button.
|
}
|
||||||
// Let's just catch and return error.
|
|
||||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ use danog\MadelineProto\Settings\AppInfo;
|
|||||||
|
|
||||||
echo "Worker Initializing...\n";
|
echo "Worker Initializing...\n";
|
||||||
|
|
||||||
|
// Cleanup stale locks
|
||||||
|
if (file_exists('/app/data/login.lock')) unlink('/app/data/login.lock');
|
||||||
|
if (file_exists('/app/data/logout.signal')) unlink('/app/data/logout.signal');
|
||||||
|
|
||||||
$redis->del('telegram:status');
|
$redis->del('telegram:status');
|
||||||
$redis->del('telegram:me');
|
$redis->del('telegram:me');
|
||||||
|
|
||||||
@@ -29,6 +33,8 @@ while (true) {
|
|||||||
sleep(2);
|
sleep(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$redis->set('telegram:status', 'starting');
|
||||||
|
|
||||||
$settings = new Settings();
|
$settings = new Settings();
|
||||||
$settings->setAppInfo((new AppInfo())
|
$settings->setAppInfo((new AppInfo())
|
||||||
->setApiId((int)$config['api_id'])
|
->setApiId((int)$config['api_id'])
|
||||||
@@ -38,6 +44,14 @@ $settings->setAppInfo((new AppInfo())
|
|||||||
$MadelineProto = null;
|
$MadelineProto = null;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
// Check for logout/reset signal
|
||||||
|
if (file_exists('/app/data/logout.signal')) {
|
||||||
|
echo "Worker received logout signal. Logging out...\n";
|
||||||
|
unlink('/app/data/logout.signal');
|
||||||
|
// We might not have an active instance to logout(), so just exit
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
// Check for login lock (Web UI is performing login)
|
// Check for login lock (Web UI is performing login)
|
||||||
if (file_exists('/app/data/login.lock')) {
|
if (file_exists('/app/data/login.lock')) {
|
||||||
echo "Worker detected login in progress. Sleeping...\n";
|
echo "Worker detected login in progress. Sleeping...\n";
|
||||||
@@ -67,9 +81,14 @@ while (true) {
|
|||||||
// Passive Wait Loop
|
// Passive Wait Loop
|
||||||
$waited = 0;
|
$waited = 0;
|
||||||
while ($waited < 60) {
|
while ($waited < 60) {
|
||||||
|
if (file_exists('/app/data/logout.signal')) {
|
||||||
|
echo "Worker received logout signal during wait. Logging out...\n";
|
||||||
|
unlink('/app/data/logout.signal');
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
if (file_exists('/app/data/login.lock')) {
|
if (file_exists('/app/data/login.lock')) {
|
||||||
echo "Worker detected login activity! Retrying...\n";
|
echo "Worker detected login activity! Retrying...\n";
|
||||||
break; // Break inner loop to restart main loop (which handles lock)
|
break; // Break inner loop
|
||||||
}
|
}
|
||||||
sleep(1);
|
sleep(1);
|
||||||
$waited++;
|
$waited++;
|
||||||
@@ -82,9 +101,14 @@ while (true) {
|
|||||||
$MadelineProto = null;
|
$MadelineProto = null;
|
||||||
unset($MadelineProto);
|
unset($MadelineProto);
|
||||||
|
|
||||||
// Passive Wait Loop
|
// Passive Wait Loop Wait Loop
|
||||||
$waited = 0;
|
$waited = 0;
|
||||||
while ($waited < 60) {
|
while ($waited < 60) {
|
||||||
|
if (file_exists('/app/data/logout.signal')) {
|
||||||
|
echo "Worker received logout signal during wait. Logging out...\n";
|
||||||
|
unlink('/app/data/logout.signal');
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
if (file_exists('/app/data/login.lock')) {
|
if (file_exists('/app/data/login.lock')) {
|
||||||
echo "Worker detected login activity! Retrying...\n";
|
echo "Worker detected login activity! Retrying...\n";
|
||||||
break;
|
break;
|
||||||
@@ -148,39 +172,68 @@ while (true) {
|
|||||||
// Request Call (Synchronous return of VoIP controller)
|
// Request Call (Synchronous return of VoIP controller)
|
||||||
$call = $MadelineProto->requestCall($task['username']);
|
$call = $MadelineProto->requestCall($task['username']);
|
||||||
|
|
||||||
echo "Ringing...\n";
|
$timeoutSeconds = isset($config['call_timeout']) ? (int)$config['call_timeout'] : 15;
|
||||||
|
$timeoutLoops = $timeoutSeconds * 2; // 0.5s per loop
|
||||||
|
|
||||||
|
echo "Ringing (Timeout: {$timeoutSeconds}s)...\n";
|
||||||
$answered = false;
|
$answered = false;
|
||||||
for ($i = 0; $i < 60; $i++) {
|
$rejected = false;
|
||||||
|
|
||||||
|
for ($i = 0; $i < $timeoutLoops; $i++) {
|
||||||
try {
|
try {
|
||||||
// Check for answer
|
$state = $call->getCallState();
|
||||||
// Note: STATE_ESTABLISHED is typically 3 or 4. We use the constant.
|
|
||||||
if ($call->getCallState() === \danog\MadelineProto\VoIP::STATE_ESTABLISHED) {
|
// Check for answer (RUNNING or CONFIRMED)
|
||||||
|
if ($state === \danog\MadelineProto\VoIP\CallState::RUNNING || $state === \danog\MadelineProto\VoIP\CallState::CONFIRMED) {
|
||||||
$answered = true;
|
$answered = true;
|
||||||
echo "Call Answered!\n";
|
echo "Call Answered! (State: " . $state->name . ")\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($state === \danog\MadelineProto\VoIP\CallState::ENDED) {
|
||||||
|
echo "Call Rejected or Ended prematurely.\n";
|
||||||
|
$rejected = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Ignore state check errors temporarily
|
// echo "State check error: " . $e->getMessage() . "\n";
|
||||||
}
|
}
|
||||||
sleep(1);
|
usleep(500000); // 0.5s
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$answered) {
|
if ($rejected) {
|
||||||
echo "No answer after 60s. Cancelling.\n";
|
$redis->hset("job:{$task['id']}", 'status', 'rejected');
|
||||||
|
} elseif (!$answered) {
|
||||||
|
echo "No answer after {$timeoutSeconds}s. Cancelling.\n";
|
||||||
$call->discard();
|
$call->discard();
|
||||||
$redis->hset("job:{$task['id']}", 'status', 'no_answer');
|
$redis->hset("job:{$task['id']}", 'status', 'no_answer');
|
||||||
continue;
|
continue;
|
||||||
|
} else {
|
||||||
|
// Answered case
|
||||||
|
// Play Audio AFTER answer
|
||||||
|
$call->play(new \danog\MadelineProto\LocalFile($audioFile));
|
||||||
|
|
||||||
|
// Wait for playback to complete, checking for hangup
|
||||||
|
$duration = TTS::getDuration($audioFile);
|
||||||
|
echo "Audio duration: {$duration}s. Playing...\n";
|
||||||
|
|
||||||
|
$startTime = microtime(true);
|
||||||
|
$waitTime = $duration + 2; // Buffer
|
||||||
|
|
||||||
|
while ((microtime(true) - $startTime) < $waitTime) {
|
||||||
|
try {
|
||||||
|
$state = $call->getCallState();
|
||||||
|
if ($state === \danog\MadelineProto\VoIP\CallState::ENDED) {
|
||||||
|
echo "User hung up during playback.\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Ignore state check errors during playback
|
||||||
|
}
|
||||||
|
usleep(500000); // 0.5s
|
||||||
|
}
|
||||||
|
|
||||||
|
$call->discard();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play Audio AFTER answer
|
|
||||||
$call->play(new \danog\MadelineProto\LocalFile($audioFile));
|
|
||||||
|
|
||||||
// Wait for playback to complete (now accurate since we just started)
|
|
||||||
$duration = TTS::getDuration($audioFile);
|
|
||||||
echo "Audio duration: {$duration}s. Sleeping...\n";
|
|
||||||
sleep((int)ceil($duration) + 2); // Buffer of 2s
|
|
||||||
|
|
||||||
$call->discard();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$redis->hset("job:{$task['id']}", 'status', 'completed');
|
$redis->hset("job:{$task['id']}", 'status', 'completed');
|
||||||
|
|||||||
Reference in New Issue
Block a user