Skip to content

Commit faf924e

Browse files
committed
feat: Add RSS/Atom feed for Luma events via our proxy
Signed-off-by: Felicitas Pojtinger <[email protected]>
1 parent 913bf7c commit faf924e

File tree

9 files changed

+167
-21
lines changed

9 files changed

+167
-21
lines changed

data/links.yaml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@ sidebar:
3030
url: https://thecanadian.social/@vanlug
3131
icon: fa-brands fa-mastodon
3232
description: Follow us on the fediverse
33-
- title: RSS
33+
- title: Posts RSS
3434
url: https://thecanadian.social/@vanlug.rss
3535
icon: fa-solid fa-rss
36-
description: Subscribe to our posts
36+
description: Subscribe to our Mastodon posts
37+
- title: Events RSS
38+
icon: fa-solid fa-rss
39+
description: Subscribe to upcoming events
40+
eventsFeed: true
3741
- title: Bluesky
3842
url: https://bsky.app/profile/vanlug.thecanadian.social.ap.brid.gy
3943
icon: fa-brands fa-bluesky
@@ -70,10 +74,10 @@ donate:
7074
url: https://www.paypal.com/ncp/payment/5LJ46YPC7JJ7E
7175
icon: fa-brands fa-paypal
7276
feed:
73-
rss: https://thecanadian.social/@vanlug.rss
77+
mastodon: https://thecanadian.social/@vanlug.rss
7478
luma:
7579
calendar: cal-x23GENOxTsZqSMO
7680
url: https://luma.com/vanlug
77-
proxy: http://localhost:8080
81+
proxy: https://vanlug-website-backend.up.railway.app/
7882
meetup:
7983
url: https://www.meetup.com/vanlug-bc/events/

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ require (
77
github.com/pojntfx/felicitas.pojtinger.com v0.4.1
88
)
99

10+
require github.com/gorilla/feeds v1.2.0
11+
1012
require (
1113
github.com/gorilla/websocket v1.5.3 // indirect
1214
github.com/tomnomnom/linkheader v0.0.0-20250811210735-e5fe3b51442e // indirect

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
2+
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
13
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
24
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
5+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
6+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
7+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
8+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
39
github.com/mattn/go-mastodon v0.0.11 h1:Zcvc/8EHpf3os1mwAuUUB5es5VnfVdAeb4ed6ByJnCY=
410
github.com/mattn/go-mastodon v0.0.11/go.mod h1:0DcwYEkqigrvknMvjmfKXLP0vYyeYm+vBdUOvoHcczg=
511
github.com/pojntfx/felicitas.pojtinger.com v0.4.1 h1:bTUaI/b8csLNWjsEBqb2d3Xao6njUst5XPOREuOfkiM=
612
github.com/pojntfx/felicitas.pojtinger.com v0.4.1/go.mod h1:XaKmAAyinrHKMlRAXF7/rhBeENkC6e/Zf2SrhIGMgbo=
13+
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
14+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
715
github.com/tomnomnom/linkheader v0.0.0-20250811210735-e5fe3b51442e h1:tD38/4xg4nuQCASJ/JxcvCHNb46w0cdAaJfkzQOO1bA=
816
github.com/tomnomnom/linkheader v0.0.0-20250811210735-e5fe3b51442e/go.mod h1:krvJ5AY/MjdPkTeRgMYbIDhbbbVvnPQPzsIsDJO8xrY=

layouts/_partials/section-events.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ <h3>Upcoming</h3>
2727
target="_blank"
2828
rel="noopener"
2929
>Luma calendar<icon-external></icon-external></a
30-
>. RSVP there to get reminders.
30+
>. RSVP there to get reminders, or subscribe via
31+
<a
32+
href="{{ .Site.Data.links.luma.proxy }}/events/feed?calendar={{ .Site.Data.links.luma.calendar }}"
33+
target="_blank"
34+
rel="noopener"
35+
>RSS<icon-custom data-icon="fa-solid fa-rss"></icon-custom></a
36+
>.
3137
</p>
3238
</div>
3339
<luma-upcoming-events

layouts/_partials/section-home.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,14 @@ <h3>{{ $c.next_meeting.title }}</h3>
9797
<div class="pf-v6-c-content">
9898
<p>
9999
{{ $c.next_meeting.body }}
100-
<a href="{{ "/events/" | relURL }}">Full schedule</a>
100+
See
101+
<a href="{{ "/events/" | relURL }}">full schedule</a>, or subscribe via
102+
<a
103+
href="{{ .Site.Data.links.luma.proxy }}/events/feed?calendar={{ .Site.Data.links.luma.calendar }}"
104+
target="_blank"
105+
rel="noopener"
106+
>RSS<icon-custom data-icon="fa-solid fa-rss"></icon-custom></a
107+
>.
101108
</p>
102109
</div>
103110
</div>

layouts/_partials/sidebar.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,21 @@
66
{{ range .Site.Data.links.sidebar }}
77
{{ if .separator }}
88
<li class="pf-v6-c-divider" role="presentation"></li>
9+
{{ else if .eventsFeed }}
10+
<li class="pf-v6-c-nav__item">
11+
<a
12+
class="pf-v6-c-nav__link"
13+
href="{{ $.Site.Data.links.luma.proxy }}/events/feed?calendar={{ $.Site.Data.links.luma.calendar }}"
14+
target="_blank"
15+
rel="noopener"
16+
><span
17+
>{{ .title }}{{ partial "icon" .icon }}<br /><small
18+
class="pf-v6-u-font-size-xs pf-v6-u-text-color-subtle"
19+
>{{ .description }}</small
20+
></span
21+
></a
22+
>
23+
</li>
924
{{ else if .url }}
1025
<li class="pf-v6-c-nav__item">
1126
<a

layouts/baseof.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
{{ range .Site.Data.links.me }}
3535
<link rel="me" href="{{ . }}" />
3636
{{ end }}
37-
<link rel="alternate" type="application/rss+xml" title="Fediverse posts from {{ .Site.Title }}" href="{{ .Site.Data.links.feed.rss }}" />
37+
<link rel="alternate" type="application/rss+xml" title="Mastodon posts from {{ .Site.Title }}" href="{{ .Site.Data.links.feed.mastodon }}" />
38+
<link rel="alternate" type="application/atom+xml" title="Upcoming events from {{ .Site.Title }}" href="{{ .Site.Data.links.luma.proxy }}/events/feed?calendar={{ .Site.Data.links.luma.calendar }}" />
3839

3940
<meta name="author" content="{{ .Site.Data.site.author }}" />
4041
<meta name="fediverse:creator" content="@[email protected]" />
@@ -54,7 +55,7 @@
5455
content="{{ .Site.Data.site.description }}"
5556
/>
5657
<meta property="og:image" content="/design/header.png" />
57-
<meta property="og:url" content="https://vanlug.github.io/" />
58+
<meta property="og:url" content="{{ .Site.BaseURL }}" />
5859
<meta property="og:type" content="website" />
5960
<meta property="og:site_name" content="{{ .Site.Data.site.author }}" />
6061
<meta property="og:locale" content="en_CA" />

main.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ func main() {
3232
apiBase = defaultLumaAPIBase
3333
}
3434

35+
siteURL := os.Getenv("SITE_URL")
36+
if siteURL == "" {
37+
siteURL = "https://vanlug.github.io/"
38+
}
39+
3540
mastodonServer := os.Getenv("MASTODON_SERVER")
3641
if mastodonServer == "" {
3742
mastodonServer = "https://thecanadian.social"
@@ -87,6 +92,20 @@ func main() {
8792
handlers.EventsHandler(w, r, apiBase)
8893
}))
8994

95+
mux.HandleFunc("/events/feed", cors(func(w http.ResponseWriter, r *http.Request) {
96+
defer func() {
97+
if err := recover(); err != nil {
98+
log.Println("Error occured in events feed API:", err)
99+
100+
http.Error(w, "Error occured in events feed API", http.StatusInternalServerError)
101+
102+
return
103+
}
104+
}()
105+
106+
handlers.EventsFeedHandler(w, r, apiBase, siteURL)
107+
}))
108+
90109
mux.HandleFunc("/mastodon", cors(func(w http.ResponseWriter, r *http.Request) {
91110
defer func() {
92111
if err := recover(); err != nil {

pkg/handlers/luma.go

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ package handlers
33
import (
44
"encoding/json"
55
"fmt"
6+
"html"
67
"io"
78
"net/http"
89
"net/url"
10+
"time"
11+
12+
"github.com/gorilla/feeds"
913
)
1014

1115
type LumaEvent struct {
@@ -39,12 +43,10 @@ type EventOutput struct {
3943
Entries []EventItem `json:"entries"`
4044
}
4145

42-
func lumaFetch(w http.ResponseWriter, r *http.Request, apiBase string, limit string) {
46+
func lumaFetchItems(r *http.Request, apiBase string, limit string) ([]EventItem, error) {
4347
cal := r.URL.Query().Get("calendar")
4448
if cal == "" {
45-
w.Write([]byte("missing calendar query parameter"))
46-
47-
panic("missing calendar query parameter")
49+
return nil, fmt.Errorf("missing calendar query parameter")
4850
}
4951

5052
u := apiBase + "?" + url.Values{
@@ -55,22 +57,21 @@ func lumaFetch(w http.ResponseWriter, r *http.Request, apiBase string, limit str
5557

5658
resp, err := http.Get(u)
5759
if err != nil {
58-
panic(err)
60+
return nil, err
5961
}
6062
defer resp.Body.Close()
6163

6264
body, err := io.ReadAll(resp.Body)
6365
if err != nil {
64-
panic(err)
66+
return nil, err
6567
}
6668

6769
var lr LumaResponse
6870
if err := json.Unmarshal(body, &lr); err != nil {
69-
panic(err)
71+
return nil, err
7072
}
7173

72-
output := EventOutput{}
73-
74+
var items []EventItem
7475
for _, entry := range lr.Entries {
7576
evt := entry.Event
7677

@@ -82,7 +83,7 @@ func lumaFetch(w http.ResponseWriter, r *http.Request, apiBase string, limit str
8283
}
8384
}
8485

85-
output.Entries = append(output.Entries, EventItem{
86+
items = append(items, EventItem{
8687
Name: evt.Name,
8788
StartAt: evt.StartAt,
8889
URL: evt.URL,
@@ -91,7 +92,18 @@ func lumaFetch(w http.ResponseWriter, r *http.Request, apiBase string, limit str
9192
})
9293
}
9394

94-
j, err := json.Marshal(output)
95+
return items, nil
96+
}
97+
98+
func lumaJSON(w http.ResponseWriter, r *http.Request, apiBase string, limit string) {
99+
items, err := lumaFetchItems(r, apiBase, limit)
100+
if err != nil {
101+
w.Write([]byte(err.Error()))
102+
103+
panic(err)
104+
}
105+
106+
j, err := json.Marshal(EventOutput{Entries: items})
95107
if err != nil {
96108
panic(err)
97109
}
@@ -100,9 +112,81 @@ func lumaFetch(w http.ResponseWriter, r *http.Request, apiBase string, limit str
100112
}
101113

102114
func NextEventHandler(w http.ResponseWriter, r *http.Request, apiBase string) {
103-
lumaFetch(w, r, apiBase, "1")
115+
lumaJSON(w, r, apiBase, "1")
104116
}
105117

106118
func EventsHandler(w http.ResponseWriter, r *http.Request, apiBase string) {
107-
lumaFetch(w, r, apiBase, "20")
119+
lumaJSON(w, r, apiBase, "20")
120+
}
121+
122+
func EventsFeedHandler(w http.ResponseWriter, r *http.Request, apiBase string, siteURL string) {
123+
items, err := lumaFetchItems(r, apiBase, "20")
124+
if err != nil {
125+
w.Write([]byte(err.Error()))
126+
127+
panic(err)
128+
}
129+
130+
now := time.Now()
131+
132+
feed := &feeds.Feed{
133+
Title: "VanLUG Events",
134+
Link: &feeds.Link{Href: siteURL + "events/"},
135+
Description: "Upcoming events from the Vancouver Linux Users Group",
136+
Author: &feeds.Author{Name: "Vancouver Linux Users Group"},
137+
Updated: now,
138+
Created: now,
139+
Id: siteURL + "events/",
140+
}
141+
142+
for _, item := range items {
143+
link := "https://luma.com/vanlug"
144+
if item.URL != "" {
145+
link = "https://luma.com/" + item.URL
146+
}
147+
148+
published := now
149+
if t, err := time.Parse(time.RFC3339, item.StartAt); err == nil {
150+
published = t
151+
}
152+
153+
description := item.StartAt
154+
if item.Location != "" {
155+
description = item.Location + " — " + description
156+
}
157+
158+
content := "<p>" + html.EscapeString(item.Name) + "</p>"
159+
if item.Location != "" {
160+
content += "<p>" + html.EscapeString(item.Location) + "</p>"
161+
}
162+
if item.CoverURL != "" {
163+
content += `<p><img src="` + html.EscapeString(item.CoverURL) + `" alt="` + html.EscapeString(item.Name) + `" /></p>`
164+
}
165+
166+
feedItem := &feeds.Item{
167+
Title: item.Name,
168+
Link: &feeds.Link{Href: link},
169+
Description: description,
170+
Id: link,
171+
Created: published,
172+
Updated: published,
173+
Content: content,
174+
}
175+
176+
if item.CoverURL != "" {
177+
feedItem.Enclosure = &feeds.Enclosure{
178+
Url: item.CoverURL,
179+
Type: "image/jpeg",
180+
}
181+
}
182+
183+
feed.Add(feedItem)
184+
}
185+
186+
w.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")
187+
w.Header().Set("Cache-Control", "public, max-age=900")
188+
189+
if err := feed.WriteAtom(w); err != nil {
190+
panic(err)
191+
}
108192
}

0 commit comments

Comments
 (0)