mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 18:47:51 +00:00
feat: Add event subscription option to webhooks (#4540)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -23,7 +23,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def webhook_params
|
def webhook_params
|
||||||
params.require(:webhook).permit(:inbox_id, :url)
|
params.require(:webhook).permit(:inbox_id, :url, subscriptions: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_webhook
|
def fetch_webhook
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
margin-right: var(--space-small);
|
margin-right: var(--space-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.margin-bottom-small {
|
||||||
|
margin-bottom: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
.margin-right-smaller {
|
.margin-right-smaller {
|
||||||
margin-right: var(--space-smaller);
|
margin-right: var(--space-smaller);
|
||||||
}
|
}
|
||||||
|
|||||||
50
app/javascript/dashboard/components/widgets/ShowMore.vue
Normal file
50
app/javascript/dashboard/components/widgets/ShowMore.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
{{ textToBeDisplayed }}
|
||||||
|
<button class="show-more--button" @click="toggleShowMore">
|
||||||
|
{{ buttonLabel }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
default: 120,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showMore: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
textToBeDisplayed() {
|
||||||
|
if (this.showMore) {
|
||||||
|
return this.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.text.slice(0, this.limit) + '...';
|
||||||
|
},
|
||||||
|
buttonLabel() {
|
||||||
|
const i18nKey = !this.showMore ? 'SHOW_MORE' : 'SHOW_LESS';
|
||||||
|
return this.$t(`COMPONENTS.SHOW_MORE_BLOCK.${i18nKey}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleShowMore() {
|
||||||
|
this.showMore = !this.showMore;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.show-more--button {
|
||||||
|
color: var(--w-500);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,6 +2,29 @@
|
|||||||
"INTEGRATION_SETTINGS": {
|
"INTEGRATION_SETTINGS": {
|
||||||
"HEADER": "Integrations",
|
"HEADER": "Integrations",
|
||||||
"WEBHOOK": {
|
"WEBHOOK": {
|
||||||
|
"SUBSCRIBED_EVENTS": "Subscribed Events",
|
||||||
|
"FORM": {
|
||||||
|
"CANCEL": "Cancel",
|
||||||
|
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
|
||||||
|
"SUBSCRIPTIONS": {
|
||||||
|
"LABEL": "Events",
|
||||||
|
"EVENTS": {
|
||||||
|
"CONVERSATION_CREATED": "Conversation Created",
|
||||||
|
"CONVERSATION_STATUS_CHANGED": "Conversation Status Changed",
|
||||||
|
"CONVERSATION_UPDATED": "Conversation Updated",
|
||||||
|
"MESSAGE_CREATED": "Message created",
|
||||||
|
"MESSAGE_UPDATED": "Message updated",
|
||||||
|
"WEBWIDGET_TRIGGERED": "Live chat widget opened by the user"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"END_POINT": {
|
||||||
|
"LABEL": "Webhook URL",
|
||||||
|
"PLACEHOLDER": "Example: https://example/api/webhook",
|
||||||
|
"ERROR": "Please enter a valid URL"
|
||||||
|
},
|
||||||
|
"EDIT_SUBMIT": "Update webhook",
|
||||||
|
"ADD_SUBMIT": "Create webhook"
|
||||||
|
},
|
||||||
"TITLE": "Webhook",
|
"TITLE": "Webhook",
|
||||||
"CONFIGURE": "Configure",
|
"CONFIGURE": "Configure",
|
||||||
"HEADER": "Webhook settings",
|
"HEADER": "Webhook settings",
|
||||||
@@ -17,35 +40,16 @@
|
|||||||
"EDIT": {
|
"EDIT": {
|
||||||
"BUTTON_TEXT": "Edit",
|
"BUTTON_TEXT": "Edit",
|
||||||
"TITLE": "Edit webhook",
|
"TITLE": "Edit webhook",
|
||||||
"CANCEL": "Cancel",
|
|
||||||
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
|
|
||||||
"FORM": {
|
|
||||||
"END_POINT": {
|
|
||||||
"LABEL": "Webhook URL",
|
|
||||||
"PLACEHOLDER": "Example: https://example/api/webhook",
|
|
||||||
"ERROR": "Please enter a valid URL"
|
|
||||||
},
|
|
||||||
"SUBMIT": "Edit webhook"
|
|
||||||
},
|
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Webhook URL updated successfully",
|
"SUCCESS_MESSAGE": "Webhook configuration updated successfully",
|
||||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"CANCEL": "Cancel",
|
"CANCEL": "Cancel",
|
||||||
"TITLE": "Add new webhook",
|
"TITLE": "Add new webhook",
|
||||||
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
|
|
||||||
"FORM": {
|
|
||||||
"END_POINT": {
|
|
||||||
"LABEL": "Webhook URL",
|
|
||||||
"PLACEHOLDER": "Example: https://example/api/webhook",
|
|
||||||
"ERROR": "Please enter a valid URL"
|
|
||||||
},
|
|
||||||
"SUBMIT": "Create webhook"
|
|
||||||
},
|
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Webhook added successfully",
|
"SUCCESS_MESSAGE": "Webhook configuration added successfully",
|
||||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -57,16 +61,16 @@
|
|||||||
},
|
},
|
||||||
"CONFIRM": {
|
"CONFIRM": {
|
||||||
"TITLE": "Confirm Deletion",
|
"TITLE": "Confirm Deletion",
|
||||||
"MESSAGE": "Are you sure to delete ",
|
"MESSAGE": "Are you sure to delete the webhook? (%{webhookURL})",
|
||||||
"YES": "Yes, Delete ",
|
"YES": "Yes, Delete ",
|
||||||
"NO": "No, Keep it"
|
"NO": "No, Keep it"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SLACK": {
|
"SLACK": {
|
||||||
"HELP_TEXT" : {
|
"HELP_TEXT" : {
|
||||||
"TITLE": "Using Slack Integration",
|
"TITLE": "Using Slack Integration",
|
||||||
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
|
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
|
|||||||
@@ -127,6 +127,10 @@
|
|||||||
"BUTTON_TEXT": "Copy",
|
"BUTTON_TEXT": "Copy",
|
||||||
"COPY_SUCCESSFUL": "Code copied to clipboard successfully"
|
"COPY_SUCCESSFUL": "Code copied to clipboard successfully"
|
||||||
},
|
},
|
||||||
|
"SHOW_MORE_BLOCK": {
|
||||||
|
"SHOW_MORE": "Show More",
|
||||||
|
"SHOW_LESS": "Show Less"
|
||||||
|
},
|
||||||
"FILE_BUBBLE": {
|
"FILE_BUBBLE": {
|
||||||
"DOWNLOAD": "Download",
|
"DOWNLOAD": "Download",
|
||||||
"UPLOADING": "Uploading..."
|
"UPLOADING": "Uploading..."
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="column content-box">
|
|
||||||
<woot-modal-header
|
|
||||||
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.TITLE')"
|
|
||||||
/>
|
|
||||||
<form class="row" @submit.prevent="editWebhook">
|
|
||||||
<div class="medium-12 columns">
|
|
||||||
<label :class="{ error: $v.endPoint.$error }">
|
|
||||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.END_POINT.LABEL') }}
|
|
||||||
<input
|
|
||||||
v-model.trim="endPoint"
|
|
||||||
type="text"
|
|
||||||
name="endPoint"
|
|
||||||
:placeholder="
|
|
||||||
$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.END_POINT.PLACEHOLDER')
|
|
||||||
"
|
|
||||||
@input="$v.endPoint.$touch"
|
|
||||||
/>
|
|
||||||
<span v-if="$v.endPoint.$error" class="message">
|
|
||||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.END_POINT.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<div class="medium-12 columns">
|
|
||||||
<woot-button
|
|
||||||
:is-disabled="
|
|
||||||
$v.endPoint.$invalid || uiFlags.updatingItem || endPoint === url
|
|
||||||
"
|
|
||||||
:is-loading="uiFlags.updatingItem"
|
|
||||||
>
|
|
||||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.SUBMIT') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button class="button clear" @click.prevent="onClose">
|
|
||||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.CANCEL') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { required, url, minLength } from 'vuelidate/lib/validators';
|
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
onClose: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
alertMessage: '',
|
|
||||||
endPoint: this.url,
|
|
||||||
webhookId: this.id,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
validations: {
|
|
||||||
endPoint: {
|
|
||||||
required,
|
|
||||||
minLength: minLength(7),
|
|
||||||
url,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ uiFlags: 'webhooks/getUIFlags' }),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
resetForm() {
|
|
||||||
this.endPoint = '';
|
|
||||||
this.$v.endPoint.$reset();
|
|
||||||
},
|
|
||||||
async editWebhook() {
|
|
||||||
try {
|
|
||||||
await this.$store.dispatch('webhooks/update', {
|
|
||||||
webhook: { url: this.endPoint },
|
|
||||||
id: this.webhookId,
|
|
||||||
});
|
|
||||||
this.alertMessage = this.$t(
|
|
||||||
'INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.SUCCESS_MESSAGE'
|
|
||||||
);
|
|
||||||
this.resetForm();
|
|
||||||
this.onClose();
|
|
||||||
} catch (error) {
|
|
||||||
this.alertMessage =
|
|
||||||
error.response.data.message ||
|
|
||||||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE');
|
|
||||||
} finally {
|
|
||||||
this.showAlert(this.alertMessage);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
<template>
|
|
||||||
<modal :show.sync="show" :on-close="onClose" :close-on-backdrop-click="false">
|
|
||||||
<div class="column content-box">
|
|
||||||
<woot-modal-header
|
|
||||||
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.TITLE')"
|
|
||||||
:header-content="
|
|
||||||
useInstallationName(
|
|
||||||
$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.DESC'),
|
|
||||||
globalConfig.installationName
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<form class="row" @submit.prevent="addWebhook">
|
|
||||||
<div class="medium-12 columns">
|
|
||||||
<label :class="{ error: $v.endPoint.$error }">
|
|
||||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.END_POINT.LABEL') }}
|
|
||||||
<input
|
|
||||||
v-model.trim="endPoint"
|
|
||||||
type="text"
|
|
||||||
name="endPoint"
|
|
||||||
:placeholder="
|
|
||||||
$t(
|
|
||||||
'INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.END_POINT.PLACEHOLDER'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
@input="$v.endPoint.$touch"
|
|
||||||
/>
|
|
||||||
<span v-if="$v.endPoint.$error" class="message">
|
|
||||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.END_POINT.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<div class="medium-12 columns">
|
|
||||||
<woot-button
|
|
||||||
:disabled="$v.endPoint.$invalid || addWebHook.showLoading"
|
|
||||||
:is-loading="addWebHook.showLoading"
|
|
||||||
>
|
|
||||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.SUBMIT') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button class="button clear" @click.prevent="onClose">
|
|
||||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.CANCEL') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { required, url, minLength } from 'vuelidate/lib/validators';
|
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
import Modal from '../../../../components/Modal';
|
|
||||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Modal,
|
|
||||||
},
|
|
||||||
mixins: [alertMixin, globalConfigMixin],
|
|
||||||
props: {
|
|
||||||
onClose: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
endPoint: '',
|
|
||||||
addWebHook: {
|
|
||||||
showAlert: false,
|
|
||||||
showLoading: false,
|
|
||||||
},
|
|
||||||
show: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
|
||||||
},
|
|
||||||
validations: {
|
|
||||||
endPoint: {
|
|
||||||
required,
|
|
||||||
minLength: minLength(7),
|
|
||||||
url,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
resetForm() {
|
|
||||||
this.endPoint = '';
|
|
||||||
this.$v.endPoint.$reset();
|
|
||||||
},
|
|
||||||
async addWebhook() {
|
|
||||||
this.addWebHook.showLoading = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.$store.dispatch('webhooks/create', {
|
|
||||||
webhook: { url: this.endPoint },
|
|
||||||
});
|
|
||||||
this.addWebHook.showLoading = false;
|
|
||||||
|
|
||||||
this.addWebHook.message = this.$t(
|
|
||||||
'INTEGRATION_SETTINGS.WEBHOOK.ADD.API.SUCCESS_MESSAGE'
|
|
||||||
);
|
|
||||||
this.resetForm();
|
|
||||||
this.onClose();
|
|
||||||
} catch (error) {
|
|
||||||
this.addWebHook.showLoading = false;
|
|
||||||
this.addWebHook.message =
|
|
||||||
error.response.data.message ||
|
|
||||||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE');
|
|
||||||
} finally {
|
|
||||||
this.addWebHook.showLoading = false;
|
|
||||||
this.showAlert(this.addWebHook.message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<div class="column content-box">
|
||||||
|
<woot-modal-header
|
||||||
|
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.TITLE')"
|
||||||
|
/>
|
||||||
|
<webhook-form
|
||||||
|
:value="value"
|
||||||
|
:is-submitting="uiFlags.updatingItem"
|
||||||
|
:submit-label="$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.EDIT_SUBMIT')"
|
||||||
|
@submit="onSubmit"
|
||||||
|
@cancel="onClose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import WebhookForm from './WebhookForm.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { WebhookForm },
|
||||||
|
mixins: [alertMixin],
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
onClose: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ uiFlags: 'webhooks/getUIFlags' }),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async onSubmit(webhook) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('webhooks/update', {
|
||||||
|
webhook,
|
||||||
|
id: this.id,
|
||||||
|
});
|
||||||
|
this.showAlert(
|
||||||
|
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.SUCCESS_MESSAGE')
|
||||||
|
);
|
||||||
|
this.onClose();
|
||||||
|
} catch (error) {
|
||||||
|
const alertMessage =
|
||||||
|
error.response.data.message ||
|
||||||
|
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE');
|
||||||
|
this.showAlert(alertMessage);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
color-scheme="success"
|
color-scheme="success"
|
||||||
class-names="button--fixed-right-top"
|
class-names="button--fixed-right-top"
|
||||||
icon="add-circle"
|
icon="add-circle"
|
||||||
@click="openAddPopup()"
|
@click="openAddPopup"
|
||||||
>
|
>
|
||||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.HEADER_BTN_TXT') }}
|
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.HEADER_BTN_TXT') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
@@ -37,35 +37,14 @@
|
|||||||
</th>
|
</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(webHookItem, index) in records" :key="webHookItem.id">
|
<webhook-row
|
||||||
<td class="webhook-link">
|
v-for="(webHookItem, index) in records"
|
||||||
{{ webHookItem.url }}
|
:key="webHookItem.id"
|
||||||
</td>
|
:index="index"
|
||||||
<td class="button-wrapper">
|
:webhook="webHookItem"
|
||||||
<woot-button
|
@edit="openEditPopup"
|
||||||
v-tooltip.top="
|
@delete="openDeletePopup"
|
||||||
$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.BUTTON_TEXT')
|
/>
|
||||||
"
|
|
||||||
variant="smooth"
|
|
||||||
size="tiny"
|
|
||||||
color-scheme="secondary"
|
|
||||||
icon="edit"
|
|
||||||
@click="openEditPopup(webHookItem)"
|
|
||||||
>
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
v-tooltip.top="
|
|
||||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')
|
|
||||||
"
|
|
||||||
variant="smooth"
|
|
||||||
color-scheme="alert"
|
|
||||||
size="tiny"
|
|
||||||
icon="dismiss-circle"
|
|
||||||
@click="openDeletePopup(webHookItem, index)"
|
|
||||||
>
|
|
||||||
</woot-button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,24 +62,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||||
<new-webhook :on-close="hideAddPopup" />
|
<new-webhook v-if="showAddPopup" :on-close="hideAddPopup" />
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
|
|
||||||
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
||||||
<edit-webhook
|
<edit-webhook
|
||||||
v-if="showEditPopup"
|
v-if="showEditPopup"
|
||||||
:id="selectedWebHook.id"
|
:id="selectedWebHook.id"
|
||||||
:url="selectedWebHook.url"
|
:value="selectedWebHook"
|
||||||
:on-close="hideEditPopup"
|
:on-close="hideEditPopup"
|
||||||
/>
|
/>
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
|
|
||||||
<woot-delete-modal
|
<woot-delete-modal
|
||||||
:show.sync="showDeleteConfirmationPopup"
|
:show.sync="showDeleteConfirmationPopup"
|
||||||
:on-close="closeDeletePopup"
|
:on-close="closeDeletePopup"
|
||||||
:on-confirm="confirmDeletion"
|
:on-confirm="confirmDeletion"
|
||||||
:title="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.TITLE')"
|
:title="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.TITLE')"
|
||||||
:message="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.MESSAGE')"
|
:message="
|
||||||
|
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.MESSAGE', {
|
||||||
|
webhookURL: selectedWebHook.url,
|
||||||
|
})
|
||||||
|
"
|
||||||
:confirm-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.YES')"
|
:confirm-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.YES')"
|
||||||
:reject-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.NO')"
|
:reject-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.NO')"
|
||||||
/>
|
/>
|
||||||
@@ -112,11 +94,13 @@ import NewWebhook from './NewWebHook';
|
|||||||
import EditWebhook from './EditWebHook';
|
import EditWebhook from './EditWebHook';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||||
|
import WebhookRow from './WebhookRow';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NewWebhook,
|
NewWebhook,
|
||||||
EditWebhook,
|
EditWebhook,
|
||||||
|
WebhookRow,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, globalConfigMixin],
|
mixins: [alertMixin, globalConfigMixin],
|
||||||
data() {
|
data() {
|
||||||
@@ -179,11 +163,3 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
|
||||||
.webhook-link {
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
.button-wrapper button:nth-child(2) {
|
|
||||||
margin-left: var(--space-normal);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div class="column content-box">
|
||||||
|
<woot-modal-header
|
||||||
|
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.TITLE')"
|
||||||
|
:header-content="
|
||||||
|
useInstallationName(
|
||||||
|
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.DESC'),
|
||||||
|
globalConfig.installationName
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<webhook-form
|
||||||
|
:is-submitting="uiFlags.creatingItem"
|
||||||
|
:submit-label="$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.ADD_SUBMIT')"
|
||||||
|
@submit="onSubmit"
|
||||||
|
@cancel="onClose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import WebhookForm from './WebhookForm.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { WebhookForm },
|
||||||
|
mixins: [alertMixin, globalConfigMixin],
|
||||||
|
props: {
|
||||||
|
onClose: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
globalConfig: 'globalConfig/get',
|
||||||
|
uiFlags: 'webhooks/getUIFlags',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async onSubmit(webhook) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('webhooks/create', { webhook });
|
||||||
|
this.showAlert(
|
||||||
|
this.$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.API.SUCCESS_MESSAGE')
|
||||||
|
);
|
||||||
|
this.onClose();
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error.response.data.message ||
|
||||||
|
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE');
|
||||||
|
this.showAlert(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<form class="row" @submit.prevent="onSubmit">
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<label :class="{ error: $v.url.$error }">
|
||||||
|
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="url"
|
||||||
|
type="text"
|
||||||
|
name="url"
|
||||||
|
:placeholder="
|
||||||
|
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
@input="$v.url.$touch"
|
||||||
|
/>
|
||||||
|
<span v-if="$v.url.$error" class="message">
|
||||||
|
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.ERROR') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label :class="{ error: $v.url.$error }" class="margin-bottom-small">
|
||||||
|
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<div v-for="event in supportedWebhookEvents" :key="event">
|
||||||
|
<input
|
||||||
|
:id="event"
|
||||||
|
v-model="subscriptions"
|
||||||
|
type="checkbox"
|
||||||
|
:value="event"
|
||||||
|
name="subscriptions"
|
||||||
|
class="margin-right-small"
|
||||||
|
/>
|
||||||
|
<span class="fs-small">
|
||||||
|
{{ `${getEventLabel(event)} (${event})` }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<woot-button
|
||||||
|
:disabled="$v.$invalid || isSubmitting"
|
||||||
|
:is-loading="isSubmitting"
|
||||||
|
>
|
||||||
|
{{ submitLabel }}
|
||||||
|
</woot-button>
|
||||||
|
<woot-button class="button clear" @click.prevent="$emit('cancel')">
|
||||||
|
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.CANCEL') }}
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { required, url, minLength } from 'vuelidate/lib/validators';
|
||||||
|
import webhookMixin from './webhookMixin';
|
||||||
|
|
||||||
|
const SUPPORTED_WEBHOOK_EVENTS = [
|
||||||
|
'conversation_created',
|
||||||
|
'conversation_status_changed',
|
||||||
|
'conversation_updated',
|
||||||
|
'message_created',
|
||||||
|
'message_updated',
|
||||||
|
'webwidget_triggered',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [webhookMixin],
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
isSubmitting: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
submitLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validations: {
|
||||||
|
url: {
|
||||||
|
required,
|
||||||
|
minLength: minLength(7),
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
subscriptions: {
|
||||||
|
required,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
url: this.value.url || '',
|
||||||
|
subscriptions: this.value.subscriptions || [],
|
||||||
|
supportedWebhookEvents: SUPPORTED_WEBHOOK_EVENTS,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSubmit() {
|
||||||
|
this.$emit('submit', {
|
||||||
|
url: this.url,
|
||||||
|
subscriptions: this.subscriptions,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="webhook--link">{{ webhook.url }}</div>
|
||||||
|
<span class="webhook--subscribed-events">
|
||||||
|
<span class="webhook--subscribed-label">
|
||||||
|
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.SUBSCRIBED_EVENTS') }}:
|
||||||
|
</span>
|
||||||
|
<show-more :text="subscribedEvents" :limit="60" />
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="button-wrapper">
|
||||||
|
<woot-button
|
||||||
|
v-tooltip.top="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.BUTTON_TEXT')"
|
||||||
|
variant="smooth"
|
||||||
|
size="tiny"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="edit"
|
||||||
|
@click="$emit('edit', webhook)"
|
||||||
|
>
|
||||||
|
</woot-button>
|
||||||
|
<woot-button
|
||||||
|
v-tooltip.top="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')"
|
||||||
|
variant="smooth"
|
||||||
|
color-scheme="alert"
|
||||||
|
size="tiny"
|
||||||
|
icon="dismiss-circle"
|
||||||
|
@click="$emit('delete', webhook, index)"
|
||||||
|
>
|
||||||
|
</woot-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import webhookMixin from './webhookMixin';
|
||||||
|
import ShowMore from 'dashboard/components/widgets/ShowMore';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { ShowMore },
|
||||||
|
mixins: [webhookMixin],
|
||||||
|
props: {
|
||||||
|
webhook: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
index: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
subscribedEvents() {
|
||||||
|
const { subscriptions } = this.webhook;
|
||||||
|
return subscriptions.map(event => this.getEventLabel(event)).join(', ');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.webhook--link {
|
||||||
|
color: var(--s-700);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.webhook--subscribed-events {
|
||||||
|
color: var(--s-500);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
}
|
||||||
|
|
||||||
|
.webhook--subscribed-label {
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper {
|
||||||
|
max-width: var(--space-mega);
|
||||||
|
min-width: auto;
|
||||||
|
|
||||||
|
button:nth-child(2) {
|
||||||
|
margin-left: var(--space-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { createWrapper } from '@vue/test-utils';
|
||||||
|
import webhookMixin from '../webhookMixin';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
describe('webhookMixin', () => {
|
||||||
|
describe('#getEventLabel', () => {
|
||||||
|
it('returns correct i18n translation:', () => {
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
title: 'WebhookComponent',
|
||||||
|
mixins: [webhookMixin],
|
||||||
|
methods: {
|
||||||
|
$t(text) {
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const Constructor = Vue.extend(Component);
|
||||||
|
const vm = new Constructor().$mount();
|
||||||
|
const wrapper = createWrapper(vm);
|
||||||
|
expect(wrapper.vm.getEventLabel('message_created')).toEqual(
|
||||||
|
`INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.MESSAGE_CREATED`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
getEventLabel(event) {
|
||||||
|
const eventName = event.toUpperCase();
|
||||||
|
return this.$t(
|
||||||
|
`INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.${eventName}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import Index from './Index';
|
import Index from './Index';
|
||||||
import SettingsContent from '../Wrapper';
|
import SettingsContent from '../Wrapper';
|
||||||
import Webhook from './Webhook';
|
import Webhook from './Webhooks/Index';
|
||||||
import ShowIntegration from './ShowIntegration';
|
import ShowIntegration from './ShowIntegration';
|
||||||
import { frontendURL } from '../../../../helper/URLHelper';
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const state = {
|
|||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
getWebhooks(_state) {
|
getWebhooks(_state) {
|
||||||
return _state.records;
|
return _state.records.sort((w1, w2) => w1.id - w2.id);
|
||||||
},
|
},
|
||||||
getUIFlags(_state) {
|
getUIFlags(_state) {
|
||||||
return _state.uiFlags;
|
return _state.uiFlags;
|
||||||
|
|||||||
@@ -1,22 +1,4 @@
|
|||||||
class WebhookListener < BaseListener
|
class WebhookListener < BaseListener
|
||||||
# FIXME: deprecate the opened and resolved events in future in favor of status changed event.
|
|
||||||
def conversation_resolved(event)
|
|
||||||
conversation = extract_conversation_and_account(event)[0]
|
|
||||||
changed_attributes = extract_changed_attributes(event)
|
|
||||||
inbox = conversation.inbox
|
|
||||||
payload = conversation.webhook_data.merge(event: __method__.to_s, changed_attributes: changed_attributes)
|
|
||||||
deliver_webhook_payloads(payload, inbox)
|
|
||||||
end
|
|
||||||
|
|
||||||
# FIXME: deprecate the opened and resolved events in future in favor of status changed event.
|
|
||||||
def conversation_opened(event)
|
|
||||||
conversation = extract_conversation_and_account(event)[0]
|
|
||||||
changed_attributes = extract_changed_attributes(event)
|
|
||||||
inbox = conversation.inbox
|
|
||||||
payload = conversation.webhook_data.merge(event: __method__.to_s, changed_attributes: changed_attributes)
|
|
||||||
deliver_webhook_payloads(payload, inbox)
|
|
||||||
end
|
|
||||||
|
|
||||||
def conversation_status_changed(event)
|
def conversation_status_changed(event)
|
||||||
conversation = extract_conversation_and_account(event)[0]
|
conversation = extract_conversation_and_account(event)[0]
|
||||||
changed_attributes = extract_changed_attributes(event)
|
changed_attributes = extract_changed_attributes(event)
|
||||||
@@ -71,15 +53,23 @@ class WebhookListener < BaseListener
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def deliver_webhook_payloads(payload, inbox)
|
def deliver_account_webhooks(payload, inbox)
|
||||||
# Account webhooks
|
|
||||||
inbox.account.webhooks.account.each do |webhook|
|
inbox.account.webhooks.account.each do |webhook|
|
||||||
|
next unless webhook.subscriptions.include?(payload[:event])
|
||||||
|
|
||||||
WebhookJob.perform_later(webhook.url, payload)
|
WebhookJob.perform_later(webhook.url, payload)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def deliver_api_inbox_webhooks(payload, inbox)
|
||||||
return unless inbox.channel_type == 'Channel::Api'
|
return unless inbox.channel_type == 'Channel::Api'
|
||||||
return if inbox.channel.webhook_url.blank?
|
return if inbox.channel.webhook_url.blank?
|
||||||
|
|
||||||
WebhookJob.perform_later(inbox.channel.webhook_url, payload)
|
WebhookJob.perform_later(inbox.channel.webhook_url, payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def deliver_webhook_payloads(payload, inbox)
|
||||||
|
deliver_account_webhooks(payload, inbox)
|
||||||
|
deliver_api_inbox_webhooks(payload, inbox)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
#
|
#
|
||||||
# Table name: webhooks
|
# Table name: webhooks
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# url :string
|
# subscriptions :jsonb
|
||||||
# webhook_type :integer default("account")
|
# url :string
|
||||||
# created_at :datetime not null
|
# webhook_type :integer default("account")
|
||||||
# updated_at :datetime not null
|
# created_at :datetime not null
|
||||||
# account_id :integer
|
# updated_at :datetime not null
|
||||||
# inbox_id :integer
|
# account_id :integer
|
||||||
|
# inbox_id :integer
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
@@ -21,6 +22,18 @@ class Webhook < ApplicationRecord
|
|||||||
|
|
||||||
validates :account_id, presence: true
|
validates :account_id, presence: true
|
||||||
validates :url, uniqueness: { scope: [:account_id] }, format: URI::DEFAULT_PARSER.make_regexp(%w[http https])
|
validates :url, uniqueness: { scope: [:account_id] }, format: URI::DEFAULT_PARSER.make_regexp(%w[http https])
|
||||||
|
validate :validate_webhook_subscriptions
|
||||||
enum webhook_type: { account: 0, inbox: 1 }
|
enum webhook_type: { account: 0, inbox: 1 }
|
||||||
|
|
||||||
|
ALLOWED_WEBHOOK_EVENTS = %w[conversation_status_changed conversation_updated conversation_created message_created message_updated
|
||||||
|
webwidget_triggered].freeze
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate_webhook_subscriptions
|
||||||
|
invalid_subscriptions = !subscriptions.instance_of?(Array) ||
|
||||||
|
subscriptions.blank? ||
|
||||||
|
(subscriptions.uniq - ALLOWED_WEBHOOK_EVENTS).length.positive?
|
||||||
|
errors.add(:subscriptions, I18n.t('errors.webhook.invalid')) if invalid_subscriptions
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
json.id webhook.id
|
json.id webhook.id
|
||||||
json.url webhook.url
|
json.url webhook.url
|
||||||
json.account_id webhook.account_id
|
json.account_id webhook.account_id
|
||||||
|
json.subscriptions webhook.subscriptions
|
||||||
if webhook.inbox
|
if webhook.inbox
|
||||||
json.inbox do
|
json.inbox do
|
||||||
json.id webhook.inbox.id
|
json.id webhook.inbox.id
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ en:
|
|||||||
reset_password_failure: Uh ho! We could not find any user with the specified email.
|
reset_password_failure: Uh ho! We could not find any user with the specified email.
|
||||||
|
|
||||||
errors:
|
errors:
|
||||||
|
webhook:
|
||||||
|
invalid: Invalid events
|
||||||
signup:
|
signup:
|
||||||
disposable_email: We do not allow disposable emails
|
disposable_email: We do not allow disposable emails
|
||||||
invalid_email: You have entered an invalid email
|
invalid_email: You have entered an invalid email
|
||||||
|
|||||||
12
db/migrate/20220424081117_add_subscriptions_to_webhooks.rb
Normal file
12
db/migrate/20220424081117_add_subscriptions_to_webhooks.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
class AddSubscriptionsToWebhooks < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :webhooks, :subscriptions, :jsonb, default: %w[
|
||||||
|
conversation_status_changed
|
||||||
|
conversation_updated
|
||||||
|
conversation_created
|
||||||
|
message_created
|
||||||
|
message_updated
|
||||||
|
webwidget_triggered
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2022_04_18_094715) do
|
ActiveRecord::Schema.define(version: 2022_04_24_081117) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
@@ -763,6 +763,7 @@ ActiveRecord::Schema.define(version: 2022_04_18_094715) do
|
|||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
t.integer "webhook_type", default: 0
|
t.integer "webhook_type", default: 0
|
||||||
|
t.jsonb "subscriptions", default: ["conversation_status_changed", "conversation_updated", "conversation_created", "message_created", "message_updated", "webwidget_triggered"]
|
||||||
t.index ["account_id", "url"], name: "index_webhooks_on_account_id_and_url", unique: true
|
t.index ["account_id", "url"], name: "index_webhooks_on_account_id_and_url", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,36 @@ RSpec.describe 'Webhooks API', type: :request do
|
|||||||
expect(response).to have_http_status(:unprocessable_entity)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
expect(JSON.parse(response.body)['message']).to eql 'Url is invalid'
|
expect(JSON.parse(response.body)['message']).to eql 'Url is invalid'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'throws error if subscription events are invalid' do
|
||||||
|
post "/api/v1/accounts/#{account.id}/webhooks",
|
||||||
|
params: { url: 'https://hello.com', subscriptions: ['conversation_random_event'] },
|
||||||
|
headers: administrator.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(JSON.parse(response.body)['message']).to eql 'Subscriptions Invalid events'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'throws error if subscription events are empty' do
|
||||||
|
post "/api/v1/accounts/#{account.id}/webhooks",
|
||||||
|
params: { url: 'https://hello.com', subscriptions: [] },
|
||||||
|
headers: administrator.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(JSON.parse(response.body)['message']).to eql 'Subscriptions Invalid events'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'use default if subscription events are nil' do
|
||||||
|
post "/api/v1/accounts/#{account.id}/webhooks",
|
||||||
|
params: { url: 'https://hello.com', subscriptions: nil },
|
||||||
|
headers: administrator.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(
|
||||||
|
JSON.parse(response.body)['payload']['webhook']['subscriptions']
|
||||||
|
).to eql %w[conversation_status_changed conversation_updated conversation_created message_created message_updated
|
||||||
|
webwidget_triggered]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,15 @@ FactoryBot.define do
|
|||||||
account_id { 1 }
|
account_id { 1 }
|
||||||
inbox_id { 1 }
|
inbox_id { 1 }
|
||||||
url { 'https://api.chatwoot.com' }
|
url { 'https://api.chatwoot.com' }
|
||||||
|
subscriptions do
|
||||||
|
%w[
|
||||||
|
conversation_status_changed
|
||||||
|
conversation_updated
|
||||||
|
conversation_created
|
||||||
|
message_created
|
||||||
|
message_updated
|
||||||
|
webwidget_triggered
|
||||||
|
]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,14 +23,22 @@ describe WebhookListener do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when webhook is configured' do
|
context 'when webhook is configured and event is subscribed' do
|
||||||
it 'triggers webhook' do
|
it 'triggers the webhook event' do
|
||||||
webhook = create(:webhook, inbox: inbox, account: account)
|
webhook = create(:webhook, inbox: inbox, account: account)
|
||||||
expect(WebhookJob).to receive(:perform_later).with(webhook.url, message.webhook_data.merge(event: 'message_created')).once
|
expect(WebhookJob).to receive(:perform_later).with(webhook.url, message.webhook_data.merge(event: 'message_created')).once
|
||||||
listener.message_created(message_created_event)
|
listener.message_created(message_created_event)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when webhook is configured and event is not subscribed' do
|
||||||
|
it 'does not trigger the webhook event' do
|
||||||
|
create(:webhook, subscriptions: ['conversation_created'], inbox: inbox, account: account)
|
||||||
|
expect(WebhookJob).not_to receive(:perform_later)
|
||||||
|
listener.message_created(message_created_event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when inbox is an API Channel' do
|
context 'when inbox is an API Channel' do
|
||||||
it 'triggers webhook if webhook_url is present' do
|
it 'triggers webhook if webhook_url is present' do
|
||||||
channel_api = create(:channel_api, account: account)
|
channel_api = create(:channel_api, account: account)
|
||||||
@@ -106,36 +114,6 @@ describe WebhookListener do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#conversation_resolved' do
|
|
||||||
let!(:conversation_resolved_event) do
|
|
||||||
Events::Base.new(event_name, Time.zone.now, conversation: conversation.reload, changed_attributes: { status: [:open, :resolved] })
|
|
||||||
end
|
|
||||||
let(:event_name) { :'conversation.resolved' }
|
|
||||||
|
|
||||||
context 'when webhook is not configured' do
|
|
||||||
it 'does not trigger webhook' do
|
|
||||||
expect(WebhookJob).to receive(:perform_later).exactly(0).times
|
|
||||||
listener.conversation_resolved(conversation_resolved_event)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when webhook is configured' do
|
|
||||||
it 'triggers webhook' do
|
|
||||||
webhook = create(:webhook, inbox: inbox, account: account)
|
|
||||||
|
|
||||||
conversation.update(status: :resolved)
|
|
||||||
|
|
||||||
expect(WebhookJob).to receive(:perform_later).with(webhook.url,
|
|
||||||
conversation.webhook_data.merge(event: 'conversation_resolved',
|
|
||||||
changed_attributes: [{ status: {
|
|
||||||
current_value: :resolved, previous_value: :open
|
|
||||||
} }])).once
|
|
||||||
|
|
||||||
listener.conversation_resolved(conversation_resolved_event)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#conversation_updated' do
|
describe '#conversation_updated' do
|
||||||
let(:custom_attributes) { { test: nil } }
|
let(:custom_attributes) { { test: nil } }
|
||||||
let!(:conversation_updated_event) do
|
let!(:conversation_updated_event) do
|
||||||
|
|||||||
Reference in New Issue
Block a user