Skip to content
This repository has been archived by the owner on Sep 3, 2024. It is now read-only.

Commit

Permalink
Add subscription management system
Browse files Browse the repository at this point in the history
  • Loading branch information
mlacorte committed Jun 4, 2022
1 parent ca62ade commit c749d4d
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 19 deletions.
52 changes: 33 additions & 19 deletions cmd/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ type publicTpl struct {
Description string
}

type unsubTpl struct {
type updateTpl struct {
publicTpl
SubUUID string
Email string
AM bool
PM bool
Sponsored bool
AllowBlocklist bool
AllowExport bool
AllowWipe bool
Expand Down Expand Up @@ -156,39 +160,49 @@ func handleViewCampaignMessage(c echo.Context) error {
// campaigns link to.
func handleSubscriptionPage(c echo.Context) error {
var (
app = c.Get("app").(*App)
campUUID = c.Param("campUUID")
subUUID = c.Param("subUUID")
unsub = c.Request().Method == http.MethodPost
blocklist, _ = strconv.ParseBool(c.FormValue("blocklist"))
out = unsubTpl{}
app = c.Get("app").(*App)
subUUID = c.Param("subUUID")
update = c.Request().Method == http.MethodPost
out = updateTpl{}
)
out.SubUUID = subUUID
out.Title = app.i18n.T("public.unsubscribeTitle")
out.Title = app.i18n.T("public.rtmSubscriptionTitle")
out.AllowBlocklist = app.constants.Privacy.AllowBlocklist
out.AllowExport = app.constants.Privacy.AllowExport
out.AllowWipe = app.constants.Privacy.AllowWipe

// Unsubscribe.
if unsub {
// Is blocklisting allowed?
if !app.constants.Privacy.AllowBlocklist {
blocklist = false
}
var sub models.RtmSubscriptions
if err := app.queries.GetRtmSubscriptions.Get(&sub, subUUID); err != nil {
app.log.Printf("GetRtmSubscriptions error: %v", err)
return c.Render(http.StatusInternalServerError, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "",
app.i18n.Ts("public.errorProcessingRequest")))
}

if _, err := app.queries.Unsubscribe.Exec(campUUID, subUUID, blocklist); err != nil {
app.log.Printf("error unsubscribing: %v", err)
out.Email = sub.Email
out.AM = sub.AM
out.PM = sub.PM
out.Sponsored = sub.Sponsored

// Update subscriptions.
if update {
am := c.FormValue("am") != ""
pm := c.FormValue("pm") != ""
sponsored := c.FormValue("sponsored") != ""

if _, err := app.queries.UpdateRtmSubscriptions.Exec(subUUID, am, pm, sponsored); err != nil {
app.log.Printf("UpdateRtmSubscriptions error: %v", err)
return c.Render(http.StatusInternalServerError, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "",
app.i18n.Ts("public.errorProcessingRequest")))
}

return c.Render(http.StatusOK, tplMessage,
makeMsgTpl(app.i18n.T("public.unsubbedTitle"), "",
app.i18n.T("public.unsubbedInfo")))
makeMsgTpl(app.i18n.T("public.rtmSubscriptionSuccess"), "",
app.i18n.T("public.rtmSubscriptionInfo")))
}

return c.Render(http.StatusOK, "subscription", out)
return c.Render(http.StatusOK, "rtm-subscription", out)
}

// handleOptinPage renders the double opt-in confirmation page that subscribers
Expand Down
4 changes: 4 additions & 0 deletions cmd/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ type Queries struct {
Unsubscribe *sqlx.Stmt `query:"unsubscribe"`
ExportSubscriberData *sqlx.Stmt `query:"export-subscriber-data"`

// RTM queries
GetRtmSubscriptions *sqlx.Stmt `query:"get-rtm-subscriptions"`
UpdateRtmSubscriptions *sqlx.Stmt `query:"update-rtm-subscriptions"`

// Non-prepared arbitrary subscriber queries.
QuerySubscribers string `query:"query-subscribers"`
QuerySubscribersCount string `query:"query-subscribers-count"`
Expand Down
5 changes: 5 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,11 @@
"public.unsubHelp": "Do you want to unsubscribe from this mailing list?",
"public.unsubTitle": "Unsubscribe",
"public.unsubbedInfo": "You have unsubscribed successfully.",
"public.rtmSubscription": "Update",
"public.rtmSubscriptionSuccess": "Success",
"public.rtmSubscriptionTitle": "Update Subscriptions",
"public.rtmSubscriptionHelp": "Select which newsletters you'd like to be subscribed to.",
"public.rtmSubscriptionInfo": "The changes you made to your subscriptions have been succesfully applied.",
"public.unsubbedTitle": "Unsubscribed",
"public.unsubscribeTitle": "Unsubscribe from mailing list",
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
Expand Down
8 changes: 8 additions & 0 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,14 @@ type Bounce struct {
Total int `db:"total" json:"-"`
}

// Data used for RTM subscription management
type RtmSubscriptions struct {
Email string `db:"email" json:"email"`
AM bool `db:"am" json:"am"`
PM bool `db:"pm" json:"pm"`
Sponsored bool `db:"sponsored" json:"sponsored"`
}

// markdown is a global instance of Markdown parser and renderer.
var markdown = goldmark.New(
goldmark.WithParserOptions(
Expand Down
126 changes: 126 additions & 0 deletions queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,132 @@ UPDATE subscriber_lists SET status = 'unsubscribed' WHERE
-- If $3 is false, unsubscribe from the campaign's lists, otherwise all lists.
CASE WHEN $3 IS FALSE THEN list_id = ANY(SELECT list_id FROM lists) ELSE list_id != 0 END;

-- name: get-rtm-subscriptions
-- Get subscriptions by user and tag
-- $1 = subscribers.uuid
WITH
subscriber AS (
SELECT id, email
FROM subscribers
WHERE uuid = $1
),
subscribed AS (
SELECT array_agg(list_id) AS lists
FROM subscriber_lists AS l, subscriber AS s
WHERE l.subscriber_id = s.id
AND l.status != 'unsubscribed'
),
am AS (
SELECT array_agg(id) AS lists
FROM lists
WHERE 'am' = ANY (tags)
),
pm AS (
SELECT array_agg(id) AS lists
FROM lists
WHERE 'pm' = ANY (tags)
),
sponsored AS (
SELECT array_agg(id) AS lists
FROM lists
WHERE 'sponsored' = ANY (tags)
)
SELECT
subscriber.email,
COALESCE(am.lists && subscribed.lists, false) AS am,
COALESCE(pm.lists && subscribed.lists, false) AS pm,
COALESCE(sponsored.lists && subscribed.lists, false) AS sponsored
FROM subscriber, am, pm, sponsored, subscribed;

-- name: update-rtm-subscriptions
-- Subscribe AM, PM, Sponsored subscriptions by user and tag
-- $1 = subscribers.uuid
-- $2 = bool (am)
-- $3 = bool (pm)
-- $4 = bool (sponsored)
WITH
subscriber AS (
SELECT id
FROM subscribers
WHERE uuid = $1
),
amSubscribe AS (
INSERT INTO subscriber_lists (subscriber_id, list_id, status, updated_at)
SELECT subscriber.id, am.primary, 'unconfirmed', now()
FROM subscriber, (
SELECT id AS primary
FROM lists
WHERE 'am' = ANY (tags)
AND 'primary' = ANY (tags)
) AS am
WHERE true = $2
ON CONFLICT (subscriber_id, list_id)
DO UPDATE SET status = 'unconfirmed', updated_at = now()
),
pmSubscribe AS (
INSERT INTO subscriber_lists (subscriber_id, list_id, status, updated_at)
SELECT subscriber.id, pm.primary, 'unconfirmed', now()
FROM subscriber, (
SELECT id AS primary
FROM lists
WHERE 'pm' = ANY (tags)
AND 'primary' = ANY (tags)
) AS pm
WHERE true = $3
ON CONFLICT (subscriber_id, list_id)
DO UPDATE SET status = 'unconfirmed', updated_at = now()
),
sponsoredSubscribe AS (
INSERT INTO subscriber_lists (subscriber_id, list_id, status, updated_at)
SELECT subscriber.id, sponsored.primary, 'unconfirmed', now()
FROM subscriber, (
SELECT id AS primary
FROM lists
WHERE 'sponsored' = ANY (tags)
AND 'primary' = ANY (tags)
) AS sponsored
WHERE true = $4
ON CONFLICT (subscriber_id, list_id)
DO UPDATE SET status = 'unconfirmed', updated_at = now()
),
amUnsubscribe AS (
UPDATE subscriber_lists SET
status = 'unsubscribed', updated_at = now()
FROM subscriber, (
SELECT id
FROM lists
WHERE 'am' = ANY (tags)
) AS am
WHERE false = $2
AND subscriber_id = subscriber.id
AND list_id = am.id
),
pmUnsubscribe AS (
UPDATE subscriber_lists SET
status = 'unsubscribed', updated_at = now()
FROM subscriber, (
SELECT id
FROM lists
WHERE 'pm' = ANY (tags)
) AS pm
WHERE false = $3
AND subscriber_id = subscriber.id
AND list_id = pm.id
),
sponsoredUnsubscribe AS (
UPDATE subscriber_lists SET
status = 'unsubscribed', updated_at = now()
FROM subscriber, (
SELECT id
FROM lists
WHERE 'sponsored' = ANY (tags)
) AS sponsored
WHERE false = $4
AND subscriber_id = subscriber.id
AND list_id = sponsored.id
)
SELECT true;

-- privacy
-- name: export-subscriber-data
WITH prof AS (
Expand Down
70 changes: 70 additions & 0 deletions static/public/templates/rtm-subscription.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{{ define "rtm-subscription" }}
{{ template "header" .}}
<section class="section">
<h2>{{ L.T "public.rtmSubscriptionTitle" }}</h2>
<p>{{ L.T "public.rtmSubscriptionHelp" }}</p>
<form method="post">
<div>
<p>
<input id="am-newsletter" type="checkbox" name="am" value="true" {{ if .Data.AM }}checked{{ end }}/>
<label for="am-newsletter">Resist the Mainstream AM</label>
</p>
<p>
<input id="pm-newsletter" type="checkbox" name="pm" value="true" {{ if .Data.PM }}checked{{ end }} />
<label for="pm-newsletter">Resist the Mainstream PM</label>
</p>
<p>
<input id="sponsored-newsletter" type="checkbox" name="sponsored" value="true" {{ if .Data.Sponsored }}checked{{ end }} />
<label for="sponsored-newsletter">Resist the Mainstream Sponsored</label>
</p>
<p>
<button type="submit" class="button" id="btn-unsub">{{ L.T "public.rtmSubscription" }}</button>
</p>
</div>
</form>
</section>

{{ if or .Data.AllowExport .Data.AllowWipe }}
<form id="data-form" method="post" action="" onsubmit="return handleData()">
<section>
<h2>{{ L.T "public.privacyTitle" }}</h2>
{{ if .Data.AllowExport }}
<div class="row">
<input id="privacy-export" type="radio" name="data-action" value="export" required />
<label for="privacy-export"><strong>{{ L.T "public.privacyExport" }}</strong></label>
<br />
{{ L.T "public.privacyExportHelp" }}
</div>
{{ end }}

{{ if .Data.AllowWipe }}
<div class="row">
<input id="privacy-wipe" type="radio" name="data-action" value="wipe" required />
<label for="privacy-wipe"><strong>{{ L.T "public.privacyWipe" }}</strong></label>
<br />
{{ L.T "public.privacyWipeHelp" }}
</div>
{{ end }}
<p>
<input type="submit" value="{{ L.T "globals.buttons.continue" }}" class="button button-outline" />
</p>
</section>
</form>
<script>
function handleData() {
var a = document.querySelector('input[name="data-action"]:checked').value,
f = document.querySelector("#data-form");
if (a == "export") {
f.action = "/subscription/export/{{ .Data.SubUUID }}";
return true;
} else if (confirm("{{ L.T "public.privacyConfirmWipe" }}")) {
f.action = "/subscription/wipe/{{ .Data.SubUUID }}";
return true;
}
return false;
}
</script>
{{ end }}

{{ template "footer" .}}
{{ end }}

0 comments on commit c749d4d

Please sign in to comment.