diff --git a/Gemfile b/Gemfile index 8c0bb82a5..dfcdb3e30 100644 --- a/Gemfile +++ b/Gemfile @@ -195,10 +195,10 @@ gem 'reverse_markdown' gem 'iso-639' gem 'ruby-openai' -gem 'ai-agents', '>= 0.9.1' +gem 'ai-agents', '>= 0.10.0' # TODO: Move this gem as a dependency of ai-agents -gem 'ruby_llm', '>= 1.8.2' +gem 'ruby_llm', '>= 1.14.1' gem 'ruby_llm-schema' gem 'cld3', '~> 3.7' diff --git a/Gemfile.lock b/Gemfile.lock index bd21b7a36..7b94e6fe1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,8 +126,8 @@ GEM jbuilder (~> 2) rails (>= 4.2, < 7.2) selectize-rails (~> 0.6) - ai-agents (0.9.1) - ruby_llm (~> 1.9.1) + ai-agents (0.10.0) + ruby_llm (~> 1.14) annotaterb (4.20.0) activerecord (>= 6.0.0) activesupport (>= 6.0.0) @@ -307,8 +307,8 @@ GEM faraday-mashify (1.0.0) faraday (~> 2.0) hashie - faraday-multipart (1.0.4) - multipart-post (~> 2) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) faraday-net_http (3.4.2) net-http (~> 0.5) faraday-net_http_persistent (2.1.0) @@ -466,7 +466,7 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.19.2) + json (2.19.5) json_refs (0.1.8) hana json_schemer (0.2.24) @@ -590,14 +590,14 @@ GEM newrelic_rpm (9.6.0) base64 nio4r (2.7.3) - nokogiri (1.19.1) + nokogiri (1.19.3) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.19.1-arm64-darwin) + nokogiri (1.19.3-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.1-x86_64-darwin) + nokogiri (1.19.3-x86_64-darwin) racc (~> 1.4) - nokogiri (1.19.1-x86_64-linux-gnu) + nokogiri (1.19.3-x86_64-linux-gnu) racc (~> 1.4) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) @@ -835,17 +835,17 @@ GEM ruby2ruby (2.5.0) ruby_parser (~> 3.1) sexp_processor (~> 4.6) - ruby_llm (1.9.2) + ruby_llm (1.15.0) base64 event_stream_parser (~> 1) faraday (>= 1.10.0) faraday-multipart (>= 1) faraday-net_http (>= 1) faraday-retry (>= 1) - marcel (~> 1.0) - ruby_llm-schema (~> 0.2.1) + marcel (~> 1) + ruby_llm-schema (~> 0) zeitwerk (~> 2) - ruby_llm-schema (0.2.5) + ruby_llm-schema (0.3.0) ruby_parser (3.20.0) sexp_processor (~> 4.16) sass (3.7.4) @@ -1019,7 +1019,7 @@ GEM working_hours (1.4.1) activesupport (>= 3.2) tzinfo - zeitwerk (2.7.4) + zeitwerk (2.7.5) PLATFORMS arm64-darwin-20 @@ -1039,7 +1039,7 @@ DEPENDENCIES administrate (>= 0.20.1) administrate-field-active_storage (>= 1.0.3) administrate-field-belongs_to_search (>= 0.9.0) - ai-agents (>= 0.9.1) + ai-agents (>= 0.10.0) annotaterb attr_extras audited (~> 5.4, >= 5.4.1) @@ -1144,7 +1144,7 @@ DEPENDENCIES rubocop-rails rubocop-rspec ruby-openai - ruby_llm (>= 1.8.2) + ruby_llm (>= 1.14.1) ruby_llm-schema scout_apm scss_lint diff --git a/app/builders/notification_builder.rb b/app/builders/notification_builder.rb index d5461eb4e..e09e44d50 100644 --- a/app/builders/notification_builder.rb +++ b/app/builders/notification_builder.rb @@ -27,6 +27,8 @@ class NotificationBuilder return if notification_type == 'conversation_creation' && !user_subscribed_to_notification? # skip notifications for blocked conversations except for user mentions return if primary_actor.contact.blocked? && notification_type != 'conversation_mention' + # respect conversation access (inbox/team membership and custom-role permissions) + return unless user_can_access_conversation? user.notifications.create!( notification_type: notification_type, @@ -36,4 +38,17 @@ class NotificationBuilder secondary_actor: secondary_actor || current_user ) end + + def user_can_access_conversation? + conversation = primary_actor.is_a?(Conversation) ? primary_actor : primary_actor.try(:conversation) + return true if conversation.blank? + + account_user = AccountUser.find_by(account_id: account.id, user_id: user.id) + return false if account_user.blank? + + ConversationPolicy.new( + { user: user, account: account, account_user: account_user }, + conversation + ).show? + end end diff --git a/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb b/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb index 69df99e14..7bd9ae0c3 100644 --- a/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb +++ b/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb @@ -1,6 +1,7 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Accounts::BaseController before_action :fetch_custom_attributes_definitions, except: [:create] before_action :fetch_custom_attribute_definition, only: [:show, :update, :destroy] + before_action :check_authorization DEFAULT_ATTRIBUTE_MODEL = 'conversation_attribute'.freeze def index; end diff --git a/app/controllers/api/v1/accounts/labels_controller.rb b/app/controllers/api/v1/accounts/labels_controller.rb index 54455943b..6889d30a4 100644 --- a/app/controllers/api/v1/accounts/labels_controller.rb +++ b/app/controllers/api/v1/accounts/labels_controller.rb @@ -18,7 +18,16 @@ class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController end def destroy + label_title = @label.title + account_id = Current.account.id + label_deleted_at = Time.current + @label.destroy! + Labels::RemoveAssociationsJob.perform_later( + label_title: label_title, + account_id: account_id, + label_deleted_at: label_deleted_at + ) head :ok end diff --git a/app/helpers/api/v1/inboxes_helper.rb b/app/helpers/api/v1/inboxes_helper.rb index 3d6b559c8..8a10fa99c 100644 --- a/app/helpers/api/v1/inboxes_helper.rb +++ b/app/helpers/api/v1/inboxes_helper.rb @@ -17,15 +17,12 @@ module Api::V1::InboxesHelper def validate_imap(channel_data) return unless channel_data.key?('imap_enabled') && channel_data[:imap_enabled] - Mail.defaults do - retriever_method :imap, { address: channel_data[:imap_address], - port: channel_data[:imap_port], - user_name: channel_data[:imap_login], - password: channel_data[:imap_password], - enable_ssl: channel_data[:imap_enable_ssl] } - end + # Validate the user-selected auth mechanism before opening the connection. + authentication = Imap::Authentication.validate_user_configurable!(channel_data[:imap_authentication]) - check_imap_connection(channel_data) + # Use the same auth adapter as the fetch service so LOGIN uses the IMAP LOGIN command, + # not SASL AUTH=LOGIN. + check_imap_connection(channel_data, authentication) end def validate_smtp(channel_data) @@ -37,8 +34,8 @@ module Api::V1::InboxesHelper check_smtp_connection(channel_data, smtp) end - def check_imap_connection(channel_data) - Mail.connection {} # rubocop:disable:block + def check_imap_connection(channel_data, authentication) + imap = open_imap_connection(channel_data, authentication) rescue SocketError => e raise StandardError, I18n.t('errors.inboxes.imap.socket_error') rescue Net::IMAP::NoResponseError => e @@ -53,9 +50,20 @@ module Api::V1::InboxesHelper rescue StandardError => e raise StandardError, e.message ensure + imap.disconnect if imap.present? && !imap.disconnected? Rails.logger.error "[Api::V1::InboxesHelper] check_imap_connection failed with #{e.message}" if e.present? end + def open_imap_connection(channel_data, authentication) + imap = build_imap_connection(channel_data) + Imap::Authentication.authenticate!(imap, authentication, channel_data[:imap_login], channel_data[:imap_password]) + imap + end + + def build_imap_connection(channel_data) + Net::IMAP.new(channel_data[:imap_address], port: channel_data[:imap_port], ssl: channel_data[:imap_enable_ssl]) + end + def check_smtp_connection(channel_data, smtp) smtp.open_timeout = 10 smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password], diff --git a/app/javascript/dashboard/api/captain/document.js b/app/javascript/dashboard/api/captain/document.js index dc22b0c32..e23a8c460 100644 --- a/app/javascript/dashboard/api/captain/document.js +++ b/app/javascript/dashboard/api/captain/document.js @@ -6,15 +6,22 @@ class CaptainDocument extends ApiClient { super('captain/documents', { accountScoped: true }); } - get({ page = 1, searchKey, assistantId } = {}) { + get({ page = 1, searchKey, assistantId, filter, source, sort } = {}) { return axios.get(this.url, { params: { page, - searchKey, + search_key: searchKey, assistant_id: assistantId, + filter, + source, + sort, }, }); } + + sync(id) { + return axios.post(`${this.url}/${id}/sync`); + } } export default new CaptainDocument(); diff --git a/app/javascript/dashboard/api/companies.js b/app/javascript/dashboard/api/companies.js index b98a59983..a45b21d68 100644 --- a/app/javascript/dashboard/api/companies.js +++ b/app/javascript/dashboard/api/companies.js @@ -28,6 +28,14 @@ class CompanyAPI extends ApiClient { return axios.get(`${this.url}/${id}/contacts?${buildParams({ page })}`); } + listNotes(id) { + return axios.get(`${this.url}/${id}/notes`); + } + + listConversations(id) { + return axios.get(`${this.url}/${id}/conversations`); + } + searchContacts(id, query = '', page = 1) { const requestURL = `${this.url}/${id}/contacts/search?${buildParams({ q: query, page })}`; return axios.get(requestURL); diff --git a/app/javascript/dashboard/components-next/Companies/CompaniesDetailsLayout.vue b/app/javascript/dashboard/components-next/Companies/CompaniesDetailsLayout.vue index 7736ccffd..14b5e62d9 100644 --- a/app/javascript/dashboard/components-next/Companies/CompaniesDetailsLayout.vue +++ b/app/javascript/dashboard/components-next/Companies/CompaniesDetailsLayout.vue @@ -56,9 +56,14 @@ const closeMobileSidebar = () => {
{
- +
+ +
+
+ +
diff --git a/app/javascript/dashboard/components-next/Companies/CompaniesHeader/CompanyHeader.vue b/app/javascript/dashboard/components-next/Companies/CompaniesHeader/CompanyHeader.vue index f85b613a3..00c1a1694 100644 --- a/app/javascript/dashboard/components-next/Companies/CompaniesHeader/CompanyHeader.vue +++ b/app/javascript/dashboard/components-next/Companies/CompaniesHeader/CompanyHeader.vue @@ -2,6 +2,7 @@ import Input from 'dashboard/components-next/input/Input.vue'; import Icon from 'dashboard/components-next/icon/Icon.vue'; import CompanySortMenu from './components/CompanySortMenu.vue'; +import CompanyMoreActions from './components/CompanyMoreActions.vue'; defineProps({ showSearch: { type: Boolean, default: true }, @@ -11,7 +12,7 @@ defineProps({ activeOrdering: { type: String, default: '' }, }); -const emit = defineEmits(['search', 'update:sort']); +const emit = defineEmits(['search', 'update:sort', 'create']);