Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,84 @@ Type in `calculator functions` to see the full list of available operators and f

Type in `calculator parameters` to see the full list of available configuration parameters for `application.conf`.

#### Calendar

You can add events to a configured calendar and view oncoming events. Currently only Google calendars are supported.

**Pre-configuring Google calendar**

To be able to view and modify events, you have to allow the calendar to be accessed from within the application.
Open the settings for your calendar and add `cccapp@commandcentercalendar.iam.gserviceaccount.com` to the *Share with specific people* section.
Select the option "Make changes to events".

![configuring Google calendar](assets/configure-google-calendar.png "Configuring Google calendar")

**Configuring application**

Then copy the calendar ID from the *Integrate calendar* section

![Google calendar ID](assets/configure-google-calendar_ID.png "Google calendar ID")

and paste it to the `application.conf`:

```hocon
type: "CalendarCommand"
client: {
type: "Google"
calendarId: "xyz@gmail.com"
}
```

There you can also configure the date and time format (used both for parsing and displaying) as well as provide some date aliases:

```hocon
formats: {
dateFormat: "dd.MM.yyyy"
timeFormat: "HH:mm"
dayOffsets: {
today: 0
tomorrow: 1
next week: 7
}
}
```

**Listing oncoming events**

Use the `list` subcommand to get `n` oncoming events (by default `n == 3`):

`cal list 4`

**Adding new event**

For adding a new event there are two options:
- the simplest form: `<summary> @ <date/time>`:

`John's welcome party @ 19:00`

You can specify either date, or time, or both. Time without a date counts as a today event.

- if you want to provide more details use the full form:

`cal insert "An important event" --date tomorrow --time 18:00 --desc "A very important event, trust me" --loc Somewhere`

Available options are:
- `--date` (or `-d`) - (start) date
- `--time` (or `-t`) - optional (start) time; omit for all-day events
- `--enddate` - optional end date; if omitted, equals to start date
- `--endtime` - optional end time; required if start time is given
- `--desc` - optional detailed description
- `--loc` - optional location

Both for simple and full form you can use configured date aliases instead of concrete dates. For each specify a name and an offset (in days) from the current day.
You can also use localized names, e. g.
```
dayOffsets: {
heute: 0
morgen: 1
}
```

## Installation

At the moment there is no simple "1-step install". You need to compile and generate an executable yourself (or run
Expand Down
15 changes: 15 additions & 0 deletions application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,21 @@ commands: [
{type: "SearchMavenCommand"}
{type: "HoogleCommand"}
{type: "CalculatorCommand"}
{
type: "CalendarCommand"
client: {
type: "Google"
calendarId: "xyz@gmail.com"
}
formats: {
dateFormat: "dd.MM.yyyy"
timeFormat: "HH:mm"
dayOffsets: {
today: 0
tomorrow: 1
}
}
}
]

aliases = {
Expand Down
Binary file added assets/configure-google-calendar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/configure-google-calendar_ID.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 24 additions & 21 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -48,27 +48,30 @@ lazy val root = project
lazy val core = module("core")
.settings(
libraryDependencies ++= Seq(
"dev.zio" %% "zio" % Version.zio,
"dev.zio" %% "zio-streams" % Version.zio,
"dev.zio" %% "zio-process" % "0.4.0",
"dev.zio" %% "zio-logging" % "0.5.10",
"io.github.kitlangton" %% "zio-magic" % "0.3.2",
"io.circe" %% "circe-config" % "0.8.0",
"org.scala-lang" % "scala-reflect" % "2.13.6",
"io.circe" %% "circe-core" % Version.circe,
"io.circe" %% "circe-parser" % Version.circe,
"com.monovore" %% "decline" % "2.0.0",
"com.lihaoyi" %% "fansi" % "0.2.14",
"com.beachape" %% "enumeratum" % Version.enumeratum,
"com.beachape" %% "enumeratum-circe" % Version.enumeratum,
"com.softwaremill.sttp.client" %% "core" % Version.sttp,
"com.softwaremill.sttp.client" %% "circe" % Version.sttp,
"com.softwaremill.sttp.client" %% "httpclient-backend-zio" % Version.sttp,
"com.lihaoyi" %% "fastparse" % "2.3.2",
"org.typelevel" %% "spire" % "0.17.0",
"org.cache2k" % "cache2k-core" % "1.6.0.Final",
"net.java.dev.jna" % "jna" % Version.jna,
"net.java.dev.jna" % "jna-platform" % Version.jna
"dev.zio" %% "zio" % Version.zio,
"dev.zio" %% "zio-streams" % Version.zio,
"dev.zio" %% "zio-process" % "0.4.0",
"dev.zio" %% "zio-logging" % "0.5.10",
"io.github.kitlangton" %% "zio-magic" % "0.3.2",
"io.circe" %% "circe-config" % "0.8.0",
"org.scala-lang" % "scala-reflect" % "2.13.6",
"io.circe" %% "circe-core" % Version.circe,
"io.circe" %% "circe-parser" % Version.circe,
"com.monovore" %% "decline" % "2.0.0",
"com.lihaoyi" %% "fansi" % "0.2.14",
"com.beachape" %% "enumeratum" % Version.enumeratum,
"com.beachape" %% "enumeratum-circe" % Version.enumeratum,
"com.softwaremill.sttp.client" %% "core" % Version.sttp,
"com.softwaremill.sttp.client" %% "circe" % Version.sttp,
"com.softwaremill.sttp.client" %% "httpclient-backend-zio" % Version.sttp,
"com.lihaoyi" %% "fastparse" % "2.3.2",
"org.typelevel" %% "spire" % "0.17.0",
"org.cache2k" % "cache2k-core" % "1.6.0.Final",
"net.java.dev.jna" % "jna" % Version.jna,
"net.java.dev.jna" % "jna-platform" % Version.jna,
"com.google.apis" % "google-api-services-calendar" % "v3-rev20210215-1.31.0",
"com.google.oauth-client" % "google-oauth-client-jetty" % "1.31.2",
"com.google.auth" % "google-auth-library-oauth2-http" % "0.25.2"
),
buildInfoKeys := Seq[BuildInfoKey](version, scalaVersion, sbtVersion),
buildInfoPackage := "commandcenter"
Expand Down
12 changes: 12 additions & 0 deletions core/src/main/resources/calendar/google/credentials.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"type": "service_account",
"project_id": "commandcentercalendar",
"private_key_id": "3cca8a21871afb7ef67e0595e5cb40d00d4a7a83",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC4RfP2vPt+si0B\nRBVKxgNzbBiVq4sPGiNdkKh1VOWwk4eX8p5P6cn/s/eIOvTtCnbICBddOEjm59jH\nS6ajyLPf1QTMhoww9IPFzP4vJqQOQIbXWfAyndWs+OcY7jFNJFe+DOfmP+QdVWyF\n7t2DbttlWOCg7s8SOGYbtEdBqHPm90iDMSqQD2Yj+WHB/TcUeAuvn4ptjvCJBwCS\nrjmDyuuLXt9u+eT9qg3+5VTbBoXqjquLdaydVZP6nIQ1QCnHsLUmOSTTi4EFLuqt\nc7pD8Ff+1wc7u9vXZjDCIw+G1Ot+XYkH9X9a6GeXOpoYekdyx8IfbovRV8bga8dz\n3gGBvgMVAgMBAAECggEABEZc4gNBVX1QiPcOBED73svTGnd8YbphJYJ0hyTdH4Z5\nNZaMp4HMlaLv9DOagfxlX3ZzcpeIUf8BD332k58ei2vXmnGfw2KwwwlQLkKO3eTq\n5Qh7mc/c1llbAEAhNRglGTvOIJBfTpcwO60ua10TgS2+J7tSh3Obo1+bx9ynxohd\n/xY7debIsZRb7Y+I5Lazk0sFBFZrWAQK08nYpCp373LvPQ4+6vSgocL6WjXLbD+z\nYL8gI/bUkcbZUOHESumbWkWKyNTuDYsuGje1lv1P2TD8yMn2AbhxTbCfkdd5l0UO\nxbmGvpUTLX23DwzpUGZz+J6sUMhh2pQZq1hMzvenwwKBgQD+T9nr+WkfdVvbMao+\n1eOuM44aTjPY6Y6jrga/5AcK8H3HuMLOmPbpnLpzDocsxHXfqcypzD1Gc9K4P0bn\nX5ZhA9IFJZY1oNi7GVPYIA1fXLHGrF9QHhRNZiu1A6By4E5Iktyl0yD7jtDWsu9x\nNsStnXrKgr/ulSDd/30k9naeWwKBgQC5fxYDQnQkfNBKmO4LdU/F8TWl30wiy46j\nwx33pL//48Y3W2yTvlfgbJOew2qK8kM+SBJXMmZ5a4Me+26gxC0kifmka+R5zCot\nd44xkhvdBK0j4GhtUxWzIbfzRsjZxLbVFzjiK/UR2nyyolIv8PonS0CDCY88BMXG\n00phe/x/TwKBgFzO7VuVuMx3Ot+Cf3vQ+PdA71IgSgGWMqz/PI1Y/Uz7uRtjDQzy\ne+GDhfOpUKGAzPej8wHgfFgyuqrsxMZ5dtrO1x1zux61JYMaWiPchqTPoj07+Mi3\nQXeBmt/DhBGIVGld11JY+4dydjp0MLfjYeFuQDqZfsvl9omtzJDptR5TAoGAZgK+\nz1IXXw2I2s1Zc9Gy6i9pimvPif8Z1XNzIoJm2Emh80WC44k0+IWddR0QlZL/advm\nwi9EbZezhzFMuHrKPKLoOAThpB2kQFbUSuyICDcPJIC/zQd5EocDi3Us9Z2Z0nwv\n2ynDX2shUnez7Qt/9mYK90UlkSMqxNnjuNKfnD8CgYBCj0L6gL2HiO99s+ghPAek\n+Uys6o6lmN60BRyn36RspdEUvD+29WpVzvxBvVHpATqs2EN+rfLZz1fSkxcMgywX\ni2hlRKsX/PItBaXnSGSP1hq7N4wmiKhjJhv46YjpPCc5lUn4/ACOQpDXOvdIEPFf\nzN7UkG6p2OTqhOrecu8cNQ==\n-----END PRIVATE KEY-----\n",
"client_email": "cccapp@commandcentercalendar.iam.gserviceaccount.com",
"client_id": "104849388392349094923",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/cccapp%40commandcentercalendar.iam.gserviceaccount.com"
}
140 changes: 140 additions & 0 deletions core/src/main/scala/commandcenter/command/CalendarCommand.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package commandcenter.command

import com.monovore.decline
import com.typesafe.config.Config
import commandcenter.CCRuntime.Env
import commandcenter.CommandContext
import commandcenter.command.CalendarCommand.Formats
import commandcenter.command.calendar._
import commandcenter.view.DefaultView
import fansi.Str
import io.circe
import zio.{ TaskManaged, UIO, ZIO, ZManaged }

import java.time.format.DateTimeFormatter
import java.time.{ LocalDate, LocalDateTime, LocalTime, ZoneId }
import scala.util.Try

final case class CalendarCommand(override val commandNames: List[String], client: Client, formats: Formats)
extends Command[Unit] {
val commandType: CommandType = CommandType.CalendarCommand
val title: String = "Calendar"
val freeTextParser: CalendarFreeTextParser = new CalendarFreeTextParser(formats)
val cliCommand: decline.Command[Product] = new CalendarCommandLineParser(formats).parseCommand

override def preview(searchInput: SearchInput): ZIO[Env, CommandError, PreviewResults[Unit]] =
previewFreeText(searchInput) <> previewCommandLine(searchInput)

private def previewFreeText(searchInput: SearchInput): ZIO[Any, CommandError, PreviewResults[Unit]] =
for {
request <- ZIO.fromOption(freeTextParser.parseText(searchInput.input)).orElseFail(CommandError.NotApplicable)
} yield previewInsertRequest(request, searchInput.context)

private def previewCommandLine(searchInput: SearchInput): ZIO[Any, CommandError, PreviewResults[Unit]] =
for {
input <- ZIO.fromOption(searchInput.asArgs).orElseFail(CommandError.NotApplicable)
parsed = cliCommand.parse(input.args)
previewItem <- ZIO
.fromEither(parsed)
.foldM(
help => UIO(HelpMessage.formatted(help)),
{
case request: ListRequest => client.list(request).mapError(CommandError.UnexpectedException)
case request: InsertRequest => UIO(request)
}
)
} yield previewItem match {
case ListResponse(events) =>
PreviewResults.fromIterable(
events.map { event =>
Preview.unit
.score(Scores.high(input.context))
.view(DefaultView("Calendar event", event.toString(formats)))
}
)
case request: InsertRequest =>
previewInsertRequest(request, input.context)
case help: Str =>
PreviewResults.one(
Preview.unit
.score(Scores.high(input.context))
.view(DefaultView(title, help))
)
}

private def previewInsertRequest(request: InsertRequest, context: CommandContext) = {
val run = for {
_ <- client.insert(request)
} yield ()
PreviewResults.one(
Preview.unit
.onRun(run)
.score(Scores.high(context))
.view(DefaultView("Add calendar event", request.event.toString(formats)))
)
}
}

object CalendarCommand extends CommandPlugin[CalendarCommand] {

override def make(config: Config): TaskManaged[CalendarCommand] =
ZManaged.fromEither(
for {
commandNames <- config.get[Option[List[String]]]("commandNames")
client <- getClient(config.getConfig("client"))
formats <- getFormats(config.getConfig("formats"))
} yield CalendarCommand(commandNames.getOrElse(List("calendar", "cal")), client, formats)
)

private def getClient(config: Config): Either[Exception, Client] =
for {
clientTypeString <- config.get[String]("type")
clientType <- ClientType.withNameEither(clientTypeString)
client <- clientType match {
case ClientType.Google => getGoogleClient(config)
}
} yield client

private def getGoogleClient(config: Config): Either[circe.Error, GoogleClient] =
for {
calendarId <- config.get[String]("calendarId")
} yield GoogleClient(calendarId)

private def getFormats(config: Config): Either[circe.Error, Formats] = for {
dateFormat <- config.get[String]("dateFormat")
timeFormat <- config.get[String]("timeFormat")
dayOffsets <- config.get[Map[String, Int]]("dayOffsets")
} yield Formats(dateFormat, timeFormat, dayOffsets)

final case class Formats(dateFormat: String, timeFormat: String, dayOffsets: Map[String, Int]) {
val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat)
val timeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern(timeFormat)
val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern(s"$dateFormat $timeFormat")

def parseDate(dateString: String): Option[LocalDate] =
dayOffsets.get(dateString.toLowerCase) match {
case Some(offset) => Some(getLocalDateByOffset(offset))
case _ => Try(LocalDate.parse(dateString, dateFormatter)).toOption
}

def parseTime(timeString: String): Option[LocalTime] =
Try(LocalTime.parse(timeString, timeFormatter)).toOption

def parseDateTime(dateTimeString: String): Option[LocalDateTime] =
dayOffsets.find { case (key, _) =>
dateTimeString.startsWith(key)
} match {
case Some((key, offset)) =>
parseTime(dateTimeString.stripPrefix(s"$key ")) match {
case Some(time) =>
val date = getLocalDateByOffset(offset)
Some(LocalDateTime.of(date, time))
case _ => None
}
case _ => Try(LocalDateTime.parse(dateTimeString, dateTimeFormatter)).toOption
}

private def getLocalDateByOffset(offset: Int): LocalDate =
LocalDate.now(ZoneId.systemDefault()).plusDays(offset)
}
}
1 change: 1 addition & 0 deletions core/src/main/scala/commandcenter/command/Command.scala
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ object Command {
typeName <- Task(config.getString("type")).toManaged_
command <- CommandType.withNameOption(typeName).getOrElse(CommandType.External(typeName)) match {
case CommandType.CalculatorCommand => CalculatorCommand.make(config)
case CommandType.CalendarCommand => CalendarCommand.make(config)
case CommandType.DecodeBase64Command => DecodeBase64Command.make(config)
case CommandType.DecodeUrlCommand => DecodeUrlCommand.make(config)
case CommandType.EncodeBase64Command => EncodeBase64Command.make(config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ sealed trait CommandType extends EnumEntry

object CommandType extends Enum[CommandType] {
case object CalculatorCommand extends CommandType
case object CalendarCommand extends CommandType
case object DecodeBase64Command extends CommandType
case object DecodeUrlCommand extends CommandType
case object EncodeBase64Command extends CommandType
Expand Down
Loading