Eric Bower
·
2025-03-28
feed.go
1package shared
2
3import (
4 "fmt"
5 "sort"
6 "time"
7
8 "github.com/gorilla/feeds"
9 "github.com/picosh/pico/pkg/db"
10)
11
12func genUserFeedTmpl(title, msg string) string {
13 return fmt.Sprintf(`
14<html>
15 <head>
16 <title>%s</title>
17 <style>
18 code {
19 background-color: #ddd;
20 border-radius: 5px;
21 padding: 1px 3px;
22 }
23 </style>
24 </head>
25 <body>
26 %s
27 </body>
28</html>
29`, title, msg)
30}
31
32func PicoPlusFeed(expiration time.Time) string {
33 msg := fmt.Sprintf(`<h1>Thanks for joining <code>pico+</code>!</h1>
34<p>
35 You now have access to all our premium services until <strong>%s</strong>.
36</p>
37<p>
38 We will send you <code>pico+</code> expiration notifications through this RSS feed.
39 Go to <a href="https://pico.sh/getting-started#next-steps">pico.sh/getting-started#next-steps</a>
40 to start using our services.
41</p>
42<p>
43 If you have any questions, please do not hesitate to <a href="https://pico.sh/contact">contact us</a>.
44</p>`, expiration.Format(time.DateOnly))
45 return genUserFeedTmpl("pico+ activated", msg)
46}
47
48func PicoPlusExpirationFeed(expiration time.Time, txt string, plusLink string) string {
49 title := fmt.Sprintf("pico+ %s expiration notification!", txt)
50 msg := fmt.Sprintf(`<h1>%s</h1>
51<p>
52 Your <code>pico+</code> membership will expire on <strong>%s</strong>.
53</p>
54<p>
55 If your pico+ membership expires then we will:
56
57 <ul>
58 <li>revoke access to <a href="https">https://tuns.sh</a></li>
59 <li>reject new sites being created for <a href="https://pgs.sh">pgs.sh</a></li>
60 <li>revoke access to our IRC bouncer</li>
61 </ul>
62</p>
63<p>
64 In order to continue using our premium services, you need to purchase another year:
65 <a href="%s">purchase pico+</a>
66</p>
67<p>
68 If you have any questions, please do not hesitate to <a href="https://pico.sh/contact">contact us</a>.
69</p>`, title, expiration.Format(time.DateOnly), plusLink)
70 return genUserFeedTmpl(title, msg)
71}
72
73func UserFeed(me db.DB, user *db.User, token string) (*feeds.Feed, error) {
74 var err error
75 if token == "" {
76 token, err = me.UpsertToken(user.ID, "pico-rss")
77 if err != nil {
78 return nil, err
79 }
80 }
81
82 href := fmt.Sprintf("https://auth.pico.sh/rss/%s", token)
83
84 feed := &feeds.Feed{
85 Title: "pico+",
86 Link: &feeds.Link{Href: href},
87 Description: "get notified of important membership updates",
88 Author: &feeds.Author{Name: "team pico"},
89 }
90 var feedItems []*feeds.Item
91
92 now := time.Now()
93 ff, err := me.FindFeature(user.ID, "plus")
94 if err != nil {
95 // still want to send an empty feed
96 } else {
97 createdAt := ff.CreatedAt
98 createdAtStr := createdAt.Format(time.RFC3339)
99 id := fmt.Sprintf("pico-plus-activated-%d", createdAt.Unix())
100 content := PicoPlusFeed(*ff.ExpiresAt)
101 plus := &feeds.Item{
102 Id: id,
103 Title: fmt.Sprintf("pico+ membership activated on %s", createdAtStr),
104 Link: &feeds.Link{Href: "https://pico.sh"},
105 Content: content,
106 Created: *createdAt,
107 Updated: *createdAt,
108 Description: content,
109 Author: &feeds.Author{Name: "team pico"},
110 }
111 feedItems = append(feedItems, plus)
112
113 oneMonthWarning := ff.ExpiresAt.AddDate(0, -1, 0)
114 mo := genFeedItem(user.Name, now, *ff.ExpiresAt, oneMonthWarning, "1-month")
115 if mo != nil {
116 feedItems = append(feedItems, mo)
117 }
118
119 oneWeekWarning := ff.ExpiresAt.AddDate(0, 0, -7)
120 wk := genFeedItem(user.Name, now, *ff.ExpiresAt, oneWeekWarning, "1-week")
121 if wk != nil {
122 feedItems = append(feedItems, wk)
123 }
124
125 oneDayWarning := ff.ExpiresAt.AddDate(0, 0, -2)
126 day := genFeedItem(user.Name, now, *ff.ExpiresAt, oneDayWarning, "1-day")
127 if day != nil {
128 feedItems = append(feedItems, day)
129 }
130 }
131
132 tunsLogs, _ := me.FindTunsEventLogs(user.ID)
133 for _, eventLog := range tunsLogs {
134 content := fmt.Sprintf(`Created At: %s<br />
135Event type: %s<br />
136Connection type: %s<br />
137Remote addr: %s<br />
138Tunnel type: %s<br />
139Tunnel ID: %s<br />
140Server: %s`,
141 eventLog.CreatedAt.Format(time.RFC3339), eventLog.EventType, eventLog.ConnectionType,
142 eventLog.RemoteAddr, eventLog.TunnelType, eventLog.TunnelID, eventLog.ServerID,
143 )
144 logItem := &feeds.Item{
145 Id: fmt.Sprintf("%d", eventLog.CreatedAt.Unix()),
146 Title: fmt.Sprintf(
147 "%s tuns event for %s",
148 eventLog.EventType, eventLog.TunnelID,
149 ),
150 Link: &feeds.Link{Href: "https://pico.sh"},
151 Content: content,
152 Created: *eventLog.CreatedAt,
153 Updated: *eventLog.CreatedAt,
154 Description: content,
155 Author: &feeds.Author{Name: "team pico"},
156 }
157 feedItems = append(feedItems, logItem)
158 }
159
160 sort.Slice(feedItems, func(i, j int) bool {
161 return feedItems[i].Created.After(feedItems[j].Created)
162 })
163
164 feed.Items = feedItems
165 return feed, nil
166}
167
168func genFeedItem(userName string, now time.Time, expiresAt time.Time, warning time.Time, txt string) *feeds.Item {
169 if now.After(warning) {
170 content := PicoPlusExpirationFeed(
171 expiresAt,
172 txt,
173 "https://auth.pico.sh/checkout/"+userName,
174 )
175 return &feeds.Item{
176 Id: fmt.Sprintf("%d", warning.Unix()),
177 Title: fmt.Sprintf("pico+ %s expiration notice", txt),
178 Link: &feeds.Link{Href: "https://pico.sh"},
179 Content: content,
180 Created: warning,
181 Updated: warning,
182 Description: content,
183 Author: &feeds.Author{Name: "team pico"},
184 }
185 }
186
187 return nil
188}