Task: Add Google Calendar upcoming events to Discord

n8n workflow JSON:
{"id":1,"name":"Google Calendar to Discord","nodes":[{"name":"On clicking 'execute'","type":"n8n-nodes-base.manualTrigger","position":[-370,340],"parameters":{},"typeVersion":1},{"name":"GetAllEvents","type":"n8n-nodes-base.googleCalendar","notes":"Get all events from Google Calendar in a timeframe we set before.","position":[110,520],"parameters":{"options":{"timeMax":"={{$json[\"event_end\"]}}","timeMin":"={{$json[\"event_start\"]}}","singleEvents":true},"calendar":"xxxxx@group.calendar.google.com","operation":"getAll","returnAll":true},"credentials":{"googleCalendarOAuth2Api":"Google Calendar account"},"notesInFlow":true,"retryOnFail":true,"typeVersion":1,"alwaysOutputData":true},{"name":"SetTimeframe","type":"n8n-nodes-base.function","notes":"Define two dates to act as range filter","position":[-120,520],"parameters":{"functionCode":"// There is a lot of dealing with proper timezone.\n// I'm sure it can be done much better\n\n// Timezone\nvar timezoneOffset = 3;\n\nvar timezoneFormatted = timezoneOffset + \":00\";\nif (timezoneOffset.toString().length == 1) {\n  timezoneFormatted = \"0\" + timezoneFormatted;\n}\nif (Math.sign(timezoneOffset) < 0) {\n  timezoneFormatted = \"-\" + timezoneFormatted ;\n} else {\n  timezoneFormatted = \"+\" + timezoneFormatted ;\n}\n\n// Range between two dates\nvar minutes = 15;\n\n// Create dates\nvar curr = new Date;\nvar from= (curr.getUTCDate());\nvar fromDate = new Date(curr.setDate(from) + timezoneOffset * 3600000);\nvar toDate = new Date(curr.setDate(from) + timezoneOffset * 3600000 + minutes * 60000);\n\n// Format dates in a proper format e.g. 2021-10-08T11:15:00+03:00\nitems[0].json.event_start = fromDate.toISOString().slice(0, 19) + timezoneFormatted;\nitems[0].json.event_end= toDate.toISOString().slice(0, 19) + timezoneFormatted;\n//items[0].json.event_start = fromDate.toLocaleString(\"sv\", { timeZone: timezoneID  }).replace(' ', 'T') + \":00.000Z\";\n//items[0].json.event_end= toDate.toLocaleString(\"sv\", { timeZone: timezoneID  }).replace(' ', 'T') + \":00.000Z\";\n\n// Debug\n//console.log(\"from \"  + items[0].json.event_start);\n//console.log(\"to \" + items[0].json.event_end);\n\nreturn items;\n"},"notesInFlow":true,"typeVersion":1},{"name":"PollingInterval","type":"n8n-nodes-base.interval","notes":"How often we trigger this awesome logic","position":[-360,520],"parameters":{"unit":"minutes","interval":15},"notesInFlow":true,"typeVersion":1},{"name":"IfEventFitsInTimeframe","type":"n8n-nodes-base.if","notes":"Compare each event's start date if it fits in timeframe boundaries.","position":[550,520],"parameters":{"conditions":{"number":[{"value1":"={{$json[\"start\"][\"unix\"]}}","value2":"={{$json[\"start\"][\"from\"]}}","operation":"largerEqual"},{"value1":"={{$json[\"start\"][\"unix\"]}}","value2":"={{$json[\"start\"][\"to\"]}}","operation":"smallerEqual"}],"string":[],"boolean":[],"dateTime":[]}},"notesInFlow":true,"typeVersion":1},{"name":"PrepareDatetimes","type":"n8n-nodes-base.functionItem","notes":"Convert our timeframe and each event's start date to Unix time to make compare a bit easier","position":[330,520],"parameters":{"functionCode":"// Add unitx times for start date and date range from Set Interval node\n\n// Add a new field called 'myNewField' to the JSON of the item\nitem.start.unix = parseInt((new Date(item.start.dateTime).getTime() / 1000).toFixed(0));\nitem.start.from = parseInt((new Date($items(\"SetTimeframe\")[0][\"json\"][\"event_start\"]).getTime() / 1000).toFixed(0));\nitem.start.to = parseInt((new Date($items(\"SetTimeframe\")[0][\"json\"][\"event_end\"]).getTime() / 1000).toFixed(0));\n\nreturn item;"},"notesInFlow":true,"typeVersion":1},{"name":"ComposePayload","type":"n8n-nodes-base.functionItem","notes":"Create Discord embed object and stylize it's appearance","position":[770,500],"parameters":{"functionCode":"// Debug\n//console.log(item);\n\n// Format Dates (show time only)\n// Start date\nif (item.start !== undefined) {\n  item.start.dateTime = new Date(item.start.dateTime).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', timeZone: 'Europe/Moscow'});\n}\n// End date\nif (item.end !== undefined) {\n  item.end.dateTime = new Date(item.end.dateTime).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', timeZone: 'Europe/Moscow'});\n}\n\n// Format Attendees list\nitem.attendeesFormatted = \"\";\nif (item.attendees !== undefined && item.attendees.length) {\n  item.attendees.forEach(function (elem) {\n    // Debug\n    //console.log(elem.email);\n    //console.log(elem.displayName);\n\n    if (elem.displayName !== undefined) {\n      item.attendeesFormatted += elem.displayName + \" (\" + elem.email + \") • \";\n    } else {\n      item.attendeesFormatted += elem.email + \" • \";\n    }\n  });\n}\nitem.attendeesFormatted = item.attendeesFormatted.slice(0, -3); \n\n// Discord \nitem.discord = {};\nitem.discord.username = \"Calendar Notifier\";\nitem.discord.avatar_url = \"https://i.pravatar.cc/300\";\n\n// Format Description\nif (item.description === undefined) {\n  item.description = \"\";\n}\nlet description = \"\";\n// Deal if html content is detected\nif (/<(br|basefont|hr|input|source|frame|param|area|meta|!--|col|link|option|base|img|wbr|!DOCTYPE).*?>|<(a|abbr|acronym|address|applet|article|aside|audio|b|bdi|bdo|big|blockquote|body|button|canvas|caption|center|cite|code|colgroup|command|datalist|dd|del|details|dfn|dialog|dir|div|dl|dt|em|embed|fieldset|figcaption|figure|font|footer|form|frameset|head|header|hgroup|h1|h2|h3|h4|h5|h6|html|i|iframe|ins|kbd|keygen|label|legend|li|map|mark|menu|meter|nav|noframes|noscript|object|ol|optgroup|output|p|pre|progress|q|rp|rt|ruby|s|samp|script|section|select|small|span|strike|strong|style|sub|summary|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|track|tt|u|ul|var|video).*?<\\/\\2>/i.test(item.description)) {\n  description = item.description.replace(/<[^p>]*(>|$)|<[^br>]*(>|$)/g, '\\n');\n  description = description.replace(/&nbsp;/g, ' ');\n  description = description.replace(/&raquo;|&laquo;/g, '\"');\n  description = description.replace(/<a([^>]*?)href\\s*=\\s*(['\"])([^\\2]*?)\\2\\1*>/g, '$3\\n');\n} else if (item.description.length > 0) {\n  description = \"\\n```\" + item.description + \"```\";\n}\n\ndescription=\"`\" + item.start.dateTime + \" - \" + item.end.dateTime + \"`\" + description + \"\";\n\n// Icon url\nlet iconURL = \"\";\nif (item.conferenceData !== undefined) {\n  iconURL = item.conferenceData.conferenceSolution.iconUri;\n}\n\nitem.discord.embeds = [\n  {\n    title: item.summary,\n    url: item.hangoutLink,\n    description: description,\n    thumbnail: {\n      url: iconURL \n    },\n    footer: {\n      text: item.attendeesFormatted\n    }  \n  }\n];\n\nreturn item;"},"notesInFlow":true,"typeVersion":1},{"name":"PostToDiscordWebhook","type":"n8n-nodes-base.httpRequest","notes":"Post it to Discord webhook.\n\nTHE END.","position":[1030,500],"parameters":{"url":"https://discordapp.com/api/webhooks/xxx/xxxxxx","options":{},"requestMethod":"POST","bodyParametersUi":{"parameter":[{"name":"content"},{"name":"embeds","value":"={{$node[\"ComposePayload\"].json[\"discord\"][\"embeds\"]}}"},{"name":"username","value":"={{$json[\"discord\"][\"username\"]}}"},{"name":"avatar_url","value":"={{$json[\"discord\"][\"avatar_url\"]}}"}]}},"notesInFlow":true,"typeVersion":1}],"active":true,"settings":{"timezone":"Europe/Kaliningrad","saveExecutionProgress":"DEFAULT"},"connections":{"GetAllEvents":{"main":[[{"node":"PrepareDatetimes","type":"main","index":0}]]},"SetTimeframe":{"main":[[{"node":"GetAllEvents","type":"main","index":0}]]},"ComposePayload":{"main":[[{"node":"PostToDiscordWebhook","type":"main","index":0}]]},"PollingInterval":{"main":[[{"node":"SetTimeframe","type":"main","index":0}]]},"PrepareDatetimes":{"main":[[{"node":"IfEventFitsInTimeframe","type":"main","index":0}]]},"On clicking 'execute'":{"main":[[{"node":"SetTimeframe","type":"main","index":0}]]},"IfEventFitsInTimeframe":{"main":[[{"node":"ComposePayload","type":"main","index":0}]]}}}