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

  1. Calendar Initialization: A /start command triggers the display of the initial calendar view.
  2. Calendar Navigation: Users can navigate through months and years using inline buttons.
  3. Date Selection: When a date is chosen, the flow presents an hour selector for time selection.
  4. Time Selection: Once a time is selected, it confirms the chosen date and time.

Workflow Implementation

Importing and Setting Up the Flow

  1. Open Node-RED, and import the provided JSON flow.
  2. Configure your Telegram bot's credentials by setting the telegram bot node to your bot's API token.

Node Descriptions

  1. Telegram Command Node (/start): Triggers the calendar interface.
  2. Initial Show Calendar Function: Initializes the calendar with the current month and year.
  3. Calendar Navigation: Inline buttons allow users to navigate months and years.
  4. Date Selection Handling: Upon selecting a date, the flow presents the user with time options.
  5. 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 the Initial 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

  1. Deploy the flow and ensure the bot is running.
  2. Test by sending /start to the Telegram bot. You should see the interactive calendar appear.
  3. 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}]

Flow Info

Created 4 days ago
Rating: not yet rated

Owner

Actions

Rate:

Node Types

Core
  • comment (x1)
  • function (x7)
  • switch (x1)
Other

Tags

  • Telegrambot
  • Calendar
  • Date/time_picker
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option