/* Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments Copyright (C) ITsysCOM GmbH This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see */ package engine import ( "bytes" "encoding/json" "errors" "fmt" "html/template" "net/http" "net/smtp" "reflect" "slices" "sort" "strconv" "strings" "time" "github.com/cgrates/birpc" "github.com/cgrates/birpc/context" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/guardian" "github.com/cgrates/cgrates/utils" "github.com/cgrates/rpcclient" "github.com/mitchellh/mapstructure" ) // Action will be filled for each tariff plan with the bonus value for received calls minutes. type Action struct { Id string ActionType string ExtraParameters string Filters []string ExpirationString string // must stay as string because it can have relative values like 1month Weight float64 Balance *BalanceFilter balanceValue float64 // balance value after action execution, used with cdrlog } // Clone returns a clone of the action func (a *Action) Clone() (cln *Action) { if a == nil { return } var fltrs []string if a.Filters != nil { fltrs = slices.Clone(a.Filters) } return &Action{ Id: a.Id, ActionType: a.ActionType, ExtraParameters: a.ExtraParameters, Filters: fltrs, ExpirationString: a.ExpirationString, Weight: a.Weight, Balance: a.Balance.Clone(), } } // SharedActionsData holds shared data for processing actions within a group. type SharedActionsData struct { // idx represents the current iteration index of the action being processed. // It is used as a unique key to link stored data to the correct action, since actions // within a group do not have a unique identifier. idx int refTime time.Time // reference time, constant for all actions in the group transferBal map[int]transferInfo // data for *transfer_balance actions remBal map[int]BalanceSummaries // data for *remove_balance actions remExp map[int]BalanceSummaries // data for *remove_expired actions cdrLog bool // indicates if a *cdrlog action is in the group } // NewSharedActionsData initializes SharedActionsData based on the provided actions. func NewSharedActionsData(acts Actions) SharedActionsData { sd := SharedActionsData{ refTime: time.Now(), cdrLog: acts.HasAction(utils.CDRLog), } if acts.HasAction(utils.MetaRemoveBalance) { sd.remBal = make(map[int]BalanceSummaries) } if acts.HasAction(utils.MetaRemoveExpired) { sd.remExp = make(map[int]BalanceSummaries) } if acts.HasAction(utils.MetaTransferBalance) { sd.transferBal = make(map[int]transferInfo) } return sd } // transferInfo holds information for *transfer_balance actions. type transferInfo struct { srcAccID string // source account ID destAccID string // destination account ID units float64 // number of units to transfer srcBal *BalanceSummary // source balance summary destBal *BalanceSummary // destination balance summary } type ActionConnCfg struct { ConnIDs []string } func newActionConnCfg(source, action string, cfg *config.CGRConfig) ActionConnCfg { sessionActions := []string{ utils.MetaAlterSessions, utils.MetaForceDisconnectSessions, } dynamicActions := []string{ utils.MetaDynamicThreshold, utils.MetaDynamicStats, utils.MetaDynamicAttribute, utils.MetaDynamicActionPlan, utils.MetaDynamicActionPlanAccounts, utils.MetaDynamicAction, utils.MetaDynamicDestination, utils.MetaDynamicFilter, utils.MetaDynamicRoute, utils.MetaDynamicRanking, utils.MetaDynamicRatingProfile, utils.MetaDynamicTrend, utils.MetaDynamicResource, utils.MetaDynamicActionTrigger, } act := ActionConnCfg{} switch source { case utils.ThresholdS: switch { case slices.Contains(sessionActions, action): act.ConnIDs = cfg.ThresholdSCfg().SessionSConns case slices.Contains(dynamicActions, action): act.ConnIDs = cfg.ThresholdSCfg().ApierSConns } case utils.RALs: switch { case slices.Contains(sessionActions, action): act.ConnIDs = cfg.RalsCfg().SessionSConns } } return act } type actionTypeFunc func(*Account, *Action, Actions, *FilterS, any, SharedActionsData, ActionConnCfg) error var actionFuncMap = make(map[string]actionTypeFunc) var accountActions = map[string]bool{ utils.MetaTopUp: true, utils.MetaDebit: true, utils.MetaResetTriggers: true, utils.MetaSetRecurrent: true, utils.MetaUnsetRecurrent: true, utils.MetaAllowNegative: true, utils.MetaDenyNegative: true, utils.MetaResetAccount: true, utils.MetaTopUpReset: true, utils.MetaDebitReset: true, utils.MetaTransferBalance: true, utils.MetaResetCounters: true, utils.MetaEnableAccount: true, utils.MetaDisableAccount: true, utils.MetaTransferMonetaryDefault: true, utils.MetaPublishAccount: true, utils.MetaCDRAccount: true, utils.SetExpiry: true, utils.TopUpZeroNegative: true, utils.MetaSetBalance: true, utils.MetaRemoveBalance: true, } func init() { actionFuncMap[utils.MetaLog] = logAction actionFuncMap[utils.MetaResetTriggers] = resetTriggersAction actionFuncMap[utils.CDRLog] = cdrLogAction actionFuncMap[utils.MetaSetRecurrent] = setRecurrentAction actionFuncMap[utils.MetaUnsetRecurrent] = unsetRecurrentAction actionFuncMap[utils.MetaAllowNegative] = allowNegativeAction actionFuncMap[utils.MetaDenyNegative] = denyNegativeAction actionFuncMap[utils.MetaResetAccount] = resetAccountAction actionFuncMap[utils.MetaTopUpReset] = topupResetAction actionFuncMap[utils.MetaTopUp] = topupAction actionFuncMap[utils.MetaDebitReset] = debitResetAction actionFuncMap[utils.MetaDebit] = debitAction actionFuncMap[utils.MetaTransferBalance] = transferBalanceAction actionFuncMap[utils.MetaResetCounters] = resetCountersAction actionFuncMap[utils.MetaEnableAccount] = enableAccountAction actionFuncMap[utils.MetaDisableAccount] = disableAccountAction actionFuncMap[utils.MetaMailAsync] = mailAsync actionFuncMap[utils.MetaSetDDestinations] = setddestinations actionFuncMap[utils.MetaRemoveAccount] = removeAccountAction actionFuncMap[utils.MetaRemoveBalance] = removeBalanceAction actionFuncMap[utils.MetaSetBalance] = setBalanceAction actionFuncMap[utils.MetaTransferMonetaryDefault] = transferMonetaryDefaultAction actionFuncMap[utils.MetaCgrRpc] = cgrRPCAction actionFuncMap[utils.MetaAlterSessions] = alterSessionsAction actionFuncMap[utils.MetaForceDisconnectSessions] = forceDisconnectSessionsAction actionFuncMap[utils.TopUpZeroNegative] = topupZeroNegativeAction actionFuncMap[utils.SetExpiry] = setExpiryAction actionFuncMap[utils.MetaPublishAccount] = publishAccount actionFuncMap[utils.MetaRemoveSessionCosts] = removeSessionCosts actionFuncMap[utils.MetaRemoveExpired] = removeExpired actionFuncMap[utils.MetaCDRAccount] = resetAccountCDR actionFuncMap[utils.MetaExport] = export actionFuncMap[utils.MetaResetThreshold] = resetThreshold actionFuncMap[utils.MetaResetStatQueue] = resetStatQueue actionFuncMap[utils.MetaRemoteSetAccount] = remoteSetAccount actionFuncMap[utils.MetaDynamicThreshold] = dynamicThreshold actionFuncMap[utils.MetaDynamicStats] = dynamicStats actionFuncMap[utils.MetaDynamicAttribute] = dynamicAttribute actionFuncMap[utils.MetaDynamicActionPlan] = dynamicActionPlan actionFuncMap[utils.MetaDynamicActionPlanAccounts] = dynamicActionPlanAccount actionFuncMap[utils.MetaDynamicAction] = dynamicAction actionFuncMap[utils.MetaDynamicDestination] = dynamicDestination actionFuncMap[utils.MetaDynamicFilter] = dynamicFilter actionFuncMap[utils.MetaDynamicRoute] = dynamicRoute actionFuncMap[utils.MetaDynamicRanking] = dynamicRanking actionFuncMap[utils.MetaDynamicRatingProfile] = dynamicRatingProfile actionFuncMap[utils.MetaDynamicTrend] = dynamicTrend actionFuncMap[utils.MetaDynamicResource] = dynamicResource actionFuncMap[utils.MetaDynamicActionTrigger] = dynamicActionTrigger } func getActionFunc(typ string) (f actionTypeFunc, exists bool) { f, exists = actionFuncMap[typ] return } func RegisterActionFunc(action string, f actionTypeFunc) { actionFuncMap[action] = f } // transferBalanceAction transfers units between accounts' balances. // It ensures both source and destination balances are of the same type and non-expired. // Destination account and balance IDs, and optionally a reference value, are obtained from Action's ExtraParameters. // If a reference value is specified, the transfer ensures the destination balance reaches this value. // If the destination account is different from the source, it is locked during the transfer. func transferBalanceAction(srcAcc *Account, act *Action, acts Actions, fltrS *FilterS, _ any, sd SharedActionsData, _ ActionConnCfg) error { if srcAcc == nil { return errors.New("source account is nil") } if act.Balance.ID == nil { return errors.New("source balance ID is missing") } if act.ExtraParameters == "" { return errors.New("ExtraParameters used to identify the destination balance are missing") } if len(srcAcc.BalanceMap) == 0 { return fmt.Errorf("account %s has no balances to transfer from", srcAcc.ID) } srcBalance, srcBalanceType := srcAcc.FindBalanceByID(*act.Balance.ID) if srcBalance == nil || srcBalance.IsExpiredAt(time.Now()) { return errors.New("source balance not found or expired") } destInfo := struct { AccID string `json:"DestinationAccountID"` BalID string `json:"DestinationBalanceID"` RefVal *float64 `json:"DestinationReferenceValue"` }{} if err := json.Unmarshal([]byte(act.ExtraParameters), &destInfo); err != nil { return err } // Lock the destination account if different from source, otherwise // pass without lock key and timeout. diffAcnts := srcAcc.ID != destInfo.AccID var lockTimeout time.Duration lockKeys := make([]string, 0, 1) if diffAcnts { lockTimeout = config.CgrConfig().GeneralCfg().LockingTimeout lockKeys = append(lockKeys, utils.AccountPrefix+destInfo.AccID) } // This guard is meant to lock the destination account as we are making changes // to it. It is needed for the source account due to it being locked from outside // this function. guardErr := guardian.Guardian.Guard(func() error { var destAcc *Account switch diffAcnts { case true: var err error if destAcc, err = dm.GetAccount(destInfo.AccID); err != nil { return fmt.Errorf("retrieving destination account failed: %w", err) } case false: destAcc = srcAcc } if destAcc.BalanceMap == nil { destAcc.BalanceMap = make(map[string]Balances) } // Look for the destination balance only through balances of the same type as the source balance. destBalance := destAcc.GetBalanceWithID(srcBalanceType, destInfo.BalID) if destBalance != nil && destBalance.IsExpiredAt(time.Now()) { return errors.New("destination balance expired") } if destBalance == nil { // Destination Balance was not found. Create it and add it to the balance map. destBalance = &Balance{ ID: destInfo.BalID, Uuid: utils.GenUUID(), } destAcc.BalanceMap[srcBalanceType] = append(destAcc.BalanceMap[srcBalanceType], destBalance) } // If DestinationReferenceValue is specified adjust transferUnits to make the // destination balance match the DestinationReferenceValue. transferUnits := act.Balance.GetValue() if destInfo.RefVal != nil { transferUnits = *destInfo.RefVal - destBalance.Value } if transferUnits == 0 { return errors.New("transfer amount is missing or 0") } if srcBalance.ID != utils.MetaDefault && transferUnits > srcBalance.Value { return fmt.Errorf("insufficient credits in source balance %q (account %q) for transfer of %.2f units", srcBalance.ID, srcAcc.ID, transferUnits) } if destBalance.ID != utils.MetaDefault && -transferUnits > destBalance.Value { return fmt.Errorf("insufficient credits in destination balance %q (account %q) for transfer of %.2f units", destBalance.ID, destAcc.ID, transferUnits) } srcBalance.SubtractValue(transferUnits) srcBalance.dirty = true destBalance.AddValue(transferUnits) destBalance.dirty = true if sd.cdrLog { sd.transferBal[sd.idx] = transferInfo{ srcAccID: utils.NewTenantID(srcAcc.ID).ID, destAccID: utils.NewTenantID(destAcc.ID).ID, units: transferUnits, srcBal: srcBalance.AsBalanceSummary(srcBalanceType), destBal: destBalance.AsBalanceSummary(srcBalanceType), } sd.transferBal[sd.idx].srcBal.Initial = sd.transferBal[sd.idx].srcBal.Value + transferUnits sd.transferBal[sd.idx].destBal.Initial = sd.transferBal[sd.idx].destBal.Value - transferUnits } if diffAcnts { destAcc.InitCounters() destAcc.ExecuteActionTriggers(act, fltrS) if err := dm.SetAccount(destAcc); err != nil { return fmt.Errorf("updating destination account failed: %w", err) } } return nil }, lockTimeout, lockKeys...) if guardErr != nil { return guardErr } // Execute action triggers for the source account. // This account will be updated in the parent function. srcAcc.InitCounters() srcAcc.ExecuteActionTriggers(act, fltrS) return nil } func logAction(ub *Account, _ *Action, _ Actions, _ *FilterS, extraData any, _ SharedActionsData, _ ActionConnCfg) (err error) { switch { case ub != nil: body, _ := json.Marshal(ub) utils.Logger.Info(fmt.Sprintf("LOG Account: %s", body)) case extraData != nil: body, _ := json.Marshal(extraData) utils.Logger.Info(fmt.Sprintf("LOG ExtraData: %s", body)) } return } func cdrLogAction(acc *Account, a *Action, acs Actions, _ *FilterS, extraData any, sd SharedActionsData, _ ActionConnCfg) (err error) { if len(config.CgrConfig().SchedulerCfg().CDRsConns) == 0 { return errors.New("No connection with CDR Server") } defaultTemplate := map[string]config.RSRParsers{ utils.ToR: config.NewRSRParsersMustCompile(utils.DynamicDataPrefix+utils.MetaAcnt+utils.NestingSep+utils.BalanceType, utils.InfieldSep), utils.OriginHost: config.NewRSRParsersMustCompile("127.0.0.1", utils.InfieldSep), utils.RequestType: config.NewRSRParsersMustCompile(utils.MetaNone, utils.InfieldSep), utils.Tenant: config.NewRSRParsersMustCompile(utils.DynamicDataPrefix+utils.MetaAcnt+utils.NestingSep+utils.Tenant, utils.InfieldSep), utils.AccountField: config.NewRSRParsersMustCompile(utils.DynamicDataPrefix+utils.MetaAcnt+utils.NestingSep+utils.AccountField, utils.InfieldSep), utils.Subject: config.NewRSRParsersMustCompile(utils.DynamicDataPrefix+utils.MetaAcnt+utils.NestingSep+utils.AccountField, utils.InfieldSep), utils.Cost: config.NewRSRParsersMustCompile(utils.DynamicDataPrefix+utils.MetaAct+utils.NestingSep+utils.ActionValue, utils.InfieldSep), } template := make(map[string]string) // overwrite default template if a.ExtraParameters != "" { if err = json.Unmarshal([]byte(a.ExtraParameters), &template); err != nil { return err } for field, rsr := range template { if defaultTemplate[field], err = config.NewRSRParsers(rsr, config.CgrConfig().GeneralCfg().RSRSep); err != nil { return err } } } //In case that we have extra data we populate default templates mapExtraData, _ := extraData.(map[string]any) for key, val := range mapExtraData { if defaultTemplate[key], err = config.NewRSRParsers(utils.IfaceAsString(val), config.CgrConfig().GeneralCfg().RSRSep); err != nil { return err } } // set stored cdr values var cdrs []*CDR for i, action := range acs { if !slices.Contains( []string{utils.MetaDebit, utils.MetaDebitReset, utils.MetaTopUp, utils.MetaTopUpReset, utils.MetaSetBalance, utils.MetaRemoveBalance, utils.MetaRemoveExpired, utils.MetaTransferBalance, }, action.ActionType) || action.Balance == nil { continue // Only log specific actions } cdr := &CDR{ RunID: action.ActionType, Source: utils.CDRLog, SetupTime: sd.refTime, AnswerTime: sd.refTime, OriginID: utils.GenUUID(), ExtraFields: make(map[string]string), PreRated: true, Usage: time.Duration(1), } cdr.CGRID = utils.Sha1(cdr.OriginID, cdr.OriginHost) cdrLogProvider := newCdrLogProvider(acc, action) elem := reflect.ValueOf(cdr).Elem() for key, rsrFlds := range defaultTemplate { parsedValue, err := rsrFlds.ParseDataProvider(cdrLogProvider) if err != nil { return err } field := elem.FieldByName(key) if field.IsValid() && field.CanSet() { switch field.Kind() { case reflect.Float64: value, err := strconv.ParseFloat(parsedValue, 64) if err != nil { continue } field.SetFloat(value) case reflect.String: field.SetString(parsedValue) case reflect.Int64: value, err := strconv.ParseInt(parsedValue, 10, 64) if err != nil { continue } field.SetInt(value) } } else { // invalid fields go in extraFields of CDR cdr.ExtraFields[key] = parsedValue } } // Function to create (and append) CDRs based on each BalanceSummary element. processBalances := func(balances BalanceSummaries) error { if len(balances) == 0 { return utils.ErrNotFound } for _, b := range balances { // Create a new CDR instance for each balance that meets the condition. newCDR := *cdr // Copy CDR's values to a new CDR instance. newCDR.Cost = b.Value newCDR.OriginID = utils.GenUUID() // OriginID must be unique for every CDR. newCDR.CGRID = utils.Sha1(newCDR.OriginID, newCDR.OriginHost) newCDR.ToR = b.Type // Clone the ExtraFields map to avoid changing its value in // CDRs appended previously. newCDR.ExtraFields = make(map[string]string, len(cdr.ExtraFields)+1) for key, val := range cdr.ExtraFields { newCDR.ExtraFields[key] = val } newCDR.ExtraFields[utils.BalanceID] = b.ID cdrs = append(cdrs, &newCDR) // Append the address of the new instance. } return nil } // If the action is of type *remove_balance or *remove_expired, for each matched balance, // assign the balance values to the CDR cost and append to the list of CDRs. switch action.ActionType { case utils.MetaRemoveBalance: if err = processBalances(sd.remBal[i]); err != nil { return err } continue case utils.MetaRemoveExpired: if err = processBalances(sd.remExp[i]); err != nil { return err } continue case utils.MetaTransferBalance: cdr.Account = sd.transferBal[i].srcAccID cdr.Destination = sd.transferBal[i].destAccID cdr.Cost = sd.transferBal[i].units cdr.ExtraFields[utils.SourceBalanceSummary] = utils.ToJSON(sd.transferBal[i].srcBal) cdr.ExtraFields[utils.DestinationBalanceSummary] = utils.ToJSON(sd.transferBal[i].destBal) } cdrs = append(cdrs, cdr) } events := make([]*utils.CGREvent, 0, len(cdrs)) for _, cdr := range cdrs { events = append(events, cdr.AsCGREvent()) } var reply string if err := connMgr.Call(context.TODO(), config.CgrConfig().SchedulerCfg().CDRsConns, utils.CDRsV1ProcessEvents, &ArgV1ProcessEvents{ Flags: []string{utils.ConcatenatedKey(utils.MetaChargers, "false")}, CGREvents: events, }, &reply); err != nil { return err } b, _ := json.Marshal(cdrs) a.ExpirationString = string(b) // testing purpose only return nil } func resetTriggersAction(ub *Account, a *Action, _ Actions, fltrS *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { if ub == nil { return errors.New("nil account") } ub.ResetActionTriggers(a, fltrS) return } func setRecurrentAction(ub *Account, a *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { if ub == nil { return errors.New("nil account") } ub.SetRecurrent(a, true) return } func unsetRecurrentAction(ub *Account, a *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { if ub == nil { return errors.New("nil account") } ub.SetRecurrent(a, false) return } func allowNegativeAction(ub *Account, _ *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { if ub == nil { return errors.New("nil account") } ub.AllowNegative = true return } func denyNegativeAction(ub *Account, _ *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { if ub == nil { return errors.New("nil account") } ub.AllowNegative = false return } func resetAccountAction(ub *Account, _ *Action, _ Actions, fltrS *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { if ub == nil { return errors.New("nil account") } return genericReset(ub, fltrS) } func topupResetAction(ub *Account, a *Action, _ Actions, fltrS *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { if ub == nil { return errors.New("nil account") } if ub.BalanceMap == nil { // Init the map since otherwise will get error if nil ub.BalanceMap = make(map[string]Balances) } c := a.Clone() genericMakeNegative(c) err = genericDebit(ub, c, true, fltrS) a.balanceValue = c.balanceValue return } func topupAction(ub *Account, a *Action, _ Actions, fltrS *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { if ub == nil { return errors.New("nil account") } c := a.Clone() genericMakeNegative(c) err = genericDebit(ub, c, false, fltrS) a.balanceValue = c.balanceValue return } func debitResetAction(ub *Account, a *Action, _ Actions, fltrS *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { if ub == nil { return errors.New("nil account") } if ub.BalanceMap == nil { // Init the map since otherwise will get error if nil ub.BalanceMap = make(map[string]Balances) } return genericDebit(ub, a, true, fltrS) } func debitAction(ub *Account, a *Action, _ Actions, fltrS *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { if ub == nil { return errors.New("nil account") } err = genericDebit(ub, a, false, fltrS) return } func resetCountersAction(ub *Account, a *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { if ub == nil { return errors.New("nil account") } if ub.UnitCounters != nil { ub.UnitCounters.resetCounters(a) } return } func genericMakeNegative(a *Action) { if a.Balance != nil && a.Balance.GetValue() > 0 { // only apply if not allready negative a.Balance.SetValue(-a.Balance.GetValue()) } } func genericDebit(ub *Account, a *Action, reset bool, fltrS *FilterS) (err error) { if ub == nil { return errors.New("nil account") } if ub.BalanceMap == nil { ub.BalanceMap = make(map[string]Balances) } return ub.debitBalanceAction(a, reset, false, fltrS) } func enableAccountAction(acc *Account, _ *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { if acc == nil { return errors.New("nil account") } acc.Disabled = false return } func disableAccountAction(acc *Account, _ *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { if acc == nil { return errors.New("nil account") } acc.Disabled = true return } /*func enableDisableBalanceAction(ub *Account, sq *CDRStatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { return errors.New("nil account") } ub.enableDisableBalanceAction(a) return }*/ func genericReset(ub *Account, fltrS *FilterS) error { for k := range ub.BalanceMap { ub.BalanceMap[k] = Balances{&Balance{Value: 0}} } ub.InitCounters() ub.ResetActionTriggers(nil, fltrS) return nil } // Mails the balance hitting the threshold towards predefined list of addresses func mailAsync(ub *Account, a *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) error { cgrCfg := config.CgrConfig() params := strings.Split(a.ExtraParameters, string(utils.CSVSep)) if len(params) == 0 { return errors.New("Unconfigured parameters for mail action") } toAddrs := strings.Split(params[0], string(utils.FallbackSep)) toAddrStr := "" for idx, addr := range toAddrs { if idx != 0 { toAddrStr += ", " } toAddrStr += addr } var message []byte if ub != nil { balJsn, err := json.Marshal(ub) if err != nil { return err } message = []byte(fmt.Sprintf("To: %s\r\nSubject: [CGR Notification] Threshold hit on Balance: %s\r\n\r\nTime: \r\n\t%s\r\n\r\nBalance:\r\n\t%s\r\n\r\nYours faithfully,\r\nCGR Balance Monitor\r\n", toAddrStr, ub.ID, time.Now(), balJsn)) } var auth smtp.Auth if len(cgrCfg.MailerCfg().MailerAuthUser) > 0 || len(cgrCfg.MailerCfg().MailerAuthPass) > 0 { //use auth if user/pass not empty in config auth = smtp.PlainAuth("", cgrCfg.MailerCfg().MailerAuthUser, cgrCfg.MailerCfg().MailerAuthPass, strings.Split(cgrCfg.MailerCfg().MailerServer, ":")[0]) // We only need host part, so ignore port } go func() { for i := 0; i < 5; i++ { // Loop so we can increase the success rate on best effort if err := smtp.SendMail(cgrCfg.MailerCfg().MailerServer, auth, cgrCfg.MailerCfg().MailerFromAddr, toAddrs, message); err == nil { break } else if i == 4 { if ub != nil { utils.Logger.Warning(fmt.Sprintf(" WARNING: Failed emailing, params: [%s], error: [%s], BalanceId: %s", a.ExtraParameters, err.Error(), ub.ID)) } break } time.Sleep(time.Duration(i) * time.Minute) } }() return nil } func setddestinations(ub *Account, a *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { var ddcDestID string for _, bchain := range ub.BalanceMap { for _, b := range bchain { for destID := range b.DestinationIDs { if strings.HasPrefix(destID, utils.MetaDDC) { ddcDestID = destID break } } if ddcDestID != "" { break } } if ddcDestID != "" { break } } if ddcDestID != "" { destinations := utils.NewStringSet(nil) for _, statID := range strings.Split(a.ExtraParameters, utils.InfieldSep) { if statID == utils.EmptyString { continue } var sts StatQueue if err = connMgr.Call(context.TODO(), config.CgrConfig().RalsCfg().StatSConns, utils.StatSv1GetStatQueue, &utils.TenantIDWithAPIOpts{ TenantID: &utils.TenantID{ Tenant: config.CgrConfig().GeneralCfg().DefaultTenant, ID: statID, }, }, &sts); err != nil { return } ddcIface, has := sts.SQMetrics[utils.MetaDDC] if !has { continue } ddcMetric := ddcIface.(*StatDDC) // make slice from prefixes // Review here prefixes for p := range ddcMetric.FieldValues { destinations.Add(p) } } newDest := &Destination{Id: ddcDestID, Prefixes: destinations.AsSlice()} oldDest, err := dm.GetDestination(ddcDestID, true, true, utils.NonTransactional) if err != nil { return err } // update destid in storage if err = dm.SetDestination(newDest, utils.NonTransactional); err != nil { return err } if err = dm.CacheDataFromDB(utils.DestinationPrefix, []string{ddcDestID}, true); err != nil { return err } if oldDest != nil { if err = dm.UpdateReverseDestination(oldDest, newDest, utils.NonTransactional); err != nil { return err } } } else { return utils.ErrNotFound } return nil } func removeAccountAction(ub *Account, a *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) error { var accID string if ub != nil { accID = ub.ID } else { accountInfo := struct { Tenant string Account string }{} if a.ExtraParameters != "" { if err := json.Unmarshal([]byte(a.ExtraParameters), &accountInfo); err != nil { return err } } accID = utils.ConcatenatedKey(accountInfo.Tenant, accountInfo.Account) } if accID == "" { return utils.ErrInvalidKey } if err := dm.RemoveAccount(accID); err != nil { utils.Logger.Err(fmt.Sprintf("Could not remove account Id: %s: %v", accID, err)) return err } return guardian.Guardian.Guard(func() error { acntAPids, err := dm.GetAccountActionPlans(accID, true, true, utils.NonTransactional) if err != nil && err != utils.ErrNotFound { utils.Logger.Err(fmt.Sprintf("Could not get action plans: %s: %v", accID, err)) return err } for _, apID := range acntAPids { ap, err := dm.GetActionPlan(apID, true, true, utils.NonTransactional) if err != nil { utils.Logger.Err(fmt.Sprintf("Could not retrieve action plan: %s: %v", apID, err)) return err } delete(ap.AccountIDs, accID) if err := dm.SetActionPlan(apID, ap, true, utils.NonTransactional); err != nil { utils.Logger.Err(fmt.Sprintf("Could not save action plan: %s: %v", apID, err)) return err } } if err = dm.CacheDataFromDB(utils.ActionPlanPrefix, acntAPids, true); err != nil { return err } if err = dm.RemAccountActionPlans(accID, nil); err != nil { return err } if err = dm.CacheDataFromDB(utils.AccountActionPlansPrefix, []string{accID}, true); err != nil && err.Error() != utils.ErrNotFound.Error() { return err } return nil }, config.CgrConfig().GeneralCfg().LockingTimeout, utils.ActionPlanPrefix) } func removeBalanceAction(acc *Account, a *Action, acts Actions, _ *FilterS, _ any, sd SharedActionsData, _ ActionConnCfg) error { if acc == nil { return fmt.Errorf("nil account for %s action", utils.ToJSON(a)) } found := false for bType, bChain := range acc.BalanceMap { for i := 0; i < len(bChain); i++ { if bChain[i].MatchFilter(a.Balance, bType, false, false) { if sd.cdrLog { // If *cdrlog action is present, add the balance summary to remBal in SharedActionsData // for CDR creation. sd.remBal[sd.idx] = append(sd.remBal[sd.idx], bChain[i].AsBalanceSummary(bType)) } // Remove balance without preserving order. bChain[i] = bChain[len(bChain)-1] bChain = bChain[:len(bChain)-1] i-- found = true } } acc.BalanceMap[bType] = bChain } if !found { return utils.ErrNotFound } return nil } func setBalanceAction(ub *Account, a *Action, _ Actions, fltrS *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) error { if ub == nil { return fmt.Errorf("nil account for %s action", utils.ToJSON(a)) } return ub.setBalanceAction(a, fltrS) } func transferMonetaryDefaultAction(ub *Account, a *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) error { if ub == nil { utils.Logger.Err("*transfer_monetary_default called without account") return utils.ErrAccountNotFound } if _, exists := ub.BalanceMap[utils.MetaMonetary]; !exists { return utils.ErrNotFound } defaultBalance := ub.GetDefaultMoneyBalance() bChain := ub.BalanceMap[utils.MetaMonetary] for _, balance := range bChain { if balance.Uuid != defaultBalance.Uuid && balance.ID != defaultBalance.ID && // extra caution balance.MatchFilter(a.Balance, "", false, false) { if balance.Value > 0 { defaultBalance.Value += balance.Value balance.Value = 0 } } } return nil } // RPCRequest used by rpc action type RPCRequest struct { Address string Transport string Method string Attempts int Async bool Params map[string]any } /* << .Object.Property >> Property can be a attribute or a method both used without () Please also note the initial dot . Currently there are following objects that can be used: Account - the account that this action is called on Action - the action with all it's attributs Actions - the list of actions in the current action set Sq - CDRStatsQueueTriggered object We can actually use everythiong that go templates offer. You can read more here: https://golang.org/pkg/text/template/ */ func cgrRPCAction(ub *Account, a *Action, acs Actions, _ *FilterS, extraData any, _ SharedActionsData, _ ActionConnCfg) (err error) { // parse template tmpl := template.New("extra_params") tmpl.Delims("<<", ">>") if tmpl, err = tmpl.Parse(a.ExtraParameters); err != nil { utils.Logger.Err(fmt.Sprintf("error parsing *cgr_rpc template: %s", err.Error())) return } var buf bytes.Buffer if err = tmpl.Execute(&buf, struct { Account *Account Action *Action Actions Actions ExtraData any }{ub, a, acs, extraData}); err != nil { utils.Logger.Err(fmt.Sprintf("error executing *cgr_rpc template %s:", err.Error())) return } var req RPCRequest if err = json.Unmarshal(buf.Bytes(), &req); err != nil { return } var params *utils.RpcParams if params, err = utils.GetRpcParams(req.Method); err != nil { return } var client birpc.ClientConnector if req.Address == utils.MetaInternal { client = params.Object.(birpc.ClientConnector) } else if client, err = rpcclient.NewRPCClient(context.TODO(), utils.TCP, req.Address, false, "", "", "", req.Attempts, 0, config.CgrConfig().GeneralCfg().MaxReconnectInterval, utils.FibDuration, config.CgrConfig().GeneralCfg().ConnectTimeout, config.CgrConfig().GeneralCfg().ReplyTimeout, req.Transport, nil, false, nil); err != nil { return } // Decode's output parameter requires a pointer. if reflect.TypeOf(params.InParam).Kind() == reflect.Pointer { err = mapstructure.Decode(req.Params, params.InParam) } else { err = mapstructure.Decode(req.Params, ¶ms.InParam) } if err != nil { utils.Logger.Info("<*cgr_rpc> err: " + err.Error()) return } if params.InParam == nil { utils.Logger.Info(fmt.Sprintf("<*cgr_rpc> nil params err: req.Params: %+v params: %+v", req.Params, params)) return utils.ErrParserError } utils.Logger.Info(fmt.Sprintf("<*cgr_rpc> calling: %s with: %s and result %v", req.Method, utils.ToJSON(params.InParam), params.OutParam)) if !req.Async { err = client.Call(context.TODO(), req.Method, params.InParam, params.OutParam) utils.Logger.Info(fmt.Sprintf("<*cgr_rpc> result: %s err: %v", utils.ToJSON(params.OutParam), err)) return } go func() { err := client.Call(context.TODO(), req.Method, params.InParam, params.OutParam) utils.Logger.Info(fmt.Sprintf("<*cgr_rpc> result: %s err: %v", utils.ToJSON(params.OutParam), err)) }() return } // alterSessionsAction processes the `ExtraParameters` field from the action to construct a request // for the `SessionSv1.AlterSessions` API call. // // The ExtraParameters field format is expected as follows: // - tenant: string // - filters: strings separated by "&". // - limit: integer, specifying the maximum number of sessions to alter. // - APIOpts: set of key-value pairs (separated by "&"). // - Event: set of key-value pairs (separated by "&"). // // Parameters are separated by ";" and must be provided in the specified order. func alterSessionsAction(_ *Account, act *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, ";") if len(params) != 5 { return fmt.Errorf("invalid number of parameters <%d> expected 5", len(params)) } // If conversion fails, limit will default to 0. limit, _ := strconv.Atoi(params[2]) // Prepare request arguments based on provided parameters. attr := utils.SessionFilterWithEvent{ SessionFilter: &utils.SessionFilter{ Limit: &limit, Tenant: params[0], Filters: strings.Split(params[1], "&"), APIOpts: make(map[string]any), }, Event: make(map[string]any), } if err := parseParamStringToMap(params[3], attr.APIOpts); err != nil { return err } if err := parseParamStringToMap(params[4], attr.Event); err != nil { return err } var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.SessionSv1AlterSessions, attr, &reply) } // forceDisconnectSessionsAction processes the `ExtraParameters` field from the action to construct a request // for the `SessionSv1.ForceDisconnect` API call. // // The ExtraParameters field format is expected as follows: // - tenant: string // - filters: strings separated by "&". // - limit: integer, specifying the maximum number of sessions to disconnect. // - APIOpts: set of key-value pairs (separated by "&"). // - Event: set of key-value pairs (separated by "&"). // // Parameters are separated by ";" and must be provided in the specified order. func forceDisconnectSessionsAction(_ *Account, act *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, ";") if len(params) != 5 { return fmt.Errorf("invalid number of parameters <%d> expected 5", len(params)) } // If conversion fails, limit will default to 0. limit, _ := strconv.Atoi(params[2]) // Prepare request arguments based on provided parameters. attr := utils.SessionFilterWithEvent{ SessionFilter: &utils.SessionFilter{ Limit: &limit, Tenant: params[0], Filters: strings.Split(params[1], "&"), APIOpts: make(map[string]any), }, Event: make(map[string]any), } if err := parseParamStringToMap(params[3], attr.APIOpts); err != nil { return err } if err := parseParamStringToMap(params[4], attr.Event); err != nil { return err } var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.SessionSv1ForceDisconnect, attr, &reply) } // parseParamStringToMap parses a string containing key-value pairs separated by "&" and assigns // these pairs to a given map. Each pair is expected to be in the format "key:value". func parseParamStringToMap(paramStr string, targetMap map[string]any) error { for _, tuple := range strings.Split(paramStr, "&") { // Use strings.Cut to split 'tuple' into key-value pairs at the first occurrence of ':'. // This ensures that additional ':' characters within the value do not affect parsing. key, value, found := strings.Cut(tuple, ":") if !found { return fmt.Errorf("invalid key-value pair: %s", tuple) } targetMap[key] = value } return nil } func topupZeroNegativeAction(ub *Account, a *Action, _ Actions, fltrS *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) error { if ub == nil { return errors.New("nil account") } if ub.BalanceMap == nil { ub.BalanceMap = make(map[string]Balances) } return ub.debitBalanceAction(a, false, true, fltrS) } func setExpiryAction(ub *Account, a *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) error { if ub == nil { return errors.New("nil account") } balanceType := a.Balance.GetType() for _, b := range ub.BalanceMap[balanceType] { if b.MatchFilter(a.Balance, "", false, true) { b.ExpirationDate = a.Balance.GetExpirationDate() } } return nil } // publishAccount will publish the account as well as each balance received to ThresholdS func publishAccount(ub *Account, a *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) error { if ub == nil { return errors.New("nil account") } initBal := make(map[string]float64) for _, bals := range ub.BalanceMap { for _, bal := range bals { initBal[bal.Uuid] = bal.Value } } ub.Publish(initBal) return nil } // Actions used to store actions according to weight type Actions []*Action func (a Actions) Len() int { return len(a) } func (a Actions) Swap(i, j int) { a[i], a[j] = a[j], a[i] } // we need higher weights earlyer in the list func (a Actions) Less(j, i int) bool { return a[i].Weight < a[j].Weight } // Sort used to implement sort interface func (a Actions) Sort() { sort.Sort(a) } // Clone returns a clone from object func (a Actions) Clone() Actions { if a == nil { return nil } cln := make(Actions, len(a)) for i, action := range a { cln[i] = action.Clone() } return cln } // CacheClone returns a clone of Actions used by ltcache CacheCloner func (a Actions) CacheClone() any { return a.Clone() } // HasAction checks if the action list contains an action of the given type. func (a Actions) HasAction(typ string) bool { return slices.ContainsFunc(a, func(act *Action) bool { return act.ActionType == typ }) } // newCdrLogProvider constructs a DataProvider func newCdrLogProvider(acnt *Account, action *Action) (dP utils.DataProvider) { dP = &cdrLogProvider{acnt: acnt, action: action, cache: utils.MapStorage{}} return } // cdrLogProvider implements utils.DataProvider so we can pass it to filters type cdrLogProvider struct { acnt *Account action *Action cache utils.MapStorage } // String is part of utils.DataProvider interface // when called, it will display the already parsed values out of cache func (cdrP *cdrLogProvider) String() string { return utils.ToJSON(cdrP) } // FieldAsInterface is part of utils.DataProvider interface func (cdrP *cdrLogProvider) FieldAsInterface(fldPath []string) (data any, err error) { if data, err = cdrP.cache.FieldAsInterface(fldPath); err == nil || err != utils.ErrNotFound { // item found in cache return } err = nil // cancel previous err if len(fldPath) == 2 { switch fldPath[0] { case utils.MetaAcnt: switch fldPath[1] { case utils.AccountID: data = cdrP.acnt.ID case utils.Tenant: tntAcnt := new(utils.TenantAccount) // Init with empty values if cdrP.acnt != nil { if tntAcnt, err = utils.NewTAFromAccountKey(cdrP.acnt.ID); err != nil { return } } data = tntAcnt.Tenant case utils.AccountField: tntAcnt := new(utils.TenantAccount) // Init with empty values if cdrP.acnt != nil { if tntAcnt, err = utils.NewTAFromAccountKey(cdrP.acnt.ID); err != nil { return } } data = tntAcnt.Account case utils.BalanceType: data = cdrP.action.Balance.GetType() case utils.BalanceUUID: data = cdrP.action.Balance.CreateBalance().Uuid case utils.BalanceID: data = cdrP.action.Balance.CreateBalance().ID case utils.BalanceValue: data = strconv.FormatFloat(cdrP.action.balanceValue, 'f', -1, 64) case utils.DestinationIDs: data = cdrP.action.Balance.CreateBalance().DestinationIDs.String() case utils.ExtraParameters: data = cdrP.action.ExtraParameters case utils.RatingSubject: data = cdrP.action.Balance.CreateBalance().RatingSubject case utils.Category: data = cdrP.action.Balance.Categories.String() case utils.SharedGroups: data = cdrP.action.Balance.SharedGroups.String() case utils.Factors: data = cdrP.action.Balance.Factors.String() } case utils.MetaAct: switch fldPath[1] { case utils.ActionID: data = cdrP.action.Id case utils.ActionType: data = cdrP.action.ActionType case utils.ActionValue: data = strconv.FormatFloat(cdrP.action.Balance.CreateBalance().GetValue(), 'f', -1, 64) } } } else { data = fldPath[0] } cdrP.cache.Set(fldPath, data) return } // FieldAsString is part of utils.DataProvider interface func (cdrP *cdrLogProvider) FieldAsString(fldPath []string) (data string, err error) { var valIface any valIface, err = cdrP.FieldAsInterface(fldPath) if err != nil { return } return utils.IfaceAsString(valIface), nil } func removeSessionCosts(_ *Account, action *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) error { // FiltersID;inlineFilter tenant := config.CgrConfig().GeneralCfg().DefaultTenant smcFilter := new(utils.SMCostFilter) for _, fltrID := range strings.Split(action.ExtraParameters, utils.InfieldSep) { if len(fltrID) == 0 { continue } fltr, err := dm.GetFilter(tenant, fltrID, true, true, utils.NonTransactional) if err != nil { utils.Logger.Warning(fmt.Sprintf("<%s> Error: %s for filter: %s in action: <%s>", utils.Actions, err.Error(), fltrID, utils.MetaRemoveSessionCosts)) continue } for _, rule := range fltr.Rules { smcFilter, err = utils.AppendToSMCostFilter(smcFilter, rule.Type, rule.Element, rule.Values, config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { utils.Logger.Warning(fmt.Sprintf("<%s> %s in action: <%s>", utils.Actions, err.Error(), utils.MetaRemoveSessionCosts)) } } } return cdrStorage.RemoveSMCosts(smcFilter) } func removeExpired(acc *Account, action *Action, acts Actions, _ *FilterS, _ any, sd SharedActionsData, _ ActionConnCfg) error { if acc == nil { return fmt.Errorf("nil account for %s action", utils.ToJSON(action)) } found := false for bType, bChain := range acc.BalanceMap { for i := 0; i < len(bChain); i++ { if bChain[i].IsExpiredAt(sd.refTime) && bChain[i].MatchFilter(action.Balance, bType, false, false) { if sd.cdrLog { // If *cdrlog action is present, add the balance summary to remExp in SharedActionsData // for CDR creation. sd.remExp[sd.idx] = append(sd.remExp[sd.idx], bChain[i].AsBalanceSummary(bType)) } // Remove balance without maintaining order. bChain[i] = bChain[len(bChain)-1] bChain = bChain[:len(bChain)-1] i-- found = true } } acc.BalanceMap[bType] = bChain } if !found { return utils.ErrNotFound } return nil } // resetAccountCDR resets the account out of values from CDR func resetAccountCDR(ub *Account, action *Action, _ Actions, fltrS *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) error { if ub == nil { return errors.New("nil account") } if cdrStorage == nil { return fmt.Errorf("nil cdrStorage for %s action", utils.ToJSON(action)) } account := ub.GetID() filter := &utils.CDRsFilter{ Accounts: []string{account}, NotCosts: []float64{-1}, OrderBy: fmt.Sprintf("%s%sdesc", utils.OrderID, utils.InfieldSep), Paginator: utils.Paginator{Limit: utils.IntPointer(1)}, } cdrs, _, err := cdrStorage.GetCDRs(filter, false) if err != nil { return err } cd := cdrs[0].CostDetails if cd == nil { return errors.New("nil CostDetails") } acs := cd.AccountSummary if acs == nil { return errors.New("nil AccountSummary") } for _, bsum := range acs.BalanceSummaries { if bsum == nil { continue } if err := ub.setBalanceAction(&Action{ Balance: &BalanceFilter{ Uuid: &bsum.UUID, ID: &bsum.ID, Type: &bsum.Type, Value: &utils.ValueFormula{Static: bsum.Value}, Weight: &bsum.Weight, Disabled: &bsum.Disabled, Factors: &bsum.Factors, }, }, fltrS); err != nil { utils.Logger.Warning(fmt.Sprintf("<%s> Error %s setting balance %s for account: %s", utils.Actions, err, bsum.UUID, account)) } } return nil } func export(ub *Account, a *Action, _ Actions, _ *FilterS, extraData any, _ SharedActionsData, _ ActionConnCfg) (err error) { var cgrEv *utils.CGREvent switch { case ub != nil: cgrEv = &utils.CGREvent{ Tenant: utils.NewTenantID(ub.ID).Tenant, ID: utils.GenUUID(), Event: map[string]any{ utils.AccountField: ub, utils.EventType: utils.AccountUpdate, utils.EventSource: utils.AccountService, }, APIOpts: map[string]any{ utils.MetaEventType: utils.AccountUpdate, utils.MetaEventTime: time.Now(), }, } case extraData != nil: ev, canCast := extraData.(*utils.CGREvent) if !canCast { return } cgrEv = ev // only export CGREvents default: return // nothing to post } args := &CGREventWithEeIDs{ EeIDs: strings.Split(a.ExtraParameters, utils.InfieldSep), CGREvent: cgrEv, } var rply map[string]map[string]any return connMgr.Call(context.TODO(), config.CgrConfig().ApierCfg().EEsConns, utils.EeSv1ProcessEvent, args, &rply) } func resetThreshold(_ *Account, a *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { args := &utils.TenantIDWithAPIOpts{ TenantID: utils.NewTenantID(a.ExtraParameters), } var rply string return connMgr.Call(context.TODO(), config.CgrConfig().SchedulerCfg().ThreshSConns, utils.ThresholdSv1ResetThreshold, args, &rply) } func resetStatQueue(_ *Account, a *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { args := &utils.TenantIDWithAPIOpts{ TenantID: utils.NewTenantID(a.ExtraParameters), } var rply string return connMgr.Call(context.TODO(), config.CgrConfig().SchedulerCfg().StatSConns, utils.StatSv1ResetStatQueue, args, &rply) } func remoteSetAccount(ub *Account, a *Action, _ Actions, _ *FilterS, _ any, _ SharedActionsData, _ ActionConnCfg) (err error) { client := &http.Client{Transport: httpPstrTransport} var resp *http.Response req := new(bytes.Buffer) if err = json.NewEncoder(req).Encode(ub); err != nil { return } if resp, err = client.Post(a.ExtraParameters, "application/json", req); err != nil { return } acc := new(Account) err = json.NewDecoder(resp.Body).Decode(acc) if err != nil { return } if len(acc.BalanceMap) != 0 { *ub = *acc } return } // dynamicThreshold processes the `ExtraParameters` field from the action to construct a Threshold profile // // The ExtraParameters field format is expected as follows: // // 0 Tenant: string // 1 ID: string // 2 FilterIDs: strings separated by "&". // 3 ActivationInterval: strings separated by "&". // 4 MaxHits: integer // 5 MinHits: integer // 6 MinSleep: duration // 7 Blocker: bool, should always be true // 8 Weight: float, should be higher than the threshold weight that triggers this action // 9 ActionIDs: strings separated by "&". // 10 Async: bool // 11 EeIDs: strings separated by "&". // 12 APIOpts: set of key-value pairs (separated by "&"). // // Parameters are separated by ";" and must be provided in the specified order. func dynamicThreshold(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 13 { return fmt.Errorf("invalid number of parameters <%d> expected 13", len(params)) } // parse dynamic parameters for i := range params { if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } // Prepare request arguments based on provided parameters. thProf := &ThresholdProfileWithAPIOpts{ ThresholdProfile: &ThresholdProfile{ Tenant: params[0], ID: params[1], ActivationInterval: &utils.ActivationInterval{}, // avoid reaching inside a nil pointer }, APIOpts: make(map[string]any), } // populate Threshold's FilterIDs if params[2] != utils.EmptyString { thProf.FilterIDs = strings.Split(params[2], utils.ANDSep) } // populate Threshold's ActivationInterval aISplit := strings.Split(params[3], utils.ANDSep) if len(aISplit) > 2 { return utils.ErrUnsupportedFormat } if len(aISplit) > 0 && aISplit[0] != utils.EmptyString { for i := range aISplit { if timeParsed, err := utils.ParseTimeDetectLayout(aISplit[i], config.CgrConfig().GeneralCfg().DefaultTimezone); err != nil { return err } else if i == 0 { thProf.ActivationInterval.ActivationTime = timeParsed } else if i == 1 { thProf.ActivationInterval.ExpiryTime = timeParsed } } } // populate Threshold's MaxHits if params[4] != utils.EmptyString { thProf.MaxHits, err = strconv.Atoi(params[4]) if err != nil { return err } } // populate Threshold's MinHits if params[5] != utils.EmptyString { thProf.MinHits, err = strconv.Atoi(params[5]) if err != nil { return err } } // populate Threshold's MinSleep if params[6] != utils.EmptyString { thProf.MinSleep, err = utils.ParseDurationWithNanosecs(params[6]) if err != nil { return err } } // populate Threshold's Blocker if params[7] != utils.EmptyString { thProf.Blocker, err = strconv.ParseBool(params[7]) if err != nil { return err } } // populate Threshold's Weight if params[8] != utils.EmptyString { thProf.Weight, err = strconv.ParseFloat(params[8], 64) if err != nil { return err } } // populate Threshold's ActionIDs if params[9] != utils.EmptyString { thProf.ActionIDs = strings.Split(params[9], utils.ANDSep) } // populate Threshold's Async bool if params[10] != utils.EmptyString { thProf.Async, err = strconv.ParseBool(params[10]) if err != nil { return err } } // populate Threshold's EeIDs if params[11] != utils.EmptyString { thProf.EeIDs = strings.Split(params[11], utils.ANDSep) } // populate Threshold's APIOpts if params[12] != utils.EmptyString { if err := parseParamStringToMap(params[12], thProf.APIOpts); err != nil { return err } } // create the ThresholdProfile based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetThresholdProfile, thProf, &reply) } // dynamicStats processes the `ExtraParameters` field from the action to construct a StatQueueProfile // // The ExtraParameters field format is expected as follows: // // 0 Tenant: string // 1 ID: string // 2 FilterIDs: strings separated by "&". // 3 ActivationInterval: strings separated by "&". // 4 QueueLength: integer // 5 TTL: duration // 6 MinItems: integer // 7 Metrics: strings separated by "&". // 8 MetricFilterIDs: strings separated by "&". // 9 Stored: bool // 10 Blocker: bool // 11 Weight: float // 12 ThresholdIDs: strings separated by "&". // 13 APIOpts: set of key-value pairs (separated by "&"). // // Parameters are separated by ";" and must be provided in the specified order. func dynamicStats(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 14 { return fmt.Errorf("invalid number of parameters <%d> expected 14", len(params)) } // parse dynamic parameters for i := range params { if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } // Prepare request arguments based on provided parameters. stQProf := &StatQueueProfileWithAPIOpts{ StatQueueProfile: &StatQueueProfile{ Tenant: params[0], ID: params[1], ActivationInterval: &utils.ActivationInterval{}, // avoid reaching inside a nil pointer }, APIOpts: make(map[string]any), } // populate Stat's FilterIDs if params[2] != utils.EmptyString { stQProf.FilterIDs = strings.Split(params[2], utils.ANDSep) } // populate Stat's ActivationInterval aISplit := strings.Split(params[3], utils.ANDSep) if len(aISplit) > 2 { return utils.ErrUnsupportedFormat } if len(aISplit) > 0 && aISplit[0] != utils.EmptyString { for i := range aISplit { if timeParsed, err := utils.ParseTimeDetectLayout(aISplit[i], config.CgrConfig().GeneralCfg().DefaultTimezone); err != nil { return err } else if i == 0 { stQProf.ActivationInterval.ActivationTime = timeParsed } else if i == 1 { stQProf.ActivationInterval.ExpiryTime = timeParsed } } } // populate Stat's QueueLengh if params[4] != utils.EmptyString { stQProf.QueueLength, err = strconv.Atoi(params[4]) if err != nil { return err } } // populate Stat's TTL if params[5] != utils.EmptyString { stQProf.TTL, err = utils.ParseDurationWithNanosecs(params[5]) if err != nil { return err } } // populate Stat's MinItems if params[6] != utils.EmptyString { stQProf.MinItems, err = strconv.Atoi(params[6]) if err != nil { return err } } // populate Stat's MetricID if params[7] != utils.EmptyString { metrics := strings.Split(params[7], utils.ANDSep) stQProf.Metrics = make([]*MetricWithFilters, len(metrics)) for i, strM := range metrics { stQProf.Metrics[i] = &MetricWithFilters{MetricID: strM} } } // populate Stat's metricFliters if params[8] != utils.EmptyString { metricFliters := strings.Split(params[8], utils.ANDSep) for i := range stQProf.Metrics { stQProf.Metrics[i].FilterIDs = metricFliters } } // populate Stat's Stored bool if params[9] != utils.EmptyString { stQProf.Stored, err = strconv.ParseBool(params[9]) if err != nil { return err } } // populate Stat's Blocker if params[10] != utils.EmptyString { stQProf.Blocker, err = strconv.ParseBool(params[10]) if err != nil { return err } } // populate Stat's Weight if params[11] != utils.EmptyString { stQProf.Weight, err = strconv.ParseFloat(params[11], 64) if err != nil { return err } } // populate Stat's ThresholdIDs if params[12] != utils.EmptyString { stQProf.ThresholdIDs = strings.Split(params[12], utils.ANDSep) } // populate Stat's APIOpts if params[13] != utils.EmptyString { if err := parseParamStringToMap(params[13], stQProf.APIOpts); err != nil { return err } } // create the StatQueueProfile based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetStatQueueProfile, stQProf, &reply) } // dynamicAttribute processes the `ExtraParameters` field from the action to construct a AttributeProfile // // The ExtraParameters field format is expected as follows: // // 0 Tenant: string // 1 ID: string // 2 Context: strings separated by "&". // 3 FilterIDs: strings separated by "&". // 4 ActivationInterval: strings separated by "&". // 5 AttributeFilterIDs: strings separated by "&". // 6 Path: string // 7 Type: string // 8 Value: strings separated by "&". // 9 Blocker: bool // 10 Weight: float // 11 APIOpts: set of key-value pairs (separated by "&"). // // Parameters are separated by ";" and must be provided in the specified order. func dynamicAttribute(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 12 { return fmt.Errorf("invalid number of parameters <%d> expected 12", len(params)) } // parse dynamic parameters for i := range params { if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } // Prepare request arguments based on provided parameters. attrP := &AttributeProfileWithAPIOpts{ AttributeProfile: &AttributeProfile{ Tenant: params[0], ID: params[1], ActivationInterval: &utils.ActivationInterval{}, // avoid reaching inside a nil pointer }, APIOpts: make(map[string]any), } // populate Attribute's Context if params[2] != utils.EmptyString { attrP.Contexts = strings.Split(params[2], utils.ANDSep) } // populate Attribute's FilterIDs if params[3] != utils.EmptyString { attrP.FilterIDs = strings.Split(params[3], utils.ANDSep) } // populate Attribute's ActivationInterval aISplit := strings.Split(params[4], utils.ANDSep) if len(aISplit) > 2 { return utils.ErrUnsupportedFormat } if len(aISplit) > 0 && aISplit[0] != utils.EmptyString { for i := range aISplit { if timeParsed, err := utils.ParseTimeDetectLayout(aISplit[i], config.CgrConfig().GeneralCfg().DefaultTimezone); err != nil { return err } else if i == 0 { attrP.ActivationInterval.ActivationTime = timeParsed } else if i == 1 { attrP.ActivationInterval.ExpiryTime = timeParsed } } } // populate Attribute's Attributes if params[6] != utils.EmptyString { value, err := config.NewRSRParsers(params[8], "&") if err != nil { return err } var attrFltrIDs []string if params[5] != utils.EmptyString { attrFltrIDs = strings.Split(params[5], utils.ANDSep) } attrP.Attributes = append(attrP.Attributes, &Attribute{ FilterIDs: attrFltrIDs, Path: params[6], Type: params[7], Value: value, }) } // populate Attribute's Blocker if params[9] != utils.EmptyString { attrP.Blocker, err = strconv.ParseBool(params[9]) if err != nil { return err } } // populate Attribute's Weight if params[10] != utils.EmptyString { attrP.Weight, err = strconv.ParseFloat(params[10], 64) if err != nil { return err } } // populate Attribute's APIOpts if params[11] != utils.EmptyString { if err := parseParamStringToMap(params[11], attrP.APIOpts); err != nil { return err } } // create the AttributeProfile based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetAttributeProfile, attrP, &reply) } // dynamicActionPlan processes the `ExtraParameters` field from the action to construct an ActionPlan // // The ExtraParameters field format is expected as follows: // // 0 Id: string // 1 ActionsId: string // 2 TimingId: string // 3 Weight: float // 4 Overwrite: bool // // Parameters are separated by ";" and must be provided in the specified order. func dynamicActionPlan(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 5 { return fmt.Errorf("invalid number of parameters <%d> expected 5", len(params)) } // parse dynamic parameters for i := range params { if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } // Prepare request arguments based on provided parameters. ap := &AttrSetActionPlan{ Id: params[0], ReloadScheduler: true, } // populate ActionPlan's ActionsId if params[1] == utils.EmptyString { return fmt.Errorf("empty ActionsId for <%s> dynamic_action_plan", params[0]) } // Make sure ActionsId exists in DataDB var actsRply []*utils.TPAction if err := connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1GetActions, utils.StringPointer(params[1]), &actsRply); err != nil { return err } ap.ActionPlan = append(ap.ActionPlan, &AttrActionPlan{}) ap.ActionPlan[0].ActionsId = params[1] if params[2] != utils.EmptyString { // Make sure TimingID exists in DataDB and use it for the action plan var tpTiming utils.TPTiming if err := connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1GetTiming, &utils.ArgsGetTimingID{ID: params[2]}, &tpTiming); err != nil { return err } ap.ActionPlan[0].TimingID = tpTiming.ID ap.ActionPlan[0].Years = tpTiming.Years.Serialize(";") ap.ActionPlan[0].Months = tpTiming.Months.Serialize(";") ap.ActionPlan[0].MonthDays = tpTiming.MonthDays.Serialize(";") ap.ActionPlan[0].WeekDays = tpTiming.WeekDays.Serialize(";") if tpTiming.EndTime != utils.EmptyString { ap.ActionPlan[0].Time = utils.InfieldJoin(tpTiming.StartTime, tpTiming.EndTime) } else { ap.ActionPlan[0].Time = tpTiming.StartTime } } // populate ActionPlan's Weight if params[3] != utils.EmptyString { ap.ActionPlan[0].Weight, err = strconv.ParseFloat(params[3], 64) if err != nil { return err } } // populate ActionPlan's Overwrite if params[4] != utils.EmptyString { ap.Overwrite, err = strconv.ParseBool(params[4]) if err != nil { return err } } // create the ActionPlan based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetActionPlan, ap, &reply) } // dynamicActionPlanAccount processes the `ExtraParameters` field from the action to construct an ActionPlan with account ids // // The ExtraParameters field format is expected as follows: // // 0 Id: string // 1 ActionsId: string // 2 TimingId: string // 3 Weight: float // 4 Overwrite: bool // 5 Tenant:AccountIDs: strings separated by "&". // // Parameters are separated by ";" and must be provided in the specified order. func dynamicActionPlanAccount(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 6 { return fmt.Errorf("invalid number of parameters <%d> expected 6", len(params)) } // parse dynamic parameters for i := range params { if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } // Prepare request arguments based on provided parameters. ap := &AttrSetActionPlanAccounts{ Id: params[0], ReloadScheduler: true, } // populate ActionPlan's ActionsId if params[1] == utils.EmptyString { return fmt.Errorf("empty ActionsId for <%s> dynamic_action_plan", params[0]) } // Make sure ActionsId exists in DataDB var actsRply []*utils.TPAction if err := connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1GetActions, utils.StringPointer(params[1]), &actsRply); err != nil { return err } ap.ActionPlan = append(ap.ActionPlan, &AttrActionPlan{}) ap.ActionPlan[0].ActionsId = params[1] if params[2] != utils.EmptyString { // Make sure TimingID exists in DataDB and use it for the action plan var tpTiming utils.TPTiming if err := connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1GetTiming, &utils.ArgsGetTimingID{ID: params[2]}, &tpTiming); err != nil { return err } ap.ActionPlan[0].TimingID = tpTiming.ID ap.ActionPlan[0].Years = tpTiming.Years.Serialize(";") ap.ActionPlan[0].Months = tpTiming.Months.Serialize(";") ap.ActionPlan[0].MonthDays = tpTiming.MonthDays.Serialize(";") ap.ActionPlan[0].WeekDays = tpTiming.WeekDays.Serialize(";") if tpTiming.EndTime != utils.EmptyString { ap.ActionPlan[0].Time = utils.InfieldJoin(tpTiming.StartTime, tpTiming.EndTime) } else { ap.ActionPlan[0].Time = tpTiming.StartTime } } // populate ActionPlan's Weight if params[3] != utils.EmptyString { ap.ActionPlan[0].Weight, err = strconv.ParseFloat(params[3], 64) if err != nil { return err } } // populate ActionPlan's Overwrite if params[4] != utils.EmptyString { ap.Overwrite, err = strconv.ParseBool(params[4]) if err != nil { return err } } // populate ActionPlan's AccountIDs if params[5] != utils.EmptyString { ap.AccountIDs = strings.Split(params[5], utils.ANDSep) } // create the ActionPlan based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetActionPlanAccounts, ap, &reply) } // dynamicAction processes the `ExtraParameters` field from the action to construct a new Action // // The ExtraParameters field format is expected as follows: // // 0 ActionsId: string // 1 Action: string // 2 ExtraParameters: string encapsulated by \f // 3 Filters: strings separated by "&". // 4 BalanceId: string // 5 BalanceType: string // 6 Categories: strings separated by "&". // 7 DestinationIds: strings separated by "&". // 8 RatingSubject: string // 9 SharedGroups: strings separated by "&". // 10 ExpiryTime: string // 11 TimingIds: strings separated by "&". // 12 Units: string // 13 BalanceWeight: string // 14 BalanceBlocker: string // 15 BalanceDisabled: string // 16 Weight: float // 17 Overwrite: bool // // Parameters are separated by ";" and must be provided in the specified order. func dynamicAction(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } var params []string // parameters split by ; var bildr strings.Builder // used to build the params strings by looking at each character of act.ExtraParameters inEncapsulation := false for i := range len(act.ExtraParameters) { // Check for \f (form feed character) if act.ExtraParameters[i] == '\f' { inEncapsulation = !inEncapsulation // Don't add \f to the current string - just skip it } else if act.ExtraParameters[i] == ';' && !inEncapsulation { // Found separator ";" outside encapsulation params = append(params, bildr.String()) bildr.Reset() } else { // Regular character or semicolon inside encapsulation bildr.WriteByte(act.ExtraParameters[i]) } } params = append(params, bildr.String()) // append last param left even if empty // Parse action parameters based on the predefined format. if len(params) != 18 { return fmt.Errorf("invalid number of parameters <%d> expected 18", len(params)) } // replace '&' with ';' before parsing to comply with TPAction fields that need ";" seperators params[3] = strings.ReplaceAll(params[3], utils.ANDSep, utils.InfieldSep) params[6] = strings.ReplaceAll(params[6], utils.ANDSep, utils.InfieldSep) params[7] = strings.ReplaceAll(params[7], utils.ANDSep, utils.InfieldSep) params[9] = strings.ReplaceAll(params[9], utils.ANDSep, utils.InfieldSep) params[11] = strings.ReplaceAll(params[11], utils.ANDSep, utils.InfieldSep) // parse dynamic parameters for i := range params { if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } if params[0] == utils.EmptyString { return fmt.Errorf("empty ActionsId for dynamic_action") } if params[1] == utils.EmptyString { return fmt.Errorf("empty Action for <%s> dynamic_action", params[0]) } var weight float64 // populate Action's Weight if params[16] != utils.EmptyString { weight, err = strconv.ParseFloat(params[16], 64) if err != nil { return err } } var overwrite bool // populate Action's Overwrite if params[17] != utils.EmptyString { overwrite, err = strconv.ParseBool(params[17]) if err != nil { return err } } // populate action parameters ap := &utils.AttrSetActions{ ActionsId: params[0], Actions: []*utils.TPAction{ { Identifier: params[1], ExtraParameters: params[2], Filters: params[3], BalanceId: params[4], BalanceType: params[5], Categories: params[6], DestinationIds: params[7], RatingSubject: params[8], SharedGroups: params[9], ExpiryTime: params[10], TimingTags: params[11], Units: params[12], BalanceWeight: params[13], BalanceBlocker: params[14], BalanceDisabled: params[15], Weight: weight, }, }, Overwrite: overwrite, } // create the Action based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv2SetActions, ap, &reply) } // dynamicDestination processes the `ExtraParameters` field from the action to construct a new Destination // // The ExtraParameters field format is expected as follows: // // 0 Id: string // 1 Prefix: strings separated by "&". // // Parameters are separated by ";" and must be provided in the specified order. func dynamicDestination(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaOpts: cgrEv.APIOpts, } // Parse Destination parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 2 { return fmt.Errorf("invalid number of parameters <%d> expected 2", len(params)) } // parse dynamic parameters for i := range params { if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } // populate Destination's parameters dest := &utils.AttrSetDestination{ Id: params[0], } // populate Destination's Prefixes if params[1] != utils.EmptyString { dest.Prefixes = strings.Split(params[1], utils.ANDSep) } // create the Destination based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetDestination, dest, &reply) } // dynamicFilter processes the `ExtraParameters` field from the action to // construct a Filter // // The ExtraParameters field format is expected as follows: // // 0 Tenant: string // 1 ID: string // 2 Type: string // 3 Path: string // 4 Values: strings separated by "&". // 5 ActivationInterval: strings separated by "&". // 6 APIOpts: set of key-value pairs (separated by "&"). // // Parameters are separated by ";" and must be provided in the specified order. func dynamicFilter(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 7 { return fmt.Errorf("invalid number of parameters <%d> expected 7", len(params)) } // parse dynamic parameters for i := range params { var onlyEncapsulatead bool if i == 3 { // dont parse un-encapsulated "< >" string from Path onlyEncapsulatead = true } if params[i], err = utils.ParseParamForDataProvider(params[i], dP, onlyEncapsulatead); err != nil { return err } } // Prepare request arguments based on provided parameters. fltr := &FilterWithAPIOpts{ Filter: &Filter{ Tenant: params[0], ID: params[1], Rules: []*FilterRule{{ Type: params[2], Element: params[3], }}, ActivationInterval: &utils.ActivationInterval{}, // avoid reaching inside a nil pointer }, APIOpts: make(map[string]any), } // populate Filter's Values if params[4] != utils.EmptyString { fltr.Filter.Rules[0].Values = strings.Split(params[4], utils.ANDSep) } // populate Filter's ActivationInterval aISplit := strings.Split(params[5], utils.ANDSep) if len(aISplit) > 2 { return utils.ErrUnsupportedFormat } if len(aISplit) > 0 && aISplit[0] != utils.EmptyString { for i := range aISplit { if timeParsed, err := utils.ParseTimeDetectLayout(aISplit[i], config.CgrConfig().GeneralCfg().DefaultTimezone); err != nil { return err } else if i == 0 { fltr.ActivationInterval.ActivationTime = timeParsed } else if i == 1 { fltr.ActivationInterval.ExpiryTime = timeParsed } } } // populate Filter's APIOpts if params[6] != utils.EmptyString { if err := parseParamStringToMap(params[6], fltr.APIOpts); err != nil { return err } } // create the Filter based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetFilter, fltr, &reply) } // dynamicRoute processes the `ExtraParameters` field from the action to // construct a RouteProfile // // The ExtraParameters field format is expected as follows: // // 0 Tenant: string // 1 ID: string // 2 FilterIDs: strings separated by "&". // 3 ActivationInterval: strings separated by "&". // 4 Sorting: string // 5 SortingParameters: strings separated by "&". // 6 RouteID: string // 7 RouteFilterIDs: strings separated by "&". // 8 RouteAccountIDs: strings separated by "&". // 9 RouteRatingPlanIDs: strings separated by "&". // 10 RouteResourceIDs: strings separated by "&". // 11 RouteStatIDs: strings separated by "&". // 12 RouteWeight: float // 13 RouteBlocker: bool // 14 RouteParameters: string // 15 Weight: float // 16 APIOpts: set of key-value pairs (separated by "&"). // // Parameters are separated by ";" and must be provided in the specified order. func dynamicRoute(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 17 { return fmt.Errorf("invalid number of parameters <%d> expected 17", len(params)) } // parse dynamic parameters for i := range params { if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } // Take only the string after @, for cases when the RouteProfileID is gotten from a switch agents event routeFieldParts := strings.Split(params[1], utils.AtChar) routeProfileFound := new(RouteProfile) if len(routeFieldParts) > 2 { return fmt.Errorf("more than 1 \"@\" character for RouteProfileID: <%s>", params[1]) } else if len(routeFieldParts) > 1 { params[1] = routeFieldParts[1] if err := connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1GetRouteProfile, &utils.TenantID{Tenant: utils.FirstNonEmpty(cgrEv.Tenant, config.CgrConfig().GeneralCfg().DefaultTenant), ID: params[1]}, &routeProfileFound); err != nil && err.Error() != utils.ErrNotFound.Error() { return err } } // Prepare request arguments based on provided parameters. If any param is left empty, // the field corresponding to it will be populated by the routeProfileFound field if it exists route := &RouteWithAPIOpts{ RouteProfile: &RouteProfile{ Tenant: utils.FirstNonEmpty(params[0], routeProfileFound.Tenant), ID: params[1], ActivationInterval: &utils.ActivationInterval{}, // avoid reaching inside a nil pointer Sorting: utils.FirstNonEmpty(params[4], routeProfileFound.Sorting), }, APIOpts: make(map[string]any), } // populate RouteProfile's FilterIDs if params[2] != utils.EmptyString { route.FilterIDs = strings.Split(params[2], utils.ANDSep) } else { route.FilterIDs = routeProfileFound.FilterIDs } // populate RouteProfile's ActivationInterval aISplit := strings.Split(params[3], utils.ANDSep) if len(aISplit) > 2 { return utils.ErrUnsupportedFormat } if len(aISplit) > 0 && aISplit[0] != utils.EmptyString { for i := range aISplit { if timeParsed, err := utils.ParseTimeDetectLayout(aISplit[i], config.CgrConfig().GeneralCfg().DefaultTimezone); err != nil { return err } else if i == 0 { route.ActivationInterval.ActivationTime = timeParsed } else if i == 1 { route.ActivationInterval.ExpiryTime = timeParsed } } } else { route.ActivationInterval = routeProfileFound.ActivationInterval } // populate RouteProfile's SortingParameters if params[5] != utils.EmptyString { route.SortingParameters = strings.Split(params[5], utils.ANDSep) } else { route.SortingParameters = routeProfileFound.SortingParameters } // populate RouteProfile's Routes if params[6] != utils.EmptyString { // keep the existing routes if routeProfile already existed, and modify the specified Routes by ID var routeModified bool // if route doesnt exist in the found route Profile for _, existingRoute := range routeProfileFound.Routes { if existingRoute.ID == params[6] { // modify routes with ID // populate RouteProfile's RouteFilterIDs if params[7] != utils.EmptyString { existingRoute.FilterIDs = strings.Split(params[7], utils.ANDSep) } // populate RouteProfile's RouteAccountIDs if params[8] != utils.EmptyString { existingRoute.AccountIDs = strings.Split(params[8], utils.ANDSep) } // populate RouteProfile's RouteRatingPlanIDs if params[9] != utils.EmptyString { existingRoute.RatingPlanIDs = strings.Split(params[9], utils.ANDSep) } // populate RouteProfile's RouteResourceIDs if params[10] != utils.EmptyString { existingRoute.ResourceIDs = strings.Split(params[10], utils.ANDSep) } // populate RouteProfile's RouteStatIDs if params[11] != utils.EmptyString { existingRoute.StatIDs = strings.Split(params[11], utils.ANDSep) } // populate RouteProfile's RouteWeight if params[12] != utils.EmptyString { existingRoute.Weight, err = strconv.ParseFloat(params[12], 64) if err != nil { return err } } // populate RouteProfile's RouteBlocker if params[13] != utils.EmptyString { existingRoute.Blocker, err = strconv.ParseBool(params[13]) if err != nil { return err } } if params[14] != utils.EmptyString { existingRoute.RouteParameters = params[14] } routeModified = true } route.Routes = append(route.Routes, existingRoute) } if !routeModified { // if no existing routes were modified, append a new route appendRoute := new(Route) // new route to be appended // populate RouteProfile's RouteID appendRoute.ID = params[6] // populate RouteProfile's RouteFilterIDs if params[7] != utils.EmptyString { appendRoute.FilterIDs = strings.Split(params[7], utils.ANDSep) } // populate RouteProfile's RouteAccountIDs if params[8] != utils.EmptyString { appendRoute.AccountIDs = strings.Split(params[8], utils.ANDSep) } // populate RouteProfile's RouteRatingPlanIDs if params[9] != utils.EmptyString { appendRoute.RatingPlanIDs = strings.Split(params[9], utils.ANDSep) } // populate RouteProfile's RouteResourceIDs if params[10] != utils.EmptyString { appendRoute.ResourceIDs = strings.Split(params[10], utils.ANDSep) } // populate RouteProfile's RouteStatIDs if params[11] != utils.EmptyString { appendRoute.StatIDs = strings.Split(params[11], utils.ANDSep) } // populate RouteProfile's RouteWeight if params[12] != utils.EmptyString { appendRoute.Weight, err = strconv.ParseFloat(params[12], 64) if err != nil { return err } } // populate RouteProfile's RouteBlocker if params[13] != utils.EmptyString { appendRoute.Blocker, err = strconv.ParseBool(params[13]) if err != nil { return err } } // populate RouteProfile's RouteParameters appendRoute.RouteParameters = params[14] route.Routes = append(route.Routes, appendRoute) } } else { route.Routes = routeProfileFound.Routes } // populate RouteProfile's Weight if params[15] != utils.EmptyString { route.Weight, err = strconv.ParseFloat(params[15], 64) if err != nil { return err } } else { route.Weight = routeProfileFound.Weight } // populate RouteProfile's APIOpts if params[16] != utils.EmptyString { if err := parseParamStringToMap(params[16], route.APIOpts); err != nil { return err } } // create the RouteProfile based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetRouteProfile, route, &reply) } // dynamicRanking processes the `ExtraParameters` field from the action to // construct a RankingProfile // // The ExtraParameters field format is expected as follows: // // 0 Tenant: string // 1 ID: string // 2 Schedule: string // 3 StatIDs: strings separated by "&". // 4 MetricIDs: strings separated by "&". // 5 Sorting: string // 6 SortingParameters: strings separated by "&". // 7 Stored: bool // 8 ThresholdIDs: strings separated by "&". // 9 APIOpts: set of key-value pairs (separated by "&"). // // Parameters are separated by ";" and must be provided in the specified order. func dynamicRanking(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 10 { return fmt.Errorf("invalid number of parameters <%d> expected 10", len(params)) } // parse dynamic parameters for i := range params { if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } // Prepare request arguments based on provided parameters. ranking := &RankingProfileWithAPIOpts{ RankingProfile: &RankingProfile{ Tenant: params[0], ID: params[1], Schedule: params[2], Sorting: params[5], }, APIOpts: make(map[string]any), } // populate Ranking's StatIDs if params[3] != utils.EmptyString { ranking.StatIDs = strings.Split(params[3], utils.ANDSep) } // populate Ranking's MetricIDs if params[4] != utils.EmptyString { ranking.MetricIDs = strings.Split(params[4], utils.ANDSep) } // populate Ranking's SortingParameters if params[6] != utils.EmptyString { ranking.SortingParameters = strings.Split(params[6], utils.ANDSep) } // populate Ranking's Stored if params[7] != utils.EmptyString { ranking.Stored, err = strconv.ParseBool(params[7]) if err != nil { return err } } // populate Ranking's ThresholdIDs if params[8] != utils.EmptyString { ranking.ThresholdIDs = strings.Split(params[8], utils.ANDSep) } // populate Ranking's APIOpts if params[9] != utils.EmptyString { if err := parseParamStringToMap(params[9], ranking.APIOpts); err != nil { return err } } // create the RankingProfile based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetRankingProfile, ranking, &reply) } // dynamicRatingProfile processes the `ExtraParameters` field from the action to // construct a RatingProfile // // The ExtraParameters field format is expected as follows: // // 0 Tenant: string // 1 Category: string // 2 Subject: string // 3 ActivationTime: string // 4 RatingPlanId: string // 5 RatesFallbackSubject: string // 6 APIOpts: set of key-value pairs (separated by "&"). // // Parameters are separated by ";" and must be provided in the specified order. func dynamicRatingProfile(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 7 { return fmt.Errorf("invalid number of parameters <%d> expected 7", len(params)) } // parse dynamic parameters for i := range params { var onlyEncapsulatead bool if i == 3 { // dont parse "*now" string for ActivationTime onlyEncapsulatead = true } if params[i], err = utils.ParseParamForDataProvider(params[i], dP, onlyEncapsulatead); err != nil { return err } } // Prepare request arguments based on provided parameters. ratingProf := &utils.AttrSetRatingProfile{ Tenant: params[0], Category: params[1], Subject: params[2], RatingPlanActivations: []*utils.TPRatingActivation{ { ActivationTime: params[3], RatingPlanId: params[4], FallbackSubjects: params[5], }, }, APIOpts: make(map[string]any), } // populate RatingProfiles's APIOpts if params[6] != utils.EmptyString { if err := parseParamStringToMap(params[6], ratingProf.APIOpts); err != nil { return err } } // create the RatingProfile based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetRatingProfile, ratingProf, &reply) } // dynamicTrend processes the `ExtraParameters` field from the action to // construct a TrendProfile // // The ExtraParameters field format is expected as follows: // // 0 Tenant: string // 1 ID: string // 2 Schedule: string // 3 StatID: string // 4 Metrics: strings separated by "&". // 5 TTL: duration // 6 QueueLength: integer // 7 MinItems: integer // 8 CorrelationType: string // 9 Tolerance: float // 10 Stored: bool // 11 ThresholdIDs: strings separated by "&". // 12 APIOpts: set of key-value pairs (separated by "&"). // // Parameters are separated by ";" and must be provided in the specified order. func dynamicTrend(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 13 { return fmt.Errorf("invalid number of parameters <%d> expected 13", len(params)) } // parse dynamic parameters for i := range params { if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } // Prepare request arguments based on provided parameters. trend := &TrendProfileWithAPIOpts{ TrendProfile: &TrendProfile{ Tenant: params[0], ID: params[1], Schedule: params[2], StatID: params[3], CorrelationType: params[8], }, APIOpts: make(map[string]any), } // populate Trend's Metrics if params[4] != utils.EmptyString { trend.Metrics = strings.Split(params[4], utils.ANDSep) } // populate Trend's TTL if params[5] != utils.EmptyString { trend.TTL, err = utils.ParseDurationWithNanosecs(params[5]) if err != nil { return err } } // populate Trend's QueueLength if params[6] != utils.EmptyString { trend.QueueLength, err = strconv.Atoi(params[6]) if err != nil { return err } } // populate Trend's MinItems if params[7] != utils.EmptyString { trend.MinItems, err = strconv.Atoi(params[7]) if err != nil { return err } } // populate Trend's Tolerance if params[9] != utils.EmptyString { trend.Tolerance, err = strconv.ParseFloat(params[9], 64) if err != nil { return err } } // populate Trend's Stored if params[10] != utils.EmptyString { trend.Stored, err = strconv.ParseBool(params[10]) if err != nil { return err } } // populate Trend's ThresholdIDs if params[11] != utils.EmptyString { trend.ThresholdIDs = strings.Split(params[11], utils.ANDSep) } // populate Trend's APIOpts if params[12] != utils.EmptyString { if err := parseParamStringToMap(params[12], trend.APIOpts); err != nil { return err } } // create the TrendProfile based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetTrendProfile, trend, &reply) } // dynamicResource processes the `ExtraParameters` field from the action to // construct a ResourceProfile // // The ExtraParameters field format is expected as follows: // // 0 Tenant: string // 1 Id: string // 2 FilterIDs: strings separated by "&". // 3 ActivationInterval: strings separated by "&". // 4 TTL: duration // 5 Limit: float // 6 AllocationMessage: string // 7 Blocker: bool // 8 Stored: bool // 9 Weight: float // 10 ThresholdIDs: strings separated by "&". // 11 APIOpts: set of key-value pairs (separated by "&"). // // Parameters are separated by ";" and must be provided in the specified order. func dynamicResource(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 12 { return fmt.Errorf("invalid number of parameters <%d> expected 12", len(params)) } // parse dynamic parameters for i := range params { if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } // Prepare request arguments based on provided parameters. rsc := &ResourceProfileWithAPIOpts{ ResourceProfile: &ResourceProfile{ Tenant: params[0], ID: params[1], ActivationInterval: &utils.ActivationInterval{}, // avoid reaching inside a nil pointer AllocationMessage: params[6], }, APIOpts: make(map[string]any), } // populate Resource's FilterIDs if params[2] != utils.EmptyString { rsc.FilterIDs = strings.Split(params[2], utils.ANDSep) } // populate Resource's ActivationInterval aISplit := strings.Split(params[3], utils.ANDSep) if len(aISplit) > 2 { return utils.ErrUnsupportedFormat } if len(aISplit) > 0 && aISplit[0] != utils.EmptyString { for i := range aISplit { if timeParsed, err := utils.ParseTimeDetectLayout(aISplit[i], config.CgrConfig().GeneralCfg().DefaultTimezone); err != nil { return err } else if i == 0 { rsc.ActivationInterval.ActivationTime = timeParsed } else if i == 1 { rsc.ActivationInterval.ExpiryTime = timeParsed } } } // populate Resource's UsageTTL if params[4] != utils.EmptyString { rsc.UsageTTL, err = utils.ParseDurationWithNanosecs(params[4]) if err != nil { return err } } // populate Resource's Limit if params[5] != utils.EmptyString { rsc.Limit, err = strconv.ParseFloat(params[5], 64) if err != nil { return err } } // populate Resource's Blocker if params[7] != utils.EmptyString { rsc.Blocker, err = strconv.ParseBool(params[7]) if err != nil { return err } } // populate Resource's Stored if params[8] != utils.EmptyString { rsc.Stored, err = strconv.ParseBool(params[8]) if err != nil { return err } } // populate Resource's Weight if params[9] != utils.EmptyString { rsc.Weight, err = strconv.ParseFloat(params[9], 64) if err != nil { return err } } // populate Resource's ThresholdIDs if params[10] != utils.EmptyString { rsc.ThresholdIDs = strings.Split(params[10], utils.ANDSep) } // populate Resource's APIOpts if params[11] != utils.EmptyString { if err := parseParamStringToMap(params[11], rsc.APIOpts); err != nil { return err } } // create the ResourceProfile based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetResourceProfile, rsc, &reply) } // dynamicActionTrigger processes the `ExtraParameters` field from the action to // construct a ActionTrigger // // The ExtraParameters field format is expected as follows: // // 0 Tag: string // 1 UniqueId: string // 2 ThresholdType: string // 3 ThresholdValue: float // 4 Recurrent: bool // 5 MinSleep: duration // 6 ExpiryTime: time // 7 ActivationTime: time // 8 BalanceTag: string // 9 BalanceType: string // 10 BalanceCategories: strings separated by "&". // 11 BalanceDestinationIds: strings separated by "&". // 12 BalanceRatingSubject: string // 13 BalanceSharedGroup: strings separated by "&". // 14 BalanceExpiryTime: time // 15 BalanceTimingIds: strings separated by "&". // 16 BalanceWeight: float // 17 BalanceBlocker: bool // 18 BalanceDisabled: bool // 19 ActionsId: string // 20 Weight: float // // Parameters are separated by ";" and must be provided in the specified order. func dynamicActionTrigger(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { return errors.New("Couldn't cast event to CGREvent") } dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 21 { return fmt.Errorf("invalid number of parameters <%d> expected 21", len(params)) } // parse dynamic parameters for i := range params { if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } // Prepare request arguments based on provided parameters. at := &AttrSetActionTrigger{ GroupID: params[0], UniqueID: utils.FirstNonEmpty(params[1], utils.GenUUID()), ActionTrigger: make(map[string]any), } // populate ActionTrigger's ThresholdType if params[2] != utils.EmptyString { at.ActionTrigger[utils.ThresholdType] = params[2] } // populate ActionTrigger's ThresholdValue if params[3] != utils.EmptyString { at.ActionTrigger[utils.ThresholdValue], err = strconv.ParseFloat(params[3], 64) if err != nil { return err } } // populate ActionTrigger's Recurrent if params[4] != utils.EmptyString { at.ActionTrigger[utils.Recurrent], err = strconv.ParseBool(params[4]) if err != nil { return err } } // populate ActionTrigger's MinSleep if params[5] != utils.EmptyString { at.ActionTrigger[utils.MinSleep], err = utils.ParseDurationWithNanosecs(params[5]) if err != nil { return err } } // populate ActionTrigger's ExpirationDate if params[6] != utils.EmptyString { at.ActionTrigger[utils.ExpirationDate], err = utils.ParseTimeDetectLayout(params[6], config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { return err } } // populate ActionTrigger's ActivationDate if params[7] != utils.EmptyString { at.ActionTrigger[utils.ActivationDate], err = utils.ParseTimeDetectLayout(params[7], config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { return err } } // populate ActionTrigger's BalanceID if params[8] != utils.EmptyString { at.ActionTrigger[utils.BalanceID] = params[8] } // populate ActionTrigger's BalanceType if params[9] != utils.EmptyString { at.ActionTrigger[utils.BalanceType] = params[9] } // populate ActionTrigger's BalanceCategories if params[10] != utils.EmptyString { at.ActionTrigger[utils.BalanceCategories] = strings.Split(params[10], utils.ANDSep) } // populate ActionTrigger's BalanceDestinationIds if params[11] != utils.EmptyString { at.ActionTrigger[utils.BalanceDestinationIds] = strings.Split(params[11], utils.ANDSep) } // populate ActionTrigger's BalanceRatingSubject if params[12] != utils.EmptyString { at.ActionTrigger[utils.BalanceRatingSubject] = params[12] } // populate ActionTrigger's BalanceSharedGroups if params[13] != utils.EmptyString { at.ActionTrigger[utils.BalanceSharedGroups] = strings.Split(params[13], utils.ANDSep) } // populate ActionTrigger's BalanceExpirationDate if params[14] != utils.EmptyString { at.ActionTrigger[utils.BalanceExpirationDate], err = utils.ParseTimeDetectLayout(params[14], config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { return err } } // populate ActionTrigger's BalanceTimingTags if params[15] != utils.EmptyString { at.ActionTrigger[utils.BalanceTimingTags] = strings.Split(params[15], utils.ANDSep) } // populate ActionTrigger's BalanceWeight if params[16] != utils.EmptyString { at.ActionTrigger[utils.BalanceWeight], err = strconv.ParseFloat(params[16], 64) if err != nil { return err } } // populate ActionTrigger's BalanceBlocker if params[17] != utils.EmptyString { at.ActionTrigger[utils.BalanceBlocker], err = strconv.ParseBool(params[17]) if err != nil { return err } } // populate ActionTrigger's BalanceDisabled if params[18] != utils.EmptyString { at.ActionTrigger[utils.BalanceDisabled], err = strconv.ParseBool(params[18]) if err != nil { return err } } // populate ActionTrigger's ActionsID if params[19] != utils.EmptyString { at.ActionTrigger[utils.ActionsID] = params[19] } // populate ActionTrigger's Weight if params[20] != utils.EmptyString { at.ActionTrigger[utils.Weight], err = strconv.ParseFloat(params[20], 64) if err != nil { return err } } // create the ActionTrigger based on the populated parameters var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetActionTrigger, at, &reply) }