diff --git a/Overlord-Server/public/assets/notifications.js b/Overlord-Server/public/assets/notifications.js
index 7d6468b..8738531 100644
--- a/Overlord-Server/public/assets/notifications.js
+++ b/Overlord-Server/public/assets/notifications.js
@@ -52,6 +52,11 @@ const eventNotifOfflineInput = document.getElementById("event-notif-offline");
const eventNotifPurgatoryInput = document.getElementById("event-notif-purgatory");
const saveEventNotifsBtn = document.getElementById("save-event-notifs");
+const clientEventWebhookInput = document.getElementById("client-event-webhook");
+const clientEventTelegramInput = document.getElementById("client-event-telegram");
+const clientEventPushInput = document.getElementById("client-event-push");
+const saveEventChannelsBtn = document.getElementById("save-event-channels");
+
// Browser permission status bar
const desktopNotifStatusBar = document.getElementById("desktop-notif-status-bar");
const desktopNotifStatusIcon = document.getElementById("desktop-notif-status-icon");
@@ -354,6 +359,9 @@ function applyMySettings(settings) {
if (telegramBotTokenInput) telegramBotTokenInput.value = settings.telegram_bot_token || "";
if (telegramChatIdInput) telegramChatIdInput.value = settings.telegram_chat_id || "";
if (telegramTemplateInput) telegramTemplateInput.value = settings.telegram_template || "";
+ if (clientEventWebhookInput) clientEventWebhookInput.checked = settings.client_event_webhook !== 0;
+ if (clientEventTelegramInput) clientEventTelegramInput.checked = settings.client_event_telegram !== 0;
+ if (clientEventPushInput) clientEventPushInput.checked = settings.client_event_push !== 0;
}
async function loadMySettings() {
@@ -676,6 +684,22 @@ function wireEventNotifSave() {
});
}
+function wireEventChannelsSave() {
+ if (!saveEventChannelsBtn) return;
+ saveEventChannelsBtn.addEventListener("click", async () => {
+ try {
+ await saveMySettings({
+ client_event_webhook: clientEventWebhookInput?.checked ?? true,
+ client_event_telegram: clientEventTelegramInput?.checked ?? true,
+ client_event_push: clientEventPushInput?.checked ?? true,
+ });
+ window.showToast?.("Event delivery channel settings saved", "success", 3000);
+ } catch (err) {
+ window.showToast?.(err?.message || "Failed to save event channel settings", "error", 4000);
+ }
+ });
+}
+
// ── Bootstrap ─────────────────────────────────────────────────────────────────
initWebhookTemplateEditor();
wireKeywordSave();
@@ -684,6 +708,7 @@ wireTelegramSave();
loadMySettings();
loadEventNotifPrefs();
wireEventNotifSave();
+wireEventChannelsSave();
updateDesktopPermissionUi();
wireDesktopPermissionBtn();
initRoleUi();
diff --git a/Overlord-Server/public/notifications.html b/Overlord-Server/public/notifications.html
index 04368e8..2444601 100644
--- a/Overlord-Server/public/notifications.html
+++ b/Overlord-Server/public/notifications.html
@@ -313,6 +313,49 @@
+
+
+
+
+
Client Event Delivery Channels
+
+ Choose which channels receive client lifecycle events (online, offline, purgatory).
+
+
+
+
+
+
+
+
+
+
+
+
"?").join(",");
+ where.push(`(target_client_id IN (${placeholders}) OR target_client_id IS NULL)`);
+ params.push(...filters.allowedClientIds);
+ } else if (filters.deniedClientIds && filters.deniedClientIds.length > 0) {
+ const placeholders = filters.deniedClientIds.map(() => "?").join(",");
+ where.push(`(target_client_id NOT IN (${placeholders}) OR target_client_id IS NULL)`);
+ params.push(...filters.deniedClientIds);
+ }
+
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
const countQuery = `SELECT COUNT(*) as total FROM audit_logs ${whereSql}`;
diff --git a/Overlord-Server/src/db.ts b/Overlord-Server/src/db.ts
index 55c50c4..5c98aab 100644
--- a/Overlord-Server/src/db.ts
+++ b/Overlord-Server/src/db.ts
@@ -1215,16 +1215,31 @@ export function lookupClientByPublicKey(
return { id: row.id, enrollmentStatus: row.enrollment_status };
}
-export function getEnrollmentStats(): {
+export function getEnrollmentStats(opts?: {
+ allowedClientIds?: string[];
+ deniedClientIds?: string[];
+}): {
pending: number;
approved: number;
denied: number;
} {
- const rows = db
- .query<{ status: string; c: number }>(
- `SELECT COALESCE(enrollment_status,'pending') as status, COUNT(*) as c FROM clients GROUP BY enrollment_status`,
- )
- .all();
+ let sql = `SELECT COALESCE(enrollment_status,'pending') as status, COUNT(*) as c FROM clients`;
+ const params: any[] = [];
+
+ if (opts?.allowedClientIds) {
+ if (opts.allowedClientIds.length === 0) return { pending: 0, approved: 0, denied: 0 };
+ const placeholders = opts.allowedClientIds.map(() => "?").join(",");
+ sql += ` WHERE id IN (${placeholders})`;
+ params.push(...opts.allowedClientIds);
+ } else if (opts?.deniedClientIds && opts.deniedClientIds.length > 0) {
+ const placeholders = opts.deniedClientIds.map(() => "?").join(",");
+ sql += ` WHERE id NOT IN (${placeholders})`;
+ params.push(...opts.deniedClientIds);
+ }
+
+ sql += ` GROUP BY enrollment_status`;
+
+ const rows = db.query<{ status: string; c: number }>(sql).all(...params);
const stats = { pending: 0, approved: 0, denied: 0 };
for (const r of rows) {
if (r.status === "approved") stats.approved = Number(r.c);
@@ -1287,7 +1302,10 @@ export function getAllPushSubscriptions(): PushSubscriptionRecord[] {
}));
}
-export function getPendingClients(): {
+export function getPendingClients(opts?: {
+ allowedClientIds?: string[];
+ deniedClientIds?: string[];
+}): {
id: string;
host: string | null;
os: string | null;
@@ -1298,12 +1316,26 @@ export function getPendingClients(): {
keyFingerprint: string | null;
lastSeen: number;
}[] {
+ let sql = `SELECT id, host, os, user, ip, country, public_key as publicKey, key_fingerprint as keyFingerprint, last_seen as lastSeen
+ FROM clients WHERE (enrollment_status='pending' OR enrollment_status IS NULL)`;
+ const params: any[] = [];
+
+ if (opts?.allowedClientIds) {
+ if (opts.allowedClientIds.length === 0) return [];
+ const placeholders = opts.allowedClientIds.map(() => "?").join(",");
+ sql += ` AND id IN (${placeholders})`;
+ params.push(...opts.allowedClientIds);
+ } else if (opts?.deniedClientIds && opts.deniedClientIds.length > 0) {
+ const placeholders = opts.deniedClientIds.map(() => "?").join(",");
+ sql += ` AND id NOT IN (${placeholders})`;
+ params.push(...opts.deniedClientIds);
+ }
+
+ sql += ` ORDER BY last_seen DESC`;
+
return db
- .query
(
- `SELECT id, host, os, user, ip, country, public_key as publicKey, key_fingerprint as keyFingerprint, last_seen as lastSeen
- FROM clients WHERE enrollment_status='pending' OR enrollment_status IS NULL ORDER BY last_seen DESC`,
- )
- .all()
+ .query(sql)
+ .all(...params)
.map((r: any) => ({
id: r.id,
host: r.host,
diff --git a/Overlord-Server/src/main-server.ts b/Overlord-Server/src/main-server.ts
index 382f4f3..5e10985 100644
--- a/Overlord-Server/src/main-server.ts
+++ b/Overlord-Server/src/main-server.ts
@@ -264,6 +264,9 @@ const deliverNotificationWithScreenshotForRecord = (record: NotificationRecord)
telegramBotToken: u.telegram_bot_token || "",
telegramChatId: u.telegram_chat_id || "",
telegramTemplate: u.telegram_template,
+ clientEventWebhook: u.client_event_webhook === 1,
+ clientEventTelegram: u.client_event_telegram === 1,
+ clientEventPush: u.client_event_push === 1,
}));
};
return deliverNotificationWithScreenshot(record, getUserDeliveryTargets);
@@ -293,6 +296,9 @@ const notificationPluginHandlers = createNotificationPluginHandlers({
telegramBotToken: u.telegram_bot_token || "",
telegramChatId: u.telegram_chat_id || "",
telegramTemplate: u.telegram_template,
+ clientEventWebhook: u.client_event_webhook === 1,
+ clientEventTelegram: u.client_event_telegram === 1,
+ clientEventPush: u.client_event_push === 1,
});
if (event === "client_purgatory") {
return getUsersForNotificationDelivery()
diff --git a/Overlord-Server/src/rbac.test.ts b/Overlord-Server/src/rbac.test.ts
index ab9dfb0..bbb1c30 100644
--- a/Overlord-Server/src/rbac.test.ts
+++ b/Overlord-Server/src/rbac.test.ts
@@ -54,8 +54,8 @@ describe("checkPermission", () => {
expect(checkPermission(makeUser({ role: "admin" }), "audit:view")).toBe(true);
});
- test("operator does not have audit:view", () => {
- expect(checkPermission(makeUser({ role: "operator" }), "audit:view")).toBe(false);
+ test("operator has audit:view", () => {
+ expect(checkPermission(makeUser({ role: "operator" }), "audit:view")).toBe(true);
});
});
diff --git a/Overlord-Server/src/server/notification-delivery.ts b/Overlord-Server/src/server/notification-delivery.ts
index 7db53cf..858b8ae 100644
--- a/Overlord-Server/src/server/notification-delivery.ts
+++ b/Overlord-Server/src/server/notification-delivery.ts
@@ -43,6 +43,9 @@ export type UserDeliveryTarget = {
telegramBotToken: string;
telegramChatId: string;
telegramTemplate: string | null;
+ clientEventWebhook: boolean;
+ clientEventTelegram: boolean;
+ clientEventPush: boolean;
};
export const DEFAULT_WEBHOOK_TEMPLATE =
@@ -324,6 +327,7 @@ export async function deliverWebPushClientEvent(
info: { id: string; host?: string; user?: string; os?: string },
canUserAccessClient: (userId: number, userRole: string, clientId: string) => boolean,
getUserRole: (userId: number) => string | undefined,
+ isClientEventPushEnabled?: (userId: number) => boolean,
): Promise {
const subs = getAllPushSubscriptions();
if (subs.length === 0) return;
@@ -356,6 +360,7 @@ export async function deliverWebPushClientEvent(
subs.map(async (sub) => {
const role = getUserRole(sub.userId);
if (!role) return;
+ if (isClientEventPushEnabled && !isClientEventPushEnabled(sub.userId)) return;
if (event === "client_purgatory") {
if (role !== "admin" && role !== "operator") return;
} else if (role !== "admin") {
@@ -418,7 +423,7 @@ export async function deliverClientEventToExternalChannels(
await Promise.allSettled(
targets.map(async (target) => {
- if (target.webhookEnabled && target.webhookUrl) {
+ if (target.webhookEnabled && target.clientEventWebhook && target.webhookUrl) {
const url = target.webhookUrl.trim();
if (!url) return;
let parsed: URL;
@@ -474,7 +479,7 @@ export async function deliverClientEventToExternalChannels(
}
}
- if (target.telegramEnabled && target.telegramBotToken && target.telegramChatId) {
+ if (target.telegramEnabled && target.clientEventTelegram && target.telegramBotToken && target.telegramChatId) {
const token = target.telegramBotToken.trim();
const chatId = target.telegramChatId.trim();
if (!token || !chatId) return;
diff --git a/Overlord-Server/src/server/routes/enrollment-routes.ts b/Overlord-Server/src/server/routes/enrollment-routes.ts
index db1e07a..19d1b38 100644
--- a/Overlord-Server/src/server/routes/enrollment-routes.ts
+++ b/Overlord-Server/src/server/routes/enrollment-routes.ts
@@ -14,6 +14,25 @@ import {
import { logAudit, AuditAction } from "../../auditLog";
import * as clientManager from "../../clientManager";
import { setOnlineState } from "../../db";
+import {
+ canUserAccessClient,
+ getUserClientAccessScope,
+ listUserClientRuleIdsByAccess,
+} from "../../users";
+
+function getClientScopeFilters(userId: number, role: string): { allowedClientIds?: string[]; deniedClientIds?: string[] } {
+ if (role === "admin") return {};
+ const scope = getUserClientAccessScope(userId);
+ if (scope === "none") return { allowedClientIds: [] };
+ if (scope === "all") return {};
+ if (scope === "allowlist") {
+ return { allowedClientIds: listUserClientRuleIdsByAccess(userId, "allow") };
+ }
+ if (scope === "denylist") {
+ return { deniedClientIds: listUserClientRuleIdsByAccess(userId, "deny") };
+ }
+ return { allowedClientIds: [] };
+}
let _postApproveHook: ((clientId: string) => void) | undefined;
@@ -31,7 +50,8 @@ export async function handleEnrollmentRoutes(
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role === "viewer") return new Response("Forbidden", { status: 403 });
- const clients = getPendingClients();
+ const scopeFilters = getClientScopeFilters(user.userId, user.role);
+ const clients = getPendingClients(scopeFilters);
return Response.json({ items: clients });
}
@@ -40,7 +60,8 @@ export async function handleEnrollmentRoutes(
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
- const stats = getEnrollmentStats();
+ const scopeFilters = getClientScopeFilters(user.userId, user.role);
+ const stats = getEnrollmentStats(scopeFilters);
return Response.json(stats);
}
@@ -66,6 +87,10 @@ export async function handleEnrollmentRoutes(
const current = getClientEnrollmentStatus(clientId);
if (!current) return Response.json({ error: "Client not found" }, { status: 404 });
+ if (!canUserAccessClient(user.userId, user.role, clientId)) {
+ return new Response("Forbidden: Client access denied", { status: 403 });
+ }
+
setClientEnrollmentStatus(clientId, "approved", user.username);
_postApproveHook?.(clientId);
@@ -94,6 +119,10 @@ export async function handleEnrollmentRoutes(
const current = getClientEnrollmentStatus(clientId);
if (!current) return Response.json({ error: "Client not found" }, { status: 404 });
+ if (!canUserAccessClient(user.userId, user.role, clientId)) {
+ return new Response("Forbidden: Client access denied", { status: 403 });
+ }
+
setClientEnrollmentStatus(clientId, "denied");
logAudit({
@@ -120,6 +149,10 @@ export async function handleEnrollmentRoutes(
const current = getClientEnrollmentStatus(clientId);
if (!current) return Response.json({ error: "Client not found" }, { status: 404 });
+ if (!canUserAccessClient(user.userId, user.role, clientId)) {
+ return new Response("Forbidden: Client access denied", { status: 403 });
+ }
+
setClientEnrollmentStatus(clientId, "pending");
return Response.json({ ok: true, status: "pending" });
@@ -151,6 +184,7 @@ export async function handleEnrollmentRoutes(
if (action === "ban-ip") {
let banned = 0;
for (const id of ids) {
+ if (!canUserAccessClient(user.userId, user.role, id)) continue;
const clientIp = getClientIp(id);
if (!clientIp) continue;
banIp(clientIp, `Bulk banned from purgatory by ${user.username}`);
@@ -178,6 +212,7 @@ export async function handleEnrollmentRoutes(
const status = action === "approve" ? "approved" : action === "deny" ? "denied" : "pending";
let updated = 0;
for (const id of ids) {
+ if (!canUserAccessClient(user.userId, user.role, id)) continue;
const ok = setClientEnrollmentStatus(
id,
status as "approved" | "denied" | "pending",
@@ -215,6 +250,10 @@ export async function handleEnrollmentRoutes(
const targetIp = getClientIp(clientId);
if (!targetIp) return Response.json({ error: "Client IP not found" }, { status: 404 });
+ if (!canUserAccessClient(user.userId, user.role, clientId)) {
+ return new Response("Forbidden: Client access denied", { status: 403 });
+ }
+
banIp(targetIp, `Banned from purgatory by ${user.username} for client ${clientId}`);
setClientEnrollmentStatus(clientId, "denied");
diff --git a/Overlord-Server/src/server/routes/misc-routes.ts b/Overlord-Server/src/server/routes/misc-routes.ts
index 8c58936..63ce47a 100644
--- a/Overlord-Server/src/server/routes/misc-routes.ts
+++ b/Overlord-Server/src/server/routes/misc-routes.ts
@@ -4,7 +4,7 @@ import { getConfig, updateSecurityConfig, updateTlsConfig, updateAppearanceConfi
import { getClientMetricsSummary } from "../../db";
import { metrics } from "../../metrics";
import { requirePermission } from "../../rbac";
-import { getUserTelegramChatId, setUserTelegramChatId } from "../../users";
+import { getUserTelegramChatId, setUserTelegramChatId, getUserClientAccessScope, listUserClientRuleIdsByAccess } from "../../users";
import { runCertbotSetup } from "../certbot-setup";
import {
getActiveProxies,
@@ -311,6 +311,20 @@ export async function handleMiscRoutes(
const endDate = Number(url.searchParams.get("endDate") || 0) || undefined;
const successOnly = url.searchParams.get("successOnly") === "true";
+ let allowedClientIds: string[] | undefined;
+ let deniedClientIds: string[] | undefined;
+ if (user.role !== "admin") {
+ const scope = getUserClientAccessScope(user.userId);
+ if (scope === "none") {
+ return Response.json({ logs: [], total: 0, page, pageSize }, { headers: deps.CORS_HEADERS });
+ }
+ if (scope === "allowlist") {
+ allowedClientIds = listUserClientRuleIdsByAccess(user.userId, "allow");
+ } else if (scope === "denylist") {
+ deniedClientIds = listUserClientRuleIdsByAccess(user.userId, "deny");
+ }
+ }
+
const result = getAuditLogs({
page,
pageSize,
@@ -320,6 +334,8 @@ export async function handleMiscRoutes(
startDate,
endDate,
successOnly,
+ allowedClientIds,
+ deniedClientIds,
});
return Response.json(result, { headers: deps.CORS_HEADERS });
diff --git a/Overlord-Server/src/server/routes/notifications-config-routes.ts b/Overlord-Server/src/server/routes/notifications-config-routes.ts
index 4700efd..6de815e 100644
--- a/Overlord-Server/src/server/routes/notifications-config-routes.ts
+++ b/Overlord-Server/src/server/routes/notifications-config-routes.ts
@@ -224,6 +224,9 @@ export async function handleNotificationsConfigRoutes(
telegram_bot_token: null,
telegram_chat_id: null,
telegram_template: null,
+ client_event_webhook: 1,
+ client_event_telegram: 1,
+ client_event_push: 1,
};
return Response.json({
settings,
@@ -276,6 +279,15 @@ export async function handleNotificationsConfigRoutes(
if (typeof body?.telegram_template === "string") {
patch.telegram_template = body.telegram_template.trim() || null;
}
+ if (typeof body?.client_event_webhook === "boolean" || typeof body?.client_event_webhook === "number") {
+ patch.client_event_webhook = body.client_event_webhook ? 1 : 0;
+ }
+ if (typeof body?.client_event_telegram === "boolean" || typeof body?.client_event_telegram === "number") {
+ patch.client_event_telegram = body.client_event_telegram ? 1 : 0;
+ }
+ if (typeof body?.client_event_push === "boolean" || typeof body?.client_event_push === "number") {
+ patch.client_event_push = body.client_event_push ? 1 : 0;
+ }
const result = updateUserNotificationSettings(user.userId, patch);
if (!result.success) {
diff --git a/Overlord-Server/src/server/ws-notifications-plugin.ts b/Overlord-Server/src/server/ws-notifications-plugin.ts
index 44c13e7..072a040 100644
--- a/Overlord-Server/src/server/ws-notifications-plugin.ts
+++ b/Overlord-Server/src/server/ws-notifications-plugin.ts
@@ -357,9 +357,14 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
safeSendViewer(session.viewer, item);
}
- void deliverWebPushClientEvent(event, info, deps.canUserAccessClient, deps.getUserRole);
-
const externalTargets = deps.getDeliveryTargetsForClientEvent(event, info.id);
+ const pushEnabledByUser = new Map(externalTargets.map((t) => [t.userId, t.clientEventPush]));
+
+ void deliverWebPushClientEvent(event, info, deps.canUserAccessClient, deps.getUserRole, (userId) => {
+ const enabled = pushEnabledByUser.get(userId);
+ return enabled !== undefined ? enabled : true;
+ });
+
void deliverClientEventToExternalChannels(event, info, externalTargets);
},
diff --git a/Overlord-Server/src/users.ts b/Overlord-Server/src/users.ts
index d2c5b02..03522b5 100644
--- a/Overlord-Server/src/users.ts
+++ b/Overlord-Server/src/users.ts
@@ -176,6 +176,22 @@ try {
}
}
+const clientEventColumns: Array<{ sql: string; label: string }> = [
+ { sql: `ALTER TABLE users ADD COLUMN client_event_webhook INTEGER DEFAULT 1`, label: "client_event_webhook" },
+ { sql: `ALTER TABLE users ADD COLUMN client_event_telegram INTEGER DEFAULT 1`, label: "client_event_telegram" },
+ { sql: `ALTER TABLE users ADD COLUMN client_event_push INTEGER DEFAULT 1`, label: "client_event_push" },
+];
+for (const col of clientEventColumns) {
+ try {
+ db.exec(col.sql);
+ logger.info(`[users] Added ${col.label} column to existing database`);
+ } catch (err: any) {
+ if (!err.message?.includes("duplicate column name")) {
+ logger.error("[users] Migration error:", err);
+ }
+ }
+}
+
const userCount = db.prepare("SELECT COUNT(*) as count FROM users").get() as {
count: number;
};
@@ -569,7 +585,7 @@ export function canBuildClients(userId: number, role: UserRole): boolean {
}
export function canViewAuditLogs(role: UserRole): boolean {
- return role === "admin";
+ return role === "admin" || role === "operator";
}
export function hasPermission(role: UserRole, permission: string, userId?: number): boolean {
@@ -661,12 +677,15 @@ export interface UserNotificationSettings {
telegram_bot_token: string | null;
telegram_chat_id: string | null;
telegram_template: string | null;
+ client_event_webhook: number;
+ client_event_telegram: number;
+ client_event_push: number;
}
export function getUserNotificationSettings(userId: number): UserNotificationSettings | null {
const row = db
.prepare(
- "SELECT webhook_enabled, webhook_url, webhook_template, telegram_enabled, telegram_bot_token, telegram_chat_id, telegram_template FROM users WHERE id = ?",
+ "SELECT webhook_enabled, webhook_url, webhook_template, telegram_enabled, telegram_bot_token, telegram_chat_id, telegram_template, client_event_webhook, client_event_telegram, client_event_push FROM users WHERE id = ?",
)
.get(userId) as UserNotificationSettings | undefined;
return row ?? null;
@@ -707,6 +726,18 @@ export function updateUserNotificationSettings(
fields.push("telegram_template = ?");
values.push(settings.telegram_template || null);
}
+ if ("client_event_webhook" in settings) {
+ fields.push("client_event_webhook = ?");
+ values.push(settings.client_event_webhook ? 1 : 0);
+ }
+ if ("client_event_telegram" in settings) {
+ fields.push("client_event_telegram = ?");
+ values.push(settings.client_event_telegram ? 1 : 0);
+ }
+ if ("client_event_push" in settings) {
+ fields.push("client_event_push = ?");
+ values.push(settings.client_event_push ? 1 : 0);
+ }
if (fields.length === 0) return { success: true };
@@ -733,6 +764,9 @@ export interface UserDeliveryRow {
telegram_bot_token: string | null;
telegram_chat_id: string | null;
telegram_template: string | null;
+ client_event_webhook: number;
+ client_event_telegram: number;
+ client_event_push: number;
}
export function getUsersForNotificationDelivery(): UserDeliveryRow[] {
@@ -744,7 +778,8 @@ export function getUsersForNotificationDelivery(): UserDeliveryRow[] {
.prepare(
`SELECT id, username, role, client_scope,
webhook_enabled, webhook_url, webhook_template,
- telegram_enabled, telegram_bot_token, telegram_chat_id, telegram_template
+ telegram_enabled, telegram_bot_token, telegram_chat_id, telegram_template,
+ client_event_webhook, client_event_telegram, client_event_push
FROM users
WHERE (webhook_enabled = 1 AND webhook_url IS NOT NULL AND webhook_url != '')
OR (telegram_enabled = 1 AND telegram_bot_token IS NOT NULL AND telegram_bot_token != ''