mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-23 02:53:32 +00:00
more commits.
This commit is contained in:
@@ -61,7 +61,7 @@ const navLinks = [
|
||||
title: 'Account',
|
||||
component: 'account',
|
||||
label: '',
|
||||
icon: 'lucide:circle-user-round',
|
||||
icon: 'lucide:settings',
|
||||
},
|
||||
]
|
||||
const userStore = useUserStore()
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
.tab-container-default {
|
||||
padding: 15px 15px;
|
||||
padding: 2rem 2rem;
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import SidebarNav from '@/components/account/SidebarNav.vue'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
const props = defineProps({
|
||||
page: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const currentPage = computed(() => {
|
||||
return props.page
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="md:hidden">
|
||||
<VPImage alt="Forms" width="1280" height="1214" class="block" :image="{
|
||||
dark: '/examples/forms-dark.png',
|
||||
light: '/examples/forms-light.png',
|
||||
}" />
|
||||
</div>
|
||||
<div class="hidden space-y-6 md:block tab-container-default">
|
||||
<div class="space-y-0.5">
|
||||
<h2 class="text-2xl font-bold tracking-tight">
|
||||
Profile settings
|
||||
</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Manage your account settings and set e-mail preferences.
|
||||
</p>
|
||||
</div>
|
||||
<Separator class="my-6" />
|
||||
<div class="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
|
||||
<aside class="-mx-4 lg:w-1/5">
|
||||
<SidebarNav :current-page="currentPage" />
|
||||
</aside>
|
||||
<div class="flex-1 lg:max-w-2xl">
|
||||
<div class="space-y-6">
|
||||
<slot :page="currentPage" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
defineProps({
|
||||
currentPage: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const sidebarNavItems = [
|
||||
{
|
||||
title: 'Profile',
|
||||
href: '/account/profile',
|
||||
},
|
||||
{
|
||||
title: 'Account',
|
||||
href: '/account/account',
|
||||
},
|
||||
{
|
||||
title: 'Appearance',
|
||||
href: '/account/appearance',
|
||||
},
|
||||
{
|
||||
title: 'Notifications',
|
||||
href: '/account/notifications',
|
||||
},
|
||||
{
|
||||
title: 'Display',
|
||||
href: '/account/display',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1">
|
||||
<router-link v-for="item in sidebarNavItems" :key="item.title" :to="item.href" custom>
|
||||
<template v-slot="{ navigate, isActive }">
|
||||
<Button as="a" :href="item.href" variant="ghost" :class="cn(
|
||||
'w-full text-left justify-start',
|
||||
isActive && 'bg-muted hover:bg-muted'
|
||||
)" @click="navigate">
|
||||
{{ item.title }}
|
||||
</Button>
|
||||
</template>
|
||||
</router-link>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -5,33 +5,6 @@
|
||||
<h3 class="scroll-m-20 text-2xl font-medium flex gap-x-2">
|
||||
Conversations
|
||||
</h3>
|
||||
<div class="w-[8rem]">
|
||||
<Select @update:modelValue="handleFilterChange" v-model="predefinedFilter">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a filter" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<!-- <SelectLabel>Status</SelectLabel> -->
|
||||
<SelectItem value="status_all">
|
||||
All
|
||||
</SelectItem>
|
||||
<SelectItem value="status_open">
|
||||
Open
|
||||
</SelectItem>
|
||||
<SelectItem value="status_processing">
|
||||
Processing
|
||||
</SelectItem>
|
||||
<SelectItem value="status_spam">
|
||||
Spam
|
||||
</SelectItem>
|
||||
<SelectItem value="status_resolved">
|
||||
Resolved
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
@@ -58,16 +31,33 @@
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div class="space-x-2">
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<button>
|
||||
<ListFilter :size="30" class="p-2 bg-slate-100 rounded-sm" :stroke-width="1.9" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="flex flex-col gap-3 w-full">
|
||||
Work in progress.
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div class="w-[8rem]">
|
||||
<Select @update:modelValue="handleFilterChange" v-model="predefinedFilter">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a filter" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<!-- <SelectLabel>Status</SelectLabel> -->
|
||||
<SelectItem value="status_all">
|
||||
All
|
||||
</SelectItem>
|
||||
<SelectItem value="status_open">
|
||||
Open
|
||||
</SelectItem>
|
||||
<SelectItem value="status_processing">
|
||||
Processing
|
||||
</SelectItem>
|
||||
<SelectItem value="status_spam">
|
||||
Spam
|
||||
</SelectItem>
|
||||
<SelectItem value="status_resolved">
|
||||
Resolved
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,13 +78,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-center items-center mt-6 relative"
|
||||
v-if="conversationStore.conversations.hasMore && !hasErrored && hasConversations">
|
||||
<Button variant="link" @click="loadNextPage">
|
||||
<Spinner v-if="conversationStore.conversations.loading" />
|
||||
<p v-else>Load more...</p>
|
||||
</Button>
|
||||
<div class="flex justify-center items-center mt-5 relative">
|
||||
<div v-if="conversationStore.conversations.hasMore && !hasErrored && hasConversations">
|
||||
<Button variant="link" @click="loadNextPage">
|
||||
<Spinner v-if="conversationStore.conversations.loading" />
|
||||
<p v-else>Load more...</p>
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else-if="everythingLoaded">
|
||||
All conversations loaded 😎
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,18 +101,11 @@ import { CONVERSATION_LIST_TYPE, CONVERSATION_PRE_DEFINED_FILTERS } from '@/cons
|
||||
|
||||
import { Error } from '@/components/ui/error'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Search, ListFilter } from 'lucide-vue-next'
|
||||
import {
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@/components/ui/tabs'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Select,
|
||||
@@ -179,6 +165,11 @@ const hasErrored = computed(() => {
|
||||
return conversationStore.conversations.errorMessage ? true : false
|
||||
})
|
||||
|
||||
const everythingLoaded = computed(() => {
|
||||
return !conversationStore.conversations.errorMessage && !emptyConversations.value
|
||||
})
|
||||
|
||||
|
||||
const conversationsLoading = computed(() => {
|
||||
return conversationStore.conversations.loading
|
||||
})
|
||||
|
||||
@@ -26,10 +26,11 @@ const routes = [
|
||||
component: UserLoginView
|
||||
},
|
||||
{
|
||||
path: '/account',
|
||||
name: "account",
|
||||
component: AccountView
|
||||
},
|
||||
path: '/account/:page?',
|
||||
name: 'account',
|
||||
component: AccountView,
|
||||
props: true,
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import { format } from 'date-fns'
|
||||
import { format, differenceInMinutes, differenceInHours, differenceInDays } from 'date-fns';
|
||||
|
||||
export function formatTime (t) {
|
||||
export function formatTime(t) {
|
||||
try {
|
||||
return format(t, 'h:mm a')
|
||||
const now = new Date();
|
||||
const minutesDifference = differenceInMinutes(now, t);
|
||||
const hoursDifference = differenceInHours(now, t);
|
||||
const daysDifference = differenceInDays(now, t);
|
||||
|
||||
if (minutesDifference < 60) {
|
||||
return `${minutesDifference} minutes ago`;
|
||||
} else if (hoursDifference < 24) {
|
||||
return `${hoursDifference} hours ago`;
|
||||
} else if (daysDifference < 7) {
|
||||
return `${daysDifference} days ago`;
|
||||
} else {
|
||||
return format(t, 'MMMM d, yyyy h:mm a');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("error parsing time", error)
|
||||
console.error("error parsing time", error, "time", t)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<div class="tab-container-default">
|
||||
<h1>Work in progress.</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
import Account from '@/components/account/Account.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Account>
|
||||
yo
|
||||
</Account>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<!-- Resizable panel last resize value is stored in the localstorage -->
|
||||
<ResizablePanelGroup direction="horizontal" auto-save-id="conversation.vue.resizable.panel">
|
||||
<ResizablePanel :min-size="18" :default-size="23" :max-size="23">
|
||||
<ResizablePanel :min-size="20" :default-size="23" :max-size="23">
|
||||
<ConversationList></ConversationList>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/abhinavxd/artemis/internal/automation/models"
|
||||
cmodels "github.com/abhinavxd/artemis/internal/conversation/models"
|
||||
@@ -18,12 +20,13 @@ var (
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
q queries
|
||||
lo *logf.Logger
|
||||
conversationStore ConversationStore
|
||||
messageStore MessageStore
|
||||
rules []models.Rule
|
||||
conversationQ chan cmodels.Conversation
|
||||
q queries
|
||||
lo *logf.Logger
|
||||
conversationStore ConversationStore
|
||||
messageStore MessageStore
|
||||
rules []models.Rule
|
||||
newConversationQ chan string
|
||||
updateConversationQ chan string
|
||||
}
|
||||
|
||||
type Opts struct {
|
||||
@@ -32,8 +35,12 @@ type Opts struct {
|
||||
}
|
||||
|
||||
type ConversationStore interface {
|
||||
Get(uuid string) (cmodels.Conversation, error)
|
||||
GetRecentConversations(t time.Time) ([]cmodels.Conversation, error)
|
||||
UpdateTeamAssignee(uuid string, assigneeUUID []byte) error
|
||||
UpdateUserAssignee(uuid string, assigneeUUID []byte) error
|
||||
UpdateStatus(uuid string, status []byte) error
|
||||
UpdatePriority(uuid string, priority []byte) error
|
||||
}
|
||||
|
||||
type MessageStore interface {
|
||||
@@ -49,20 +56,21 @@ func New(opt Opts) (*Engine, error) {
|
||||
var (
|
||||
q queries
|
||||
e = &Engine{
|
||||
lo: opt.Lo,
|
||||
conversationQ: make(chan cmodels.Conversation, 10000),
|
||||
lo: opt.Lo,
|
||||
newConversationQ: make(chan string, 5000),
|
||||
updateConversationQ: make(chan string, 5000),
|
||||
}
|
||||
)
|
||||
if err := dbutil.ScanSQLFile("queries.sql", &q, opt.DB, efs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.q = q
|
||||
e.rules = e.getRules()
|
||||
e.rules = e.queryRules()
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (e *Engine) ReloadRules() {
|
||||
e.rules = e.getRules()
|
||||
e.rules = e.queryRules()
|
||||
}
|
||||
|
||||
func (e *Engine) SetMessageStore(messageStore MessageStore) {
|
||||
@@ -74,34 +82,98 @@ func (e *Engine) SetConversationStore(conversationStore ConversationStore) {
|
||||
}
|
||||
|
||||
func (e *Engine) Serve(ctx context.Context) {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Create separate semaphores for each channel
|
||||
maxWorkers := 10
|
||||
newConversationSemaphore := make(chan struct{}, maxWorkers)
|
||||
updateConversationSemaphore := make(chan struct{}, maxWorkers)
|
||||
timeTriggerSemaphore := make(chan struct{}, maxWorkers)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case conversation := <-e.conversationQ:
|
||||
e.processConversations(conversation)
|
||||
case conversationUUID := <-e.newConversationQ:
|
||||
newConversationSemaphore <- struct{}{}
|
||||
go e.handleNewConversation(conversationUUID, newConversationSemaphore)
|
||||
case conversationUUID := <-e.updateConversationQ:
|
||||
updateConversationSemaphore <- struct{}{}
|
||||
go e.handleUpdateConversation(conversationUUID, updateConversationSemaphore)
|
||||
case <-ticker.C:
|
||||
e.lo.Info("evaluating time triggers")
|
||||
timeTriggerSemaphore <- struct{}{}
|
||||
go e.handleTimeTrigger(timeTriggerSemaphore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) EvaluateRules(c cmodels.Conversation) {
|
||||
select {
|
||||
case e.conversationQ <- c:
|
||||
default:
|
||||
// Queue is full.
|
||||
e.lo.Warn("EvaluateRules: conversationQ is full, unable to enqueue conversation")
|
||||
func (e *Engine) handleNewConversation(conversationUUID string, semaphore chan struct{}) {
|
||||
defer func() { <-semaphore }()
|
||||
conversation, err := e.conversationStore.Get(conversationUUID)
|
||||
if err != nil {
|
||||
e.lo.Error("error could not fetch conversations to evaluate new conversation rules", "conversation_uuid", conversationUUID)
|
||||
return
|
||||
}
|
||||
rules := e.filterRulesByType(models.RuleTypeNewConversation)
|
||||
e.evalConversationRules(rules, conversation)
|
||||
}
|
||||
|
||||
func (e *Engine) handleUpdateConversation(conversationUUID string, semaphore chan struct{}) {
|
||||
defer func() { <-semaphore }()
|
||||
conversation, err := e.conversationStore.Get(conversationUUID)
|
||||
if err != nil {
|
||||
e.lo.Error("error could not fetch conversations to evaluate update conversation rules", "conversation_uuid", conversationUUID)
|
||||
return
|
||||
}
|
||||
rules := e.filterRulesByType(models.RuleTypeConversationUpdate)
|
||||
e.evalConversationRules(rules, conversation)
|
||||
}
|
||||
|
||||
func (e *Engine) handleTimeTrigger(semaphore chan struct{}) {
|
||||
defer func() { <-semaphore }()
|
||||
thirtyDaysAgo := time.Now().Add(-30 * 24 * time.Hour)
|
||||
conversations, err := e.conversationStore.GetRecentConversations(thirtyDaysAgo)
|
||||
if err != nil {
|
||||
e.lo.Error("error could not fetch conversations to evaluate time triggers")
|
||||
return
|
||||
}
|
||||
rules := e.filterRulesByType(models.RuleTypeTimeTrigger)
|
||||
for _, conversation := range conversations {
|
||||
e.evalConversationRules(rules, conversation)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) getRules() []models.Rule {
|
||||
var rulesJSON []string
|
||||
func (e *Engine) EvaluateNewConversationRules(conversationUUID string) {
|
||||
select {
|
||||
case e.newConversationQ <- conversationUUID:
|
||||
default:
|
||||
// Queue is full.
|
||||
e.lo.Warn("EvaluateNewConversationRules: newConversationQ is full, unable to enqueue conversation")
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) EvaluateConversationUpdateRules(conversationUUID string) {
|
||||
select {
|
||||
case e.updateConversationQ <- conversationUUID:
|
||||
default:
|
||||
// Queue is full.
|
||||
e.lo.Warn("EvaluateConversationUpdateRules: updateConversationQ is full, unable to enqueue conversation")
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) queryRules() []models.Rule {
|
||||
var (
|
||||
rulesJSON []string
|
||||
rules []models.Rule
|
||||
)
|
||||
err := e.q.GetRules.Select(&rulesJSON)
|
||||
if err != nil {
|
||||
e.lo.Error("error fetching automation rules", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var rules []models.Rule
|
||||
for _, ruleJSON := range rulesJSON {
|
||||
var rulesBatch []models.Rule
|
||||
if err := json.Unmarshal([]byte(ruleJSON), &rulesBatch); err != nil {
|
||||
@@ -111,6 +183,16 @@ func (e *Engine) getRules() []models.Rule {
|
||||
rules = append(rules, rulesBatch...)
|
||||
}
|
||||
|
||||
e.lo.Debug("fetched rules", "num", len(rules), "rules", rules)
|
||||
e.lo.Debug("fetched rules", "num", len(rules), "rules", fmt.Sprintf("%+v", rules))
|
||||
return rules
|
||||
}
|
||||
|
||||
func (e *Engine) filterRulesByType(ruleType string) []models.Rule {
|
||||
var filteredRules []models.Rule
|
||||
for _, rule := range e.rules {
|
||||
if rule.Type == ruleType {
|
||||
filteredRules = append(filteredRules, rule)
|
||||
}
|
||||
}
|
||||
return filteredRules
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package automation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/abhinavxd/artemis/internal/automation/models"
|
||||
@@ -9,10 +10,11 @@ import (
|
||||
"github.com/abhinavxd/artemis/internal/systeminfo"
|
||||
)
|
||||
|
||||
func (e *Engine) processConversations(conversation cmodels.Conversation) {
|
||||
e.lo.Debug("num rules", "rules", len(e.rules))
|
||||
for _, rule := range e.rules {
|
||||
func (e *Engine) evalConversationRules(rules []models.Rule, conversation cmodels.Conversation) {
|
||||
e.lo.Debug("num rules", "rules", len(rules))
|
||||
for _, rule := range rules {
|
||||
e.lo.Debug("eval rule", "groups", len(rule.Groups), "rule", rule)
|
||||
// At max there can be only 2 groups.
|
||||
if len(rule.Groups) > 2 {
|
||||
continue
|
||||
}
|
||||
@@ -29,6 +31,8 @@ func (e *Engine) processConversations(conversation cmodels.Conversation) {
|
||||
for _, action := range rule.Actions {
|
||||
e.executeActions(conversation, action)
|
||||
}
|
||||
} else {
|
||||
e.lo.Debug("rule evaluation failed, NOT executing actions")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,45 +85,57 @@ func (e *Engine) evaluateGroup(rules []models.RuleDetail, operator string, conve
|
||||
|
||||
func (e *Engine) evaluateRule(rule models.RuleDetail, conversation cmodels.Conversation) bool {
|
||||
var (
|
||||
conversationValue string
|
||||
conditionMet bool
|
||||
valueToCompare string
|
||||
conditionMet bool
|
||||
)
|
||||
|
||||
// Extract the value from the conversation based on the rule's field
|
||||
switch rule.Field {
|
||||
case "subject":
|
||||
conversationValue = conversation.Subject
|
||||
case "content":
|
||||
conversationValue = conversation.FirstMessage
|
||||
case "status":
|
||||
conversationValue = conversation.Status.String
|
||||
case "priority":
|
||||
conversationValue = conversation.Priority.String
|
||||
case models.ConversationFieldSubject:
|
||||
valueToCompare = conversation.Subject
|
||||
case models.ConversationFieldContent:
|
||||
valueToCompare = conversation.FirstMessage
|
||||
case models.ConversationFieldStatus:
|
||||
valueToCompare = conversation.Status.String
|
||||
case models.ConversationFieldPriority:
|
||||
valueToCompare = conversation.Priority.String
|
||||
case models.ConversationFieldAssignedTeamID:
|
||||
if conversation.AssignedTeamID.Valid {
|
||||
valueToCompare = strconv.Itoa(conversation.AssignedTeamID.Int)
|
||||
}
|
||||
case models.ConversationFieldAssignedUserID:
|
||||
if conversation.AssignedUserID.Valid {
|
||||
valueToCompare = strconv.Itoa(conversation.AssignedUserID.Int)
|
||||
}
|
||||
default:
|
||||
e.lo.Error("rule field not recognized", "field", rule.Field)
|
||||
return false
|
||||
}
|
||||
|
||||
// Lower case the value.
|
||||
conversationValue = strings.ToLower(conversationValue)
|
||||
|
||||
// Compare the conversation value with the rule's value based on the operator
|
||||
switch rule.Operator {
|
||||
case "equals":
|
||||
conditionMet = conversationValue == rule.Value
|
||||
case "not equal":
|
||||
conditionMet = conversationValue != rule.Value
|
||||
case "contains":
|
||||
conditionMet = strings.Contains(conversationValue, rule.Value)
|
||||
case "startsWith":
|
||||
conditionMet = strings.HasPrefix(conversationValue, rule.Value)
|
||||
case "endsWith":
|
||||
conditionMet = strings.HasSuffix(conversationValue, rule.Value)
|
||||
default:
|
||||
e.lo.Error("logical operator not recognized for evaluating rules", "operator", rule.Operator)
|
||||
return false
|
||||
if !rule.CaseSensitiveMatch {
|
||||
valueToCompare = strings.ToLower(valueToCompare)
|
||||
rule.Value = strings.ToLower(rule.Value)
|
||||
}
|
||||
|
||||
e.lo.Debug("comparing values", "conversation_value", valueToCompare, "rule_value", rule.Value)
|
||||
|
||||
switch rule.Operator {
|
||||
case models.RuleEquals:
|
||||
conditionMet = valueToCompare == rule.Value
|
||||
case models.RuleNotEqual:
|
||||
conditionMet = valueToCompare != rule.Value
|
||||
case models.RuleContains:
|
||||
conditionMet = strings.Contains(valueToCompare, rule.Value)
|
||||
case models.RuleNotContains:
|
||||
conditionMet = !strings.Contains(valueToCompare, rule.Value)
|
||||
case models.RuleSet:
|
||||
conditionMet = bool(len(valueToCompare) > 0)
|
||||
case models.RuleNotSet:
|
||||
conditionMet = !bool(len(valueToCompare) > 0)
|
||||
default:
|
||||
e.lo.Error("rule logical operator not recognized", "operator", rule.Operator)
|
||||
return false
|
||||
}
|
||||
return conditionMet
|
||||
}
|
||||
|
||||
@@ -140,12 +156,20 @@ func (e *Engine) applyAction(action models.RuleAction, conversation cmodels.Conv
|
||||
return err
|
||||
}
|
||||
case models.ActionAssignAgent:
|
||||
if err := e.conversationStore.UpdateStatus(conversation.UUID, []byte(action.Action)); err != nil {
|
||||
if err := e.conversationStore.UpdateUserAssignee(conversation.UUID, []byte(action.Action)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.messageStore.RecordStatusChange(action.Action, conversation.UUID, systeminfo.SystemUserUUID); err != nil {
|
||||
return err
|
||||
}
|
||||
case models.ActionSetPriority:
|
||||
if err := e.conversationStore.UpdatePriority(conversation.UUID, []byte(action.Action)); err != nil {
|
||||
return err
|
||||
}
|
||||
case models.ActionSetStatus:
|
||||
if err := e.conversationStore.UpdateStatus(conversation.UUID, []byte(action.Action)); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unrecognized rule action: %s", action.Type)
|
||||
}
|
||||
@@ -3,11 +3,33 @@ package models
|
||||
const (
|
||||
ActionAssignTeam = "assign_team"
|
||||
ActionAssignAgent = "assign_agent"
|
||||
OperatorAnd = "AND"
|
||||
OperatorOR = "OR"
|
||||
ActionSetStatus = "set_status"
|
||||
ActionSetPriority = "set_priority"
|
||||
|
||||
OperatorAnd = "AND"
|
||||
OperatorOR = "OR"
|
||||
|
||||
RuleContains = "contains"
|
||||
RuleNotContains = "not contains"
|
||||
RuleEquals = "equals"
|
||||
RuleNotEqual = "not equal"
|
||||
RuleSet = "set"
|
||||
RuleNotSet = "not set"
|
||||
|
||||
RuleTypeNewConversation = "new_conversation"
|
||||
RuleTypeConversationUpdate = "conversation_update"
|
||||
RuleTypeTimeTrigger = "time_trigger"
|
||||
|
||||
ConversationFieldSubject = "subject"
|
||||
ConversationFieldContent = "content"
|
||||
ConversationFieldStatus = "status"
|
||||
ConversationFieldPriority = "priority"
|
||||
ConversationFieldAssignedUserID = "assigned_user_id"
|
||||
ConversationFieldAssignedTeamID = "assigned_team_id"
|
||||
)
|
||||
|
||||
type Rule struct {
|
||||
Type string `json:"type" db:"type"`
|
||||
GroupOperator string `json:"group_operator" db:"group_operator"`
|
||||
Groups []RuleGroup `json:"groups" db:"groups"`
|
||||
Actions []RuleAction `json:"actions" db:"actions"`
|
||||
@@ -19,9 +41,10 @@ type RuleGroup struct {
|
||||
}
|
||||
|
||||
type RuleDetail struct {
|
||||
Field string `json:"field" db:"field"`
|
||||
Operator string `json:"operator" db:"operator"`
|
||||
Value string `json:"value" db:"value"`
|
||||
Field string `json:"field" db:"field"`
|
||||
Operator string `json:"operator" db:"operator"`
|
||||
Value string `json:"value" db:"value"`
|
||||
CaseSensitiveMatch bool `json:"case_sensitive_match" db:"case_sensitive_match"`
|
||||
}
|
||||
|
||||
type RuleAction struct {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
-- name: get-rules
|
||||
select rules
|
||||
select
|
||||
rules
|
||||
from automation_rules;
|
||||
@@ -76,6 +76,7 @@ type queries struct {
|
||||
GetUUID *sqlx.Stmt `query:"get-uuid"`
|
||||
GetInboxID *sqlx.Stmt `query:"get-inbox-id"`
|
||||
GetConversation *sqlx.Stmt `query:"get-conversation"`
|
||||
GetRecentConversations *sqlx.Stmt `query:"get-recent-conversations"`
|
||||
GetUnassigned *sqlx.Stmt `query:"get-unassigned"`
|
||||
GetConversationParticipants *sqlx.Stmt `query:"get-conversation-participants"`
|
||||
GetConversations string `query:"get-conversations"`
|
||||
@@ -124,15 +125,29 @@ func (c *Manager) Create(contactID int, inboxID int, meta []byte) (int, string,
|
||||
}
|
||||
|
||||
func (c *Manager) Get(uuid string) (models.Conversation, error) {
|
||||
var conv models.Conversation
|
||||
if err := c.q.GetConversation.Get(&conv, uuid); err != nil {
|
||||
var conversation models.Conversation
|
||||
if err := c.q.GetConversation.Get(&conversation, uuid); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return conv, fmt.Errorf("conversation not found")
|
||||
c.lo.Error("conversation not found", "uuid", uuid)
|
||||
return conversation, fmt.Errorf("conversation not found")
|
||||
}
|
||||
c.lo.Error("fetching conversation from DB", "error", err)
|
||||
return conv, fmt.Errorf("error fetching conversation")
|
||||
return conversation, fmt.Errorf("error fetching conversation")
|
||||
}
|
||||
return conv, nil
|
||||
return conversation, nil
|
||||
}
|
||||
|
||||
func (c *Manager) GetRecentConversations(time time.Time) ([]models.Conversation, error) {
|
||||
var conversations []models.Conversation
|
||||
if err := c.q.GetRecentConversations.Select(&conversations, time); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.lo.Error("conversations not found", "created_after", time)
|
||||
return conversations, fmt.Errorf("conversation not found")
|
||||
}
|
||||
c.lo.Error("fetching conversation from DB", "error", err)
|
||||
return conversations, fmt.Errorf("error fetching conversation")
|
||||
}
|
||||
return conversations, nil
|
||||
}
|
||||
|
||||
func (c *Manager) UpdateAssigneeLastSeen(uuid string) error {
|
||||
|
||||
@@ -69,6 +69,40 @@ LEFT JOIN users u ON u.id = c.assigned_user_id
|
||||
LEFT JOIN teams at ON at.id = c.assigned_team_id
|
||||
WHERE c.uuid = $1;
|
||||
|
||||
|
||||
-- name: get-recent-conversations
|
||||
SELECT
|
||||
c.created_at,
|
||||
c.updated_at,
|
||||
c.closed_at,
|
||||
c.resolved_at,
|
||||
c.priority,
|
||||
c.status,
|
||||
c.uuid,
|
||||
c.reference_number,
|
||||
c.first_reply_at,
|
||||
ct.uuid AS contact_uuid,
|
||||
ct.first_name as first_name,
|
||||
ct.last_name as last_name,
|
||||
ct.email as email,
|
||||
ct.phone_number as phone_number,
|
||||
ct.avatar_url as avatar_url,
|
||||
u.uuid AS assigned_user_uuid,
|
||||
at.uuid AS assigned_team_uuid,
|
||||
(SELECT COALESCE(
|
||||
(SELECT json_agg(t.name)
|
||||
FROM tags t
|
||||
INNER JOIN conversation_tags ct ON ct.tag_id = t.id
|
||||
WHERE ct.conversation_id = c.id),
|
||||
'[]'::json
|
||||
)) AS tags
|
||||
FROM conversations c
|
||||
JOIN contacts ct ON c.contact_id = ct.id
|
||||
LEFT JOIN users u ON u.id = c.assigned_user_id
|
||||
LEFT JOIN teams at ON at.id = c.assigned_team_id
|
||||
WHERE c.created_at > $1 and c.uuid = 'e2f69c9f-17f5-4d09-9aae-12c2a79046a2';
|
||||
|
||||
|
||||
-- name: get-id
|
||||
SELECT id from conversations where uuid = $1;
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/abhinavxd/artemis/internal/automation"
|
||||
"github.com/abhinavxd/artemis/internal/contact"
|
||||
"github.com/abhinavxd/artemis/internal/conversation"
|
||||
cmodels "github.com/abhinavxd/artemis/internal/conversation/models"
|
||||
"github.com/abhinavxd/artemis/internal/dbutil"
|
||||
"github.com/abhinavxd/artemis/internal/inbox"
|
||||
"github.com/abhinavxd/artemis/internal/message/models"
|
||||
@@ -457,13 +456,11 @@ func (m *Manager) processIncomingMessage(in models.IncomingMessage) error {
|
||||
m.conversationMgr.UpdateLastMessage(in.Message.ConversationID, in.Message.ConversationUUID, content, in.Message.CreatedAt)
|
||||
}
|
||||
|
||||
// Evaluate automation rules for this new conversation.
|
||||
// Evaluate automation rules for this conversation.
|
||||
if isNewConversation {
|
||||
m.automationEngine.EvaluateRules(cmodels.Conversation{
|
||||
UUID: in.Message.ConversationUUID,
|
||||
FirstMessage: in.Message.Content,
|
||||
Subject: in.Message.Subject,
|
||||
})
|
||||
m.automationEngine.EvaluateNewConversationRules(in.Message.ConversationUUID)
|
||||
} else {
|
||||
m.automationEngine.EvaluateConversationUpdateRules(in.Message.ConversationUUID)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user