Add subscription forms
This commit is contained in:
parent
b205761fb3
commit
c08ca14a5b
|
@ -0,0 +1,96 @@
|
||||||
|
import React from "react"
|
||||||
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Checkbox,
|
||||||
|
} from "antd"
|
||||||
|
|
||||||
|
import * as cs from "./constants"
|
||||||
|
|
||||||
|
class Forms extends React.PureComponent {
|
||||||
|
state = {
|
||||||
|
lists: [],
|
||||||
|
selected: []
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.pageTitle("Forms")
|
||||||
|
this.props
|
||||||
|
.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet, {
|
||||||
|
per_page: "all"
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.setState({ lists: this.props.data[cs.ModelLists].results })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelection(sel) {
|
||||||
|
let out = []
|
||||||
|
sel.forEach(s => {
|
||||||
|
const item = this.state.lists.find(l => {
|
||||||
|
return l.uuid === s
|
||||||
|
})
|
||||||
|
if (item) {
|
||||||
|
out.push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(out)
|
||||||
|
this.setState({ selected: out })
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<section className="content list-form">
|
||||||
|
<h1>Forms</h1>
|
||||||
|
<Row>
|
||||||
|
<Col span={8}>
|
||||||
|
<Checkbox.Group
|
||||||
|
className="lists"
|
||||||
|
options={this.state.lists.map(l => {
|
||||||
|
return { label: l.name, value: l.uuid }
|
||||||
|
})}
|
||||||
|
defaultValue={[]}
|
||||||
|
onChange={(sel) => this.handleSelection(sel)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
<h1>Form HTML</h1>
|
||||||
|
<p>Use the following HTML to show a subscription form on an external webpage.</p>
|
||||||
|
<p>
|
||||||
|
The form should have the <code><strong>email</strong></code> field and one or more{" "}
|
||||||
|
<code><strong>l</strong></code> (list UUID) fields. The <code><strong>name</strong></code> field is optional.
|
||||||
|
</p>
|
||||||
|
<pre className="html">
|
||||||
|
|
||||||
|
{`<form method="post" action="${window.CONFIG.rootURL}/subscription/form" class="listmonk-subscription">
|
||||||
|
<div>
|
||||||
|
<h3>Subscribe</h3>
|
||||||
|
<p><input type="text" name="email" value="" placeholder="E-mail" /></p>
|
||||||
|
<p><input type="text" name="name" value="" placeholder="Name (optional)" /></p>`}
|
||||||
|
{(() => {
|
||||||
|
let out = [];
|
||||||
|
this.state.selected.forEach(l => {
|
||||||
|
out.push(`
|
||||||
|
<p>
|
||||||
|
<input type="checkbox" name="l" value="${l.uuid}" id="${l.uuid.substr(0,5)}" />
|
||||||
|
<label for="${l.uuid.substr(0,5)}">${l.name}</label>
|
||||||
|
</p>`);
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
})()}
|
||||||
|
{`
|
||||||
|
<p><input type="submit" value="Subscribe" /></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`}
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Forms
|
|
@ -1,42 +1,43 @@
|
||||||
import React from "react";
|
import React from "react"
|
||||||
import { Switch, Route } from "react-router-dom";
|
import { Switch, Route } from "react-router-dom"
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom"
|
||||||
import { Layout, Menu, Icon } from "antd";
|
import { Layout, Menu, Icon } from "antd"
|
||||||
|
|
||||||
import logo from "./static/listmonk.svg";
|
import logo from "./static/listmonk.svg"
|
||||||
|
|
||||||
// Views.
|
// Views.
|
||||||
import Dashboard from "./Dashboard";
|
import Dashboard from "./Dashboard"
|
||||||
import Lists from "./Lists";
|
import Lists from "./Lists"
|
||||||
import Subscribers from "./Subscribers";
|
import Forms from "./Forms"
|
||||||
import Subscriber from "./Subscriber";
|
import Subscribers from "./Subscribers"
|
||||||
import Templates from "./Templates";
|
import Subscriber from "./Subscriber"
|
||||||
import Import from "./Import";
|
import Templates from "./Templates"
|
||||||
import Campaigns from "./Campaigns";
|
import Import from "./Import"
|
||||||
import Campaign from "./Campaign";
|
import Campaigns from "./Campaigns"
|
||||||
import Media from "./Media";
|
import Campaign from "./Campaign"
|
||||||
|
import Media from "./Media"
|
||||||
|
|
||||||
const { Content, Footer, Sider } = Layout;
|
const { Content, Footer, Sider } = Layout
|
||||||
const SubMenu = Menu.SubMenu;
|
const SubMenu = Menu.SubMenu
|
||||||
const year = new Date().getUTCFullYear();
|
const year = new Date().getUTCFullYear()
|
||||||
|
|
||||||
class Base extends React.Component {
|
class Base extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
basePath: "/" + window.location.pathname.split("/")[1],
|
basePath: "/" + window.location.pathname.split("/")[1],
|
||||||
error: null,
|
error: null,
|
||||||
collapsed: false
|
collapsed: false
|
||||||
};
|
}
|
||||||
|
|
||||||
onCollapse = collapsed => {
|
onCollapse = collapsed => {
|
||||||
this.setState({ collapsed });
|
this.setState({ collapsed })
|
||||||
};
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// For small screen devices collapse the menu by default.
|
// For small screen devices collapse the menu by default.
|
||||||
if (window.screen.width < 768) {
|
if (window.screen.width < 768) {
|
||||||
this.setState({ collapsed: true });
|
this.setState({ collapsed: true })
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
@ -65,12 +66,28 @@ class Base extends React.Component {
|
||||||
<span>Dashboard</span>
|
<span>Dashboard</span>
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="/lists">
|
<SubMenu
|
||||||
<Link to="/lists">
|
key="/lists"
|
||||||
<Icon type="bars" />
|
title={
|
||||||
<span>Lists</span>
|
<span>
|
||||||
</Link>
|
<Icon type="bars" />
|
||||||
</Menu.Item>
|
<span>Lists</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Menu.Item key="/lists">
|
||||||
|
<Link to="/lists">
|
||||||
|
<Icon type="bars" />
|
||||||
|
<span>All lists</span>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="/lists/forms">
|
||||||
|
<Link to="/lists/forms">
|
||||||
|
<Icon type="form" />
|
||||||
|
<span>Forms</span>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
</SubMenu>
|
||||||
<SubMenu
|
<SubMenu
|
||||||
key="/subscribers"
|
key="/subscribers"
|
||||||
title={
|
title={
|
||||||
|
@ -146,6 +163,14 @@ class Base extends React.Component {
|
||||||
<Lists {...{ ...this.props, route: props }} />
|
<Lists {...{ ...this.props, route: props }} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
key="/lists/forms"
|
||||||
|
path="/lists/forms"
|
||||||
|
render={props => (
|
||||||
|
<Forms {...{ ...this.props, route: props }} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
key="/subscribers"
|
key="/subscribers"
|
||||||
|
@ -230,8 +255,8 @@ class Base extends React.Component {
|
||||||
>
|
>
|
||||||
listmonk
|
listmonk
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
© 2019 {year !== 2019 ? " - " + year : ""}.
|
© 2019 {year !== 2019 ? " - " + year : ""}. Version{" "}
|
||||||
Version { process.env.REACT_APP_VERSION } —{" "}
|
{process.env.REACT_APP_VERSION} —{" "}
|
||||||
<a
|
<a
|
||||||
href="https://listmonk.app/docs"
|
href="https://listmonk.app/docs"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -243,8 +268,8 @@ class Base extends React.Component {
|
||||||
</Footer>
|
</Footer>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Base;
|
export default Base
|
||||||
|
|
|
@ -93,7 +93,6 @@ class CreateFormDef extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
modalTitle(formType, record) {
|
modalTitle(formType, record) {
|
||||||
console.log(formType)
|
|
||||||
if (formType === cs.FormCreate) {
|
if (formType === cs.FormCreate) {
|
||||||
return "Create a list"
|
return "Create a list"
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,13 @@ hr {
|
||||||
padding: 30px !important;
|
padding: 30px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.no {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
ul.no li {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Layout */
|
/* Layout */
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -94,6 +101,16 @@ section.content {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form */
|
/* Form */
|
||||||
|
.list-form .html {
|
||||||
|
background: #fafafa;
|
||||||
|
padding: 30px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 600px;
|
||||||
|
}
|
||||||
|
.list-form .lists label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Table actions */
|
/* Table actions */
|
||||||
|
|
|
@ -95,6 +95,7 @@ func registerHandlers(e *echo.Echo) {
|
||||||
e.DELETE("/api/templates/:id", handleDeleteTemplate)
|
e.DELETE("/api/templates/:id", handleDeleteTemplate)
|
||||||
|
|
||||||
// Subscriber facing views.
|
// Subscriber facing views.
|
||||||
|
e.POST("/subscription/form", handleSubscriptionForm)
|
||||||
e.GET("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPage),
|
e.GET("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPage),
|
||||||
"campUUID", "subUUID"))
|
"campUUID", "subUUID"))
|
||||||
e.POST("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPage),
|
e.POST("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPage),
|
||||||
|
|
48
public.go
48
public.go
|
@ -8,9 +8,11 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/knadh/listmonk/messenger"
|
"github.com/knadh/listmonk/messenger"
|
||||||
"github.com/knadh/listmonk/models"
|
"github.com/knadh/listmonk/models"
|
||||||
|
"github.com/knadh/listmonk/subimporter"
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
@ -58,6 +60,11 @@ type msgTpl struct {
|
||||||
Message string
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type subForm struct {
|
||||||
|
subimporter.SubReq
|
||||||
|
SubListUUIDs []string `form:"l"`
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
pixelPNG = drawTransparentImage(3, 14)
|
pixelPNG = drawTransparentImage(3, 14)
|
||||||
)
|
)
|
||||||
|
@ -169,6 +176,47 @@ func handleOptinPage(c echo.Context) error {
|
||||||
return c.Render(http.StatusOK, "optin", out)
|
return c.Render(http.StatusOK, "optin", out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleOptinPage handles a double opt-in confirmation from subscribers.
|
||||||
|
func handleSubscriptionForm(c echo.Context) error {
|
||||||
|
var (
|
||||||
|
app = c.Get("app").(*App)
|
||||||
|
req subForm
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get and validate fields.
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.SubListUUIDs) == 0 {
|
||||||
|
return c.Render(http.StatusInternalServerError, "message",
|
||||||
|
makeMsgTpl("Error", "",
|
||||||
|
`No lists to subscribe to.`))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's no name, use the name bit from the e-mail.
|
||||||
|
req.Email = strings.ToLower(req.Email)
|
||||||
|
if req.Name == "" {
|
||||||
|
req.Name = strings.Split(req.Email, "@")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate fields.
|
||||||
|
if err := subimporter.ValidateFields(req.SubReq); err != nil {
|
||||||
|
return c.Render(http.StatusInternalServerError, "message",
|
||||||
|
makeMsgTpl("Error", "", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the subscriber into the DB.
|
||||||
|
req.Status = models.SubscriberStatusEnabled
|
||||||
|
req.ListUUIDs = pq.StringArray(req.SubListUUIDs)
|
||||||
|
if _, err := insertSubscriber(req.SubReq, app); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render(http.StatusInternalServerError, "message",
|
||||||
|
makeMsgTpl("Done", "", `Subscribed successfully.`))
|
||||||
|
}
|
||||||
|
|
||||||
// handleLinkRedirect handles link UUID to real link redirection.
|
// handleLinkRedirect handles link UUID to real link redirection.
|
||||||
func handleLinkRedirect(c echo.Context) error {
|
func handleLinkRedirect(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -54,11 +54,16 @@ WITH sub AS (
|
||||||
VALUES($1, $2, $3, $4, $5)
|
VALUES($1, $2, $3, $4, $5)
|
||||||
returning id
|
returning id
|
||||||
),
|
),
|
||||||
|
listIDs AS (
|
||||||
|
SELECT id FROM lists WHERE
|
||||||
|
(CASE WHEN ARRAY_LENGTH($6::INT[], 1) > 0 THEN id=ANY($6)
|
||||||
|
ELSE uuid=ANY($7::UUID[]) END)
|
||||||
|
),
|
||||||
subs AS (
|
subs AS (
|
||||||
INSERT INTO subscriber_lists (subscriber_id, list_id, status)
|
INSERT INTO subscriber_lists (subscriber_id, list_id, status)
|
||||||
VALUES(
|
VALUES(
|
||||||
(SELECT id FROM sub),
|
(SELECT id FROM sub),
|
||||||
UNNEST($6::INT[]),
|
UNNEST(ARRAY(SELECT id FROM listIDs)),
|
||||||
(CASE WHEN $4='blacklisted' THEN 'unsubscribed'::subscription_status ELSE 'unconfirmed' END)
|
(CASE WHEN $4='blacklisted' THEN 'unsubscribed'::subscription_status ELSE 'unconfirmed' END)
|
||||||
)
|
)
|
||||||
ON CONFLICT (subscriber_id, list_id) DO UPDATE
|
ON CONFLICT (subscriber_id, list_id) DO UPDATE
|
||||||
|
@ -302,7 +307,7 @@ SELECT COUNT(*) OVER () AS total, lists.*, COUNT(subscriber_lists.subscriber_id)
|
||||||
|
|
||||||
-- name: get-lists-by-optin
|
-- name: get-lists-by-optin
|
||||||
-- Can have a list of IDs or a list of UUIDs.
|
-- Can have a list of IDs or a list of UUIDs.
|
||||||
SELECT * FROM lists WHERE optin=$1::list_optin AND
|
SELECT * FROM lists WHERE (CASE WHEN $1 != '' THEN optin=$1::list_optin ELSE TRUE END) AND
|
||||||
(CASE WHEN $2::INT[] IS NOT NULL THEN id = ANY($2::INT[])
|
(CASE WHEN $2::INT[] IS NOT NULL THEN id = ANY($2::INT[])
|
||||||
WHEN $3::UUID[] IS NOT NULL THEN uuid = ANY($3::UUID[])
|
WHEN $3::UUID[] IS NOT NULL THEN uuid = ANY($3::UUID[])
|
||||||
END) ORDER BY name;
|
END) ORDER BY name;
|
||||||
|
|
|
@ -80,7 +80,8 @@ type Status struct {
|
||||||
// SubReq is a wrapper over the Subscriber model.
|
// SubReq is a wrapper over the Subscriber model.
|
||||||
type SubReq struct {
|
type SubReq struct {
|
||||||
models.Subscriber
|
models.Subscriber
|
||||||
Lists pq.Int64Array `json:"lists"`
|
Lists pq.Int64Array `json:"lists"`
|
||||||
|
ListUUIDs pq.StringArray `json:"list_uuids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type importStatusTpl struct {
|
type importStatusTpl struct {
|
||||||
|
@ -562,8 +563,11 @@ func (s *Session) mapCSVHeaders(csvHdrs []string, knownHdrs map[string]bool) map
|
||||||
|
|
||||||
// ValidateFields validates incoming subscriber field values.
|
// ValidateFields validates incoming subscriber field values.
|
||||||
func ValidateFields(s SubReq) error {
|
func ValidateFields(s SubReq) error {
|
||||||
|
if len(s.Email) > 1000 {
|
||||||
|
return errors.New(`e-mail too long`)
|
||||||
|
}
|
||||||
if !govalidator.IsEmail(s.Email) {
|
if !govalidator.IsEmail(s.Email) {
|
||||||
return errors.New(`invalid email "` + s.Email + `"`)
|
return errors.New(`invalid e-mail "` + s.Email + `"`)
|
||||||
}
|
}
|
||||||
if !govalidator.IsByteLength(s.Name, 1, stdInputMaxLen) {
|
if !govalidator.IsByteLength(s.Name, 1, stdInputMaxLen) {
|
||||||
return errors.New(`invalid or empty name "` + s.Name + `"`)
|
return errors.New(`invalid or empty name "` + s.Name + `"`)
|
||||||
|
|
|
@ -161,28 +161,16 @@ func handleCreateSubscriber(c echo.Context) error {
|
||||||
// Get and validate fields.
|
// Get and validate fields.
|
||||||
if err := c.Bind(&req); err != nil {
|
if err := c.Bind(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if err := subimporter.ValidateFields(req); err != nil {
|
}
|
||||||
|
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
||||||
|
if err := subimporter.ValidateFields(req); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert and read ID.
|
// Insert the subscriber into the DB.
|
||||||
var (
|
subID, err := insertSubscriber(req, app)
|
||||||
email = strings.ToLower(strings.TrimSpace(req.Email))
|
|
||||||
)
|
|
||||||
req.UUID = uuid.NewV4().String()
|
|
||||||
err := app.Queries.InsertSubscriber.Get(&req.ID,
|
|
||||||
req.UUID,
|
|
||||||
email,
|
|
||||||
strings.TrimSpace(req.Name),
|
|
||||||
req.Status,
|
|
||||||
req.Attribs,
|
|
||||||
req.Lists)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Constraint == "subscribers_email_key" {
|
return err
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "The e-mail already exists.")
|
|
||||||
}
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
|
||||||
fmt.Sprintf("Error creating subscriber: %v", err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the lists are double-optins, send confirmation e-mails.
|
// If the lists are double-optins, send confirmation e-mails.
|
||||||
|
@ -191,7 +179,7 @@ func handleCreateSubscriber(c echo.Context) error {
|
||||||
|
|
||||||
// Hand over to the GET handler to return the last insertion.
|
// Hand over to the GET handler to return the last insertion.
|
||||||
c.SetParamNames("id")
|
c.SetParamNames("id")
|
||||||
c.SetParamValues(fmt.Sprintf("%d", req.ID))
|
c.SetParamValues(fmt.Sprintf("%d", subID))
|
||||||
return c.JSON(http.StatusOK, handleGetSubscriber(c))
|
return c.JSON(http.StatusOK, handleGetSubscriber(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -506,6 +494,31 @@ func handleExportSubscriberData(c echo.Context) error {
|
||||||
return c.Blob(http.StatusOK, "application/json", b)
|
return c.Blob(http.StatusOK, "application/json", b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// insertSubscriber inserts a subscriber and returns the ID.
|
||||||
|
func insertSubscriber(req subimporter.SubReq, app *App) (int, error) {
|
||||||
|
req.UUID = uuid.NewV4().String()
|
||||||
|
err := app.Queries.InsertSubscriber.Get(&req.ID,
|
||||||
|
req.UUID,
|
||||||
|
req.Email,
|
||||||
|
strings.TrimSpace(req.Name),
|
||||||
|
req.Status,
|
||||||
|
req.Attribs,
|
||||||
|
req.Lists,
|
||||||
|
req.ListUUIDs)
|
||||||
|
if err != nil {
|
||||||
|
if pqErr, ok := err.(*pq.Error); ok && pqErr.Constraint == "subscribers_email_key" {
|
||||||
|
return 0, echo.NewHTTPError(http.StatusBadRequest, "The e-mail already exists.")
|
||||||
|
}
|
||||||
|
return 0, echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
|
fmt.Sprintf("Error creating subscriber: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the lists are double-optins, send confirmation e-mails.
|
||||||
|
// Todo: This arbitrary goroutine should be moved to a centralised pool.
|
||||||
|
go sendOptinConfirmation(req.Subscriber, []int64(req.Lists), app)
|
||||||
|
return req.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
// exportSubscriberData collates the data of a subscriber including profile,
|
// exportSubscriberData collates the data of a subscriber including profile,
|
||||||
// subscriptions, campaign_views, link_clicks (if they're enabled in the config)
|
// subscriptions, campaign_views, link_clicks (if they're enabled in the config)
|
||||||
// and returns a formatted, indented JSON payload. Either takes a numeric id
|
// and returns a formatted, indented JSON payload. Either takes a numeric id
|
||||||
|
|
Loading…
Reference in New Issue