repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

pico / pkg / shared
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}