Merge remote-tracking branch 'origin/develop' into captain/pdf_support_be

This commit is contained in:
Tanmay Sharma
2025-08-12 12:15:34 +05:30
83 changed files with 554 additions and 260 deletions
@@ -1,4 +1,11 @@
class Platform::Api::V1::AccountsController < PlatformController
def index
@resources = @platform_app.platform_app_permissibles
.where(permissible_type: 'Account')
.includes(:permissible)
.map(&:permissible)
end
def show; end
def create
-1
View File
@@ -137,7 +137,6 @@ export default {
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
id="app"
class="flex flex-col w-full h-screen min-h-0"
:class="{ 'app-rtl--wrapper': isRTL }"
:dir="isRTL ? 'rtl' : 'ltr'"
>
<UpdateBanner :latest-chatwoot-version="latestChatwootVersion" />
@@ -315,7 +315,9 @@ const handleAvatarDelete = () => {
class="[&>div>button:not(.focused)]:!outline-n-weak"
/>
</div>
<div class="flex items-start justify-between w-full gap-2">
<div
class="grid items-start justify-between w-full gap-2 grid-cols-[200px,1fr]"
>
<label
class="text-sm font-medium whitespace-nowrap py-2.5 text-n-slate-12"
>
@@ -170,7 +170,7 @@ useKeyboardEvents(keyboardEvents);
/>
<EmojiInput
v-if="isEmojiPickerOpen"
class="left-0 top-full mt-1.5"
class="ltr:left-0 rtl:right-0 top-full mt-1.5"
:on-click="onClickInsertEmoji"
/>
</div>
@@ -57,7 +57,7 @@ const removeAttachment = id => {
variant="ghost"
icon="i-lucide-trash"
color="slate"
class="absolute top-1 right-1 !w-5 !h-5 transition-opacity duration-150 ease-in-out opacity-0 group-hover/image:opacity-100"
class="absolute top-1 ltr:right-1 rtl:left-1 !w-5 !h-5 transition-opacity duration-150 ease-in-out opacity-0 group-hover/image:opacity-100"
@click="removeAttachment(attachment.resource.id)"
/>
</div>
@@ -83,7 +83,7 @@ const targetInboxLabel = computed(() => {
<DropdownMenu
v-if="contactableInboxesList?.length > 0 && showInboxesDropdown"
:menu-items="contactableInboxesList"
class="left-0 z-[100] top-8 overflow-y-auto max-h-60 w-fit max-w-sm dark:!outline-n-slate-5"
class="ltr:left-0 rtl:right-0 z-[100] top-8 overflow-y-auto max-h-60 w-fit max-w-sm dark:!outline-n-slate-5"
@action="emit('handleInboxAction', $event)"
/>
</div>
@@ -2,6 +2,7 @@
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import WhatsappTemplateParser from './WhatsappTemplateParser.vue';
@@ -84,10 +85,13 @@ const handleSendMessage = template => {
/>
<div
v-if="showTemplatesMenu"
class="absolute top-full mt-1.5 max-h-96 overflow-y-auto left-0 flex flex-col gap-2 p-4 items-center w-[21.875rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
class="absolute top-full mt-1.5 max-h-96 overflow-y-auto ltr:left-0 rtl:right-0 flex flex-col gap-2 p-4 items-center w-[21.875rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
>
<div class="relative w-full">
<span class="absolute i-lucide-search size-3.5 top-2 left-3" />
<Icon
icon="i-lucide-search"
class="absolute size-3.5 top-2 ltr:left-3 rtl:right-3"
/>
<input
v-model="searchQuery"
type="search"
@@ -96,7 +100,7 @@ const handleSendMessage = template => {
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.SEARCH_PLACEHOLDER'
)
"
class="w-full h-8 py-2 pl-10 pr-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
class="w-full h-8 py-2 ltr:pl-10 rtl:pr-10 ltr:pr-2 rtl:pl-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
/>
</div>
<div
@@ -106,7 +106,7 @@ onMounted(() => {
<template>
<div
class="absolute top-full mt-1.5 max-h-[30rem] overflow-y-auto left-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[28.75rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
class="absolute top-full mt-1.5 max-h-[30rem] overflow-y-auto ltr:left-0 rtl:right-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[28.75rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
>
<span class="text-sm text-n-slate-12">
{{
@@ -138,7 +138,8 @@ onMounted(() => {
class="flex items-center w-full gap-2"
>
<span
class="flex items-center h-8 text-sm min-w-6 ltr:text-left rtl:text-right text-n-slate-10"
class="block h-8 text-sm min-w-6 text-start truncate text-n-slate-10 leading-8"
:title="key"
>
{{ key }}
</span>
@@ -207,7 +207,7 @@ watch(
<!-- Delete Avatar Button -->
<div
v-if="src && allowUpload"
class="absolute z-20 flex items-center justify-center invisible w-6 h-6 transition-all duration-300 ease-in-out opacity-0 cursor-pointer outline outline-1 outline-n-container -top-2 -right-2 rounded-xl bg-n-solid-3 group-hover/avatar:visible group-hover/avatar:opacity-100"
class="absolute z-20 flex items-center justify-center invisible w-6 h-6 transition-all duration-300 ease-in-out opacity-0 cursor-pointer outline outline-1 outline-n-container -top-2 ltr:-right-2 rtl:-left-2 rounded-xl bg-n-solid-3 group-hover/avatar:visible group-hover/avatar:opacity-100"
@click="handleDismiss"
>
<Icon icon="i-lucide-x" class="text-n-slate-11 size-4" />
@@ -303,26 +303,17 @@ watch(
<input
v-model="state.features.conversationFaqs"
type="checkbox"
class="form-checkbox"
/>
{{
t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS')
}}
</label>
<label class="flex items-center gap-2">
<input
v-model="state.features.memories"
type="checkbox"
class="form-checkbox"
/>
<input v-model="state.features.memories" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
</label>
<label class="flex items-center gap-2">
<input
v-model="state.features.citations"
type="checkbox"
class="form-checkbox"
/>
<input v-model="state.features.citations" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CITATIONS') }}
</label>
</div>
@@ -126,27 +126,15 @@ watch(
</label>
<div class="flex flex-col gap-2">
<label class="flex items-center gap-2">
<input
v-model="state.features.conversationFaqs"
type="checkbox"
class="form-checkbox"
/>
<input v-model="state.features.conversationFaqs" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS') }}
</label>
<label class="flex items-center gap-2">
<input
v-model="state.features.memories"
type="checkbox"
class="form-checkbox"
/>
<input v-model="state.features.memories" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
</label>
<label class="flex items-center gap-2">
<input
v-model="state.features.citations"
type="checkbox"
class="form-checkbox"
/>
<input v-model="state.features.citations" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CITATIONS') }}
</label>
</div>
@@ -218,8 +218,8 @@ onMounted(async () => {
left: 0;
}
.app-rtl--wrapper .sidebar-group-children > .child-item:last-child::after,
.app-rtl--wrapper
#app[dir='rtl'] .sidebar-group-children > .child-item:last-child::after,
#app[dir='rtl']
.sidebar-group-children
> *:last-child
> *:last-child
@@ -230,7 +230,7 @@ const handleBlur = e => emit('blur', e);
v-if="showDropdownMenu"
:menu-items="filteredMenuItems"
:is-searching="isLoading"
class="left-0 z-[100] top-8 overflow-y-auto max-h-60 w-[inherit] max-w-md dark:!outline-n-slate-5"
class="ltr:left-0 rtl:right-0 z-[100] top-8 overflow-y-auto max-h-60 w-[inherit] max-w-md dark:!outline-n-slate-5"
@action="handleDropdownAction"
/>
</div>
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "رابط الصفحة الرئيسية للبوابة",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Enllaç a la pàgina d'inici del portal",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Link til portalens hjemmeside",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Snegl",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Link zur Startseite des Portals",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Σύνδεσμος αρχικής σελίδας πύλης",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Enlace de página de inicio del portal",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "پیوند صفحه اصلی پورتال",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Lien vers la page d'accueil du portail",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -2,13 +2,13 @@
"AGENT_BOTS": {
"HEADER": "בוטים",
"LOADING_EDITOR": "Loading editor...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"DESCRIPTION": "בוטים של סוכנים הם כמו החברים הכי נפלאים בצוות שלכם. הם יכולים להתמודד עם הדברים הקטנים, כך שאתם יכולים להתמקד בדברים החשובים. נסו אותם. אתם יכולים לנהל את הבוטים שלכם מדף זה או ליצור חדשים באמצעות כפתור 'הוסף בוט'.",
"LEARN_MORE": "Learn about agent bots",
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT": "בוט מערכת",
"GLOBAL_BOT_BADGE": "System",
"AVATAR": {
"SUCCESS_DELETE": "Bot avatar deleted successfully",
"ERROR_DELETE": "Error deleting bot avatar, please try again"
"SUCCESS_DELETE": "בוט אווטר נמחק בהצלחה",
"ERROR_DELETE": "תקלה במחיקת בוט אווטר, יש לנסות שוב"
},
"BOT_CONFIGURATION": {
"TITLE": "בחר סוכן בוט",
@@ -22,7 +22,7 @@
"SELECT_PLACEHOLDER": "Select bot"
},
"ADD": {
"TITLE": "Add Bot",
"TITLE": "הוסף בוט",
"CANCEL_BUTTON_TEXT": "ביטול",
"API": {
"SUCCESS_MESSAGE": "הבוט התווסף בהצלחה.",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "קישור לדף הבית של הפורטל",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "שבלול",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portál főoldal link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Tautan halaman utama portal",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Link della pagina iniziale del portale",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "ホームページリンク",
"PLACEHOLDER": "ポータルホームページリンク",
"ERROR": "無効なURLです。ホームページリンクは「http://」または「https://」で始まる必要があります。"
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "スラッグ",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portalo nuoroda į pagrindinį puslapį",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Mājas lapas saite",
"PLACEHOLDER": "Portāla mājaslapas saite",
"ERROR": "Nederīgs URL. Sākumlapas saitei jāsākas ar “http://” vai “https://."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Link do strony głównej portalu",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Link da página inicial do portal",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Link da Página Inicial",
"PLACEHOLDER": "Link da página inicial do portal",
"ERROR": "URL inválida. O link da página inicial deve começar com 'http://' ou 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Link pagină portal",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Ссылка на домашнюю страницу",
"PLACEHOLDER": "Ссылка на главную страницу портала",
"ERROR": "Неправильный URL-адрес. Ссылка на главную страницу должна начинаться с 'http://' или 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Метка",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Veza do početne stranice portala",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal ana sayfa bağlantısı",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Посилання на головну сторінку порталу",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Мітка",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Liên kế trang chủ cổng thông tin",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "主页链接",
"PLACEHOLDER": "门户主页链接",
"ERROR": "无效的URL。主页链接必须以 'http://' 'https://' 开头。"
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
+17 -15
View File
@@ -2,24 +2,26 @@
#
# Table name: account_users
#
# id :bigint not null, primary key
# active_at :datetime
# auto_offline :boolean default(TRUE), not null
# availability :integer default("online"), not null
# role :integer default("agent")
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint
# custom_role_id :bigint
# inviter_id :bigint
# user_id :bigint
# id :bigint not null, primary key
# active_at :datetime
# auto_offline :boolean default(TRUE), not null
# availability :integer default("online"), not null
# role :integer default("agent")
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint
# agent_capacity_policy_id :bigint
# custom_role_id :bigint
# inviter_id :bigint
# user_id :bigint
#
# Indexes
#
# index_account_users_on_account_id (account_id)
# index_account_users_on_custom_role_id (custom_role_id)
# index_account_users_on_user_id (user_id)
# uniq_user_id_per_account_id (account_id,user_id) UNIQUE
# index_account_users_on_account_id (account_id)
# index_account_users_on_agent_capacity_policy_id (agent_capacity_policy_id)
# index_account_users_on_custom_role_id (custom_role_id)
# index_account_users_on_user_id (user_id)
# uniq_user_id_per_account_id (account_id,user_id) UNIQUE
#
class AccountUser < ApplicationRecord
@@ -0,0 +1,3 @@
json.array! @resources do |account|
json.partial! 'platform/api/v1/models/account', formats: [:json], resource: account
end
+1 -1
View File
@@ -425,7 +425,7 @@ Rails.application.routes.draw do
resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
delete :avatar, on: :member
end
resources :accounts, only: [:create, :show, :update, :destroy] do
resources :accounts, only: [:index, :create, :show, :update, :destroy] do
resources :account_users, only: [:index, :create] do
collection do
delete :destroy
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class CreateAssignmentPolicies < ActiveRecord::Migration[7.1]
def change
create_table :assignment_policies do |t|
t.references :account, null: false, index: true
t.string :name, null: false, limit: 255
t.text :description
t.integer :assignment_order, null: false, default: 0 # 0: round_robin, 1: balanced
t.integer :conversation_priority, null: false, default: 0 # 0: earliest_created, 1: longest_waiting
t.integer :fair_distribution_limit, null: false, default: 100
t.integer :fair_distribution_window, null: false, default: 3600 # seconds
t.boolean :enabled, null: false, default: true
t.timestamps
end
add_index :assignment_policies, [:account_id, :name], unique: true
add_index :assignment_policies, :enabled
end
end
@@ -0,0 +1,12 @@
# frozen_string_literal: true
class CreateInboxAssignmentPolicies < ActiveRecord::Migration[7.1]
def change
create_table :inbox_assignment_policies do |t|
t.references :inbox, null: false, index: { unique: true }
t.references :assignment_policy, null: false, index: true
t.timestamps
end
end
end
@@ -0,0 +1,14 @@
# frozen_string_literal: true
class CreateAgentCapacityPolicies < ActiveRecord::Migration[7.1]
def change
create_table :agent_capacity_policies do |t|
t.references :account, null: false, index: true
t.string :name, null: false, limit: 255
t.text :description
t.jsonb :exclusion_rules, default: {}, null: false
t.timestamps
end
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateInboxCapacityLimits < ActiveRecord::Migration[7.1]
def change
create_table :inbox_capacity_limits do |t|
t.references :agent_capacity_policy, null: false, index: true
t.references :inbox, null: false, index: true
t.integer :conversation_limit, null: false
t.timestamps
end
add_index :inbox_capacity_limits, [:agent_capacity_policy_id, :inbox_id], unique: true
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddAgentCapacityPolicyToAccountUsers < ActiveRecord::Migration[7.1]
def change
add_reference :account_users, :agent_capacity_policy, null: true, index: true
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class CreateLeaves < ActiveRecord::Migration[7.1]
def change
create_table :leaves do |t|
t.references :account, null: false
t.references :user, null: false
t.date :start_date, null: false
t.date :end_date, null: false
t.integer :leave_type, null: false, default: 0
t.integer :status, null: false, default: 0
t.text :reason
t.references :approved_by
t.datetime :approved_at
t.timestamps
end
add_index :leaves, [:account_id, :status]
end
end
+66
View File
@@ -39,8 +39,10 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
t.integer "availability", default: 0, null: false
t.boolean "auto_offline", default: true, null: false
t.bigint "custom_role_id"
t.bigint "agent_capacity_policy_id"
t.index ["account_id", "user_id"], name: "uniq_user_id_per_account_id", unique: true
t.index ["account_id"], name: "index_account_users_on_account_id"
t.index ["agent_capacity_policy_id"], name: "index_account_users_on_agent_capacity_policy_id"
t.index ["custom_role_id"], name: "index_account_users_on_custom_role_id"
t.index ["user_id"], name: "index_account_users_on_user_id"
end
@@ -120,6 +122,16 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
t.index ["account_id"], name: "index_agent_bots_on_account_id"
end
create_table "agent_capacity_policies", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "name", limit: 255, null: false
t.text "description"
t.jsonb "exclusion_rules", default: {}, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_agent_capacity_policies_on_account_id"
end
create_table "applied_slas", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "sla_policy_id", null: false
@@ -169,6 +181,22 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
t.index ["views"], name: "index_articles_on_views"
end
create_table "assignment_policies", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "name", limit: 255, null: false
t.text "description"
t.integer "assignment_order", default: 0, null: false
t.integer "conversation_priority", default: 0, null: false
t.integer "fair_distribution_limit", default: 100, null: false
t.integer "fair_distribution_window", default: 3600, null: false
t.boolean "enabled", default: true, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "name"], name: "index_assignment_policies_on_account_id_and_name", unique: true
t.index ["account_id"], name: "index_assignment_policies_on_account_id"
t.index ["enabled"], name: "index_assignment_policies_on_enabled"
end
create_table "attachments", id: :serial, force: :cascade do |t|
t.integer "file_type", default: 0
t.string "external_url"
@@ -729,6 +757,26 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
t.datetime "updated_at", null: false
end
create_table "inbox_assignment_policies", force: :cascade do |t|
t.bigint "inbox_id", null: false
t.bigint "assignment_policy_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["assignment_policy_id"], name: "index_inbox_assignment_policies_on_assignment_policy_id"
t.index ["inbox_id"], name: "index_inbox_assignment_policies_on_inbox_id", unique: true
end
create_table "inbox_capacity_limits", force: :cascade do |t|
t.bigint "agent_capacity_policy_id", null: false
t.bigint "inbox_id", null: false
t.integer "conversation_limit", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["agent_capacity_policy_id", "inbox_id"], name: "idx_on_agent_capacity_policy_id_inbox_id_71c7ec4caf", unique: true
t.index ["agent_capacity_policy_id"], name: "index_inbox_capacity_limits_on_agent_capacity_policy_id"
t.index ["inbox_id"], name: "index_inbox_capacity_limits_on_inbox_id"
end
create_table "inbox_members", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "inbox_id", null: false
@@ -801,6 +849,24 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
t.index ["title", "account_id"], name: "index_labels_on_title_and_account_id", unique: true
end
create_table "leaves", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "user_id", null: false
t.date "start_date", null: false
t.date "end_date", null: false
t.integer "leave_type", default: 0, null: false
t.integer "status", default: 0, null: false
t.text "reason"
t.bigint "approved_by_id"
t.datetime "approved_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "status"], name: "index_leaves_on_account_id_and_status"
t.index ["account_id"], name: "index_leaves_on_account_id"
t.index ["approved_by_id"], name: "index_leaves_on_approved_by_id"
t.index ["user_id"], name: "index_leaves_on_user_id"
end
create_table "macros", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "name", null: false
@@ -17,40 +17,34 @@ describe Messages::Instagram::MessageBuilder do
let!(:shared_reel_params) { build(:instagram_shared_reel_event).with_indifferent_access }
let!(:instagram_story_reply_event) { build(:instagram_story_reply_event).with_indifferent_access }
let!(:instagram_message_reply_event) { build(:instagram_message_reply_event).with_indifferent_access }
let!(:contact) { create(:contact, id: 'Sender-id-1', name: 'Jane Dae') }
let!(:contact_inbox) { create(:contact_inbox, contact_id: contact.id, inbox_id: instagram_inbox.id, source_id: 'Sender-id-1') }
let(:conversation) do
create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id)
end
let(:message) do
create(:message, account_id: account.id, inbox_id: instagram_inbox.id, conversation_id: conversation.id, message_type: 'outgoing',
source_id: 'message-id-1')
end
describe '#perform' do
before do
instagram_channel.update(access_token: 'valid_instagram_token')
stub_request(:get, %r{https://graph\.instagram\.com/.*?/Sender-id-1\?.*})
stub_request(:get, %r{https://graph\.instagram\.com/.*?/Sender-id-.*?\?.*})
.to_return(
status: 200,
body: {
name: 'Jane',
username: 'some_user_name',
profile_pic: 'https://chatwoot-assets.local/sample.png',
id: 'Sender-id-1',
follower_count: 100,
is_user_follow_business: true,
is_business_follow_user: true,
is_verified_user: false
}.to_json,
body: proc { |request|
sender_id = request.uri.path.split('/').last.split('?').first
{
name: 'Jane',
username: 'some_user_name',
profile_pic: 'https://chatwoot-assets.local/sample.png',
id: sender_id,
follower_count: 100,
is_user_follow_business: true,
is_business_follow_user: true,
is_verified_user: false
}.to_json
},
headers: { 'Content-Type' => 'application/json' }
)
end
it 'creates contact and message for the instagram direct inbox' do
messaging = dm_params[:entry][0]['messaging'][0]
contact_inbox
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
described_class.new(messaging, instagram_inbox).perform
instagram_inbox.reload
@@ -63,13 +57,15 @@ describe Messages::Instagram::MessageBuilder do
end
it 'discard echo message already sent by chatwoot' do
conversation
message
messaging = dm_params[:entry][0]['messaging'][0]
contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id)
create(:message, account_id: account.id, inbox_id: instagram_inbox.id, conversation_id: conversation.id, message_type: 'outgoing',
source_id: 'message-id-1')
expect(instagram_inbox.conversations.count).to be 1
expect(instagram_inbox.messages.count).to be 1
messaging = dm_params[:entry][0]['messaging'][0]
messaging[:message][:mid] = 'message-id-1' # Set same source_id as the existing message
described_class.new(messaging, instagram_inbox, outgoing_echo: true).perform
@@ -81,6 +77,7 @@ describe Messages::Instagram::MessageBuilder do
it 'discards duplicate messages from webhook events with the same message_id' do
messaging = dm_params[:entry][0]['messaging'][0]
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
described_class.new(messaging, instagram_inbox).perform
initial_message_count = instagram_inbox.messages.count
@@ -93,6 +90,7 @@ describe Messages::Instagram::MessageBuilder do
it 'creates message for shared reel' do
messaging = shared_reel_params[:entry][0]['messaging'][0]
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
described_class.new(messaging, instagram_inbox).perform
message = instagram_inbox.messages.first
@@ -103,7 +101,9 @@ describe Messages::Instagram::MessageBuilder do
end
it 'creates message with story id' do
story_source_id = instagram_story_reply_event[:entry][0]['messaging'][0]['message']['mid']
messaging = instagram_story_reply_event[:entry][0]['messaging'][0]
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
story_source_id = messaging['message']['mid']
stub_request(:get, %r{https://graph\.instagram\.com/.*?/#{story_source_id}\?.*})
.to_return(
@@ -121,7 +121,6 @@ describe Messages::Instagram::MessageBuilder do
headers: { 'Content-Type' => 'application/json' }
)
messaging = instagram_story_reply_event[:entry][0]['messaging'][0]
described_class.new(messaging, instagram_inbox).perform
message = instagram_inbox.messages.first
@@ -134,10 +133,13 @@ describe Messages::Instagram::MessageBuilder do
it 'creates message with reply to mid' do
# Create first message to ensure reply to is valid
first_messaging = dm_params[:entry][0]['messaging'][0]
sender_id = first_messaging['sender']['id']
create_instagram_contact_for_sender(sender_id, instagram_inbox)
described_class.new(first_messaging, instagram_inbox).perform
# Create second message with reply to mid
# Create second message with reply to mid, using same sender_id
messaging = instagram_message_reply_event[:entry][0]['messaging'][0]
messaging['sender']['id'] = sender_id
described_class.new(messaging, instagram_inbox).perform
first_message = instagram_inbox.messages.first
@@ -148,12 +150,13 @@ describe Messages::Instagram::MessageBuilder do
end
it 'handles deleted story' do
story_source_id = story_mention_params[:entry][0][:messaging][0]['message']['mid']
messaging = story_mention_params[:entry][0][:messaging][0]
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
story_source_id = messaging['message']['mid']
stub_request(:get, %r{https://graph\.instagram\.com/.*?/#{story_source_id}\?.*})
.to_return(status: 404, body: { error: { message: 'Story not found', code: 1_609_005 } }.to_json)
messaging = story_mention_params[:entry][0][:messaging][0]
described_class.new(messaging, instagram_inbox).perform
message = instagram_inbox.messages.first
@@ -163,11 +166,12 @@ describe Messages::Instagram::MessageBuilder do
end
it 'does not create message for unsupported file type' do
conversation
messaging = story_mention_params[:entry][0][:messaging][0]
contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id)
# try to create a message with unsupported file type
story_mention_params[:entry][0][:messaging][0]['message']['attachments'][0]['type'] = 'unsupported_type'
messaging = story_mention_params[:entry][0][:messaging][0]
messaging['message']['attachments'][0]['type'] = 'unsupported_type'
described_class.new(messaging, instagram_inbox, outgoing_echo: false).perform
@@ -177,7 +181,11 @@ describe Messages::Instagram::MessageBuilder do
end
it 'does not create message if the message is already exists' do
message
messaging = dm_params[:entry][0]['messaging'][0]
contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id)
create(:message, account_id: account.id, inbox_id: instagram_inbox.id, conversation_id: conversation.id, message_type: 'outgoing',
source_id: 'message-id-1')
expect(instagram_inbox.conversations.count).to be 1
expect(instagram_inbox.messages.count).to be 1
@@ -194,7 +202,7 @@ describe Messages::Instagram::MessageBuilder do
instagram_channel.update(access_token: 'invalid_token')
# Stub the request to return authorization error status
stub_request(:get, %r{https://graph\.instagram\.com/.*?/Sender-id-1\?.*})
stub_request(:get, %r{https://graph\.instagram\.com/.*?/Sender-id-.*?\?.*})
.to_return(
status: 401,
body: { error: { message: 'unauthorized access token', code: 190 } }.to_json,
@@ -218,6 +226,7 @@ describe Messages::Instagram::MessageBuilder do
it 'creates a new conversation if existing conversation is not present' do
initial_count = Conversation.count
messaging = dm_params[:entry][0]['messaging'][0]
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
described_class.new(messaging, instagram_inbox).perform
@@ -226,21 +235,23 @@ describe Messages::Instagram::MessageBuilder do
end
it 'will not create a new conversation if last conversation is not resolved' do
messaging = dm_params[:entry][0]['messaging'][0]
contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
existing_conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id,
contact_id: contact.id, status: :open)
messaging = dm_params[:entry][0]['messaging'][0]
described_class.new(messaging, instagram_inbox).perform
expect(instagram_inbox.conversations.last.id).to eq(existing_conversation.id)
end
it 'creates a new conversation if last conversation is resolved' do
messaging = dm_params[:entry][0]['messaging'][0]
contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
existing_conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id,
contact_id: contact.id, status: :resolved)
initial_count = Conversation.count
messaging = dm_params[:entry][0]['messaging'][0]
described_class.new(messaging, instagram_inbox).perform
@@ -257,6 +268,7 @@ describe Messages::Instagram::MessageBuilder do
it 'creates a new conversation if existing conversation is not present' do
initial_count = Conversation.count
messaging = dm_params[:entry][0]['messaging'][0]
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
described_class.new(messaging, instagram_inbox).perform
@@ -265,6 +277,8 @@ describe Messages::Instagram::MessageBuilder do
end
it 'reopens last conversation if last conversation is resolved' do
messaging = dm_params[:entry][0]['messaging'][0]
contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
existing_conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id,
contact_id: contact.id, status: :resolved)
@@ -307,6 +321,7 @@ describe Messages::Instagram::MessageBuilder do
it 'saves story information when story mention is processed' do
messaging = story_mention_params[:entry][0][:messaging][0]
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
described_class.new(messaging, instagram_inbox).perform
message = instagram_inbox.messages.first
@@ -328,6 +343,7 @@ describe Messages::Instagram::MessageBuilder do
)
messaging = story_mention_params[:entry][0][:messaging][0]
create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox)
described_class.new(messaging, instagram_inbox).perform
message = instagram_inbox.messages.first
@@ -17,30 +17,23 @@ describe Messages::Instagram::Messenger::MessageBuilder do
let!(:instagram_story_reply_event) { build(:instagram_story_reply_event).with_indifferent_access }
let!(:instagram_message_reply_event) { build(:instagram_message_reply_event).with_indifferent_access }
let(:fb_object) { double }
let(:contact) { create(:contact, id: 'Sender-id-1', name: 'Jane Dae') }
let(:contact_inbox) { create(:contact_inbox, contact_id: contact.id, inbox_id: instagram_messenger_inbox.id, source_id: 'Sender-id-1') }
let(:conversation) do
create(:conversation, account_id: account.id, inbox_id: instagram_messenger_inbox.id, contact_id: contact.id,
additional_attributes: { type: 'instagram_direct_message', conversation_language: 'en' })
end
let(:message) do
create(:message, account_id: account.id, inbox_id: instagram_messenger_inbox.id, conversation_id: conversation.id, message_type: 'outgoing',
source_id: 'message-id-1')
end
describe '#perform' do
it 'creates contact and message for the facebook inbox' do
messaging = dm_params[:entry][0]['messaging'][0]
sender_id = messaging['sender']['id']
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
allow(fb_object).to receive(:get_object).and_return(
{
name: 'Jane',
id: 'Sender-id-1',
id: sender_id,
account_id: instagram_messenger_inbox.account_id,
profile_pic: 'https://chatwoot-assets.local/sample.png'
}.with_indifferent_access
)
messaging = dm_params[:entry][0]['messaging'][0]
contact_inbox
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
described_class.new(messaging, instagram_messenger_inbox).perform
instagram_messenger_inbox.reload
@@ -56,7 +49,13 @@ describe Messages::Instagram::Messenger::MessageBuilder do
end
it 'discard echo message already sent by chatwoot' do
message
messaging = dm_params[:entry][0]['messaging'][0]
sender_id = messaging['sender']['id']
contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
conversation = create(:conversation, account_id: account.id, inbox_id: instagram_messenger_inbox.id, contact_id: contact.id,
additional_attributes: { type: 'instagram_direct_message', conversation_language: 'en' })
create(:message, account_id: account.id, inbox_id: instagram_messenger_inbox.id, conversation_id: conversation.id, message_type: 'outgoing',
source_id: 'message-id-1')
expect(instagram_messenger_inbox.conversations.count).to be 1
expect(instagram_messenger_inbox.messages.count).to be 1
@@ -65,13 +64,11 @@ describe Messages::Instagram::Messenger::MessageBuilder do
allow(fb_object).to receive(:get_object).and_return(
{
name: 'Jane',
id: 'Sender-id-1',
id: sender_id,
account_id: instagram_messenger_inbox.account_id,
profile_pic: 'https://chatwoot-assets.local/sample.png'
}.with_indifferent_access
)
messaging = dm_params[:entry][0]['messaging'][0]
contact_inbox
described_class.new(messaging, instagram_messenger_inbox, outgoing_echo: true).perform
instagram_messenger_inbox.reload
@@ -81,17 +78,20 @@ describe Messages::Instagram::Messenger::MessageBuilder do
end
it 'creates message for shared reel' do
messaging = shared_reel_params[:entry][0]['messaging'][0]
sender_id = messaging['sender']['id']
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
allow(fb_object).to receive(:get_object).and_return(
{
name: 'Jane',
id: 'Sender-id-1',
id: sender_id,
account_id: instagram_messenger_inbox.account_id,
profile_pic: 'https://chatwoot-assets.local/sample.png'
}.with_indifferent_access
)
messaging = shared_reel_params[:entry][0]['messaging'][0]
contact_inbox
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
described_class.new(messaging, instagram_messenger_inbox).perform
message = instagram_messenger_channel.inbox.messages.first
@@ -102,18 +102,20 @@ describe Messages::Instagram::Messenger::MessageBuilder do
end
it 'creates message with for reply with story id' do
messaging = instagram_story_reply_event[:entry][0]['messaging'][0]
sender_id = messaging['sender']['id']
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
allow(fb_object).to receive(:get_object).and_return(
{
name: 'Jane',
id: 'Sender-id-1',
id: sender_id,
account_id: instagram_messenger_inbox.account_id,
profile_pic: 'https://chatwoot-assets.local/sample.png'
}.with_indifferent_access
)
messaging = instagram_story_reply_event[:entry][0]['messaging'][0]
contact_inbox
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
described_class.new(messaging, instagram_messenger_inbox).perform
message = instagram_messenger_channel.inbox.messages.first
@@ -125,24 +127,26 @@ describe Messages::Instagram::Messenger::MessageBuilder do
end
it 'creates message with for reply with mid' do
# create first message to ensure reply to is valid
first_message_data = dm_params[:entry][0]['messaging'][0]
sender_id = first_message_data['sender']['id']
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
allow(fb_object).to receive(:get_object).and_return(
{
name: 'Jane',
id: 'Sender-id-1',
id: sender_id,
account_id: instagram_messenger_inbox.account_id,
profile_pic: 'https://chatwoot-assets.local/sample.png'
}.with_indifferent_access
)
# create first message to ensure reply to is valid
first_message = dm_params[:entry][0]['messaging'][0]
contact_inbox
described_class.new(first_message, instagram_messenger_inbox).perform
# create the second message with the reply to mid set
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
described_class.new(first_message_data, instagram_messenger_inbox).perform
# create the second message with the reply to mid set, ensure same sender_id
messaging = instagram_message_reply_event[:entry][0]['messaging'][0]
contact_inbox
messaging['sender']['id'] = sender_id # Use the same sender_id
described_class.new(messaging, instagram_messenger_inbox).perform
first_message = instagram_messenger_channel.inbox.messages.first
message = instagram_messenger_channel.inbox.messages.last
@@ -153,14 +157,16 @@ describe Messages::Instagram::Messenger::MessageBuilder do
end
it 'raises exception on deleted story' do
messaging = story_mention_params[:entry][0][:messaging][0]
sender_id = messaging['sender']['id']
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
allow(fb_object).to receive(:get_object).and_raise(Koala::Facebook::ClientError.new(
190,
'This Message has been deleted by the user or the business.'
))
messaging = story_mention_params[:entry][0][:messaging][0]
contact_inbox
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
described_class.new(messaging, instagram_messenger_inbox, outgoing_echo: false).perform
instagram_messenger_inbox.reload
@@ -180,22 +186,24 @@ describe Messages::Instagram::Messenger::MessageBuilder do
end
it 'does not create message for unsupported file type' do
# create a message with unsupported file type
story_mention_params[:entry][0][:messaging][0]['message']['attachments'][0]['type'] = 'unsupported_type'
messaging = story_mention_params[:entry][0][:messaging][0]
sender_id = messaging['sender']['id']
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
allow(fb_object).to receive(:get_object).and_return(
{
name: 'Jane',
id: 'Sender-id-1',
id: sender_id,
account_id: instagram_messenger_inbox.account_id,
profile_pic: 'https://chatwoot-assets.local/sample.png'
}.with_indifferent_access
)
conversation
# create a message with unsupported file type
story_mention_params[:entry][0][:messaging][0]['message']['attachments'][0]['type'] = 'unsupported_type'
messaging = story_mention_params[:entry][0][:messaging][0]
contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
create(:conversation, account_id: account.id, inbox_id: instagram_messenger_inbox.id, contact_id: contact.id,
additional_attributes: { type: 'instagram_direct_message', conversation_language: 'en' })
described_class.new(messaging, instagram_messenger_inbox, outgoing_echo: false).perform
instagram_messenger_inbox.reload
@@ -218,18 +226,22 @@ describe Messages::Instagram::Messenger::MessageBuilder do
it 'creates a new conversation if existing conversation is not present' do
inital_count = Conversation.count
message = dm_params[:entry][0]['messaging'][0]
contact_inbox
sender_id = message['sender']['id']
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
described_class.new(message, instagram_messenger_inbox).perform
instagram_messenger_inbox.reload
contact_inbox.reload
expect(instagram_messenger_inbox.conversations.count).to eq(1)
expect(Conversation.count).to eq(inital_count + 1)
end
it 'will not create a new conversation if last conversation is not resolved' do
message = dm_params[:entry][0]['messaging'][0]
sender_id = message['sender']['id']
contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
existing_conversation = create(
:conversation,
account_id: account.id,
@@ -239,18 +251,18 @@ describe Messages::Instagram::Messenger::MessageBuilder do
additional_attributes: { type: 'instagram_direct_message', conversation_language: 'en' }
)
message = dm_params[:entry][0]['messaging'][0]
contact_inbox
described_class.new(message, instagram_messenger_inbox).perform
instagram_messenger_inbox.reload
contact_inbox.reload
expect(instagram_messenger_inbox.conversations.last.id).to eq(existing_conversation.id)
end
it 'creates a new conversation if last conversation is resolved' do
message = dm_params[:entry][0]['messaging'][0]
sender_id = message['sender']['id']
contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
existing_conversation = create(
:conversation,
account_id: account.id,
@@ -261,13 +273,9 @@ describe Messages::Instagram::Messenger::MessageBuilder do
)
inital_count = Conversation.count
message = dm_params[:entry][0]['messaging'][0]
contact_inbox
described_class.new(message, instagram_messenger_inbox).perform
instagram_messenger_inbox.reload
contact_inbox.reload
expect(instagram_messenger_inbox.conversations.last.id).not_to eq(existing_conversation.id)
expect(Conversation.count).to eq(inital_count + 1)
@@ -283,18 +291,22 @@ describe Messages::Instagram::Messenger::MessageBuilder do
it 'creates a new conversation if existing conversation is not present' do
inital_count = Conversation.count
message = dm_params[:entry][0]['messaging'][0]
contact_inbox
sender_id = message['sender']['id']
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
described_class.new(message, instagram_messenger_inbox).perform
instagram_messenger_inbox.reload
contact_inbox.reload
expect(instagram_messenger_inbox.conversations.count).to eq(1)
expect(Conversation.count).to eq(inital_count + 1)
end
it 'reopens last conversation if last conversation is resolved' do
message = dm_params[:entry][0]['messaging'][0]
sender_id = message['sender']['id']
contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
existing_conversation = create(
:conversation,
account_id: account.id,
@@ -306,13 +318,9 @@ describe Messages::Instagram::Messenger::MessageBuilder do
inital_count = Conversation.count
message = dm_params[:entry][0]['messaging'][0]
contact_inbox
described_class.new(message, instagram_messenger_inbox).perform
instagram_messenger_inbox.reload
contact_inbox.reload
expect(instagram_messenger_inbox.conversations.last.id).to eq(existing_conversation.id)
expect(Conversation.count).to eq(inital_count)
@@ -344,7 +352,9 @@ describe Messages::Instagram::Messenger::MessageBuilder do
allow(fb_object).to receive(:get_object).and_return(story_data)
messaging = story_mention_params[:entry][0][:messaging][0]
contact_inbox
sender_id = messaging['sender']['id']
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
builder = described_class.new(messaging, instagram_messenger_inbox)
builder.perform
@@ -358,18 +368,20 @@ describe Messages::Instagram::Messenger::MessageBuilder do
end
it 'handles story mentions specifically in the Instagram builder' do
messaging = story_mention_params[:entry][0][:messaging][0]
sender_id = messaging['sender']['id']
# First allow contact info fetch
allow(fb_object).to receive(:get_object).and_return({
name: 'Jane',
id: 'Sender-id-1'
id: sender_id
}.with_indifferent_access)
# Then allow story data fetch
allow(fb_object).to receive(:get_object).with(anything, fields: %w[story from])
.and_return(story_data)
messaging = story_mention_params[:entry][0][:messaging][0]
contact_inbox
create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox)
described_class.new(messaging, instagram_messenger_inbox).perform
message = instagram_messenger_inbox.messages.first
@@ -78,6 +78,42 @@ RSpec.describe 'Platform Accounts API', type: :request do
end
end
describe 'GET /platform/api/v1/accounts' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
get '/platform/api/v1/accounts'
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an invalid platform app token' do
it 'returns unauthorized' do
get '/platform/api/v1/accounts', headers: { api_access_token: 'invalid' }, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
let!(:account1) { create(:account, name: 'Account A') }
let!(:account2) { create(:account, name: 'Account B') }
before do
create(:platform_app_permissible, platform_app: platform_app, permissible: account1)
create(:platform_app_permissible, platform_app: platform_app, permissible: account2)
end
it 'returns all permissible accounts' do
get '/platform/api/v1/accounts', headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response.size).to eq(2)
expect(json_response.map { |acc| acc['name'] }).to include('Account A', 'Account B')
end
end
end
describe 'GET /platform/api/v1/accounts/{account_id}' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
@@ -1,14 +1,18 @@
FactoryBot.define do
factory :instagram_message_create_event, class: Hash do
transient do
ig_entry_id { SecureRandom.uuid }
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
end
entry do
[
{
'id': 'instagram-message-id-123',
'id': ig_entry_id,
'time': '2021-09-08T06:34:04+0000',
'messaging': [
{
'sender': {
'id': 'Sender-id-1'
'id': sender_id
},
'recipient': {
'id': 'chatwoot-app-user-id-1'
@@ -27,15 +31,19 @@ FactoryBot.define do
end
factory :instagram_message_standby_event, class: Hash do
transient do
ig_entry_id { SecureRandom.uuid }
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
end
entry do
[
{
'time': '2021-09-08T06:34:04+0000',
'id': 'instagram-message-id-123',
'id': ig_entry_id,
'standby': [
{
'sender': {
'id': 'Sender-id-1'
'id': sender_id
},
'recipient': {
'id': 'chatwoot-app-user-id-1'
@@ -54,15 +62,19 @@ FactoryBot.define do
end
factory :instagram_story_reply_event, class: Hash do
transient do
ig_entry_id { SecureRandom.uuid }
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
end
entry do
[
{
'id': 'instagram-message-id-123',
'id': ig_entry_id,
'time': '2021-09-08T06:34:04+0000',
'messaging': [
{
'sender': {
'id': 'Sender-id-1'
'id': sender_id
},
'recipient': {
'id': 'chatwoot-app-user-id-1'
@@ -87,15 +99,19 @@ FactoryBot.define do
end
factory :instagram_message_reply_event, class: Hash do
transient do
ig_entry_id { SecureRandom.uuid }
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
end
entry do
[
{
'id': 'instagram-message-id-123',
'id': ig_entry_id,
'time': '2021-09-08T06:35:04+0000',
'messaging': [
{
'sender': {
'id': 'Sender-id-1'
'id': sender_id
},
'recipient': {
'id': 'chatwoot-app-user-id-1'
@@ -117,11 +133,14 @@ FactoryBot.define do
end
factory :instagram_test_text_event, class: Hash do
transient do
ig_entry_id { SecureRandom.uuid }
end
entry do
[
{
'time' => 1_661_141_837_537,
'id' => '0',
'id' => ig_entry_id,
'messaging' => [
{
'sender' => {
@@ -144,15 +163,19 @@ FactoryBot.define do
end
factory :instagram_message_unsend_event, class: Hash do
transient do
ig_entry_id { SecureRandom.uuid }
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
end
entry do
[
{
'id': 'instagram-message-id-123',
'id': ig_entry_id,
'time': '2021-09-08T06:34:04+0000',
'messaging': [
{
'sender': {
'id': 'Sender-id-1'
'id': sender_id
},
'recipient': {
'id': 'chatwoot-app-user-id-1'
@@ -171,15 +194,19 @@ FactoryBot.define do
end
factory :instagram_message_attachment_event, class: Hash do
transient do
ig_entry_id { SecureRandom.uuid }
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
end
entry do
[
{
'id': 'instagram-message-id-1234',
'id': ig_entry_id,
'time': '2021-09-08T06:34:04+0000',
'messaging': [
{
'sender': {
'id': 'Sender-id-1'
'id': sender_id
},
'recipient': {
'id': 'chatwoot-app-user-id-1'
@@ -205,15 +232,19 @@ FactoryBot.define do
end
factory :instagram_shared_reel_event, class: Hash do
transient do
ig_entry_id { SecureRandom.uuid }
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
end
entry do
[
{
'id': 'instagram-message-id-1234',
'id': ig_entry_id,
'time': '2021-09-08T06:34:04+0000',
'messaging': [
{
'sender': {
'id': 'Sender-id-1'
'id': sender_id
},
'recipient': {
'id': 'chatwoot-app-user-id-1'
@@ -241,15 +272,19 @@ FactoryBot.define do
end
factory :instagram_story_mention_event, class: Hash do
transient do
ig_entry_id { SecureRandom.uuid }
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
end
entry do
[
{
'id': 'instagram-message-id-1234',
'id': ig_entry_id,
'time': '2021-09-08T06:34:04+0000',
'messaging': [
{
'sender': {
'id': 'Sender-id-1'
'id': sender_id
},
'recipient': {
'id': 'chatwoot-app-user-id-1'
@@ -275,15 +310,19 @@ FactoryBot.define do
end
factory :instagram_story_mention_event_with_echo, class: Hash do
transient do
ig_entry_id { SecureRandom.uuid }
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
end
entry do
[
{
'id': 'instagram-message-id-1234',
'id': ig_entry_id,
'time': '2021-09-08T06:34:04+0000',
'messaging': [
{
'sender': {
'id': 'Sender-id-1'
'id': sender_id
},
'recipient': {
'id': 'chatwoot-app-user-id-1'
@@ -310,15 +349,19 @@ FactoryBot.define do
end
factory :instagram_message_unsupported_event, class: Hash do
transient do
ig_entry_id { SecureRandom.uuid }
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
end
entry do
[
{
'id': 'instagram-message-unsupported-id-123',
'id': ig_entry_id,
'time': '2021-09-08T06:34:04+0000',
'messaging': [
{
'sender': {
'id': 'Sender-id-1'
'id': sender_id
},
'recipient': {
'id': 'chatwoot-app-user-id-1'
@@ -337,15 +380,19 @@ FactoryBot.define do
end
factory :messaging_seen_event, class: Hash do
transient do
ig_entry_id { SecureRandom.uuid }
sender_id { "Sender-id-#{SecureRandom.hex(4)}" }
end
entry do
[
{
'id': 'instagram-message-id-123',
'id': ig_entry_id,
'time': '2021-09-08T06:34:04+0000',
'messaging': [
{
'sender': {
'id': 'Sender-id-1'
'id': sender_id
},
'recipient': {
'id': 'chatwoot-app-user-id-1'
+37 -28
View File
@@ -10,19 +10,10 @@ describe Webhooks::InstagramEventsJob do
end
let!(:account) { create(:account) }
let(:return_object) do
{ name: 'Jane',
id: 'Sender-id-1',
account_id: instagram_messenger_inbox.account_id,
profile_pic: 'https://chatwoot-assets.local/sample.png',
username: 'some_user_name' }
end
let!(:instagram_messenger_channel) { create(:channel_instagram_fb_page, account: account, instagram_id: 'chatwoot-app-user-id-1') }
let!(:instagram_messenger_inbox) { create(:inbox, channel: instagram_messenger_channel, account: account, greeting_enabled: false) }
let!(:instagram_channel) { create(:channel_instagram, account: account, instagram_id: 'chatwoot-app-user-id-1') }
let!(:instagram_inbox) { create(:inbox, channel: instagram_channel, account: account, greeting_enabled: false) }
# Combined message events into one helper
let(:message_events) do
{
@@ -37,6 +28,14 @@ describe Webhooks::InstagramEventsJob do
}
end
def return_object_for(sender_id)
{ name: 'Jane',
id: sender_id,
account_id: instagram_messenger_inbox.account_id,
profile_pic: 'https://chatwoot-assets.local/sample.png',
username: 'some_user_name' }
end
describe '#perform' do
context 'when handling messaging events for Instagram via Facebook page' do
let(:fb_object) { double }
@@ -47,8 +46,9 @@ describe Webhooks::InstagramEventsJob do
it 'creates incoming message in the instagram inbox' do
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
sender_id = message_events[:dm][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return(
return_object.with_indifferent_access
return_object_for(sender_id).with_indifferent_access
)
instagram_webhook.perform_now(message_events[:dm][:entry])
@@ -63,8 +63,9 @@ describe Webhooks::InstagramEventsJob do
it 'creates standby message in the instagram inbox' do
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
sender_id = message_events[:standby][:entry][0][:standby][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return(
return_object.with_indifferent_access
return_object_for(sender_id).with_indifferent_access
)
instagram_webhook.perform_now(message_events[:standby][:entry])
@@ -82,10 +83,11 @@ describe Webhooks::InstagramEventsJob do
it 'handle instagram unsend message event' do
message = create(:message, inbox_id: instagram_messenger_inbox.id, source_id: 'message-id-to-delete')
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
sender_id = message_events[:unsend][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return(
{
name: 'Jane',
id: 'Sender-id-1',
id: sender_id,
account_id: instagram_messenger_inbox.account_id,
profile_pic: 'https://chatwoot-assets.local/sample.png'
}.with_indifferent_access
@@ -104,8 +106,9 @@ describe Webhooks::InstagramEventsJob do
it 'creates incoming message with attachments in the instagram inbox' do
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
sender_id = message_events[:attachment][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return(
return_object.with_indifferent_access
return_object_for(sender_id).with_indifferent_access
)
instagram_webhook.perform_now(message_events[:attachment][:entry])
@@ -118,8 +121,9 @@ describe Webhooks::InstagramEventsJob do
it 'creates incoming message with attachments in the instagram inbox for story mention' do
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
sender_id = message_events[:story_mention][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return(
return_object.with_indifferent_access,
return_object_for(sender_id).with_indifferent_access,
{ story:
{
mention: {
@@ -165,8 +169,9 @@ describe Webhooks::InstagramEventsJob do
it 'handles unsupported message' do
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
sender_id = message_events[:unsupported][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return(
return_object.with_indifferent_access
return_object_for(sender_id).with_indifferent_access
)
instagram_webhook.perform_now(message_events[:unsupported][:entry])
@@ -184,19 +189,22 @@ describe Webhooks::InstagramEventsJob do
before do
instagram_channel.update(access_token: 'valid_instagram_token')
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/Sender-id-1\?.*})
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/Sender-id-.*\?.*})
.to_return(
status: 200,
body: {
name: 'Jane',
username: 'some_user_name',
profile_pic: 'https://chatwoot-assets.local/sample.png',
id: 'Sender-id-1',
follower_count: 100,
is_user_follow_business: true,
is_business_follow_user: true,
is_verified_user: false
}.to_json,
body: proc { |request|
sender_id = request.uri.path.split('/').last.split('?').first
{
name: 'Jane',
username: 'some_user_name',
profile_pic: 'https://chatwoot-assets.local/sample.png',
id: sender_id,
follower_count: 100,
is_user_follow_business: true,
is_business_follow_user: true,
is_verified_user: false
}.to_json
},
headers: { 'Content-Type' => 'application/json' }
)
end
@@ -289,14 +297,15 @@ describe Webhooks::InstagramEventsJob do
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/.*\?.*})
.to_return(status: 401, body: { error: { message: 'No matching Instagram user', code: 9010 } }.to_json)
sender_id = message_events[:dm][:entry][0][:messaging][0][:sender][:id]
instagram_webhook.perform_now(message_events[:dm][:entry])
instagram_inbox.reload
expect(instagram_inbox.contacts.count).to be 1
expect(instagram_inbox.contacts.last.name).to eq 'Unknown (IG: Sender-id-1)'
expect(instagram_inbox.contacts.last.name).to eq "Unknown (IG: #{sender_id})"
expect(instagram_inbox.contacts.last.contact_inboxes.count).to be 1
expect(instagram_inbox.contacts.last.contact_inboxes.first.source_id).to eq 'Sender-id-1'
expect(instagram_inbox.contacts.last.contact_inboxes.first.source_id).to eq sender_id
expect(instagram_inbox.conversations.count).to eq 1
expect(instagram_inbox.messages.count).to eq 1
+1
View File
@@ -70,6 +70,7 @@ RSpec.configure do |config|
config.include SlackStubs
config.include FileUploadHelpers
config.include CsvSpecHelpers
config.include InstagramSpecHelpers
config.include Devise::Test::IntegrationHelpers, type: :request
config.include ActiveSupport::Testing::TimeHelpers
config.include ActionCable::TestHelper
+20
View File
@@ -0,0 +1,20 @@
module InstagramSpecHelpers
def create_instagram_contact_for_sender(sender_id, inbox)
contact = Contact.find_by(identifier: sender_id)
if contact.nil?
contact = create(:contact, identifier: sender_id, name: 'Jane Dae')
create(:contact_inbox, contact_id: contact.id, inbox_id: inbox.id, source_id: sender_id)
end
contact
end
def instagram_user_response_object_for(sender_id, account_id)
{
name: 'Jane',
id: sender_id,
account_id: account_id,
profile_pic: 'https://chatwoot-assets.local/sample.png',
username: 'some_user_name'
}
end
end