Skip to content

Commit

Permalink
Merge pull request #10 from pankajsoni19/feature/traffic-split
Browse files Browse the repository at this point in the history
Feature/traffic split
  • Loading branch information
pankajsoni19 authored Jan 18, 2025
2 parents ed5d891 + cc56de2 commit be07e91
Show file tree
Hide file tree
Showing 15 changed files with 269 additions and 68 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ I tagged it to db schema version, and will follow that as semver. Current `5.3.0
- From is moved into smtp, messenger config.
- Sample -> `One <[email protected]>,10,Two <[email protected]>,5`. This will send based on weights assigned. Can be phone number for messenger.

##### Multi messenger send weighted

- Can send to multiple channels at once
- traffic can be split based on weights or duplicated on all channels
- For duplicating traffic sliding window limiter counts campaign subscriber triggered not messengers triggered per subscriber.

##### Per Campaign smtp/messenger

- In `Settings`>`SMTP` config specify a name.
Expand Down
31 changes: 16 additions & 15 deletions cmd/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,22 @@ import (
"encoding/json"
"fmt"
"net/http"
"sort"
"syscall"
"time"

"github.com/labstack/echo/v4"
)

type serverConfig struct {
RootURL string `json:"root_url"`
Messengers []string `json:"messengers"`
Langs []i18nLang `json:"langs"`
Lang string `json:"lang"`
Permissions json.RawMessage `json:"permissions"`
Update *AppUpdate `json:"update"`
NeedsRestart bool `json:"needs_restart"`
HasLegacyUser bool `json:"has_legacy_user"`
Version string `json:"version"`
RootURL string `json:"root_url"`
Messengers []map[string]interface{} `json:"messengers"`
Langs []i18nLang `json:"langs"`
Lang string `json:"lang"`
Permissions json.RawMessage `json:"permissions"`
Update *AppUpdate `json:"update"`
NeedsRestart bool `json:"needs_restart"`
HasLegacyUser bool `json:"has_legacy_user"`
Version string `json:"version"`
}

// handleGetServerConfig returns general server config.
Expand All @@ -45,13 +44,15 @@ func handleGetServerConfig(c echo.Context) error {
out.Langs = langList

// Sort messenger names with `email` always as the first item.
var names []string
for name := range app.messengers {
names = append(names, name)
messengers := make([]map[string]interface{}, 0)
for _, v := range app.messengers {
messengers = append(messengers, map[string]interface{}{
"name": v.Name(),
"uuid": v.UUID(),
})
}

sort.Strings(names)
out.Messengers = names
out.Messengers = messengers

app.Lock()
out.NeedsRestart = app.needsRestart
Expand Down
8 changes: 1 addition & 7 deletions cmd/campaigns.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,6 @@ func handleCreateCampaign(c echo.Context) error {
if o.ContentType == "" {
o.ContentType = models.CampaignContentTypeRichtext
}
if o.Messenger == "" {
o.Messenger = app.defaultMessenger.Name()
}

// Validate.
if c, err := validateCampaignFields(o, app); err != nil {
Expand Down Expand Up @@ -479,6 +476,7 @@ func handleTestCampaign(c echo.Context) error {
camp.ContentType = req.ContentType
camp.Headers = req.Headers
camp.TemplateID = req.TemplateID
camp.TrafficType = req.TrafficType
for _, id := range req.MediaIDs {
if id > 0 {
camp.MediaIDs = append(camp.MediaIDs, int64(id))
Expand Down Expand Up @@ -584,10 +582,6 @@ func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) {
return c, errors.New(app.i18n.T("campaigns.fieldInvalidListIDs"))
}

if !app.manager.HasMessenger(c.Messenger) {
return c, errors.New(app.i18n.Ts("campaigns.fieldInvalidMessenger", "name", c.Messenger))
}

camp := models.Campaign{Body: c.Body, TemplateBody: tplTag}
if err := c.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
return c, errors.New(app.i18n.Ts("campaigns.fieldInvalidBody", "error", err.Error()))
Expand Down
1 change: 1 addition & 0 deletions cmd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ var migList = []migFunc{
{"v5.1.0", migrations.V5_1_0},
{"v5.2.0", migrations.V5_2_0},
{"v5.3.0", migrations.V5_3_0},
{"v5.4.0", migrations.V5_4_0},
}

// upgrade upgrades the database to the current version by running SQL migration files
Expand Down
119 changes: 102 additions & 17 deletions frontend/src/views/Campaign.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,45 @@
</b-select>
</b-field>

<b-field :label="$tc('globals.terms.messenger')" label-position="on-border">
<b-select :placeholder="$tc('globals.terms.messenger')" v-model="form.messenger" name="messenger"
:disabled="!canEdit" required>
<option v-for="m in messengers" :value="m" :key="m">
{{ m }}
</option>
</b-select>
</b-field>
<div class="columns">
<div class="column is-4">
<b-field label="Traffic" label-position="on-border">
<b-select v-model="form.trafficType" name="run_type" :disabled="!canEdit">
<option value="split">
Split
</option>
<option value="duplicate">
Duplicate
</option>
</b-select>
</b-field>
</div>
</div>

<div class="columns">
<div class="column is-12">
<b-field :label="$tc('globals.terms.messenger')"
label-position="on-border"
message="For spliting data choose integer weights between 1 to 1000. For duplicate all messages are replicated on all selected messengers"
style="border-radius: 5px; border: 1px solid hsl(0, 0%, 86%); padding: 10px;box-shadow: 2px 2px 0 hsl(0, 0%, 96%);"
:disabled="!canEditWindow">
<div style="display: flex; flex-direction: column;">
<div v-for="(item, index) in messengers" :key="index" style="display: flex; flex-direction: row; margin-top: 24px;">
<div style="min-width: 192px;align-content: center;">
<b-checkbox v-model="selectedStates[item.name]" :disabled="!canEditWindow">{{ item.name }}</b-checkbox>
</div>
<b-field label="Weight" label-position="on-border" :hidden="form.trafficType == 'duplicate'">
<b-input :maxlength="2" :max="10" :min="1" placeholder="Weight..." style="width: 108px;" v-model="itemValues[item.name]" :disabled="!canEditWindow" />
</b-field>
</div>
</div>
</b-field>
</div>
</div>

<div v-if="showError" style="color: red; padding-bottom: 24px;">
Please select at least one option and provide a weight between 1-1000
</div>

<b-field :label="$t('globals.terms.tags')" label-position="on-border">
<b-taginput v-model="form.tags" name="tags" :disabled="!canEdit" ellipsis icon="tag-outline"
Expand Down Expand Up @@ -342,6 +373,13 @@ export default Vue.extend({
isAttachModalOpen: false,
activeTab: 'campaign',
// Tracks checkbox states
selectedStates: {},
// Tracks number input values
itemValues: {},
showError: false,
submitted: false,
data: {},
// IDs from ?list_id query param.
Expand All @@ -354,7 +392,6 @@ export default Vue.extend({
subject: '',
headersStr: '[]',
headers: [],
messenger: 'email',
templateId: 0,
lists: [],
tags: [],
Expand Down Expand Up @@ -431,8 +468,38 @@ export default Vue.extend({
const archiveStr = `{"email": "[email protected]", "name": "${this.$t('globals.fields.name')}", "attribs": {}}`;
this.form.archiveMetaStr = this.$utils.getPref('campaign.archiveMetaStr') || JSON.stringify(JSON.parse(archiveStr), null, 4);
},
selectedMessengers() {
const filtered = this.messengers.filter((item) => this.selectedStates[item.name]);
return filtered.map((item) => {
const mrow = {
uuid: item.uuid,
name: item.name,
weight: parseInt(this.itemValues[item.name], 10) || 1,
};
return mrow;
});
},
onSubmit(typ) {
// type -> create | update | test
const messengers = this.selectedMessengers();
console.log(JSON.stringify(messengers));

Check warning on line 488 in frontend/src/views/Campaign.vue

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
if (messengers.length === 0) {
this.showError = true;
return;
}
if (this.form.trafficType === 'split') {
const outofbound = messengers.find((x) => !x.weight || x.weight < 1 || x.weight > 1000);
if (outofbound) {
this.showError = true;
return;
}
}
// Validate custom JSON headers.
if (this.form.headersStr && this.form.headersStr !== '[]') {
try {
Expand Down Expand Up @@ -478,6 +545,15 @@ export default Vue.extend({
getCampaign(id) {
return this.$api.getCampaign(id).then((data) => {
this.data = data;
console.log('init', JSON.stringify(this.selectedStates), JSON.stringify(this.itemValues));

Check warning on line 549 in frontend/src/views/Campaign.vue

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
JSON.parse(data.messenger).forEach((m) => {
this.selectedStates[m.name] = true;
this.itemValues[m.name] = m.weight;
});
console.log('updated', JSON.stringify(this.selectedStates), JSON.stringify(this.itemValues));

Check warning on line 556 in frontend/src/views/Campaign.vue

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
this.form = {
...this.form,
...data,
Expand Down Expand Up @@ -510,7 +586,7 @@ export default Vue.extend({
name: this.form.name,
subject: this.form.subject,
lists: this.form.lists.map((l) => l.id),
messenger: this.form.messenger,
messenger: JSON.stringify(this.selectedMessengers()),
type: 'regular',
headers: this.form.headers,
tags: this.form.tags,
Expand All @@ -520,6 +596,7 @@ export default Vue.extend({
altbody: this.form.content.contentType !== 'plain' ? this.form.altbody : null,
subscribers: this.form.testEmails,
media: this.form.media.map((m) => m.id),
traffic_type: this.form.trafficType || 'split',
};
this.$api.testCampaign(data).then(() => {
Expand All @@ -535,7 +612,7 @@ export default Vue.extend({
subject: this.form.subject,
lists: this.form.lists.map((l) => l.id),
content_type: 'richtext',
messenger: this.form.messenger,
messenger: JSON.stringify(this.selectedMessengers()),
type: 'regular',
tags: this.form.tags,
send_later: this.form.sendLater,
Expand All @@ -547,6 +624,7 @@ export default Vue.extend({
sliding_window_rate: this.form.slidingWindowRate || 1,
sliding_window_duration: this.form.slidingWindowDuration || '1h',
run_type: this.form.runType || 'list',
traffic_type: this.form.trafficType || 'split',
// body: this.form.body,
};
Expand All @@ -562,7 +640,7 @@ export default Vue.extend({
name: this.form.name,
subject: this.form.subject,
lists: this.form.lists.map((l) => l.id),
messenger: this.form.messenger,
messenger: JSON.stringify(this.selectedMessengers()),
type: 'regular',
tags: this.form.tags,
send_later: this.form.sendLater,
Expand All @@ -580,6 +658,7 @@ export default Vue.extend({
sliding_window_rate: this.form.slidingWindowRate || 1,
sliding_window_duration: this.form.slidingWindowDuration || '1h',
run_type: this.form.runType || 'list',
traffic_type: this.form.trafficType || 'split',
};
let typMsg = 'globals.messages.updated';
Expand Down Expand Up @@ -694,9 +773,17 @@ export default Vue.extend({
return this.lists.results.filter((l) => this.selListIDs.indexOf(l.id) > -1);
},
messengers() {
return [...this.serverConfig.messengers];
return this.serverConfig.messengers.map((item) => {
const row = {
uuid: item.uuid,
name: item.name,
selected: false,
weight: 1,
};
return row;
});
},
},
Expand Down Expand Up @@ -747,7 +834,7 @@ export default Vue.extend({
this.$api.getTemplates().then((data) => {
if (data.length > 0) {
if (!this.form.templateId) {
this.form.templateId = data.find((i) => i.isDefault === true).id;
this.form.templateId = data.find((i) => i.isDefault === true)?.id || data[0]?.id;
}
}
});
Expand All @@ -759,8 +846,6 @@ export default Vue.extend({
this.activeTab = this.$route.hash.replace('#', '');
}
});
} else {
this.form.messenger = 'email';
}
this.$nextTick(() => {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/views/Campaigns.vue
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,8 @@ export default Vue.extend({
sliding_window_rate: c.slidingWindowRate || 1,
sliding_window_duration: c.slidingWindowDuration || '1h',
run_type: c.runType,
messengers: c.messengers,
traffic_type: c.trafficType,
};
if (c.archive) {
Expand Down
4 changes: 3 additions & 1 deletion internal/core/campaigns.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ func (c *Core) CreateCampaign(o models.Campaign, listIDs []int, mediaIDs []int)
o.SlidingWindowRate,
o.SlidingWindowDuration,
o.RunType,
o.TrafficType,
); err != nil {
if err == sql.ErrNoRows {
return models.Campaign{}, echo.NewHTTPError(http.StatusBadRequest, c.i18n.T("campaigns.noSubs"))
Expand Down Expand Up @@ -233,7 +234,8 @@ func (c *Core) UpdateCampaign(id int, o models.Campaign, listIDs []int, mediaIDs
o.SlidingWindow,
o.SlidingWindowRate,
o.SlidingWindowDuration,
o.RunType)
o.RunType,
o.TrafficType)
if err != nil {
c.log.Printf("error updating campaign: %v", err)
return models.Campaign{}, echo.NewHTTPError(http.StatusInternalServerError,
Expand Down
Loading

0 comments on commit be07e91

Please sign in to comment.