Showing Daily Report: Oct 23, 2026
Checking if each member sent at least 1 photo per day in their assigned topic before 23:59:59 Cairo time.
0
Across selected period & topics
0
Sent ≥ 1 photo on active days
0
Missed at least 1 required day
0%
Fulfilled member-days in period
Sent vs. Missing Members by Topic
Compliance breakdown across CS, Patrolling, Reports, and Splash
Daily Photo Volume Trend (Cairo Time)
Total photos collected per day across selected topic(s)
Member Compliance & Photo Count Per Topic
Automatically matched by Telegram Display Name • Green = Sent daily, Red = Missing day(s)
Manage Team Roster Per Topic
Add or edit expected team members for CS, Patrolling, Reports, and Splash. If a member belongs to multiple topics, add an entry for each topic they must report in.
Simulate / Manually Record a Telegram Photo Post
Test how the bot captures a member's Telegram display name, topic, and Cairo timestamp when they send a task completion photo.
Telegram Topic IDs (`message_thread_id`)
Telegram assigns an ID to each forum topic. You can type /register CS in Telegram or set IDs here:
Collected Photo Submissions Log
Every photo captured by the bot with Cairo Date, Exact Timestamp, Topic, and Member Display Name
Connect Your Telegram Bot & Google Sheet Backend
One single Telegram bot in Review | CS | Patrolling | Monitoring listens to all 4 topics (CS, Patrolling, Reports, Splash), pushes every photo to Google Sheets in Cairo time, and syncs live with this website.
Used only in your browser to link your bot's webhook directly to your Google Apps Script URL.
Paste the Web App URL after deploying the Google Apps Script code below.
5-Minute Free Setup Guide
-
1
Create Bot in @BotFather
Open Telegram, message @BotFather, send
/newbot, and copy the Bot Token. Send/setprivacy→ select your bot → choose Disable so it can read all photos in topics. -
2
Add Bot to Your Group as Admin
Add the bot to Review | CS | Patrolling | Monitoring and promote it to Admin (only "Read Messages" is needed).
-
3
Paste Script in Google Sheets
Create a blank Google Sheet, click Extensions → Apps Script, paste the code on the right, and click Deploy → New deployment → Web app (Execute as: Me, Who has access: Anyone).
-
4
Link Webhook & Register Topics
Paste your Bot Token and Web App URL above and click Activate Telegram Webhook Now. Then inside each of your 4 Telegram topics, send once:
/register CS
/register Patrolling
/register Reports
/register Splash
Google Apps Script Code (`Code.gs`)
Auto-captures Telegram Display Name, Topic (`CS`, `Patrolling`, `Reports`, `Splash`), and Cairo Date/Time (`Africa/Cairo`)
/**
* Review | CS | Patrolling | Monitoring - Telegram Topic Photo Tracker
* Timezone: Africa/Cairo (Daily reset at 00:00 Midnight Cairo Time)
*/
const TIMEZONE = 'Africa/Cairo';
const ALLOWED_TOPICS = ['CS', 'Patrolling', 'Reports', 'Splash'];
function setupSheets() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let logs = ss.getSheetByName('Logs');
if (!logs) {
logs = ss.insertSheet('Logs');
logs.appendRow(['id', 'cairoDate', 'cairoTime', 'timestampIso', 'topic', 'displayName', 'username', 'userId', 'threadId']);
}
let roster = ss.getSheetByName('Roster');
if (!roster) {
roster = ss.insertSheet('Roster');
roster.appendRow(['id', 'name', 'username', 'topic', 'shift']);
}
let topicMap = ss.getSheetByName('TopicMap');
if (!topicMap) {
topicMap = ss.insertSheet('TopicMap');
topicMap.appendRow(['threadId', 'topicName']);
}
return { logs, roster, topicMap };
}
function getTopicMapDict(topicMapSheet) {
const rows = topicMapSheet.getDataRange().getValues();
const map = {};
for (let i = 1; i < rows.length; i++) {
if (rows[i][0] && rows[i][1]) map[String(rows[i][0])] = String(rows[i][1]);
}
return map;
}
function doPost(e) {
const { logs, roster, topicMap } = setupSheets();
try {
const payload = JSON.parse(e.postData.contents);
// 1. Handle Dashboard Roster Sync / Manual Entry POST
if (payload.action === 'syncRoster' && Array.isArray(payload.roster)) {
roster.clearContents();
roster.appendRow(['id', 'name', 'username', 'topic', 'shift']);
payload.roster.forEach(m => {
roster.appendRow([m.id, m.name, m.username || '', m.topic, m.shift || '']);
});
return ContentService.createTextOutput(JSON.stringify({ ok: true })).setMimeType(ContentService.MimeType.JSON);
}
// 2. Handle Incoming Telegram Bot Webhook Update
const msg = payload.message || payload.edited_message;
if (!msg) return ContentService.createTextOutput('OK');
const threadId = String(msg.message_thread_id || 'general');
const from = msg.from || {};
const displayName = [from.first_name || '', from.last_name || ''].join(' ').trim() || from.username || ('User_' + (from.id || 'unknown'));
const username = from.username ? '@' + from.username : '';
// Allow "/register Splash" or "/register@BotName Splash" inside each topic
const rawText = (msg.text || msg.caption || '').trim();
if (rawText.toLowerCase().startsWith('/register')) {
const parts = rawText.split(/\s+/);
const requestedTopic = parts.slice(1).join(' ');
const matched = ALLOWED_TOPICS.find(t => requestedTopic.toLowerCase().includes(t.toLowerCase()));
if (matched) {
topicMap.appendRow([threadId, matched]);
// Also ensure sender is added to Roster for this topic
const rRows = roster.getDataRange().getValues();
const exists = rRows.slice(1).some(r => String(r[3]) === matched && String(r[1]).toLowerCase() === displayName.toLowerCase());
if (!exists) {
roster.appendRow(['m_' + new Date().getTime(), displayName, username, matched, 'Registered via Bot']);
}
}
return ContentService.createTextOutput('OK');
}
// Only count messages that contain a Photo (or image document)
const isPhoto = Boolean(msg.photo && msg.photo.length > 0) || Boolean(msg.document && msg.document.mime_type && msg.document.mime_type.startsWith('image/'));
if (!isPhoto) return ContentService.createTextOutput('OK');
// Resolve Topic Name from threadId or forum_topic_created
const mapDict = getTopicMapDict(topicMap);
let topicName = mapDict[threadId] || '';
if (!topicName && msg.reply_to_message && msg.reply_to_message.forum_topic_created) {
const rawTitle = msg.reply_to_message.forum_topic_created.name || '';
const found = ALLOWED_TOPICS.find(t => rawTitle.toLowerCase().includes(t.toLowerCase()));
if (found) {
topicName = found;
topicMap.appendRow([threadId, found]);
}
}
if (!topicName) topicName = 'CS'; // Fallback if unmapped
// Convert timestamp to Cairo Time (Midnight 00:00 daily boundary)
const msgDate = msg.date ? new Date(msg.date * 1000) : new Date();
const cairoDate = Utilities.formatDate(msgDate, TIMEZONE, 'yyyy-MM-dd');
const cairoTime = Utilities.formatDate(msgDate, TIMEZONE, 'HH:mm:ss');
const id = 'tg_' + (msg.message_id || new Date().getTime());
logs.appendRow([id, cairoDate, cairoTime, msgDate.toISOString(), topicName, displayName, username, String(from.id || ''), threadId]);
return ContentService.createTextOutput('OK');
} catch (err) {
return ContentService.createTextOutput(JSON.stringify({ ok: false, error: String(err) })).setMimeType(ContentService.MimeType.JSON);
}
}
function doGet(e) {
const { logs, roster, topicMap } = setupSheets();
const logRows = logs.getDataRange().getValues().slice(1).map(r => ({
id: String(r[0]),
cairoDate: Utilities.formatDate(new Date(r[1]), TIMEZONE, 'yyyy-MM-dd').slice(0, 10) || String(r[1]),
cairoTime: String(r[2]),
timestampIso: String(r[3]),
topic: String(r[4]),
displayName: String(r[5]),
username: String(r[6]),
userId: String(r[7]),
threadId: String(r[8])
}));
const rosterRows = roster.getDataRange().getValues().slice(1).map(r => ({
id: String(r[0]),
name: String(r[1]),
username: String(r[2]),
topic: String(r[3]),
shift: String(r[4])
}));
return ContentService.createTextOutput(JSON.stringify({
ok: true,
logs: logRows,
roster: rosterRows,
topicMap: getTopicMapDict(topicMap)
})).setMimeType(ContentService.MimeType.JSON);
}