Skip to content

Commit

Permalink
Load up the two schedules and compare them, ready for the actual sync…
Browse files Browse the repository at this point in the history
…hronization
  • Loading branch information
Rosuav committed Jan 15, 2025
1 parent 154b669 commit 6775631
Showing 1 changed file with 78 additions and 7 deletions.
85 changes: 78 additions & 7 deletions modules/http/chan_calendar.pike
Original file line number Diff line number Diff line change
Expand Up @@ -88,27 +88,98 @@ __async__ mapping get_chan_state(object channel, string grp) {

__async__ void synchronize(int userid) {
mapping cfg = await(G->G->DB->load_config(userid, "calendar"));
//TODO: If the token has expired, refresh it. Or maybe do that inside google_api?
//werror("Token expires in %d seconds.\n", cfg->oauth->expires - time());
//This isn't entirely correct. It seems that the token only expires after a period
//of inactivity?? While I'm actively testing things, the token remains valid.
//Test this again after a day or two of quietness, and see what we need to do.

//Updating your Twitch schedule from your Google calendar is done in two parts: Weekly
//and non-weekly events. Twitch doesn't have recurrence rules for anything other than
//weekly, so if your Gcal specifies "every second Tuesday" or "first Thursday of the
//month" or something, we will turn those into individual (non-recurring) events.
//This means we need four pieces of information:
//1) Google recurring events
//2) Google non-recurring events
//3) Twitch weekly events
//4) Twitch single events
mapping timespan = ([
"timeMin": strftime("%Y-%m-%dT%H:%M:%SZ", gmtime(time())),
"timeMax": strftime("%Y-%m-%dT%H:%M:%SZ", gmtime(time() + 604800 * 4)), //Give us four weeks' worth of events
]);
//Using singleEvents: true makes it much easier to query the current schedule, as without this
//the events are given at the time that the recurrence began (maybe years ago). But we don't get
//the actual recurrence rule this way. Thus, we fetch BOTH ways, recording the IDs of all of the
//recurring events and their recurrence rules.
mapping recurring = await(google_api("calendar/v3/calendars/" + cfg->gcal_sync + "/events", "apikey", ([
"variables": timespan,
])));
mapping recurrence_rule = ([]);
foreach (recurring->items || ({ }), mapping ev) catch {
string rr = ev->recurrence[0]; //If it's absent or empty, bail.
//For now, a very strict and simplistic way to recognize recurring events.
//TODO: What happens if you delete one instance of a recurring event? We need to cancel
//the corresponding slot in Twitch, but ideally, we want to retain the record that it's
//a weekly event.
if (has_prefix(rr, "RRULE:FREQ=WEEKLY;")) recurrence_rule[ev->id] = rr;
};

//Query the current Twitch schedule. Twitch (currently) does not allow you to update the start time
//for a recurring event, and I don't think there's a way to cancel just one instance. So if you move
//one instance of a recurrer, we'll have to create that one as a one-off, and probably cancel the old
//recurrer and create a brand new one starting the following week? I think?? Incidentally, one-offs
//are only available to affiliates and partners. Not sure why, but it's something I'll have to test,
//probably on the Mustard Mine's account.
array twitch = await(get_stream_schedule(userid, 0, 1000, 604800 * 4));
//werror("Twitch schedule %O\n", twitch);
mapping existing_schedule = ([]);
foreach (twitch, mapping ev) {
//NOTE: ev->id is probably meant to be opaque, but it's the only way to recognize which
//ones are from the same recurring event. It's base 64 JSON and contains a segmentID that
//is the same for all instances of the same recurring event.

//Twitch gives us UTC start times eg "2025-01-19T23:00:00Z", but Google gives us local
//start times eg "2025-01-20T10:00:00+11:00". Convert both into time_t.
int start = Calendar.parse("%Y-%M-%DT%h:%m:%s%z", ev->start_time)->unix_time();

//werror("Twitch schedule %O id %s\n", ev->start_time, MIME.decode_base64(ev->id));
existing_schedule[start] = ev;
//ev->category->id, ev->title, ev->end_time, ev->is_recurring
//Note that Twitch won't let us update the end_time directly; instead we update the duration,
//so we'll need to do the arithmetic. But we should be able to compare end_time to endTime.
}

//XKCD 713: Meet hot singles in your calendar today!
mapping singles = await(google_api("calendar/v3/calendars/" + cfg->gcal_sync + "/events", "apikey", ([
"variables": timespan | (["singleEvents": "true", "orderBy": "startTime"]),
])));
foreach (singles->items || ({ }), mapping ev) {
string|zero rr = recurrence_rule[ev->recurringEventId];
//Note that we assume that an event starts and ends in the same timezone (eg Australia/Melbourne).
//Twitch only allows one timezone per event anyway.
int start = Calendar.parse("%Y-%M-%DT%h:%m:%s%z", ev->start->dateTime)->unix_time(); //Can't shortcut by comparing the text strings as these ones are in local time
mapping tw = m_delete(existing_schedule, start);
//For now, assume that once we've seen one event from a recurring set, we've seen 'em all.
//TODO: Handle single-instance deletion or moving of an event.
if (rr == "*Done*") continue;
werror("%s EVENT %O->%O %O %O %s\n", tw ? "EXISTING" : "NEW", ev->start->dateTime, ev->end->dateTime, ev->start->timeZone, rr, ev->summary);
//TODO: If "Category: ...." is in ev->description, set the category_id for the Twitch event
if (rr) recurrence_rule[ev->recurringEventId] = "*Done*";
}
if (sizeof(existing_schedule)) werror("Delete me: %O\n", existing_schedule);
}

__async__ mapping|zero wscmd_fetchcal(object channel, mapping(string:mixed) conn, mapping(string:mixed) msg) {
if (!stringp(msg->calendarid)) return 0;
string calendarid = msg->calendarid;
//TODO: Allow hash character in calendar ID, and properly encode. Probably not common but we should allow all valid calendar IDs.
//Be sure to also properly encode any use of cfg->gcal_sync in a URL too.
sscanf(calendarid, "%*[A-Za-z0-9@.]%s", string residue); if (residue != "") return 0;
mapping events = await(google_api("calendar/v3/calendars/" + calendarid + "/events", "apikey", ([
"variables": ([
//Using singleEvents: true makes it much easier to query the current schedule, as without this
//the events are given at the time that the recurrence began (maybe years ago). But we don't get
//the actual recurrence rule this way, so it may instead be better to calculate recurrences
//ourselves and calculate the current equivalent. Alternatively, fetch BOTH ways, as the single
//events carry a recurringEventId that links with the id of the original recurring one.
"singleEvents": "true", "orderBy": "startTime",
"timeMin": strftime("%Y-%m-%dT%H:%M:%SZ", gmtime(time())),
"timeMax": strftime("%Y-%m-%dT%H:%M:%SZ", gmtime(time() + 604800)), //Give us one week's worth of events
"timeMax": strftime("%Y-%m-%dT%H:%M:%SZ", gmtime(time() + 604800)), //Give us one week's worth of events for a quick preview
])]),
));
if (events->error) {
Expand Down Expand Up @@ -169,4 +240,4 @@ __async__ mapping wscmd_googlelogin(object channel, mapping(string:mixed) conn,
return (["cmd": "googlelogin", "uri": uri]);
}

protected void create(string name) {::create(name);}
protected void create(string name) {::create(name); /*synchronize(49497888);*/}

0 comments on commit 6775631

Please sign in to comment.