Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
deanishe committed Nov 30, 2017
0 parents commit afc3b68
Show file tree
Hide file tree
Showing 42 changed files with 4,141 additions and 0 deletions.
69 changes: 69 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/gcal
/build
.autoenv*

# Created by https://www.gitignore.io/api/go,sublimetext,vim

### Go ###
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib

# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/

### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache

# workspace files are user-specific
*.sublime-workspace

# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project

# sftp configuration file
sftp-config.json

# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
Package Control.merged-ca-bundle
Package Control.user-ca-bundle
oscrypto-ca-bundle.crt
bh_unicode_properties.cache

# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings

### Vim ###
# swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
# session
Session.vim
# temporary
.netrwhist
*~
# auto-generated tag files
tags

# End of https://www.gitignore.io/api/go,sublimetext,vim
21 changes: 21 additions & 0 deletions LICENCE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
112 changes: 112 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@

<div align="center">
<img height="128" width="128" src="./icons/icon.png">
</div>

Google Calendar for Alfred
==========================

View Google Calendar events in [Alfred][alfred].

<!-- MarkdownTOC autolink="true" bracket="round" depth="3" autoanchor="true" -->

- [Download & installation](#download--installation)
- [Usage](#usage)
- [Date format](#date-format)
- [Configuration](#configuration)
- [Licensing & thanks](#licensing--thanks)

<!-- /MarkdownTOC -->


<a name="download--installation"></a>
Download & installation
-----------------------

Grab the workflow from [GitHub releases][download]. Download the `Google-Calendar-Events-X.X.alfredworkflow` file and double-click it to install.


<a name="usage"></a>
Usage
-----

When run, the workflow will open Google Calendar in your browser and ask for permission to read your calendars. If you do not grant permission, it won't work.

You will also be prompted to activate some calendars (the workflow will show events from these calendars).

- `gcal` — Show upcoming events.
- `<query>` — Filter list of events.
- `` — Open event in browser or day in workflow.
- `⌘↩` — Open event in Google or Apple Maps (if event has a location).
- `` / `⌘Y` — Quicklook event details.
- `today` / `tomorrow` / `yesterday` — Show events for the given day.
- `<query>` / `` / `⌘↩` / `` / `⌘Y` — As above.
- `gdate [<date>]` — Show one or more dates. See below for query format.
- `` — Show events for the given day.
- `gcalconf [<query>]` — Show workflow configuration.
- `Active Calendars` — Turn calendars on/off.
- `` — Toggle calendar on/off.
- `Workflow is up to Date` / `An Update is Available` — Whether a newer version of the workflow is available.
- `` — Check for or install update.
- `Open Documentation` — Open this page in your brower.
- `Get Help` — Visit [the thread for this workflow][forumthread] on [AlfredForum.com][alfredforum].
- `Report Issue`[Open an issue][issues] on GitHub.
- `Clear Cached Calendars & Events` — Remove cached data.


<a name="date-format"></a>
### Date format ###

The keyword `gdate` supports an optional date. This can be specified in a number of format:

- `YYYY-MM-DD` — e.g. `2017-12-01`
- `YYYYMMDD` — e.g. `20180101`
- `[+|-]N[d|w]` — e.g.:
- `1`, `1d` or `+1d` for tomorrow
- `-1` or `-1d` for yesterday
- `3w` for 21 days from now
- `-4w` for 4 weeks ago

<a name="configuration"></a>
Configuration
-------------

There are a couple of options in the workflow's configuration sheet (the `[x]` button in Alfred Preferences):

| Setting | Description |
|--------------------|-------------------------------------------------------------------------------------------------------------|
| `APPLE_MAPS` | Set to `1` to open map links in Apple Maps instead of Google Maps. |
| `CALENDAR_APP` | Name of application to open Google Calendar URLs (not map URLs) in. If blank, your default browser is used. |
| `EVENT_CACHE_MINS` | Number of minutes to cache event lists before updating from the server. |
| `SCHEDULE_DAYS` | The number of days' events to show with the `gcal` keyword. |


<a name="licensing--thanks"></a>
Licensing & thanks
------------------

This workflow is released under the [MIT Licence][mit].

It is heavily based on the [Google API libraries for Go][google-libs] ([BSD 3-clause licence][google-licence]) and [AwGo][awgo] libraries ([MIT][mit]), and of course, [Google Calendar][gcal].


The icons are from [Elusive Icons][elusive], [Font Awesome][awesome], [Material Icons][material], [Weather Icons][weather] (all [SIL][sil]) and [Octicons][octicons] ([MIT][mit]), via the [workflow icon generator][icongen].


[gcal]: https://calendar.google.com/calendar/
[google-libs]: https://github.com/google/google-api-go-client
[google-licence]: https://github.com/google/google-api-go-client/blob/master/LICENSE
[alfred]: https://alfredapp.com/
[alfredforum]: https://www.alfredforum.com/
[awgo]: https://github.com/deanishe/awgo
[forumthread]: https://www.alfredforum.com/
[download]: https://github.com/deanishe/alfred-gcal/releases/latest
[issues]: https://github.com/deanishe/alfred-gcal/issues
[sil]: http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL
[mit]: https://opensource.org/licenses/MIT
[elusive]: https://github.com/aristath/elusive-iconfont
[awesome]: http://fortawesome.github.io/Font-Awesome/
[material]: http://zavoloklom.github.io/material-design-iconic-font/
[octicons]: https://octicons.github.com/
[weather]: https://erikflowers.github.io/weather-icons/
[icongen]: http://icons.deanishe.net
16 changes: 16 additions & 0 deletions alfred_env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash

# When sourced, creates an Alfred-like environment needed by modd
# and ./bin/build (which sources the file itself)

# getvar <name> | Read a value from info.plist
getvar() {
local v="$1"
/usr/libexec/PlistBuddy -c "Print :$v" info.plist
}

export alfred_workflow_bundleid=$( getvar "bundleid" )
export alfred_workflow_version=$( getvar "version" )
export alfred_workflow_name=$( getvar "name" )
export SCHEDULE_DAYS=$( getvar "variables:SCHEDULE_DAYS" )
export EVENT_CACHE_MINS=$( getvar "variables:EVENT_CACHE_MINS" )
175 changes: 175 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//
// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>
//
// MIT Licence. See http://opensource.org/licenses/MIT
//
// Created on 2017-11-25
//

package main

import (
"crypto/rand"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"

"golang.org/x/net/context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/calendar/v3"
)

const (
authServerURL = "localhost:61432"
)

type response struct {
code string
err error
}

// Authenticator creates an authenticated Google API client
type Authenticator struct {
Secret []byte
TokenFile string
state string
client *http.Client
}

// NewAuthenticator creates a new Authenticator
func NewAuthenticator(tokenFile string, secret []byte) *Authenticator {
return &Authenticator{Secret: secret, TokenFile: tokenFile}
}

// GetClient returns an authenticated Google API client
func (a *Authenticator) GetClient() (*http.Client, error) {
if a.client != nil {
return a.client, nil
}

// generate CSRF token
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return nil, fmt.Errorf("couldn't read random bytes: %v", err)
}
a.state = fmt.Sprintf("%x", b)

ctx := context.Background()
cfg, err := google.ConfigFromJSON(a.Secret, calendar.CalendarReadonlyScope)
if err != nil {
return nil, fmt.Errorf("couldn't load config: %v", err)
}

tok, err := a.tokenFromFile()
if err != nil {
tok, err = a.tokenFromWeb(cfg)
if err != nil {
return nil, fmt.Errorf("couldn't get token from web: %v", err)
}
a.saveToken(tok)
}

a.client = cfg.Client(ctx, tok)
return a.client, nil
}

// tokenFromFile loads the oauth2 token from a file
func (a *Authenticator) tokenFromFile() (*oauth2.Token, error) {
f, err := os.Open(a.TokenFile)
if err != nil {
return nil, fmt.Errorf("couldn't open token file: %v", err)
}
tok := &oauth2.Token{}
err = json.NewDecoder(f).Decode(tok)
defer f.Close()
return tok, err
}

// saveToken saves an oauth2 token to a file
func (a *Authenticator) saveToken(tok *oauth2.Token) error {
f, err := os.OpenFile(a.TokenFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("couldn't open token file: %v", err)
}
defer f.Close()
return json.NewEncoder(f).Encode(tok)
}

// tokenFromWeb initiates web-based authentication and retrieves the oauth2 token
func (a *Authenticator) tokenFromWeb(cfg *oauth2.Config) (*oauth2.Token, error) {
if err := a.openAuthURL(cfg); err != nil {
return nil, fmt.Errorf("couldn't open auth URL: %v", err)
}

code, err := a.codeFromLocalServer()
if err != nil {
return nil, fmt.Errorf("couldn't get token from local server: %v", err)
}

tok, err := cfg.Exchange(oauth2.NoContext, code)
if err != nil {
return nil, fmt.Errorf("couldn't retrieve token from web: %v", err)
}
return tok, nil
}

// openAuthURL opens the Google API authentication URL in the default browser
func (a *Authenticator) openAuthURL(cfg *oauth2.Config) error {
authURL := cfg.AuthCodeURL(a.state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
cmd := exec.Command("/usr/bin/open", authURL)
if err := cmd.Run(); err != nil {
return fmt.Errorf("couldn't open auth URL: %v", err)
}
return nil
}

// codeFromLocalServer starts a local webserver to receive the oauth2 token
// from Google
func (a *Authenticator) codeFromLocalServer() (string, error) {
c := make(chan response)
srv := &http.Server{Addr: authServerURL}

go func() {
log.Printf("local webserver started")
if err := srv.ListenAndServe(); err != nil {
c <- response{err: err}
}
}()

http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
vars := req.URL.Query()
code := vars.Get("code")
state := vars.Get("state")
log.Printf("oauth2 state=%v", state)
log.Printf("oauth2 code=%s", code)

// Verify state to prevent CSRF
if state != a.state {
c <- response{err: fmt.Errorf("state mismatch: expected=%s, got=%s", a.state, state)}
io.WriteString(w, "bad state\n")
return
}

c <- response{code: code}
io.WriteString(w, "ok\n")
})

r := <-c

// log.Printf("srv=%+v, response=%+v", srv, r)
if err := srv.Shutdown(context.Background()); err != nil {
log.Printf("shutdown error: %v", err)
if err != http.ErrServerClosed {
return "", fmt.Errorf("local webserver error: %v", err)
}
}
log.Printf("local webserver stopped")

return r.code, r.err
}
Loading

0 comments on commit afc3b68

Please sign in to comment.