Skip to content

Instantly share code, notes, and snippets.

@maartenba
Created January 15, 2021 09:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save maartenba/cf919233b44d9a53c29ff6459388bc43 to your computer and use it in GitHub Desktop.
Save maartenba/cf919233b44d9a53c29ff6459388bc43 to your computer and use it in GitHub Desktop.
JetBrains Space - Synchronize a personal iCal calendar with Space calendar using Automation
@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