Initial code commit

This commit is contained in:
Alan Bridgeman 2024-10-14 14:41:44 -05:00
parent 88e75758af
commit 08f2127864
14 changed files with 1633 additions and 0 deletions

142
src/API.ts Normal file
View file

@ -0,0 +1,142 @@
import axios, { AxiosResponse } from 'axios';
/**
* Type defining the credentials to use when authenticating with the Listmonk API.
*/
export type APICredentials = {
host: string,
username: string,
password: string
}
/**
* A class that provides an interface to the Listmonk API.
*/
export class API {
/** The hostname of the Listmonk instance. */
private host: string;
/** The username to use when authenticating with Listmonk. */
private username: string;
/** The password to use when authenticating with Listmonk. */
private password: string;
/**
* Create a new Listmonk API instance.
*
* @param credentials The credentials to use when authenticating with Listmonk.
*/
constructor(credentials?: APICredentials) {
if(typeof credentials === 'undefined') {
credentials = this.getDefaultCredentialsFromEnvVars();
}
this.host = credentials.host;
this.username = credentials.username;
this.password = credentials.password;
}
/**
* Get the default credentials for the Listmonk API (as found in the environment variables).
*
* The table below describes the environment variables that are used:
*
* | Environment Variable | Description |
* | -------------------- | ------------------------------------------------------ |
* | LISTMONK_HOST | The hostname of the Listmonk instance. |
* | LISTMONK_USERNAME | The username to use when authenticating with Listmonk. |
* | LISTMONK_PASSWORD | The password to use when authenticating with Listmonk. |
*
* @returns The default credentials for the Listmonk API (as found in the environment variables).
*/
private getDefaultCredentialsFromEnvVars(): APICredentials {
if(typeof process.env.LISTMONK_HOST === 'undefined' || process.env.LISTMONK_HOST === null || process.env.LISTMONK_HOST === '') {
throw new Error('LISTMONK_HOST is not defined');
}
if(typeof process.env.LISTMONK_USERNAME === 'undefined' || process.env.LISTMONK_USERNAME === null || process.env.LISTMONK_USERNAME === '') {
throw new Error('LISTMONK_USERNAME is not defined');
}
if(typeof process.env.LISTMONK_PASSWORD === 'undefined' || process.env.LISTMONK_PASSWORD === null || process.env.LISTMONK_PASSWORD === '') {
throw new Error('LISTMONK_PASSWORD is not defined');
}
return {
host: process.env.LISTMONK_HOST,
username: process.env.LISTMONK_USERNAME,
password: process.env.LISTMONK_PASSWORD
};
}
/**
* @returns The credentials for the Listmonk API.
*/
getCredentials(): APICredentials {
return {
host: this.host,
username: this.username,
password: this.password
}
}
/**
* Make a GET request to the Listmonk API.
*
* T (type parameter 1) is the type of the data returned by the API call.
*
* @param endpoint The endpoint/path to send the GET request to (that is, the URL without the hostname).
* @returns A promise that resolves to the results of the API call.
*/
async get<T = any>(endpoint: string): Promise<T> {
const results = await axios.get<{ data: { results: T } }>(endpoint.startsWith('/') ? this.host + '/api' + endpoint : this.host + '/api/' + endpoint, { auth: { username: this.username, password: this.password } })
.then(response => { /*console.log(`${endpoint}: ${JSON.stringify(response.data, null, 4)}`);*/ return typeof response.data.data.results !== 'undefined' ? response.data.data.results : (response.data.data as T); });
return results;
}
/**
* Make a POST request to the Listmonk API.
*
* T (type parameter 1) is the type of the data returned by the API call.
* D (type parameter 2) is the type of the data to send to the API.
*
* @param endpoint The endpoint/path to send the POST request to (that is, the URL without the hostname).
* @param data The data to send to the API.
* @returns A promise that resolves to the result of the API call.
*/
async post<D = any,T = any>(endpoint: string, data: D): Promise<T> {
const results = await axios.post<T, AxiosResponse<T,any>,D>(endpoint.startsWith('/') ? this.host + '/api' + endpoint : this.host + '/api/' + endpoint, data, { auth: { username: this.username, password: this.password } }).then(response => response.data);
return results;
}
/**
* Make a PUT request to the Listmonk API.
*
* T (type parameter 1) is the type of the data returned by the API call.
* D (type parameter 2) is the type of the data to send to the API.
*
* @param endpoint The endpoint/path to send the PUT request to (that is, the URL without the hostname).
* @param data The data to send to the API.
* @returns A promise that resolves to the result of the API call.
*/
async put<D = any,T = any>(endpoint: string, data: D): Promise<T> {
const results = await axios.put<T, AxiosResponse<T,any>,D>(endpoint.startsWith('/') ? this.host + '/api' + endpoint : this.host + '/api/' + endpoint, data, { auth: { username: this.username, password: this.password } }).then(response => response.data);
return results;
}
/**
* Make a DELETE request to the Listmonk API.
*
* T (type parameter 1) is the type of the data returned by the API call.
*
* @param endpoint The endpoint/path to send the DELETE request to (that is, the URL without the hostname).
* @returns A promise that resolves to the result of the API call.
*/
async delete<T = any>(endpoint: string): Promise<T> {
const results = await axios.delete<T>(endpoint.startsWith('/') ? this.host + '/api' + endpoint : this.host + '/api/' + endpoint, { auth: { username: this.username, password: this.password } }).then(response => response.data);
return results;
}
}

29
src/APIObject.ts Normal file
View file

@ -0,0 +1,29 @@
import { API, APICredentials } from "./API";
export abstract class APIObject {
/** The API instance to use for making requests. */
protected api: API;
/**
* Create a new API object.
*
* @param api The API instance to use for making requests. Defaults to a new API instance if not provided.
*/
protected constructor(credentials?: APICredentials) {
this.api = new API(credentials);
}
/**
* Convert the object to a JSON object.
*
* This is mostly used for API call. But also sometimes for code reuse purposes.
*/
abstract toJSON(): any;
/**
* Allows the object to be created from data.
*
* @param data The data to create the object from.
*/
abstract fromData<T>(data: T): APIObject | Promise<APIObject>;
}

529
src/Campaign.ts Normal file
View file

@ -0,0 +1,529 @@
import { APIObject } from './APIObject';
import { API, APICredentials } from './API';
import { List, ListData } from './List';
import { Template } from './Template';
type CampaignData = {
id?: number,
uuid?: string,
name: string,
subject: string,
lists: number[] | ListData[],
type: "regular" | "optin",
content_type: 'richtext' | 'html' | 'markdown' | 'plain',
body: string,
from_email?: string,
alt_body?: string,
send_at?: string,
messenger?: string,
template_id?: number,
tags?: string[],
headers?: { [key: string]: string },
status?: "draft" | "scheduled" | "running" | "paused" | "cancelled",
created_at?: string,
updated_at?: string
}
/**
* Represents a campaign in Listmonk.
*/
export class Campaign extends APIObject {
/** Identifier for the Campaign */
private id?: number;
/** Universal Unique Identifier (UUID) for the Campaign */
private uuid?: string;
/** Campaign name. */
private name: string;
/** Campaign email subject. */
private subject: string;
/** Lists to send campaign to. */
private lists: List[];
/** Campaign type: 'regular' or 'optin'. */
private type?: 'regular' | 'optin';
/** Content type: `richtext`, `html`, `markdown`, `plain` */
private contentType?: 'richtext' | 'html' | 'markdown' | 'plain';
/** Content body of campaign. */
private campaignBody: string;
/** 'From' email in campaign emails. Defaults to value from settings if not provided. */
private fromEmail?: string;
/** Alternate plain text body for HTML (and richtext) emails. */
private altBody?: string;
/** Timestamp to schedule campaign. Format: `YYYY-MM-DDTHH:MM:SSZ` */
private sendAt?: string;
/** `email` or a custom messenger defined in settings. Defaults to `email` if not provided. */
private messenger?: string;
/** Template to use */
private template?: Template;
/** Tags to mark campaign. */
private tags?: string[];
/** Key-value pairs to send as SMTP headers. Example: `[{"x-custom-header": "value"}]`. */
private headers?: { [key: string]: string };
/**
* status for campaign: `draft`, `scheduled`, `running`, `paused`, `cancelled`.
*
* Note:
* - Only `scheduled` campaigns can change status to `draft`.
* - Only `draft` campaigns can change status to `scheduled`.
* - Only `paused` and `draft` campaigns can start (`running` status).
* - Only `running` campaigns can change status to `cancelled` and `paused`.
*/
private status?: 'draft' | 'scheduled' | 'running' | 'paused' | 'cancelled';
/** Timestamp for when the campaign was created. Format: `YYYY-MM-DDTHH:MM:SSZ` */
private createdAt?: string;
/** Timestamp for when the campaign was last updated. Format: `YYYY-MM-DDTHH:MM:SSZ` */
private updatedAt?: string;
constructor(credentials?: APICredentials, id?: number, uuid?: string, name?: string, subject?: string, lists?: List[], type?: 'regular' | 'optin', contentType?: 'richtext' | 'html' | 'markdown' | 'plain', campaignBody?: string, fromEmail?: string, altBody?: string, sendAt?: string, messenger?: string, template?: Template, tags?: string[], headers?: { [key: string]: string }, status?: 'draft' | 'scheduled' | 'running' | 'paused' | 'cancelled', createdAt?: string, updatedAt?: string) {
super(credentials);
this.id = id;
this.uuid = uuid;
this.name = typeof name !== 'undefined' ? name : '';
this.subject = typeof subject !== 'undefined' ? subject : '';
this.lists = typeof lists !== 'undefined' ? lists : [];
this.type = type;
this.contentType = contentType;
this.campaignBody = typeof campaignBody !== 'undefined' ? campaignBody : '';
this.fromEmail = fromEmail;
this.altBody = altBody;
this.sendAt = sendAt;
this.messenger = messenger;
this.template = template;
this.tags = typeof tags !== 'undefined' ? tags : [];
this.headers = headers;
this.status = status;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
/**
* @returns Identifier for the Campaign
*/
getID(): number | undefined {
return this.id;
}
/**
* @returns Universal Unique Identifier (UUID) for the Campaign
*/
getUUID(): string | undefined {
return this.uuid;
}
/**
* @returns Campaign name
*/
getName(): string {
return this.name;
}
/**
* Set the name of the campaign
*
* @param name The name to set the Campaign name to
*/
setName(name: string): void {
this.name = name;
}
/**
* @returns Campaign email subject
*/
getSubject(): string {
return this.subject;
}
/**
* Set the subject of the campaign
*
* @param subject The subject to set the Campaign email subject to
*/
setSubject(subject: string): void {
this.subject = subject;
}
/**
* @returns Lists to send the campaign to
*/
getLists(): List[] {
return this.lists;
}
/**
* Add a list for the campaign to send to
*
* @param list List to add to the campaign
*/
addList(list: List): void {
this.lists.push(list);
}
/**
* Remove a list from the campaign
*
* @param list List to remove from the campaign
*/
removeList(list: List): void {
this.lists = this.lists.filter((l: List) => l.getID() !== list.getID());
}
/**
* Check if the campaign includes a list
*
* @param list The list to check if the campaign includes
* @returns A boolean indicating if the campaign includes the list
*/
includesList(list: List): boolean {
return this.lists.some((l: List) => l.getID() === list.getID());
}
/**
* @returns Campaign type: 'regular' or 'optin'
*/
getType(): 'regular' | 'optin' | undefined {
return this.type;
}
/**
* Set the type of the campaign
*
* @param type Campaign type: 'regular' or 'optin'
*/
setType(type: 'regular' | 'optin'): void {
this.type = type;
}
/**
* @returns Content type: `richtext`, `html`, `markdown`, `plain`
*/
getContentType(): 'richtext' | 'html' | 'markdown' | 'plain' | undefined {
return this.contentType;
}
/**
* Set the content type of the campaign
*
* @param contentType Content type: `richtext`, `html`, `markdown`, `plain`
*/
setContentType(contentType: 'richtext' | 'html' | 'markdown' | 'plain'): void {
this.contentType = contentType;
}
/**
* @returns Content body of the campaign
*/
getCampaignBody(): string {
return this.campaignBody;
}
/**
* Set the content body of the campaign
*
* @param campaignBody The content body to set the campaign body to
*/
setCampaignBody(campaignBody: string): void {
this.campaignBody = campaignBody;
}
/**
* @returns 'From' email in campaign emails. Defaults to value from settings if not provided.
*/
getFromEmail(): string | undefined {
return this.fromEmail;
}
/**
* Set the 'From' email in campaign emails
*
* @param fromEmail The email to set the 'From' email in campaign emails to
*/
setFromEmail(fromEmail: string): void {
this.fromEmail = fromEmail;
}
/**
* @returns Alternate plain text body for HTML (and richtext) emails
*/
getAltBody(): string | undefined {
return this.altBody;
}
/**
* Set the alternate plain text body for HTML (and richtext) emails
*
* @param altBody The alternate plain text body to set for HTML (and richtext) emails
*/
setAltBody(altBody: string): void {
this.altBody = altBody;
}
/**
* @returns Timestamp to schedule the campaign. Format: `YYYY-MM-DDTHH:MM:SSZ`
*/
getSendAt(): string | undefined {
return this.sendAt;
}
/**
* Set the timestamp to schedule the campaign
*
* @param sendAt The timestamp to set to schedule the campaign
*/
setSendAt(sendAt: string): void {
this.sendAt = sendAt;
}
/**
* @returns `email` or a custom messenger defined in settings. Defaults to `email` if not provided.
*/
getMessenger(): string | undefined {
return this.messenger;
}
/**
* Set the messenger for the campaign
*
* @param messenger The messenger to set for the campaign
*/
setMessenger(messenger: string): void {
this.messenger = messenger;
}
/**
* Get the ID of the template used for the campaign
*
* @returns ID of the template used for the campaign or -1 if the template isn't set explicitly
*/
getTemplateID(): number {
return this.getTemplate()?.getID() ?? -1;
}
/**
* @returns The template used for the campaign
*/
getTemplate(): Template | undefined {
return this.template;
}
/**
* Set the template for the campaign
*
* @param template The template to set for the campaign
*/
setTemplate(template: Template): void {
this.template = template;
}
/**
* Set the template for the campaign by it's ID
*
* @param templateID The ID of the template to set for the campaign
*/
setTemplateByID(templateID: number): void {
this.template = new Template(this.api.getCredentials(), templateID);
}
/**
* @returns Tags to mark the campaign
*/
getTags(): string[] | undefined {
return this.tags;
}
/**
* Add a tag to the campaign
*
* @param tag The tag to add to the campaign
*/
addTag(tag: string): void {
if(this.tags === undefined) {
this.tags = [];
}
this.tags.push(tag);
}
/**
* Remove a tag from the campaign
*
* @param tag The tag to remove from the campaign
*/
removeTag(tag: string): void {
if(this.tags === undefined) {
return;
}
this.tags = this.tags.filter((t: string) => t !== tag);
}
/**
* @returns Key-value pairs to send as SMTP headers
*/
getHeaders(): { [key: string]: string } | undefined {
return this.headers;
}
/**
* Add a header to the campaign
*
* @param key The key of the header to add
* @param value The value of the header to add
*/
addHeader(key: string, value: string): void {
if(this.headers === undefined) {
this.headers = {};
}
this.headers[key] = value;
}
/**
* Remove a header from the campaign
*
* @param key The key of the header to remove
*/
removeHeader(key: string): void {
if(this.headers === undefined) {
return;
}
delete this.headers[key];
}
/**
* @returns Status for the campaign: `draft`, `scheduled`, `running`, `paused`, `cancelled`
*/
getStatus(): 'draft' | 'scheduled' | 'running' | 'paused' | 'cancelled' | undefined {
return this.status;
}
/**
* Set the status for the campaign
*
* Note:
* - Only `scheduled` campaigns can change status to `draft`.
* - Only `draft` campaigns can change status to `scheduled`.
* - Only `paused` and `draft` campaigns can start (`running` status).
* - Only `running` campaigns can change status to `cancelled` and `paused`.
*
* @param status The status to set for the campaign: `draft`, `scheduled`, `running`, `paused`, `cancelled`
*/
setStatus(status: 'draft' | 'scheduled' | 'running' | 'paused' | 'cancelled'): void {
this.status = status;
}
/**
* @returns Timestamp for when the campaign was created. Format: `YYYY-MM-DDTHH:MM:SSZ`
*/
getCreatedAt(): string | undefined {
return this.createdAt;
}
/**
* @returns Timestamp for when the campaign was last updated. Format: `YYYY-MM-DDTHH:MM:SSZ`
*/
getUpdatedAt(): string | undefined {
return this.updatedAt;
}
/**
* Creates or updates the campaign in Listmonk.
*/
async save() {
const data = this.toJSON();
// If the UUID is not set, then we assume we need to create the subscriber in Listmonk
if(typeof data.uuid === 'undefined') {
// Technically, this might be unnecessary since the values should be set to undefined anyway, but it's good to be safe
delete data.id;
delete data.uuid;
delete data.created_at;
delete data.updated_at;
delete data.status;
// Make the API call to create the subscriber in Listmonk
await this.api.post<CampaignData>('/campaigns', data);
}
else {
await this.api.put<CampaignData>('/campaigns/' + data.id, data);
}
}
toJSON(): CampaignData {
return {
id: this.id,
uuid: this.uuid,
name: this.name,
subject: this.subject,
lists: this.lists.map((list: List) => list.getID() as number),
type: typeof this.type !== 'undefined' ? this.type : 'regular',
content_type: typeof this.contentType !== 'undefined' ? this.contentType : 'plain',
body: this.campaignBody,
from_email: this.fromEmail,
alt_body: this.altBody,
send_at: this.sendAt,
messenger: this.messenger,
template_id: this.template?.getID(),
tags: this.tags,
headers: this.headers,
status: typeof this.status !== 'undefined' ? this.status : 'draft',
created_at: this.createdAt,
updated_at: this.updatedAt
};
}
async fromData<CampaignData>(data: CampaignData): Promise<Campaign> {
const typedData = data as {id?: number, uuid?: string, name: string, subject: string, lists: number[] | ListData[], type: "regular" | "optin", content_type: 'richtext' | 'html' | 'markdown' | 'plain', body: string, from_email?: string, alt_body?: string, send_at?: string, messenger?: string, template_id?: number, tags?: string[], headers?: { [key: string]: string }, status: "draft" | "scheduled" | "running" | "paused" | "cancelled", created_at?: string, updated_at?: string };
const lists = (
await Promise.all(
typedData.lists.map(
async (listData: Number | ListData) => {
if(typeof listData === 'number') {
return await List.find((list: List) => list.getID() === listData);
}
else {
return new List().fromData(listData);
}
}
)
)
).filter((list: List | undefined) => typeof list !== 'undefined');
const template = typeof typedData.template_id !== 'undefined' ? await Template.find((template: Template) => template.getID() === typedData.template_id) : undefined;
this.id = typedData.id;
this.uuid = typedData.uuid;
this.setName(typedData.name);
this.setSubject(typedData.subject);
this.lists = lists;
this.setType(typedData.type);
this.setContentType(typedData.content_type);
this.setCampaignBody(typedData.body);
this.fromEmail = typedData.from_email;
this.altBody = typedData.alt_body;
this.sendAt = typedData.send_at;
this.messenger = typedData.messenger;
this.template = template;
this.tags = typedData.tags;
this.headers = typedData.headers;
this.setStatus(typedData.status);
this.createdAt = typedData.created_at;
this.updatedAt = typedData.updated_at;
return this;
}
/**
* Create a new Campaign object with the given data.
*
* Note, this doesn't automatically create the campaign in Listmonk. You need to call `save` to do that.
*
* @param name Name of the new campaign.
* @param subject Subject of the new campaign.
* @param lists Lists to send the campaign to.
* @param type Type of the campaign: 'regular' or 'optin'.
* @param contentType Content type: `richtext`, `html`, `markdown`, `plain`
* @param body Raw HTML body of the campaign
* @returns The new campaign object
*/
static async create(name: string, subject: string, lists: List[], type: "regular" | "optin", content_type: 'richtext' | 'html' | 'markdown' | 'plain', body: string, credentials?: APICredentials): Promise<Campaign> {
return await new Campaign(credentials).fromData({ name, subject, lists: lists.map((list: List) => list.toJSON()), type, content_type, body });
}
/**
* Find a campaign based on the given predicate.
*
* @param predicate A function that takes a campaign and returns true if it matches the desired template.
* @param credentials Optional credentials to use to authenticate API requests
* @returns The campaign that matches the predicate, or undefined if no campaign matches the predicate.
*/
static async find(predicate: (campaign: Campaign) => boolean, credentials?: APICredentials): Promise<Campaign | undefined> {
return await new API(credentials).get<CampaignData[]>('/campaigns')
.then(async (campaigns: CampaignData[]) => {
// Convert each CampaignData JSON object to a Campaign object
const campaignObjs: Campaign[] = await Promise.all(campaigns.map((campaign: CampaignData) => new Campaign(credentials).fromData(campaign)));
// Use the predicate function provided to find the desired campaign
const campaign = campaignObjs.find(predicate);
return campaign;
});
}
}

131
src/List.ts Normal file
View file

@ -0,0 +1,131 @@
import { APIObject } from "./APIObject";
import { API, APICredentials } from "./API";
export type ListData = {
id?: number,
uuid?: string,
name: string,
type: 'public' | 'private',
optin?: 'single' | 'double',
tags: string[],
created_at?: string,
updated_at?: string
};
export class List extends APIObject {
private id?: number;
private uuid?: string;
private name: string;
private type?: 'public' | 'private';
private optin?: 'single' | 'double';
private tags: string[];
private createdAt?: string;
private updatedAt?: string;
constructor(credentials?: APICredentials, id?: number, uuid?: string, name?: string, type?: 'public' | 'private', optin?: 'single' | 'double', tags?: string[], createdAt?: string, updatedAt?: string) {
super(credentials);
this.id = id;
this.uuid = uuid;
this.name = typeof name !== 'undefined' ? name : '';
this.type = type;
this.optin = optin;
this.tags = typeof tags !== 'undefined' ? tags : [];
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
getID(): number | undefined {
return this.id;
}
getName(): string {
return this.name;
}
/**
* Creates or updates the list in Listmonk.
*/
async save() {
const data = this.toJSON();
// If the UUID is not set, then we assume we need to create the list in Listmonk
if(typeof data.uuid === 'undefined') {
// Technically, this might be unnecessary since the values should be set to undefined anyway, but it's good to be safe
delete data.id;
delete data.created_at;
delete data.updated_at;
delete data.uuid;
// Make the API call to create the list in Listmonk
await this.api.post<ListData>('/lists', data);
} else {
// Make the API call to update the list in Listmonk
await this.api.post<ListData>('/lists/' + data.uuid, data);
}
}
toJSON(): ListData {
return {
id: this.id,
uuid: this.uuid,
name: this.name,
type: typeof this.type !== 'undefined' ? this.type : 'private',
optin: this.optin,
tags: this.tags,
created_at: this.createdAt,
updated_at: this.updatedAt
};
}
fromData<ListData>(data: ListData): List {
const typedData = data as { id?: number, uuid?: string, name: string, type: 'public' | 'private', optin?: 'single' | 'double', tags: string[], created_at?: string, updated_at?: string };
this.id = typedData.id;
this.uuid = typedData.uuid;
this.name = typedData.name;
this.type = typedData.type;
this.optin = typedData.optin;
this.tags = typedData.tags;
this.createdAt = typedData.created_at;
this.updatedAt = typedData.updated_at;
return this;
}
/**
* Create a new List object with the given data.
*
* Note, this doesn't automatically create the list in Listmonk. You need to call `save` to do that.
*
* @param name Name of the new list.
* @param type Type of list. Options: private, public.
* @param optin Opt-in type. Options: single, double.
* @param tags Associated tags for a list.
* @param credentials Optional credentials to use for the API call.
* @returns A new List object with the given data.
*/
static async create(name: string, type: 'public' | 'private', optin: 'single' | 'double', tags?: string[], credentials?: APICredentials): Promise<List> {
return await new List(credentials).fromData({ name, type, optin, tags });
}
/**
* Find a list based on a predicate function.
*
* @param predicate The function to use to find the desired list.
* @param credentials Optional The credentials to use for the API call.
* @returns The list that matches the predicate, or undefined if no list matches the predicate.
*/
static async find(predicate: (list: List) => boolean, credentials?: APICredentials): Promise<List | undefined> {
return await new API(credentials).get<ListData[]>('/lists')
.then((lists: ListData[]) => {
// Convert each LIstData JSOn object to a List object
const listObjs: List[] = lists.map((list: ListData) => new List(credentials).fromData(list));
// Use the predicate function provided to find the desired list
const list = listObjs.find(predicate);
return list;
});
}
}

47
src/ListMembership.ts Normal file
View file

@ -0,0 +1,47 @@
import { APIObject } from './APIObject';
import { APICredentials } from './API';
import { List, ListData } from './List';
export type ListMembershipData = {
subscription_status: 'unconfirmed' | 'confirmed',
[key: string]: any
};
export class ListMembership extends APIObject {
private subscriptionStatus?: 'unconfirmed' | 'confirmed';
private list?: List;
constructor(credentials?: APICredentials, subscriptionStatus?: 'unconfirmed' | 'confirmed', list?: List) {
super(credentials);
this.subscriptionStatus = subscriptionStatus;
this.list = list;
}
getSubscriptionStatus(): 'unconfirmed' | 'confirmed' | undefined {
return this.subscriptionStatus;
}
getList(): List | undefined {
return this.list;
}
toJSON(): ListMembershipData {
return {
subscription_status: typeof this.subscriptionStatus !== 'undefined' ? this.subscriptionStatus : 'unconfirmed',
...this.list?.toJSON()
};
}
fromData<ListMembershipData>(data: ListMembershipData): ListMembership {
// NOTE types are weird here because TypeScript was complaining otherwise
const subscriptionStatus = (data as { subscription_status: 'unconfirmed' | 'confirmed', [key: string]: any }).subscription_status;
delete (data as { subscription_status?: 'unconfirmed' | 'confirmed', [key: string]: any }).subscription_status;
const list = new List().fromData(data as ListData);
this.subscriptionStatus = subscriptionStatus;
this.list = list;
return this;
}
}

216
src/Subscriber.ts Normal file
View file

@ -0,0 +1,216 @@
import { APIObject } from "./APIObject";
import { API, APICredentials } from "./API";
import { List } from "./List";
import { ListMembership, ListMembershipData } from "./ListMembership";
type SubscriberData = {
id?: number,
uuid?: string,
email: string,
name: string,
attribs: {
[key: string]: any
},
status: "enabled" | "blocklisted",
lists: number[] | ListMembershipData[],
created_at?: string,
updated_at?: string
};
export class Subscriber extends APIObject {
private id?: number;
private uuid?: string;
private email: string;
private name: string;
private attribs: { [key: string]: any };
private status?: 'enabled' | 'blocklisted';
private lists: ListMembership[];
private createdAt?: string;
private updatedAt?: string;
constructor(credentials?: APICredentials, id?: number, uuid?: string, email?: string, name?: string, attribs?: { [key: string]: any }, status?: 'enabled' | 'blocklisted', lists?: ListMembership[], createdAt?: string, updatedAt?: string) {
super(credentials);
this.id = id;
this.uuid = uuid;
this.email = typeof email !== 'undefined' ? email : '';
this.name = typeof name !== 'undefined' ? name : '';
this.attribs = typeof attribs !== 'undefined' ? attribs : {};
this.status = status;
this.lists = typeof lists !== 'undefined' ? lists : [];
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
getID(): number | undefined {
return this.id;
}
getUUID(): string | undefined {
return this.uuid;
}
getEmail(): string {
return this.email;
}
getName(): string {
return this.name;
}
getAttributes(): { [key: string]: any } {
return this.attribs;
}
hasAttribute(key: string): boolean {
return typeof this.attribs[key] !== 'undefined';
}
getAttribute(key: string): any {
return this.hasAttribute(key) ? this.attribs[key] : undefined;
}
setAttribute(key: string, value: any) {
this.attribs[key] = value;
}
removeAttribute(key: string) {
delete this.attribs[key];
}
getStatus(): 'enabled' | 'blocklisted' | undefined {
return this.status;
}
getLists(): ListMembership[] {
return this.lists;
}
addList(list: List) {
this.lists.push(new ListMembership(this.api.getCredentials()).fromData({ subscription_status: 'unconfirmed', ...list.toJSON() }));
}
removeList(list: ListMembership) {
this.lists = this.lists.filter((listMembership: ListMembership) => listMembership !== list);
}
getCreatedAt(): string | undefined {
return this.createdAt;
}
getUpdatedAt(): string | undefined {
return this.updatedAt;
}
/**
* Creates or updates the subscriber in Listmonk.
*/
async save() {
const data = this.toJSON();
// If the UUID is not set, then we assume we need to create the subscriber in Listmonk
if(typeof data.uuid === 'undefined') {
// Technically, this might be unnecessary since the values should be set to undefined anyway, but it's good to be safe
delete data.id;
delete data.created_at;
delete data.updated_at;
delete data.uuid;
// Make the API call to create the subscriber in Listmonk
await this.api.post<SubscriberData>('/subscribers', data);
}
else {
await this.api.put<SubscriberData>('/subscribers/' + data.id, data);
}
}
toJSON(): SubscriberData {
return {
id: this.id,
created_at: this.createdAt,
updated_at: this.updatedAt,
uuid: this.uuid,
email: this.email,
name: this.name,
attribs: this.attribs,
status: this.status as 'enabled' | 'blocklisted',
lists: this.lists.map((listMembership: ListMembership) => (listMembership.getList() as List).getID() as number)
};
}
async fromData<SubscriberData>(data: SubscriberData): Promise<Subscriber> {
// NOTE types are weird here because TypeScript was complaining otherwise
const typedData = (data as { id?: number, uuid?: string, email: string, name: string, attribs: { [key: string]: any }, status: "enabled" | "blocklisted", lists: number[] | ListMembershipData[], created_at?: string, updated_at?: string });
let listMemberships: ListMembership[] = [];
// We only want to bother processing the lists if there are any
if(typedData.lists.length > 0) {
listMemberships = await Promise.all(
typedData.lists.map(
async (listMembershipData: number | ListMembershipData) => {
if(typeof listMembershipData === 'number') {
// Get the list object based on it's IDs from Listmonk
const listObj = await List.find((list: List) => (list.getID() as number) === listMembershipData);
// Verify we found a list with the given ID
if(typeof listObj === 'undefined') {
throw new Error('List with ID ' + listMembershipData + ' not found');
}
// Default the membership object to unconfirmed
listMembershipData = {
subscription_status: 'unconfirmed',
...listObj.toJSON()
}
}
return new ListMembership(this.api.getCredentials()).fromData(listMembershipData);
}
)
);
}
this.id = typedData.id;
this.uuid = typedData.uuid;
this.email = typedData.email;
this.name = typedData.name;
this.attribs = typedData.attribs;
this.status = typedData.status;
this.lists = listMemberships;
this.createdAt = typedData.created_at;
this.updatedAt = typedData.updated_at;
return this;
}
/**
* Create a new Subscriber object with the given data.
*
* Note, this doesn't automatically create the subscriber in Listmonk. You need to call `save` to do that.
*
* @param email Subscriber's email address |
* @param name Subscriber's name. |
* @param status Subscriber's status. |
* @param lists List of list IDs to subscribe to. |
* @param attribs Attributes of the new subscriber. |
* @param preconfirm_subscriptions If true, subscriptions are marked as confirmed and no-optin emails are sent for double opt-in lists. |
* @param credentials Optional The credentials to use for the API call.
*/
static async create(email: string, name: string, status: 'enabled' | 'blocklisted', lists?: List[], attribs?: { [key: string]: any }, preconfirm_subscriptions?: boolean, credentials?: APICredentials): Promise<Subscriber> {
return await new Subscriber(credentials).fromData({ email, name, status, lists: typeof lists !== 'undefined' ? lists.map((list: List) => list.getID()) : [] , attribs });
}
/**
* Find a subscriber based on the given predicate.
*
* @param predicate A function that takes a subscriber and returns true if it matches the desired subscriber.
* @param credentials Optional The credentials to use for the API call.
* @returns The subscriber that matches the predicate, or undefined if no subscriber matches the predicate.
*/
static async find(predicate: (subscriber: Subscriber) => boolean, credentials?: APICredentials): Promise<Subscriber | undefined> {
return await new API(credentials).get<SubscriberData[]>('/subscribers?per_page=all')
.then(async (subscribers: SubscriberData[]) => {
// Convert each SubscriberData JSON object to a Subscriber object
const subscriberObjs: Subscriber[] = await Promise.all(subscribers.map((subscriber: SubscriberData) => new Subscriber(credentials).fromData(subscriber)));
// Use the predicate function provided to find the desired list
const subscriber = subscriberObjs.find(predicate);
return subscriber;
});
}
}

238
src/Template.ts Normal file
View file

@ -0,0 +1,238 @@
import { APIObject } from './APIObject';
import { API, APICredentials } from './API';
type TemplateData = {
id?: number,
name: string,
type: 'campaign' | 'tx',
body: string,
subject?: string,
is_default?: boolean,
created_at?: string,
updated_at?: string
};
/**
* A class that represents a template in Listmonk.
*/
export class Template extends APIObject {
private id?: number;
/** Name of the template */
private name: string;
/** Type of the template (`campaign` or `tx`) */
private type?: 'campaign' | 'tx';
/** Raw HTML body of the template */
private body: string;
/** Subject line for the template (only for tx) */
private subject?: string;
/** Whether the template is the default template */
private isDefault?: boolean;
/** Timestamp when the template was created. Format: `YYYY-MM-DDTHH:MM:SSZ` */
private createdAt?: string;
/** Timestamp when the template was last updated. Format: `YYYY-MM-DDTHH:MM:SSZ` */
private updatedAt?: string;
/**
* Create a new template object
*
* @param credentials Optional credentials to use to authenticate API requests
* @param id The identifier for the template
* @param name The name of the template
* @param type The type of the template (`campaign` or `tx`)
* @param body The raw HTML body of the template
* @param isDefault Whether the template is the default template
* @param createdAt When the template was created. Format: `YYYY-MM-DDTHH:MM:SSZ`
* @param updatedAt When the template was last updated. Format: `YYYY-MM-DDTHH:MM:SSZ`
*/
constructor(credentials?: APICredentials, id?: number, name?: string, type?: "campaign" | "tx", body?: string, isDefault?: boolean, createdAt?: string, updatedAt?: string) {
super(credentials);
this.id = id;
this.name = typeof name !== 'undefined' ? name : '';
this.body = typeof body !== 'undefined' ? body : '';
this.type = type;
this.isDefault = isDefault;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
/**
* @returns Identifier for the template
*/
getID(): number | undefined {
return this.id;
}
/**
* @returns The name of the template
*/
getName(): string {
return this.name;
}
/**
* Set the name of the template
*
* @param name Name of the template
*/
setName(name: string): void {
this.name = name;
}
/**
* @returns Raw HTML body of the template
*/
getBody(): string {
return this.body;
}
/**
* Set the raw HTML body of the template
*
* @param body Raw HTML body of the template
*/
setBody(body: string): void {
this.body = body;
}
/**
* @returns Type of the template (`campaign` or `tx`)
*/
getType(): 'campaign' | 'tx' | undefined {
return this.type;
}
/**
* Set the type of the template
*
* @param type Type of the template (`campaign` or `tx`)
*/
setType(type: 'campaign' | 'tx'): void {
this.type = type;
}
/**
* @returns Subject line for the template (only for tx)
*/
getSubject(): string | undefined {
return this.subject;
}
/**
* Set the subject line for the template
*
* NOTE: ONLY relevant for `tx` type templates
*
* @param subject Subject line for the template
*/
setSubject(subject: string): void {
this.subject = subject;
}
/**
* @returns Whether the template is the default template
*/
getIsDefault(): boolean | undefined {
return this.isDefault;
}
/**
* Set whether the template is the default template
*
* @param isDefault Whether the template is the default template
*/
setIsDefault(isDefault: boolean): void {
this.isDefault = isDefault;
}
/**
* @returns Timestamp when the template was created. Format: `YYYY-MM-DDTHH:MM:SSZ`
*/
getCreatedAt(): string | undefined {
return this.createdAt;
}
/**
* @returns Timestamp when the template was last updated. Format: `YYYY-MM-DDTHH:MM:SSZ`
*/
getUpdatedAt(): string | undefined {
return this.updatedAt;
}
/**
* Creates or updates the template in Listmonk.
*/
async save() {
const data = this.toJSON();
// If the UUID is not set, then we assume we need to create the subscriber in Listmonk
if(typeof data.id === 'undefined') {
// Technically, this might be unnecessary since the values should be set to undefined anyway, but it's good to be safe
delete data.id;
delete data.created_at;
delete data.updated_at;
// Make the API call to create the subscriber in Listmonk
await this.api.post<TemplateData>('/templates', data);
}
else {
await this.api.put<TemplateData>('/templates/' + data.id, data);
}
}
toJSON(): TemplateData {
return {
id: this.id,
name: this.name,
body: this.body,
type: typeof this.type !== 'undefined' ? this.type : 'campaign',
is_default: this.isDefault,
created_at: this.createdAt,
updated_at: this.updatedAt
};
}
fromData<TemplateData>(data: TemplateData): Template {
const typedData = data as { id?: number, name: string, type: 'campaign' | 'tx', body: string, subject?: string, is_default?: boolean, created_at?: string, updated_at?: string};
this.id = typedData.id;
this.name = typedData.name
this.type = typedData.type;
this.body = typedData.body;
this.isDefault = typedData.is_default;
this.createdAt = typedData.created_at;
this.updatedAt = typedData.updated_at;
return this;
}
/**
* Create a new Template object with the given data.
*
* Note, this doesn't automatically create the template in Listmonk. You need to call `save` to do that.
*
* @param name Name of the new template.
* @param type Type of the template (`campaign` or `tx`)
* @param body Raw HTML body of the template
* @param credentials Optional credentials to use to authenticate API requests
* @returns The new template object
*/
static async create(name: string, type: 'campaign' | 'tx', body: string, credentials?: APICredentials): Promise<Template> {
return await new Template(credentials).fromData({ name, type, body });
}
/**
* Find a template based on the given predicate.
*
* @param predicate A function that takes a template and returns true if it matches the desired template.
* @param credentials Optional credentials to use to authenticate API requests
* @returns The template that matches the predicate, or undefined if no template matches the predicate.
*/
static async find(predicate: (template: Template) => boolean, credentials?: APICredentials): Promise<Template | undefined> {
return await new API(credentials).get<TemplateData[]>('/templates')
.then(async (templates: TemplateData[]) => {
// Convert each TemplateData JSON object to a Template object
const templateObjs: Template[] = await Promise.all(templates.map((template: TemplateData) => new Template(credentials).fromData(template)));
// Use the predicate function provided to find the desired template
const template = templateObjs.find(predicate);
return template;
});
}
}

53
src/demo.ts Normal file
View file

@ -0,0 +1,53 @@
import dotenv from 'dotenv';
import { List } from './List';
import { Subscriber } from './Subscriber';
import { Template } from './Template';
import { Campaign } from './Campaign';
// Load environment variables from .env file (mostly Listmonki API credentials)
dotenv.config();
async function main() {
// Create a new list
await (await List.create('API Generated List', 'private', 'single')).save();
// Get a list by it's name
const list = await List.find((list: List) => list.getName() === 'API Generated List');
if(typeof list === 'undefined') {
throw new Error('List not found');
}
// Create a subscriber
await (await Subscriber.create('api@nodejs.listmonk', 'Listmonk Node Client Library', 'enabled', [/*list*/], { phone: '+12345678912' })).save();
// Get a subscriber by their email
const subscriber = await Subscriber.find((subscriber: Subscriber) => subscriber.getEmail() === 'api@nodejs.listmonk');
if(typeof subscriber === 'undefined') {
throw new Error('Subscriber not found');
}
// Add the subscriber to the list
subscriber.addList(list);
await subscriber.save();
// Create a template
await (await Template.create('API Generated Template', 'campaign', 'Hello {{ .Subscriber.FirstName }},<br>{{ template "content" . }}<br>Your Phone number on record is: {{ .Subscriber.Attribs.phone }}')).save();
// Get a template by it's name
const template = await Template.find((template: Template) => template.getName() === 'API Generated Template');
if(typeof template === 'undefined') {
throw new Error('Template not found');
}
// Create a campaign
const campaign = (await Campaign.create('API Generated Campaign', 'API Generated Campaign Subject', [list], 'regular', 'html', '<h1>API Generated Text</h1><br><p>A demo campaign generated using the Listmonk Node Client Library</p>'));
// Set the template for the campaign
campaign.setTemplate(template);
// Save the campaign to Listmonk
await campaign.save();
}
main();