Full functioned and good looking Calendar - date/time picker for Telegrambot
Here's an overview and detailed implementation guide for creating a date/time picker for a Telegram bot on Node-RED. This includes instructions on setting up moment.js and using the workflow to display an interactive calendar and time picker via Telegram.
Overview
This date and time picker uses Node-RED with a Telegram bot to display an inline calendar where users can pick a date, navigate between months and years, and select a specific time. This implementation relies on moment.js to handle dates and times. By setting up moment globally in Node-RED, we simplify date manipulations throughout the flow.
Setup Steps
Step 1: Install moment.js in Node-RED
In your terminal:
cd ~/.node-red
npm install moment
This command installs moment.js into your Node-RED environment.
Step 2: Update Node-RED Settings to Include moment Globally
Edit settings.js
in Node-RED to make moment accessible globally.
nano ~/.node-red/settings.js
Add moment to functionGlobalContext
:
functionGlobalContext: {
moment: require('moment')
},
Save and close settings.js
.
Step 3: Restart Node-RED
For the changes to take effect:
node-red-stop
node-red-start
Step 4: Access moment in Node-RED Function Nodes
With moment set in the global context, you can use it in any function node:
const moment = global.get('moment');
const now = moment();
Workflow Overview
- Calendar Initialization: A
/start
command triggers the display of the initial calendar view. - Calendar Navigation: Users can navigate through months and years using inline buttons.
- Date Selection: When a date is chosen, the flow presents an hour selector for time selection.
- Time Selection: Once a time is selected, it confirms the chosen date and time.
Workflow Implementation
Importing and Setting Up the Flow
- Open Node-RED, and import the provided JSON flow.
- Configure your Telegram bot's credentials by setting the
telegram bot
node to your bot's API token.
Node Descriptions
- Telegram Command Node (
/start
): Triggers the calendar interface. - Initial Show Calendar Function: Initializes the calendar with the current month and year.
- Calendar Navigation: Inline buttons allow users to navigate months and years.
- Date Selection Handling: Upon selecting a date, the flow presents the user with time options.
- Time Selection and Confirmation: After time selection, it confirms the full date and time chosen.
Workflow Walkthrough
- Starting the Bot: A user types
/start
, which triggers theInitial Show Calendar
function to display the current month. - Navigating the Calendar: The
Callback Query
node listens for navigation inputs like previous/next month or year, updating the calendar view dynamically. - Selecting Date: Clicking a day sets the chosen date and transitions to the time picker.
- Selecting Time: The time picker displays hourly options, and selecting one completes the process, confirming the date and time choice.
Code Snippets and Explanations
Calendar Creation Function (
createCalendar
):function createCalendar(year, month) { const daysInMonth = moment({ year, month }).daysInMonth(); // Code for generating calendar buttons with navigation here }
Calendar Update on Navigation:
const [action, dateStr] = callbackData.split(":"); if (action === "prev_month" || action === "next_month") { // Logic for updating the displayed month or year }
Date and Time Selection Confirmation:
msg.payload = { type: 'editMessageText', content: `You selected: ${fullDate.format("YYYY-MM-DD HH:mm")}` };
Running the Flow
- Deploy the flow and ensure the bot is running.
- Test by sending
/start
to the Telegram bot. You should see the interactive calendar appear. - Navigate, select a date, and choose a time to complete the date/time picker interaction.
This implementation creates an interactive and user-friendly date/time picker in Telegram using Node-RED and moment.js, making date and time selection easy.
[{"id":"0584183ffff75734","type":"tab","label":"Telegram Date Picker","disabled":false,"info":""},{"id":"58e3773cdfd6335b","type":"group","z":"0584183ffff75734","name":"Calendar block","style":{"fill":"#c8e7a7","label":true},"nodes":["3130a0d6d612d3bc","e90d07a320938904","f940a811af433850","10897e12081e7fe0","10f3dd98ded8c3c0","9a9b429b9ddf4921","c169efe0d1e41535","61f5125617a0c155","e7082205be0e4e8c","35657a74e8d4a32b","a1147c806b765b35","138476609a0438dd","f737be1001e76974","e3edd145b42a3c59"],"x":34,"y":19,"w":832,"h":342},{"id":"3130a0d6d612d3bc","type":"telegram command","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"/start","command":"/start","description":"","registercommand":false,"language":"","bot":"0b92e9ea258e4c4e","strict":false,"hasresponse":false,"useregex":false,"removeregexcommand":false,"outputs":1,"x":110,"y":60,"wires":[["e90d07a320938904","138476609a0438dd"]]},{"id":"e90d07a320938904","type":"function","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"Initial Show Calendar","func":"context.global.keyboard = { pending: true, messageId: msg.payload.messageId };\n\n// Import moment.js\nconst moment = global.get('moment');\n\n// Initialize calendar view with the specified month and year (zero-based month)\nconst year = moment().year();\nconst month = moment().month(); // Use zero-based indexing\n\n// Function to create a calendar inline keyboard\nfunction createCalendar(year, month) {\n const daysInMonth = moment({ year, month }).daysInMonth();\n const startDay = (moment({ year, month }).day() + 6) % 7; // Shift Sunday (0) to last to make Monday the first day\n\n let buttons = [];\n let row = [];\n\n // Add blank buttons for days before the first of the month to start on Monday\n for (let i = 0; i < startDay; i++) {\n row.push({ text: \" \", callback_data: \"noop\" });\n }\n\n // Add day buttons\n for (let d = 1; d <= daysInMonth; d++) {\n const date = moment({ year, month, day: d });\n row.push({\n text: date.format(\"D\"),\n callback_data: `date:${date.format(\"YYYY-MM-DD\")}`\n });\n\n // Push the row if it has 7 buttons\n if (row.length === 7) {\n buttons.push(row);\n row = [];\n }\n }\n\n // If the last row has any valid date buttons but less than 7, fill with \"noop\" buttons\n if (row.some(button => button.callback_data !== \"noop\")) {\n while (row.length < 7) {\n row.push({ text: \" \", callback_data: \"noop\" });\n }\n buttons.push(row);\n }\n\n\n // Add navigation row for month and year controls\n const prevMonth = moment({ year, month }).subtract(1, 'month');\n const nextMonth = moment({ year, month }).add(1, 'month');\n const prevYear = moment({ year, month }).subtract(1, 'year');\n const nextYear = moment({ year, month }).add(1, 'year');\n\n buttons.push([\n { text: \"< Month\", callback_data: `prev_month:${prevMonth.format('YYYY-MM')}` },\n { text: \"Month >\", callback_data: `next_month:${nextMonth.format('YYYY-MM')}` }\n ], [\n { text: \"< Year\", callback_data: `prev_year:${prevYear.format('YYYY-MM')}` },\n { text: \"Year >\", callback_data: `next_year:${nextYear.format('YYYY-MM')}` }\n ], [\n { text: \"Restart\", callback_data: `restart` }\n ]);\n\n return buttons;\n}\n\n// Creating a new message\nmsg.payload = {\n chatId: msg.payload.chatId,\n type: 'message',\n content: `Select a date: ${moment({ year, month }).format(\"MMMM YYYY\")}`,\n options: {\n message_id: msg.payload.messageId,\n reply_markup: JSON.stringify({\n inline_keyboard: createCalendar(year, month)\n })\n }\n};\n\nreturn [msg];\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":100,"wires":[["f940a811af433850"]]},{"id":"f940a811af433850","type":"telegram sender","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"","bot":"0b92e9ea258e4c4e","haserroroutput":false,"outputs":1,"x":760,"y":100,"wires":[["c169efe0d1e41535"]]},{"id":"10897e12081e7fe0","type":"telegram event","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"Callback Query","bot":"0b92e9ea258e4c4e","event":"callback_query","autoanswer":true,"x":140,"y":320,"wires":[["e7082205be0e4e8c"]]},{"id":"10f3dd98ded8c3c0","type":"function","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"Process Date","func":"var messageId = context.global.keyboard.messageId;\nconst moment = global.get('moment');\nconst callbackData = msg.originalMessage.data;\n\nif (!callbackData) return null;\n\nconst [action, dateStr] = callbackData.split(\":\");\n\nif (action === \"date\") {\n const date = moment(dateStr);\n let buttons = [];\n let row = [];\n\n // Generate buttons for 24 hours in HH:00 format\n for (let hour = 0; hour < 24; hour++) {\n const timeLabel = `${String(hour).padStart(2, '0')}:00`;\n row.push({ text: timeLabel, callback_data: `time:${dateStr}:${timeLabel}` });\n\n // Distribute buttons into rows of 8\n if (row.length === 6) {\n buttons.push(row);\n row = [];\n }\n }\n\n // Push the remaining row if it contains any buttons\n if (row.length > 0) buttons.push(row);\n\n // Add \"Go back\" button at the bottom\n buttons.push([\n { text: \"Restart\", callback_data: `restart` }\n ]);\n\n // Prepare the message payload\n msg.payload = {\n //chatId: msg.payload.chatId,\n type: 'editMessageText',\n content: `You selected: ${date.format(\"dddd, YYYY-MM-DD\")}.\\nPlease choose a time:`,\n options: {\n chat_id: msg.payload.chatId,\n message_id: messageId,\n reply_markup: {\n inline_keyboard: buttons\n }\n }\n };\n\n return msg;\n}\n\n// Handle month navigation\nif (action === \"prev_month\" || action === \"next_month\") {\n const [year, month] = dateStr.split(\"-\").map(Number);\n msg.payload = { chatId: msg.payload.chatId, year, month };\n return [null, msg];\n}\nif (action === \"prev_year\" || action === \"next_year\") {\n const [year, month] = dateStr.split(\"-\").map(Number);\n msg.payload = { chatId: msg.payload.chatId, year, month };\n return [null, msg];\n}\n\nreturn null;\n","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":220,"wires":[["f940a811af433850"],["61f5125617a0c155"]]},{"id":"9a9b429b9ddf4921","type":"function","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"Display Final Date","func":"var messageId = context.global.keyboard.messageId;\nconst moment = global.get('moment');\nconst callbackData = msg.originalMessage.data;\n\nif (!callbackData) return null;\n\nconst [action, dateStr, timeStr] = callbackData.split(\":\");\n\nif (action === \"time\") {\n const fullDate = moment(`${dateStr} ${timeStr}`, \"YYYY-MM-DD HH:mm\");\n msg.payload = {\n type: 'editMessageText',\n content: `You selected: ${fullDate.format(\"dddd, YYYY-MM-DD HH:mm\")}`,\n options: {\n chat_id: msg.payload.chatId,\n message_id: messageId,\n reply_markup: {\n inline_keyboard: [\n [\n { text: \"Restart\", callback_data: `restart` }\n ]\n ]\n }\n }\n };\n return [msg];\n}\n\nreturn null;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":320,"wires":[["f940a811af433850"]]},{"id":"c169efe0d1e41535","type":"function","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"messageId","func":"context.global.keyboard.messageId = msg.payload.sentMessageId;\nreturn [msg];","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":770,"y":180,"wires":[[]]},{"id":"61f5125617a0c155","type":"function","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"Continue Calendar","func":"var messageId = context.global.keyboard.messageId;\n\n// Import moment.js\nconst moment = global.get('moment');\n\n// Initialize calendar view with the current month or passed month/year (ensure zero-based month)\nconst year = msg.payload.year;\nconst month = msg.payload.month - 1; // Adjust to zero-based indexing if payload provides a one-based month\n\n// Function to create a calendar inline keyboard\nfunction createCalendar(year, month) {\n const daysInMonth = moment({ year, month }).daysInMonth();\n const startDay = (moment({ year, month }).day() + 6) % 7; // Adjust startDay to make Monday the first day\n\n let buttons = [];\n let row = [];\n\n // Add blank buttons for days before the first of the month to start on Monday\n for (let i = 0; i < startDay; i++) {\n row.push({ text: \" \", callback_data: \"noop\" });\n }\n\n // Add day buttons\n for (let d = 1; d <= daysInMonth; d++) {\n const date = moment({ year, month, day: d });\n row.push({\n text: date.format(\"D\"),\n callback_data: `date:${date.format(\"YYYY-MM-DD\")}`\n });\n\n // Push the row if it has 7 buttons\n if (row.length === 7) {\n buttons.push(row);\n row = [];\n }\n }\n\n // If the last row has any valid date buttons but less than 7, fill with \"noop\" buttons\n if (row.some(button => button.callback_data !== \"noop\")) {\n while (row.length < 7) {\n row.push({ text: \" \", callback_data: \"noop\" });\n }\n buttons.push(row);\n }\n\n // Add navigation row for month and year controls\n const prevMonth = moment({ year, month }).subtract(1, 'month');\n const nextMonth = moment({ year, month }).add(1, 'month');\n const prevYear = moment({ year, month }).subtract(1, 'year');\n const nextYear = moment({ year, month }).add(1, 'year');\n\n buttons.push([\n { text: \"< Month\", callback_data: `prev_month:${prevMonth.format('YYYY-MM')}` },\n { text: \"Month >\", callback_data: `next_month:${nextMonth.format('YYYY-MM')}` }\n ], [\n { text: \"< Year\", callback_data: `prev_year:${prevYear.format('YYYY-MM')}` },\n { text: \"Year >\", callback_data: `next_year:${nextYear.format('YYYY-MM')}` }\n ], [\n { text: \"Restart\", callback_data: `restart` }\n ]);\n\n return buttons;\n}\n\n// Create the inline keyboard with the updated month navigation\nvar reply_markup = JSON.stringify({\n \"inline_keyboard\": createCalendar(year, month)\n});\n\nvar options = {\n chat_id: msg.payload.chatId,\n reply_markup: reply_markup,\n message_id: messageId\n};\n\nmsg.payload.type = 'editMessageText';\nmsg.payload.content = `Select a date: ${moment({ year, month }).format(\"MMMM YYYY\")}`;\nmsg.payload.options = options;\n\nreturn [msg];\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":280,"wires":[["f940a811af433850"]]},{"id":"e7082205be0e4e8c","type":"switch","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"","property":"originalMessage.data","propertyType":"msg","rules":[{"t":"eq","v":"restart","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":290,"y":320,"wires":[["35657a74e8d4a32b"],["10f3dd98ded8c3c0","9a9b429b9ddf4921"]]},{"id":"35657a74e8d4a32b","type":"function","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"delete","func":"msg.payload.type=\"deleteMessage\";\nmsg.payload.content = context.global.keyboard.messageId;\nmsg.payload.chatId = msg.payload.chatId;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":180,"wires":[["a1147c806b765b35"]]},{"id":"a1147c806b765b35","type":"telegram sender","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"","bot":"0b92e9ea258e4c4e","haserroroutput":false,"outputs":1,"x":360,"y":140,"wires":[["e90d07a320938904"]]},{"id":"138476609a0438dd","type":"function","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"delete","func":"msg.payload.type=\"deleteMessage\";\nmsg.payload.content = context.global.keyboard.messageId;\nmsg.payload.chatId = msg.payload.chatId;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":60,"wires":[["f737be1001e76974"]]},{"id":"f737be1001e76974","type":"telegram sender","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"","bot":"0b92e9ea258e4c4e","haserroroutput":false,"outputs":1,"x":520,"y":60,"wires":[[]]},{"id":"e3edd145b42a3c59","type":"comment","z":"0584183ffff75734","g":"58e3773cdfd6335b","name":"Instruction","info":"1. Install moment.js in Node-RED\nOpen a terminal window and navigate to your Node-RED user directory (usually ~/.node-red) and install moment using npm:\n\nbash\nCopy code\n\ncd ~/.node-red\nnpm install moment\n\nThis will add moment.js to your Node-RED environment.\n\n2. Update the Node-RED Settings to Include moment Globally\nEdit the settings.js file for Node-RED to make moment available in the global context. The settings.js file is typically located in your Node-RED user directory (~/.node-red/settings.js).\n\nOpen settings.js in a text editor:\nbash\nCopy code\n\nnano ~/.node-red/settings.js\n\nIn the settings.js file, locate the functionGlobalContext section (it might be commented out). Enable it and add moment as follows:\njavascript\nCopy code\n\nfunctionGlobalContext: {\n moment: require('moment')\n},\n\nThis makes moment accessible globally as global.get('moment') in Node-RED function nodes.\nSave and close the file.\n3. Restart Node-RED\nAfter editing the settings, restart Node-RED for the changes to take effect:\n\nbash\nCopy code\n\nnode-red-stop\nnode-red-start\n\n4. Access moment in Node-RED Function Nodes\nNow, you can use moment in any function node by accessing it through the global context:\n\njavascript\nCopy code\n\nconst moment = global.get('moment');\nconst now = moment();\n\nThis setup allows moment.js to be used across all function nodes in your Node-RED instance.\n\n\n\n\n\n\n","x":140,"y":180,"wires":[]},{"id":"0b92e9ea258e4c4e","type":"telegram bot","botname":"Krasner_bot_ 1058611204:AAFV5Q4ihoR5CN30KZo5GGNkhPbshbNRGm4","usernames":"","chatids":"","baseapiurl":"","updatemode":"polling","pollinterval":"300","usesocks":false,"sockshost":"","socksprotocol":"socks5","socksport":"6667","socksusername":"anonymous","sockspassword":"","bothost":"","botpath":"","localbotport":"8443","publicbotport":"8443","privatekey":"","certificate":"","useselfsignedcertificate":false,"sslterminated":false,"verboselogging":false}]