diff --git a/Gemfile.lock b/Gemfile.lock index fb62589039..7128897536 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,7 +47,7 @@ GIT GIT remote: https://github.com/meedan/rails-i18n.git - revision: ba4b3a267ff09b58a29fe8fc9eb002f9b75d7cba + revision: f8dd347402d9de9125af4db05f94f6933147b18f branch: rails-6-x specs: rails-i18n (6.0.0) diff --git a/app/controllers/api/v1/admin_controller.rb b/app/controllers/api/v1/admin_controller.rb index b9e208a761..c74e45cf80 100644 --- a/app/controllers/api/v1/admin_controller.rb +++ b/app/controllers/api/v1/admin_controller.rb @@ -67,10 +67,22 @@ def save_facebook_credentials_for_smooch_bot(platform) # "platform" is either "i tbi = TeamBotInstallation.find(params[:id]) auth = session['check.facebook.authdata'] status = nil - if params[:token].to_s.gsub('#_=_', '') == tbi.get_smooch_authorization_token - response = Net::HTTP.get_response(URI("https://graph.facebook.com/me/accounts?client_id=#{CheckConfig.get('smooch_facebook_app_id')}&client_secret=#{CheckConfig.get('smooch_facebook_app_secret')}&access_token=#{auth['token']}&limit=100")) + if auth.blank? + status = 400 + @message = I18n.t(:invalid_facebook_authdata) + error_msg = StandardError.new('Could not authenticate Facebook account for tipline Messenger integration.') + CheckSentry.notify(error_msg, team_bot_installation_id: tbi.id, platform: platform) + elsif params[:token].to_s.gsub('#_=_', '') == tbi.get_smooch_authorization_token + q_params = { + client_id: CheckConfig.get('smooch_facebook_app_id'), + client_secret: CheckConfig.get('smooch_facebook_app_secret'), + access_token: auth['token'], + limit: 100, + } + response = Net::HTTP.get_response(URI("https://graph.facebook.com/me/accounts?#{q_params.to_query}")) pages = JSON.parse(response.body)['data'] if pages.size != 1 + CheckSentry.notify(StandardError.new('Unexpected list of Facebook pages returned for tipline Messenger integration'), team_bot_installation_id: tbi.id, response: response.body) @message = I18n.t(:must_select_exactly_one_facebook_page) status = 400 else diff --git a/app/controllers/api/v1/graphql_controller.rb b/app/controllers/api/v1/graphql_controller.rb index e8b0a12c8d..6028110a1c 100644 --- a/app/controllers/api/v1/graphql_controller.rb +++ b/app/controllers/api/v1/graphql_controller.rb @@ -43,6 +43,7 @@ def batch protected def parse_graphql_result + log_graphql_activity context = { ability: @ability, file: parse_uploaded_files } @output = nil begin @@ -64,6 +65,17 @@ def parse_graphql_result end end + def log_graphql_activity + return if User.current.nil? + + uid = User.current.id + user_name = User.current.name + team = Team.current || User.current.current_team + team = team.nil? ? '' : team.name + role = User.current.role + Rails.logger.info("[Graphql] Logging activity: uid: #{uid} user_name: #{user_name} team: #{team} role: #{role}") + end + def parse_uploaded_files file_param = request.params[:file] file = file_param diff --git a/app/graph/mutations/api_key_mutations.rb b/app/graph/mutations/api_key_mutations.rb new file mode 100644 index 0000000000..d4894728c3 --- /dev/null +++ b/app/graph/mutations/api_key_mutations.rb @@ -0,0 +1,11 @@ +module ApiKeyMutations + MUTATION_TARGET = 'api_key'.freeze + PARENTS = ['team'].freeze + + class Create < Mutations::CreateMutation + argument :title, GraphQL::Types::String, required: false + argument :description, GraphQL::Types::String, required: false + end + + class Destroy < Mutations::DestroyMutation; end +end diff --git a/app/graph/mutations/explainer_mutations.rb b/app/graph/mutations/explainer_mutations.rb new file mode 100644 index 0000000000..7856561994 --- /dev/null +++ b/app/graph/mutations/explainer_mutations.rb @@ -0,0 +1,25 @@ +module ExplainerMutations + MUTATION_TARGET = 'explainer'.freeze + PARENTS = ['team'].freeze + + module SharedCreateAndUpdateFields + extend ActiveSupport::Concern + + included do + argument :title, GraphQL::Types::String, required: true + argument :description, GraphQL::Types::String, required: false + argument :url, GraphQL::Types::String, required: false + argument :language, GraphQL::Types::String, required: false + end + end + + class Create < Mutations::CreateMutation + include SharedCreateAndUpdateFields + end + + class Update < Mutations::UpdateMutation + include SharedCreateAndUpdateFields + end + + class Destroy < Mutations::DestroyMutation; end +end diff --git a/app/graph/mutations/feed_mutations.rb b/app/graph/mutations/feed_mutations.rb index e30e546196..893dbc35b3 100644 --- a/app/graph/mutations/feed_mutations.rb +++ b/app/graph/mutations/feed_mutations.rb @@ -29,4 +29,25 @@ class Update < Mutations::UpdateMutation end class Destroy < Mutations::DestroyMutation; end + + class ImportMedia < Mutations::BaseMutation + argument :feed_id, GraphQL::Types::Int, required: true + argument :project_media_id, GraphQL::Types::Int, required: true + argument :parent_id, GraphQL::Types::Int, required: false + argument :claim_title, GraphQL::Types::String, required: false + argument :claim_context, GraphQL::Types::String, required: false + + field :project_media, ProjectMediaType, null: false + + def resolve(feed_id:, project_media_id:, parent_id: nil, claim_title: nil, claim_context: nil) + ability = context[:ability] || Ability.new + feed = Feed.find(feed_id) + pm = nil + if Team.current&.id && User.current&.id && ability.can?(:import_media, feed) + cluster = Cluster.where(feed_id: feed_id, project_media_id: project_media_id).last + pm = cluster.import_medias_to_team(Team.current, claim_title, claim_context, parent_id) + end + { project_media: pm } + end + end end diff --git a/app/graph/types/api_key_type.rb b/app/graph/types/api_key_type.rb new file mode 100644 index 0000000000..8c00bb6685 --- /dev/null +++ b/app/graph/types/api_key_type.rb @@ -0,0 +1,17 @@ +class ApiKeyType < DefaultObject + description "ApiKey type" + + implements GraphQL::Types::Relay::Node + + field :dbid, GraphQL::Types::Int, null: true + field :team_id, GraphQL::Types::Int, null: true + field :user_id, GraphQL::Types::Int, null: true + field :title, GraphQL::Types::String, null: true + field :access_token, GraphQL::Types::String, null: true + field :description, GraphQL::Types::String, null: true + field :application, GraphQL::Types::String, null: true + field :expire_at, GraphQL::Types::String, null: true + + field :team, TeamType, null: true + field :user, UserType, null: true +end diff --git a/app/graph/types/article_union.rb b/app/graph/types/article_union.rb new file mode 100644 index 0000000000..58a7e7479a --- /dev/null +++ b/app/graph/types/article_union.rb @@ -0,0 +1,6 @@ +class ArticleUnion < BaseUnion + description 'A union type of all article types we can handle' + possible_types( + ExplainerType, + ) +end diff --git a/app/graph/types/cluster_type.rb b/app/graph/types/cluster_type.rb index e38aed8d90..092d15a46c 100644 --- a/app/graph/types/cluster_type.rb +++ b/app/graph/types/cluster_type.rb @@ -61,10 +61,11 @@ def cluster_teams end field :project_medias, ProjectMediaType.connection_type, null: true do - argument :team_id, GraphQL::Types::Int, required: true + argument :team_id, GraphQL::Types::Int, required: false end - def project_medias(team_id:) + def project_medias(team_id: nil) + team_id ||= Team.current.id return ProjectMedia.none unless object.team_ids.include?(team_id) object.project_medias.where(team_id: team_id.to_i) end diff --git a/app/graph/types/explainer_type.rb b/app/graph/types/explainer_type.rb new file mode 100644 index 0000000000..dea3441e0c --- /dev/null +++ b/app/graph/types/explainer_type.rb @@ -0,0 +1,21 @@ +class ExplainerType < DefaultObject + description "Explainer type" + + implements GraphQL::Types::Relay::Node + + field :dbid, GraphQL::Types::Int, null: true + field :title, GraphQL::Types::String, null: true + field :description, GraphQL::Types::String, null: true + field :url, GraphQL::Types::String, null: true + field :language, GraphQL::Types::String, null: true + field :user_id, GraphQL::Types::Int, null: true + field :team_id, GraphQL::Types::Int, null: true + field :user, UserType, null: true + field :team, PublicTeamType, null: true + + field :tags, TagType.connection_type, null: true + + def tags + Tag.where(annotation_type: 'tag', annotated_type: object.class.name, annotated_id: object.id) + end +end diff --git a/app/graph/types/mutation_type.rb b/app/graph/types/mutation_type.rb index 5b5b3d3507..63be2053c6 100644 --- a/app/graph/types/mutation_type.rb +++ b/app/graph/types/mutation_type.rb @@ -121,6 +121,7 @@ class MutationType < BaseObject field :createFeed, mutation: FeedMutations::Create field :updateFeed, mutation: FeedMutations::Update field :destroyFeed, mutation: FeedMutations::Destroy + field :feedImportMedia, mutation: FeedMutations::ImportMedia field :updateFeedTeam, mutation: FeedTeamMutations::Update field :destroyFeedTeam, mutation: FeedTeamMutations::Destroy @@ -141,4 +142,11 @@ class MutationType < BaseObject field :addNluKeywordToTiplineMenu, mutation: NluMutations::AddKeywordToTiplineMenu field :removeNluKeywordFromTiplineMenu, mutation: NluMutations::RemoveKeywordFromTiplineMenu + + field :createExplainer, mutation: ExplainerMutations::Create + field :updateExplainer, mutation: ExplainerMutations::Update + field :destroyExplainer, mutation: ExplainerMutations::Destroy + + field :createApiKey, mutation: ApiKeyMutations::Create + field :destroyApiKey, mutation: ApiKeyMutations::Destroy end diff --git a/app/graph/types/project_media_type.rb b/app/graph/types/project_media_type.rb index c5fa19640b..df165d8ac6 100644 --- a/app/graph/types/project_media_type.rb +++ b/app/graph/types/project_media_type.rb @@ -44,6 +44,15 @@ class ProjectMediaType < DefaultObject field :custom_title, GraphQL::Types::String, null: true field :title_field, GraphQL::Types::String, null: true field :suggestions_count, GraphQL::Types::Int, null: true + field :imported_from_feed_id, GraphQL::Types::Int, null: true + field :imported_from_project_media_id, GraphQL::Types::Int, null: true + field :imported_from_feed, FeedType, null: true + + def imported_from_feed + ability = context[:ability] || Ability.new + feed = Feed.find_by_id(object.imported_from_feed_id) + (feed && ability.can?(:read, feed)) ? feed : nil + end field :claim_description, ClaimDescriptionType, null: true @@ -205,12 +214,12 @@ def comments object.get_annotations("comment").map(&:load) end - field :requests, - TiplineRequestType.connection_type, - null: true + field :requests, TiplineRequestType.connection_type, null: true do + argument :include_children, GraphQL::Types::Boolean, required: false + end - def requests - object.get_requests + def requests(include_children: false) + object.get_requests(include_children) end field :last_status, GraphQL::Types::String, null: true diff --git a/app/graph/types/team_type.rb b/app/graph/types/team_type.rb index c8029ab173..d897d2efc0 100644 --- a/app/graph/types/team_type.rb +++ b/app/graph/types/team_type.rb @@ -144,6 +144,12 @@ def get_shorten_outgoing_urls object.get_shorten_outgoing_urls end + field :get_explainers_enabled, GraphQL::Types::Boolean, null: true + + def get_explainers_enabled + object.get_explainers_enabled + end + field :public_team, PublicTeamType, null: true def public_team @@ -281,4 +287,32 @@ def shared_teams def tipline_messages(uid:) TiplineMessagesPagination.new(object.tipline_messages.where(uid: uid).order('sent_at DESC')) end + + field :articles, ::ArticleUnion.connection_type, null: true do + argument :article_type, GraphQL::Types::String, required: true, camelize: false + end + + def articles(article_type:) + object.explainers if article_type == 'explainer' + end + + field :api_key, ApiKeyType, null: true do + argument :dbid, GraphQL::Types::Int, required: true + end + + def api_key(dbid:) + ability = context[:ability] || Ability.new + api_key = object.get_api_key(dbid) + ability.can?(:read, api_key) ? api_key : nil + end + + field :api_keys, ApiKeyType.connection_type, null: true + def api_keys + ability = context[:ability] || Ability.new + api_keys = object.api_keys.order(created_at: :desc) + + api_keys.select do |api_key| + ability.can?(:read, api_key) + end + end end diff --git a/app/mailers/updated_terms_mailer.rb b/app/mailers/updated_terms_mailer.rb index 68138ca688..b1cff66499 100644 --- a/app/mailers/updated_terms_mailer.rb +++ b/app/mailers/updated_terms_mailer.rb @@ -1,6 +1,8 @@ class UpdatedTermsMailer < ApplicationMailer layout nil + rescue_from(Net::SMTPFatalError) { nil } + def notify(recipient, name) @name = name @accept_terms_url = CheckConfig.get('tos_url') diff --git a/app/models/ability.rb b/app/models/ability.rb index 4b18cf6213..825d05d775 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -66,6 +66,7 @@ def admin_perms can :destroy, FeedTeam do |obj| obj.team_id == @context_team.id || obj.feed.team_id == @context_team.id end + can [:create, :update, :read, :destroy], ApiKey, :team_id => @context_team.id end def editor_perms @@ -110,7 +111,10 @@ def editor_perms end can [:read], FeedTeam, :team_id => @context_team.id can [:read], FeedInvitation, { feed: { team_id: @context_team.id } } - can [:read, :create, :update], Feed, :team_id => @context_team.id + can [:read, :create, :update, :import_media], Feed, :team_id => @context_team.id + can :import_media, Feed do |obj| + obj.team_ids.include?(@context_team.id) + end end def collaborator_perms @@ -167,6 +171,7 @@ def collaborator_perms !v_obj.nil? and v_obj.team_id == @context_team.id and v_obj.media.user_id = @user.id end can [:create, :update, :read, :destroy], FactCheck, { claim_description: { project_media: { team_id: @context_team.id } } } + can [:create, :update, :read, :destroy], Explainer, team_id: @context_team.id can [:create, :update, :read], ClaimDescription, { project_media: { team_id: @context_team.id } } end diff --git a/app/models/api_key.rb b/app/models/api_key.rb index 3486e2eea5..3a7adcd977 100644 --- a/app/models/api_key.rb +++ b/app/models/api_key.rb @@ -2,13 +2,21 @@ class Check::TooManyRequestsError < StandardError end class ApiKey < ApplicationRecord + belongs_to :team, optional: true + belongs_to :user, optional: true + validates_presence_of :access_token, :expire_at validates_uniqueness_of :access_token + validates :title, uniqueness: { scope: :team } before_validation :generate_access_token, on: :create before_validation :calculate_expiration_date, on: :create + before_validation :set_user_and_team + after_create :create_bot_user + + validate :validate_team_api_keys_limit, on: :create - has_one :bot_user + has_one :bot_user, dependent: :destroy # Reimplement this method in your application def self.applications @@ -34,7 +42,30 @@ def generate_access_token end end + def create_bot_user + if self.bot_user.blank? && self.team.present? + bot_name = "#{self.team.slug}-bot-#{self.title}" + new_bot_user = BotUser.new(api_key: self, name: bot_name, login: bot_name) + new_bot_user.skip_check_ability = true + new_bot_user.set_role 'editor' + new_bot_user.save! + end + end + + def set_user_and_team + self.user = User.current unless User.current.nil? + self.team = Team.current unless Team.current.nil? + end + def calculate_expiration_date - self.expire_at ||= Time.now.since(30.days) + api_default_expiry_days = CheckConfig.get('api_default_expiry_days', 30).to_i + self.expire_at ||= Time.now.since(api_default_expiry_days.days) + end + + def validate_team_api_keys_limit + return unless team + + max_team_api_keys = CheckConfig.get('max_team_api_keys', 20).to_i + errors.add(:base, "Maximum number of API keys exceeded") if team.api_keys.count >= max_team_api_keys end end diff --git a/app/models/bot/alegre.rb b/app/models/bot/alegre.rb index 5159102607..7a22aa9b8d 100644 --- a/app/models/bot/alegre.rb +++ b/app/models/bot/alegre.rb @@ -34,7 +34,7 @@ def similar_items_ids_and_scores(team_ids, thresholds = {}) 'UploadedImage' => 'image', }[self.media.type].to_s threshold = [{value: thresholds.dig(media_type.to_sym, :value)}] || Bot::Alegre.get_threshold_for_query(media_type, self, true) - ids_and_scores = Bot::Alegre.get_items_with_similar_media_v2(Bot::Alegre.media_file_url(self), threshold, team_ids, media_type).to_h + ids_and_scores = Bot::Alegre.get_items_with_similar_media_v2(project_media: self, threshold: threshold, team_ids: team_ids, media_type: media_type).to_h elsif self.is_text? ids_and_scores = {} threads = [] @@ -152,13 +152,13 @@ def self.run(body) if body.dig(:event) == 'create_project_media' && !pm.nil? Rails.logger.info("[Alegre Bot] [ProjectMedia ##{pm.id}] This item was just created, processing...") self.get_language(pm) - if self.get_pm_type(pm) == "audio" || self.get_pm_type(pm) == "image" - self.relate_project_media(pm) + if ['audio', 'image', 'video'].include?(self.get_pm_type(pm)) + self.relate_project_media_async(pm) else - self.send_to_media_similarity_index(pm) - self.send_field_to_similarity_index(pm, 'original_title') - self.send_field_to_similarity_index(pm, 'original_description') - self.relate_project_media_to_similar_items(pm) + Bot::Alegre.send_to_media_similarity_index(pm) + Bot::Alegre.send_field_to_similarity_index(pm, 'original_title') + Bot::Alegre.send_field_to_similarity_index(pm, 'original_description') + Bot::Alegre.relate_project_media_to_similar_items(pm) end self.get_extracted_text(pm) self.get_flags(pm) @@ -233,10 +233,11 @@ def self.valid_match_types(type) def self.restrict_to_same_modality(pm, matches) other_pms = Hash[ProjectMedia.where(id: matches.keys).includes(:media).all.collect{ |item| [item.id, item] }] if pm.is_text? - matches.select{ |k, _v| other_pms[k.to_i]&.is_text? || !other_pms[k.to_i]&.extracted_text.blank? || !other_pms[k.to_i]&.transcription.blank? || other_pms[k.to_i]&.is_blank? } + selected_matches = matches.select{ |k, _v| other_pms[k.to_i]&.is_text? || !other_pms[k.to_i]&.extracted_text.blank? || !other_pms[k.to_i]&.transcription.blank? || other_pms[k.to_i]&.is_blank? } else - matches.select{ |k, _v| (self.valid_match_types(other_pms[k.to_i]&.media&.type) & self.valid_match_types(pm.media.type)).length > 0 } + selected_matches = matches.select{ |k, _v| (self.valid_match_types(other_pms[k.to_i]&.media&.type) & self.valid_match_types(pm.media.type)).length > 0 } end + selected_matches end def self.merge_suggested_and_confirmed(suggested_or_confirmed, confirmed, pm) @@ -399,7 +400,11 @@ def self.transcribe_audio(pm) def self.media_file_url(pm) # FIXME Ugly hack to get a usable URL in docker-compose development environment. - url = (ENV['RAILS_ENV'] != 'development' ? pm.media.file.file.public_url : "#{CheckConfig.get('storage_endpoint')}/#{CheckConfig.get('storage_bucket')}/#{pm.media.file.file.path}") + if pm.is_a?(TemporaryProjectMedia) + url = pm.url + else + url = (ENV['RAILS_ENV'] != 'development' ? pm.media.file.file.public_url : "#{CheckConfig.get('storage_endpoint')}/#{CheckConfig.get('storage_bucket')}/#{pm.media.file.file.path}") + end # FIXME: Another hack mostly for local development and CI environments... a way to expose media URLs as public URLs url = url.gsub(/^https?:\/\/[^\/]+/, CheckConfig.get('similarity_media_file_url_host')) unless CheckConfig.get('similarity_media_file_url_host').blank? url diff --git a/app/models/bot_user.rb b/app/models/bot_user.rb index d100dba775..6e7283494c 100644 --- a/app/models/bot_user.rb +++ b/app/models/bot_user.rb @@ -416,8 +416,9 @@ def set_default_identifier def create_api_key if self.api_key_id.blank? - api_key = ApiKey.new + api_key = ApiKey.new(bot_user: self) api_key.skip_check_ability = true + api_key.title = self.name api_key.save! api_key.expire_at = api_key.expire_at.since(100.years) api_key.save! diff --git a/app/models/claim_description.rb b/app/models/claim_description.rb index 716323a550..28e73e3cc9 100644 --- a/app/models/claim_description.rb +++ b/app/models/claim_description.rb @@ -1,5 +1,5 @@ class ClaimDescription < ApplicationRecord - include ClaimAndFactCheck + include Article belongs_to :project_media has_one :fact_check, dependent: :destroy @@ -20,4 +20,16 @@ def fact_checks def text_fields ['claim_description_content'] end + + def article_elasticsearch_data(action = 'create_or_update') + return if self.disable_es_callbacks || RequestStore.store[:disable_es_callbacks] + data = action == 'destroy' ? { + 'claim_description_content' => '', + 'claim_description_context' => '' + } : { + 'claim_description_content' => self.description, + 'claim_description_context' => self.context + } + self.index_in_elasticsearch(data) + end end diff --git a/app/models/cluster.rb b/app/models/cluster.rb index 47bf7193ee..eb1e2f7f21 100644 --- a/app/models/cluster.rb +++ b/app/models/cluster.rb @@ -16,4 +16,43 @@ def items def size self.project_medias.count end + + def import_media_to_team(team, from_project_media, claim_title = nil, claim_context = nil) + ProjectMedia.create!({ + team: team, + channel: { main: CheckChannels::ChannelCodes::SHARED_DATABASE }, + media_id: from_project_media.media_id, + imported_from_feed_id: self.feed_id, + imported_from_project_media_id: from_project_media.id, + set_claim_description: claim_title, + set_claim_context: claim_context + }) + end + + def import_medias_to_team(team, claim_title, claim_context, parent_id = nil) + # Find the first item in this cluster for which the media_id doesn't exist in the target team yet + from_project_media = self.project_medias.where.not(team_id: team.id).select(:id, :media_id).find { |item| !ProjectMedia.where(team_id: team.id, media_id: item.media_id).exists? } + raise 'No media to import. All media items from this item already exist in your workspace.' if from_project_media.nil? + parent = nil + if parent_id.nil? + parent = self.import_media_to_team(team, from_project_media, claim_title, claim_context) + else + parent = ProjectMedia.where(id: parent_id, team_id: team.id).last + end + self.class.import_other_medias_to_team(self.id, parent.id, CheckConfig.get('shared_feed_min_media_to_bulk_import', 5, :integer)) # If there are just a few items, we don't even need to wait for the background job + self.class.delay_for(1.second).import_other_medias_to_team(self.id, parent.id, CheckConfig.get('shared_feed_max_media_to_bulk_import', 100, :integer)) + parent + end + + def self.import_other_medias_to_team(cluster_id, parent_id, max) + cluster = Cluster.find_by_id(cluster_id) + parent = ProjectMedia.find_by_id(parent_id) + return if cluster.nil? || parent.nil? + team = parent.team + cluster.project_medias.where.not(team_id: team.id).limit(max).select(:id, :media_id).find_each do |pm| + next if ProjectMedia.where(team_id: team.id, media_id: pm.media_id).exists? + target = cluster.import_media_to_team(team, pm) + Relationship.create(source: parent, target: target, relationship_type: Relationship.confirmed_type) # Didn't use "!" so if fails silently if the similarity bot creates a relationship first + end + end end diff --git a/app/models/concerns/alegre_similarity.rb b/app/models/concerns/alegre_similarity.rb index a46b966ddf..65b88acb2d 100644 --- a/app/models/concerns/alegre_similarity.rb +++ b/app/models/concerns/alegre_similarity.rb @@ -37,11 +37,12 @@ def get_similar_items(pm) def get_items_with_similarity(type, pm, threshold) if type == 'text' - self.get_merged_items_with_similar_text(pm, threshold) + response = self.get_merged_items_with_similar_text(pm, threshold) else - results = self.get_items_with_similar_media_v2(self.media_file_url(pm), threshold, pm.team_id, type).reject{ |id, _score_with_context| pm.id == id } - self.merge_response_with_source_and_target_fields(results, type) + results = self.get_items_with_similar_media_v2(project_media: pm, team_ids: pm.team_id, type: type).reject{ |id, _score_with_context| pm.id == id } + response = self.merge_response_with_source_and_target_fields(results, type) end + self.restrict_contexts(pm, response) end def get_pm_type(pm) @@ -285,18 +286,6 @@ def similar_texts_from_api_conditions(text, models, fuzzy, team_id, fields, thre params end - def get_items_with_similar_media(media_url, threshold, team_id, path) - self.get_similar_items_from_api( - path, - self.similar_media_content_from_api_conditions( - team_id, - media_url, - threshold - ), - threshold - ) - end - def similar_media_content_from_api_conditions(team_id, media_url, threshold, match_across_content_types=true) { url: media_url, diff --git a/app/models/concerns/alegre_v2.rb b/app/models/concerns/alegre_v2.rb index ba64e50e43..69514e1028 100644 --- a/app/models/concerns/alegre_v2.rb +++ b/app/models/concerns/alegre_v2.rb @@ -1,4 +1,42 @@ require 'active_support/concern' +class AlegreTimeoutError < StandardError; end +class TemporaryProjectMedia + attr_accessor :team_id, :id, :url, :type + def media + media_type_map = { + "claim" => "Claim", + "link" => "Link", + "image" => "UploadedImage", + "video" => "UploadedVideo", + "audio" => "UploadedAudio", + } + Struct.new(:type).new(media_type_map[type]) + end + + def is_blank? + self.type == "blank" + end + + def is_link? + self.type == "link" + end + + def is_text? + self.type == "text" + end + + def is_image? + self.type == "image" + end + + def is_video? + self.type == "video" + end + + def is_audio? + self.type == "audio" + end +end module AlegreV2 extend ActiveSupport::Concern @@ -131,6 +169,14 @@ def generic_package_media(project_media, params) ).merge(params) end + def generic_package_video(project_media, params) + generic_package_media(project_media, params) + end + + def delete_package_video(project_media, _field, params) + generic_package_video(project_media, params) + end + def generic_package_image(project_media, params) generic_package_media(project_media, params) end @@ -164,9 +210,14 @@ def get_context(project_media, field=nil) has_custom_id: true } context[:field] = field if field && is_not_generic_field(field) + context[:temporary_media] = project_media.is_a?(TemporaryProjectMedia) context end + def store_package_video(project_media, _field, params) + generic_package_video(project_media, params) + end + def store_package_image(project_media, _field, params) generic_package_image(project_media, params) end @@ -206,7 +257,7 @@ def get_per_model_threshold(project_media, threshold) end def isolate_relevant_context(project_media, result) - result["context"].select{|x| x["team_id"] == project_media.team_id}.first + result["context"].select{|x| ([x["team_id"]].flatten & [project_media.team_id].flatten).count > 0 && !x["temporary_media"]}.first end def get_target_field(project_media, field) @@ -233,6 +284,16 @@ def parse_similarity_results(project_media, field, results, relationship_type) }.reject{ |k,_| k == project_media.id }] end + def safe_get_async(project_media, field, params={}) + response = get_async(project_media, field, params) + retries = 0 + while response.nil? && retries < 3 + response = get_async(project_media, field, params) + retries += 1 + end + response + end + def safe_get_sync(project_media, field, params={}) response = get_sync(project_media, field, params) retries = 0 @@ -243,10 +304,23 @@ def safe_get_sync(project_media, field, params={}) response end - def get_items(project_media, field, confirmed=false) + def cache_items_via_callback(project_media, field, confirmed, results) + relationship_type = confirmed ? Relationship.confirmed_type : Relationship.suggested_type + parse_similarity_results( + project_media, + field, + results, + relationship_type + ) + end + + def get_items(project_media, field, confirmed=false, initial_threshold=nil) relationship_type = confirmed ? Relationship.confirmed_type : Relationship.suggested_type type = get_type(project_media) - threshold = get_per_model_threshold(project_media, Bot::Alegre.get_threshold_for_query(type, project_media, confirmed)) + if initial_threshold.nil? + initial_threshold = get_threshold_for_query(type, project_media, confirmed) + end + threshold = get_per_model_threshold(project_media, initial_threshold) parse_similarity_results( project_media, field, @@ -255,22 +329,81 @@ def get_items(project_media, field, confirmed=false) ) end - def get_suggested_items(project_media, field) - get_items(project_media, field) + def get_items_async(project_media, field, confirmed=false, initial_threshold=nil) + type = get_type(project_media) + if initial_threshold.nil? + initial_threshold = get_threshold_for_query(type, project_media, confirmed) + end + threshold = get_per_model_threshold(project_media, initial_threshold) + safe_get_async(project_media, field, threshold.merge(confirmed: confirmed)) end - def get_confirmed_items(project_media, field) - get_items(project_media, field, true) + def get_suggested_items(project_media, field, threshold=nil) + get_items(project_media, field, false, threshold) end - def get_similar_items_v2(project_media, field) + def get_confirmed_items(project_media, field, threshold=nil) + get_items(project_media, field, true, threshold) + end + + def get_suggested_items_async(project_media, field, threshold=nil) + get_items_async(project_media, field, false, threshold) + end + + def get_confirmed_items_async(project_media, field, threshold=nil) + get_items_async(project_media, field, true, threshold) + end + + def get_similar_items_v2(project_media, field, threshold=nil) type = get_type(project_media) - if !Bot::Alegre.should_get_similar_items_of_type?('master', project_media.team_id) || !Bot::Alegre.should_get_similar_items_of_type?(type, project_media.team_id) + if !should_get_similar_items_of_type?('master', project_media.team_id) || !should_get_similar_items_of_type?(type, project_media.team_id) {} else - suggested_or_confirmed = get_suggested_items(project_media, field) - confirmed = get_confirmed_items(project_media, field) - Bot::Alegre.merge_suggested_and_confirmed(suggested_or_confirmed, confirmed, project_media) + suggested_or_confirmed = get_suggested_items(project_media, field, threshold) + confirmed = get_confirmed_items(project_media, field, threshold) + merge_suggested_and_confirmed(suggested_or_confirmed, confirmed, project_media) + end + end + + def get_similar_items_v2_async(project_media, field, threshold=nil) + type = get_type(project_media) + if !should_get_similar_items_of_type?('master', project_media.team_id) || !should_get_similar_items_of_type?(type, project_media.team_id) + return false + else + get_suggested_items_async(project_media, field, threshold) + get_confirmed_items_async(project_media, field, threshold) + return true + end + end + + def get_required_keys(project_media, field) + { + confirmed_results: "alegre:async_results:#{project_media.id}_#{field}_true", + suggested_or_confirmed_results: "alegre:async_results:#{project_media.id}_#{field}_false" + } + end + + def get_cached_data(required_keys) + redis = Redis.new(REDIS_CONFIG) + # For a given project media, we expect a set of keys to be set by the webhook callbacks sent from alegre back to check-api. + # For each callback response (which is set via #process_alegre_callback), we store the value as serialized YAML to persist + # the data such that symbolized keys return as symbols (as opposed to JSON, which loses the distinction). Here, in effect, + # we check to see if all the responses we expect from Alegre have been sent - downstream of this, we check to see if all + # responses are non-empty before proceeding to creating relationships. + Hash[required_keys.collect{|k,v| [k, (Hash[YAML.load(redis.get(v)).collect{|kk,vv| [kk.to_i, vv]}] rescue [])]}] + end + + def get_similar_items_v2_callback(project_media, field) + type = get_type(project_media) + if !should_get_similar_items_of_type?('master', project_media.team_id) || !should_get_similar_items_of_type?(type, project_media.team_id) + return {} + else + cached_data = get_cached_data(get_required_keys(project_media, field)) + if !cached_data.values.include?(nil) + suggested_or_confirmed = cached_data[:suggested_or_confirmed_results] + confirmed = cached_data[:confirmed_results] + merge_suggested_and_confirmed(suggested_or_confirmed, confirmed, project_media) + end end end @@ -278,11 +411,76 @@ def relate_project_media(project_media, field=nil) self.add_relationships(project_media, self.get_similar_items_v2(project_media, field)) unless project_media.is_blank? end - def get_items_with_similar_media_v2(media_url, threshold, team_ids, type) - alegre_path = ['audio', 'image'].include?(type) ? self.sync_path_for_type(type) : "/#{type}/similarity/search/" - # FIXME: Stop using this method from v1 once all media types are supported by v2 - # FIXME: Alegre crashes if `media_url` was already requested before, this is why I append a hash - self.get_items_with_similar_media("#{media_url}?hash=#{SecureRandom.hex}", threshold, team_ids, alegre_path) + def relate_project_media_async(project_media, field=nil) + self.get_similar_items_v2_async(project_media, field) unless project_media.is_blank? + end + + def relate_project_media_callback(project_media, field=nil) + self.add_relationships(project_media, get_similar_items_v2_callback(project_media, field)) unless project_media.is_blank? + end + + def is_cached_data_not_good(cached_data) + cached_data.values.collect{|x| x.to_a.empty?}.include?(true) + end + + def get_items_with_similar_media_v2(args={}) + media_url = args[:media_url] + project_media = args[:project_media] + threshold = args[:threshold] + team_ids = args[:team_ids] + type = args[:type] + if ['audio', 'image', 'video'].include?(type) + if project_media.nil? + project_media = TemporaryProjectMedia.new + project_media.url = media_url + project_media.id = Digest::MD5.hexdigest(project_media.url).to_i(16) + project_media.team_id = team_ids + project_media.type = type + end + get_similar_items_v2_async(project_media, nil, threshold) + cached_data = get_cached_data(get_required_keys(project_media, nil)) + timeout = args[:timeout] || 60 + start_time = Time.now + while start_time + timeout > Time.now && is_cached_data_not_good(cached_data) #more robust for any type of null response + sleep(1) + cached_data = get_cached_data(get_required_keys(project_media, nil)) + end + CheckSentry.notify(AlegreTimeoutError.new('Timeout when waiting for async response from Alegre'), params: args.merge({ cached_data: cached_data })) if start_time + timeout > Time.now + response = get_similar_items_v2_callback(project_media, nil) + delete(project_media, nil) if project_media.is_a?(TemporaryProjectMedia) + return response + end + end + + def process_alegre_callback(params) + redis = Redis.new(REDIS_CONFIG) + project_media = ProjectMedia.find(params.dig('data', 'item', 'raw', 'context', 'project_media_id')) rescue nil + should_relate = true + if project_media.nil? + project_media = TemporaryProjectMedia.new + project_media.url = params.dig('data', 'item', 'raw', 'url') + project_media.id = params.dig('data', 'item', 'raw', 'context', 'project_media_id') + project_media.team_id = params.dig('data', 'item', 'raw', 'context', 'team_id') + project_media.type = params['model_type'] + should_relate = false + end + confirmed = params.dig('data', 'item', 'raw', 'confirmed') + field = params.dig('data', 'item', 'raw', 'context', 'field') + access_key = confirmed ? :confirmed_results : :suggested_or_confirmed_results + key = get_required_keys(project_media, field)[access_key] + response = cache_items_via_callback(project_media, field, confirmed, params.dig('data', 'results', 'result').dup) #dup so we can better debug when playing with this in a repl + redis.set(key, response.to_yaml) + redis.expire(key, 1.day.to_i) + relate_project_media_callback(project_media, field) if should_relate + end + + def restrict_contexts(project_media, project_media_id_scores) + Hash[project_media_id_scores.collect{|project_media_id, response_data| + [ + project_media_id, + response_data.merge(context: [response_data[:context]].flatten.select{|c| c.with_indifferent_access[:team_id] == project_media.team_id}) + ] + }] end end end diff --git a/app/models/concerns/alegre_webhooks.rb b/app/models/concerns/alegre_webhooks.rb index 326bd4297c..9832e44417 100644 --- a/app/models/concerns/alegre_webhooks.rb +++ b/app/models/concerns/alegre_webhooks.rb @@ -10,16 +10,32 @@ def valid_request?(request) !token.blank? && token == CheckConfig.get('alegre_token') end + def is_from_alegre_search_result_callback(params) + params.dig('data', 'is_shortcircuited_search_result_callback') || params.dig('data', 'is_search_result_callback') + end + + def parse_body(request) + JSON.parse(request.body.read) + end + def webhook(request) - begin - doc_id = request.params.dig('data', 'requested', 'id') - raise 'Unexpected params format' if doc_id.blank? - redis = Redis.new(REDIS_CONFIG) + key = nil + body = parse_body(request) + redis = Redis.new(REDIS_CONFIG) + doc_id = body.dig('data', 'requested', 'id') + # search for doc_id on completed full-circuit callbacks + doc_id = body.dig('data', 'item', 'id') if doc_id.nil? + # search for doc_id on completed short-circuit callbacks (i.e. items already known to Alegre but added context TODO make these the same structure) + doc_id = body.dig('data', 'item', 'raw', 'doc_id') if doc_id.nil? + if doc_id.blank? + CheckSentry.notify(AlegreCallbackError.new('Unexpected params format from Alegre'), params: {alegre_response: request.params, body: body}) + end + if is_from_alegre_search_result_callback(body) + Bot::Alegre.process_alegre_callback(body) + else key = "alegre:webhook:#{doc_id}" - redis.lpush(key, request.params.to_json) - redis.expire(key, 1.day.to_i) - rescue StandardError => e - CheckSentry.notify(AlegreCallbackError.new(e.message), params: { alegre_response: request.params }) + redis.lpush(key, body.to_json) + redis.expire(key, 1.day.to_i) if !key.nil? end end end diff --git a/app/models/concerns/claim_and_fact_check.rb b/app/models/concerns/article.rb similarity index 63% rename from app/models/concerns/claim_and_fact_check.rb rename to app/models/concerns/article.rb index cc88b91595..854fb6ca81 100644 --- a/app/models/concerns/claim_and_fact_check.rb +++ b/app/models/concerns/article.rb @@ -1,6 +1,6 @@ require 'active_support/concern' -module ClaimAndFactCheck +module Article extend ActiveSupport::Concern included do @@ -26,11 +26,15 @@ def set_user end def update_elasticsearch_data - self.index_in_elasticsearch + self.article_elasticsearch_data end def destroy_elasticsearch_data - self.index_in_elasticsearch('destroy') + self.article_elasticsearch_data('destroy') + end + + def article_elasticsearch_data(action = 'create_or_update') + # Implement it in the child class end def send_to_alegre @@ -47,30 +51,7 @@ def notify_bots protected - def index_in_elasticsearch(action = 'create_or_update') - return if self.disable_es_callbacks || RequestStore.store[:disable_es_callbacks] - data = {} - if self.class.name == 'FactCheck' - data = action == 'destroy' ? { - 'fact_check_title' => '', - 'fact_check_summary' => '', - 'fact_check_url' => '', - 'fact_check_languages' => [] - } : { - 'fact_check_title' => self.title, - 'fact_check_summary' => self.summary, - 'fact_check_url' => self.url, - 'fact_check_languages' => [self.language] - } - else - data = action == 'destroy' ? { - 'claim_description_content' => '', - 'claim_description_context' => '' - } : { - 'claim_description_content' => self.description, - 'claim_description_context' => self.context - } - end + def index_in_elasticsearch(data) # touch project media to update `updated_at` date pm = self.project_media pm = ProjectMedia.find_by_id(pm.id) diff --git a/app/models/concerns/project_media_cached_fields.rb b/app/models/concerns/project_media_cached_fields.rb index 7da264558f..62bdec962a 100644 --- a/app/models/concerns/project_media_cached_fields.rb +++ b/app/models/concerns/project_media_cached_fields.rb @@ -469,6 +469,21 @@ def title_or_description_update } ] + cached_field :negative_tipline_search_results_count, + update_es: true, + recalculate: :recalculate_negative_tipline_search_results_count, + update_on: [ + { + model: TiplineRequest, + if: proc { |tr| tr.smooch_request_type == 'irrelevant_search_result_requests' }, + affected_ids: proc { |tr| [tr.associated_id] }, + events: { + save: :recalculate, + destroy: :recalculate, + } + } + ] + cached_field :tipline_search_results_count, update_es: true, recalculate: :recalculate_tipline_search_results_count, @@ -658,6 +673,10 @@ def recalculate_positive_tipline_search_results_count TiplineRequest.where(associated_type: 'ProjectMedia', associated_id: self.id, smooch_request_type: 'relevant_search_result_requests').count end + def recalculate_negative_tipline_search_results_count + TiplineRequest.where(associated_type: 'ProjectMedia', associated_id: self.id, smooch_request_type: 'irrelevant_search_result_requests').count + end + def recalculate_tipline_search_results_count types = ["relevant_search_result_requests", "irrelevant_search_result_requests", "timeout_search_requests"] TiplineRequest.where(associated_type: 'ProjectMedia', associated_id: self.id, smooch_request_type: types).count diff --git a/app/models/concerns/project_media_creators.rb b/app/models/concerns/project_media_creators.rb index 7c2aabf92c..402c111a6c 100644 --- a/app/models/concerns/project_media_creators.rb +++ b/app/models/concerns/project_media_creators.rb @@ -183,7 +183,7 @@ def create_relationship(type = Relationship.confirmed_type) end def create_claim_description_and_fact_check - cd = ClaimDescription.create!(description: self.set_claim_description, project_media: self, skip_check_ability: true) unless self.set_claim_description.blank? + cd = ClaimDescription.create!(description: self.set_claim_description, context: self.set_claim_context, project_media: self, skip_check_ability: true) unless self.set_claim_description.blank? fc = nil unless self.set_fact_check.blank? fact_check = self.set_fact_check.with_indifferent_access diff --git a/app/models/concerns/smooch_menus.rb b/app/models/concerns/smooch_menus.rb index 9a7296a871..5cf8e59590 100644 --- a/app/models/concerns/smooch_menus.rb +++ b/app/models/concerns/smooch_menus.rb @@ -120,10 +120,14 @@ def send_message_to_user_with_main_menu_appended(uid, text, workflow, language, def adjust_language_options(rows, language, number_of_options) # WhatsApp just supports up to 10 options, so if we already have 10, we need to replace the # individual language options by a single "Languages" option (because we still have the "Privacy and Policy" option) + # We can display this single "Languages" option in two languages: the current one and the default one + title = [self.get_string('languages', language)] + default_language = Team.find_by_id(self.config['team_id'].to_i)&.default_language || 'en' + title << self.get_string('languages', default_language) if language != default_language new_rows = rows.dup new_rows = [{ id: { state: 'main', keyword: JSON.parse(rows.first[:id])['keyword'] }.to_json, - title: '🌐 ' + self.get_string('languages', language, 24) + title: '🌐 ' + title.join(' / ').truncate(21) # Maximum is 24 }] if number_of_options >= 10 new_rows end diff --git a/app/models/concerns/smooch_search.rb b/app/models/concerns/smooch_search.rb index 171aecf5d8..788d212b45 100644 --- a/app/models/concerns/smooch_search.rb +++ b/app/models/concerns/smooch_search.rb @@ -83,8 +83,14 @@ def is_a_valid_search_result(pm) pm.report_status == 'published' && [CheckArchivedFlags::FlagCodes::NONE, CheckArchivedFlags::FlagCodes::UNCONFIRMED].include?(pm.archived) end + def reject_temporary_results(results) + results.select do |_, result_data| + ![result_data[:context]].flatten.compact.select{|x| x[:temporary_media].nil? || x[:temporary_media] == false}.empty? + end + end + def parse_search_results_from_alegre(results, after = nil, feed_id = nil, team_ids = nil) - pms = results.sort_by{ |a| [a[1][:model] != Bot::Alegre::ELASTICSEARCH_MODEL ? 1 : 0, a[1][:score]] }.to_h.keys.reverse.collect{ |id| Relationship.confirmed_parent(ProjectMedia.find_by_id(id)) } + pms = reject_temporary_results(results).sort_by{ |a| [a[1][:model] != Bot::Alegre::ELASTICSEARCH_MODEL ? 1 : 0, a[1][:score]] }.to_h.keys.reverse.collect{ |id| Relationship.confirmed_parent(ProjectMedia.find_by_id(id)) } filter_search_results(pms, after, feed_id, team_ids).uniq(&:id).first(3) end @@ -151,6 +157,7 @@ def search_for_similar_published_fact_checks_no_cache(type, query, team_ids, aft text = [link.pender_data['description'].to_s, text.to_s.gsub(/https?:\/\/[^\s]+/, '').strip].max_by(&:length) end return [] if text.blank? + text = self.remove_meaningless_phrases(text) words = text.split(/\s+/) Rails.logger.info "[Smooch Bot] Search query (text): #{text}" if Bot::Alegre.get_number_of_words(text) <= self.max_number_of_words_for_keyword_search @@ -166,13 +173,21 @@ def search_for_similar_published_fact_checks_no_cache(type, query, team_ids, aft return [] if media_url.blank? media_url = self.save_locally_and_return_url(media_url, type, feed_id) threshold = Bot::Alegre.get_threshold_for_query(type, pm)[0][:value] - alegre_results = Bot::Alegre.get_items_with_similar_media_v2(media_url, [{ value: threshold }], team_ids, type) + alegre_results = Bot::Alegre.get_items_with_similar_media_v2(media_url: media_url, threshold: [{ value: threshold }], team_ids: team_ids, type: type) results = self.parse_search_results_from_alegre(alegre_results, after, feed_id, team_ids) Rails.logger.info "[Smooch Bot] Media similarity search got #{results.count} results while looking for '#{query}' after date #{after.inspect} for teams #{team_ids}" end results end + def remove_meaningless_phrases(text) + redis = Redis.new(REDIS_CONFIG) + meaningless_phrases = JSON.parse(redis.get("smooch_search_meaningless_phrases") || "[]") + meaningless_phrases.each{|phrase| text.sub!(/^#{phrase}\W/i,'')} + text.strip!() + text + end + def save_locally_and_return_url(media_url, type, feed_id) feed = Feed.find_by_id(feed_id.to_i) return media_url if feed.nil? diff --git a/app/models/concerns/team_associations.rb b/app/models/concerns/team_associations.rb index 1e839fddd8..5d0cbb5adc 100644 --- a/app/models/concerns/team_associations.rb +++ b/app/models/concerns/team_associations.rb @@ -21,6 +21,8 @@ module TeamAssociations has_many :tipline_messages has_many :tipline_newsletters has_many :tipline_requests, as: :associated + has_many :explainers, dependent: :destroy + has_many :api_keys has_annotations end diff --git a/app/models/explainer.rb b/app/models/explainer.rb new file mode 100644 index 0000000000..52a01f214f --- /dev/null +++ b/app/models/explainer.rb @@ -0,0 +1,32 @@ +class Explainer < ApplicationRecord + include Article + + belongs_to :team + + has_annotations + + before_validation :set_team + validates_format_of :url, with: URI.regexp, allow_blank: true, allow_nil: true + validates_presence_of :team + validate :language_in_allowed_values, unless: proc { |e| e.language.blank? } + + def notify_bots + # Nothing to do for Explainer + end + + def send_to_alegre + # Nothing to do for Explainer + end + + private + + def set_team + self.team ||= Team.current + end + + def language_in_allowed_values + allowed_languages = self.team.get_languages || ['en'] + allowed_languages << 'und' + errors.add(:language, I18n.t(:"errors.messages.invalid_article_language_value")) unless allowed_languages.include?(self.language) + end +end diff --git a/app/models/fact_check.rb b/app/models/fact_check.rb index eeac39a38b..fccd7d90f6 100644 --- a/app/models/fact_check.rb +++ b/app/models/fact_check.rb @@ -1,5 +1,5 @@ class FactCheck < ApplicationRecord - include ClaimAndFactCheck + include Article attr_accessor :skip_report_update, :publish_report @@ -32,7 +32,7 @@ def set_language def language_in_allowed_values allowed_languages = self.project_media&.team&.get_languages || ['en'] allowed_languages << 'und' - errors.add(:language, I18n.t(:"errors.messages.invalid_fact_check_language_value")) unless allowed_languages.include?(self.language) + errors.add(:language, I18n.t(:"errors.messages.invalid_article_language_value")) unless allowed_languages.include?(self.language) end def title_or_summary_exists @@ -76,4 +76,20 @@ def update_report reports.skip_check_ability = true reports.save! end + + def article_elasticsearch_data(action = 'create_or_update') + return if self.disable_es_callbacks || RequestStore.store[:disable_es_callbacks] + data = action == 'destroy' ? { + 'fact_check_title' => '', + 'fact_check_summary' => '', + 'fact_check_url' => '', + 'fact_check_languages' => [] + } : { + 'fact_check_title' => self.title, + 'fact_check_summary' => self.summary, + 'fact_check_url' => self.url, + 'fact_check_languages' => [self.language] + } + self.index_in_elasticsearch(data) + end end diff --git a/app/models/project_media.rb b/app/models/project_media.rb index d9411fff2e..5565d19ca8 100644 --- a/app/models/project_media.rb +++ b/app/models/project_media.rb @@ -1,5 +1,5 @@ class ProjectMedia < ApplicationRecord - attr_accessor :quote, :quote_attributions, :file, :media_type, :set_annotation, :set_tasks_responses, :previous_project_id, :cached_permissions, :is_being_created, :related_to_id, :skip_rules, :set_claim_description, :set_fact_check, :set_tags, :set_title, :set_status + attr_accessor :quote, :quote_attributions, :file, :media_type, :set_annotation, :set_tasks_responses, :previous_project_id, :cached_permissions, :is_being_created, :related_to_id, :skip_rules, :set_claim_description, :set_claim_context, :set_fact_check, :set_tags, :set_title, :set_status belongs_to :media has_one :claim_description @@ -388,9 +388,9 @@ def version_metadata(changes) meta.to_json end - def get_requests + def get_requests(include_children = false) # Get related items for parent item - pm_ids = Relationship.confirmed_parent(self).id == self.id ? self.related_items_ids : [self.id] + pm_ids = (Relationship.confirmed_parent(self).id == self.id && include_children) ? self.related_items_ids : [self.id] TiplineRequest.where(associated_type: 'ProjectMedia', associated_id: pm_ids).order('created_at ASC') end diff --git a/app/models/team.rb b/app/models/team.rb index b8e1b5edbf..c6c1ff5ee3 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -610,6 +610,10 @@ def get_feed(feed_id) self.feeds.where(id: feed_id.to_i).last end + def get_api_key(api_key_id) + self.api_keys.where(id: api_key_id.to_i).last + end + # A newsletter header type is available only if there are WhatsApp templates for it def available_newsletter_header_types available = [] diff --git a/app/models/user.rb b/app/models/user.rb index 4737048d28..22cd943c4f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -28,6 +28,7 @@ class ToSOrPrivacyPolicyReadError < StandardError; end has_many :feeds has_many :feed_invitations has_many :tipline_requests + has_many :api_keys devise :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable, diff --git a/app/repositories/media_search.rb b/app/repositories/media_search.rb index 3fcf8739b5..44748659cc 100644 --- a/app/repositories/media_search.rb +++ b/app/repositories/media_search.rb @@ -139,6 +139,8 @@ class MediaSearch indexes :positive_tipline_search_results_count, { type: 'long' } + indexes :negative_tipline_search_results_count, { type: 'long' } + indexes :tipline_search_results_count, { type: 'long' } end end diff --git a/app/resources/api/v2/report_resource.rb b/app/resources/api/v2/report_resource.rb index 309de6253a..29ee00cc87 100644 --- a/app/resources/api/v2/report_resource.rb +++ b/app/resources/api/v2/report_resource.rb @@ -27,7 +27,7 @@ class ReportResource < BaseResource attribute :report_text_content def score - RequestStore.store[:scores] ? RequestStore.store[:scores][@model.id][:score].to_f : nil + (RequestStore.store[:scores] && @model && RequestStore.store[:scores][@model.id]) ? RequestStore.store[:scores][@model.id][:score].to_f : nil end def self.records(options = {}) @@ -100,7 +100,7 @@ def self.apply_media_similarity_filter(organization_ids, threshold, media_path, unless media.blank? media[0].rewind CheckS3.write(media_path, media[0].content_type.gsub(/^video/, 'application'), media[0].read) - ids_and_scores = Bot::Alegre.get_items_with_similar_media_v2(CheckS3.public_url(media_path), [{ value: threshold }], organization_ids, media_type) + ids_and_scores = Bot::Alegre.get_items_with_similar_media_v2(media_url: CheckS3.public_url(media_path), threshold: [{ value: threshold }], team_ids: organization_ids, type: media_type) RequestStore.store[:scores] = ids_and_scores # Store the scores so we can return them ids = ids_and_scores.keys.uniq || [0] CheckS3.delete(media_path) diff --git a/app/workers/terms_of_service_update_worker.rb b/app/workers/terms_of_service_update_worker.rb index 21c8387157..451ae8022f 100644 --- a/app/workers/terms_of_service_update_worker.rb +++ b/app/workers/terms_of_service_update_worker.rb @@ -6,9 +6,15 @@ class TermsOfServiceUpdateWorker def perform last_updated = Time.at(User.terms_last_updated_at) updated_time = Time.now - User.where(is_active: true).where('email IS NOT NULL').where('last_received_terms_email_at < ?', last_updated).find_each do |u| - UpdatedTermsMailer.delay({ retry: 0, queue: 'terms_mailer' }).notify(u.email, u.name) - u.update_columns(last_received_terms_email_at: updated_time) + # Based on our AWS SES account (Maximum send rate: 200 emails per second) I set a batch size 200 and do a sleep 1 + User.where(is_active: true).where('email IS NOT NULL') + .where('last_received_terms_email_at < ?', last_updated) + .find_in_batches(:batch_size => 200) do |users| + users.each do |u| + UpdatedTermsMailer.delay({ retry: 1, queue: 'terms_mailer' }).notify(u.email, u.name) + u.update_columns(last_received_terms_email_at: updated_time) + end + sleep 2 end end end diff --git a/config/application.rb b/config/application.rb index 456a124946..64ba05c020 100644 --- a/config/application.rb +++ b/config/application.rb @@ -47,7 +47,7 @@ class Application < Rails::Application locale = ENV['locale'] || cfg['locale'] if locale.blank? - config.i18n.available_locales = ["ar","bho","bn","fil","fr","de","hi","id","kn","mk","ml","mn","mr","pa","pt","ro","ru","es","sw","ta","te","ur","en","am","as","bn_BD","gu","ks","ne","si","tl"] # Do not change manually! Use `rake transifex:languages` instead, or set the `locale` key in your `config/config.yml` + config.i18n.available_locales = ["ar","bho","bn","ckb","fil","fr","de","hi","id","kn","mk","ml","mn","mr","pa","pt","ro","ru","es","sw","ta","te","ur","en","am","as","bn_BD","gu","ks","ne","si","tl"] # Do not change manually! Use `rake transifex:languages` instead, or set the `locale` key in your `config/config.yml` else config.i18n.available_locales = [locale].flatten end diff --git a/config/locales/en.yml b/config/locales/en.yml index 9cd425a71c..f4cb941008 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -107,7 +107,7 @@ en: invalid_project_media_channel_value: Sorry, channel value not supported invalid_project_media_channel_update: Sorry, you could not update channel value invalid_project_media_archived_value: Sorry, archived value not supported - invalid_fact_check_language_value: Sorry, language value not supported + invalid_article_language_value: Sorry, language value not supported fact_check_empty_title_and_summary: Sorry, you should fill title or summary invalid_feed_saved_search_value: should belong to a workspace that is part of this feed platform_allowed_values_error: 'cannot be of type %{type}, allowed types: %{allowed_types}' @@ -654,6 +654,7 @@ en: Please reload your tipline settings page to see the new changes. must_select_exactly_one_facebook_page: Please select exactly the one Facebook page you want to integrate with the tipline. + invalid_facebook_authdata: It was not possible to authenticate your Facebook account, please reach out to our support team. invalid_task_answer: Invalid task answer format team_rule_name: A unique name that identifies what this rule does team_rule_project_ids: Apply to folder diff --git a/config/tipline_strings.yml b/config/tipline_strings.yml index 8dfac808e1..0e465c5470 100644 --- a/config/tipline_strings.yml +++ b/config/tipline_strings.yml @@ -186,6 +186,45 @@ bn_BD: subscribed: আপনি বর্তমানে আমাদের নিউজলেটারে সাবস্ক্রাইব করেছেন। unsubscribe_button_label: আনসাবস্ক্রাইব unsubscribed: আপনি বর্তমানে আমাদের নিউজলেটারে সাবস্ক্রাইব করেন নি। +ckb: + add_more_details_state_button_label: زیاد کردن + ask_if_ready_state_button_label: هەڵوەشاندنەوە + cancelled: باشە + confirm_preferred_language: تکایە زمانی خوازراوت جێگیر بکە + invalid_format: "ببورە، ئەو پەڕگەیەی کە پێشکەشت کردووە فۆرماتێکی پشتگیری نەکراوە." + keep_subscription_button_label: هێشتنەوەی بەشداری + languages: زمان + languages_and_privacy_title: زمان و پاراستنی زانیاری + main_menu: لیستی سەرەکی + main_state_button_label: هەڵوەشاندنەوە + navigation_button: دوگمەکان بەکاربنە بۆ گەڕان. + no_results_in_language: "هیچ وەنجامک نەدۆزرایەوە لە %{language}. ئەمانە هەندێک ئەنجامی دیکەن بە زمانی تر کە لەوانەیە پەیوەندیدار بن." + privacy_and_purpose: |- + پاراستنی زانیاری و مەبەست + + بەخێربێن بۆ هێڵی هاوکاری %{team} %{channel}. + + دەتوانیت ئەم ژمارەیە بەکاربهێنیت بۆ پێشکەشکردنی پرسیارەکەت بۆ پشتڕاستکردنەوە. + + داتاکانتان سەلامەتن. ئێمە بەرپرسیارێتی خۆمان بە گرنگییەوە وەردەگرین بۆ پاراستنی زانیارییە کەسییەکانت و پاراستنی %{channel} بە تایبەت و پارێزراو؛ هەرگیز زانیارییە کەسییە ناسراوەکانت (PII) هاوبەش ناکات، نایفرۆشێت، یان بە شێوەیەکی تر بەکاری ناهێنێت جگە لە پێشکەشکردن و باشترکردنی ئەم خزمەتگوزارییە. + + بۆ دیاریکردنی زانیاری هەڵەی ڤایرۆسی بە زووترین کات لە داهاتوودا، ڕەنگە ناوەڕۆکی نا-PII لەم هێڵە ئامۆژگارییە لەگەڵ توێژەرانی پشکنراو و هاوبەشەکانی پشکنینی ڕاستییەکان هاوبەش بکەین. + + تکایە ئاگاداربن ئەو ماڵپەڕانەی کە بەستەریان بۆ دەکەین سیاسەتی پاراستنی زانیاری تایبەت بە خۆیان دەبێت. + + ئەگەر ناتەوێت پێشکەشکردنەکانتان لەم کارەدا بەکاربهێنرێن، تکایە بەشداری لە سیستەمەکەماندا مەکەن + privacy_statement: بەیاننامەی پاراستنی زانیاری + privacy_title: پاراستنی زانیاری + report_updated: "ئەم پشکنینی-ڕاستییەی خوارەوە *نوێکراوەتەوە* بە زانیاری نوێ:" + search_result_is_not_relevant_button_label: "نەخێر" + search_result_is_relevant_button_label: "بەڵێ" + search_state_button_label: پێشکەشکردن + subscribe_button_label: بەشداریکردن + subscribed: لە ئێستادا ئێوە بەشدارن لە هەواڵنامەکەمان. + unsubscribe_button_label: وەستاندنی بەشداری + unsubscribed: لە ئێستادا ئێوە بەشدارنین لە هەواڵنامەکەمان. + nlu_disambiguation: "یەک لەم بژاردانەی خوارەوە هەڵبژێرە:" + nlu_cancel: "گەڕانەوە بۆ لیست" zh_CN: add_more_details_state_button_label: 添加 ask_if_ready_state_button_label: 取消 diff --git a/data/ckb/languages.yml b/data/ckb/languages.yml new file mode 100644 index 0000000000..cbcacea417 --- /dev/null +++ b/data/ckb/languages.yml @@ -0,0 +1,3 @@ +ckb: + languages: + ckb: کوردی \ No newline at end of file diff --git a/db/migrate/20150729232909_create_api_keys.rb b/db/migrate/20150729232909_create_api_keys.rb index 2c6abe03ab..50907412f8 100644 --- a/db/migrate/20150729232909_create_api_keys.rb +++ b/db/migrate/20150729232909_create_api_keys.rb @@ -2,6 +2,9 @@ class CreateApiKeys < ActiveRecord::Migration[4.2] def change create_table :api_keys do |t| t.string :access_token, null: false, default: '' + t.string :title + t.references :user, null: true + t.references :team, null: true t.datetime :expire_at t.jsonb :rate_limits, default: {} t.string :application diff --git a/db/migrate/20240401000710_add_imported_columns_to_project_medias.rb b/db/migrate/20240401000710_add_imported_columns_to_project_medias.rb new file mode 100644 index 0000000000..bc3ee61bb4 --- /dev/null +++ b/db/migrate/20240401000710_add_imported_columns_to_project_medias.rb @@ -0,0 +1,6 @@ +class AddImportedColumnsToProjectMedias < ActiveRecord::Migration[6.1] + def change + add_column :project_medias, :imported_from_feed_id, :integer + add_column :project_medias, :imported_from_project_media_id, :integer + end +end diff --git a/db/migrate/20240417140727_create_explainers.rb b/db/migrate/20240417140727_create_explainers.rb new file mode 100644 index 0000000000..75b45dea79 --- /dev/null +++ b/db/migrate/20240417140727_create_explainers.rb @@ -0,0 +1,14 @@ +class CreateExplainers < ActiveRecord::Migration[6.1] + def change + create_table :explainers do |t| + t.string :title + t.text :description + t.string :url + t.string :language + t.references :user, foreign_key: true, null: false + t.references :team, foreign_key: true, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20240420104318_add_title_desc_to_api_keys.rb b/db/migrate/20240420104318_add_title_desc_to_api_keys.rb new file mode 100644 index 0000000000..e440b29119 --- /dev/null +++ b/db/migrate/20240420104318_add_title_desc_to_api_keys.rb @@ -0,0 +1,8 @@ +class AddTitleDescToApiKeys < ActiveRecord::Migration[6.1] + def change + add_column(:api_keys, :title, :string) unless column_exists?(:api_keys, :title) + add_column :api_keys, :description, :string + add_reference(:api_keys, :team, foreign_key: true, null: true) unless column_exists?(:api_keys, :team_id) + add_reference(:api_keys, :user, foreign_key: true, null: true) unless column_exists?(:api_keys, :user_id) + end +end diff --git a/db/schema.rb b/db/schema.rb index b2578b0715..3620cd4a75 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_04_04_154458) do +ActiveRecord::Schema.define(version: 2024_04_20_104318) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -172,11 +172,15 @@ create_table "api_keys", id: :serial, force: :cascade do |t| t.string "access_token", default: "", null: false + t.string "title" + t.integer "user_id" + t.integer "team_id" t.datetime "expire_at" t.jsonb "rate_limits", default: {} t.string "application" t.datetime "created_at" t.datetime "updated_at" + t.string "description" end create_table "assignments", id: :serial, force: :cascade do |t| @@ -285,7 +289,7 @@ t.jsonb "value_json", default: "{}" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index "dynamic_annotation_fields_value(field_name, value)", name: "dynamic_annotation_fields_value", where: "((field_name)::text = ANY ((ARRAY['external_id'::character varying, 'smooch_user_id'::character varying, 'verification_status_status'::character varying])::text[]))" + t.index "dynamic_annotation_fields_value(field_name, value)", name: "dynamic_annotation_fields_value", where: "((field_name)::text = ANY (ARRAY[('external_id'::character varying)::text, ('smooch_user_id'::character varying)::text, ('verification_status_status'::character varying)::text]))" t.index ["annotation_id", "field_name"], name: "index_dynamic_annotation_fields_on_annotation_id_and_field_name" t.index ["annotation_id"], name: "index_dynamic_annotation_fields_on_annotation_id" t.index ["annotation_type"], name: "index_dynamic_annotation_fields_on_annotation_type" @@ -298,6 +302,19 @@ t.index ["value_json"], name: "index_dynamic_annotation_fields_on_value_json", using: :gin end + create_table "explainers", force: :cascade do |t| + t.string "title" + t.text "description" + t.string "url" + t.string "language" + t.bigint "user_id", null: false + t.bigint "team_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["team_id"], name: "index_explainers_on_team_id" + t.index ["user_id"], name: "index_explainers_on_user_id" + end + create_table "fact_checks", force: :cascade do |t| t.text "summary" t.string "url" @@ -469,7 +486,7 @@ t.index ["user_id"], name: "index_project_media_users_on_user_id" end - create_table "project_medias", id: :serial, force: :cascade do |t| + create_table "project_medias", force: :cascade do |t| t.integer "project_id" t.integer "media_id" t.integer "user_id" @@ -486,6 +503,8 @@ t.integer "unmatched", default: 0 t.string "custom_title" t.string "title_field" + t.integer "imported_from_feed_id" + t.integer "imported_from_project_media_id" t.index ["channel"], name: "index_project_medias_on_channel" t.index ["last_seen"], name: "index_project_medias_on_last_seen" t.index ["media_id"], name: "index_project_medias_on_media_id" @@ -862,8 +881,7 @@ end create_table "versions", id: :serial, force: :cascade do |t| - t.string "item_type" - t.string "{:null=>false}" + t.string "item_type", null: false t.string "item_id", null: false t.string "event", null: false t.string "whodunnit" @@ -884,6 +902,8 @@ add_foreign_key "claim_descriptions", "project_medias" add_foreign_key "claim_descriptions", "users" + add_foreign_key "explainers", "teams" + add_foreign_key "explainers", "users" add_foreign_key "fact_checks", "claim_descriptions" add_foreign_key "fact_checks", "users" add_foreign_key "feed_invitations", "feeds" diff --git a/db/seeds.rb b/db/seeds.rb index 376ed5cf49..7d7271810d 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -29,12 +29,19 @@ def open_file(file) { type: 'UploadedVideo', file: open_file(video) } end -MEDIAS_PARAMS = [ - *CLAIMS_PARAMS, - *UPLOADED_AUDIO_PARAMS, - *UPLOADED_IMAGE_PARAMS, - *UPLOADED_VIDEO_PARAMS, -].shuffle! +LINK_PARAMS = -> {[ + 'https://meedan.com/post/addressing-misinformation-across-countries-a-pioneering-collaboration-between-taiwan-factcheck-center-vera-files', + 'https://meedan.com/post/entre-becos-a-women-led-hyperlocal-newsletter-from-the-peripheries-of-brazil', + 'https://meedan.com/post/check-global-launches-independent-media-response-fund-tackles-on-climate-misinformation', + 'https://meedan.com/post/chambal-media', + 'https://meedan.com/post/application-process-for-the-check-global-independent-media-response-fund', + 'https://meedan.com/post/new-e-course-on-the-fundamentals-of-climate-and-environmental-reporting-in-africa', + 'https://meedan.com/post/annual-report-2022', + 'https://meedan.com/post/meedan-joins-partnership-on-ais-ai-and-media-integrity-steering-committee', +].map do |url| + { type: 'Link', url: url+"?timestamp=#{Time.now.to_f}" } + end +} class Setup @@ -220,7 +227,7 @@ def populate_projects title: "#{teams[:main_team_a][:name]} / [a] Main User: Main Team", user: users[:main_user_a], team: teams[:main_team_a], - project_medias_attributes: medias_params_with_links.map.with_index { |media_params, index| + project_medias_attributes: medias_params.map.with_index { |media_params, index| { media_attributes: media_params, user: users[:main_user_a], @@ -244,7 +251,7 @@ def populate_projects title: "#{teams[:invited_team_b1][:name]} / [b] Invited User: Project Team #1", user: users[:invited_user_b], team: teams[:invited_team_b1], - project_medias_attributes: MEDIAS_PARAMS.map.with_index { |media_params, index| + project_medias_attributes: medias_params.map.with_index { |media_params, index| { media_attributes: media_params, user: users[:invited_user_b], @@ -262,7 +269,7 @@ def populate_projects title: "#{teams[:invited_team_b2][:name]} / [b] Invited User: Project Team #2", user: users[:invited_user_b], team: teams[:invited_team_b2], - project_medias_attributes: MEDIAS_PARAMS.map.with_index { |media_params, index| + project_medias_attributes: medias_params.map.with_index { |media_params, index| { media_attributes: media_params, user: users[:invited_user_b], @@ -280,7 +287,7 @@ def populate_projects title: "#{teams[:invited_team_c][:name]} / [c] Invited User: Project Team #1", user: users[:invited_user_c], team: teams[:invited_team_c], - project_medias_attributes: MEDIAS_PARAMS.map.with_index { |media_params, index| + project_medias_attributes: medias_params.map.with_index { |media_params, index| { media_attributes: media_params, user: users[:invited_user_c], @@ -311,6 +318,10 @@ def saved_searches teams.each_value { |team| saved_search(team) } end + def explainers + teams.each_value { |team| 5.times { create_explainer(team) } } + end + def main_user_feed(to_be_shared) if to_be_shared == "share_factchecks" data_points = [1] @@ -390,37 +401,24 @@ def suggest_relationships def tipline_requests teams_project_medias.each_value do |team_project_medias| - create_tipline_requests(team_project_medias) + create_tipline_requests(team_project_medias) end end private - def medias_params_with_links - links_params = [ - 'https://meedan.com/post/addressing-misinformation-across-countries-a-pioneering-collaboration-between-taiwan-factcheck-center-vera-files', - 'https://meedan.com/post/entre-becos-a-women-led-hyperlocal-newsletter-from-the-peripheries-of-brazil', - 'https://meedan.com/post/check-global-launches-independent-media-response-fund-tackles-on-climate-misinformation', - 'https://meedan.com/post/chambal-media', - 'https://meedan.com/post/application-process-for-the-check-global-independent-media-response-fund', - 'https://meedan.com/post/new-e-course-on-the-fundamentals-of-climate-and-environmental-reporting-in-africa', - 'https://meedan.com/post/annual-report-2022', - 'https://meedan.com/post/meedan-joins-partnership-on-ais-ai-and-media-integrity-steering-committee', - ].map do |url| - { type: 'Link', url: url+"?timestamp=#{Time.now.to_f}" } - end - + def medias_params [ *CLAIMS_PARAMS, *UPLOADED_AUDIO_PARAMS, *UPLOADED_IMAGE_PARAMS, *UPLOADED_VIDEO_PARAMS, - *links_params, + *LINK_PARAMS.call ].shuffle! end def items_total - @items_total ||= MEDIAS_PARAMS.size + @items_total ||= medias_params.size end def title_from_link(link) @@ -481,6 +479,16 @@ def saved_search(team) end end + def create_explainer(team) + Explainer.create!({ + title: Faker::Lorem.sentence, + url: random_url, + description: Faker::Lorem.paragraph(sentence_count: 8), + team: team, + user: users[:main_user_a], + }) + end + def feed_invitation(feed, invited_user) feed_invitation_params = { email: invited_user.email, @@ -703,6 +711,8 @@ def cluster(project_media, feed, team_id) populated_workspaces.publish_fact_checks puts 'Creating Clusters' populated_workspaces.clusters(feed_2) + puts 'Creating Explainers' + populated_workspaces.explainers rescue RuntimeError => e if e.message.include?('We could not parse this link') puts "—————" diff --git a/lib/check_search.rb b/lib/check_search.rb index 7bf9780c9d..c259fda4ba 100644 --- a/lib/check_search.rb +++ b/lib/check_search.rb @@ -259,7 +259,7 @@ def alegre_file_similar_items file_path = "check_search/#{hash}" end threshold = Bot::Alegre.get_threshold_for_query(@options['file_type'], ProjectMedia.new(team_id: Team.current.id))[0][:value] - results = Bot::Alegre.get_items_with_similar_media_v2(CheckS3.public_url(file_path), [{ value: threshold }], @options['team_id'].first, @options['file_type']) + results = Bot::Alegre.get_items_with_similar_media_v2(media_url: CheckS3.public_url(file_path), threshold: [{ value: threshold }], team_ids: @options['team_id'].first, type: @options['file_type']) results.blank? ? [0] : results.keys end @@ -361,7 +361,7 @@ def adjust_channel_filter def adjust_numeric_range_filter @options['range_numeric'] = {} - [:linked_items_count, :suggestions_count, :demand].each do |field| + [:linked_items_count, :suggestions_count, :demand, :positive_tipline_search_results_count, :negative_tipline_search_results_count].each do |field| if @options.has_key?(field) && !@options[field].blank? @options['range_numeric'][field] = @options[field] end diff --git a/lib/relay.idl b/lib/relay.idl index 6cf6a7c594..d87e82c146 100644 --- a/lib/relay.idl +++ b/lib/relay.idl @@ -457,6 +457,103 @@ type Annotator implements Node { user: User } +""" +ApiKey type +""" +type ApiKey implements Node { + access_token: String + application: String + created_at: String + dbid: Int + description: String + expire_at: String + id: ID! + permissions: String + team: PublicTeam + team_id: Int + title: String + updated_at: String + user: User + user_id: Int +} + +""" +The connection type for ApiKey. +""" +type ApiKeyConnection { + """ + A list of edges. + """ + edges: [ApiKeyEdge] + + """ + A list of nodes. + """ + nodes: [ApiKey] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int +} + +""" +An edge in a connection. +""" +type ApiKeyEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ApiKey +} + +""" +A union type of all article types we can handle +""" +union ArticleUnion = Explainer + +""" +The connection type for ArticleUnion. +""" +type ArticleUnionConnection { + """ + A list of edges. + """ + edges: [ArticleUnionEdge] + + """ + A list of nodes. + """ + nodes: [ArticleUnion] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int +} + +""" +An edge in a connection. +""" +type ArticleUnionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ArticleUnion +} + """ Bot User type """ @@ -708,7 +805,7 @@ type Cluster implements Node { Returns the last _n_ elements from the list. """ last: Int - teamId: Int! + teamId: Int ): ProjectMediaConnection requests_count: Int team_ids: [Int] @@ -1046,6 +1143,32 @@ type CreateAccountSourcePayload { source: Source } +""" +Autogenerated input type of CreateApiKey +""" +input CreateApiKeyInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + description: String + title: String +} + +""" +Autogenerated return type of CreateApiKey +""" +type CreateApiKeyPayload { + api_key: ApiKey + api_keyEdge: ApiKeyEdge + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + team: Team +} + """ Autogenerated input type of CreateClaimDescription """ @@ -2402,6 +2525,33 @@ type CreateDynamicPayload { versionEdge: VersionEdge } +""" +Autogenerated input type of CreateExplainer +""" +input CreateExplainerInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + description: String + language: String + title: String! + url: String +} + +""" +Autogenerated return type of CreateExplainer +""" +type CreateExplainerPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + explainer: Explainer + explainerEdge: ExplainerEdge + team: Team +} + """ Autogenerated input type of CreateFactCheck """ @@ -3038,6 +3188,29 @@ type DestroyAnnotationPayload { task: Task } +""" +Autogenerated input type of DestroyApiKey +""" +input DestroyApiKeyInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + id: ID +} + +""" +Autogenerated return type of DestroyApiKey +""" +type DestroyApiKeyPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + deletedId: ID + team: Team +} + """ Autogenerated input type of DestroyDynamicAnnotationAnalysis """ @@ -3940,6 +4113,29 @@ type DestroyDynamicPayload { version: Version } +""" +Autogenerated input type of DestroyExplainer +""" +input DestroyExplainerInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + id: ID +} + +""" +Autogenerated return type of DestroyExplainer +""" +type DestroyExplainerPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + deletedId: ID + team: Team +} + """ Autogenerated input type of DestroyFactCheck """ @@ -7998,6 +8194,61 @@ type Dynamic_annotation_verification_statusEdge { node: Dynamic_annotation_verification_status } +""" +Explainer type +""" +type Explainer implements Node { + created_at: String + dbid: Int + description: String + id: ID! + language: String + permissions: String + tags( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): TagConnection + team: PublicTeam + team_id: Int + title: String + updated_at: String + url: String + user: User + user_id: Int +} + +""" +An edge in a connection. +""" +type ExplainerEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Explainer +} + """ Autogenerated input type of ExtractText """ @@ -8502,6 +8753,33 @@ type GenerateTwoFactorBackupCodesPayload { success: Boolean } +""" +Autogenerated input type of ImportMedia +""" +input ImportMediaInput { + claimContext: String + claimTitle: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + feedId: Int! + parentId: Int + projectMediaId: Int! +} + +""" +Autogenerated return type of ImportMedia +""" +type ImportMediaPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + projectMedia: ProjectMedia! +} + scalar JsonStringType """ @@ -8796,6 +9074,12 @@ type MutationType { """ input: CreateAccountSourceInput! ): CreateAccountSourcePayload + createApiKey( + """ + Parameters for CreateApiKey + """ + input: CreateApiKeyInput! + ): CreateApiKeyPayload createClaimDescription( """ Parameters for CreateClaimDescription @@ -9024,6 +9308,12 @@ type MutationType { """ input: CreateDynamicAnnotationVerificationStatusInput! ): CreateDynamicAnnotationVerificationStatusPayload + createExplainer( + """ + Parameters for CreateExplainer + """ + input: CreateExplainerInput! + ): CreateExplainerPayload createFactCheck( """ Parameters for CreateFactCheck @@ -9150,6 +9440,12 @@ type MutationType { """ input: DestroyAnnotationInput! ): DestroyAnnotationPayload + destroyApiKey( + """ + Parameters for DestroyApiKey + """ + input: DestroyApiKeyInput! + ): DestroyApiKeyPayload destroyDynamic( """ Parameters for DestroyDynamic @@ -9366,6 +9662,12 @@ type MutationType { """ input: DestroyDynamicAnnotationVerificationStatusInput! ): DestroyDynamicAnnotationVerificationStatusPayload + destroyExplainer( + """ + Parameters for DestroyExplainer + """ + input: DestroyExplainerInput! + ): DestroyExplainerPayload destroyFactCheck( """ Parameters for DestroyFactCheck @@ -9474,6 +9776,12 @@ type MutationType { """ input: ExtractTextInput! ): ExtractTextPayload + feedImportMedia( + """ + Parameters for ImportMedia + """ + input: ImportMediaInput! + ): ImportMediaPayload generateTwoFactorBackupCodes( """ Parameters for GenerateTwoFactorBackupCodes @@ -9798,6 +10106,12 @@ type MutationType { """ input: UpdateDynamicAnnotationVerificationStatusInput! ): UpdateDynamicAnnotationVerificationStatusPayload + updateExplainer( + """ + Parameters for UpdateExplainer + """ + input: UpdateExplainerInput! + ): UpdateExplainerPayload updateFactCheck( """ Parameters for UpdateFactCheck @@ -11075,6 +11389,9 @@ type ProjectMedia implements Node { ): FlagConnection full_url: String id: ID! + imported_from_feed: Feed + imported_from_feed_id: Int + imported_from_project_media_id: Int is_confirmed: Boolean is_confirmed_similar_to_another_item: Boolean is_main: Boolean @@ -11145,6 +11462,7 @@ type ProjectMedia implements Node { Returns the first _n_ elements from the list. """ first: Int + includeChildren: Boolean """ Returns the last _n_ elements from the list. @@ -12563,7 +12881,51 @@ type TaskEdge { Team type """ type Team implements Node { + api_key(dbid: Int!): ApiKey + api_keys( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ApiKeyConnection archived: Int + articles( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + article_type: String! + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ArticleUnionConnection available_newsletter_header_types: JsonStringType avatar: String check_search_spam: CheckSearch @@ -12620,6 +12982,7 @@ type Team implements Node { ): FeedConnection get_data_report_url: String get_embed_whitelist: String + get_explainers_enabled: Boolean get_fieldsets: JsonStringType get_language: String get_language_detection: Boolean @@ -14968,6 +15331,34 @@ type UpdateDynamicPayload { versionEdge: VersionEdge } +""" +Autogenerated input type of UpdateExplainer +""" +input UpdateExplainerInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + description: String + id: ID + language: String + title: String! + url: String +} + +""" +Autogenerated return type of UpdateExplainer +""" +type UpdateExplainerPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + explainer: Explainer + explainerEdge: ExplainerEdge + team: Team +} + """ Autogenerated input type of UpdateFactCheck """ diff --git a/lib/sample_data.rb b/lib/sample_data.rb index a3358b2dd5..b1dc6d3b37 100644 --- a/lib/sample_data.rb +++ b/lib/sample_data.rb @@ -36,6 +36,8 @@ def create_api_key(options = {}) options.each do |key, value| a.send("#{key}=", value) if a.respond_to?("#{key}=") end + a.title = options[:title] || random_string + a.description = options[:description] || random_string a.save! a.reload end @@ -900,6 +902,16 @@ def create_fact_check(options = {}) }.merge(options)) end + def create_explainer(options = {}) + Explainer.create!({ + title: random_string, + url: random_url, + description: random_string, + user: options[:user] || create_user, + team: options[:team] || create_team, + }.merge(options)) + end + def create_feed(options = {}) Feed.create!({ name: random_string, diff --git a/lib/tasks/data/statistics.rake b/lib/tasks/data/statistics.rake index 3084cf8e5d..13ff2a0ada 100644 --- a/lib/tasks/data/statistics.rake +++ b/lib/tasks/data/statistics.rake @@ -16,75 +16,80 @@ namespace :check do ActiveRecord::Base.logger = nil errors = [] - team_ids = Team.joins(:project_medias).where(project_medias: { user: BotUser.smooch_user }).distinct.pluck(:id) + team_ids = Team.joins(:project_medias).where(project_medias: { user: BotUser.pluck(:id) }).distinct.pluck(:id) current_time = DateTime.now puts "[#{Time.now}] Detected #{team_ids.length} teams with tipline data" team_ids.each_with_index do |team_id, index| - tipline_bot = TeamBotInstallation.where(team_id: team_id, user: BotUser.smooch_user).last - if tipline_bot.nil? - puts "[#{Time.now}] No tipline bot installed for team #{team_id}; skipping team" - next - end + TeamBotInstallation.where(team_id: team_id, user: BotUser.pluck(:id)).find_each do |bot| + tipline_message_statistics = Check::TiplineMessageStatistics.new(team_id) + date = ProjectMedia.where(team_id: team_id, user: bot.user).order('created_at ASC').first&.created_at&.beginning_of_day + + next if date.nil? + + team = Team.find(team_id) + languages = team.get_languages.to_a + if bot.user == BotUser.smooch_user + platforms = bot.smooch_enabled_integrations.keys + else + platforms = Bot::Smooch::SUPPORTED_INTEGRATION_NAMES.keys + end - tipline_message_statistics = Check::TiplineMessageStatistics.new(team_id) - date = ProjectMedia.where(team_id: team_id, user: BotUser.smooch_user).order('created_at ASC').first&.created_at&.beginning_of_day - team = Team.find(team_id) - languages = team.get_languages.to_a - platforms = tipline_bot.smooch_enabled_integrations.keys - - team_stats = Hash.new(0) - puts "[#{Time.now}] Generating month tipline statistics for team with ID #{team_id}. (#{index + 1} / #{team_ids.length})" - begin - month_start = date.beginning_of_month - month_end = date.end_of_month - - platforms.each do |platform| - languages.each do |language| - if MonthlyTeamStatistic.where(team_id: team_id, platform: platform, language: language, start_date: month_start, end_date: month_end).any? - team_stats[:skipped] += 1 - next - end + team_stats = Hash.new(0) + puts "[#{Time.now}] Generating month tipline statistics for team with ID #{team_id}. (#{index + 1} / #{team_ids.length})" + begin + month_start = date.beginning_of_month + month_end = date.end_of_month + + platforms.each do |platform| + languages.each do |language| + if MonthlyTeamStatistic.where(team_id: team_id, platform: platform, language: language, start_date: month_start, end_date: month_end).any? + team_stats[:skipped] += 1 + next + end - period_end = current_time < month_end ? current_time : month_end - tracing_attributes = { "app.team.id" => team_id, "app.attr.platform" => platform, "app.attr.language" => language} - - row_attributes = {} - begin - row_attributes = CheckStatistics.get_statistics(month_start.to_date, period_end, team_id, platform, language) - - # Start date for new conversation calculation, with optional override for testing - if args.ignore_convo_cutoff || month_start >= DateTime.new(2023,4,1) - CheckTracer.in_span("Check::TiplineMessageStatistics.monthly_conversations", attributes: tracing_attributes) do - row_attributes[:conversations_24hr] = tipline_message_statistics.monthly_conversations( - Bot::Smooch::SUPPORTED_INTEGRATION_NAMES[platform], - language, - month_start, - period_end - ) + period_end = current_time < month_end ? current_time : month_end + tracing_attributes = { "app.team.id" => team_id, "app.attr.platform" => platform, "app.attr.language" => language} + + row_attributes = {} + begin + row_attributes = CheckStatistics.get_statistics(month_start.to_date, period_end, team_id, platform, language) + + # Start date for new conversation calculation, with optional override for testing + if args.ignore_convo_cutoff || month_start >= DateTime.new(2023,4,1) + CheckTracer.in_span("Check::TiplineMessageStatistics.monthly_conversations", attributes: tracing_attributes) do + row_attributes[:conversations_24hr] = tipline_message_statistics.monthly_conversations( + Bot::Smooch::SUPPORTED_INTEGRATION_NAMES[platform], + language, + month_start, + period_end + ) + end + end + + partial_month = MonthlyTeamStatistic.find_by(team_id: team_id, platform: platform, language: language, start_date: month_start) + if partial_month.present? + partial_month.update!(row_attributes.merge!(team_id: team_id)) + team_stats[:updated] += 1 + else + MonthlyTeamStatistic.create!(row_attributes.merge!(team_id: team_id)) + team_stats[:created] += 1 end - end - partial_month = MonthlyTeamStatistic.find_by(team_id: team_id, platform: platform, language: language, start_date: month_start) - if partial_month.present? - partial_month.update!(row_attributes.merge!(team_id: team_id)) - team_stats[:updated] += 1 - else - MonthlyTeamStatistic.create!(row_attributes.merge!(team_id: team_id)) - team_stats[:created] += 1 + rescue StandardError => e + error = Check::Statistics::CalculationError.new(e) + errors.push(error) + puts "Error #{error}" + CheckSentry.notify(error, team_id: team_id, platform: platform, language: language, start_date: month_start, end_date: period_end) + team_stats[:errored] += 1 end - rescue StandardError => e - error = Check::Statistics::CalculationError.new(e) - errors.push(error) - CheckSentry.notify(error, team_id: team_id, platform: platform, language: language, start_date: month_start, end_date: period_end) - team_stats[:errored] += 1 end end - end - date += 1.month - # Protect against generating stats for the future, by bailing if start of the start of first day of month is later than time rake task was run - end while date.beginning_of_month <= current_time + date += 1.month + # Protect against generating stats for the future, by bailing if start of the start of first day of month is later than time rake task was run + end while date.beginning_of_month <= current_time - puts "[#{Time.now}] Stats summary for team with ID #{team_id}: #{team_stats.map{|k,v| "#{k} - #{v}" }.join("; ") }. Platforms: #{platforms.join(',')}. Languages: #{languages.join(', ')}" + puts "[#{Time.now}] Stats summary for team with ID #{team_id} : #{team_stats.map{|k,v| "#{k} - #{v}" }.join("; ") }. Platforms: #{platforms.join(',')}. Languages: #{languages.join(', ')}" + end end ActiveRecord::Base.logger = old_logger diff --git a/public/relay.json b/public/relay.json index 9a1ad9de06..a93b33b0f4 100644 --- a/public/relay.json +++ b/public/relay.json @@ -2038,6 +2038,491 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "ApiKey", + "description": "ApiKey type", + "fields": [ + { + "name": "access_token", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "application", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "created_at", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dbid", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expire_at", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "permissions", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "team", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "PublicTeam", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "team_id", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updated_at", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user_id", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ApiKeyConnection", + "description": "The connection type for ApiKey.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ApiKeyEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ApiKey", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ApiKeyEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "ApiKey", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "ArticleUnion", + "description": "A union type of all article types we can handle", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Explainer", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "ArticleUnionConnection", + "description": "The connection type for ArticleUnion.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ArticleUnionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "UNION", + "name": "ArticleUnion", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ArticleUnionEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "UNION", + "name": "ArticleUnion", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "SCALAR", "name": "Boolean", @@ -3618,13 +4103,9 @@ "name": "teamId", "description": null, "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } + "kind": "SCALAR", + "name": "Int", + "ofType": null }, "defaultValue": null, "isDeprecated": false, @@ -5249,6 +5730,122 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "CreateApiKeyInput", + "description": "Autogenerated input type of CreateApiKey", + "fields": null, + "inputFields": [ + { + "name": "title", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateApiKeyPayload", + "description": "Autogenerated return type of CreateApiKey", + "fields": [ + { + "name": "api_key", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "ApiKey", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "api_keyEdge", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "ApiKeyEdge", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "team", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Team", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "CreateClaimDescriptionInput", @@ -14599,6 +15196,150 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "CreateExplainerInput", + "description": "Autogenerated input type of CreateExplainer", + "fields": null, + "inputFields": [ + { + "name": "title", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "language", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateExplainerPayload", + "description": "Autogenerated return type of CreateExplainer", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "explainer", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Explainer", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "explainerEdge", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "ExplainerEdge", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "team", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Team", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "CreateFactCheckInput", @@ -18368,8 +19109,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "DestroyDynamicAnnotationAnalysisInput", - "description": "Autogenerated input type of DestroyDynamicAnnotationAnalysis", + "name": "DestroyApiKeyInput", + "description": "Autogenerated input type of DestroyApiKey", "fields": null, "inputFields": [ { @@ -18403,8 +19144,98 @@ }, { "kind": "OBJECT", - "name": "DestroyDynamicAnnotationAnalysisPayload", - "description": "Autogenerated return type of DestroyDynamicAnnotationAnalysis", + "name": "DestroyApiKeyPayload", + "description": "Autogenerated return type of DestroyApiKey", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deletedId", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "team", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Team", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DestroyDynamicAnnotationAnalysisInput", + "description": "Autogenerated input type of DestroyDynamicAnnotationAnalysis", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DestroyDynamicAnnotationAnalysisPayload", + "description": "Autogenerated return type of DestroyDynamicAnnotationAnalysis", "fields": [ { "name": "clientMutationId", @@ -22644,98 +23475,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "DestroyFactCheckInput", - "description": "Autogenerated input type of DestroyFactCheck", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DestroyFactCheckPayload", - "description": "Autogenerated return type of DestroyFactCheck", - "fields": [ - { - "name": "claim_description", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "ClaimDescription", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deletedId", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "DestroyFeedInput", - "description": "Autogenerated input type of DestroyFeed", + "name": "DestroyExplainerInput", + "description": "Autogenerated input type of DestroyExplainer", "fields": null, "inputFields": [ { @@ -22767,100 +23508,10 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "INPUT_OBJECT", - "name": "DestroyFeedInvitationInput", - "description": "Autogenerated input type of DestroyFeedInvitation", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DestroyFeedInvitationPayload", - "description": "Autogenerated return type of DestroyFeedInvitation", - "fields": [ - { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deletedId", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "feed", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "Feed", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - - ], - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", - "name": "DestroyFeedPayload", - "description": "Autogenerated return type of DestroyFeed", + "name": "DestroyExplainerPayload", + "description": "Autogenerated return type of DestroyExplainer", "fields": [ { "name": "clientMutationId", @@ -22914,8 +23565,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "DestroyFeedTeamInput", - "description": "Autogenerated input type of DestroyFeedTeam", + "name": "DestroyFactCheckInput", + "description": "Autogenerated input type of DestroyFactCheck", "fields": null, "inputFields": [ { @@ -22949,46 +23600,46 @@ }, { "kind": "OBJECT", - "name": "DestroyFeedTeamPayload", - "description": "Autogenerated return type of DestroyFeedTeam", + "name": "DestroyFactCheckPayload", + "description": "Autogenerated return type of DestroyFactCheck", "fields": [ { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", + "name": "claim_description", + "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "ClaimDescription", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "deletedId", - "description": null, + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", "args": [ ], "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "feed", + "name": "deletedId", "description": null, "args": [ ], "type": { - "kind": "OBJECT", - "name": "Feed", + "kind": "SCALAR", + "name": "ID", "ofType": null }, "isDeprecated": false, @@ -23004,8 +23655,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "DestroyProjectInput", - "description": "Autogenerated input type of DestroyProject", + "name": "DestroyFeedInput", + "description": "Autogenerated input type of DestroyFeed", "fields": null, "inputFields": [ { @@ -23021,11 +23672,34 @@ "deprecationReason": null }, { - "name": "items_destination_project_id", + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DestroyFeedInvitationInput", + "description": "Autogenerated input type of DestroyFeedInvitation", + "fields": null, + "inputFields": [ + { + "name": "id", "description": null, "type": { "kind": "SCALAR", - "name": "Int", + "name": "ID", "ofType": null }, "defaultValue": null, @@ -23051,23 +23725,9 @@ }, { "kind": "OBJECT", - "name": "DestroyProjectPayload", - "description": "Autogenerated return type of DestroyProject", + "name": "DestroyFeedInvitationPayload", + "description": "Autogenerated return type of DestroyFeedInvitation", "fields": [ - { - "name": "check_search_team", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "CheckSearch", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "clientMutationId", "description": "A unique identifier for the client performing the mutation.", @@ -23097,42 +23757,55 @@ "deprecationReason": null }, { - "name": "previous_default_project", + "name": "feed", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Project", + "name": "Feed", "ofType": null }, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DestroyFeedPayload", + "description": "Autogenerated return type of DestroyFeed", + "fields": [ { - "name": "project_group", - "description": null, + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", "args": [ ], "type": { - "kind": "OBJECT", - "name": "ProjectGroup", + "kind": "SCALAR", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "project_group_was", + "name": "deletedId", "description": null, "args": [ ], "type": { - "kind": "OBJECT", - "name": "ProjectGroup", + "kind": "SCALAR", + "name": "ID", "ofType": null }, "isDeprecated": false, @@ -23162,8 +23835,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "DestroyRelationshipInput", - "description": "Autogenerated input type of DestroyRelationship", + "name": "DestroyFeedTeamInput", + "description": "Autogenerated input type of DestroyFeedTeam", "fields": null, "inputFields": [ { @@ -23178,18 +23851,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "archive_target", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "clientMutationId", "description": "A unique identifier for the client performing the mutation.", @@ -23209,8 +23870,8 @@ }, { "kind": "OBJECT", - "name": "DestroyRelationshipPayload", - "description": "Autogenerated return type of DestroyRelationship", + "name": "DestroyFeedTeamPayload", + "description": "Autogenerated return type of DestroyFeedTeam", "fields": [ { "name": "clientMutationId", @@ -23241,28 +23902,14 @@ "deprecationReason": null }, { - "name": "source_project_media", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "ProjectMedia", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "target_project_media", + "name": "feed", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "ProjectMedia", + "name": "Feed", "ofType": null }, "isDeprecated": false, @@ -23278,41 +23925,29 @@ }, { "kind": "INPUT_OBJECT", - "name": "DestroyRelationshipsInput", - "description": "Autogenerated input type of DestroyRelationships", + "name": "DestroyProjectInput", + "description": "Autogenerated input type of DestroyProject", "fields": null, "inputFields": [ { - "name": "ids", + "name": "id", "description": null, "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - } + "kind": "SCALAR", + "name": "ID", + "ofType": null }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "source_id", + "name": "items_destination_project_id", "description": null, "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } + "kind": "SCALAR", + "name": "Int", + "ofType": null }, "defaultValue": null, "isDeprecated": false, @@ -23337,126 +23972,88 @@ }, { "kind": "OBJECT", - "name": "DestroyRelationshipsPayload", - "description": "Autogenerated return type of DestroyRelationships", + "name": "DestroyProjectPayload", + "description": "Autogenerated return type of DestroyProject", "fields": [ { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", + "name": "check_search_team", + "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "CheckSearch", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "ids", - "description": null, + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", "args": [ ], "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "source_project_media", + "name": "deletedId", "description": null, "args": [ ], - "type": { - "kind": "OBJECT", - "name": "ProjectMedia", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "DestroySavedSearchInput", - "description": "Autogenerated input type of DestroySavedSearch", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, "type": { "kind": "SCALAR", "name": "ID", "ofType": null }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", + "name": "previous_default_project", + "description": null, + "args": [ + + ], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "Project", "ofType": null }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DestroySavedSearchPayload", - "description": "Autogenerated return type of DestroySavedSearch", - "fields": [ + }, { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", + "name": "project_group", + "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "ProjectGroup", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "deletedId", + "name": "project_group_was", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "ProjectGroup", "ofType": null }, "isDeprecated": false, @@ -23486,8 +24083,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "DestroyTagInput", - "description": "Autogenerated input type of DestroyTag", + "name": "DestroyRelationshipInput", + "description": "Autogenerated input type of DestroyRelationship", "fields": null, "inputFields": [ { @@ -23502,6 +24099,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "archive_target", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "clientMutationId", "description": "A unique identifier for the client performing the mutation.", @@ -23521,8 +24130,8 @@ }, { "kind": "OBJECT", - "name": "DestroyTagPayload", - "description": "Autogenerated return type of DestroyTag", + "name": "DestroyRelationshipPayload", + "description": "Autogenerated return type of DestroyRelationship", "fields": [ { "name": "clientMutationId", @@ -23553,7 +24162,7 @@ "deprecationReason": null }, { - "name": "project_media", + "name": "source_project_media", "description": null, "args": [ @@ -23567,42 +24176,14 @@ "deprecationReason": null }, { - "name": "source", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "Source", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "tag_text_object", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "TagText", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "team", + "name": "target_project_media", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Team", + "name": "ProjectMedia", "ofType": null }, "isDeprecated": false, @@ -23618,17 +24199,41 @@ }, { "kind": "INPUT_OBJECT", - "name": "DestroyTagTextInput", - "description": "Autogenerated input type of DestroyTagText", + "name": "DestroyRelationshipsInput", + "description": "Autogenerated input type of DestroyRelationships", "fields": null, "inputFields": [ { - "name": "id", + "name": "ids", "description": null, "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source_id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } }, "defaultValue": null, "isDeprecated": false, @@ -23653,8 +24258,8 @@ }, { "kind": "OBJECT", - "name": "DestroyTagTextPayload", - "description": "Autogenerated return type of DestroyTagText", + "name": "DestroyRelationshipsPayload", + "description": "Autogenerated return type of DestroyRelationships", "fields": [ { "name": "clientMutationId", @@ -23671,28 +24276,32 @@ "deprecationReason": null }, { - "name": "deletedId", + "name": "ids", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null }, { - "name": "team", + "name": "source_project_media", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Team", + "name": "ProjectMedia", "ofType": null }, "isDeprecated": false, @@ -23708,8 +24317,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "DestroyTeamBotInstallationInput", - "description": "Autogenerated input type of DestroyTeamBotInstallation", + "name": "DestroySavedSearchInput", + "description": "Autogenerated input type of DestroySavedSearch", "fields": null, "inputFields": [ { @@ -23743,23 +24352,9 @@ }, { "kind": "OBJECT", - "name": "DestroyTeamBotInstallationPayload", - "description": "Autogenerated return type of DestroyTeamBotInstallation", + "name": "DestroySavedSearchPayload", + "description": "Autogenerated return type of DestroySavedSearch", "fields": [ - { - "name": "bot_user", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "BotUser", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "clientMutationId", "description": "A unique identifier for the client performing the mutation.", @@ -23812,8 +24407,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "DestroyTeamInput", - "description": "Autogenerated input type of DestroyTeam", + "name": "DestroyTagInput", + "description": "Autogenerated input type of DestroyTag", "fields": null, "inputFields": [ { @@ -23847,102 +24442,88 @@ }, { "kind": "OBJECT", - "name": "DestroyTeamPayload", - "description": "Autogenerated return type of DestroyTeam", + "name": "DestroyTagPayload", + "description": "Autogenerated return type of DestroyTag", "fields": [ { - "name": "check_search_spam", - "description": null, + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", "args": [ ], "type": { - "kind": "OBJECT", - "name": "CheckSearch", + "kind": "SCALAR", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "check_search_team", + "name": "deletedId", "description": null, "args": [ ], "type": { - "kind": "OBJECT", - "name": "CheckSearch", + "kind": "SCALAR", + "name": "ID", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "check_search_trash", + "name": "project_media", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "CheckSearch", + "name": "ProjectMedia", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "check_search_unconfirmed", + "name": "source", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "CheckSearch", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", + "name": "Source", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "deletedId", + "name": "tag_text_object", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "TagText", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "public_team", + "name": "team", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "PublicTeam", + "name": "Team", "ofType": null }, "isDeprecated": false, @@ -23958,8 +24539,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "DestroyTeamTaskInput", - "description": "Autogenerated input type of DestroyTeamTask", + "name": "DestroyTagTextInput", + "description": "Autogenerated input type of DestroyTagText", "fields": null, "inputFields": [ { @@ -23974,18 +24555,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "keep_completed_tasks", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "clientMutationId", "description": "A unique identifier for the client performing the mutation.", @@ -24005,8 +24574,8 @@ }, { "kind": "OBJECT", - "name": "DestroyTeamTaskPayload", - "description": "Autogenerated return type of DestroyTeamTask", + "name": "DestroyTagTextPayload", + "description": "Autogenerated return type of DestroyTagText", "fields": [ { "name": "clientMutationId", @@ -24060,8 +24629,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "DestroyTeamUserInput", - "description": "Autogenerated input type of DestroyTeamUser", + "name": "DestroyTeamBotInstallationInput", + "description": "Autogenerated input type of DestroyTeamBotInstallation", "fields": null, "inputFields": [ { @@ -24095,60 +24664,60 @@ }, { "kind": "OBJECT", - "name": "DestroyTeamUserPayload", - "description": "Autogenerated return type of DestroyTeamUser", + "name": "DestroyTeamBotInstallationPayload", + "description": "Autogenerated return type of DestroyTeamBotInstallation", "fields": [ { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", + "name": "bot_user", + "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "BotUser", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "deletedId", - "description": null, + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", "args": [ ], "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "team", + "name": "deletedId", "description": null, "args": [ ], "type": { - "kind": "OBJECT", - "name": "Team", + "kind": "SCALAR", + "name": "ID", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "user", + "name": "team", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "User", + "name": "Team", "ofType": null }, "isDeprecated": false, @@ -24164,8 +24733,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "DestroyTiplineResourceInput", - "description": "Autogenerated input type of DestroyTiplineResource", + "name": "DestroyTeamInput", + "description": "Autogenerated input type of DestroyTeam", "fields": null, "inputFields": [ { @@ -24199,8 +24768,360 @@ }, { "kind": "OBJECT", - "name": "DestroyTiplineResourcePayload", - "description": "Autogenerated return type of DestroyTiplineResource", + "name": "DestroyTeamPayload", + "description": "Autogenerated return type of DestroyTeam", + "fields": [ + { + "name": "check_search_spam", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "CheckSearch", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "check_search_team", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "CheckSearch", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "check_search_trash", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "CheckSearch", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "check_search_unconfirmed", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "CheckSearch", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deletedId", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "public_team", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "PublicTeam", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DestroyTeamTaskInput", + "description": "Autogenerated input type of DestroyTeamTask", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "keep_completed_tasks", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DestroyTeamTaskPayload", + "description": "Autogenerated return type of DestroyTeamTask", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deletedId", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "team", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Team", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DestroyTeamUserInput", + "description": "Autogenerated input type of DestroyTeamUser", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DestroyTeamUserPayload", + "description": "Autogenerated return type of DestroyTeamUser", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deletedId", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "team", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Team", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DestroyTiplineResourceInput", + "description": "Autogenerated input type of DestroyTiplineResource", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DestroyTiplineResourcePayload", + "description": "Autogenerated return type of DestroyTiplineResource", "fields": [ { "name": "clientMutationId", @@ -25040,63 +25961,647 @@ "deprecationReason": null }, { - "name": "permissions", + "name": "permissions", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "smooch_report_received_at", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "smooch_report_update_received_at", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "smooch_user_external_identifier", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "smooch_user_request_language", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "smooch_user_slack_channel_url", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updated_at", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "value_json", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DynamicConnection", + "description": "The connection type for Dynamic.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DynamicEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Dynamic", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DynamicEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Dynamic", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Dynamic_annotation_analysis", + "description": null, + "fields": [ + { + "name": "annotated_id", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "annotated_type", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "annotation_type", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "annotations", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "annotation_type", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AnnotationUnionConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "annotator", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Annotator", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignments", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UserConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "content", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "created_at", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "data", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dbid", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "file_data", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lock_version", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Int", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "smooch_report_received_at", + "name": "locked", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "smooch_report_update_received_at", + "name": "medias", "description": null, "args": [ - + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } ], "type": { - "kind": "SCALAR", - "name": "Int", + "kind": "OBJECT", + "name": "ProjectMediaConnection", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "smooch_user_external_identifier", + "name": "parsed_fragment", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "String", + "name": "JsonStringType", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "smooch_user_request_language", + "name": "permissions", "description": null, "args": [ @@ -25110,127 +26615,56 @@ "deprecationReason": null }, { - "name": "smooch_user_slack_channel_url", + "name": "project", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "Project", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "updated_at", + "name": "team", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "Team", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "value_json", + "name": "updated_at", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "JsonStringType", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DynamicConnection", - "description": "The connection type for Dynamic.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [ - - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "DynamicEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null }, { - "name": "nodes", - "description": "A list of nodes.", - "args": [ - - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Dynamic", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [ - - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "totalCount", + "name": "version", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "Int", + "kind": "OBJECT", + "name": "Version", "ofType": null }, "isDeprecated": false, @@ -25239,14 +26673,18 @@ ], "inputFields": null, "interfaces": [ - + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } ], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", - "name": "DynamicEdge", + "name": "Dynamic_annotation_analysisEdge", "description": "An edge in a connection.", "fields": [ { @@ -25275,7 +26713,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic", + "name": "Dynamic_annotation_analysis", "ofType": null }, "isDeprecated": false, @@ -25291,7 +26729,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_analysis", + "name": "Dynamic_annotation_archiver", "description": null, "fields": [ { @@ -25763,7 +27201,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_analysisEdge", + "name": "Dynamic_annotation_archiverEdge", "description": "An edge in a connection.", "fields": [ { @@ -25792,7 +27230,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_analysis", + "name": "Dynamic_annotation_archiver", "ofType": null }, "isDeprecated": false, @@ -25808,7 +27246,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_archiver", + "name": "Dynamic_annotation_clip", "description": null, "fields": [ { @@ -26280,7 +27718,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_archiverEdge", + "name": "Dynamic_annotation_clipEdge", "description": "An edge in a connection.", "fields": [ { @@ -26309,7 +27747,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_archiver", + "name": "Dynamic_annotation_clip", "ofType": null }, "isDeprecated": false, @@ -26325,7 +27763,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_clip", + "name": "Dynamic_annotation_embed_code", "description": null, "fields": [ { @@ -26797,7 +28235,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_clipEdge", + "name": "Dynamic_annotation_embed_codeEdge", "description": "An edge in a connection.", "fields": [ { @@ -26826,7 +28264,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_clip", + "name": "Dynamic_annotation_embed_code", "ofType": null }, "isDeprecated": false, @@ -26842,7 +28280,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_embed_code", + "name": "Dynamic_annotation_extracted_text", "description": null, "fields": [ { @@ -27314,7 +28752,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_embed_codeEdge", + "name": "Dynamic_annotation_extracted_textEdge", "description": "An edge in a connection.", "fields": [ { @@ -27343,7 +28781,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_embed_code", + "name": "Dynamic_annotation_extracted_text", "ofType": null }, "isDeprecated": false, @@ -27359,7 +28797,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_extracted_text", + "name": "Dynamic_annotation_flag", "description": null, "fields": [ { @@ -27831,7 +29269,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_extracted_textEdge", + "name": "Dynamic_annotation_flagEdge", "description": "An edge in a connection.", "fields": [ { @@ -27860,7 +29298,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_extracted_text", + "name": "Dynamic_annotation_flag", "ofType": null }, "isDeprecated": false, @@ -27876,7 +29314,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_flag", + "name": "Dynamic_annotation_geolocation", "description": null, "fields": [ { @@ -28348,7 +29786,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_flagEdge", + "name": "Dynamic_annotation_geolocationEdge", "description": "An edge in a connection.", "fields": [ { @@ -28377,7 +29815,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_flag", + "name": "Dynamic_annotation_geolocation", "ofType": null }, "isDeprecated": false, @@ -28393,7 +29831,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_geolocation", + "name": "Dynamic_annotation_language", "description": null, "fields": [ { @@ -28865,7 +30303,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_geolocationEdge", + "name": "Dynamic_annotation_languageEdge", "description": "An edge in a connection.", "fields": [ { @@ -28894,7 +30332,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_geolocation", + "name": "Dynamic_annotation_language", "ofType": null }, "isDeprecated": false, @@ -28910,7 +30348,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_language", + "name": "Dynamic_annotation_memebuster", "description": null, "fields": [ { @@ -29382,7 +30820,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_languageEdge", + "name": "Dynamic_annotation_memebusterEdge", "description": "An edge in a connection.", "fields": [ { @@ -29411,7 +30849,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_language", + "name": "Dynamic_annotation_memebuster", "ofType": null }, "isDeprecated": false, @@ -29427,7 +30865,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_memebuster", + "name": "Dynamic_annotation_metadata", "description": null, "fields": [ { @@ -29899,7 +31337,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_memebusterEdge", + "name": "Dynamic_annotation_metadataEdge", "description": "An edge in a connection.", "fields": [ { @@ -29928,7 +31366,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_memebuster", + "name": "Dynamic_annotation_metadata", "ofType": null }, "isDeprecated": false, @@ -29944,7 +31382,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_metadata", + "name": "Dynamic_annotation_metrics", "description": null, "fields": [ { @@ -30416,7 +31854,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_metadataEdge", + "name": "Dynamic_annotation_metricsEdge", "description": "An edge in a connection.", "fields": [ { @@ -30445,7 +31883,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_metadata", + "name": "Dynamic_annotation_metrics", "ofType": null }, "isDeprecated": false, @@ -30461,7 +31899,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_metrics", + "name": "Dynamic_annotation_mt", "description": null, "fields": [ { @@ -30933,7 +32371,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_metricsEdge", + "name": "Dynamic_annotation_mtEdge", "description": "An edge in a connection.", "fields": [ { @@ -30962,7 +32400,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_metrics", + "name": "Dynamic_annotation_mt", "ofType": null }, "isDeprecated": false, @@ -30978,7 +32416,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_mt", + "name": "Dynamic_annotation_report_design", "description": null, "fields": [ { @@ -31450,7 +32888,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_mtEdge", + "name": "Dynamic_annotation_report_designEdge", "description": "An edge in a connection.", "fields": [ { @@ -31479,7 +32917,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_mt", + "name": "Dynamic_annotation_report_design", "ofType": null }, "isDeprecated": false, @@ -31495,7 +32933,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_report_design", + "name": "Dynamic_annotation_reverse_image", "description": null, "fields": [ { @@ -31967,7 +33405,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_report_designEdge", + "name": "Dynamic_annotation_reverse_imageEdge", "description": "An edge in a connection.", "fields": [ { @@ -31996,7 +33434,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_report_design", + "name": "Dynamic_annotation_reverse_image", "ofType": null }, "isDeprecated": false, @@ -32012,7 +33450,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_reverse_image", + "name": "Dynamic_annotation_slack_message", "description": null, "fields": [ { @@ -32484,7 +33922,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_reverse_imageEdge", + "name": "Dynamic_annotation_slack_messageEdge", "description": "An edge in a connection.", "fields": [ { @@ -32513,7 +33951,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_reverse_image", + "name": "Dynamic_annotation_slack_message", "ofType": null }, "isDeprecated": false, @@ -32529,7 +33967,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_slack_message", + "name": "Dynamic_annotation_smooch_response", "description": null, "fields": [ { @@ -33001,7 +34439,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_slack_messageEdge", + "name": "Dynamic_annotation_smooch_responseEdge", "description": "An edge in a connection.", "fields": [ { @@ -33030,7 +34468,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_slack_message", + "name": "Dynamic_annotation_smooch_response", "ofType": null }, "isDeprecated": false, @@ -33046,7 +34484,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_smooch_response", + "name": "Dynamic_annotation_smooch_user", "description": null, "fields": [ { @@ -33518,7 +34956,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_smooch_responseEdge", + "name": "Dynamic_annotation_smooch_userEdge", "description": "An edge in a connection.", "fields": [ { @@ -33547,7 +34985,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_smooch_response", + "name": "Dynamic_annotation_smooch_user", "ofType": null }, "isDeprecated": false, @@ -33563,7 +35001,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_smooch_user", + "name": "Dynamic_annotation_syrian_archive_data", "description": null, "fields": [ { @@ -34035,7 +35473,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_smooch_userEdge", + "name": "Dynamic_annotation_syrian_archive_dataEdge", "description": "An edge in a connection.", "fields": [ { @@ -34064,7 +35502,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_smooch_user", + "name": "Dynamic_annotation_syrian_archive_data", "ofType": null }, "isDeprecated": false, @@ -34080,7 +35518,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_syrian_archive_data", + "name": "Dynamic_annotation_task_response_datetime", "description": null, "fields": [ { @@ -34552,7 +35990,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_syrian_archive_dataEdge", + "name": "Dynamic_annotation_task_response_datetimeEdge", "description": "An edge in a connection.", "fields": [ { @@ -34581,7 +36019,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_syrian_archive_data", + "name": "Dynamic_annotation_task_response_datetime", "ofType": null }, "isDeprecated": false, @@ -34597,7 +36035,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_datetime", + "name": "Dynamic_annotation_task_response_file_upload", "description": null, "fields": [ { @@ -35069,7 +36507,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_datetimeEdge", + "name": "Dynamic_annotation_task_response_file_uploadEdge", "description": "An edge in a connection.", "fields": [ { @@ -35098,7 +36536,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_datetime", + "name": "Dynamic_annotation_task_response_file_upload", "ofType": null }, "isDeprecated": false, @@ -35114,7 +36552,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_file_upload", + "name": "Dynamic_annotation_task_response_free_text", "description": null, "fields": [ { @@ -35586,7 +37024,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_file_uploadEdge", + "name": "Dynamic_annotation_task_response_free_textEdge", "description": "An edge in a connection.", "fields": [ { @@ -35615,7 +37053,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_file_upload", + "name": "Dynamic_annotation_task_response_free_text", "ofType": null }, "isDeprecated": false, @@ -35631,7 +37069,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_free_text", + "name": "Dynamic_annotation_task_response_geolocation", "description": null, "fields": [ { @@ -36103,7 +37541,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_free_textEdge", + "name": "Dynamic_annotation_task_response_geolocationEdge", "description": "An edge in a connection.", "fields": [ { @@ -36132,7 +37570,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_free_text", + "name": "Dynamic_annotation_task_response_geolocation", "ofType": null }, "isDeprecated": false, @@ -36148,7 +37586,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_geolocation", + "name": "Dynamic_annotation_task_response_multiple_choice", "description": null, "fields": [ { @@ -36620,7 +38058,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_geolocationEdge", + "name": "Dynamic_annotation_task_response_multiple_choiceEdge", "description": "An edge in a connection.", "fields": [ { @@ -36649,7 +38087,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_geolocation", + "name": "Dynamic_annotation_task_response_multiple_choice", "ofType": null }, "isDeprecated": false, @@ -36665,7 +38103,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_multiple_choice", + "name": "Dynamic_annotation_task_response_number", "description": null, "fields": [ { @@ -37137,7 +38575,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_multiple_choiceEdge", + "name": "Dynamic_annotation_task_response_numberEdge", "description": "An edge in a connection.", "fields": [ { @@ -37166,7 +38604,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_multiple_choice", + "name": "Dynamic_annotation_task_response_number", "ofType": null }, "isDeprecated": false, @@ -37182,7 +38620,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_number", + "name": "Dynamic_annotation_task_response_single_choice", "description": null, "fields": [ { @@ -37654,7 +39092,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_numberEdge", + "name": "Dynamic_annotation_task_response_single_choiceEdge", "description": "An edge in a connection.", "fields": [ { @@ -37683,7 +39121,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_number", + "name": "Dynamic_annotation_task_response_single_choice", "ofType": null }, "isDeprecated": false, @@ -37699,7 +39137,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_single_choice", + "name": "Dynamic_annotation_task_response_url", "description": null, "fields": [ { @@ -38171,7 +39609,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_single_choiceEdge", + "name": "Dynamic_annotation_task_response_urlEdge", "description": "An edge in a connection.", "fields": [ { @@ -38200,7 +39638,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_single_choice", + "name": "Dynamic_annotation_task_response_url", "ofType": null }, "isDeprecated": false, @@ -38216,7 +39654,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_url", + "name": "Dynamic_annotation_task_response_yes_no", "description": null, "fields": [ { @@ -38688,7 +40126,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_urlEdge", + "name": "Dynamic_annotation_task_response_yes_noEdge", "description": "An edge in a connection.", "fields": [ { @@ -38717,7 +40155,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_url", + "name": "Dynamic_annotation_task_response_yes_no", "ofType": null }, "isDeprecated": false, @@ -38733,7 +40171,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_yes_no", + "name": "Dynamic_annotation_task_status", "description": null, "fields": [ { @@ -39205,7 +40643,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_yes_noEdge", + "name": "Dynamic_annotation_task_statusEdge", "description": "An edge in a connection.", "fields": [ { @@ -39234,7 +40672,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_yes_no", + "name": "Dynamic_annotation_task_status", "ofType": null }, "isDeprecated": false, @@ -39250,7 +40688,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_status", + "name": "Dynamic_annotation_team_bot_response", "description": null, "fields": [ { @@ -39722,7 +41160,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_task_statusEdge", + "name": "Dynamic_annotation_team_bot_responseEdge", "description": "An edge in a connection.", "fields": [ { @@ -39751,7 +41189,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_status", + "name": "Dynamic_annotation_team_bot_response", "ofType": null }, "isDeprecated": false, @@ -39767,7 +41205,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_team_bot_response", + "name": "Dynamic_annotation_transcript", "description": null, "fields": [ { @@ -40239,7 +41677,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_team_bot_responseEdge", + "name": "Dynamic_annotation_transcriptEdge", "description": "An edge in a connection.", "fields": [ { @@ -40268,7 +41706,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_team_bot_response", + "name": "Dynamic_annotation_transcript", "ofType": null }, "isDeprecated": false, @@ -40284,7 +41722,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_transcript", + "name": "Dynamic_annotation_transcription", "description": null, "fields": [ { @@ -40756,7 +42194,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_transcriptEdge", + "name": "Dynamic_annotation_transcriptionEdge", "description": "An edge in a connection.", "fields": [ { @@ -40785,7 +42223,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_transcript", + "name": "Dynamic_annotation_transcription", "ofType": null }, "isDeprecated": false, @@ -40801,7 +42239,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_transcription", + "name": "Dynamic_annotation_translation", "description": null, "fields": [ { @@ -41273,7 +42711,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_transcriptionEdge", + "name": "Dynamic_annotation_translationEdge", "description": "An edge in a connection.", "fields": [ { @@ -41302,7 +42740,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_transcription", + "name": "Dynamic_annotation_translation", "ofType": null }, "isDeprecated": false, @@ -41318,7 +42756,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_translation", + "name": "Dynamic_annotation_translation_request", "description": null, "fields": [ { @@ -41790,7 +43228,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_translationEdge", + "name": "Dynamic_annotation_translation_requestEdge", "description": "An edge in a connection.", "fields": [ { @@ -41819,7 +43257,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_translation", + "name": "Dynamic_annotation_translation_request", "ofType": null }, "isDeprecated": false, @@ -41835,7 +43273,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_translation_request", + "name": "Dynamic_annotation_translation_status", "description": null, "fields": [ { @@ -42307,7 +43745,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_translation_requestEdge", + "name": "Dynamic_annotation_translation_statusEdge", "description": "An edge in a connection.", "fields": [ { @@ -42336,7 +43774,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_translation_request", + "name": "Dynamic_annotation_translation_status", "ofType": null }, "isDeprecated": false, @@ -42352,7 +43790,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_translation_status", + "name": "Dynamic_annotation_verification_status", "description": null, "fields": [ { @@ -42824,7 +44262,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_translation_statusEdge", + "name": "Dynamic_annotation_verification_statusEdge", "description": "An edge in a connection.", "fields": [ { @@ -42853,7 +44291,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_translation_status", + "name": "Dynamic_annotation_verification_status", "ofType": null }, "isDeprecated": false, @@ -42869,217 +44307,9 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_verification_status", - "description": null, + "name": "Explainer", + "description": "Explainer type", "fields": [ - { - "name": "annotated_id", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "annotated_type", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "annotation_type", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "annotations", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "annotation_type", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "type": { - "kind": "OBJECT", - "name": "AnnotationUnionConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "annotator", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "Annotator", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignments", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "type": { - "kind": "OBJECT", - "name": "UserConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "content", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "created_at", "description": null, @@ -43094,20 +44324,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "data", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "JsonStringType", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "dbid", "description": null, @@ -43116,21 +44332,21 @@ ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Int", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "file_data", + "name": "description", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "JsonStringType", + "name": "String", "ofType": null }, "isDeprecated": false, @@ -43155,35 +44371,35 @@ "deprecationReason": null }, { - "name": "lock_version", + "name": "language", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "Int", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "locked", + "name": "permissions", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "medias", + "name": "tags", "description": null, "args": [ { @@ -43237,70 +44453,70 @@ ], "type": { "kind": "OBJECT", - "name": "ProjectMediaConnection", + "name": "TagConnection", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "parsed_fragment", + "name": "team", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "JsonStringType", + "kind": "OBJECT", + "name": "PublicTeam", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "permissions", + "name": "team_id", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Int", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "project", + "name": "title", "description": null, "args": [ ], "type": { - "kind": "OBJECT", - "name": "Project", + "kind": "SCALAR", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "team", + "name": "updated_at", "description": null, "args": [ ], "type": { - "kind": "OBJECT", - "name": "Team", + "kind": "SCALAR", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "updated_at", + "name": "url", "description": null, "args": [ @@ -43314,14 +44530,28 @@ "deprecationReason": null }, { - "name": "version", + "name": "user", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Version", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user_id", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null }, "isDeprecated": false, @@ -43341,7 +44571,7 @@ }, { "kind": "OBJECT", - "name": "Dynamic_annotation_verification_statusEdge", + "name": "ExplainerEdge", "description": "An edge in a connection.", "fields": [ { @@ -43370,7 +44600,7 @@ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_verification_status", + "name": "Explainer", "ofType": null }, "isDeprecated": false, @@ -46324,6 +47554,142 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "ImportMediaInput", + "description": "Autogenerated input type of ImportMedia", + "fields": null, + "inputFields": [ + { + "name": "feedId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "projectMediaId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "parentId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "claimTitle", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "claimContext", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ImportMediaPayload", + "description": "Autogenerated return type of ImportMedia", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "projectMedia", + "description": null, + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ProjectMedia", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "SCALAR", "name": "Int", @@ -47984,6 +49350,35 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "createApiKey", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateApiKey", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateApiKeyInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateApiKeyPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "createClaimDescription", "description": null, @@ -49086,6 +50481,35 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "createExplainer", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateExplainer", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateExplainerInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateExplainerPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "createFactCheck", "description": null, @@ -49695,6 +51119,35 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "destroyApiKey", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DestroyApiKey", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DestroyApiKeyInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DestroyApiKeyPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "destroyDynamic", "description": null, @@ -50739,6 +52192,35 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "destroyExplainer", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DestroyExplainer", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DestroyExplainerInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DestroyExplainerPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "destroyFactCheck", "description": null, @@ -51261,6 +52743,35 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "feedImportMedia", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for ImportMedia", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ImportMediaInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ImportMediaPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "generateTwoFactorBackupCodes", "description": null, @@ -52827,6 +54338,35 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "updateExplainer", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateExplainer", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateExplainerInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateExplainerPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "updateFactCheck", "description": null, @@ -53555,6 +55095,11 @@ "name": "Annotator", "ofType": null }, + { + "kind": "OBJECT", + "name": "ApiKey", + "ofType": null + }, { "kind": "OBJECT", "name": "BotUser", @@ -53775,6 +55320,11 @@ "name": "Dynamic_annotation_verification_status", "ofType": null }, + { + "kind": "OBJECT", + "name": "Explainer", + "ofType": null + }, { "kind": "OBJECT", "name": "FactCheck", @@ -58306,6 +59856,48 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "imported_from_feed", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Feed", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "imported_from_feed_id", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "imported_from_project_media_id", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "is_confirmed", "description": null, @@ -58911,6 +60503,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "includeChildren", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -65911,6 +67515,96 @@ "name": "Team", "description": "Team type", "fields": [ + { + "name": "api_key", + "description": null, + "args": [ + { + "name": "dbid", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ApiKey", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "api_keys", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ApiKeyConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "archived", "description": null, @@ -65925,6 +67619,83 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "articles", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "article_type", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ArticleUnionConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "available_newsletter_header_types", "description": null, @@ -66262,6 +68033,20 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "get_explainers_enabled", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "get_fieldsets", "description": null, @@ -71300,34 +73085,328 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "task", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "Task", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "version", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "Version", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, + { + "name": "task", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Task", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "version", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Version", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "versionEdge", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "VersionEdge", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateDynamicAnnotationAnalysisInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationAnalysis", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fragment", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "annotated_id", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "annotated_type", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "action", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "set_attribution", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "action_data", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "set_fields", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lock_version", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assigned_to_ids", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locked", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UpdateDynamicAnnotationAnalysisPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationAnalysis", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dynamic", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Dynamic", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dynamicEdge", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "DynamicEdge", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dynamic_annotation_analysis", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Dynamic_annotation_analysis", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dynamic_annotation_analysisEdge", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Dynamic_annotation_analysisEdge", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "project", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Project", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "project_media", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "ProjectMedia", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Source", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "versionEdge", "description": null, @@ -71352,8 +73431,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationAnalysisInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationAnalysis", + "name": "UpdateDynamicAnnotationArchiverInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationArchiver", "fields": null, "inputFields": [ { @@ -71507,8 +73586,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationAnalysisPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationAnalysis", + "name": "UpdateDynamicAnnotationArchiverPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationArchiver", "fields": [ { "name": "clientMutationId", @@ -71553,28 +73632,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_analysis", + "name": "dynamic_annotation_archiver", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_analysis", + "name": "Dynamic_annotation_archiver", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_analysisEdge", + "name": "dynamic_annotation_archiverEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_analysisEdge", + "name": "Dynamic_annotation_archiverEdge", "ofType": null }, "isDeprecated": false, @@ -71646,8 +73725,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationArchiverInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationArchiver", + "name": "UpdateDynamicAnnotationClipInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationClip", "fields": null, "inputFields": [ { @@ -71801,8 +73880,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationArchiverPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationArchiver", + "name": "UpdateDynamicAnnotationClipPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationClip", "fields": [ { "name": "clientMutationId", @@ -71847,28 +73926,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_archiver", + "name": "dynamic_annotation_clip", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_archiver", + "name": "Dynamic_annotation_clip", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_archiverEdge", + "name": "dynamic_annotation_clipEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_archiverEdge", + "name": "Dynamic_annotation_clipEdge", "ofType": null }, "isDeprecated": false, @@ -71940,8 +74019,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationClipInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationClip", + "name": "UpdateDynamicAnnotationEmbedCodeInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationEmbedCode", "fields": null, "inputFields": [ { @@ -72095,8 +74174,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationClipPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationClip", + "name": "UpdateDynamicAnnotationEmbedCodePayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationEmbedCode", "fields": [ { "name": "clientMutationId", @@ -72141,28 +74220,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_clip", + "name": "dynamic_annotation_embed_code", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_clip", + "name": "Dynamic_annotation_embed_code", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_clipEdge", + "name": "dynamic_annotation_embed_codeEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_clipEdge", + "name": "Dynamic_annotation_embed_codeEdge", "ofType": null }, "isDeprecated": false, @@ -72234,8 +74313,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationEmbedCodeInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationEmbedCode", + "name": "UpdateDynamicAnnotationExtractedTextInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationExtractedText", "fields": null, "inputFields": [ { @@ -72389,8 +74468,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationEmbedCodePayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationEmbedCode", + "name": "UpdateDynamicAnnotationExtractedTextPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationExtractedText", "fields": [ { "name": "clientMutationId", @@ -72435,28 +74514,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_embed_code", + "name": "dynamic_annotation_extracted_text", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_embed_code", + "name": "Dynamic_annotation_extracted_text", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_embed_codeEdge", + "name": "dynamic_annotation_extracted_textEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_embed_codeEdge", + "name": "Dynamic_annotation_extracted_textEdge", "ofType": null }, "isDeprecated": false, @@ -72528,8 +74607,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationExtractedTextInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationExtractedText", + "name": "UpdateDynamicAnnotationFlagInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationFlag", "fields": null, "inputFields": [ { @@ -72683,8 +74762,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationExtractedTextPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationExtractedText", + "name": "UpdateDynamicAnnotationFlagPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationFlag", "fields": [ { "name": "clientMutationId", @@ -72729,28 +74808,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_extracted_text", + "name": "dynamic_annotation_flag", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_extracted_text", + "name": "Dynamic_annotation_flag", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_extracted_textEdge", + "name": "dynamic_annotation_flagEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_extracted_textEdge", + "name": "Dynamic_annotation_flagEdge", "ofType": null }, "isDeprecated": false, @@ -72822,8 +74901,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationFlagInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationFlag", + "name": "UpdateDynamicAnnotationGeolocationInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationGeolocation", "fields": null, "inputFields": [ { @@ -72977,8 +75056,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationFlagPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationFlag", + "name": "UpdateDynamicAnnotationGeolocationPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationGeolocation", "fields": [ { "name": "clientMutationId", @@ -73023,28 +75102,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_flag", + "name": "dynamic_annotation_geolocation", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_flag", + "name": "Dynamic_annotation_geolocation", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_flagEdge", + "name": "dynamic_annotation_geolocationEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_flagEdge", + "name": "Dynamic_annotation_geolocationEdge", "ofType": null }, "isDeprecated": false, @@ -73116,8 +75195,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationGeolocationInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationGeolocation", + "name": "UpdateDynamicAnnotationLanguageInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationLanguage", "fields": null, "inputFields": [ { @@ -73271,8 +75350,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationGeolocationPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationGeolocation", + "name": "UpdateDynamicAnnotationLanguagePayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationLanguage", "fields": [ { "name": "clientMutationId", @@ -73317,28 +75396,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_geolocation", + "name": "dynamic_annotation_language", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_geolocation", + "name": "Dynamic_annotation_language", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_geolocationEdge", + "name": "dynamic_annotation_languageEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_geolocationEdge", + "name": "Dynamic_annotation_languageEdge", "ofType": null }, "isDeprecated": false, @@ -73410,8 +75489,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationLanguageInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationLanguage", + "name": "UpdateDynamicAnnotationMemebusterInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationMemebuster", "fields": null, "inputFields": [ { @@ -73565,8 +75644,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationLanguagePayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationLanguage", + "name": "UpdateDynamicAnnotationMemebusterPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationMemebuster", "fields": [ { "name": "clientMutationId", @@ -73611,28 +75690,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_language", + "name": "dynamic_annotation_memebuster", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_language", + "name": "Dynamic_annotation_memebuster", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_languageEdge", + "name": "dynamic_annotation_memebusterEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_languageEdge", + "name": "Dynamic_annotation_memebusterEdge", "ofType": null }, "isDeprecated": false, @@ -73704,8 +75783,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationMemebusterInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationMemebuster", + "name": "UpdateDynamicAnnotationMetadataInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationMetadata", "fields": null, "inputFields": [ { @@ -73859,8 +75938,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationMemebusterPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationMemebuster", + "name": "UpdateDynamicAnnotationMetadataPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationMetadata", "fields": [ { "name": "clientMutationId", @@ -73905,28 +75984,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_memebuster", + "name": "dynamic_annotation_metadata", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_memebuster", + "name": "Dynamic_annotation_metadata", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_memebusterEdge", + "name": "dynamic_annotation_metadataEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_memebusterEdge", + "name": "Dynamic_annotation_metadataEdge", "ofType": null }, "isDeprecated": false, @@ -73998,8 +76077,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationMetadataInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationMetadata", + "name": "UpdateDynamicAnnotationMetricsInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationMetrics", "fields": null, "inputFields": [ { @@ -74153,8 +76232,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationMetadataPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationMetadata", + "name": "UpdateDynamicAnnotationMetricsPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationMetrics", "fields": [ { "name": "clientMutationId", @@ -74199,28 +76278,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_metadata", + "name": "dynamic_annotation_metrics", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_metadata", + "name": "Dynamic_annotation_metrics", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_metadataEdge", + "name": "dynamic_annotation_metricsEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_metadataEdge", + "name": "Dynamic_annotation_metricsEdge", "ofType": null }, "isDeprecated": false, @@ -74292,8 +76371,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationMetricsInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationMetrics", + "name": "UpdateDynamicAnnotationMtInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationMt", "fields": null, "inputFields": [ { @@ -74447,8 +76526,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationMetricsPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationMetrics", + "name": "UpdateDynamicAnnotationMtPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationMt", "fields": [ { "name": "clientMutationId", @@ -74493,28 +76572,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_metrics", + "name": "dynamic_annotation_mt", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_metrics", + "name": "Dynamic_annotation_mt", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_metricsEdge", + "name": "dynamic_annotation_mtEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_metricsEdge", + "name": "Dynamic_annotation_mtEdge", "ofType": null }, "isDeprecated": false, @@ -74586,8 +76665,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationMtInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationMt", + "name": "UpdateDynamicAnnotationReportDesignInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationReportDesign", "fields": null, "inputFields": [ { @@ -74741,8 +76820,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationMtPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationMt", + "name": "UpdateDynamicAnnotationReportDesignPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationReportDesign", "fields": [ { "name": "clientMutationId", @@ -74787,28 +76866,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_mt", + "name": "dynamic_annotation_report_design", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_mt", + "name": "Dynamic_annotation_report_design", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_mtEdge", + "name": "dynamic_annotation_report_designEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_mtEdge", + "name": "Dynamic_annotation_report_designEdge", "ofType": null }, "isDeprecated": false, @@ -74880,8 +76959,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationReportDesignInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationReportDesign", + "name": "UpdateDynamicAnnotationReverseImageInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationReverseImage", "fields": null, "inputFields": [ { @@ -75035,8 +77114,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationReportDesignPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationReportDesign", + "name": "UpdateDynamicAnnotationReverseImagePayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationReverseImage", "fields": [ { "name": "clientMutationId", @@ -75081,28 +77160,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_report_design", + "name": "dynamic_annotation_reverse_image", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_report_design", + "name": "Dynamic_annotation_reverse_image", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_report_designEdge", + "name": "dynamic_annotation_reverse_imageEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_report_designEdge", + "name": "Dynamic_annotation_reverse_imageEdge", "ofType": null }, "isDeprecated": false, @@ -75174,8 +77253,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationReverseImageInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationReverseImage", + "name": "UpdateDynamicAnnotationSlackMessageInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationSlackMessage", "fields": null, "inputFields": [ { @@ -75329,8 +77408,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationReverseImagePayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationReverseImage", + "name": "UpdateDynamicAnnotationSlackMessagePayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationSlackMessage", "fields": [ { "name": "clientMutationId", @@ -75375,28 +77454,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_reverse_image", + "name": "dynamic_annotation_slack_message", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_reverse_image", + "name": "Dynamic_annotation_slack_message", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_reverse_imageEdge", + "name": "dynamic_annotation_slack_messageEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_reverse_imageEdge", + "name": "Dynamic_annotation_slack_messageEdge", "ofType": null }, "isDeprecated": false, @@ -75468,8 +77547,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationSlackMessageInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationSlackMessage", + "name": "UpdateDynamicAnnotationSmoochResponseInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationSmoochResponse", "fields": null, "inputFields": [ { @@ -75623,8 +77702,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationSlackMessagePayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationSlackMessage", + "name": "UpdateDynamicAnnotationSmoochResponsePayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationSmoochResponse", "fields": [ { "name": "clientMutationId", @@ -75669,28 +77748,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_slack_message", + "name": "dynamic_annotation_smooch_response", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_slack_message", + "name": "Dynamic_annotation_smooch_response", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_slack_messageEdge", + "name": "dynamic_annotation_smooch_responseEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_slack_messageEdge", + "name": "Dynamic_annotation_smooch_responseEdge", "ofType": null }, "isDeprecated": false, @@ -75762,8 +77841,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationSmoochResponseInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationSmoochResponse", + "name": "UpdateDynamicAnnotationSmoochUserInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationSmoochUser", "fields": null, "inputFields": [ { @@ -75917,8 +77996,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationSmoochResponsePayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationSmoochResponse", + "name": "UpdateDynamicAnnotationSmoochUserPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationSmoochUser", "fields": [ { "name": "clientMutationId", @@ -75963,28 +78042,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_smooch_response", + "name": "dynamic_annotation_smooch_user", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_smooch_response", + "name": "Dynamic_annotation_smooch_user", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_smooch_responseEdge", + "name": "dynamic_annotation_smooch_userEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_smooch_responseEdge", + "name": "Dynamic_annotation_smooch_userEdge", "ofType": null }, "isDeprecated": false, @@ -76056,8 +78135,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationSmoochUserInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationSmoochUser", + "name": "UpdateDynamicAnnotationSyrianArchiveDataInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationSyrianArchiveData", "fields": null, "inputFields": [ { @@ -76211,8 +78290,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationSmoochUserPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationSmoochUser", + "name": "UpdateDynamicAnnotationSyrianArchiveDataPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationSyrianArchiveData", "fields": [ { "name": "clientMutationId", @@ -76257,28 +78336,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_smooch_user", + "name": "dynamic_annotation_syrian_archive_data", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_smooch_user", + "name": "Dynamic_annotation_syrian_archive_data", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_smooch_userEdge", + "name": "dynamic_annotation_syrian_archive_dataEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_smooch_userEdge", + "name": "Dynamic_annotation_syrian_archive_dataEdge", "ofType": null }, "isDeprecated": false, @@ -76350,8 +78429,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationSyrianArchiveDataInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationSyrianArchiveData", + "name": "UpdateDynamicAnnotationTaskResponseDatetimeInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseDatetime", "fields": null, "inputFields": [ { @@ -76505,8 +78584,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationSyrianArchiveDataPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationSyrianArchiveData", + "name": "UpdateDynamicAnnotationTaskResponseDatetimePayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseDatetime", "fields": [ { "name": "clientMutationId", @@ -76551,28 +78630,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_syrian_archive_data", + "name": "dynamic_annotation_task_response_datetime", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_syrian_archive_data", + "name": "Dynamic_annotation_task_response_datetime", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_syrian_archive_dataEdge", + "name": "dynamic_annotation_task_response_datetimeEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_syrian_archive_dataEdge", + "name": "Dynamic_annotation_task_response_datetimeEdge", "ofType": null }, "isDeprecated": false, @@ -76644,8 +78723,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseDatetimeInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseDatetime", + "name": "UpdateDynamicAnnotationTaskResponseFileUploadInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseFileUpload", "fields": null, "inputFields": [ { @@ -76799,8 +78878,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseDatetimePayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseDatetime", + "name": "UpdateDynamicAnnotationTaskResponseFileUploadPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseFileUpload", "fields": [ { "name": "clientMutationId", @@ -76845,28 +78924,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_datetime", + "name": "dynamic_annotation_task_response_file_upload", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_datetime", + "name": "Dynamic_annotation_task_response_file_upload", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_datetimeEdge", + "name": "dynamic_annotation_task_response_file_uploadEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_datetimeEdge", + "name": "Dynamic_annotation_task_response_file_uploadEdge", "ofType": null }, "isDeprecated": false, @@ -76938,8 +79017,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseFileUploadInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseFileUpload", + "name": "UpdateDynamicAnnotationTaskResponseFreeTextInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseFreeText", "fields": null, "inputFields": [ { @@ -77093,8 +79172,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseFileUploadPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseFileUpload", + "name": "UpdateDynamicAnnotationTaskResponseFreeTextPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseFreeText", "fields": [ { "name": "clientMutationId", @@ -77139,28 +79218,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_file_upload", + "name": "dynamic_annotation_task_response_free_text", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_file_upload", + "name": "Dynamic_annotation_task_response_free_text", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_file_uploadEdge", + "name": "dynamic_annotation_task_response_free_textEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_file_uploadEdge", + "name": "Dynamic_annotation_task_response_free_textEdge", "ofType": null }, "isDeprecated": false, @@ -77232,8 +79311,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseFreeTextInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseFreeText", + "name": "UpdateDynamicAnnotationTaskResponseGeolocationInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseGeolocation", "fields": null, "inputFields": [ { @@ -77387,8 +79466,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseFreeTextPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseFreeText", + "name": "UpdateDynamicAnnotationTaskResponseGeolocationPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseGeolocation", "fields": [ { "name": "clientMutationId", @@ -77433,28 +79512,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_free_text", + "name": "dynamic_annotation_task_response_geolocation", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_free_text", + "name": "Dynamic_annotation_task_response_geolocation", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_free_textEdge", + "name": "dynamic_annotation_task_response_geolocationEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_free_textEdge", + "name": "Dynamic_annotation_task_response_geolocationEdge", "ofType": null }, "isDeprecated": false, @@ -77526,8 +79605,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseGeolocationInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseGeolocation", + "name": "UpdateDynamicAnnotationTaskResponseMultipleChoiceInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseMultipleChoice", "fields": null, "inputFields": [ { @@ -77681,8 +79760,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseGeolocationPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseGeolocation", + "name": "UpdateDynamicAnnotationTaskResponseMultipleChoicePayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseMultipleChoice", "fields": [ { "name": "clientMutationId", @@ -77727,28 +79806,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_geolocation", + "name": "dynamic_annotation_task_response_multiple_choice", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_geolocation", + "name": "Dynamic_annotation_task_response_multiple_choice", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_geolocationEdge", + "name": "dynamic_annotation_task_response_multiple_choiceEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_geolocationEdge", + "name": "Dynamic_annotation_task_response_multiple_choiceEdge", "ofType": null }, "isDeprecated": false, @@ -77820,8 +79899,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseMultipleChoiceInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseMultipleChoice", + "name": "UpdateDynamicAnnotationTaskResponseNumberInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseNumber", "fields": null, "inputFields": [ { @@ -77975,8 +80054,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseMultipleChoicePayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseMultipleChoice", + "name": "UpdateDynamicAnnotationTaskResponseNumberPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseNumber", "fields": [ { "name": "clientMutationId", @@ -78021,28 +80100,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_multiple_choice", + "name": "dynamic_annotation_task_response_number", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_multiple_choice", + "name": "Dynamic_annotation_task_response_number", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_multiple_choiceEdge", + "name": "dynamic_annotation_task_response_numberEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_multiple_choiceEdge", + "name": "Dynamic_annotation_task_response_numberEdge", "ofType": null }, "isDeprecated": false, @@ -78114,8 +80193,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseNumberInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseNumber", + "name": "UpdateDynamicAnnotationTaskResponseSingleChoiceInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseSingleChoice", "fields": null, "inputFields": [ { @@ -78269,8 +80348,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseNumberPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseNumber", + "name": "UpdateDynamicAnnotationTaskResponseSingleChoicePayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseSingleChoice", "fields": [ { "name": "clientMutationId", @@ -78315,28 +80394,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_number", + "name": "dynamic_annotation_task_response_single_choice", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_number", + "name": "Dynamic_annotation_task_response_single_choice", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_numberEdge", + "name": "dynamic_annotation_task_response_single_choiceEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_numberEdge", + "name": "Dynamic_annotation_task_response_single_choiceEdge", "ofType": null }, "isDeprecated": false, @@ -78408,8 +80487,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseSingleChoiceInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseSingleChoice", + "name": "UpdateDynamicAnnotationTaskResponseUrlInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseUrl", "fields": null, "inputFields": [ { @@ -78563,8 +80642,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseSingleChoicePayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseSingleChoice", + "name": "UpdateDynamicAnnotationTaskResponseUrlPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseUrl", "fields": [ { "name": "clientMutationId", @@ -78609,28 +80688,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_single_choice", + "name": "dynamic_annotation_task_response_url", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_single_choice", + "name": "Dynamic_annotation_task_response_url", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_single_choiceEdge", + "name": "dynamic_annotation_task_response_urlEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_single_choiceEdge", + "name": "Dynamic_annotation_task_response_urlEdge", "ofType": null }, "isDeprecated": false, @@ -78702,8 +80781,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseUrlInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseUrl", + "name": "UpdateDynamicAnnotationTaskResponseYesNoInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseYesNo", "fields": null, "inputFields": [ { @@ -78857,8 +80936,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseUrlPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseUrl", + "name": "UpdateDynamicAnnotationTaskResponseYesNoPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseYesNo", "fields": [ { "name": "clientMutationId", @@ -78903,28 +80982,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_url", + "name": "dynamic_annotation_task_response_yes_no", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_url", + "name": "Dynamic_annotation_task_response_yes_no", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_urlEdge", + "name": "dynamic_annotation_task_response_yes_noEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_urlEdge", + "name": "Dynamic_annotation_task_response_yes_noEdge", "ofType": null }, "isDeprecated": false, @@ -78996,8 +81075,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseYesNoInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTaskResponseYesNo", + "name": "UpdateDynamicAnnotationTaskStatusInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTaskStatus", "fields": null, "inputFields": [ { @@ -79151,8 +81230,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTaskResponseYesNoPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTaskResponseYesNo", + "name": "UpdateDynamicAnnotationTaskStatusPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTaskStatus", "fields": [ { "name": "clientMutationId", @@ -79197,28 +81276,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_yes_no", + "name": "dynamic_annotation_task_status", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_yes_no", + "name": "Dynamic_annotation_task_status", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_task_response_yes_noEdge", + "name": "dynamic_annotation_task_statusEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_response_yes_noEdge", + "name": "Dynamic_annotation_task_statusEdge", "ofType": null }, "isDeprecated": false, @@ -79290,8 +81369,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTaskStatusInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTaskStatus", + "name": "UpdateDynamicAnnotationTeamBotResponseInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTeamBotResponse", "fields": null, "inputFields": [ { @@ -79445,8 +81524,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTaskStatusPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTaskStatus", + "name": "UpdateDynamicAnnotationTeamBotResponsePayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTeamBotResponse", "fields": [ { "name": "clientMutationId", @@ -79491,28 +81570,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_task_status", + "name": "dynamic_annotation_team_bot_response", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_status", + "name": "Dynamic_annotation_team_bot_response", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_task_statusEdge", + "name": "dynamic_annotation_team_bot_responseEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_task_statusEdge", + "name": "Dynamic_annotation_team_bot_responseEdge", "ofType": null }, "isDeprecated": false, @@ -79584,8 +81663,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTeamBotResponseInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTeamBotResponse", + "name": "UpdateDynamicAnnotationTranscriptInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTranscript", "fields": null, "inputFields": [ { @@ -79739,8 +81818,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTeamBotResponsePayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTeamBotResponse", + "name": "UpdateDynamicAnnotationTranscriptPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTranscript", "fields": [ { "name": "clientMutationId", @@ -79785,28 +81864,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_team_bot_response", + "name": "dynamic_annotation_transcript", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_team_bot_response", + "name": "Dynamic_annotation_transcript", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_team_bot_responseEdge", + "name": "dynamic_annotation_transcriptEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_team_bot_responseEdge", + "name": "Dynamic_annotation_transcriptEdge", "ofType": null }, "isDeprecated": false, @@ -79878,8 +81957,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTranscriptInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTranscript", + "name": "UpdateDynamicAnnotationTranscriptionInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTranscription", "fields": null, "inputFields": [ { @@ -80033,8 +82112,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTranscriptPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTranscript", + "name": "UpdateDynamicAnnotationTranscriptionPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTranscription", "fields": [ { "name": "clientMutationId", @@ -80079,28 +82158,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_transcript", + "name": "dynamic_annotation_transcription", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_transcript", + "name": "Dynamic_annotation_transcription", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_transcriptEdge", + "name": "dynamic_annotation_transcriptionEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_transcriptEdge", + "name": "Dynamic_annotation_transcriptionEdge", "ofType": null }, "isDeprecated": false, @@ -80172,8 +82251,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTranscriptionInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTranscription", + "name": "UpdateDynamicAnnotationTranslationInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTranslation", "fields": null, "inputFields": [ { @@ -80327,8 +82406,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTranscriptionPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTranscription", + "name": "UpdateDynamicAnnotationTranslationPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTranslation", "fields": [ { "name": "clientMutationId", @@ -80373,28 +82452,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_transcription", + "name": "dynamic_annotation_translation", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_transcription", + "name": "Dynamic_annotation_translation", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_transcriptionEdge", + "name": "dynamic_annotation_translationEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_transcriptionEdge", + "name": "Dynamic_annotation_translationEdge", "ofType": null }, "isDeprecated": false, @@ -80466,8 +82545,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTranslationInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTranslation", + "name": "UpdateDynamicAnnotationTranslationRequestInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTranslationRequest", "fields": null, "inputFields": [ { @@ -80621,8 +82700,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTranslationPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTranslation", + "name": "UpdateDynamicAnnotationTranslationRequestPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTranslationRequest", "fields": [ { "name": "clientMutationId", @@ -80667,28 +82746,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_translation", + "name": "dynamic_annotation_translation_request", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_translation", + "name": "Dynamic_annotation_translation_request", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_translationEdge", + "name": "dynamic_annotation_translation_requestEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_translationEdge", + "name": "Dynamic_annotation_translation_requestEdge", "ofType": null }, "isDeprecated": false, @@ -80760,8 +82839,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTranslationRequestInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTranslationRequest", + "name": "UpdateDynamicAnnotationTranslationStatusInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationTranslationStatus", "fields": null, "inputFields": [ { @@ -80915,8 +82994,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTranslationRequestPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTranslationRequest", + "name": "UpdateDynamicAnnotationTranslationStatusPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationTranslationStatus", "fields": [ { "name": "clientMutationId", @@ -80961,28 +83040,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_translation_request", + "name": "dynamic_annotation_translation_status", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_translation_request", + "name": "Dynamic_annotation_translation_status", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_translation_requestEdge", + "name": "dynamic_annotation_translation_statusEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_translation_requestEdge", + "name": "Dynamic_annotation_translation_statusEdge", "ofType": null }, "isDeprecated": false, @@ -81054,8 +83133,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationTranslationStatusInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationTranslationStatus", + "name": "UpdateDynamicAnnotationVerificationStatusInput", + "description": "Autogenerated input type of UpdateDynamicAnnotationVerificationStatus", "fields": null, "inputFields": [ { @@ -81209,8 +83288,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationTranslationStatusPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationTranslationStatus", + "name": "UpdateDynamicAnnotationVerificationStatusPayload", + "description": "Autogenerated return type of UpdateDynamicAnnotationVerificationStatus", "fields": [ { "name": "clientMutationId", @@ -81255,28 +83334,28 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_translation_status", + "name": "dynamic_annotation_verification_status", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_translation_status", + "name": "Dynamic_annotation_verification_status", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_translation_statusEdge", + "name": "dynamic_annotation_verification_statusEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_translation_statusEdge", + "name": "Dynamic_annotation_verification_statusEdge", "ofType": null }, "isDeprecated": false, @@ -81348,8 +83427,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicAnnotationVerificationStatusInput", - "description": "Autogenerated input type of UpdateDynamicAnnotationVerificationStatus", + "name": "UpdateDynamicInput", + "description": "Autogenerated input type of UpdateDynamic", "fields": null, "inputFields": [ { @@ -81425,11 +83504,11 @@ "deprecationReason": null }, { - "name": "action_data", + "name": "locked", "description": null, "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null }, "defaultValue": null, @@ -81448,6 +83527,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "annotation_type", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "lock_version", "description": null, @@ -81473,11 +83564,11 @@ "deprecationReason": null }, { - "name": "locked", + "name": "assignment_message", "description": null, "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null }, "defaultValue": null, @@ -81503,8 +83594,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicAnnotationVerificationStatusPayload", - "description": "Autogenerated return type of UpdateDynamicAnnotationVerificationStatus", + "name": "UpdateDynamicPayload", + "description": "Autogenerated return type of UpdateDynamic", "fields": [ { "name": "clientMutationId", @@ -81549,70 +83640,70 @@ "deprecationReason": null }, { - "name": "dynamic_annotation_verification_status", + "name": "project", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_verification_status", + "name": "Project", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dynamic_annotation_verification_statusEdge", + "name": "project_media", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Dynamic_annotation_verification_statusEdge", + "name": "ProjectMedia", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "project", + "name": "source", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Project", + "name": "Source", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "project_media", + "name": "task", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "ProjectMedia", + "name": "Task", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "source", + "name": "version", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Source", + "name": "Version", "ofType": null }, "isDeprecated": false, @@ -81642,8 +83733,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateDynamicInput", - "description": "Autogenerated input type of UpdateDynamic", + "name": "UpdateExplainerInput", + "description": "Autogenerated input type of UpdateExplainer", "fields": null, "inputFields": [ { @@ -81659,91 +83750,23 @@ "deprecationReason": null }, { - "name": "fragment", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "annotated_id", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "annotated_type", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "action", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "set_attribution", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "locked", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "set_fields", + "name": "title", "description": null, "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "annotation_type", + "name": "description", "description": null, "type": { "kind": "SCALAR", @@ -81755,19 +83778,7 @@ "deprecationReason": null }, { - "name": "lock_version", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assigned_to_ids", + "name": "url", "description": null, "type": { "kind": "SCALAR", @@ -81779,7 +83790,7 @@ "deprecationReason": null }, { - "name": "assignment_message", + "name": "language", "description": null, "type": { "kind": "SCALAR", @@ -81809,8 +83820,8 @@ }, { "kind": "OBJECT", - "name": "UpdateDynamicPayload", - "description": "Autogenerated return type of UpdateDynamic", + "name": "UpdateExplainerPayload", + "description": "Autogenerated return type of UpdateExplainer", "fields": [ { "name": "clientMutationId", @@ -81827,112 +83838,42 @@ "deprecationReason": null }, { - "name": "dynamic", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "Dynamic", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "dynamicEdge", + "name": "explainer", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "DynamicEdge", + "name": "Explainer", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "project", + "name": "explainerEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Project", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "project_media", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "ProjectMedia", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "source", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "Source", + "name": "ExplainerEdge", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "task", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "Task", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "version", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "Version", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "versionEdge", + "name": "team", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "VersionEdge", + "name": "Team", "ofType": null }, "isDeprecated": false, diff --git a/test/contract/alegre_contract_test.rb b/test/contract/alegre_contract_test.rb index 4159c98007..1b66514a06 100644 --- a/test/contract/alegre_contract_test.rb +++ b/test/contract/alegre_contract_test.rb @@ -25,7 +25,7 @@ def stub_similarity_requests(url, pm) WebMock.stub_request(:delete, 'http://localhost:3100/text/similarity/').to_return(body: { success: true }.to_json) WebMock.stub_request(:post, 'http://localhost:3100/image/similarity/').to_return(body: { 'success': true }.to_json) WebMock.stub_request(:post, 'http://localhost:3100/image/classification/').with(body: { uri: url}).to_return(body:{ result: @flags }.to_json) - WebMock.stub_request(:post, 'http://localhost:3100/similarity/sync/image').to_return(body: { "result": [] }.to_json) + WebMock.stub_request(:post, 'http://localhost:3100/similarity/async/image').to_return(body: { "result": [] }.to_json) end # def teardown diff --git a/test/controllers/admin_controller_test.rb b/test/controllers/admin_controller_test.rb index 7cc336f89d..d083b6897e 100644 --- a/test/controllers/admin_controller_test.rb +++ b/test/controllers/admin_controller_test.rb @@ -97,6 +97,14 @@ def setup assert_response 401 end + test "should not connect Facebook page to Smooch bot if auth is nil" do + b = create_team_bot login: 'smooch' + tbi = create_team_bot_installation + session['check.facebook.authdata'] = nil + get :save_messenger_credentials_for_smooch_bot, params: { id: tbi.id, token: random_string } + assert_response 400 + end + test "should not connect Facebook page to Smooch bot if not exactly one page was selected" do Bot::Smooch.stubs(:smooch_api_client).returns(nil) SmoochApi::IntegrationApi.any_instance.expects(:create_integration).once diff --git a/test/controllers/elastic_search_10_test.rb b/test/controllers/elastic_search_10_test.rb index 47f651b3f3..82199c9852 100644 --- a/test/controllers/elastic_search_10_test.rb +++ b/test/controllers/elastic_search_10_test.rb @@ -479,4 +479,43 @@ def setup Team.current = nil end + + test "should filter by positive_tipline_search_results_count and negative_tipline_search_results_count numeric range" do + RequestStore.store[:skip_cached_field_update] = false + p = create_project + [:positive_tipline_search_results_count, :negative_tipline_search_results_count].each do |field| + query = { projects: [p.id], "#{field}": { max: 5 } } + query[field][:min] = 0 + result = CheckSearch.new(query.to_json, nil, p.team_id) + assert_equal 0, result.medias.count + end + pm1 = create_project_media project: p, quote: 'Test A', disable_es_callbacks: false + pm2 = create_project_media project: p, quote: 'Test B', disable_es_callbacks: false + + # Add positive search results + create_tipline_request team_id: p.team_id, associated: pm1, smooch_request_type: 'relevant_search_result_requests' + 2.times { create_tipline_request(team_id: p.team_id, associated: pm2, smooch_request_type: 'relevant_search_result_requests') } + + # Add negative search results + create_tipline_request team_id: p.team_id, associated: pm1, smooch_request_type: 'irrelevant_search_result_requests' + 2.times { create_tipline_request(team_id: p.team_id, associated: pm2, smooch_request_type: 'irrelevant_search_result_requests') } + + sleep 2 + + min_mapping = { + "0": [pm1.id, pm2.id], + "1": [pm1.id, pm2.id], + "2": [pm2.id], + "3": [], + } + + [:positive_tipline_search_results_count, :negative_tipline_search_results_count].each do |field| + query = { projects: [p.id], "#{field}": { max: 5 } } + min_mapping.each do |min, items| + query[field][:min] = min.to_s + result = CheckSearch.new(query.to_json, nil, p.team_id) + assert_equal items.sort, result.medias.map(&:id).sort + end + end + end end diff --git a/test/controllers/elastic_search_9_test.rb b/test/controllers/elastic_search_9_test.rb index b47980d4e1..4f6d12dcf9 100644 --- a/test/controllers/elastic_search_9_test.rb +++ b/test/controllers/elastic_search_9_test.rb @@ -71,10 +71,10 @@ def setup # Text extraction Bot::Alegre.unstub(:media_file_url) pm = create_project_media team: team, media: create_uploaded_image, disable_es_callbacks: false - WebMock.stub_request(:post, 'http://alegre/similarity/sync/image').with(body: {doc_id: Bot::Alegre.item_doc_id(pm), context: {:has_custom_id=>true, :project_media_id=>pm.id, :team_id=>pm.team_id}, threshold: 0.89, url: "some/path"}).to_return(body: { + WebMock.stub_request(:post, 'http://alegre/similarity/async/image').with(body: {doc_id: Bot::Alegre.item_doc_id(pm), context: {:has_custom_id=>true, :project_media_id=>pm.id, :team_id=>pm.team_id, :temporary_media=>false}, threshold: 0.89, url: "some/path", confirmed: false}).to_return(body: { "result": [] }.to_json) - WebMock.stub_request(:post, 'http://alegre/similarity/sync/image').with(body: {doc_id: Bot::Alegre.item_doc_id(pm), context: {:has_custom_id=>true, :project_media_id=>pm.id, :team_id=>pm.team_id}, threshold: 0.95, url: "some/path"}).to_return(body: { + WebMock.stub_request(:post, 'http://alegre/similarity/async/image').with(body: {doc_id: Bot::Alegre.item_doc_id(pm), context: {:has_custom_id=>true, :project_media_id=>pm.id, :team_id=>pm.team_id, :temporary_media=>false}, threshold: 0.95, url: "some/path", confirmed: true}).to_return(body: { "result": [] }.to_json) Bot::Alegre.stubs(:media_file_url).with(pm).returns("some/path") diff --git a/test/controllers/graphql_controller_12_test.rb b/test/controllers/graphql_controller_12_test.rb index 1dd9a3339e..db8b519c83 100644 --- a/test/controllers/graphql_controller_12_test.rb +++ b/test/controllers/graphql_controller_12_test.rb @@ -332,9 +332,139 @@ def teardown create_cluster_project_media cluster: c, project_media: pm authenticate_with_user(@u) - query = 'query { feed(id: "' + f.id.to_s + '") { cluster(project_media_id: ' + pm.id.to_s + ') { dbid, project_media(id: ' + pm.id.to_s + ') { id }, project_medias(teamId: ' + @t.id.to_s + ', first: 1) { edges { node { id } } }, cluster_teams(first: 10) { edges { node { id, team { name }, last_request_date, media_count, requests_count, fact_checks(first: 1) { edges { node { id } } } } } } } } }' + query = 'query { feed(id: "' + f.id.to_s + '") { cluster(project_media_id: ' + pm.id.to_s + ') { dbid, project_media(id: ' + pm.id.to_s + ') { id, imported_from_feed { id } }, project_medias(teamId: ' + @t.id.to_s + ', first: 1) { edges { node { id } } }, cluster_teams(first: 10) { edges { node { id, team { name }, last_request_date, media_count, requests_count, fact_checks(first: 1) { edges { node { id } } } } } } } } }' post :create, params: { query: query } assert_response :success assert_equal c.id, JSON.parse(@response.body)['data']['feed']['cluster']['dbid'] end + + test "should import medias from the feed by creating a new item" do + Sidekiq::Testing.inline! + t = create_team + pm1 = create_project_media team: t + pm2 = create_project_media team: t + f = create_feed team: @t + f.teams << t + c = create_cluster feed: f, team_ids: [t.id], project_media_id: pm1.id + create_cluster_project_media cluster: c, project_media: pm1 + create_cluster_project_media cluster: c, project_media: pm2 + assert_equal 0, @t.project_medias.count + + authenticate_with_user(@u) + query = "mutation { feedImportMedia(input: { feedId: #{f.id}, projectMediaId: #{pm1.id} }) { projectMedia { id } } }" + post :create, params: { query: query, team: @t.slug } + assert_response :success + assert_equal 2, @t.reload.project_medias.count + end + + test "should import medias from the feed by adding to existing item" do + Sidekiq::Testing.inline! + pm = create_project_media team: @t + t = create_team + pm1 = create_project_media team: t + pm2 = create_project_media team: t + f = create_feed team: @t + f.teams << t + c = create_cluster feed: f, team_ids: [t.id], project_media_id: pm1.id + create_cluster_project_media cluster: c, project_media: pm1 + create_cluster_project_media cluster: c, project_media: pm2 + assert_equal 1, @t.project_medias.count + + authenticate_with_user(@u) + query = "mutation { feedImportMedia(input: { feedId: #{f.id}, projectMediaId: #{pm1.id}, parentId: #{pm.id} }) { projectMedia { id } } }" + post :create, params: { query: query, team: @t.slug } + assert_response :success + assert_equal 3, @t.reload.project_medias.count + end + + test "should get team articles" do + @t.set_explainers_enabled = true + @t.save! + ex = create_explainer team: @t + tag = create_tag annotated: ex + authenticate_with_user(@u) + query = "query { team(slug: \"#{@t.slug}\") { get_explainers_enabled, articles(article_type: \"explainer\") { edges { node { ... on Explainer { dbid, tags { edges { node { dbid } } } } } } } } }" + post :create, params: { query: query, team: @t.slug } + team = JSON.parse(@response.body)['data']['team'] + assert team['get_explainers_enabled'] + data = team['articles']['edges'] + assert_equal [ex.id], data.collect{ |edge| edge['node']['dbid'] } + tags = data[0]['node']['tags']['edges'] + assert_equal [tag.id.to_s], tags.collect{ |edge| edge['node']['dbid'] } + assert_response :success + end + + test "should create api key" do + t = create_team + u = create_user + create_team_user user: u, team: t, role: 'admin' + authenticate_with_user(u) + query = 'mutation create { createApiKey(input: { title: "test-api-key", description: "This is a test api key" }) { api_key { id title description } } }' + + assert_difference 'ApiKey.count' do + post :create, params: { query: query, team: t } + end + end + + test "should get all api keys in a team" do + t = create_team + u = create_user + create_team_user user: u, team: t, role: 'admin' + authenticate_with_user(u) + + api_key_1 = create_api_key(team: t) + api_key_2 = create_api_key(team: t) + + query = 'query read { team { api_keys { edges { node { dbid, title, description } } } } }' + post :create, params: { query: query, team: t } + assert_response :success + edges = JSON.parse(@response.body)['data']['team']['api_keys']['edges'] + assert_equal [api_key_1.title, api_key_2.title].sort, edges.collect{ |e| e['node']['title'] }.sort + end + + test "should get api key in a team by id" do + t = create_team + u = create_user + create_team_user user: u, team: t, role: 'admin' + authenticate_with_user(u) + + a = create_api_key(team: t) + + query = "query { team { api_key(dbid: #{a.id}) { dbid } } }" + post :create, params: { query: query, team: t.slug } + assert_response :success + response = JSON.parse(@response.body).dig('data', 'team', 'api_key') + assert_equal 1, response.size + assert_equal a.id, response.dig('dbid') + end + + test "should delete api key" do + u = create_user + t = create_team + create_team_user user: u, team: t, role: 'admin' + authenticate_with_user(u) + + a = create_api_key(team: t) + query = 'mutation destroy { destroyApiKey(input: { id: "' + a.id.to_s + '" }) { deletedId } }' + post :create, params: { query: query } + assert_response :success + response = JSON.parse(@response.body).dig('data', 'destroyApiKey') + assert_equal a.id.to_s, response.dig('deletedId') + end + + test "should log all graphql activity" do + u = create_user + t = create_team + create_team_user user: u, team: t, role: 'admin' + authenticate_with_user(u) + + query = 'query me { me { name } }' + + expected_message = "[Graphql] Logging activity: uid: #{u.id} user_name: #{u.name} team: #{t.name} role: admin" + mock_logger = mock() + mock_logger.expects(:info).with(expected_message) + + Rails.stubs(:logger).returns(mock_logger) + post :create, params: { query: query } + end end diff --git a/test/controllers/graphql_controller_2_test.rb b/test/controllers/graphql_controller_2_test.rb index 294139c421..9a47232a83 100644 --- a/test/controllers/graphql_controller_2_test.rb +++ b/test/controllers/graphql_controller_2_test.rb @@ -144,7 +144,7 @@ def setup test "should get cached values for list columns" do RequestStore.store[:skip_cached_field_update] = false t = create_team - t.set_list_columns = ["updated_at_timestamp", "last_seen", "demand", "share_count", "folder", "linked_items_count", "suggestions_count", "type_of_media", "status", "created_at_timestamp", "report_status", "tags_as_sentence", "media_published_at", "comment_count", "reaction_count", "related_count"] + t.set_list_columns = ["updated_at_timestamp", "last_seen", "demand", "share_count", "folder", "linked_items_count", "suggestions_count", "type_of_media", "status", "created_at_timestamp", "report_status", "tags_as_sentence", "media_published_at", "comment_count", "reaction_count", "related_count", "positive_tipline_search_results_count", "negative_tipline_search_results_count"] t.save! 5.times { create_project_media team: t } u = create_user is_admin: true diff --git a/test/controllers/graphql_controller_3_test.rb b/test/controllers/graphql_controller_3_test.rb index b037ee8a40..5b22bc464d 100644 --- a/test/controllers/graphql_controller_3_test.rb +++ b/test/controllers/graphql_controller_3_test.rb @@ -350,7 +350,7 @@ def setup create_tipline_request team_id: t.id, associated: pm, smooch_data: { 'authorId' => random_string } create_tipline_request team_id: t.id, associated: pm2, smooch_data: { 'authorId' => random_string } r = create_relationship source_id: pm.id, target_id: pm2.id, relationship_type: Relationship.confirmed_type - query = "query { project_media(ids: \"#{pm.id}\") { requests(first: 10) { edges { node { dbid } } } } }" + query = "query { project_media(ids: \"#{pm.id}\") { requests(first: 10, includeChildren: true) { edges { node { dbid } } } } }" post :create, params: { query: query, team: t.slug } assert_response :success data = JSON.parse(@response.body)['data']['project_media']['requests']['edges'] diff --git a/test/controllers/graphql_controller_5_test.rb b/test/controllers/graphql_controller_5_test.rb index 2b1b7c68be..8264680ecb 100644 --- a/test/controllers/graphql_controller_5_test.rb +++ b/test/controllers/graphql_controller_5_test.rb @@ -39,14 +39,14 @@ def setup p = create_project team: t pm = create_project_media project: p, media: create_uploaded_audio(file: 'rails.mp3') pm2 = create_project_media project: p - Bot::Alegre.stubs(:get_items_with_similar_media).returns({ pm2.id => 0.9, pm.id => 0.8 }) + Bot::Alegre.stubs(:get_items_with_similar_media_v2).returns({ pm2.id => 0.9, pm.id => 0.8 }) query = 'query { project_media(ids: "' + [pm.id, p.id, t.id].join(',') + '") { similar_items(first: 10000) { edges { node { dbid } } } } }' post :create, params: { query: query, team: t.slug } assert_response :success assert_equal pm2.id, JSON.parse(@response.body)['data']['project_media']['similar_items']['edges'][0]['node']['dbid'] - Bot::Alegre.unstub(:get_items_with_similar_media) + Bot::Alegre.unstub(:get_items_with_similar_media_v2) end test "should find similar items to text item" do diff --git a/test/controllers/graphql_controller_6_test.rb b/test/controllers/graphql_controller_6_test.rb index 54c94a287e..6c752783a3 100644 --- a/test/controllers/graphql_controller_6_test.rb +++ b/test/controllers/graphql_controller_6_test.rb @@ -108,7 +108,7 @@ def teardown pm = create_project_media team: t - Bot::Alegre.stubs(:get_items_with_similar_media).returns({ pm.id => 0.8 }) + Bot::Alegre.stubs(:get_items_with_similar_media_v2).returns({ pm.id => 0.8 }) path = File.join(Rails.root, 'test', 'data', 'rails.png') file = Rack::Test::UploadedFile.new(path, 'image/png') query = 'query CheckSearch { search(query: "{\"file_type\":\"image\"}") { medias(first: 20) { edges { node { dbid } } } } }' @@ -116,7 +116,7 @@ def teardown assert_response :success assert_equal 1, JSON.parse(@response.body)['data']['search']['medias']['edges'].size assert_equal pm.id, JSON.parse(@response.body)['data']['search']['medias']['edges'][0]['node']['dbid'] - Bot::Alegre.unstub(:get_items_with_similar_media) + Bot::Alegre.unstub(:get_items_with_similar_media_v2) end test "should search by similar image on ES" do @@ -131,7 +131,7 @@ def teardown pm2 = create_project_media team: t, media: m2 sleep 2 - Bot::Alegre.stubs(:get_items_with_similar_media).returns({ pm.id => 0.8 }) + Bot::Alegre.stubs(:get_items_with_similar_media_v2).returns({ pm.id => 0.8 }) path = File.join(Rails.root, 'test', 'data', 'rails.png') file = Rack::Test::UploadedFile.new(path, 'image/png') query = 'query CheckSearch { search(query: "{\"keyword\":\"Test\",\"file_type\":\"image\"}") { medias(first: 20) { edges { node { dbid } } } } }' @@ -139,7 +139,7 @@ def teardown assert_response :success assert_equal 1, JSON.parse(@response.body)['data']['search']['medias']['edges'].size assert_equal pm.id, JSON.parse(@response.body)['data']['search']['medias']['edges'][0]['node']['dbid'] - Bot::Alegre.unstub(:get_items_with_similar_media) + Bot::Alegre.unstub(:get_items_with_similar_media_v2) end test "should upload search file" do diff --git a/test/controllers/webhooks_controller_test.rb b/test/controllers/webhooks_controller_test.rb index 87142f294d..d17924cea0 100644 --- a/test/controllers/webhooks_controller_test.rb +++ b/test/controllers/webhooks_controller_test.rb @@ -234,22 +234,39 @@ def setup end test "should process Alegre webhook" do - CheckSentry.expects(:notify).never redis = Redis.new(REDIS_CONFIG) - redis.del('foo') + redis.del('alegre:webhook:foo') id = random_number payload = { 'action' => 'audio', 'data' => {'requested' => { 'id' => 'foo', 'context' => { 'project_media_id' => id } }} } assert_nil redis.lpop('alegre:webhook:foo') - post :index, params: { name: :alegre, token: CheckConfig.get('alegre_token') }.merge(payload) + post :index, params: { name: :alegre, token: CheckConfig.get('alegre_token') }, body: payload.to_json response = JSON.parse(redis.lpop('alegre:webhook:foo')) assert_equal 'foo', response.dig('data', 'requested', 'id') - expectation = {"action"=>"index", "data"=>{"requested"=>{"context"=>{"project_media_id"=>id.to_s}, "id"=>"foo"}}, "token"=>"test", "name"=>"alegre", "controller"=>"api/v1/webhooks"} + expectation = payload assert_equal expectation, response end + test "should process Alegre callback webhook with is_shortcircuited_search_result_callback" do + id = random_number + payload = { 'action' => 'audio', 'data' => {'is_shortcircuited_search_result_callback' => true, 'item' => { 'callback_url' => '/presto/receive/add_item', 'id' => id.to_s }} } + Bot::Alegre.stubs(:process_alegre_callback).returns({}) + post :index, params: { name: :alegre, token: CheckConfig.get('alegre_token') }, body: payload.to_json + assert_equal '200', response.code + assert_match /success/, response.body + end + + test "should process Alegre callback webhook with is_search_result_callback" do + id = random_number + payload = { 'action' => 'audio', 'data' => {'is_search_result_callback' => true, 'item' => { 'callback_url' => '/presto/receive/add_item', 'id' => id.to_s }} } + Bot::Alegre.stubs(:process_alegre_callback).returns({}) + post :index, params: { name: :alegre, token: CheckConfig.get('alegre_token') }, body: payload.to_json + assert_equal '200', response.code + assert_match /success/, response.body + end + test "should report error if can't process Alegre webhook" do CheckSentry.expects(:notify).once - post :index, params: { name: :alegre, token: CheckConfig.get('alegre_token') }.merge({ foo: 'bar' }) + post :index, params: { name: :alegre, token: CheckConfig.get('alegre_token') }, body: {foo: "bar"}.to_json end end diff --git a/test/lib/tasks/statistics_test.rb b/test/lib/tasks/statistics_test.rb index 69af62a958..10e09643d2 100644 --- a/test/lib/tasks/statistics_test.rb +++ b/test/lib/tasks/statistics_test.rb @@ -35,6 +35,32 @@ def teardown travel_back end + test "check:data:statistics generates statistics data for teams with no tipline" do + TeamBotInstallation.delete_all + + api_key = create_api_key + bot_user = create_bot_user api_key_id: api_key.id + bot_user.approve! + + non_tipline_team = create_team(slug: 'other-team') + + create_team_bot_installation(team_id: non_tipline_team.id, user_id: bot_user.id) + 3.times{|i| create_project_media(user: bot_user, claim: "Claim: other team #{i}", team: non_tipline_team, created_at: @current_date) } + + assert_equal 0, MonthlyTeamStatistic.where(team: non_tipline_team).count + + travel_to @current_date + + out, err = capture_io do + Rake::Task['check:data:statistics'].invoke + end + Rake::Task['check:data:statistics'].reenable + + assert err.blank? + + assert_equal 7, MonthlyTeamStatistic.where(team: non_tipline_team).count + end + test "check:data:statistics generates statistics data for every platform and language of tipline teams" do @tipline_team.set_languages(['en', 'es']) @tipline_team.save! diff --git a/test/models/api_key_test.rb b/test/models/api_key_test.rb index d2dbe21a2d..683b3e223b 100644 --- a/test/models/api_key_test.rb +++ b/test/models/api_key_test.rb @@ -8,11 +8,13 @@ class ApiKeyTest < ActiveSupport::TestCase end test "should generate expiration date" do - t = Time.parse('2015-01-01 09:00:00') - Time.stubs(:now).returns(t) - k = create_api_key - Time.unstub(:now) - assert_equal Time.parse('2015-01-31 09:00:00'), k.reload.expire_at + stub_configs({'api_default_expiry_days' => 90}) do + t = Time.parse('2015-01-01 09:00:00') + Time.stubs(:now).returns(t) + k = create_api_key + Time.unstub(:now) + assert_equal Time.parse('2015-04-01 09:00:00'), k.reload.expire_at + end end test "should generate access token" do @@ -44,4 +46,22 @@ class ApiKeyTest < ActiveSupport::TestCase b = create_bot_user api_key_id: a.id assert_equal b, a.reload.bot_user end + + test "should create bot user automatically when team is provided" do + t = create_team + a = create_api_key(team: t) + assert_not_nil a.bot_user + end + + test "should validate maximum number of api keys in a team" do + stub_configs({'max_team_api_keys' => 2}) do + t = create_team + 2.times do + create_api_key(team: t) + end + assert_raises ActiveRecord::RecordInvalid do + create_api_key(team: t) + end + end + end end diff --git a/test/models/bot/alegre_2_test.rb b/test/models/bot/alegre_2_test.rb index be8b3363f2..d54ce886b4 100644 --- a/test/models/bot/alegre_2_test.rb +++ b/test/models/bot/alegre_2_test.rb @@ -20,7 +20,7 @@ def setup hex = SecureRandom.hex SecureRandom.stubs(:hex).returns(hex) @media_path = random_url - @params = { url: "#{@media_path}?hash=#{hex}", context: { has_custom_id: true, team_id: @team.id }, threshold: 0.9, match_across_content_types: true } + @params = { url: "#{@media_path}?hash=#{hex}", context: { has_custom_id: true, team_id: @team.id, temporary_media: false}, threshold: 0.9, match_across_content_types: true } end def teardown @@ -32,69 +32,72 @@ def teardown pm1 = create_project_media team: @team, media: create_uploaded_video pm2 = create_project_media team: @team, media: create_uploaded_video pm3 = create_project_media team: @team, media: create_uploaded_video - Bot::Alegre.stubs(:request).with('post', '/video/similarity/search/', @params).returns({ - result: [ - { - context: [ - { team_id: @team.id.to_s, project_media_id: pm1.id.to_s } - ], - score: 0.971234, - filename: '/app/persistent_disk/blah/12342.tmk' - }, - { - context: [ - { team_id: @team.id.to_s, project_media_id: pm2.id.to_s } - ], - score: 0.983167, - filename: '/app/persistent_disk/blah/12343.tmk' - } - ] - }.with_indifferent_access) + params = {:doc_id => Bot::Alegre.item_doc_id(pm3), :context => {:team_id => pm3.team_id, :project_media_id => pm3.id, :has_custom_id => true, :temporary_media => false}, :url => @media_path} + Bot::Alegre.stubs(:request).with('post', '/similarity/async/video', params.merge({ threshold: 0.9, confirmed: false })).returns(true) + Bot::Alegre.stubs(:request).with('post', '/similarity/async/video', params.merge({ threshold: 0.9, confirmed: true })).returns(true) + Redis.any_instance.stubs(:get).returns({ + pm1.id => { + score: 0.971234, + context: { team_id: pm1.team_id, project_media_id: pm1.id, temporary: false }, + model: "video", + source_field: "video", + target_field: "video", + relationship_type: Relationship.confirmed_type + }, + pm2.id => { + score: 0.983167, + context: { team_id: pm2.team_id, project_media_id: pm2.id, temporary: false, content_type: 'video' }, + model: "video", + source_field: "video", + target_field: "video", + relationship_type: Relationship.confirmed_type + } + }.to_yaml) Bot::Alegre.stubs(:media_file_url).with(pm3).returns(@media_path) assert_difference 'Relationship.count' do Bot::Alegre.relate_project_media_to_similar_items(pm3) end - Bot::Alegre.unstub(:request) Bot::Alegre.unstub(:media_file_url) + Redis.any_instance.unstub(:get) r = Relationship.last assert_equal pm3, r.target assert_equal pm2, r.source assert_equal r.weight, 0.983167 + Bot::Alegre.unstub(:request) end test "should match similar audios" do pm1 = create_project_media team: @team, media: create_uploaded_audio pm2 = create_project_media team: @team, media: create_uploaded_audio pm3 = create_project_media team: @team, media: create_uploaded_audio - Bot::Alegre.stubs(:request).with('post', '/similarity/sync/audio', @params).returns({ - result: [ - { - id: 1, - doc_id: random_string, - chromaprint_fingerprint: [6581800, 2386744, 2583368, 2488648, 6343163, 14978026, 300191082, 309757210, 304525578, 304386106, 841261098, 841785386], - url: 'https://foo.com/bar.wav', - context: [ - { team_id: @team.id.to_s, project_media_id: pm1.id.to_s } - ], - score: 0.971234, - }, - { - id: 2, - doc_id: random_string, - chromaprint_fingerprint: [2386744, 2583368, 2488648, 6343163, 14978026, 300191082, 309757210, 304525578, 304386106, 841261098, 841785386, 858042410, 825593963, 823509230], - url: 'https://bar.com/foo.wav', - context: [ - { team_id: @team.id.to_s, project_media_id: pm2.id.to_s } - ], - score: 0.983167, - } - ] - }.with_indifferent_access) + params = {:doc_id => Bot::Alegre.item_doc_id(pm3), :context => {:team_id => pm3.team_id, :project_media_id => pm3.id, :has_custom_id => true, :temporary_media => false}, :url => @media_path} + Bot::Alegre.stubs(:request).with('post', '/similarity/async/audio', params.merge({ threshold: 0.9, confirmed: false })).returns(true) + Bot::Alegre.stubs(:request).with('post', '/similarity/async/audio', params.merge({ threshold: 0.9, confirmed: true })).returns(true) + + Redis.any_instance.stubs(:get).returns({ + pm1.id => { + score: 0.971234, + context: { team_id: pm1.team_id, project_media_id: pm1.id, temporary: false }, + model: "audio", + source_field: "audio", + target_field: "video", + relationship_type: Relationship.confirmed_type + }, + pm2.id => { + score: 0.983167, + context: { team_id: pm2.team_id, project_media_id: pm2.id, temporary: false, content_type: 'video' }, + model: "audio", + source_field: "audio", + target_field: "audio", + relationship_type: Relationship.confirmed_type + } + }.to_yaml) Bot::Alegre.stubs(:media_file_url).with(pm3).returns(@media_path) assert_difference 'Relationship.count' do Bot::Alegre.relate_project_media_to_similar_items(pm3) end Bot::Alegre.unstub(:media_file_url) + Redis.any_instance.unstub(:get) r = Relationship.last assert_equal pm3, r.target assert_equal pm2, r.source @@ -107,7 +110,9 @@ def teardown pm1 = create_project_media team: @team, media: create_uploaded_video pm2 = create_project_media team: @team, media: create_uploaded_audio pm3 = create_project_media team: @team, media: create_uploaded_audio - Bot::Alegre.stubs(:request).with('post', '/similarity/sync/audio', @params).returns({ + request_params = {:doc_id=>Bot::Alegre.item_doc_id(pm3), :context=>{:team_id=>pm3.team_id, :project_media_id=>pm3.id, :has_custom_id=>true, :temporary_media=>false}, :url=>@media_path, :threshold=>0.9, :confirmed=>true} + request_params_unconfirmed = {:doc_id=>Bot::Alegre.item_doc_id(pm3), :context=>{:team_id=>pm3.team_id, :project_media_id=>pm3.id, :has_custom_id=>true, :temporary_media=>false}, :url=>@media_path, :threshold=>0.9, :confirmed=>false} + Bot::Alegre.stubs(:request).with('post', '/similarity/async/audio', request_params).returns({ result: [ { id: 2, @@ -131,12 +136,55 @@ def teardown } ] }.with_indifferent_access) + Bot::Alegre.stubs(:request).with('post', '/similarity/async/audio', request_params_unconfirmed).returns({ + result: [ + { + id: 2, + doc_id: random_string, + hash_value: '0111', + url: 'https://foo.com/baz.mp4', + context: [ + { team_id: @team.id.to_s, project_media_id: pm1.id.to_s } + ], + score: 0.971234, + }, + { + id: 1, + doc_id: random_string, + hash_value: '0101', + url: 'https://foo.com/bar.mp4', + context: [ + { team_id: @team.id.to_s, project_media_id: pm2.id.to_s, content_type: 'video' } + ], + score: 0.983167, + } + ] + }.with_indifferent_access) + Redis.any_instance.stubs(:get).returns({ + pm1.id => { + score: 0.971234, + context: { team_id: pm1.team_id, project_media_id: pm1.id, temporary: false }, + model: "audio", + source_field: "audio", + target_field: "video", + relationship_type: Relationship.confirmed_type + }, + pm2.id => { + score: 0.983167, + context: { team_id: pm2.team_id, project_media_id: pm2.id, temporary: false, content_type: 'video' }, + model: "audio", + source_field: "audio", + target_field: "audio", + relationship_type: Relationship.confirmed_type + } + }.to_yaml) Bot::Alegre.stubs(:media_file_url).with(pm3).returns(@media_path) assert_difference 'Relationship.count' do Bot::Alegre.relate_project_media_to_similar_items(pm3) end Bot::Alegre.unstub(:request) Bot::Alegre.unstub(:media_file_url) + Redis.any_instance.unstub(:get) r = Relationship.last assert_equal pm3, r.target assert_equal pm2, r.source @@ -173,14 +221,34 @@ def teardown } ] }.with_indifferent_access - Bot::Alegre.stubs(:request).with('post', '/similarity/sync/image', @params.merge({ threshold: 0.89 })).returns(result) - Bot::Alegre.stubs(:request).with('post', '/similarity/sync/image', @params.merge({ threshold: 0.95 })).returns(result) + params = {:doc_id => Bot::Alegre.item_doc_id(pm3), :context => {:team_id => pm3.team_id, :project_media_id => pm3.id, :has_custom_id => true, :temporary_media => false}, :url => @media_path} + Bot::Alegre.stubs(:request).with('post', '/similarity/async/image', params.merge({ threshold: 0.89, confirmed: false })).returns(result) + Bot::Alegre.stubs(:request).with('post', '/similarity/async/image', params.merge({ threshold: 0.95, confirmed: true })).returns(result) Bot::Alegre.stubs(:media_file_url).with(pm3).returns(@media_path) + Redis.any_instance.stubs(:get).returns({ + pm1.id => { + score: 0.5, + context: { team_id: pm1.team_id, project_media_id: pm1.id, temporary: false }, + model: "image", + source_field: "image", + target_field: "image", + relationship_type: Relationship.suggested_type + }, + pm2.id => { + score: 0.9, + context: { team_id: pm2.team_id, project_media_id: pm2.id, temporary: false}, + model: "image", + source_field: "image", + target_field: "image", + relationship_type: Relationship.confirmed_type + } + }.to_yaml) assert_difference 'Relationship.count' do Bot::Alegre.relate_project_media_to_similar_items(pm3) end Bot::Alegre.unstub(:request) Bot::Alegre.unstub(:media_file_url) + Redis.any_instance.unstub(:get) r = Relationship.last assert_equal pm3, r.target assert_equal pm2, r.source @@ -195,41 +263,41 @@ def teardown pm3 = create_project_media team: t3, media: create_uploaded_image pm4 = create_project_media media: create_uploaded_image pm1b = create_project_media team: @team, media: create_uploaded_image - response = { - result: [ - { - id: pm4.id, - sha256: random_string, - phash: random_string, - url: random_url, - context: [ - { - team_id: t2.id, - has_custom_id: true, - project_media_id: pm2.id - }, - { - team_id: @team.id, - has_custom_id: true, - project_media_id: pm1b.id - }, - { - team_id: t3.id, - has_custom_id: true, - project_media_id: pm3.id - }, - ], - score: 0 - } - ] - }.with_indifferent_access + Redis.any_instance.stubs(:get).returns({ + pm1b.id => { + score: 0, + context: [ + { + team_id: t2.id, + has_custom_id: true, + project_media_id: pm2.id + }, + { + team_id: @team.id, + has_custom_id: true, + project_media_id: pm1b.id + }, + { + team_id: t3.id, + has_custom_id: true, + project_media_id: pm3.id + }, + ], + model: "image", + source_field: "image", + target_field: "image", + relationship_type: Relationship.suggested_type + } + }.to_yaml) + params = {:doc_id => Bot::Alegre.item_doc_id(pm1a), :context => {:team_id => pm1a.team_id, :project_media_id => pm1a.id, :has_custom_id => true, :temporary_media => false}, :url => @media_path} Bot::Alegre.stubs(:media_file_url).with(pm1a).returns(@media_path) - Bot::Alegre.stubs(:request).with('post', '/similarity/sync/image', @params.merge({ threshold: 0.89 })).returns(response) - Bot::Alegre.stubs(:request).with('post', '/similarity/sync/image', @params.merge({ threshold: 0.95 })).returns(response) + Bot::Alegre.stubs(:request).with('post', '/similarity/async/image', params.merge({ threshold: 0.89, confirmed: false })).returns(true) + Bot::Alegre.stubs(:request).with('post', '/similarity/async/image', params.merge({ threshold: 0.95, confirmed: true })).returns(true) assert_difference 'Relationship.count' do Bot::Alegre.relate_project_media_to_similar_items(pm1a) end Bot::Alegre.unstub(:request) + Redis.any_instance.unstub(:get) assert_equal pm1b, Relationship.last.source assert_equal pm1a, Relationship.last.target end @@ -246,6 +314,7 @@ def teardown WebMock.stub_request(:post, 'http://alegre.test/text/langid/').to_return(body: { 'result' => { 'language' => 'es' }}.to_json) WebMock.stub_request(:post, 'http://alegre.test/text/similarity/').to_return(body: 'success') WebMock.stub_request(:delete, 'http://alegre.test/text/similarity/').to_return(body: { success: true }.to_json) + WebMock.stub_request(:delete, 'http://alegre.test/image/similarity/').to_return(body: { success: true }.to_json) WebMock.stub_request(:post, 'http://alegre.test/text/similarity/search/').to_return(body: { success: true }.to_json) WebMock.stub_request(:post, 'http://alegre.test/image/ocr/').to_return(body: { text: 'Foo bar' }.to_json) WebMock.stub_request(:post, 'http://alegre.test/similarity/sync/image').to_return(body: { @@ -260,6 +329,31 @@ def teardown } ] }.to_json) + response = { + "message": "Message pushed successfully", + "queue": "image__Model", + "body": { + "callback_url": "http:\/\/alegre:3100\/presto\/receive\/add_item\/image", + "id": "f0d43d29-853d-4099-9e92-073203afa75b", + "url": image_path, + "text": nil, + "raw": { + "limit": 200, + "url": image_path, + "callback_url": "http:\/\/example.com\/search_results", + "doc_id": random_string, + "context": { team_id: t.id }, + "created_at": "2023-10-27T22:40:14.205586", + "command": "search", + "threshold": 0.0, + "per_model_threshold": {}, + "match_across_content_types": false, + "requires_callback": true, + "final_task": "search" + } + } + } + WebMock.stub_request(:post, 'http://alegre.test/similarity/async/image').to_return(body: response.to_json) # Flags Bot::Alegre.unstub(:media_file_url) @@ -279,20 +373,19 @@ def teardown assert Bot::Alegre.run({ data: { dbid: pm1.id }, event: 'create_project_media' }) pm2 = create_project_media team: t, media: create_uploaded_image - WebMock.stub_request(:post, 'http://alegre.test/similarity/sync/image').to_return(body: { - result: [ - { - id: pm1.id, - sha256: '1782b1d1993fcd9f6fd8155adc6009a9693a8da7bb96d20270c4bc8a30c97570', - phash: '17399941807326929', - url: image_path, - context: { team_id: t.id, project_media_id: pm1.id }, - score: 0.8 - } - ] - }.to_json) - response = { pm1.id => { score: 0.8, context: { team_id: t.id, project_media_id: pm1.id, contexts_count: 1, field: ''}, model: nil, source_field: 'image', target_field: 'image' } } + Redis.any_instance.stubs(:get).returns({ + pm1.id => { + score: 0.8, + context: { team_id: t.id, project_media_id: pm1.id, temporary: false }, + model: "image", + source_field: "image", + target_field: "image", + relationship_type: Relationship.suggested_type + } + }.to_yaml) + response = {pm1.id.to_s=>{"score"=>0.8, "context"=>[{"team_id"=>t.id, "project_media_id"=>pm1.id, "temporary"=>false}], "model"=>"image", "source_field"=>"image", "target_field"=>"image", "relationship_type"=>{"source"=>"confirmed_sibling", "target"=>"confirmed_sibling"}}} assert_equal response.to_json, Bot::Alegre.get_items_with_similarity('image', pm2, Bot::Alegre.get_threshold_for_query('image', pm2)).to_json + Redis.any_instance.unstub(:get) end end diff --git a/test/models/bot/alegre_3_test.rb b/test/models/bot/alegre_3_test.rb index 319c3cc77e..f4304d1ddc 100644 --- a/test/models/bot/alegre_3_test.rb +++ b/test/models/bot/alegre_3_test.rb @@ -91,7 +91,10 @@ def teardown Bot::Alegre.stubs(:media_file_url).returns(media_file_url) pm1 = create_project_media team: @pm.team, media: create_uploaded_audio(file: 'rails.mp3') - WebMock.stub_request(:post, "http://alegre/similarity/sync/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>media_file_url, :threshold=>0.9}).to_return(body: { + WebMock.stub_request(:post, "http://alegre/similarity/async/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>media_file_url, :threshold=>0.9, :confirmed=> false}).to_return(body: { + "result": [] + }.to_json) + WebMock.stub_request(:post, "http://alegre/similarity/async/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>media_file_url, :threshold=>0.9, :confirmed=> true}).to_return(body: { "result": [] }.to_json) WebMock.stub_request(:post, 'http://alegre/audio/transcription/result/').with(body: {job_name: "0c481e87f2774b1bd41a0a70d9b70d11"}).to_return(body: { 'job_status' => 'DONE' }.to_json) @@ -157,7 +160,10 @@ def teardown Bot::Alegre.stubs(:media_file_url).returns(media_file_url) pm1 = create_project_media team: @pm.team, media: create_uploaded_audio(file: 'rails.mp3') - WebMock.stub_request(:post, "http://alegre/similarity/sync/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>media_file_url, :threshold=>0.9}).to_return(body: { + WebMock.stub_request(:post, "http://alegre/similarity/async/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>media_file_url, :threshold=>0.9, :confirmed=>true}).to_return(body: { + "result": [] + }.to_json) + WebMock.stub_request(:post, "http://alegre/similarity/async/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>media_file_url, :threshold=>0.9, :confirmed=>false}).to_return(body: { "result": [] }.to_json) WebMock.stub_request(:post, 'http://alegre/audio/transcription/').with({ @@ -226,22 +232,12 @@ def teardown end end - def self.extract_project_medias_from_context(search_result) - # We currently have two cases of context: - # - a straight hash with project_media_id - # - an array of hashes, each with project_media_id - context = search_result.dig('_source', 'context') - pms = [] - if context.kind_of?(Array) - context.each{ |c| pms.push(c.with_indifferent_access.dig('project_media_id')) } - elsif context.kind_of?(Hash) - pms.push(context.with_indifferent_access.dig('project_media_id')) - end - Hash[pms.flatten.collect{|pm| [pm.to_i, search_result.with_indifferent_access.dig('_score')]}] + test "should extract project medias from context as dict" do + assert_equal Bot::Alegre.extract_project_medias_from_context({"_score" => 2, "_source" => {"context" => {"project_media_id" => 1}}}), {1=>{:score=>2, :context=>{"project_media_id"=>1}, :model=>nil}} end - test "should extract project medias from context" do - assert_equal Bot::Alegre.extract_project_medias_from_context({"_score" => 2, "_source" => {"context" => {"project_media_id" => 1}}}), {1=>{:score=>2, :context=>{"project_media_id"=>1}, :model=>nil}} + test "should extract project medias from context as array" do + assert_equal Bot::Alegre.extract_project_medias_from_context({"_score" => 2, "_source" => {"context" => [{"project_media_id" => 1}]}}), {1=>{:score=>2, :context=>[{"project_media_id"=>1}], :model=>nil}} end test "should update on alegre" do @@ -360,9 +356,9 @@ def self.extract_project_medias_from_context(search_result) pm1.save! pm2 = create_project_media project: p, quote: "Blah2", team: @team pm2.save! - Bot::Alegre.stubs(:get_merged_items_with_similar_text).with(pm2, Bot::Alegre.get_threshold_for_query('text', pm2)).returns({pm1.id => {score: 0.99, context: {"blah" => 1}}}) + Bot::Alegre.stubs(:get_merged_items_with_similar_text).with(pm2, Bot::Alegre.get_threshold_for_query('text', pm2)).returns({pm1.id => {score: 0.99, context: {"team_id" => pm1.team_id, "blah" => 1}}}) Bot::Alegre.stubs(:get_merged_items_with_similar_text).with(pm2, Bot::Alegre.get_threshold_for_query('text', pm2, true)).returns({}) - assert_equal Bot::Alegre.get_similar_items(pm2), {pm1.id=>{:score=>0.99, :context => {"blah" => 1}, :relationship_type=>{:source=>"suggested_sibling", :target=>"suggested_sibling"}}} + assert_equal Bot::Alegre.get_similar_items(pm2), {pm1.id=>{:score=>0.99, :context => [{"team_id" => pm1.team_id, "blah" => 1}], :relationship_type=>{:source=>"suggested_sibling", :target=>"suggested_sibling"}}} Bot::Alegre.unstub(:get_merged_items_with_similar_text) end diff --git a/test/models/bot/alegre_test.rb b/test/models/bot/alegre_test.rb index 732166a51c..bea721a37e 100644 --- a/test/models/bot/alegre_test.rb +++ b/test/models/bot/alegre_test.rb @@ -101,6 +101,7 @@ def teardown end test "should unarchive item after running" do + WebMock.stub_request(:delete, 'http://alegre/text/similarity/').to_return(body: {success: true}.to_json) stub_configs({ 'alegre_host' => 'http://alegre', 'alegre_token' => 'test' }) do WebMock.stub_request(:delete, 'http://alegre/text/similarity/').to_return(status: 200, body: '{}') pm = create_project_media diff --git a/test/models/bot/alegre_v2_test.rb b/test/models/bot/alegre_v2_test.rb index a79d740f2f..07c119917f 100644 --- a/test/models/bot/alegre_v2_test.rb +++ b/test/models/bot/alegre_v2_test.rb @@ -25,6 +25,24 @@ def teardown super end + test "should tap-test all TemporaryProjectMedia variations" do + media_type_map = { + "claim" => "Claim", + "link" => "Link", + "image" => "UploadedImage", + "video" => "UploadedVideo", + "audio" => "UploadedAudio", + } + media_type_map.each do |k,v| + tpm = TemporaryProjectMedia.new + tpm.type = k + assert_equal tpm.media.type, v + [:is_blank?, :is_link?, :is_text?, :is_image?, :is_video?, :is_audio?].each do |meth| + assert_equal [true, false].include?(tpm.send(meth)), true + end + end + end + test "should generate media file url" do pm1 = create_project_media team: @team, media: create_uploaded_audio assert_equal Bot::Alegre.media_file_url(pm1).class, String @@ -64,6 +82,14 @@ def teardown assert_equal Bot::Alegre.delete_path(pm1), "/image/similarity/" end + test "should have host and paths for video" do + pm1 = create_project_media team: @team, media: create_uploaded_video + assert_equal Bot::Alegre.host, CheckConfig.get('alegre_host') + assert_equal Bot::Alegre.sync_path(pm1), "/similarity/sync/video" + assert_equal Bot::Alegre.async_path(pm1), "/similarity/async/video" + assert_equal Bot::Alegre.delete_path(pm1), "/video/similarity/" + end + test "should release and reconnect db" do RequestStore.store[:pause_database_connection] = true assert_equal Bot::Alegre.release_db.class, Thread::ConditionVariable @@ -73,43 +99,60 @@ def teardown test "should create a generic_package for audio" do pm1 = create_project_media team: @team, media: create_uploaded_audio - assert_equal Bot::Alegre.generic_package(pm1, "audio"), {:doc_id=>Bot::Alegre.item_doc_id(pm1, "audio"), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}} + assert_equal Bot::Alegre.generic_package(pm1, "audio"), {:doc_id=>Bot::Alegre.item_doc_id(pm1, "audio"), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}} end test "should create a generic_package for image" do pm1 = create_project_media team: @team, media: create_uploaded_image - assert_equal Bot::Alegre.generic_package(pm1, "image"), {:doc_id=>Bot::Alegre.item_doc_id(pm1, "image"), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}} + assert_equal Bot::Alegre.generic_package(pm1, "image"), {:doc_id=>Bot::Alegre.item_doc_id(pm1, "image"), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}} + end + + test "should create a generic_package for video" do + pm1 = create_project_media team: @team, media: create_uploaded_video + assert_equal Bot::Alegre.generic_package(pm1, "video"), {:doc_id=>Bot::Alegre.item_doc_id(pm1, "video"), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}} end test "should create a generic_package_audio" do pm1 = create_project_media team: @team, media: create_uploaded_audio - assert_equal Bot::Alegre.generic_package_audio(pm1, {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>Bot::Alegre.media_file_url(pm1)} - assert_equal Bot::Alegre.store_package_audio(pm1, "audio", {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>Bot::Alegre.media_file_url(pm1)} - assert_equal Bot::Alegre.store_package(pm1, "audio", {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>Bot::Alegre.media_file_url(pm1)} + assert_equal Bot::Alegre.generic_package_audio(pm1, {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1)} + assert_equal Bot::Alegre.store_package_audio(pm1, "audio", {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1)} + assert_equal Bot::Alegre.store_package(pm1, "audio", {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1)} end test "should create a generic_package_image" do pm1 = create_project_media team: @team, media: create_uploaded_image - assert_equal Bot::Alegre.generic_package_image(pm1, {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>Bot::Alegre.media_file_url(pm1)} - assert_equal Bot::Alegre.store_package_image(pm1, "image", {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>Bot::Alegre.media_file_url(pm1)} - assert_equal Bot::Alegre.store_package(pm1, "image", {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>Bot::Alegre.media_file_url(pm1)} + assert_equal Bot::Alegre.generic_package_image(pm1, {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1)} + assert_equal Bot::Alegre.store_package_image(pm1, "image", {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1)} + assert_equal Bot::Alegre.store_package(pm1, "image", {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1)} + end + + test "should create a generic_package_video" do + pm1 = create_project_media team: @team, media: create_uploaded_video + assert_equal Bot::Alegre.generic_package_image(pm1, {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1)} + assert_equal Bot::Alegre.store_package_image(pm1, "video", {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1)} + assert_equal Bot::Alegre.store_package(pm1, "video", {}), {:doc_id=>Bot::Alegre.item_doc_id(pm1, nil), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1)} end test "should create a context for audio" do pm1 = create_project_media team: @team, media: create_uploaded_audio - assert_equal Bot::Alegre.get_context(pm1, "audio"), {:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true} + assert_equal Bot::Alegre.get_context(pm1, "audio"), {:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false} end test "should create a context for image" do pm1 = create_project_media team: @team, media: create_uploaded_image - assert_equal Bot::Alegre.get_context(pm1, "image"), {:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true} + assert_equal Bot::Alegre.get_context(pm1, "image"), {:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false} + end + + test "should create a context for video" do + pm1 = create_project_media team: @team, media: create_uploaded_video + assert_equal Bot::Alegre.get_context(pm1, "video"), {:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false} end test "should create a delete_package for audio" do pm1 = create_project_media team: @team, media: create_uploaded_audio package = Bot::Alegre.delete_package(pm1, "audio") assert_equal package[:doc_id], Bot::Alegre.item_doc_id(pm1, nil) - assert_equal package[:context], {:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true} + assert_equal package[:context], {:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false} assert_equal package[:url].class, String assert_equal package[:quiet], false end @@ -118,7 +161,16 @@ def teardown pm1 = create_project_media team: @team, media: create_uploaded_image package = Bot::Alegre.delete_package(pm1, "image") assert_equal package[:doc_id], Bot::Alegre.item_doc_id(pm1, nil) - assert_equal package[:context], {:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true} + assert_equal package[:context], {:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false} + assert_equal package[:url].class, String + assert_equal package[:quiet], false + end + + test "should create a delete_package for video" do + pm1 = create_project_media team: @team, media: create_uploaded_video + package = Bot::Alegre.delete_package(pm1, "video") + assert_equal package[:doc_id], Bot::Alegre.item_doc_id(pm1, nil) + assert_equal package[:context], {:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false} assert_equal package[:url].class, String assert_equal package[:quiet], false end @@ -149,7 +201,7 @@ def teardown } } } - WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/async/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>Bot::Alegre.media_file_url(pm1)}).to_return(body: response.to_json) + WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/async/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1)}).to_return(body: response.to_json) assert_equal JSON.parse(Bot::Alegre.get_async(pm1).to_json), JSON.parse(response.to_json) end @@ -163,6 +215,11 @@ def teardown assert_equal Bot::Alegre.isolate_relevant_context(pm1, {"context"=>[{"team_id"=>pm1.team_id}]}), {"team_id"=>pm1.team_id} end + test "should isolate relevant_context for video" do + pm1 = create_project_media team: @team, media: create_uploaded_video + assert_equal Bot::Alegre.isolate_relevant_context(pm1, {"context"=>[{"team_id"=>pm1.team_id}]}), {"team_id"=>pm1.team_id} + end + test "should return field or type on get_target_field for audio" do pm1 = create_project_media team: @team, media: create_uploaded_audio Bot::Alegre.stubs(:get_type).returns(nil) @@ -177,6 +234,13 @@ def teardown Bot::Alegre.unstub(:get_type) end + test "should return field or type on get_target_field for video" do + pm1 = create_project_media team: @team, media: create_uploaded_video + Bot::Alegre.stubs(:get_type).returns(nil) + assert_equal Bot::Alegre.get_target_field(pm1, "blah"), "blah" + Bot::Alegre.unstub(:get_type) + end + test "should generate per model threshold for text" do p = create_project team: @team pm1 = create_project_media project: p, quote: "testing short text", team: @team @@ -196,6 +260,12 @@ def teardown assert_equal Bot::Alegre.get_per_model_threshold(pm1, sample), {:threshold=>0.9} end + test "should generate per model threshold for video" do + pm1 = create_project_media team: @team, media: create_uploaded_video + sample = [{:value=>0.9, :key=>"video_hash_suggestion_threshold", :automatic=>false, :model=>"hash"}] + assert_equal Bot::Alegre.get_per_model_threshold(pm1, sample), {:threshold=>0.9} + end + test "should get target field for audio" do pm1 = create_project_media team: @team, media: create_uploaded_audio assert_equal Bot::Alegre.get_target_field(pm1, nil), "audio" @@ -206,6 +276,11 @@ def teardown assert_equal Bot::Alegre.get_target_field(pm1, nil), "image" end + test "should get target field for video" do + pm1 = create_project_media team: @team, media: create_uploaded_video + assert_equal Bot::Alegre.get_target_field(pm1, nil), "video" + end + test "should parse similarity results" do pm1 = create_project_media team: @team, media: create_uploaded_audio results = [ @@ -279,7 +354,7 @@ def teardown } ] } - WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/sync/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>Bot::Alegre.media_file_url(pm1)}).to_return(body: response.to_json) + WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/sync/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1)}).to_return(body: response.to_json) assert_equal JSON.parse(Bot::Alegre.get_sync(pm1).to_json), JSON.parse(response.to_json) end @@ -291,6 +366,14 @@ def teardown assert_equal expected, actual end + test "should safe_get_async" do + pm1 = create_project_media team: @team, media: create_uploaded_audio + WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/async/audio").to_return(body: 'null') + expected = nil + actual = Bot::Alegre.safe_get_async(pm1, "audio", {}) + assert_equal expected, actual + end + test "should run delete request" do pm1 = create_project_media team: @team, media: create_uploaded_audio response = {"requested"=> @@ -387,7 +470,7 @@ def teardown } ] } - WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/sync/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>Bot::Alegre.media_file_url(pm1), :threshold=>0.9}).to_return(body: response.to_json) + WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/sync/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1), :threshold=>0.9}).to_return(body: response.to_json) assert_equal Bot::Alegre.get_items(pm1, nil), {(pm1.id+1)=>{:score=>1.0, :context=>{"team_id"=>pm1.team_id, "has_custom_id"=>true, "project_media_id"=>(pm1.id+1)}, :model=>"audio", :source_field=>"audio", :target_field=>"audio", :relationship_type=>Relationship.suggested_type}} end @@ -425,6 +508,7 @@ def teardown "team_id": pm1.team_id, "project_media_id": pm1.id, "has_custom_id": true, + "temporary_media": false, } ], "score": 1.0, @@ -460,6 +544,7 @@ def teardown "team_id": pm1.team_id, "project_media_id": pm1.id+1, "has_custom_id": true, + "temporary_media": false, } ], "score": 0.91, @@ -467,11 +552,11 @@ def teardown } ] } - WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/sync/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>Bot::Alegre.media_file_url(pm1), :threshold=>0.9}).to_return(body: response.to_json) - assert_equal Bot::Alegre.get_items(pm1, nil), {(pm1.id+1)=>{:score=>0.91, :context=>{"team_id"=>pm1.team_id, "has_custom_id"=>true, "project_media_id"=>(pm1.id+1)}, :model=>"audio", :source_field=>"audio", :target_field=>"audio", :relationship_type=>Relationship.suggested_type}} + WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/sync/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1), :threshold=>0.9}).to_return(body: response.to_json) + assert_equal Bot::Alegre.get_items(pm1, nil), {(pm1.id+1)=>{:score=>0.91, :context=>{"team_id"=>pm1.team_id, "has_custom_id"=>true, "project_media_id"=>(pm1.id+1), "temporary_media"=>false}, :model=>"audio", :source_field=>"audio", :target_field=>"audio", :relationship_type=>Relationship.suggested_type}} end - test "should get_confirmed_items zzz" do + test "should get_confirmed_items" do pm1 = create_project_media team: @team, media: create_uploaded_audio response = { "result": [ @@ -505,6 +590,7 @@ def teardown "team_id": pm1.team_id, "project_media_id": pm1.id, "has_custom_id": true, + "temporary_media": false, } ], "score": 1.0, @@ -540,6 +626,7 @@ def teardown "team_id": pm1.team_id, "project_media_id": pm1.id+1, "has_custom_id": true, + "temporary_media": false, } ], "score": 0.91, @@ -547,8 +634,51 @@ def teardown } ] } - WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/sync/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>Bot::Alegre.media_file_url(pm1), :threshold=>0.9}).to_return(body: response.to_json) - assert_equal Bot::Alegre.get_confirmed_items(pm1, nil), {(pm1.id+1)=>{:score=>0.91, :context=>{"team_id"=>pm1.team_id, "has_custom_id"=>true, "project_media_id"=>(pm1.id+1)}, :model=>"audio", :source_field=>"audio", :target_field=>"audio", :relationship_type=>Relationship.confirmed_type}} + WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/sync/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1), :threshold=>0.9}).to_return(body: response.to_json) + assert_equal Bot::Alegre.get_confirmed_items(pm1, nil), {(pm1.id+1)=>{:score=>0.91, :context=>{"team_id"=>pm1.team_id, "has_custom_id"=>true, "project_media_id"=>(pm1.id+1), "temporary_media"=>false}, :model=>"audio", :source_field=>"audio", :target_field=>"audio", :relationship_type=>Relationship.confirmed_type}} + end + + test "should get_similar_items_v2_async with false" do + pm1 = create_project_media team: @team, media: create_uploaded_audio + Bot::Alegre.stubs(:should_get_similar_items_of_type?).returns(false) + assert_equal Bot::Alegre.get_similar_items_v2_async(pm1, nil), false + end + + test "should get_similar_items_v2_callback with false" do + pm1 = create_project_media team: @team, media: create_uploaded_audio + Bot::Alegre.stubs(:should_get_similar_items_of_type?).returns(false) + assert_equal Bot::Alegre.get_similar_items_v2_callback(pm1, nil), {} + end + + test "should get_similar_items_v2_async" do + pm1 = create_project_media team: @team, media: create_uploaded_audio + response = { + "message": "Message pushed successfully", + "queue": "audio__Model", + "body": { + "callback_url": "http:\/\/alegre:3100\/presto\/receive\/add_item\/audio", + "id": "f0d43d29-853d-4099-9e92-073203afa75b", + "url": Bot::Alegre.media_file_url(pm1), + "text": nil, + "raw": { + "limit": 200, + "url": Bot::Alegre.media_file_url(pm1), + "callback_url": "http:\/\/example.com\/search_results", + "doc_id": Bot::Alegre.item_doc_id(pm1, "audio"), + "context": Bot::Alegre.get_context(pm1, "audio"), + "created_at": "2023-10-27T22:40:14.205586", + "command": "search", + "threshold": 0.0, + "per_model_threshold": {}, + "match_across_content_types": false, + "requires_callback": true, + "final_task": "search" + } + } + } + WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/async/audio").with(body: {:doc_id => Bot::Alegre.item_doc_id(pm1),:context => {:team_id => pm1.team_id, :project_media_id => pm1.id, :has_custom_id => true, :temporary_media => false}, :url => Bot::Alegre.media_file_url(pm1), :threshold => 0.9, :confirmed => false}).to_return(body: response.to_json) + WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/async/audio").with(body: {:doc_id => Bot::Alegre.item_doc_id(pm1),:context => {:team_id => pm1.team_id, :project_media_id => pm1.id, :has_custom_id => true, :temporary_media => false}, :url => Bot::Alegre.media_file_url(pm1), :threshold => 0.9, :confirmed => true}).to_return(body: response.to_json) + assert_equal Bot::Alegre.get_similar_items_v2_async(pm1, nil), true end test "should get_similar_items_v2" do @@ -585,6 +715,7 @@ def teardown "team_id": pm1.team_id, "project_media_id": pm1.id, "has_custom_id": true, + "temporary_media": false, } ], "score": 1.0, @@ -620,6 +751,7 @@ def teardown "team_id": pm1.team_id, "project_media_id": pm1.id+1, "has_custom_id": true, + "temporary_media": false, } ], "score": 0.91, @@ -627,14 +759,202 @@ def teardown } ] } - WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/sync/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true}, :url=>Bot::Alegre.media_file_url(pm1), :threshold=>0.9}).to_return(body: response.to_json) + WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/sync/audio").with(body: {:doc_id=>Bot::Alegre.item_doc_id(pm1), :context=>{:team_id=>pm1.team_id, :project_media_id=>pm1.id, :has_custom_id=>true, :temporary_media=>false}, :url=>Bot::Alegre.media_file_url(pm1), :threshold=>0.9}).to_return(body: response.to_json) assert_equal Bot::Alegre.get_similar_items_v2(pm1, nil), {} end + test "should relate project media async for audio" do + pm1 = create_project_media team: @team, media: create_uploaded_audio + pm2 = create_project_media team: @team, media: create_uploaded_audio + WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/async/audio").to_return(body: '{}') + relationship = nil + params = { + "model_type": "image", + "data": { + "item": { + "id": "Y2hlY2stcHJvamVjdF9tZWRpYS0yMTQt", + "callback_url": "http://alegre:3100/presto/receive/add_item/image", + "url": "http://minio:9000/check-api-dev/uploads/uploaded_image/55/09572dedf610aad68090214303c14829.png", + "text": nil, + "raw": { + "doc_id": "Y2hlY2stcHJvamVjdF9tZWRpYS0yMTQt", + "context": { + "team_id": pm1.team_id, + "project_media_id": pm1.id, + "has_custom_id": true, + "temporary_media": false, + }, + "url": "http://minio:9000/check-api-dev/uploads/uploaded_image/55/09572dedf610aad68090214303c14829.png", + "threshold": 0.73, + "confirmed": true, + "created_at": "2024-03-14T22:05:47.588975", + "limit": 200, + "requires_callback": true, + "final_task": "search" + }, + "hash_value": "1110101010001011110100000011110010101000000010110101101010100101101111110101101001011010100001011111110101011010010000101010010110101101010110100000001010100101101010111110101000010101011100001110101010101111100001010101001011101010101011010001010101010010" + }, + "results": { + "result": [ + { + "id": "Y2hlY2stcHJvamVjdF9tZWRpYS0yMTQt", + "doc_id": "Y2hlY2stcHJvamVjdF9tZWRpYS0yMTQt", + "pdq": "1110101010001011110100000011110010101000000010110101101010100101101111110101101001011010100001011111110101011010010000101010010110101101010110100000001010100101101010111110101000010101011100001110101010101111100001010101001011101010101011010001010101010010", + "url": "http://minio:9000/check-api-dev/uploads/uploaded_image/55/09572dedf610aad68090214303c14829.png", + "context": [ + { + "team_id": pm2.team_id, + "has_custom_id": true, + "project_media_id": pm2.id, + "temporary_media": false, + } + ], + "score": 1.0, + "model": "image/pdq" + } + ] + } + } + } + assert_difference 'Relationship.count' do + # Simulate the webhook hitting the server and being executed.... + relationship = Bot::Alegre.process_alegre_callback(JSON.parse(params.to_json)) #hack to force into stringed keys + end + assert_equal relationship.source, pm2 + assert_equal relationship.target, pm1 + assert_equal relationship.relationship_type, Relationship.confirmed_type + end + + test "should relate project media async for audio when getting a canned response" do + pm1 = create_project_media team: @team, media: create_uploaded_audio + pm2 = create_project_media team: @team, media: create_uploaded_audio + WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/async/audio").to_return(body: '{}') + relationship = nil + params = { + "model_type": "image", + "data": { + "is_shortcircuited_callback": true, + "item": { + "callback_url": "http://alegre:3100/presto/receive/add_item/image", + "url": "http://minio:9000/check-api-dev/uploads/uploaded_image/55/09572dedf610aad68090214303c14829.png", + "text": nil, + "raw": { + "doc_id": "Y2hlY2stcHJvamVjdF9tZWRpYS0yMTQt", + "context": { + "team_id": pm1.team_id, + "project_media_id": pm1.id, + "has_custom_id": true, + "temporary_media": false, + }, + "url": "http://minio:9000/check-api-dev/uploads/uploaded_image/55/09572dedf610aad68090214303c14829.png", + "threshold": 0.73, + "confirmed": true, + "created_at": "2024-03-14T22:05:47.588975", + "limit": 200, + "requires_callback": true, + "final_task": "search" + }, + "hash_value": "1110101010001011110100000011110010101000000010110101101010100101101111110101101001011010100001011111110101011010010000101010010110101101010110100000001010100101101010111110101000010101011100001110101010101111100001010101001011101010101011010001010101010010" + }, + "results": { + "result": [ + { + "id": "Y2hlY2stcHJvamVjdF9tZWRpYS0yMTQt", + "doc_id": "Y2hlY2stcHJvamVjdF9tZWRpYS0yMTQt", + "pdq": "1110101010001011110100000011110010101000000010110101101010100101101111110101101001011010100001011111110101011010010000101010010110101101010110100000001010100101101010111110101000010101011100001110101010101111100001010101001011101010101011010001010101010010", + "url": "http://minio:9000/check-api-dev/uploads/uploaded_image/55/09572dedf610aad68090214303c14829.png", + "context": [ + { + "team_id": pm2.team_id, + "has_custom_id": true, + "project_media_id": pm2.id, + "temporary_media": false, + } + ], + "score": 1.0, + "model": "image/pdq" + } + ] + } + } + } + assert_difference 'Relationship.count' do + # Simulate the webhook hitting the server and being executed.... + relationship = Bot::Alegre.process_alegre_callback(JSON.parse(params.to_json)) #hack to force into stringed keys + end + assert_equal relationship.source, pm2 + assert_equal relationship.target, pm1 + assert_equal relationship.relationship_type, Relationship.confirmed_type + end + + test "should not relate project media async for audio when temporary" do + pm1 = create_project_media team: @team, media: create_uploaded_audio + pm2 = create_project_media team: @team, media: create_uploaded_audio + WebMock.stub_request(:post, "#{CheckConfig.get('alegre_host')}/similarity/async/audio").to_return(body: '{}') + relationship = nil + params = { + "model_type": "image", + "data": { + "item": { + "id": "Y2hlY2stcHJvamVjdF9tZWRpYS0yMTQt", + "callback_url": "http://alegre:3100/presto/receive/add_item/image", + "url": "http://minio:9000/check-api-dev/uploads/uploaded_image/55/09572dedf610aad68090214303c14829.png", + "text": nil, + "raw": { + "doc_id": "Y2hlY2stcHJvamVjdF9tZWRpYS0yMTQt", + "context": { + "team_id": pm1.team_id, + "project_media_id": 123456789, + "has_custom_id": true, + "temporary_media": true, + }, + "url": "http://minio:9000/check-api-dev/uploads/uploaded_image/55/09572dedf610aad68090214303c14829.png", + "threshold": 0.73, + "confirmed": true, + "created_at": "2024-03-14T22:05:47.588975", + "limit": 200, + "requires_callback": true, + "final_task": "search" + }, + "hash_value": "1110101010001011110100000011110010101000000010110101101010100101101111110101101001011010100001011111110101011010010000101010010110101101010110100000001010100101101010111110101000010101011100001110101010101111100001010101001011101010101011010001010101010010" + }, + "results": { + "result": [ + { + "id": "Y2hlY2stcHJvamVjdF9tZWRpYS0yMTQt", + "doc_id": "Y2hlY2stcHJvamVjdF9tZWRpYS0yMTQt", + "pdq": "1110101010001011110100000011110010101000000010110101101010100101101111110101101001011010100001011111110101011010010000101010010110101101010110100000001010100101101010111110101000010101011100001110101010101111100001010101001011101010101011010001010101010010", + "url": "http://minio:9000/check-api-dev/uploads/uploaded_image/55/09572dedf610aad68090214303c14829.png", + "context": [ + { + "team_id": pm2.team_id, + "has_custom_id": true, + "project_media_id": pm2.id, + "temporary_media": false, + } + ], + "score": 1.0, + "model": "image/pdq" + } + ] + } + } + } + assert_no_difference 'Relationship.count' do + # Simulate the webhook hitting the server and being executed.... + relationship = Bot::Alegre.process_alegre_callback(JSON.parse(params.to_json)) #hack to force into stringed keys + end + end + + test "should get_cached_data with right fallbacks" do + pm1 = create_project_media team: @team, media: create_uploaded_audio + assert_equal Bot::Alegre.get_cached_data(Bot::Alegre.get_required_keys(pm1, nil)), {confirmed_results: [], suggested_or_confirmed_results: []} + end + test "should relate project media for audio" do pm1 = create_project_media team: @team, media: create_uploaded_audio pm2 = create_project_media team: @team, media: create_uploaded_audio - Bot::Alegre.stubs(:get_similar_items_v2).returns({pm2.id=>{:score=>0.91, :context=>{"team_id"=>pm2.team_id, "has_custom_id"=>true, "project_media_id"=>pm2.id}, :model=>"audio", :source_field=>"audio", :target_field=>"audio", :relationship_type=>Relationship.suggested_type}}) + Bot::Alegre.stubs(:get_similar_items_v2).returns({pm2.id=>{:score=>0.91, :context=>{"team_id"=>pm2.team_id, "has_custom_id"=>true, "project_media_id"=>pm2.id, "temporary_media"=>false}, :model=>"audio", :source_field=>"audio", :target_field=>"audio", :relationship_type=>Relationship.suggested_type}}) relationship = nil assert_difference 'Relationship.count' do relationship = Bot::Alegre.relate_project_media(pm1) @@ -648,7 +968,21 @@ def teardown test "should relate project media for image" do pm1 = create_project_media team: @team, media: create_uploaded_image pm2 = create_project_media team: @team, media: create_uploaded_image - Bot::Alegre.stubs(:get_similar_items_v2).returns({pm2.id=>{:score=>0.91, :context=>{"team_id"=>pm2.team_id, "has_custom_id"=>true, "project_media_id"=>pm2.id}, :model=>"audio", :source_field=>"audio", :target_field=>"audio", :relationship_type=>Relationship.suggested_type}}) + Bot::Alegre.stubs(:get_similar_items_v2).returns({pm2.id=>{:score=>0.91, :context=>{"team_id"=>pm2.team_id, "has_custom_id"=>true, "project_media_id"=>pm2.id, "temporary_media"=>false}, :model=>"image", :source_field=>"image", :target_field=>"image", :relationship_type=>Relationship.suggested_type}}) + relationship = nil + assert_difference 'Relationship.count' do + relationship = Bot::Alegre.relate_project_media(pm1) + end + assert_equal relationship.source, pm2 + assert_equal relationship.target, pm1 + assert_equal relationship.relationship_type, Relationship.suggested_type + Bot::Alegre.unstub(:get_similar_items_v2) + end + + test "should relate project media for video" do + pm1 = create_project_media team: @team, media: create_uploaded_video + pm2 = create_project_media team: @team, media: create_uploaded_video + Bot::Alegre.stubs(:get_similar_items_v2).returns({pm2.id=>{:score=>0.91, :context=>{"team_id"=>pm2.team_id, "has_custom_id"=>true, "project_media_id"=>pm2.id, "temporary_media"=>false}, :model=>"video", :source_field=>"video", :target_field=>"video", :relationship_type=>Relationship.suggested_type}}) relationship = nil assert_difference 'Relationship.count' do relationship = Bot::Alegre.relate_project_media(pm1) @@ -676,4 +1010,13 @@ def teardown pm = create_project_media team: @team, media: create_uploaded_image assert_equal({}, Bot::Alegre.get_similar_items_v2(pm, nil)) end + + test "should not relate project media for video if disabled on workspace" do + tbi = TeamBotInstallation.where(team: @team, user: @bot).last + tbi.set_video_similarity_enabled = false + tbi.save! + Bot::Alegre.stubs(:merge_suggested_and_confirmed).never + pm = create_project_media team: @team, media: create_uploaded_video + assert_equal({}, Bot::Alegre.get_similar_items_v2(pm, nil)) + end end diff --git a/test/models/bot/smooch_4_test.rb b/test/models/bot/smooch_4_test.rb index 22e634eb1e..34a4b37ece 100644 --- a/test/models/bot/smooch_4_test.rb +++ b/test/models/bot/smooch_4_test.rb @@ -667,7 +667,7 @@ def teardown WebMock.stub_request(:get, pender_url).with(query: { url: url }).to_return(body: response) Bot::Smooch.stubs(:bundle_list_of_messages).returns({ 'type' => 'text', 'text' => "Foo bar foo bar #{url}" }) CheckSearch.any_instance.stubs(:medias).returns([pm1]) - Bot::Alegre.stubs(:get_merged_similar_items).returns({ pm2.id => { score: 0.9, model: 'elasticsearch' } }) + Bot::Alegre.stubs(:get_merged_similar_items).returns({ pm2.id => { score: 0.9, model: 'elasticsearch', context: {foo: :bar} } }) assert_equal [pm2], Bot::Smooch.get_search_results(random_string, {}, t.id, 'en') Bot::Smooch.stubs(:bundle_list_of_messages).returns({ 'type' => 'text', 'text' => "Test #{url}" }) diff --git a/test/models/bot/smooch_5_test.rb b/test/models/bot/smooch_5_test.rb index f26e0d90c8..0321dca79f 100644 --- a/test/models/bot/smooch_5_test.rb +++ b/test/models/bot/smooch_5_test.rb @@ -59,10 +59,10 @@ def teardown alegre_results = {} ProjectMedia.order('id ASC').all.each_with_index do |pm, i| publish_report(pm) if pm.id != pm1e.id - alegre_results[pm.id] = { score: (1 - i / 10.0), model: 'elasticsearch' } unless [pm1f, pm1g, pm2b].map(&:id).include?(pm.id) + alegre_results[pm.id] = { score: (1 - i / 10.0), model: 'elasticsearch', context: {foo: :bar} } unless [pm1f, pm1g, pm2b].map(&:id).include?(pm.id) end Bot::Alegre.stubs(:get_merged_similar_items).returns(alegre_results) - Bot::Alegre.stubs(:get_items_with_similar_media).returns(alegre_results) + Bot::Alegre.stubs(:get_items_with_similar_media_v2).returns(alegre_results) # Get feed data scoped by teams that are part of the feed, taking into account the filters for the feed # and for each team participating in the feed @@ -82,6 +82,6 @@ def teardown end Bot::Alegre.unstub(:get_merged_similar_items) - Bot::Alegre.unstub(:get_items_with_similar_media) + Bot::Alegre.unstub(:get_items_with_similar_media_v2) end end diff --git a/test/models/bot/smooch_6_test.rb b/test/models/bot/smooch_6_test.rb index cdba58ad2c..b1f22421c5 100644 --- a/test/models/bot/smooch_6_test.rb +++ b/test/models/bot/smooch_6_test.rb @@ -236,7 +236,7 @@ def send_message_outside_24_hours_window(template, pm = nil) ProjectMedia.any_instance.stubs(:analysis_published_article_url).returns(random_url) pm = create_project_media(team: @team) publish_report(pm, {}, nil, { language: 'en', use_visual_card: false }) - Bot::Alegre.stubs(:get_merged_similar_items).returns({ pm.id => { score: 0.9 } }) + Bot::Alegre.stubs(:get_merged_similar_items).returns({ pm.id => { score: 0.9, context: {foo: :bar} } }) Sidekiq::Testing.inline! do send_message 'hello', '1', '1', 'Foo bar foo bar foo bar', '1' assert_state 'search_result' @@ -256,7 +256,7 @@ def send_message_outside_24_hours_window(template, pm = nil) WebMock.stub_request(:get, image_url).to_return(body: File.read(File.join(Rails.root, 'test', 'data', 'rails.png'))) ProjectMedia.any_instance.stubs(:report_status).returns('published') ProjectMedia.any_instance.stubs(:analysis_published_article_url).returns(random_url) - Bot::Alegre.stubs(:get_items_with_similar_media).returns({ @search_result.id => { score: 0.9 } }) + Bot::Alegre.stubs(:get_items_with_similar_media_v2).returns({ @search_result.id => { score: 0.9, context: {foo: :bar} } }) Bot::Smooch.stubs(:bundle_list_of_messages).returns({ 'type' => 'image', 'mediaUrl' => image_url, 'source' => { type: "whatsapp" }, language: 'en' }) CheckS3.stubs(:rewrite_url).returns(random_url) Sidekiq::Testing.inline! do diff --git a/test/models/bot/smooch_7_test.rb b/test/models/bot/smooch_7_test.rb index 5e3acf755a..6e170ecc11 100644 --- a/test/models/bot/smooch_7_test.rb +++ b/test/models/bot/smooch_7_test.rb @@ -236,7 +236,7 @@ def teardown Bot::Smooch.stubs(:bundle_list_of_messages).returns({ 'type' => 'text', 'text' => 'Foo bar foo bar foo bar' }) ProjectMedia.any_instance.stubs(:report_status).returns('published') ProjectMedia.any_instance.stubs(:analysis_published_article_url).returns(random_url) - Bot::Alegre.stubs(:get_merged_similar_items).returns({ pm.id => { score: 0.9, model: 'elasticsearch' } }) + Bot::Alegre.stubs(:get_merged_similar_items).returns({ pm.id => { score: 0.9, model: 'elasticsearch', context: {foo: :bar} } }) assert_equal [pm], Bot::Smooch.get_search_results(random_string, {}, pm.team_id, 'en') @@ -259,7 +259,7 @@ def teardown Bot::Smooch.stubs(:bundle_list_of_messages).returns({ 'type' => 'image', 'mediaUrl' => random_url }) ProjectMedia.any_instance.stubs(:report_status).returns('published') ProjectMedia.any_instance.stubs(:analysis_published_article_url).returns(random_url) - Bot::Alegre.stubs(:get_items_with_similar_media).returns({ pm.id => { score: 0.9, model: 'elasticsearch' } }) + Bot::Alegre.stubs(:get_items_with_similar_media_v2).returns({ pm.id => { score: 0.9, model: 'elasticsearch', context: {foo: :bar} } }) CheckS3.stubs(:rewrite_url).returns(random_url) assert_equal [pm], Bot::Smooch.get_search_results(random_string, {}, pm.team_id, 'en') @@ -267,7 +267,7 @@ def teardown Bot::Smooch.unstub(:bundle_list_of_messages) ProjectMedia.any_instance.unstub(:report_status) ProjectMedia.any_instance.unstub(:analysis_published_article_url) - Bot::Alegre.unstub(:get_items_with_similar_media) + Bot::Alegre.unstub(:get_items_with_similar_media_v2) end test "should handle exception when adding Smooch integration" do @@ -310,12 +310,26 @@ def teardown pm3 = create_project_media team: t #Vector high score pm4 = create_project_media team: t #Vector low score # Create more project media if needed - results = { pm1.id => { model: 'elasticsearch', score: 10.8 }, pm2.id => { model: 'elasticsearch', score: 15.2}, - pm3.id => { model: 'anything-else', score: 1.98 }, pm4.id => { model: 'anything-else', score: 1.8}} + results = { pm1.id => { model: 'elasticsearch', score: 10.8, context: {foo: :bar}}, pm2.id => { model: 'elasticsearch', score: 15.2, context: {foo: :bar}}, + pm3.id => { model: 'anything-else', score: 1.98, context: {foo: :bar}}, pm4.id => { model: 'anything-else', score: 1.8, context: {foo: :bar}}} assert_equal [pm3, pm4, pm2], Bot::Smooch.parse_search_results_from_alegre(results, t.id) ProjectMedia.any_instance.unstub(:report_status) end + test "should omit temporary results from Alegre" do + ProjectMedia.any_instance.stubs(:report_status).returns('published') # We can stub this because it's not what this test is testing + t = create_team + pm1 = create_project_media team: t #ES low score + pm2 = create_project_media team: t #ES high score + pm3 = create_project_media team: t #Vector high score + pm4 = create_project_media team: t #Vector low score + # Create more project media if needed + results = { pm1.id => { model: 'elasticsearch', score: 10.8, context: {blah: 1} }, pm2.id => { model: 'elasticsearch', score: 15.2, context: {blah: 1} }, + pm3.id => { model: 'anything-else', score: 1.98, context: {temporary_media: true} }, pm4.id => { model: 'anything-else', score: 1.8, context: {temporary_media: false}}} + assert_equal [pm4, pm2, pm1], Bot::Smooch.parse_search_results_from_alegre(results, t.id) + ProjectMedia.any_instance.unstub(:report_status) + end + test "should not search for empty link description" do ProjectMedia.any_instance.stubs(:report_status).returns('published') @@ -491,8 +505,9 @@ def teardown Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'relevant_search_result_requests', pm) Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'relevant_search_result_requests', pm) Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'timeout_search_requests', pm) - Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'irrelevant_search_result_requests') - Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'irrelevant_search_result_requests') + Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'irrelevant_search_result_requests', pm) + Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'irrelevant_search_result_requests', pm) + Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'irrelevant_search_result_requests', pm) message = lambda do { type: 'text', @@ -512,24 +527,30 @@ def teardown } end Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'relevant_search_result_requests', pm2) - Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'irrelevant_search_result_requests') + Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'irrelevant_search_result_requests', pm2) + Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'irrelevant_search_result_requests', pm2) # Verify cached field - assert_equal 5, pm.tipline_search_results_count + assert_equal 6, pm.tipline_search_results_count assert_equal 2, pm.positive_tipline_search_results_count - assert_equal 2, pm2.tipline_search_results_count + assert_equal 3, pm.negative_tipline_search_results_count + assert_equal 3, pm2.tipline_search_results_count assert_equal 1, pm2.positive_tipline_search_results_count + assert_equal 2, pm2.negative_tipline_search_results_count # Verify ES values es = $repository.find(pm.get_es_doc_id) - assert_equal 5, es['tipline_search_results_count'] + assert_equal 6, es['tipline_search_results_count'] assert_equal 2, es['positive_tipline_search_results_count'] + assert_equal 3, es['negative_tipline_search_results_count'] es2 = $repository.find(pm2.get_es_doc_id) - assert_equal 2, es2['tipline_search_results_count'] + assert_equal 3, es2['tipline_search_results_count'] assert_equal 1, es2['positive_tipline_search_results_count'] + assert_equal 2, es2['negative_tipline_search_results_count'] # Verify destroy types = ["irrelevant_search_result_requests", "timeout_search_requests"] TiplineRequest.where(associated_type: 'ProjectMedia', associated_id: pm.id, smooch_request_type: types).destroy_all assert_equal 2, pm.tipline_search_results_count assert_equal 2, pm.positive_tipline_search_results_count + assert_equal 0, pm.negative_tipline_search_results_count end end diff --git a/test/models/explainer_test.rb b/test/models/explainer_test.rb new file mode 100644 index 0000000000..bae8ccbf47 --- /dev/null +++ b/test/models/explainer_test.rb @@ -0,0 +1,99 @@ +require_relative '../test_helper' + +class ExplainerTest < ActiveSupport::TestCase + def setup + Explainer.delete_all + end + + test "should create explainer" do + assert_difference 'Explainer.count' do + create_explainer + end + end + + test "should have versions" do + with_versioning do + u = create_user + t = create_team + create_team_user team: t, user: u, role: 'admin' + with_current_user_and_team(u, t) do + ex = nil + assert_difference 'PaperTrail::Version.count', 1 do + ex = create_explainer user: u, team: t + end + assert_equal 1, ex.versions.count + end + end + end + + test "should not create explainer without user or team" do + assert_no_difference 'Explainer.count' do + assert_raises ActiveRecord::RecordInvalid do + create_explainer user: nil + end + assert_raises ActiveRecord::RecordInvalid do + create_explainer team: nil + end + end + # should set default team + t = create_team + Team.stubs(:current).returns(t) + ex = create_explainer team: nil + assert_equal t, ex.team + Team.unstub(:current) + end + + test "should validate language" do + t = create_team + t.set_language = 'fr' + t.set_languages(['fr']) + t.save! + assert_difference 'Explainer.count' do + create_explainer team: t, language: nil + end + assert_difference 'Explainer.count' do + create_explainer team: t, language: 'fr' + end + assert_raises ActiveRecord::RecordInvalid do + create_explainer team: t, language: 'en' + end + end + + test "should belong to user and team" do + u = create_user + t = create_team + ex = create_explainer user: u, team: t + assert_equal u, ex.user + assert_equal t, ex.team + end + + test "should not create an explainer if does not have permission" do + t = create_team + u = create_user + create_team_user team: t, user: u + with_current_user_and_team(u, t) do + assert_no_difference 'Explainer.count' do + assert_raises RuntimeError do + create_explainer team: create_team + end + end + end + end + + test "should create an explainer if has permission" do + t = create_team + u = create_user + create_team_user team: t, user: u + with_current_user_and_team(u, t) do + assert_difference 'Explainer.count' do + create_explainer team: t + end + end + end + + test "should tag explainer" do + ex = create_explainer + tag = create_tag annotated: ex + assert_equal [tag], ex.annotations('tag') + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 26e040e76e..78bd12412e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -857,6 +857,7 @@ def setup_smooch_bot(menu = false, extra_settings = {}) @team = create_team @project = create_project team_id: @team.id @bid = random_string + ApiKey.delete_all BotUser.delete_all @resource_uuid = random_string diff --git a/test/workers/reindex_alegre_workspace_test.rb b/test/workers/reindex_alegre_workspace_test.rb index bba484f1e7..afa42eb9f2 100644 --- a/test/workers/reindex_alegre_workspace_test.rb +++ b/test/workers/reindex_alegre_workspace_test.rb @@ -64,6 +64,7 @@ def teardown :team_id=>@pm.team_id, :project_media_id=>@pm.id, :has_custom_id=>true, + :temporary_media=>false, :field=>"title" }, :models=>["elasticsearch"] @@ -98,6 +99,7 @@ def teardown :team_id=>@pm.team_id, :project_media_id=>@pm.id, :has_custom_id=>true, + :temporary_media=>false, :field=>"title" }, :models=>["elasticsearch"]