Created
January 15, 2021 09:12
-
-
Save maartenba/cf919233b44d9a53c29ff6459388bc43 to your computer and use it in GitHub Desktop.
JetBrains Space - Synchronize a personal iCal calendar with Space calendar using Automation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@file:DependsOn("net.sf.biweekly:biweekly:0.6.4") | |
@file:DependsOn("com.squareup.okhttp3:okhttp:3.12.1") | |
import biweekly.component.* | |
import biweekly.util.* | |
import biweekly.ICalendar | |
import biweekly.io.text.ICalReader | |
import java.util.Date | |
import okhttp3.OkHttpClient | |
import okhttp3.Request | |
job("Synchronize agenda") { | |
startOn { | |
gitPush { enabled = true } // run on git push... | |
schedule { cron("0 12 * * *") } // ...and on schedule | |
} | |
failOn { | |
timeOut { timeOutInMinutes = 15 } | |
} | |
container("openjdk:11") { | |
env["SPACE_USERNAME"] = Params("sa_username") | |
env["ICAL_URL"] = Secrets("sa_ical_url") | |
kotlinScript { api -> | |
// 0. Handle parameters | |
val username = System.getenv("SPACE_USERNAME")?.trim() | |
if (username.isNullOrEmpty()) throw Exception("No username is provided. Create a Space parameter 'sa_username' and set it to the Space username to synchronize calendar events for.") | |
val icalUrl = System.getenv("ICAL_URL")?.trim() | |
if (icalUrl.isNullOrEmpty()) throw Exception("No iCal URL is provided. Create a Space secret 'sa_ical_url' and set it to the your external iCal URL (http:// or https://).") | |
// 1. Setup meeting title, synchronization period, ... | |
val freeEventTitle = "Available (personal)" | |
val busyEventTitle = "Busy (personal)" | |
val startingAfter = Clock.System.now().plus(-2, DateTimeUnit.DAY, TimeZone.UTC) | |
val endingBefore = Clock.System.now().plus(9, DateTimeUnit.DAY, TimeZone.UTC) | |
// 2. Get current meetings from Space | |
println("Get current meetings from Space...") | |
val userProfile = api.space().teamDirectory.profiles | |
.getProfile(ProfileIdentifier.Username(username)) | |
val spaceMeetings = api.space().calendars.meetings | |
.getAllMeetings( | |
profiles = listOf(userProfile.id), | |
startingAfter = startingAfter, | |
endingBefore = endingBefore, | |
includePrivate = true, | |
includeArchived = false, | |
includeMeetingInstances = true) | |
// 3. Get entries from external calendar | |
println("Get entries from external calendar...") | |
val icalEvents = mutableListOf<VEvent>() | |
val icalResponse = retrieveUrl(icalUrl) | |
ICalReader(icalResponse).use { reader -> | |
var icalCalendar: ICalendar? | |
while (reader.readNext().also { icalCalendar = it } != null) { | |
icalCalendar?.let { | |
icalEvents.addAll( | |
it.events.filter { event -> | |
event.dateStart.value.after(startingAfter) && | |
event.dateStart.value.before(endingBefore) | |
}) | |
} | |
} | |
} | |
// 4. Update existing meetings in Space... | |
println("Update existing meetings in Space...") | |
spaceMeetings.data.filter { it.summary == freeEventTitle || it.summary == busyEventTitle }.forEach { meeting -> | |
val correspondingIcalEvent = icalEvents.firstOrNull { | |
it.dateStart.value.matches(meeting.occurrenceRule.start) && | |
it.dateEnd.value.matches(meeting.occurrenceRule.end) && | |
it.busyStatus() == meeting.occurrenceRule.busyStatus | |
} | |
if (correspondingIcalEvent == null) { | |
// Remove from Space | |
api.space().calendars.meetings.deleteMeeting(meeting.id) | |
println(" [remove] " + meeting.summary) | |
} else { | |
// Already in sync, no need to re-sync... | |
icalEvents.remove(correspondingIcalEvent) | |
println(" [skip] " + correspondingIcalEvent.summary.value) | |
} | |
} | |
// 5. Create meetings in Space | |
println("Create meetings in Space...") | |
icalEvents.forEach { event -> | |
println(" [create] " + event.summary.value) | |
api.space().calendars.meetings.createMeeting( | |
summary = if (event.busyStatus() == BusyStatus.Busy) busyEventTitle else freeEventTitle, | |
occurrenceRule = CalendarEventSpec( | |
start = event.dateStart.value.asInstant(), | |
end = event.dateEnd.value.asInstant(), | |
recurrenceRule = null, | |
timezone = ATimeZone("UTC"), | |
busyStatus = event.busyStatus(), | |
allDay = false, | |
initialMeetingStart = null, | |
parentId = null, | |
nextChainId = null | |
), | |
profiles = listOf(userProfile.id), | |
visibility = MeetingVisibility.EVERYONE, | |
modificationPreference = MeetingModificationPreference.EVERYONE, | |
joiningPreference = MeetingJoiningPreference.NOBODY, | |
notifyOnExport = false, | |
organizer = userProfile.id | |
) | |
} | |
} | |
} | |
} | |
fun retrieveUrl(icalUrl: String): String { | |
val request = Request.Builder() | |
.url(icalUrl) | |
.build() | |
val client = OkHttpClient() | |
return client.newCall(request) | |
.execute() | |
.use { it.body()!!.string() } | |
} | |
fun VEvent.busyStatus(): BusyStatus { | |
if (this.transparency != null) { | |
return if (this.transparency.value == "OPAQUE") BusyStatus.Busy else BusyStatus.Free | |
} | |
return BusyStatus.Busy | |
} | |
fun ICalDate.asInstant() = Instant.fromEpochSeconds(this.toInstant().epochSecond) | |
fun ICalDate.after(other: Instant) = this.after(Date(other.toEpochMilliseconds())) | |
fun ICalDate.before(other: Instant) = this.before(Date(other.toEpochMilliseconds())) | |
fun ICalDate.matches(other: Instant) = this.toInstant().epochSecond == other.epochSeconds |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment