-
-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dynamic Calendar Options for go-calendar Project #5
Comments
Awesome! Thank you for getting a proof of concept online, that's sweet. Indeed I do use CF as the registrar/domain host, did not know their workers are free - thats cool. Feel free to submit this as a PR and I can review it when I get time and look at deploying something like this. I think the binary nature of include exclude is fine, a UI for the URL builder could reflect that. I can't envisiage a scenario where you are already only including what you want to then need to also exclude something, or vice versa. |
This is awesome @selfish! I just started using the official calendar but since I want to use it with Google Calendar it didn't work so now I've switched to using your solution. Hope you keep hosting your version of it until it can get integrated in the official one. |
F me, I got a bit overwhelmed by life after we spoke, and I completely forgot to respond. @othyn, I'm sorry for the delay. Here's the full worker code, free to use. I'll leave mine running for the foreseeable future, and if you need any help setting it up, I'm happy to chat. const categories = {
community_day: '[Community Day]',
elite_raids: '[Elite Raids]',
event: '[Event]',
go_battle_league: '[GO Battle League]',
limited_research: '[Limited Research]',
pokemon_go_tour: '[Pokémon GO Tour]',
pokemon_spotlight_hour: '[Pokémon Spotlight Hour]',
raid_battles: '[Raid Battles]',
raid_hour: '[Raid Hour]',
research_breakthrough: '[Research Breakthrough]',
research: '[Research]',
season: '[Season]',
team_go_rocket: '[Team GO Rocket]',
timed_research: '[Timed Research]',
update: '[Update]',
all_day: 'all_day'
};
/**
* Fetch the ICS calendar file, add timezone info, and respond with the modified file.
* @param {Request} request
*/
async function handleRequest(request) {
const url = new URL(request.url);
const timezone = url.searchParams.get('timezone') || 'UTC';
const exclude = url.searchParams.getAll('exclude');
const include = url.searchParams.getAll('include');
if (!isValidTimezone(timezone)) {
const validTimezones = getAllValidTimezones();
return new Response(`Invalid timezone: ${timezone}\n\nValid timezones:\n${validTimezones.join('\n')}`, {
status: 400,
headers: { 'content-type': 'text/plain' },
});
}
if (exclude.length > 0 && include.length > 0) {
return new Response('Please use either "exclude" or "include" parameters, not both.', {
status: 400,
headers: { 'content-type': 'text/plain' },
});
}
const invalidExcludes = exclude.filter(value => !(value in categories));
const invalidIncludes = include.filter(value => !(value in categories));
if (invalidExcludes.length > 0 || invalidIncludes.length > 0) {
const invalidValues = [...invalidExcludes, ...invalidIncludes].join(', ');
const validCategories = Object.keys(categories).join('\n');
return new Response(`Invalid categories provided: ${invalidValues}\n\nValid categories:\n${validCategories}`, {
status: 400,
headers: { 'content-type': 'text/plain' },
});
}
const icsUrl = 'https://github.com/othyn/go-calendar/releases/latest/download/gocal.ics';
const response = await fetch(icsUrl);
const icsData = await response.text();
const modifiedIcsData = addTimezoneInfo(icsData, timezone);
if(exclude.length === 0 && include.length === 0){
return new Response(modifiedIcsData, {
headers: { 'content-type': 'text/calendar; charset=utf-8' },
});
}
const filteredIcsData = filterEvents(modifiedIcsData, exclude, include);
return new Response(filteredIcsData, {
headers: { 'content-type': 'text/calendar; charset=utf-8' },
});
}
/**
* Check if the provided timezone is valid.
* @param {string} timezone - The timezone to be validated.
* @returns {boolean} True if the timezone is valid, otherwise false.
*/
function isValidTimezone(timezone) {
try {
new Intl.DateTimeFormat('en-US', { timeZone: timezone });
return true;
} catch (error) {
return false;
}
}
/**
* Add timezone info to an ICS calendar file.
* @param {string} icsData - The ICS calendar file data.
* @param {string} timezone - The timezone to be added.
* @returns {string} The modified ICS calendar file data with timezone info added.
*/
function addTimezoneInfo(icsData, timezone) {
const lines = icsData.split('\r\n');
const newLines = [];
// DTSTART;TZID=Asia/Jerusalem:20230329T130000
for (const line of lines) {
if (line.startsWith('DTSTART')) {
newLines.push(line.replace('DTSTART', `DTSTART;TZID=${timezone}`));
} else if (line.startsWith('DTEND')) {
newLines.push(line.replace('DTEND', `DTEND;TZID=${timezone}`));
} else if (line.startsWith('BEGIN:VCALENDAR')) {
newLines.push(line);
newLines.push(`X-WR-TIMEZONE:${timezone}`);
} else {
newLines.push(line);
}
}
return newLines.join('\r\n');
}
/**
* Get all valid timezones.
* @returns {Array<string>} An array of valid timezone strings.
*/
function getAllValidTimezones() {
return Intl.supportedValuesOf('timeZone');
}
/**
* Filter events based on include or exclude parameters.
* @param {string} icsData - The ICS calendar file data.
* @param {Array<string>} exclude - The categories to exclude.
* @param {Array<string>} include - The categories to include.
* @returns {string} The filtered ICS calendar file data.
*/
function filterEvents(icsData, exclude, include) {
const lines = icsData.split('\r\n');
const newLines = [];
let insideEvent = false;
let eventLines = [];
let isAllDayEvent = undefined;
for (const line of lines) {
if (line.startsWith('BEGIN:VEVENT')) {
insideEvent = true;
eventLines = [];
}
if (insideEvent) {
eventLines.push(line);
} else {
newLines.push(line);
}
if (line.startsWith('END:VEVENT')) {
insideEvent = false;
const summaryLine = eventLines.find(eventLine => eventLine.startsWith('SUMMARY'));
const startTimeLine = eventLines.find(eventLine => eventLine.startsWith('DTSTART'));
const isAllDayEvent = startTimeLine.includes('VALUE=DATE:');
const category = summaryLine.slice(summaryLine.indexOf('['), summaryLine.indexOf(']') + 1);
const categoryKey = Object.keys(categories).find(key => categories[key] === category);
if (exclude.length > 0 && !exclude.includes(categoryKey)) {
if (exclude.includes('all_day') && isAllDayEvent) continue;
newLines.push(...eventLines);
} else if (include.length > 0 && include.includes(categoryKey)) {
if (include.includes('all_day') && !isAllDayEvent) continue;
newLines.push(...eventLines);
}
}
}
return newLines.join('\r\n');
}
export default {
async fetch(request, env) {
return handleRequest(request);
},
}; |
Amazing, thank you so much @selfish. Sorry for my delayed reply too, this is a fabulous contribution, thank you for your efforts. I too have been overcome by life as of late, not sure when I'll get a chance to properly implement all this. Especially as I haven't played the game in months - lost interest after all the remote raid pass changes. |
Yep, my enthusiasm has settled as well after the recent changes, and admittedly, also the Pokemon Go Fest was a massive disappointment this year. |
These worked perfectly for me after I swapped the timezone parameter out.
Does Selfish have a forked repo I could look at to see how it works on their personal domain? Or is it more on the config side of Cloudflare where the changes are needed? |
@BrentTheBert this has to be manually copied into the CF config so @othyn has to do it. It may be possible to setup using some ci/cd, but setting up something like that will probably require even more effort. I have no fork, because the code I shared is meaningless without CF, but feel free to use it through my domain for now (as I have been doing for months) and if you want me to make changes, we can communicate over here or if you want to PR over this I can make a repo/gist. Cheers |
It won't be something I broach soon, so by all means create a fork or use the current hosted version 👍 thank you @selfish! |
The cloudflare worker code will need to be adjusted slightly to account for the acronym changes made in 2.4.0 a few days ago. The only part that needs to be changed is the Before: const categories = {
community_day: '[Community Day]',
elite_raids: '[Elite Raids]',
event: '[Event]',
go_battle_league: '[GO Battle League]',
limited_research: '[Limited Research]',
pokemon_go_tour: '[Pokémon GO Tour]',
pokemon_spotlight_hour: '[Pokémon Spotlight Hour]',
raid_battles: '[Raid Battles]',
raid_hour: '[Raid Hour]',
research_breakthrough: '[Research Breakthrough]',
research: '[Research]',
season: '[Season]',
team_go_rocket: '[Team GO Rocket]',
timed_research: '[Timed Research]',
update: '[Update]',
all_day: 'all_day'
}; After: const categories = {
community_day: '[CD]',
elite_raids: '[ER]',
event: '[E]',
go_battle_league: '[GBL]',
limited_research: '[LR]',
pokemon_go_tour: '[PGT]',
pokemon_spotlight_hour: '[PSH]',
raid_battles: '[RB]',
raid_hour: '[RH]',
research_breakthrough: '[RBT]',
research: '[R]',
season: '[S]',
team_go_rocket: '[TGR]',
timed_research: '[TR]',
update: '[U]',
all_day: 'all_day'
}; I've tested this on my own cloudflare worker and it appears to be working as expected. |
Originally posted by @othyn in #4 (comment)
--
Hey @othyn,
I've been thinking about your comment on Option 2 in the go-calendar project, and I decided to give the dynamic option a try. I hope it's okay that I decided to split the discussion to a new issue.
If your domain is on Cloudflare, it would be super easy to set up with a Cloudflare Worker (which is free). I made a proof of concept and put it under my personal domain. It's not fully tested, so there might be some errors, but here are the links for reference:
timezone
paramExclude
events (blacklist style)Include
events (whitelist style)all_day
(see notes)Just a few notes:
all_day
, which filters all-day events across categories.I've actually been using it with Google Calendar for about a week now with no issues.
I'd be happy to contribute the full code for this! Let me know the best way to do that, and if you need any help setting it up under your own domain.
The text was updated successfully, but these errors were encountered: