├── README.md ├── .gitignore ├── LICENSE └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # example-go-ical 2 | 3 | Example of creating an ical endpoint based on data via REST using Go 4 | 5 | ```bash 6 | go run main.go 7 | ``` 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 mario 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "github.com/jordic/goics" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | const feedPrefix = "/feed/" 18 | const expirationTime = 5 * time.Minute 19 | 20 | // Feed is an iCal feed 21 | type Feed struct { 22 | Content string 23 | ExpiresAt time.Time 24 | } 25 | 26 | // Entry is a time entry 27 | type Entry struct { 28 | DateStart time.Time `json:"dateStart"` 29 | DateEnd time.Time `json:"dateEnd"` 30 | Description string `json:"description"` 31 | } 32 | 33 | // Entries is a collection of entries 34 | type Entries []*Entry 35 | 36 | func main() { 37 | cache := make(map[string]*Feed) 38 | 39 | mux := http.NewServeMux() 40 | mux.HandleFunc("/feedURL", feedURL(cache)) 41 | mux.HandleFunc(feedPrefix, feed(cache)) 42 | 43 | log.Print("Server started on localhost:8080") 44 | log.Fatal(http.ListenAndServe(":8080", mux)) 45 | } 46 | 47 | func feedURL(cache map[string]*Feed) http.HandlerFunc { 48 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 | token := randomToken(20) 50 | _, err := createFeedForToken(token, cache) 51 | if err != nil { 52 | writeError(http.StatusInternalServerError, "Could not create feed", w, err) 53 | return 54 | } 55 | writeSuccess(fmt.Sprintf("FeedToken: %s", token), w) 56 | }) 57 | } 58 | 59 | func feed(cache map[string]*Feed) http.HandlerFunc { 60 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 61 | w.Header().Set("Content-type", "text/calendar") 62 | w.Header().Set("charset", "utf-8") 63 | w.Header().Set("Content-Disposition", "inline") 64 | w.Header().Set("filename", "calendar.ics") 65 | 66 | var result string 67 | token := parseToken(r.URL.Path) 68 | log.Print("Fetching iCal feed for Token: " + token) 69 | feed, ok := cache[token] 70 | if !ok || feed == nil { 71 | writeError(http.StatusNotFound, "No Feed for this Token", w, errors.New("No Feed for this Token")) 72 | return 73 | } 74 | 75 | result = feed.Content 76 | if feed.ExpiresAt.Before(time.Now()) { 77 | newFeed, err := createFeedForToken(token, cache) 78 | if err != nil { 79 | writeError(http.StatusInternalServerError, "Could not create feed", w, err) 80 | return 81 | } 82 | result = newFeed.Content 83 | } 84 | 85 | writeSuccess(result, w) 86 | }) 87 | } 88 | 89 | func createFeedForToken(token string, cache map[string]*Feed) (*Feed, error) { 90 | res, err := fetchData() 91 | if err != nil { 92 | return nil, errors.New("Could not fetch data") 93 | } 94 | b := bytes.Buffer{} 95 | goics.NewICalEncode(&b).Encode(res) 96 | feed := &Feed{Content: b.String(), ExpiresAt: time.Now().Add(expirationTime)} 97 | cache[token] = feed 98 | return feed, nil 99 | } 100 | 101 | func fetchData() (Entries, error) { 102 | url := "http://www.mocky.io/v2/5a88375b3000007e007f9401" 103 | resp, err := http.Get(url) 104 | if err != nil { 105 | return nil, errors.New("could not fetch data") 106 | } 107 | defer resp.Body.Close() 108 | if resp.StatusCode != 200 { 109 | return nil, fmt.Errorf("%s: %s", "could not fetch data", resp.Status) 110 | } 111 | b, err := ioutil.ReadAll(resp.Body) 112 | if err != nil { 113 | return nil, errors.New("could not read data") 114 | } 115 | result := Entries{} 116 | err = json.Unmarshal(b, &result) 117 | if err != nil { 118 | return nil, errors.New("could not unmarshal data") 119 | } 120 | return result, nil 121 | 122 | } 123 | 124 | // EmitICal implements the interface for goics 125 | func (e Entries) EmitICal() goics.Componenter { 126 | c := goics.NewComponent() 127 | c.SetType("VCALENDAR") 128 | c.AddProperty("CALSCAL", "GREGORIAN") 129 | for _, entry := range e { 130 | s := goics.NewComponent() 131 | s.SetType("VEVENT") 132 | k, v := goics.FormatDateTimeField("DTEND", entry.DateEnd) 133 | s.AddProperty(k, v) 134 | k, v = goics.FormatDateTimeField("DTSTART", entry.DateStart) 135 | s.AddProperty(k, v) 136 | s.AddProperty("SUMMARY", entry.Description) 137 | 138 | c.AddComponent(s) 139 | } 140 | return c 141 | } 142 | 143 | func parseToken(path string) string { 144 | return strings.TrimPrefix(path, feedPrefix) 145 | } 146 | 147 | func randomToken(len int) string { 148 | b := make([]byte, len) 149 | rand.Read(b) 150 | return fmt.Sprintf("%x", b) 151 | } 152 | 153 | func writeError(status int, message string, w http.ResponseWriter, err error) { 154 | log.Print("ERROR: ", err.Error()) 155 | w.WriteHeader(status) 156 | w.Write([]byte(message)) 157 | } 158 | 159 | func writeSuccess(message string, w http.ResponseWriter) { 160 | w.WriteHeader(http.StatusOK) 161 | w.Write([]byte(message)) 162 | } 163 | --------------------------------------------------------------------------------