# frozen_string_literal: true module Chat::ChatChannelFetcher MAX_PUBLIC_CHANNEL_RESULTS = 50 def self.structured(guardian) memberships = Chat::ChatChannelMembershipManager.all_for_user(guardian.user) { public_channels: secured_public_channels(guardian, memberships, status: :open, following: true), direct_message_channels: secured_direct_message_channels(guardian.user.id, memberships, guardian), memberships: memberships, } end def self.all_secured_channel_ids(guardian, following: true) allowed_channel_ids_sql = generate_allowed_channel_ids_sql(guardian) return DB.query_single(allowed_channel_ids_sql) if !following DB.query_single(<<~SQL, user_id: guardian.user.id) SELECT chat_channel_id FROM user_chat_channel_memberships WHERE user_chat_channel_memberships.user_id = :user_id AND user_chat_channel_memberships.chat_channel_id IN ( #{allowed_channel_ids_sql} ) SQL end def self.generate_allowed_channel_ids_sql(guardian, exclude_dm_channels: false) category_channel_sql = Category .post_create_allowed(guardian) .joins( "INNER JOIN chat_channels ON chat_channels.chatable_id = categories.id AND chat_channels.chatable_type = 'Category'", ) .select("chat_channels.id") .to_sql dm_channel_sql = "" if !exclude_dm_channels dm_channel_sql = <<~SQL UNION -- secured direct message chat channels #{ ChatChannel .select(:id) .joins( "INNER JOIN direct_message_channels ON direct_message_channels.id = chat_channels.chatable_id AND chat_channels.chatable_type = 'DirectMessage' INNER JOIN direct_message_users ON direct_message_users.direct_message_channel_id = direct_message_channels.id", ) .where("direct_message_users.user_id = :user_id", user_id: guardian.user.id) .to_sql } SQL end <<~SQL -- secured category chat channels #{category_channel_sql} #{dm_channel_sql} SQL end def self.secured_public_channel_slug_lookup(guardian, slugs) allowed_channel_ids = generate_allowed_channel_ids_sql(guardian, exclude_dm_channels: true) ChatChannel .joins( "LEFT JOIN categories ON categories.id = chat_channels.chatable_id AND chat_channels.chatable_type = 'Category'", ) .where(chatable_type: ChatChannel.public_channel_chatable_types) .where("chat_channels.id IN (#{allowed_channel_ids})") .where("chat_channels.slug IN (:slugs)", slugs: slugs) .limit(1) end def self.secured_public_channel_search(guardian, options = {}) allowed_channel_ids = generate_allowed_channel_ids_sql(guardian, exclude_dm_channels: true) channels = ChatChannel.includes(chatable: [:topic_only_relative_url]) channels = channels.includes(:chat_channel_archive) if options[:include_archives] channels = channels .joins( "LEFT JOIN categories ON categories.id = chat_channels.chatable_id AND chat_channels.chatable_type = 'Category'", ) .where(chatable_type: ChatChannel.public_channel_chatable_types) .where("chat_channels.id IN (#{allowed_channel_ids})") channels = channels.where(status: options[:status]) if options[:status].present? if options[:filter].present? category_filter = (options[:filter_on_category_name] ? "OR categories.name ILIKE :filter" : "") sql = "chat_channels.name ILIKE :filter OR chat_channels.slug ILIKE :filter #{category_filter}" if options[:match_filter_on_starts_with] filter_sql = "#{options[:filter].downcase}%" else filter_sql = "%#{options[:filter].downcase}%" end channels = channels.where(sql, filter: filter_sql).order("chat_channels.name ASC, categories.name ASC") end if options.key?(:slugs) channels = channels.where("chat_channels.slug IN (:slugs)", slugs: options[:slugs]) end if options.key?(:following) if options[:following] channels = channels.joins(:user_chat_channel_memberships).where( user_chat_channel_memberships: { user_id: guardian.user.id, following: true, }, ) else channels = channels.where( "chat_channels.id NOT IN (SELECT chat_channel_id FROM user_chat_channel_memberships uccm WHERE uccm.chat_channel_id = chat_channels.id AND following IS TRUE AND user_id = ?)", guardian.user.id, ) end end options[:limit] = (options[:limit] || MAX_PUBLIC_CHANNEL_RESULTS).to_i.clamp( 1, MAX_PUBLIC_CHANNEL_RESULTS, ) options[:offset] = [options[:offset].to_i, 0].max channels.limit(options[:limit]).offset(options[:offset]) end def self.secured_public_channels(guardian, memberships, options = { following: true }) channels = secured_public_channel_search( guardian, options.merge(include_archives: true, filter_on_category_name: true), ) decorate_memberships_with_tracking_data(guardian, channels, memberships) channels = channels.to_a preload_custom_fields_for(channels) channels end def self.preload_custom_fields_for(channels) preload_fields = Category.instance_variable_get(:@custom_field_types).keys Category.preload_custom_fields( channels.select { |c| c.chatable_type == "Category" }.map(&:chatable), preload_fields, ) end def self.secured_direct_message_channels(user_id, memberships, guardian) query = ChatChannel.includes(chatable: [{ direct_message_users: :user }, :users]) query = query.includes(chatable: [{ users: :user_status }]) if SiteSetting.enable_user_status channels = query .joins(:user_chat_channel_memberships) .where(user_chat_channel_memberships: { user_id: user_id, following: true }) .where(chatable_type: "DirectMessage") .where("chat_channels.id IN (#{generate_allowed_channel_ids_sql(guardian)})") .order(last_message_sent_at: :desc) .to_a preload_fields = User.allowed_user_custom_fields(guardian) + UserField.all.pluck(:id).map { |fid| "#{User::USER_FIELD_PREFIX}#{fid}" } User.preload_custom_fields(channels.map { |c| c.chatable.users }.flatten, preload_fields) decorate_memberships_with_tracking_data(guardian, channels, memberships) end def self.decorate_memberships_with_tracking_data(guardian, channels, memberships) unread_counts_per_channel = unread_counts(channels, guardian.user.id) mention_notifications = Notification.unread.where( user_id: guardian.user.id, notification_type: Notification.types[:chat_mention], ) mention_notification_data = mention_notifications.map { |m| JSON.parse(m.data) } channels.each do |channel| membership = memberships.find { |m| m.chat_channel_id == channel.id } if membership membership.unread_mentions = mention_notification_data.count do |data| data["chat_channel_id"] == channel.id && data["chat_message_id"] > (membership.last_read_message_id || 0) end membership.unread_count = unread_counts_per_channel[channel.id] if !membership.muted end end end def self.unread_counts(channels, user_id) unread_counts = DB.query_array(<<~SQL, channel_ids: channels.map(&:id), user_id: user_id).to_h SELECT cc.id, COUNT(*) as count FROM chat_messages cm JOIN chat_channels cc ON cc.id = cm.chat_channel_id JOIN user_chat_channel_memberships uccm ON uccm.chat_channel_id = cc.id WHERE cc.id IN (:channel_ids) AND cm.user_id != :user_id AND uccm.user_id = :user_id AND cm.id > COALESCE(uccm.last_read_message_id, 0) AND cm.deleted_at IS NULL GROUP BY cc.id SQL unread_counts.default = 0 unread_counts end def self.find_with_access_check(channel_id_or_name, guardian) begin channel_id_or_name = Integer(channel_id_or_name) rescue ArgumentError end base_channel_relation = ChatChannel.includes(:chatable).joins( "LEFT JOIN categories ON categories.id = chat_channels.chatable_id AND chat_channels.chatable_type = 'Category'", ) if guardian.user.staff? base_channel_relation = base_channel_relation.includes(:chat_channel_archive) end if channel_id_or_name.is_a? Integer chat_channel = base_channel_relation.find_by(id: channel_id_or_name) else chat_channel = base_channel_relation.find_by( "LOWER(categories.name) = :name OR LOWER(chat_channels.name) = :name", name: channel_id_or_name.downcase, ) end raise Discourse::NotFound if chat_channel.blank? raise Discourse::InvalidAccess if !guardian.can_join_chat_channel?(chat_channel) chat_channel end end