diff --git a/Gemfile b/Gemfile index ce4173db7e..40c90b4fb4 100644 --- a/Gemfile +++ b/Gemfile @@ -36,7 +36,7 @@ end gem 'webmock' gem 'pender_client', git: 'https://github.com/meedan/pender-client.git', ref: '89c9072' gem 'lograge' -gem 'rails', '~> 6.1.0' +gem 'rails', '~> 6.1.7' gem 'pg', '~> 1.1' gem "json", ">= 2.3.0" gem 'turbolinks' @@ -60,8 +60,9 @@ gem 'elasticsearch-persistence', '7.1.1' gem 'paper_trail', '13.0.0' gem 'graphiql-rails', git: 'https://github.com/meedan/graphiql-rails.git', ref: '8db0eac' gem 'graphql-formatter' -gem 'nokogiri', '1.14.3' +gem 'nokogiri', '1.16.2' gem 'puma' +gem 'rack-attack' gem 'rack-cors', '1.0.6', require: 'rack/cors' gem 'sidekiq', '5.2.10' gem 'sidekiq-cloudwatchmetrics' @@ -96,7 +97,7 @@ gem 'json-schema', '2.5.0' gem 'google_drive' gem 'activerecord-import', '1.1.0' gem 'redis-rails' -gem 'rack', '2.2.6.4' +gem 'rack', '2.2.8.1' gem 'jwt' gem 'smooch-api', '> 5.34.1', git: 'https://github.com/meedan/sunshine-conversations-ruby', branch: 'v5.34.2' gem 'typhoeus' diff --git a/Gemfile.lock b/Gemfile.lock index 876aec8c6a..7e67fe7939 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,62 +74,62 @@ GEM specs: aasm (5.2.0) concurrent-ruby (~> 1.0) - actioncable (6.1.7.6) - actionpack (= 6.1.7.6) - activesupport (= 6.1.7.6) + actioncable (6.1.7.7) + actionpack (= 6.1.7.7) + activesupport (= 6.1.7.7) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.6) - actionpack (= 6.1.7.6) - activejob (= 6.1.7.6) - activerecord (= 6.1.7.6) - activestorage (= 6.1.7.6) - activesupport (= 6.1.7.6) + actionmailbox (6.1.7.7) + actionpack (= 6.1.7.7) + activejob (= 6.1.7.7) + activerecord (= 6.1.7.7) + activestorage (= 6.1.7.7) + activesupport (= 6.1.7.7) mail (>= 2.7.1) - actionmailer (6.1.7.6) - actionpack (= 6.1.7.6) - actionview (= 6.1.7.6) - activejob (= 6.1.7.6) - activesupport (= 6.1.7.6) + actionmailer (6.1.7.7) + actionpack (= 6.1.7.7) + actionview (= 6.1.7.7) + activejob (= 6.1.7.7) + activesupport (= 6.1.7.7) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7.6) - actionview (= 6.1.7.6) - activesupport (= 6.1.7.6) + actionpack (6.1.7.7) + actionview (= 6.1.7.7) + activesupport (= 6.1.7.7) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.6) - actionpack (= 6.1.7.6) - activerecord (= 6.1.7.6) - activestorage (= 6.1.7.6) - activesupport (= 6.1.7.6) + actiontext (6.1.7.7) + actionpack (= 6.1.7.7) + activerecord (= 6.1.7.7) + activestorage (= 6.1.7.7) + activesupport (= 6.1.7.7) nokogiri (>= 1.8.5) - actionview (6.1.7.6) - activesupport (= 6.1.7.6) + actionview (6.1.7.7) + activesupport (= 6.1.7.7) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.7.6) - activesupport (= 6.1.7.6) + activejob (6.1.7.7) + activesupport (= 6.1.7.7) globalid (>= 0.3.6) - activemodel (6.1.7.6) - activesupport (= 6.1.7.6) - activerecord (6.1.7.6) - activemodel (= 6.1.7.6) - activesupport (= 6.1.7.6) + activemodel (6.1.7.7) + activesupport (= 6.1.7.7) + activerecord (6.1.7.7) + activemodel (= 6.1.7.7) + activesupport (= 6.1.7.7) activerecord-import (1.1.0) activerecord (>= 3.2) - activestorage (6.1.7.6) - actionpack (= 6.1.7.6) - activejob (= 6.1.7.6) - activerecord (= 6.1.7.6) - activesupport (= 6.1.7.6) + activestorage (6.1.7.7) + actionpack (= 6.1.7.7) + activejob (= 6.1.7.7) + activerecord (= 6.1.7.7) + activesupport (= 6.1.7.7) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.6) + activesupport (6.1.7.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -165,7 +165,7 @@ GEM aws-sigv4 (~> 1.1) aws-sigv4 (1.4.0) aws-eventstream (~> 1, >= 1.0.2) - bcrypt (3.1.18) + bcrypt (3.1.20) bitly (2.0.1) oauth2 (>= 0.5.0, < 2.0) bootsnap (1.16.0) @@ -196,7 +196,7 @@ GEM rexml crass (1.0.6) daemons (1.4.1) - date (3.3.3) + date (3.3.4) debugger-ruby_core_source (1.3.8) declarative (0.0.20) deep_cloneable (3.2.0) @@ -215,7 +215,7 @@ GEM devise (~> 4.0) railties (< 7.1) rotp (~> 6.0) - devise_invitable (2.0.7) + devise_invitable (2.0.9) actionmailer (>= 5.0) devise (>= 4.6) diff-lcs (1.5.0) @@ -313,8 +313,8 @@ GEM geojsonlint (0.1.3) activemodel (>= 3.0.0) json-schema (~> 2.5.0) - globalid (1.1.0) - activesupport (>= 5.0) + globalid (1.2.1) + activesupport (>= 6.1) google-apis-core (0.4.2) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) @@ -420,7 +420,7 @@ GEM mime-types-data (3.2022.0105) mini_magick (4.10.1) mini_mime (1.1.5) - mini_portile2 (2.8.4) + mini_portile2 (2.8.5) minitest (5.19.0) minitest-hooks (1.5.0) minitest (> 5.3) @@ -436,19 +436,19 @@ GEM net-http-digest_auth (1.4.1) net-http-persistent (4.0.1) connection_pool (~> 2.2) - net-imap (0.3.7) + net-imap (0.4.10) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout - net-smtp (0.3.3) + net-smtp (0.4.0.1) net-protocol netrc (0.11.0) nio4r (2.7.0) - nokogiri (1.14.3) - mini_portile2 (~> 2.8.0) + nokogiri (1.16.2) + mini_portile2 (~> 2.8.2) racc (~> 1.4) numerizer (0.1.1) oauth (0.5.8) @@ -635,8 +635,10 @@ GEM pusher-signature (~> 0.1.8) pusher-signature (0.1.8) raabro (1.4.0) - racc (1.7.1) - rack (2.2.6.4) + racc (1.7.3) + rack (2.2.8.1) + rack-attack (6.7.0) + rack (>= 1.0, < 4) rack-cors (1.0.6) rack (>= 1.6.0) rack-protection (2.2.0) @@ -644,20 +646,20 @@ GEM rack-test (1.1.0) rack (>= 1.0, < 3) railroady (1.6.0) - rails (6.1.7.6) - actioncable (= 6.1.7.6) - actionmailbox (= 6.1.7.6) - actionmailer (= 6.1.7.6) - actionpack (= 6.1.7.6) - actiontext (= 6.1.7.6) - actionview (= 6.1.7.6) - activejob (= 6.1.7.6) - activemodel (= 6.1.7.6) - activerecord (= 6.1.7.6) - activestorage (= 6.1.7.6) - activesupport (= 6.1.7.6) + rails (6.1.7.7) + actioncable (= 6.1.7.7) + actionmailbox (= 6.1.7.7) + actionmailer (= 6.1.7.7) + actionpack (= 6.1.7.7) + actiontext (= 6.1.7.7) + actionview (= 6.1.7.7) + activejob (= 6.1.7.7) + activemodel (= 6.1.7.7) + activerecord (= 6.1.7.7) + activestorage (= 6.1.7.7) + activesupport (= 6.1.7.7) bundler (>= 1.15.0) - railties (= 6.1.7.6) + railties (= 6.1.7.7) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -670,15 +672,15 @@ GEM rails-graphql-generator (0.1.0) rails-html-sanitizer (1.4.4) loofah (~> 2.19, >= 2.19.1) - railties (6.1.7.6) - actionpack (= 6.1.7.6) - activesupport (= 6.1.7.6) + railties (6.1.7.7) + actionpack (= 6.1.7.7) + activesupport (= 6.1.7.7) method_source rake (>= 12.2) thor (~> 1.0) rainbow (2.2.2) rake - rake (13.0.6) + rake (13.1.0) rb-fsevent (0.11.1) rb-inotify (0.10.1) ffi (~> 1.0) @@ -827,9 +829,9 @@ GEM daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) - thor (1.2.2) + thor (1.3.1) thread_safe (0.3.6) - timeout (0.4.0) + timeout (0.4.1) tins (1.31.0) sync to_regexp (0.2.1) @@ -874,7 +876,7 @@ GEM websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.6.11) + zeitwerk (2.6.13) PLATFORMS ruby @@ -939,7 +941,7 @@ DEPENDENCIES minitest-retry mocha (~> 2.0) multi_json (= 1.15.0) - nokogiri (= 1.14.3) + nokogiri (= 1.16.2) omniauth-facebook omniauth-google-oauth2! omniauth-slack @@ -982,11 +984,12 @@ DEPENDENCIES phony_rails puma pusher - rack (= 2.2.6.4) + rack (= 2.2.8.1) + rack-attack rack-cors (= 1.0.6) rack-protection (= 2.2.0) railroady - rails (~> 6.1.0) + rails (~> 6.1.7) rails-controller-testing rails-graphql-generator rails-html-sanitizer (= 1.4.4) diff --git a/app/graph/mutations/graphql_crud_operations.rb b/app/graph/mutations/graphql_crud_operations.rb index 15fe74f699..1520987d43 100644 --- a/app/graph/mutations/graphql_crud_operations.rb +++ b/app/graph/mutations/graphql_crud_operations.rb @@ -43,7 +43,7 @@ def self.define_returns(obj, parent_names) ret["#{obj_name}Edge".to_sym] = GraphQL::Relay::Edge.between( child, parent - ) if !%w[related_to public_team version].include?(parent_name) + ) if !%w[related_to public_team version me].include?(parent_name) ret[parent_name.to_sym] = parent end end diff --git a/app/graph/mutations/tipline_message_mutations.rb b/app/graph/mutations/tipline_message_mutations.rb index 686444dbd9..a5ca9f89be 100644 --- a/app/graph/mutations/tipline_message_mutations.rb +++ b/app/graph/mutations/tipline_message_mutations.rb @@ -6,10 +6,10 @@ class Send < Mutations::BaseMutation field :success, GraphQL::Types::Boolean, null: true def resolve(in_reply_to_id: nil, message: nil) - request = Annotation.find(in_reply_to_id).load + request = TiplineRequest.find(in_reply_to_id) ability = context[:ability] || Ability.new success = false - if Team.current&.id && User.current&.id && ability.can?(:send, TiplineMessage.new(team: Team.current)) && request.annotated.team_id == Team.current.id + if Team.current&.id && User.current&.id && ability.can?(:send, TiplineMessage.new(team: Team.current)) && request.team_id == Team.current.id success = Bot::Smooch.reply_to_request_with_custom_message(request, message) end { success: success } diff --git a/app/graph/mutations/user_disconnect_login_account_mutation.rb b/app/graph/mutations/user_disconnect_login_account_mutation.rb index 53a1b9102c..344e0847e7 100644 --- a/app/graph/mutations/user_disconnect_login_account_mutation.rb +++ b/app/graph/mutations/user_disconnect_login_account_mutation.rb @@ -5,7 +5,7 @@ class UserDisconnectLoginAccountMutation < Mutations::BaseMutation argument :uid, GraphQL::Types::String, required: true field :success, GraphQL::Types::Boolean, null: true - field :user, UserType, null: true + field :user, MeType, null: true def resolve(provider:, uid:) user = User.current diff --git a/app/graph/mutations/user_mutations.rb b/app/graph/mutations/user_mutations.rb index 665cb32fd6..30f7f11bb1 100644 --- a/app/graph/mutations/user_mutations.rb +++ b/app/graph/mutations/user_mutations.rb @@ -1,6 +1,6 @@ module UserMutations MUTATION_TARGET = 'user'.freeze - PARENTS = [].freeze + PARENTS = ['me'].freeze module SharedCreateAndUpdateFields extend ActiveSupport::Concern diff --git a/app/graph/mutations/user_two_factor_authentication_mutation.rb b/app/graph/mutations/user_two_factor_authentication_mutation.rb index b71a174df1..fd7a09e4f5 100644 --- a/app/graph/mutations/user_two_factor_authentication_mutation.rb +++ b/app/graph/mutations/user_two_factor_authentication_mutation.rb @@ -7,7 +7,7 @@ class UserTwoFactorAuthenticationMutation < Mutations::BaseMutation argument :otp_required, GraphQL::Types::Boolean, required: false, camelize: false field :success, GraphQL::Types::Boolean, null: true - field :user, UserType, null: true + field :user, MeType, null: true def resolve(id:, password:, qrcode: nil, otp_required: nil) user = User.where(id: id).last diff --git a/app/graph/relay_on_rails_schema.rb b/app/graph/relay_on_rails_schema.rb index 999dbd478d..b30ab78779 100644 --- a/app/graph/relay_on_rails_schema.rb +++ b/app/graph/relay_on_rails_schema.rb @@ -13,6 +13,8 @@ class RelayOnRailsSchema < GraphQL::Schema lazy_resolve(Concurrent::Future, :value) + disable_introspection_entry_points unless Rails.env.development? + class << self def resolve_type(_type, object, _ctx) klass = (object.respond_to?(:type) && object.type) ? object.type : object.class_name diff --git a/app/graph/types/bot_user_type.rb b/app/graph/types/bot_user_type.rb index af26bc92b0..179abed85b 100644 --- a/app/graph/types/bot_user_type.rb +++ b/app/graph/types/bot_user_type.rb @@ -46,7 +46,7 @@ def settings_as_json_schema(team_slug: nil) object.settings_as_json_schema(false, team_slug) end - field :team_author, TeamType, null: true + field :team_author, PublicTeamType, null: true def team_author RecordLoader.for(Team).load(object.team_author_id.to_i) diff --git a/app/graph/types/cluster_type.rb b/app/graph/types/cluster_type.rb index b893c969fe..a8a13b99e2 100644 --- a/app/graph/types/cluster_type.rb +++ b/app/graph/types/cluster_type.rb @@ -4,10 +4,25 @@ class ClusterType < DefaultObject implements GraphQL::Types::Relay::Node field :dbid, GraphQL::Types::Int, null: true - field :size, GraphQL::Types::Int, null: true - field :team_names, [GraphQL::Types::String, null: true], null: true - field :fact_checked_by_team_names, JsonStringType, null: true + field :team_ids, [GraphQL::Types::Int, null: true], null: true + field :channels, [GraphQL::Types::Int, null: true], null: true + field :media_count, GraphQL::Types::Int, null: true field :requests_count, GraphQL::Types::Int, null: true + field :title, GraphQL::Types::String, null: true + + field :fact_checks_count, GraphQL::Types::Int, null: true + + def fact_checks_count + object.feed.data_points.to_a.include?(1) ? object.fact_checks_count : nil + end + + field :center, ProjectMediaType, null: true + + def center + RecordLoader + .for(ProjectMedia) + .load(object.project_media_id) + end field :first_item_at, GraphQL::Types::Int, null: true @@ -21,26 +36,21 @@ def last_item_at object.last_item_at.to_i end - field :items, ProjectMediaType.connection_type, null: true do - argument :feed_id, GraphQL::Types::Int, required: true, camelize: false - end + field :last_request_date, GraphQL::Types::Int, null: true - def items(feed_id:) - Cluster.find_if_can(object.id, context[:ability]) - feed = Feed.find_if_can(feed_id.to_i, context[:ability]) - object.project_medias.joins(:team).where("teams.id" => feed.team_ids) + def last_request_date + object.last_request_date.to_i end - field :claim_descriptions, ClaimDescriptionType.connection_type, null: true do - argument :feed_id, GraphQL::Types::Int, required: true, camelize: false + field :last_fact_check_date, GraphQL::Types::Int, null: true + + def last_fact_check_date + object.last_fact_check_date.to_i end - def claim_descriptions(feed_id:) - Cluster.find_if_can(object.id, context[:ability]) - feed = Feed.find_if_can(feed_id.to_i, context[:ability]) - ClaimDescription.joins(project_media: :team).where( - "project_medias.cluster_id" => object.id, - "teams.id" => feed.team_ids - ) + field :teams, PublicTeamType.connection_type, null: true + + def teams + Team.where(id: object.team_ids) end end diff --git a/app/graph/types/feed_team_type.rb b/app/graph/types/feed_team_type.rb index 87b339fce0..42fb6abb02 100644 --- a/app/graph/types/feed_team_type.rb +++ b/app/graph/types/feed_team_type.rb @@ -6,7 +6,7 @@ class FeedTeamType < DefaultObject field :dbid, GraphQL::Types::Int, null: true field :filters, JsonStringType, null: true field :saved_search_id, GraphQL::Types::Int, null: true - field :team, TeamType, null: true + field :team, PublicTeamType, null: true field :feed, FeedType, null: true field :team_id, GraphQL::Types::Int, null: true field :feed_id, GraphQL::Types::Int, null: true diff --git a/app/graph/types/feed_type.rb b/app/graph/types/feed_type.rb index 7f258fdebf..c4e4d6b671 100644 --- a/app/graph/types/feed_type.rb +++ b/app/graph/types/feed_type.rb @@ -20,7 +20,7 @@ class FeedType < DefaultObject field :discoverable, GraphQL::Types::Boolean, null: true field :user, UserType, null: true - field :team, TeamType, null: true + field :team, PublicTeamType, null: true field :saved_search, SavedSearchType, null: true field :requests, RequestType.connection_type, null: true do @@ -50,7 +50,45 @@ def feed_invitations object.feed_invitations end - field :teams, TeamType.connection_type, null: false + field :teams, PublicTeamType.connection_type, null: false field :feed_teams, FeedTeamType.connection_type, null: false field :data_points, [GraphQL::Types::Int, null: true], null: true + + field :clusters_count, GraphQL::Types::Int, null: true do + # Filters + argument :team_ids, [GraphQL::Types::Int, null: true], required: false, default_value: nil, camelize: false + argument :channels, [GraphQL::Types::Int, null: true], required: false, default_value: nil, camelize: false + argument :medias_count_min, GraphQL::Types::Int, required: false, camelize: false + argument :medias_count_max, GraphQL::Types::Int, required: false, camelize: false + argument :requests_count_min, GraphQL::Types::Int, required: false, camelize: false + argument :requests_count_max, GraphQL::Types::Int, required: false, camelize: false + argument :last_request_date, GraphQL::Types::String, required: false, camelize: false # JSON + argument :media_type, [GraphQL::Types::String, null: true], required: false, camelize: false + end + + def clusters_count(**args) + object.clusters_count(args) + end + + field :clusters, ClusterType.connection_type, null: true do + argument :offset, GraphQL::Types::Int, required: false, default_value: 0 + argument :sort, GraphQL::Types::String, required: false, default_value: 'title' + argument :sort_type, GraphQL::Types::String, required: false, camelize: false, default_value: 'ASC' + # Filters + argument :team_ids, [GraphQL::Types::Int, null: true], required: false, default_value: nil, camelize: false + argument :channels, [GraphQL::Types::Int, null: true], required: false, default_value: nil, camelize: false + argument :medias_count_min, GraphQL::Types::Int, required: false, camelize: false + argument :medias_count_max, GraphQL::Types::Int, required: false, camelize: false + argument :requests_count_min, GraphQL::Types::Int, required: false, camelize: false + argument :requests_count_max, GraphQL::Types::Int, required: false, camelize: false + argument :last_request_date, GraphQL::Types::String, required: false, camelize: false # JSON + argument :media_type, [GraphQL::Types::String, null: true], required: false, camelize: false + end + + def clusters(**args) + sort = args[:sort].to_s + order = [:title, :media_count, :requests_count, :fact_checks_count, :last_request_date].include?(sort.downcase.to_sym) ? sort.downcase.to_sym : :title + order_type = args[:sort_type].to_s.downcase.to_sym == :desc ? :desc : :asc + object.filtered_clusters(args).offset(args[:offset].to_i).order(order => order_type) + end end diff --git a/app/graph/types/me_type.rb b/app/graph/types/me_type.rb new file mode 100644 index 0000000000..71d1e950e1 --- /dev/null +++ b/app/graph/types/me_type.rb @@ -0,0 +1,157 @@ +class MeType < DefaultObject + description "Me type" + + implements GraphQL::Types::Relay::Node + + field :dbid, GraphQL::Types::Int, null: true + field :email, GraphQL::Types::String, null: true + field :unconfirmed_email, GraphQL::Types::String, null: true + field :providers, JsonStringType, null: true + field :uuid, GraphQL::Types::String, null: true + field :profile_image, GraphQL::Types::String, null: true + field :login, GraphQL::Types::String, null: true + field :name, GraphQL::Types::String, null: true + field :current_team_id, GraphQL::Types::Int, null: true + field :permissions, GraphQL::Types::String, null: true + field :jsonsettings, GraphQL::Types::String, null: true + field :number_of_teams, GraphQL::Types::Int, null: true + field :get_send_email_notifications, GraphQL::Types::Boolean, null: true + + def get_send_email_notifications + object.get_send_email_notifications + end + + field :get_send_successful_login_notifications, GraphQL::Types::Boolean, null: true + + def get_send_successful_login_notifications + object.get_send_successful_login_notifications + end + + field :get_send_failed_login_notifications, GraphQL::Types::Boolean, null: true + + def get_send_failed_login_notifications + object.get_send_failed_login_notifications + end + + field :bot_events, GraphQL::Types::String, null: true + field :is_bot, GraphQL::Types::Boolean, null: true + field :is_active, GraphQL::Types::Boolean, null: true + field :two_factor, JsonStringType, null: true + field :settings, JsonStringType, null: true + field :accepted_terms, GraphQL::Types::Boolean, null: true + field :last_accepted_terms_at, GraphQL::Types::String, null: true + field :team_ids, [GraphQL::Types::Int, null: true], null: true + + field :user_teams, GraphQL::Types::String, null: true + + def user_teams + User.current == object ? object.user_teams : {}.to_json + end + + field :last_active_at, GraphQL::Types::Int, null: true + field :completed_signup, GraphQL::Types::Boolean, null: true + + field :source_id, GraphQL::Types::Int, null: true + + field :token, GraphQL::Types::String, null: true + + def token + object.token if object == User.current + end + + field :is_admin, GraphQL::Types::Boolean, null: true + + def is_admin + object.is_admin if object == User.current + end + + field :current_project, ProjectType, null: true + + def current_project + User.current == object ? object.current_project : nil + end + + field :confirmed, GraphQL::Types::Boolean, null: true + + def confirmed + object.is_confirmed? + end + + field :source, SourceType, null: true + + def source + Source.find(object.source_id) + end + + field :current_team, TeamType, null: true + + def current_team + User.current == object ? object.current_team : nil + end + + field :bot, BotUserType, null: true + + def bot + object if object.is_bot + end + + field :team_user, TeamUserType, null: true do + argument :team_slug, GraphQL::Types::String, required: true, camelize: false + end + + def team_user(team_slug:) + tu = TeamUser + .joins(:team) + .where("teams.slug" => team_slug, :user_id => object.id) + .last + tu.nil? ? nil : TeamUser.find_if_can(tu.id, context[:ability]) + end + + field :teams, TeamType.connection_type, null: true + + def teams + return Team.none unless object == User.current + object.teams + end + + field :team_users, TeamUserType.connection_type, null: true do + argument :status, GraphQL::Types::String, required: false + end + + def team_users(status: nil) + return TeamUser.none unless object == User.current + team_users = object.team_users + team_users = team_users.where(status: status) if status + team_users + end + + field :annotations, AnnotationType.connection_type, null: true do + argument :type, GraphQL::Types::String, required: false + end + + def annotations(type: nil) + return Annotation.none unless object == User.current + type.blank? ? object.annotations : object.annotations(type) + end + + field :assignments, ProjectMediaType.connection_type, null: true do + argument :team_id, GraphQL::Types::Int, required: false, camelize: false + end + + def assignments(team_id: nil) + return ProjectMedia.none unless object == User.current + pms = Annotation.project_media_assigned_to_user(object).order("id DESC") + team_id = team_id.to_i + pms = pms.where(team_id: team_id) if team_id > 0 + # TODO: remove finished items + # pms.reject { |pm| pm.is_finished? } + pms + end + + field :feed_invitations, FeedInvitationType.connection_type, null: false + + def feed_invitations + return FeedInvitation.none if object.email.blank? || User.current != object + FeedInvitation.where(email: object.email) + end +end diff --git a/app/graph/types/project_group_type.rb b/app/graph/types/project_group_type.rb index 33ba8de6a3..aad98569c2 100644 --- a/app/graph/types/project_group_type.rb +++ b/app/graph/types/project_group_type.rb @@ -7,7 +7,7 @@ class ProjectGroupType < DefaultObject field :title, GraphQL::Types::String, null: true field :description, GraphQL::Types::String, null: true field :team_id, GraphQL::Types::Int, null: true - field :team, TeamType, null: true + field :team, PublicTeamType, null: true field :medias_count, GraphQL::Types::Int, null: true field :projects, ProjectType.connection_type, null: true diff --git a/app/graph/types/project_media_type.rb b/app/graph/types/project_media_type.rb index f0c6411982..c5fa19640b 100644 --- a/app/graph/types/project_media_type.rb +++ b/app/graph/types/project_media_type.rb @@ -37,8 +37,6 @@ class ProjectMediaType < DefaultObject field :creator_name, GraphQL::Types::String, null: true field :team_name, GraphQL::Types::String, null: true field :channel, JsonStringType, null: true - field :cluster_id, GraphQL::Types::Int, null: true - field :cluster, ClusterType, null: true field :is_suggested, GraphQL::Types::Boolean, null: true field :is_confirmed, GraphQL::Types::Boolean, null: true field :positive_tipline_search_results_count, GraphQL::Types::Int, null: true @@ -132,6 +130,18 @@ def account field :team, TeamType, null: true def team + RecordLoader + .for(Team) + .load(object.team_id) + .then do |team| + ability = context[:ability] || Ability.new + team if ability.can?(:read, team) + end + end + + field :public_team, PublicTeamType, null: true + + def public_team RecordLoader.for(Team).load(object.team_id) end diff --git a/app/graph/types/project_type.rb b/app/graph/types/project_type.rb index 034e30cb4e..ab39118c35 100644 --- a/app/graph/types/project_type.rb +++ b/app/graph/types/project_type.rb @@ -13,7 +13,7 @@ class ProjectType < DefaultObject field :search_id, GraphQL::Types::String, null: true field :url, GraphQL::Types::String, null: true field :search, CheckSearchType, null: true - field :team, TeamType, null: true + field :team, PublicTeamType, null: true field :project_group_id, GraphQL::Types::Int, null: true field :project_group, ProjectGroupType, null: true field :privacy, GraphQL::Types::Int, null: true diff --git a/app/graph/types/public_team_type.rb b/app/graph/types/public_team_type.rb index a7c9794d66..0ed13cf45d 100644 --- a/app/graph/types/public_team_type.rb +++ b/app/graph/types/public_team_type.rb @@ -17,6 +17,12 @@ def pusher_channel Team.find(object.id).pusher_channel end + field :medias_count, GraphQL::Types::Int, null: true + + def medias_count + archived_count(object) ? 0 : object.medias_count + end + field :trash_count, GraphQL::Types::Int, null: true def trash_count diff --git a/app/graph/types/query_type.rb b/app/graph/types/query_type.rb index 5e9b9b7589..9725ce21b1 100644 --- a/app/graph/types/query_type.rb +++ b/app/graph/types/query_type.rb @@ -56,7 +56,7 @@ def about end field :me, - UserType, + MeType, description: "Information about the current user", null: true @@ -232,7 +232,6 @@ def feed_team(id: nil, feed_id: nil, team_slug: nil) bot_user project_group saved_search - cluster feed request tipline_message diff --git a/app/graph/types/root_level_type.rb b/app/graph/types/root_level_type.rb index a1ef5034f5..bc458c7493 100644 --- a/app/graph/types/root_level_type.rb +++ b/app/graph/types/root_level_type.rb @@ -5,7 +5,7 @@ class RootLevelType < BaseObject global_id_field :id - field :current_user, UserType, null: true + field :current_user, MeType, null: true def current_user User.current diff --git a/app/graph/types/saved_search_type.rb b/app/graph/types/saved_search_type.rb index 1d2e4eeffe..364fc79a90 100644 --- a/app/graph/types/saved_search_type.rb +++ b/app/graph/types/saved_search_type.rb @@ -6,7 +6,7 @@ class SavedSearchType < DefaultObject field :dbid, GraphQL::Types::Int, null: true field :title, GraphQL::Types::String, null: true field :team_id, GraphQL::Types::Int, null: true - field :team, TeamType, null: true + field :team, PublicTeamType, null: true field :items_count, GraphQL::Types::Int, null: true field :filters, GraphQL::Types::String, null: true diff --git a/app/graph/types/team_task_type.rb b/app/graph/types/team_task_type.rb index e4d1848d29..97ec6e61e0 100644 --- a/app/graph/types/team_task_type.rb +++ b/app/graph/types/team_task_type.rb @@ -9,7 +9,7 @@ class TeamTaskType < DefaultObject field :options, JsonStringType, null: true field :required, GraphQL::Types::Boolean, null: true field :team_id, GraphQL::Types::Int, null: true - field :team, TeamType, null: true + field :team, PublicTeamType, null: true field :json_schema, GraphQL::Types::String, null: true field :order, GraphQL::Types::Int, null: true field :fieldset, GraphQL::Types::String, null: true diff --git a/app/graph/types/team_type.rb b/app/graph/types/team_type.rb index 2b42dbe43c..782cc07b64 100644 --- a/app/graph/types/team_type.rb +++ b/app/graph/types/team_type.rb @@ -13,7 +13,6 @@ class TeamType < DefaultObject field :members_count, GraphQL::Types::Int, null: true field :projects_count, GraphQL::Types::Int, null: true field :permissions, GraphQL::Types::String, null: true - field :get_slack_notifications_enabled, GraphQL::Types::String, null: true field :get_slack_webhook, GraphQL::Types::String, null: true field :get_embed_whitelist, GraphQL::Types::String, null: true field :get_report_design_image_template, GraphQL::Types::String, null: true @@ -37,20 +36,15 @@ class TeamType < DefaultObject field :spam_count, GraphQL::Types::Int, null: true field :trash_count, GraphQL::Types::Int, null: true field :unconfirmed_count, GraphQL::Types::Int, null: true - field :get_languages, GraphQL::Types::String, null: true - field :get_language, GraphQL::Types::String, null: true field :get_language_detection, GraphQL::Types::Boolean, null: true field :get_report, JsonStringType, null: true field :get_fieldsets, JsonStringType, null: true field :list_columns, JsonStringType, null: true - field :get_data_report_url, GraphQL::Types::String, null: true field :url, GraphQL::Types::String, null: true - field :get_tipline_inbox_filters, JsonStringType, null: true - field :get_suggested_matches_filters, JsonStringType, null: true field :data_report, JsonStringType, null: true field :available_newsletter_header_types, JsonStringType, null: true # List of header type strings - field :get_outgoing_urls_utm_code, GraphQL::Types::String, null: true - field :get_shorten_outgoing_urls, GraphQL::Types::Boolean, null: true + + field :get_slack_notifications_enabled, GraphQL::Types::String, null: true def get_slack_notifications_enabled object.get_slack_notifications_enabled @@ -80,16 +74,6 @@ def get_status_target_turnaround object.get_status_target_turnaround end - field :pusher_channel, GraphQL::Types::String, null: true - field :search_id, GraphQL::Types::String, null: true - field :search, CheckSearchType, null: true - field :check_search_trash, CheckSearchType, null: true - field :check_search_unconfirmed, CheckSearchType, null: true - field :check_search_spam, CheckSearchType, null: true - field :trash_size, JsonStringType, null: true - field :public_team_id, GraphQL::Types::String, null: true - field :permissions_info, JsonStringType, null: true - field :dynamic_search_fields_json_schema, JsonStringType, null: true field :get_slack_notifications, JsonStringType, null: true def get_slack_notifications @@ -102,13 +86,6 @@ def get_rules object.get_rules end - field :rules_json_schema, GraphQL::Types::String, null: true - field :slack_notifications_json_schema, GraphQL::Types::String, null: true - field :rules_search_fields_json_schema, JsonStringType, null: true - field :medias_count, GraphQL::Types::Int, null: true - field :spam_count, GraphQL::Types::Int, null: true - field :trash_count, GraphQL::Types::Int, null: true - field :unconfirmed_count, GraphQL::Types::Int, null: true field :get_languages, GraphQL::Types::String, null: true def get_languages @@ -153,8 +130,6 @@ def get_suggested_matches_filters object.get_suggested_matches_filters end - field :data_report, JsonStringType, null: true - field :available_newsletter_header_types, JsonStringType, null: true # List of header type strings field :get_outgoing_urls_utm_code, GraphQL::Types::String, null: true def get_outgoing_urls_utm_code diff --git a/app/graph/types/tipline_request_type.rb b/app/graph/types/tipline_request_type.rb index 22f41f2c2e..0fa06c8b27 100644 --- a/app/graph/types/tipline_request_type.rb +++ b/app/graph/types/tipline_request_type.rb @@ -4,10 +4,9 @@ class TiplineRequestType < DefaultObject implements GraphQL::Types::Relay::Node field :dbid, GraphQL::Types::Int, null: true - field :value_json, JsonStringType, null: true - field :annotation, AnnotationType, null: true - field :annotation_id, GraphQL::Types::Int, null: true - field :associated_graphql_id, GraphQL::Types::String, null: true + field :associated_id, GraphQL::Types::Int, null: true + field :associated_type, GraphQL::Types::String, null: true + field :smooch_data, JsonStringType, null: true field :smooch_user_slack_channel_url, GraphQL::Types::String, null: true field :smooch_user_external_identifier, GraphQL::Types::String, null: true field :smooch_report_received_at, GraphQL::Types::Int, null: true @@ -16,4 +15,5 @@ class TiplineRequestType < DefaultObject field :smooch_report_sent_at, GraphQL::Types::Int, null: true field :smooch_report_correction_sent_at, GraphQL::Types::Int, null: true field :smooch_request_type, GraphQL::Types::String, null: true + field :associated_graphql_id, GraphQL::Types::String, null: true end diff --git a/app/graph/types/user_type.rb b/app/graph/types/user_type.rb index 18262bd974..04f1f914f0 100644 --- a/app/graph/types/user_type.rb +++ b/app/graph/types/user_type.rb @@ -5,153 +5,16 @@ class UserType < DefaultObject field :dbid, GraphQL::Types::Int, null: true field :email, GraphQL::Types::String, null: true - field :unconfirmed_email, GraphQL::Types::String, null: true - field :providers, JsonStringType, null: true - field :uuid, GraphQL::Types::String, null: true field :profile_image, GraphQL::Types::String, null: true - field :login, GraphQL::Types::String, null: true field :name, GraphQL::Types::String, null: true - field :current_team_id, GraphQL::Types::Int, null: true - field :permissions, GraphQL::Types::String, null: true - field :jsonsettings, GraphQL::Types::String, null: true - field :number_of_teams, GraphQL::Types::Int, null: true - field :get_send_email_notifications, GraphQL::Types::Boolean, null: true - - def get_send_email_notifications - object.get_send_email_notifications - end - - field :get_send_successful_login_notifications, GraphQL::Types::Boolean, null: true - - def get_send_successful_login_notifications - object.get_send_successful_login_notifications - end - - field :get_send_failed_login_notifications, GraphQL::Types::Boolean, null: true - - def get_send_failed_login_notifications - object.get_send_failed_login_notifications - end - - field :bot_events, GraphQL::Types::String, null: true + field :last_active_at, GraphQL::Types::Int, null: true field :is_bot, GraphQL::Types::Boolean, null: true field :is_active, GraphQL::Types::Boolean, null: true - field :two_factor, JsonStringType, null: true - field :settings, JsonStringType, null: true - field :accepted_terms, GraphQL::Types::Boolean, null: true - field :last_accepted_terms_at, GraphQL::Types::String, null: true - field :team_ids, [GraphQL::Types::Int, null: true], null: true - - field :user_teams, GraphQL::Types::String, null: true - - def user_teams - User.current == object ? object.user_teams : {}.to_json - end - - field :last_active_at, GraphQL::Types::Int, null: true - field :completed_signup, GraphQL::Types::Boolean, null: true - - field :source_id, GraphQL::Types::Int, null: true - - field :token, GraphQL::Types::String, null: true - - def token - object.token if object == User.current - end - - field :is_admin, GraphQL::Types::Boolean, null: true - - def is_admin - object.is_admin if object == User.current - end - - field :current_project, ProjectType, null: true - - def current_project - User.current == object ? object.current_project : nil - end - - field :confirmed, GraphQL::Types::Boolean, null: true - - def confirmed - object.is_confirmed? - end + field :number_of_teams, GraphQL::Types::Int, null: true field :source, SourceType, null: true def source Source.find(object.source_id) end - - field :current_team, TeamType, null: true - - def current_team - User.current == object ? object.current_team : nil - end - - field :bot, BotUserType, null: true - - def bot - object if object.is_bot - end - - field :team_user, TeamUserType, null: true do - argument :team_slug, GraphQL::Types::String, required: true, camelize: false - end - - def team_user(team_slug:) - tu = TeamUser - .joins(:team) - .where("teams.slug" => team_slug, :user_id => object.id) - .last - tu.nil? ? nil : TeamUser.find_if_can(tu.id, context[:ability]) - end - - field :teams, TeamType.connection_type, null: true - - def teams - return Team.none unless object == User.current - object.teams - end - - field :team_users, TeamUserType.connection_type, null: true do - argument :status, GraphQL::Types::String, required: false - end - - def team_users(status: nil) - return TeamUser.none unless object == User.current - team_users = object.team_users - team_users = team_users.where(status: status) if status - team_users - end - - field :annotations, AnnotationType.connection_type, null: true do - argument :type, GraphQL::Types::String, required: false - end - - def annotations(type: nil) - return Annotation.none unless object == User.current - type.blank? ? object.annotations : object.annotations(type) - end - - field :assignments, ProjectMediaType.connection_type, null: true do - argument :team_id, GraphQL::Types::Int, required: false, camelize: false - end - - def assignments(team_id: nil) - return ProjectMedia.none unless object == User.current - pms = Annotation.project_media_assigned_to_user(object).order("id DESC") - team_id = team_id.to_i - pms = pms.where(team_id: team_id) if team_id > 0 - # TODO: remove finished items - # pms.reject { |pm| pm.is_finished? } - pms - end - - field :feed_invitations, FeedInvitationType.connection_type, null: false - - def feed_invitations - return FeedInvitation.none if object.email.blank? || User.current != object - FeedInvitation.where(email: object.email) - end end diff --git a/app/lib/check_elastic_search.rb b/app/lib/check_elastic_search.rb index cc04fb646a..00cb5681ab 100644 --- a/app/lib/check_elastic_search.rb +++ b/app/lib/check_elastic_search.rb @@ -112,7 +112,7 @@ def store_elasticsearch_data(keys, data) def get_es_doc_obj obj = self.is_annotation? ? self.annotated : self - obj = obj.class.name == 'Cluster' ? obj.project_media : obj + obj = obj.class.name == 'Cluster' ? obj.center : obj obj&.id end diff --git a/app/lib/check_graphql.rb b/app/lib/check_graphql.rb index 136fe18fa8..66bcf34271 100644 --- a/app/lib/check_graphql.rb +++ b/app/lib/check_graphql.rb @@ -23,6 +23,8 @@ def object_from_id(id, ctx) if type_name == 'About' name = Rails.application.class.module_parent_name obj = OpenStruct.new({ name: name, version: VERSION, id: 1, type: 'About' }) + elsif type_name == 'Me' + obj = User.find_if_can(id) elsif ['Relationships', 'RelationshipsSource', 'RelationshipsTarget'].include?(type_name) obj = ProjectMedia.find_if_can(id) elsif type_name == 'CheckSearch' diff --git a/app/lib/smooch_nlu.rb b/app/lib/smooch_nlu.rb index 2af050bc4d..01e15fb32f 100644 --- a/app/lib/smooch_nlu.rb +++ b/app/lib/smooch_nlu.rb @@ -1,6 +1,6 @@ class SmoochNlu - class SmoochBotNotInstalledError < ::ArgumentError - end + class SmoochBotNotInstalledError < ::ArgumentError; end + class SmoochNluError < ::StandardError; end # FIXME: Make it more flexible # FIXME: Once we support paraphrase-multilingual-mpnet-base-v2 make it the only model used @@ -10,6 +10,8 @@ class SmoochBotNotInstalledError < ::ArgumentError Bot::Alegre::PARAPHRASE_MULTILINGUAL_MODEL => 0.6 } + NLU_GLOBAL_COUNTER_KEY = 'nlu_global_counter' + include SmoochNluMenus def initialize(team_slug) @@ -59,13 +61,23 @@ def self.disambiguation_threshold CheckConfig.get('nlu_disambiguation_threshold', 0.11, :float).to_f end - def self.alegre_matches_from_message(message, language, context, alegre_result_key) + def self.alegre_matches_from_message(message, language, context, alegre_result_key, uid) # FIXME: Raise exception if not in a tipline context (so, if Bot::Smooch.config is nil) matches = [] - team_slug = Team.find(Bot::Smooch.config['team_id']).slug - params = nil - response = nil - if Bot::Smooch.config.to_h['nlu_enabled'] + unless Bot::Smooch.config.to_h['nlu_enabled'] + return [] + end + if self.nlu_global_rate_limit_reached? + CheckSentry.notify(SmoochNluError.new('NLU global rate limit reached.')) + return [] + end + if self.nlu_user_rate_limit_reached?(uid) + CheckSentry.notify(SmoochNluError.new('NLU user rate limit reached.'), user_id: uid) + return [] + end + begin + self.increment_global_counter + team_slug = Team.find(Bot::Smooch.config['team_id']).slug # FIXME: In the future we could consider matches across all languages when options is nil # FIXME: No need to call Alegre if it's an exact match to one of the keywords # FIXME: No need to call Alegre if message has no word characters @@ -94,22 +106,45 @@ def self.alegre_matches_from_message(message, language, context, alegre_result_k ranked_options = sorted_options.map{ |o| { 'key' => o.dig('_source', 'context', alegre_result_key), 'score' => o['_score'] } } matches = ranked_options - # FIXME: Deal with ties (i.e., where two options have an equal _score or count) + # In all cases log for analysis + log = { + version: '0.1', # Update if schema changes + datetime: DateTime.current, + team_slug: team_slug, + user_query: message, + alegre_query: params, + alegre_response: response, + matches: matches + } + Rails.logger.info("[Smooch NLU] [Matches From Message] #{log.to_json}") + rescue StandardError => e + CheckSentry.notify(SmoochNluError.new("NLU exception: #{e.message}"), exception: e) + matches = [] + ensure + self.decrement_global_counter end - # In all cases log for analysis - log = { - version: "0.1", # Update if schema changes - datetime: DateTime.current, - team_slug: team_slug, - user_query: message, - alegre_query: params, - alegre_response: response, - matches: matches - } - Rails.logger.info("[Smooch NLU] [Matches From Message] #{log.to_json}") matches end + def self.nlu_global_rate_limit_reached? + redis = Redis.new(REDIS_CONFIG) + redis.get(NLU_GLOBAL_COUNTER_KEY).to_i > CheckConfig.get('nlu_global_rate_limit', 100, :integer) + end + + def self.nlu_user_rate_limit_reached?(uid) + TiplineMessage.where(uid: uid, created_at: Time.now.ago(1.minute)..Time.now, state: 'received').count > CheckConfig.get('nlu_user_rate_limit', 30, :integer) + end + + def self.increment_global_counter + redis = Redis.new(REDIS_CONFIG) + redis.incr(NLU_GLOBAL_COUNTER_KEY) + end + + def self.decrement_global_counter + redis = Redis.new(REDIS_CONFIG) + redis.decr(NLU_GLOBAL_COUNTER_KEY) + end + private def toggle!(enabled) diff --git a/app/lib/smooch_nlu_menus.rb b/app/lib/smooch_nlu_menus.rb index b1c14e26d0..80e3420e11 100644 --- a/app/lib/smooch_nlu_menus.rb +++ b/app/lib/smooch_nlu_menus.rb @@ -53,7 +53,10 @@ def list_menu_keywords(languages = nil, menus = nil, include_empty = true) def update_menu_option_keywords(language, menu, menu_option_index, keyword, operation) workflow = @smooch_bot_installation.get_smooch_workflows.find { |w| w['smooch_workflow_language'] == language } keywords = workflow["smooch_state_#{menu}"]['smooch_menu_options'][menu_option_index]['smooch_menu_option_nlu_keywords'].to_a - menu_option_id = (workflow["smooch_state_#{menu}"]['smooch_menu_options'][menu_option_index]['smooch_menu_option_id'] ||= SecureRandom.uuid) + if workflow["smooch_state_#{menu}"]['smooch_menu_options'][menu_option_index]['smooch_menu_option_id'].blank? + workflow["smooch_state_#{menu}"]['smooch_menu_options'][menu_option_index]['smooch_menu_option_id'] = SecureRandom.hex + end + menu_option_id = workflow["smooch_state_#{menu}"]['smooch_menu_options'][menu_option_index]['smooch_menu_option_id'] doc_id = Digest::MD5.hexdigest([ALEGRE_CONTEXT_KEY_MENU, @team_slug, menu, menu_option_id, keyword].join(':')) context = { context: ALEGRE_CONTEXT_KEY_MENU, @@ -67,13 +70,13 @@ def update_menu_option_keywords(language, menu, menu_option_index, keyword, oper end module ClassMethods - def menu_options_from_message(message, language, options) + def menu_options_from_message(message, language, options, uid) return [{ 'smooch_menu_option_value' => 'main_state' }] if message == 'cancel_nlu' return [] if options.blank? context = { context: ALEGRE_CONTEXT_KEY_MENU } - matches = SmoochNlu.alegre_matches_from_message(message, language, context, 'menu_option_id') + matches = SmoochNlu.alegre_matches_from_message(message, language, context, 'menu_option_id', uid) # Select the top two menu options that exists in `options` top_options = [] matches.each do |r| diff --git a/app/models/ability.rb b/app/models/ability.rb index b39f731ce0..026accdb89 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -60,10 +60,12 @@ def admin_perms can :duplicate, Team, :id => @context_team.id can :set_privacy, Project, :team_id => @context_team.id can :read_feed_invitations, Feed, :team_id => @context_team.id - can [:create, :update, :read, :destroy], [Feed, FeedTeam], :team_id => @context_team.id + can :destroy, Feed, :team_id => @context_team.id + can :destroy, Cluster, { feed: { team_id: @context_team.id } } + can [:create, :update], FeedTeam, :team_id => @context_team.id can [:create, :update], FeedInvitation, { feed: { team_id: @context_team.id } } can :destroy, FeedTeam do |obj| - obj.team.id == @context_team.id || obj.feed.team.id == @context_team.id + obj.team_id == @context_team.id || obj.feed.team_id == @context_team.id end end @@ -89,7 +91,7 @@ def editor_perms can [:cud], DynamicAnnotation::Field do |obj| obj.annotation.team&.id == @context_team.id end - can [:create, :update, :read, :destroy], [Account, Source, TiplineNewsletter, TiplineResource], :team_id => @context_team.id + can [:create, :update, :read, :destroy], [Account, Source, TiplineNewsletter, TiplineResource, TiplineRequest], :team_id => @context_team.id can [:cud], AccountSource, source: { team: { team_users: { team_id: @context_team.id }}} %w(annotation comment dynamic task tag).each do |annotation_type| can [:cud], annotation_type.classify.constantize do |obj| @@ -107,9 +109,10 @@ def editor_perms can :send, TiplineMessage do |obj| obj.team_id == @context_team.id end - can [:read], [Feed, FeedTeam], :team_id => @context_team.id + can [:read], FeedTeam, :team_id => @context_team.id can [:read], FeedInvitation, { feed: { team_id: @context_team.id } } - can [:create, :update], Feed, :team_id => @context_team.id + can [:read, :create, :update], Feed, :team_id => @context_team.id + can [:read, :create, :update], Cluster, { feed: { team_id: @context_team.id } } end def collaborator_perms @@ -135,6 +138,10 @@ def collaborator_perms obj.team&.id == @context_team.id && !obj.annotated_is_trashed? end end + can [:cud], TiplineRequest do |obj| + is_trashed = obj.associated.respond_to?(:archived) && obj.associated.archived == CheckArchivedFlags::FlagCodes::TRASHED + obj.team_id == @context_team.id && !is_trashed + end can [:create, :destroy], Assignment do |obj| type = obj.assigned_type obj = obj.assigned diff --git a/app/models/bot/alegre.rb b/app/models/bot/alegre.rb index 9ed845d759..13a788c586 100644 --- a/app/models/bot/alegre.rb +++ b/app/models/bot/alegre.rb @@ -162,7 +162,6 @@ def self.run(body) self.get_extracted_text(pm) self.get_flags(pm) self.auto_transcription(pm) - self.set_cluster(pm) handled = true end rescue StandardError => e @@ -175,29 +174,6 @@ def self.run(body) handled end - def self.set_cluster(pm, force = false) - pm = ProjectMedia.find(pm.id) - return if (!pm.cluster_id.blank? || !ProjectMedia.where(team_id: pm.team_id).where.not(cluster_id: nil).exists?) && !force - team_ids = ProjectMedia.where.not(cluster_id: nil).group(:team_id).count.keys - thresholds = { - audio: { value: CheckConfig.get('audio_cluster_similarity_threshold', 0.8, :float) }, - video: { value: CheckConfig.get('video_cluster_similarity_threshold', 0.8, :float) }, - image: { value: CheckConfig.get('image_cluster_similarity_threshold', 0.9, :float) }, - text: { value: CheckConfig.get('text_cluster_similarity_threshold', 0.9, :float) } - } - ids_and_scores = pm.similar_items_ids_and_scores(team_ids, thresholds) - main_id = ids_and_scores.max_by{ |_pm_id, score_and_context| score_and_context[:score] }&.first - main = ProjectMedia.find_by_id(main_id.to_i) - cluster = main&.cluster - unless cluster - cluster = Cluster.new - cluster.project_media = pm - cluster.skip_check_ability = true - cluster.save! - end - cluster.project_medias << pm - cluster - end def self.get_number_of_words(text) # Get the number of space-separated words (Does not work with Chinese/Japanese) diff --git a/app/models/bot/smooch.rb b/app/models/bot/smooch.rb index ebd0b3a429..65a62e1998 100644 --- a/app/models/bot/smooch.rb +++ b/app/models/bot/smooch.rb @@ -29,7 +29,6 @@ class CapiUnhandledMessageWarning < MessageDeliveryError; end include SmoochCapi include SmoochStrings include SmoochMenus - include SmoochFields include SmoochLanguage include SmoochBlocking @@ -40,19 +39,18 @@ def report_image self.get_dynamic_annotation('report_design')&.report_design_image_url end - def get_deduplicated_smooch_annotations + def get_deduplicated_tipline_requests uids = [] - annotations = [] + tipline_requests = [] ProjectMedia.where(id: self.related_items_ids).each do |pm| - pm.get_annotations('smooch').find_each do |annotation| - data = JSON.parse(annotation.load.get_field_value('smooch_data')) - uid = data['authorId'] + pm.tipline_requests.find_each do |tr| + uid = tr.tipline_user_uid next if uids.include?(uid) uids << uid - annotations << annotation + tipline_requests << tr end end - annotations + tipline_requests end end @@ -125,7 +123,9 @@ def self.inherit_status_and_send_report(rid) '_id': Digest::MD5.hexdigest([self.action.to_s, Time.now.to_f.to_s].join(':')), authorId: id, type: 'text', - text: message[1] + text: message[1], + source: { type: "whatsapp" }, + language: 'en' }.with_indifferent_access Bot::Smooch.save_message_later(payload, app_id) end @@ -295,14 +295,14 @@ def self.run(body) 'capi:verification' when 'message:appUser' json['messages'].each do |message| - self.parse_message(message, json['app']['_id'], json) SmoochTiplineMessageWorker.perform_async(message, json) + self.parse_message(message, json['app']['_id'], json) end true when 'message:delivery:failure' self.resend_message(json) true - when 'conversation:start' + when 'conversation:start', 'conversation:referral' message = { '_id': json['conversation']['_id'], authorId: json['appUser']['_id'], @@ -570,12 +570,12 @@ def self.process_menu_option(message, state, app_id) end # ...if nothing is matched, try using the NLU feature if state != 'query' - options = SmoochNlu.menu_options_from_message(typed, language, options) + options = SmoochNlu.menu_options_from_message(typed, language, options, uid) unless options.blank? SmoochNlu.process_menu_options(uid, options, message, language, workflow, app_id) return true end - resource = TiplineResource.resource_from_message(typed, language) + resource = TiplineResource.resource_from_message(typed, language, uid) unless resource.nil? CheckStateMachine.new(uid).reset resource = self.send_resource_to_user(uid, workflow, resource.uuid, language) @@ -602,10 +602,11 @@ def self.user_received_report(message) original = begin JSON.parse(original) rescue {} end if original['fallback_template'] =~ /report/ pmids = ProjectMedia.find(original['project_media_id']).related_items_ids - DynamicAnnotation::Field.joins(:annotation).where(field_name: 'smooch_data', 'annotations.annotated_type' => 'ProjectMedia', 'annotations.annotated_id' => pmids).where("value_json ->> 'authorId' = ?", message['appUser']['_id']).each do |f| - a = f.annotation.load - a.set_fields = { smooch_report_received: Time.now.to_i }.to_json - a.save! + TiplineRequest.where(associated_type: 'ProjectMedia', associated_id: pmids, tipline_user_uid: message['appUser']['_id']).find_each do |tr| + field_name = tr.smooch_report_received_at == 0 ? 'smooch_report_received_at' : 'smooch_report_update_received_at' + tr.send("#{field_name}=", Time.now.to_i) + tr.skip_check_ability = true + tr.save! end end end @@ -934,15 +935,15 @@ def self.send_report_to_users(pm, action) report = parent.get_annotations('report_design').last&.load return if report.nil? last_published_at = report.get_field_value('last_published').to_i - parent.get_deduplicated_smooch_annotations.each do |annotation| - data = JSON.parse(annotation.load.get_field_value('smooch_data')) + parent.get_deduplicated_tipline_requests.each do |tipline_request| + data = tipline_request.smooch_data self.get_installation(self.installation_setting_id_keys, data['app_id']) if self.config.blank? - self.send_correction_to_user(data, parent, annotation, last_published_at, action, report.get_field_value('published_count').to_i) unless self.config['smooch_disabled'] + self.send_correction_to_user(data, parent, tipline_request, last_published_at, action, report.get_field_value('published_count').to_i) unless self.config['smooch_disabled'] end end - def self.send_correction_to_user(data, pm, annotation, last_published_at, action, published_count = 0) - subscribed_at = annotation.created_at + def self.send_correction_to_user(data, pm, tipline_request, last_published_at, action, published_count = 0) + subscribed_at = tipline_request.created_at self.get_platform_from_message(data) uid = data['authorId'] lang = data['language'] @@ -959,10 +960,9 @@ def self.send_correction_to_user(data, pm, annotation, last_published_at, action self.send_report_to_user(uid, data, pm, lang, 'fact_check_report') end unless field_name.blank? - annotation = annotation.load - annotation.skip_check_ability = true - annotation.set_fields = { "#{field_name}": Time.now.to_i }.to_json - annotation.save! + tipline_request.skip_check_ability = true + tipline_request.send("#{field_name}=", Time.now.to_i) + tipline_request.save! end end @@ -1017,11 +1017,11 @@ def self.send_report_from_parent_to_child(parent_id, target_id) parent = ProjectMedia.where(id: parent_id).last child = ProjectMedia.where(id: target_id).last return if parent.nil? || child.nil? - child.get_annotations('smooch').find_each do |annotation| - data = JSON.parse(annotation.load.get_field_value('smooch_data')) + child.tipline_requests.find_each do |tr| + data = tr.smooch_data self.get_platform_from_message(data) self.get_installation(self.installation_setting_id_keys, data['app_id']) if self.config.blank? - self.send_report_to_user(data['authorId'], data, parent, data['language'], 'fact_check_report') + self.send_report_to_user(tr.tipline_user_uid, data, parent, tr.language, 'fact_check_report') end end diff --git a/app/models/claim.rb b/app/models/claim.rb index a6c44d5bfa..69d7f47171 100644 --- a/app/models/claim.rb +++ b/app/models/claim.rb @@ -12,6 +12,10 @@ def media_type 'quote' end + def uuid + Media.where(type: 'Claim', quote: self.quote.to_s.strip).first&.id || self.id + end + private def remove_null_bytes diff --git a/app/models/claim_description.rb b/app/models/claim_description.rb index 6101e342e8..716323a550 100644 --- a/app/models/claim_description.rb +++ b/app/models/claim_description.rb @@ -4,6 +4,8 @@ class ClaimDescription < ApplicationRecord belongs_to :project_media has_one :fact_check, dependent: :destroy + accepts_nested_attributes_for :fact_check, reject_if: proc { |attributes| attributes['summary'].blank? } + validates_presence_of :project_media validates_uniqueness_of :project_media_id diff --git a/app/models/cluster.rb b/app/models/cluster.rb index 8a2183e197..47bf7193ee 100644 --- a/app/models/cluster.rb +++ b/app/models/cluster.rb @@ -1,15 +1,12 @@ class Cluster < ApplicationRecord - include CheckElasticSearch + has_many :cluster_project_medias, dependent: :destroy + has_many :project_medias, through: :cluster_project_medias - belongs_to :project_media # Item that is the cluster center - has_many :project_medias, dependent: :nullify, after_add: [:update_cached_fields, :update_elasticsearch_and_timestamps] # Items that belong to the cluster - validates_presence_of :project_media_id - validates_uniqueness_of :project_media_id - validate :center_is_not_part_of_another_cluster - after_destroy :update_elasticsearch + belongs_to :feed + belongs_to :project_media, optional: true # Center def center - self.project_media + self.project_media || self.items.first end def items @@ -17,151 +14,6 @@ def items end def size - self.project_medias_count - end - - def get_requests_count - self.project_medias.select(:id).collect{ |pm| pm.requests_count }.sum - end - - def get_team_names - data = {} - self.project_medias.group(:team_id).count.keys.collect{ |tid| Team.find_by_id(tid)&.name } - tids = self.project_medias.group(:team_id).count.keys - Team.where(id: tids).find_each { |t| data[t.id] = t.name } - data - end - - def get_names_of_teams_that_fact_checked_it - data = {} - j = "INNER JOIN project_medias pm ON annotations.annotated_type = 'ProjectMedia' AND annotations.annotated_id = pm.id INNER JOIN clusters c ON c.id = pm.cluster_id" - tids = Dynamic.where(annotation_type: 'report_design').where('data LIKE ?', '%state: published%') - .joins(j).where('c.id' => self.id).group(:team_id).count.keys - Team.where(id: tids).find_each { |t| data[t.id] = t.name } - # update ES count field - pm = self.project_media - unless pm.nil? - options = { - keys: ['cluster_published_reports_count'], - data: { 'cluster_published_reports_count' => data.size }, - pm_id: pm.id - } - model = { klass: pm.class.name, id: pm.id } - ElasticSearchWorker.perform_in(1.second, YAML::dump(model), YAML::dump(options), 'update_doc') - end - data - end - - def claim_descriptions - ClaimDescription.joins(:project_media).where('project_medias.cluster_id' => self.id) - end - - cached_field :team_names, - start_as: proc { |c| c.get_team_names }, - recalculate: :recalculate_team_names, - update_on: [] # Handled by an "after_add" callback above - - cached_field :fact_checked_by_team_names, - start_as: proc { |c| c.get_names_of_teams_that_fact_checked_it }, - update_es: :cached_field_fact_checked_by_team_names_es, - es_field_name: :cluster_published_reports, - recalculate: :recalculate_fact_checked_by_team_names, - update_on: [ - # Also handled by an "after_add" callback above - { - model: Dynamic, - if: proc { |d| d.annotation_type == 'report_design' }, - affected_ids: proc { |d| ProjectMedia.where(id: d.annotated.related_items_ids).group(:cluster_id).count.keys.reject{ |cid| cid.nil? } }, - events: { - save: :recalculate - } - } - ] - - cached_field :requests_count, - start_as: proc { |c| c.get_requests_count }, - update_es: true, - es_field_name: :cluster_requests_count, - recalculate: :recalculate_requests_count, - update_on: [ - { - model: Dynamic, - if: proc { |d| d.annotation_type == 'smooch' && d.annotated_type == 'ProjectMedia' }, - affected_ids: proc { |d| ProjectMedia.where(id: d.annotated.related_items_ids).group(:cluster_id).count.keys.reject{ |cid| cid.nil? } }, - events: { - create: :cached_field_cluster_requests_count_create, - destroy: :cached_field_cluster_requests_count_destroy - } - } - ] - - def recalculate_team_names - self.get_team_names - end - - def recalculate_fact_checked_by_team_names - self.get_names_of_teams_that_fact_checked_it - end - - def recalculate_requests_count - self.get_requests_count - end - - def cached_field_fact_checked_by_team_names_es(value) - value.keys - end - - private - - def center_is_not_part_of_another_cluster - errors.add(:base, I18n.t(:center_is_not_part_of_another_cluster)) if self.center&.cluster_id && self.center&.cluster_id != self.id - end - - def update_cached_fields(_item) - self.team_names(true) - self.fact_checked_by_team_names(true) - self.requests_count(true) - end - - def update_elasticsearch_and_timestamps(item) - self.first_item_at = item.created_at if item.created_at.to_i < self.first_item_at.to_i || self.first_item_at.to_i == 0 - self.last_item_at = item.created_at if item.created_at.to_i > self.last_item_at.to_i - self.skip_check_ability = true - self.save! - # update ES - pm = self.project_media - data = { - 'cluster_size' => self.project_medias.count, - 'cluster_first_item_at' => self.first_item_at.to_i, - 'cluster_last_item_at' => self.last_item_at.to_i, - 'cluster_published_reports' => self.fact_checked_by_team_names.keys, - 'cluster_published_reports_count' => self.fact_checked_by_team_names.size, - 'cluster_requests_count' => self.requests_count, - 'cluster_teams' => self.team_names.keys, - } - options = { keys: data.keys, data: data, pm_id: pm.id } - model = { klass: pm.class.name, id: pm.id } - ElasticSearchWorker.perform_in(1.second, YAML::dump(model), YAML::dump(options), 'update_doc') - end - - def update_elasticsearch - keys = ['cluster_size', 'cluster_first_item_at', 'cluster_last_item_at', 'cluster_published_reports_count', 'cluster_requests_count'] - pm = self.project_media - data = {} - keys.each { |k| data[k] = 0 } - options = { keys: keys, data: data, pm_id: pm.id } - model = { klass: pm.class.name, id: pm.id } - ElasticSearchWorker.perform_in(1.second, YAML::dump(model), YAML::dump(options), 'update_doc') - end -end - - -Dynamic.class_eval do - def cached_field_cluster_requests_count_create(target) - target.requests_count + 1 - end - - def cached_field_cluster_requests_count_destroy(target) - target.requests_count - 1 + self.project_medias.count end end diff --git a/app/models/cluster_project_media.rb b/app/models/cluster_project_media.rb new file mode 100644 index 0000000000..baca43c7dc --- /dev/null +++ b/app/models/cluster_project_media.rb @@ -0,0 +1,7 @@ +class ClusterProjectMedia < ApplicationRecord + belongs_to :cluster + belongs_to :project_media + + validates_presence_of :cluster_id + validates_presence_of :project_media_id +end diff --git a/app/models/concerns/project_media_associations.rb b/app/models/concerns/project_media_associations.rb index e2382791f5..5f9ca5c1b1 100644 --- a/app/models/concerns/project_media_associations.rb +++ b/app/models/concerns/project_media_associations.rb @@ -16,9 +16,11 @@ module ProjectMediaAssociations has_many :targets, through: :source_relationships, source: :target has_many :project_media_users, dependent: :destroy has_many :project_media_requests, dependent: :destroy + has_many :cluster_project_medias, dependent: :destroy + has_many :clusters, through: :cluster_project_medias has_one :claim_description, dependent: :destroy - belongs_to :cluster, counter_cache: :project_medias_count, optional: true belongs_to :source, optional: true + has_many :tipline_requests, as: :associated has_annotations end end diff --git a/app/models/concerns/project_media_cached_fields.rb b/app/models/concerns/project_media_cached_fields.rb index d42b7b2d9f..7da264558f 100644 --- a/app/models/concerns/project_media_cached_fields.rb +++ b/app/models/concerns/project_media_cached_fields.rb @@ -117,9 +117,9 @@ def title_or_description_update recalculate: :recalculate_requests_count, update_on: [ { - model: Dynamic, - if: proc { |d| d.annotation_type == 'smooch' && d.annotated_type == 'ProjectMedia' }, - affected_ids: proc { |d| [d.annotated_id] }, + model: TiplineRequest, + if: proc { |tr| tr.associated_type == 'ProjectMedia' }, + affected_ids: proc { |tr| [tr.associated_id] }, events: { create: :recalculate, destroy: :recalculate, @@ -133,9 +133,9 @@ def title_or_description_update recalculate: :recalculate_demand, update_on: [ { - model: Dynamic, - if: proc { |d| d.annotation_type == 'smooch' && d.annotated_type == 'ProjectMedia' }, - affected_ids: proc { |d| d.annotated.related_items_ids }, + model: TiplineRequest, + if: proc { |tr| tr.associated_type == 'ProjectMedia' }, + affected_ids: proc { |tr| tr.associated.related_items_ids }, events: { create: :recalculate, } @@ -158,9 +158,9 @@ def title_or_description_update recalculate: :recalculate_last_seen, update_on: [ { - model: Dynamic, - if: proc { |d| d.annotation_type == 'smooch' && d.annotated_type == 'ProjectMedia' }, - affected_ids: proc { |d| d.annotated&.related_items_ids.to_a }, + model: TiplineRequest, + if: proc { |tr| tr.associated_type == 'ProjectMedia' }, + affected_ids: proc { |tr| tr.associated&.related_items_ids.to_a }, events: { create: :recalculate, } @@ -459,9 +459,9 @@ def title_or_description_update recalculate: :recalculate_positive_tipline_search_results_count, update_on: [ { - model: DynamicAnnotation::Field, - if: proc { |f| f.field_name == 'smooch_request_type' && f.value == 'relevant_search_result_requests' }, - affected_ids: proc { |f| [f.annotation&.annotated_id.to_i] }, + model: TiplineRequest, + if: proc { |tr| tr.smooch_request_type == 'relevant_search_result_requests' }, + affected_ids: proc { |tr| [tr.associated_id] }, events: { save: :recalculate, destroy: :recalculate, @@ -474,9 +474,9 @@ def title_or_description_update recalculate: :recalculate_tipline_search_results_count, update_on: [ { - model: DynamicAnnotation::Field, - if: proc { |f| f.field_name == 'smooch_request_type' && ['relevant_search_result_requests', 'irrelevant_search_result_requests', 'timeout_search_requests'].include?(f.value) }, - affected_ids: proc { |f| [f.annotation&.annotated_id.to_i] }, + model: TiplineRequest, + if: proc { |tr| ['relevant_search_result_requests', 'irrelevant_search_result_requests', 'timeout_search_requests'].include?(tr.smooch_request_type) }, + affected_ids: proc { |tr| [tr.associated_id] }, events: { save: :recalculate, destroy: :recalculate, @@ -507,7 +507,7 @@ def recalculate_related_count end def recalculate_requests_count - Dynamic.where(annotation_type: 'smooch', annotated_id: self.id).count + TiplineRequest.where(associated_type: 'ProjectMedia', associated_id: self.id).count end def recalculate_demand @@ -517,10 +517,10 @@ def recalculate_demand end def recalculate_last_seen - # If it’s a main/parent item, last_seen is related to any request (smooch annotation) to that own ProjectMedia or any similar/child ProjectMedia - # If it’s not a main item (so, single or child, a.k.a. “confirmed match” or “suggestion”), then last_seen is related only to smooch annotations (requests) related to that ProjectMedia. + # If it’s a main/parent item, last_seen is related to any tipline request to that own ProjectMedia or any similar/child ProjectMedia + # If it’s not a main item (so, single or child, a.k.a. “confirmed match” or “suggestion”), then last_seen is related only to tipline requests related to that ProjectMedia. ids = self.is_parent ? self.related_items_ids : self.id - v1 = Dynamic.where(annotation_type: 'smooch', annotated_id: ids).order('created_at DESC').first&.created_at || 0 + v1 = TiplineRequest.where(associated_type: 'ProjectMedia', associated_id: ids).order('created_at DESC').first&.created_at || 0 v2 = ProjectMedia.where(id: ids).order('created_at DESC').first&.created_at || 0 [v1, v2].max.to_i end @@ -655,16 +655,12 @@ def cached_field_published_by_es(value) end def recalculate_positive_tipline_search_results_count - DynamicAnnotation::Field.where(annotation_type: 'smooch',field_name: 'smooch_request_type', value: 'relevant_search_result_requests') - .joins('INNER JOIN annotations a ON a.id = dynamic_annotation_fields.annotation_id') - .where('a.annotated_type = ? AND a.annotated_id = ?', 'ProjectMedia', self.id).count + TiplineRequest.where(associated_type: 'ProjectMedia', associated_id: self.id, smooch_request_type: 'relevant_search_result_requests').count end def recalculate_tipline_search_results_count - DynamicAnnotation::Field.where(annotation_type: 'smooch',field_name: 'smooch_request_type') - .where('value IN (?)', ['"relevant_search_result_requests"', '"irrelevant_search_result_requests"', '"timeout_search_requests"']) - .joins('INNER JOIN annotations a ON a.id = dynamic_annotation_fields.annotation_id') - .where('a.annotated_type = ? AND a.annotated_id = ?', 'ProjectMedia', self.id).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 end end diff --git a/app/models/concerns/relationship_bulk.rb b/app/models/concerns/relationship_bulk.rb index d3526b7ca4..f973d6f0c2 100644 --- a/app/models/concerns/relationship_bulk.rb +++ b/app/models/concerns/relationship_bulk.rb @@ -73,7 +73,7 @@ def run_update_callbacks(ids_json, extra_options_json) index_alias = CheckElasticSearchModel.get_index_alias es_body = [] versions = [] - callbacks = [:reset_counters, :update_counters, :set_cluster, :propagate_inversion] + callbacks = [:reset_counters, :update_counters, :propagate_inversion] target_ids = [] Relationship.where(id: ids, source_id: extra_options['source_id']).find_each do |r| target_ids << r.target_id diff --git a/app/models/concerns/smooch_blocking.rb b/app/models/concerns/smooch_blocking.rb index cdf94b9e48..47b2c91aa6 100644 --- a/app/models/concerns/smooch_blocking.rb +++ b/app/models/concerns/smooch_blocking.rb @@ -29,9 +29,10 @@ def block_user(uid) end def unblock_user(uid) - BlockedTiplineUser.where(uid: uid).last.destroy! - Rails.logger.info("[Smooch Bot] Unblocked user #{uid}") Rails.cache.delete("smooch:banned:#{uid}") + blocked_user = BlockedTiplineUser.where(uid: uid).last + blocked_user.destroy! unless blocked_user.nil? + Rails.logger.info("[Smooch Bot] Unblocked user #{uid}") end def user_blocked?(uid) diff --git a/app/models/concerns/smooch_fields.rb b/app/models/concerns/smooch_fields.rb deleted file mode 100644 index 51ea089455..0000000000 --- a/app/models/concerns/smooch_fields.rb +++ /dev/null @@ -1,110 +0,0 @@ -require 'active_support/concern' - -module SmoochFields - extend ActiveSupport::Concern - - module ClassMethods - ::DynamicAnnotation::Field.class_eval do - def smooch_user_slack_channel_url - Concurrent::Future.execute(executor: CheckGraphql::POOL) do - return unless self.field_name == 'smooch_data' - slack_channel_url = '' - data = self.value_json - unless data.nil? - key = "SmoochUserSlackChannelUrl:Team:#{self.team.id}:#{data['authorId']}" - slack_channel_url = Rails.cache.read(key) - if slack_channel_url.blank? - # obj = self.associated - obj = self.annotation.annotated - slack_channel_url = get_slack_channel_url(obj, data) - Rails.cache.write(key, slack_channel_url) unless slack_channel_url.blank? - end - end - slack_channel_url - end - end - - def smooch_user_external_identifier - Concurrent::Future.execute(executor: CheckGraphql::POOL) do - return unless self.field_name == 'smooch_data' - data = self.value_json - Rails.cache.fetch("smooch:user:external_identifier:#{data['authorId']}") do - field = DynamicAnnotation::Field.where('field_name = ? AND dynamic_annotation_fields_value(field_name, value) = ?', 'smooch_user_id', data['authorId'].to_json).last - return '' if field.nil? - user = JSON.parse(field.annotation.load.get_field_value('smooch_user_data')).with_indifferent_access[:raw][:clients][0] - case user[:platform] - when 'whatsapp' - user[:displayName] - when 'telegram', 'instagram' - '@' + user[:raw][:username].to_s - when 'messenger', 'viber', 'line' - user[:externalId] - when 'twitter' - '@' + user[:raw][:screen_name] - else - '' - end - end - end - end - - def smooch_report_received_at - Concurrent::Future.execute(executor: CheckGraphql::POOL) do - begin self.annotation.load.get_field_value('smooch_report_received').to_i rescue nil end - end - end - - def smooch_report_update_received_at - Concurrent::Future.execute(executor: CheckGraphql::POOL) do - begin - field = self.annotation.load.get_field('smooch_report_received') - field.created_at != field.updated_at ? field.value.to_i : nil - rescue - nil - end - end - end - - def smooch_report_sent_at - Concurrent::Future.execute(executor: CheckGraphql::POOL) do - begin self.annotation.load.get_field_value('smooch_report_sent_at').to_i rescue nil end - end - end - - def smooch_report_correction_sent_at - Concurrent::Future.execute(executor: CheckGraphql::POOL) do - begin self.annotation.load.get_field_value('smooch_report_correction_sent_at').to_i rescue nil end - end - end - - def smooch_request_type - Concurrent::Future.execute(executor: CheckGraphql::POOL) do - begin self.annotation.load.get_field_value('smooch_request_type') rescue nil end - end - end - - def smooch_user_request_language - Concurrent::Future.execute(executor: CheckGraphql::POOL) do - return '' unless self.field_name == 'smooch_data' - self.value_json['language'].to_s - end - end - - private - - def get_slack_channel_url(obj, data) - slack_channel_url = nil - tid = obj.team_id - smooch_user_data = DynamicAnnotation::Field.where(field_name: 'smooch_user_id', annotation_type: 'smooch_user') - .where('dynamic_annotation_fields_value(field_name, value) = ?', data['authorId'].to_json) - .joins("INNER JOIN annotations a ON a.id = dynamic_annotation_fields.annotation_id") - .where("a.annotated_type = ? AND a.annotated_id = ?", 'Team', tid).last - unless smooch_user_data.nil? - field_value = DynamicAnnotation::Field.where(field_name: 'smooch_user_slack_channel_url', annotation_type: 'smooch_user', annotation_id: smooch_user_data.annotation_id).last - slack_channel_url = field_value.value unless field_value.nil? - end - slack_channel_url - end - end - end -end diff --git a/app/models/concerns/smooch_messages.rb b/app/models/concerns/smooch_messages.rb index e9bc5ea8a9..5170a06a98 100644 --- a/app/models/concerns/smooch_messages.rb +++ b/app/models/concerns/smooch_messages.rb @@ -330,86 +330,72 @@ def default_archived_flag Bot::Alegre.team_has_alegre_bot_installed?(team_id) ? CheckArchivedFlags::FlagCodes::PENDING_SIMILARITY_ANALYSIS : CheckArchivedFlags::FlagCodes::NONE end - def save_message(message_json, app_id, author = nil, request_type = 'default_requests', annotated_obj = nil) + def save_message(message_json, app_id, author = nil, request_type = 'default_requests', associated_obj = nil) message = JSON.parse(message_json) self.get_installation(self.installation_setting_id_keys, app_id) Team.current = Team.where(id: self.config['team_id']).last - annotated = nil + associated = nil if ['default_requests', 'timeout_requests', 'irrelevant_search_result_requests'].include?(request_type) message['archived'] = ['default_requests', 'irrelevant_search_result_requests'].include?(request_type) ? self.default_archived_flag : CheckArchivedFlags::FlagCodes::UNCONFIRMED - annotated = self.create_project_media_from_message(message) + associated = self.create_project_media_from_message(message) elsif ['menu_options_requests', 'resource_requests'].include?(request_type) - annotated = annotated_obj + associated = associated_obj elsif ['relevant_search_result_requests', 'timeout_search_requests'].include?(request_type) message['archived'] = (request_type == 'relevant_search_result_requests' ? self.default_archived_flag : CheckArchivedFlags::FlagCodes::UNCONFIRMED) - annotated = self.create_project_media_from_message(message) - if annotated != annotated_obj && annotated.is_a?(ProjectMedia) - Relationship.create(relationship_type: Relationship.suggested_type, source: annotated_obj, target: annotated, user: BotUser.smooch_user) - end + associated = self.create_project_media_from_message(message) end - return if annotated.nil? + return if associated.nil? # Remember that we received this message. hash = self.message_hash(message) - Rails.cache.write("smooch:message:#{hash}", annotated.id) + Rails.cache.write("smooch:message:#{hash}", associated.id) - self.smooch_save_annotations(message, annotated, app_id, author, request_type, annotated_obj) + self.smooch_save_tipline_request(message, associated, app_id, author, request_type, associated_obj) # If item is published (or parent item), send a report right away self.get_platform_from_message(message) - self.send_report_to_user(message['authorId'], message, annotated, message['language'], 'fact_check_report') if self.should_try_to_send_report?(request_type, annotated) - end - - def smooch_save_annotations(message, annotated, app_id, author, request_type, annotated_obj) - self.create_smooch_request(annotated, message, app_id, author) - self.create_smooch_resources_and_type(annotated, annotated_obj, author, request_type) + self.send_report_to_user(message['authorId'], message, associated, message['language'], 'fact_check_report') if self.should_try_to_send_report?(request_type, associated) end - def create_smooch_request(annotated, message, app_id, author) - fields = { smooch_data: message.merge({ app_id: app_id }).to_json } + def smooch_save_tipline_request(message, associated, app_id, author, request_type, associated_obj) + fields = { smooch_data: message.merge({ app_id: app_id }) } result = self.smooch_api_get_messages(app_id, message['authorId']) fields[:smooch_conversation_id] = result.conversation.id unless result.nil? || result.conversation.nil? fields[:smooch_message_id] = message['_id'] - self.create_smooch_annotations(annotated, author, fields) + fields[:smooch_request_type] = request_type + fields[:smooch_resource_id] = associated_obj.id if request_type == 'resource_requests' && !associated_obj.nil? + self.create_tipline_requests(associated, author, fields) # Update channel values for ProjectMedia items - if annotated.class.name == 'ProjectMedia' + if associated.class.name == 'ProjectMedia' channel_value = self.get_smooch_channel(message) unless channel_value.blank? - others = annotated.channel.with_indifferent_access[:others] || [] - annotated.channel[:others] = others.concat([channel_value]).uniq - annotated.skip_check_ability = true - annotated.save! + others = associated.channel.with_indifferent_access[:others] || [] + associated.channel[:others] = others.concat([channel_value]).uniq + associated.skip_check_ability = true + associated.save! end end end - def create_smooch_resources_and_type(annotated, annotated_obj, author, request_type) - fields = { smooch_request_type: request_type } - fields[:smooch_resource_id] = annotated_obj.id if request_type == 'resource_requests' && !annotated_obj.nil? - self.create_smooch_annotations(annotated, author, fields, true) - end - - def create_smooch_annotations(annotated, author, fields, attach_to = false) + def create_tipline_requests(associated, author, fields) # TODO: By Sawy - Should handle User.current value # In this case User.current was reset by SlackNotificationWorker worker # Quick fix - assigning it again using annotated object and reset its value at the end of creation current_user = User.current User.current = author - User.current = annotated.user if User.current.nil? && annotated.respond_to?(:user) - a = nil - a = Dynamic.where(annotation_type: 'smooch', annotated_id: annotated.id, annotated_type: annotated.class.name).last if attach_to - if a.nil? - a = Dynamic.new - a.annotation_type = 'smooch' - a.annotated = annotated + User.current = associated.user if User.current.nil? && associated.respond_to?(:user) + fields = fields.with_indifferent_access + tr = TiplineRequest.new + tr.associated = associated + tr.skip_check_ability = true + tr.skip_notifications = true + tr.disable_es_callbacks = Rails.env.to_s == 'test' + fields.each do |k, v| + tr.send("#{k}=", v) if tr.respond_to?("#{k}=") end - a.skip_check_ability = true - a.skip_notifications = true - a.disable_es_callbacks = Rails.env.to_s == 'test' - a.set_fields = fields.to_json begin - a.save! + tr.save! rescue ActiveRecord::RecordNotUnique Rails.logger.info('[Smooch Bot] Not storing tipline request because it already exists.') end @@ -426,8 +412,8 @@ def send_message_on_status_change(pm_id, status, request_actor_session_id = nil) return if pm.nil? requestors_count = 0 parent = Relationship.where(target_id: pm.id).last&.source || pm - parent.get_deduplicated_smooch_annotations.each do |annotation| - data = JSON.parse(annotation.load.get_field_value('smooch_data')) + parent.get_deduplicated_tipline_requests.each do |tr| + data = tr.smooch_data self.get_installation(self.installation_setting_id_keys, data['app_id']) if self.config.blank? message = parent.team.get_status_message_for_language(status, data['language']) unless message.blank? @@ -449,8 +435,9 @@ def send_message_to_user_on_timeout(uid, language) end def reply_to_request_with_custom_message(request, message) - data = JSON.parse(request.get_field_value('smooch_data')) - self.send_custom_message_to_user(request.annotated.team, data['authorId'], data['received'], message, data['language']) + data = request.smooch_data + team = Team.find_by_id(request.team_id) + self.send_custom_message_to_user(team, request.tipline_user_uid, data['received'], message, request.language) end def send_custom_message_to_user(team, uid, timestamp, message, language) diff --git a/app/models/concerns/team_associations.rb b/app/models/concerns/team_associations.rb index 2f506787e2..1e839fddd8 100644 --- a/app/models/concerns/team_associations.rb +++ b/app/models/concerns/team_associations.rb @@ -20,6 +20,7 @@ module TeamAssociations has_many :monthly_team_statistics # No "dependent: :destroy" because we want to retain statistics has_many :tipline_messages has_many :tipline_newsletters + has_many :tipline_requests, as: :associated has_annotations end diff --git a/app/models/concerns/team_rules.rb b/app/models/concerns/team_rules.rb index 600d228829..cfe7fac3e2 100644 --- a/app/models/concerns/team_rules.rb +++ b/app/models/concerns/team_rules.rb @@ -50,10 +50,7 @@ def text_contains_keyword(text, value) def get_smooch_message(pm) smooch_message = pm.smooch_message - if smooch_message.nil? - smooch_message = begin JSON.parse(pm.get_annotations('smooch').last.load.get_field_value('smooch_data').to_s) rescue {} end - end - smooch_message + smooch_message.nil? ? pm.tipline_requests.last.smooch_data : smooch_message end def title_matches_regexp(pm, value, _rule_id) diff --git a/app/models/concerns/tipline_resource_nlu.rb b/app/models/concerns/tipline_resource_nlu.rb index 5d92c469d2..66e299c41b 100644 --- a/app/models/concerns/tipline_resource_nlu.rb +++ b/app/models/concerns/tipline_resource_nlu.rb @@ -32,11 +32,11 @@ def update_resource_keywords(keyword, operation) end module ClassMethods - def resource_from_message(message, language) + def resource_from_message(message, language, uid) context = { context: ALEGRE_CONTEXT_KEY_RESOURCE } - matches = SmoochNlu.alegre_matches_from_message(message, language, context, 'resource_id').collect{ |m| m['key'] } + matches = SmoochNlu.alegre_matches_from_message(message, language, context, 'resource_id', uid).collect{ |m| m['key'] } # Select the top resource that exists resource_id = matches.find { |id| TiplineResource.where(id: id).exists? } Rails.logger.info("[Smooch NLU] [Resource From Message] Resource ID: #{resource_id} | Message: #{message}") diff --git a/app/models/dynamic_annotation/field.rb b/app/models/dynamic_annotation/field.rb index 04547423ae..376f6e5ffa 100644 --- a/app/models/dynamic_annotation/field.rb +++ b/app/models/dynamic_annotation/field.rb @@ -100,10 +100,7 @@ def method_suggestions(prefix) def index_field_elastic_search(op) return if self.disable_es_callbacks || RequestStore.store[:disable_es_callbacks] obj = self.annotation&.project_media - unless obj.nil? - apply_field_index(obj, op) - apply_nested_field_index(obj, op) - end + apply_field_index(obj, op) unless obj.nil? end def apply_field_index(obj, op) @@ -119,22 +116,4 @@ def apply_field_index(obj, op) end obj.update_elasticsearch_doc(data.keys, data, obj.id, true) unless data.blank? end - - def apply_nested_field_index(obj, op) - if self.field_name == 'smooch_data' - if op == 'destroy' - destroy_es_items('requests', 'destroy_doc_nested', obj.id) - else - identifier = begin self.smooch_user_external_identifier&.value rescue self.smooch_user_external_identifier end - data = { - 'username' => self.value_json['name'], - 'identifier' => identifier&.gsub(/[[:space:]|-]/, ''), - 'content' => self.value_json['text'], - 'language' => self.value_json['language'], - } - options = { op: op, pm_id: obj.id, nested_key: 'requests', keys: data.keys, data: data, skip_get_data: true } - self.add_update_nested_obj(options) - end - end - end end diff --git a/app/models/feed.rb b/app/models/feed.rb index 8622ca5d33..0a2aab0538 100755 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -7,11 +7,12 @@ class Feed < ApplicationRecord has_many :feed_teams, dependent: :restrict_with_error has_many :teams, through: :feed_teams has_many :feed_invitations, dependent: :destroy + has_many :clusters belongs_to :user, optional: true belongs_to :saved_search, optional: true belongs_to :team, optional: true - before_validation :set_user_and_team, on: :create + before_validation :set_user_and_team, :set_uuid, on: :create validates_presence_of :name validates_presence_of :licenses, if: proc { |feed| feed.discoverable } validate :saved_search_belongs_to_feed_teams @@ -26,9 +27,15 @@ class Feed < ApplicationRecord validates_inclusion_of :data_points, in: DATA_POINTS.keys # Filters for the whole feed: applies to all data from all teams - def get_feed_filters + def get_feed_filters(view = :fact_check) # "view" can be :fact_check or :media filters = {} - filters.merge!({ 'report_status' => ['published'] }) if self.published + if view.to_sym == :fact_check && self.published && self.data_points.to_a.include?(1) + filters.merge!({ 'report_status' => ['published'] }) + elsif view.to_sym == :media && self.published && self.data_points.to_a.include?(2) + filters.merge!({}) # Show everything + else + filters.merge!({ 'report_status' => ['none'] }) # Invalidate the query + end filters end @@ -123,6 +130,41 @@ def search(args = {}) query.order(sort => sort_type).offset(args[:offset].to_i) end + def clusters_count(args = {}) + self.filtered_clusters(args).count + end + + def filtered_clusters(args = {}) + team_ids = args[:team_ids] + channels = args[:channels] + query = self.clusters + + # Filter by workspace + query = query.where("ARRAY[?] && team_ids", team_ids.to_a.map(&:to_i)) unless team_ids.blank? + query = query.where(team_ids: []) if team_ids&.empty? # Invalidate the query + + # Filter by channel + query = query.where("ARRAY[?] && channels", channels.to_a.map(&:to_i)) unless channels.blank? + + # Filter by media type + query = query.joins(project_media: :media).where('medias.type' => args[:media_type]) unless args[:media_type].blank? + + # Filter by date + query = query.where(last_request_date: Range.new(*format_times_search_range_filter(JSON.parse(args[:last_request_date]), nil))) unless args[:last_request_date].blank? + + # Filters by number range + { + medias_count_min: 'media_count >= ?', + medias_count_max: 'media_count <= ?', + requests_count_min: 'requests_count >= ?', + requests_count_max: 'requests_count <= ?' + }.each do |key, condition| + query = query.where(condition, args[key].to_i) unless args[key].blank? + end + + query + end + # This takes some time to run because it involves external HTTP requests and writes to the database: # 1) If the query contains a media URL, it will be downloaded... if it contains some other URL, it will be sent to Pender # 2) Requests will be made to Alegre in order to index the request media and to look for similar requests @@ -178,4 +220,8 @@ def create_feed_team def destroy_feed_team FeedTeam.where(feed: self, team: self.team).last.destroy! end + + def set_uuid + self.uuid = SecureRandom.uuid + end end diff --git a/app/models/media.rb b/app/models/media.rb index afa98d1856..fd0588611d 100644 --- a/app/models/media.rb +++ b/app/models/media.rb @@ -74,6 +74,10 @@ def domain '' end + def uuid + self.id + end + private def set_url_nil_if_empty diff --git a/app/models/monthly_team_statistic.rb b/app/models/monthly_team_statistic.rb index e8fec6ef91..02e37067a2 100644 --- a/app/models/monthly_team_statistic.rb +++ b/app/models/monthly_team_statistic.rb @@ -14,12 +14,14 @@ class MonthlyTeamStatistic < ApplicationRecord month: 'Month', # model method whatsapp_conversations: 'WhatsApp conversations', whatsapp_conversations_business: 'Business Conversations', - whatsapp_conversations_user: 'User Conversations', + whatsapp_conversations_user: 'Service Conversations', unique_users: 'Unique users', returning_users: 'Returning users', published_reports: 'Published reports', positive_searches: 'Positive searches', negative_searches: 'Negative searches', + positive_feedback: 'Positive feedback', + negative_feedback: 'Negative feedback', reports_sent_to_users: 'Reports sent to users', unique_users_who_received_report: 'Unique users who received a report', formatted_median_response_time: 'Average (median) response time', # model method diff --git a/app/models/project.rb b/app/models/project.rb index 5e622ed495..4d579bccfb 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -12,6 +12,8 @@ class Project < ApplicationRecord belongs_to :project_group, optional: true has_many :project_medias + accepts_nested_attributes_for :user, :team, :project_medias + mount_uploader :lead_image, ImageUploader before_validation :set_description_and_team_and_user, on: :create diff --git a/app/models/project_media.rb b/app/models/project_media.rb index e3772a41f8..fcda92ef89 100644 --- a/app/models/project_media.rb +++ b/app/models/project_media.rb @@ -1,6 +1,11 @@ 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 + belongs_to :media + has_one :claim_description + + accepts_nested_attributes_for :media, :claim_description + has_paper_trail on: [:create, :update, :destroy], only: [:source_id], if: proc { |_x| User.current.present? }, versions: { class_name: 'Version' } include ProjectAssociation @@ -385,8 +390,7 @@ def version_metadata(_changes) def get_requests # Get related items for parent item pm_ids = Relationship.confirmed_parent(self).id == self.id ? self.related_items_ids : [self.id] - sm_ids = Annotation.where(annotation_type: 'smooch', annotated_type: 'ProjectMedia', annotated_id: pm_ids).map(&:id) - sm_ids.blank? ? [] : DynamicAnnotation::Field.where(annotation_id: sm_ids, field_name: 'smooch_data') + TiplineRequest.where(associated_type: 'ProjectMedia', associated_id: pm_ids) end def apply_rules_and_actions_on_update @@ -479,21 +483,14 @@ def add_nested_objects(ms) ms.attributes[:assigned_user_ids] = assignments_uids.uniq # 'requests' requests = [] - fields = DynamicAnnotation::Field.joins(:annotation) - .where( - field_name: 'smooch_data', - 'annotations.annotated_id' => self.id, - 'annotations.annotation_type' => 'smooch', - 'annotations.annotated_type' => 'ProjectMedia' - ) - fields.each do |field| - identifier = begin field.smooch_user_external_identifier&.value rescue field.smooch_user_external_identifier end + TiplineRequest.where(associated_type: 'ProjectMedia', associated_id: self.id).each do |tr| + identifier = begin tr.smooch_user_external_identifier&.value rescue tr.smooch_user_external_identifier end requests << { - id: field.id, - username: field.value_json['name'], + id: tr.id, + username: tr.smooch_data['name'], identifier: identifier&.gsub(/[[:space:]|-]/, ''), - content: field.value_json['text'], - language: field.value_json['language'], + content: tr.smooch_data['text'], + language: tr.language, } end ms.attributes[:requests] = requests diff --git a/app/models/relationship.rb b/app/models/relationship.rb index 4d7cf33256..efe056edea 100644 --- a/app/models/relationship.rb +++ b/app/models/relationship.rb @@ -12,7 +12,6 @@ class Relationship < ApplicationRecord before_validation :set_user, on: :create before_validation :set_confirmed, if: :is_being_confirmed?, on: :update - before_validation :set_cluster, if: :is_being_confirmed?, on: :update validate :relationship_type_is_valid, :items_are_from_the_same_team validate :target_not_published_report, on: :create validate :similar_item_exists, on: :create, if: proc { |r| r.is_suggested? } @@ -20,7 +19,6 @@ class Relationship < ApplicationRecord validates :relationship_type, uniqueness: { scope: [:source_id, :target_id], message: :already_exists }, on: :create before_create :destroy_same_suggested_item, if: proc { |r| r.is_confirmed? } - before_update :destroy_other_suggested_items, if: proc { |r| r.is_confirmed? } after_create :move_to_same_project_as_main, prepend: true after_create :point_targets_to_new_source, :update_counters, prepend: true after_update :reset_counters, prepend: true @@ -31,7 +29,6 @@ class Relationship < ApplicationRecord after_destroy :turn_on_unmatched_field, if: proc { |r| r.is_confirmed? || r.is_suggested? } after_commit :update_counter_and_elasticsearch, on: [:create, :update] after_commit :update_counters, :destroy_elasticsearch_relation, on: :destroy - after_commit :set_cluster, on: [:create] has_paper_trail on: [:create, :update, :destroy], if: proc { |x| User.current.present? && !x.is_being_copied? }, versions: { class_name: 'Version' } @@ -282,21 +279,6 @@ def set_confirmed end end - def set_cluster - if self.relationship_type.to_json == Relationship.confirmed_type.to_json && User.current && User.current&.id != BotUser.alegre_user&.id - pm = self.target - new_cluster = self.source.cluster - old_cluster = pm.cluster - if old_cluster.nil? || (old_cluster.size == 1 && old_cluster.project_media_id == pm.id) - unless old_cluster.nil? - old_cluster.skip_check_ability = true - old_cluster.destroy! - end - new_cluster.project_medias << pm unless new_cluster.nil? - end - end - end - def turn_off_unmatched_field set_unmatched_field(0) end @@ -332,14 +314,6 @@ def destroy_same_suggested_item .destroy_all end - def destroy_other_suggested_items - # If created by Smooch Bot, destroy other suggestions to the same media - Relationship.where(target_id: self.target_id, user: BotUser.smooch_user) - .where('relationship_type = ?', Relationship.suggested_type.to_yaml) - .where('id != ?', self.id) - .destroy_all - end - def cant_be_related_to_itself errors.add(:base, I18n.t(:item_cant_be_related_to_itself)) if self.source_id == self.target_id end diff --git a/app/models/tipline_request.rb b/app/models/tipline_request.rb new file mode 100644 index 0000000000..385f64d258 --- /dev/null +++ b/app/models/tipline_request.rb @@ -0,0 +1,139 @@ +class TiplineRequest < ApplicationRecord + include CheckElasticSearch + + belongs_to :associated, polymorphic: true + belongs_to :user, optional: true + + before_validation :set_team_and_user, :set_smooch_data_fields, on: :create + + validates_presence_of :smooch_request_type, :language, :platform + validate :platform_allowed_values + + def self.request_types + %w(default_requests timeout_requests relevant_search_result_requests resource_requests irrelevant_search_result_requests timeout_search_requests menu_options_requests) + end + validates_inclusion_of :smooch_request_type, in: TiplineRequest.request_types + + after_commit :add_elasticsearch_field, on: :create + after_commit :update_elasticsearch_field, on: :update + after_commit :destroy_elasticsearch_field, on: :destroy + + def smooch_user_slack_channel_url + return if self.smooch_data.blank? + slack_channel_url = '' + data = self.smooch_data + unless data.nil? + key = "SmoochUserSlackChannelUrl:Team:#{self.team_id}:#{data['authorId']}" + slack_channel_url = Rails.cache.read(key) + if slack_channel_url.blank? + obj = self.associated + slack_channel_url = get_slack_channel_url(obj, data) + Rails.cache.write(key, slack_channel_url) unless slack_channel_url.blank? + end + end + slack_channel_url + end + + def smooch_user_external_identifier + return if self.tipline_user_uid.blank? + Rails.cache.fetch("smooch:user:external_identifier:#{self.tipline_user_uid}") do + field = DynamicAnnotation::Field.where('field_name = ? AND dynamic_annotation_fields_value(field_name, value) = ?', 'smooch_user_id', self.tipline_user_uid.to_json).last + return '' if field.nil? + smooch_user_data = JSON.parse(field.annotation.load.get_field_value('smooch_user_data')).with_indifferent_access + user = smooch_user_data&.dig('raw', 'clients', 0) || {} + case user[:platform] + when 'whatsapp' + user[:displayName] + when 'telegram', 'instagram' + '@' + user[:raw][:username].to_s + when 'messenger', 'viber', 'line' + user[:externalId] + when 'twitter' + '@' + user[:raw][:screen_name] + else + '' + end + end + end + + def smooch_user_request_language + self.language.to_s + end + + def associated_graphql_id + Base64.encode64("#{self.associated_type}/#{self.associated_id}") + end + + private + + def set_team_and_user + self.team_id ||= Team.current&.id + self.user_id ||= User.current&.id + end + + def set_smooch_data_fields + unless self.smooch_data.blank? + # Avoid PG::UntranslatableCharacter exception + value = self.smooch_data.to_json.gsub('\u0000', '') + self.smooch_data = JSON.parse(value) + self.tipline_user_uid ||= self.smooch_data.dig('authorId') + self.language ||= self.smooch_data.dig('language') + self.platform ||= self.smooch_data.dig('source', 'type') + end + end + + def platform_allowed_values + allowed_types = Bot::Smooch::SUPPORTED_INTEGRATIONS + unless allowed_types.include?(self.platform) + errors.add(:platform, I18n.t('errors.messages.platform_allowed_values_error', **{ type: self.platform, allowed_types: allowed_types.join(', ') })) + end + end + + def get_slack_channel_url(obj, data) + slack_channel_url = nil + tid = obj.team_id + smooch_user_data = DynamicAnnotation::Field.where(field_name: 'smooch_user_id', annotation_type: 'smooch_user') + .where('dynamic_annotation_fields_value(field_name, value) = ?', data['authorId'].to_json) + .joins("INNER JOIN annotations a ON a.id = dynamic_annotation_fields.annotation_id") + .where("a.annotated_type = ? AND a.annotated_id = ?", 'Team', tid).last + unless smooch_user_data.nil? + field_value = DynamicAnnotation::Field.where(field_name: 'smooch_user_slack_channel_url', annotation_type: 'smooch_user', annotation_id: smooch_user_data.annotation_id).last + slack_channel_url = field_value.value unless field_value.nil? + end + slack_channel_url + end + + def add_elasticsearch_field + index_field_elastic_search('create') + end + + def update_elasticsearch_field + index_field_elastic_search('update') + end + + def destroy_elasticsearch_field + index_field_elastic_search('destroy') + end + + protected + + def index_field_elastic_search(op) + return if self.disable_es_callbacks || RequestStore.store[:disable_es_callbacks] || self.associated_type != 'ProjectMedia' + obj = self.associated + unless obj.nil? + if op == 'destroy' + destroy_es_items('requests', 'destroy_doc_nested', obj.id) + else + identifier = begin self.smooch_user_external_identifier&.value rescue self.smooch_user_external_identifier end + data = { + 'username' => self.smooch_data['name'], + 'identifier' => identifier&.gsub(/[[:space:]|-]/, ''), + 'content' => self.smooch_data['text'], + 'language' => self.language, + } + options = { op: op, pm_id: obj.id, nested_key: 'requests', keys: data.keys, data: data, skip_get_data: true } + self.add_update_nested_obj(options) + end + end + end +end diff --git a/app/models/tipline_resource.rb b/app/models/tipline_resource.rb index 377e36c3c0..4bcb5ac8a1 100644 --- a/app/models/tipline_resource.rb +++ b/app/models/tipline_resource.rb @@ -10,6 +10,7 @@ class TiplineResource < ApplicationRecord validates_inclusion_of :language, in: ->(resource) { resource.team.get_languages.to_a } belongs_to :team, optional: true + has_many :tipline_requests, as: :associated def format_as_tipline_message message = [] diff --git a/app/models/user.rb b/app/models/user.rb index 6384fac4aa..bfa2050daa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -27,10 +27,11 @@ class ToSOrPrivacyPolicyReadError < StandardError; end has_many :fact_checks has_many :feeds has_many :feed_invitations + has_many :tipline_requests devise :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable, - :omniauthable, omniauth_providers: [:twitter, :facebook, :slack, :google_oauth2] + :omniauthable, :lockable, omniauth_providers: [:twitter, :facebook, :slack, :google_oauth2] before_create :skip_confirmation_for_non_email_provider after_create :create_source_and_account, :set_source_image, :send_welcome_email @@ -94,6 +95,10 @@ def self.from_token(token) JSON.parse(Base64.decode64(token.gsub('++n', "\n"))) end + def me + User.current&.id == self.id ? self : nil + end + def set_source_image source = self.source unless source.nil? diff --git a/config/application.rb b/config/application.rb index 8862b01770..b3a6dac1e3 100644 --- a/config/application.rb +++ b/config/application.rb @@ -99,5 +99,8 @@ class Application < Rails::Application }) config.active_record.yaml_column_permitted_classes = [Time, Symbol] + + # Rack Attack Configuration + config.middleware.use Rack::Attack end end diff --git a/config/config.yml.example b/config/config.yml.example index 4d5602cbb4..e34a778e2c 100644 --- a/config/config.yml.example +++ b/config/config.yml.example @@ -259,13 +259,19 @@ development: &default otel_traces_sampler: otel_custom_sampling_rate: - # Rate limit for tipline submissions, tipline users are blocked after reaching this limit + # Rate limits for tiplines # # OPTIONAL - # When not set, a default number will be used. + # When not set, default values are used. # tipline_user_max_messages_per_day: 1500 + nlu_global_rate_limit: 100 + nlu_user_rate_limit: 30 + devise_maximum_attempts: 5 + devise_unlock_accounts_after: 1 + login_rate_limit: 10 + api_rate_limit: 100 test: <<: *default checkdesk_base_url_private: http://api:3000 diff --git a/config/environments/test.rb b/config/environments/test.rb index 592c08505d..6e70ec23e3 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -23,7 +23,8 @@ # Show full error reports and disable caching. config.consider_all_requests_local = true - config.action_controller.perform_caching = false + config.action_controller.perform_caching = true + config.cache_store = :memory_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 0c43abae93..2268815f45 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -49,6 +49,13 @@ def http_auth_body end config.mailer = 'DeviseMailer' config.invite_for = 1.month + + # Account lockout + config.lock_strategy = :failed_attempts + config.unlock_strategy = :time + config.unlock_keys = [ :time ] + config.maximum_attempts = CheckConfig.get('devise_maximum_attempts', 5) * 2 + config.unlock_in = CheckConfig.get('devise_unlock_accounts_after', 1).hour end AuthTrail.geocode = false diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb new file mode 100644 index 0000000000..f1971262b4 --- /dev/null +++ b/config/initializers/rack_attack.rb @@ -0,0 +1,21 @@ +class Rack::Attack + # Throttle login attempts by IP address + throttle('logins/ip', limit: proc { CheckConfig.get('login_rate_limit', 10, :integer) }, period: 60.seconds) do |req| + if req.path == '/api/users/sign_in' && req.post? + req.ip + end + end + + # Throttle login attempts by email address + throttle('logins/email', limit: proc { CheckConfig.get('login_rate_limit', 10, :integer) }, period: 60.seconds) do |req| + if req.path == '/api/users/sign_in' && req.post? + # Return the email if present, nil otherwise + req.params['user']['email'].presence if req.params['user'] + end + end + + # Throttle all graphql requests by IP address + throttle('api/graphql', limit: proc { CheckConfig.get('api_rate_limit', 100, :integer) }, period: 60.seconds) do |req| + req.ip if req.path == '/api/graphql' + end +end diff --git a/config/initializers/report_designer.rb b/config/initializers/report_designer.rb index 52667edf84..79743c8b80 100644 --- a/config/initializers/report_designer.rb +++ b/config/initializers/report_designer.rb @@ -209,7 +209,7 @@ def copy_report_image_paths def sent_count if self.annotation_type == 'report_design' pmids = self.annotated.related_items_ids - DynamicAnnotation::Field.joins(:annotation).where(field_name: 'smooch_report_received', 'annotations.annotated_type' => 'ProjectMedia', 'annotations.annotated_id' => pmids).count + TiplineRequest.where(associated_type: 'ProjectMedia', associated_id: pmids).where.not(smooch_report_received_at: 0).count end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 200415dd83..6ed46bd9f0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -110,6 +110,7 @@ en: invalid_fact_check_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}' activerecord: models: link: Link diff --git a/db/migrate/20190128175927_create_smooch_annotation_type.rb b/db/migrate/20190128175927_create_smooch_annotation_type.rb deleted file mode 100644 index 7af8e20018..0000000000 --- a/db/migrate/20190128175927_create_smooch_annotation_type.rb +++ /dev/null @@ -1,8 +0,0 @@ -class CreateSmoochAnnotationType < ActiveRecord::Migration[4.2] - require 'sample_data' - include SampleData - - def change - create_annotation_type_and_fields('Smooch', { 'Data' => ['JSON', false] }) - end -end diff --git a/db/migrate/20201016004453_add_smooch_received_field_to_smooch_annotations.rb b/db/migrate/20201016004453_add_smooch_received_field_to_smooch_annotations.rb deleted file mode 100644 index 71c2ccc436..0000000000 --- a/db/migrate/20201016004453_add_smooch_received_field_to_smooch_annotations.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'sample_data' -include SampleData -class AddSmoochReceivedFieldToSmoochAnnotations < ActiveRecord::Migration[4.2] - def change - t = DynamicAnnotation::FieldType.where(field_type: 'timestamp').last || create_field_type(field_type: 'timestamp', label: 'Timestamp') - at = DynamicAnnotation::AnnotationType.where(annotation_type: 'smooch').last - unless at.nil? - create_field_instance annotation_type_object: at, name: 'smooch_report_received', label: 'Last time the requestor received a report for this request', field_type_object: t, optional: true - end - end -end diff --git a/db/migrate/20201117131952_add_conversation_id_to_smooch.rb b/db/migrate/20201117131952_add_conversation_id_to_smooch.rb deleted file mode 100644 index abe44763af..0000000000 --- a/db/migrate/20201117131952_add_conversation_id_to_smooch.rb +++ /dev/null @@ -1,10 +0,0 @@ -class AddConversationIdToSmooch < ActiveRecord::Migration[4.2] - require 'sample_data' - include SampleData - - def change - at = DynamicAnnotation::AnnotationType.where(annotation_type: 'smooch').last - ft = DynamicAnnotation::FieldType.where(field_type: 'text').last || create_field_type(field_type: 'text', label: 'Text') - create_field_instance annotation_type_object: at, name: 'smooch_conversation_id', label: 'Conversation Id', field_type_object: ft, optional: true - end -end diff --git a/db/migrate/20201207042158_add_request_type_and_resource_to_smooch.rb b/db/migrate/20201207042158_add_request_type_and_resource_to_smooch.rb deleted file mode 100644 index b12b3e74e2..0000000000 --- a/db/migrate/20201207042158_add_request_type_and_resource_to_smooch.rb +++ /dev/null @@ -1,11 +0,0 @@ -class AddRequestTypeAndResourceToSmooch < ActiveRecord::Migration[4.2] - require 'sample_data' - include SampleData - - def change - at = DynamicAnnotation::AnnotationType.where(annotation_type: 'smooch').last - ft = DynamicAnnotation::FieldType.where(field_type: 'text').last || create_field_type(field_type: 'text', label: 'Text') - create_field_instance annotation_type_object: at, name: 'smooch_request_type', label: 'Request Type', field_type_object: ft, optional: true - create_field_instance annotation_type_object: at, name: 'smooch_resource_id', label: 'Resource Id', field_type_object: ft, optional: true - end -end diff --git a/db/migrate/20231011090947_add_smooch_sent_fields_to_smooch_annotations.rb b/db/migrate/20231011090947_add_smooch_sent_fields_to_smooch_annotations.rb deleted file mode 100644 index 51bddf7827..0000000000 --- a/db/migrate/20231011090947_add_smooch_sent_fields_to_smooch_annotations.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'sample_data' -include SampleData -class AddSmoochSentFieldsToSmoochAnnotations < ActiveRecord::Migration[6.1] - def change - t = DynamicAnnotation::FieldType.where(field_type: 'timestamp').last || create_field_type(field_type: 'timestamp', label: 'Timestamp') - at = DynamicAnnotation::AnnotationType.where(annotation_type: 'smooch').last - unless at.nil? - create_field_instance annotation_type_object: at, name: 'smooch_report_correction_sent_at', label: 'Report correction sent time', field_type_object: t, optional: true - create_field_instance annotation_type_object: at, name: 'smooch_report_sent_at', label: 'Report sent time', field_type_object: t, optional: true - end - end -end diff --git a/db/migrate/20231026162554_add_smooch_message_id_field.rb b/db/migrate/20231026162554_add_smooch_message_id_field.rb deleted file mode 100644 index 0d4fb0f6bd..0000000000 --- a/db/migrate/20231026162554_add_smooch_message_id_field.rb +++ /dev/null @@ -1,11 +0,0 @@ -class AddSmoochMessageIdField < ActiveRecord::Migration[6.1] - require 'sample_data' - include SampleData - - def change - at = DynamicAnnotation::AnnotationType.where(annotation_type: 'smooch').last - ft = DynamicAnnotation::FieldType.where(field_type: 'text').last || create_field_type(field_type: 'text', label: 'Text') - create_field_instance annotation_type_object: at, name: 'smooch_message_id', label: 'Message Id', field_type_object: ft, optional: true - execute %{CREATE UNIQUE INDEX smooch_request_message_id_unique_id ON dynamic_annotation_fields (value) WHERE field_name = 'smooch_message_id' AND value <> '' AND value <> '""'} - end -end diff --git a/db/migrate/20231122054128_create_tipline_requests.rb b/db/migrate/20231122054128_create_tipline_requests.rb new file mode 100644 index 0000000000..2adc72730e --- /dev/null +++ b/db/migrate/20231122054128_create_tipline_requests.rb @@ -0,0 +1,24 @@ +class CreateTiplineRequests < ActiveRecord::Migration[6.1] + def change + create_table :tipline_requests do |t| + t.string :language, null: false, index: true + t.string :tipline_user_uid, index: true + t.string :platform, null: false, index: true + t.string :smooch_request_type, null: false + t.string :smooch_resource_id, null: true + t.string :smooch_message_id, null: true, default: '' + t.string :smooch_conversation_id, null: true + t.jsonb :smooch_data, null: false, default: {} + t.references :associated, polymorphic: true, null: false + t.references :team, null: false + t.references :user + t.integer :smooch_report_received_at, default: 0 + t.integer :smooch_report_update_received_at, default: 0 + t.integer :smooch_report_correction_sent_at, default: 0 + t.integer :smooch_report_sent_at, default: 0 + t.timestamps + end + add_index :tipline_requests, [:associated_type, :associated_id] + add_index :tipline_requests, :smooch_message_id, unique: true, where: "smooch_message_id IS NOT NULL AND smooch_message_id != ''" + end +end diff --git a/db/migrate/20240115101312_add_lockable_to_devise.rb b/db/migrate/20240115101312_add_lockable_to_devise.rb new file mode 100644 index 0000000000..2e49cd2a9c --- /dev/null +++ b/db/migrate/20240115101312_add_lockable_to_devise.rb @@ -0,0 +1,7 @@ +class AddLockableToDevise < ActiveRecord::Migration[6.1] + def change + add_column :users, :failed_attempts, :integer, default: 0, null: false + add_column :users, :unlock_token, :string + add_column :users, :locked_at, :datetime + end +end diff --git a/db/migrate/20240212055200_create_cluster_project_medias.rb b/db/migrate/20240212055200_create_cluster_project_medias.rb new file mode 100644 index 0000000000..912cef33eb --- /dev/null +++ b/db/migrate/20240212055200_create_cluster_project_medias.rb @@ -0,0 +1,11 @@ +class CreateClusterProjectMedias < ActiveRecord::Migration[6.1] + def change + create_table :cluster_project_medias do |t| + t.references :cluster + t.references :project_media + end + add_index :cluster_project_medias, [:cluster_id, :project_media_id], unique: true + add_reference :clusters, :feed, index: true + remove_reference :project_medias, :cluster, index: true + end +end diff --git a/db/migrate/20240217191704_add_new_fields_to_feed.rb b/db/migrate/20240217191704_add_new_fields_to_feed.rb new file mode 100644 index 0000000000..20ec8d640e --- /dev/null +++ b/db/migrate/20240217191704_add_new_fields_to_feed.rb @@ -0,0 +1,10 @@ +class AddNewFieldsToFeed < ActiveRecord::Migration[6.1] + def change + add_column :feeds, :uuid, :string, null: false, default: '' + add_column :feeds, :last_clusterized_at, :datetime + add_index :feeds, :uuid + Feed.find_each do |feed| + feed.update_column :uuid, SecureRandom.uuid + end + end +end diff --git a/db/migrate/20240218010550_add_fields_to_cluster.rb b/db/migrate/20240218010550_add_fields_to_cluster.rb new file mode 100644 index 0000000000..708b37249c --- /dev/null +++ b/db/migrate/20240218010550_add_fields_to_cluster.rb @@ -0,0 +1,11 @@ +class AddFieldsToCluster < ActiveRecord::Migration[6.1] + def change + add_column :clusters, :team_ids, :integer, array: true, null: false, default: [] + add_column :clusters, :channels, :integer, array: true, null: false, default: [] + add_column :clusters, :media_count, :integer, null: false, default: 0 + add_column :clusters, :requests_count, :integer, null: false, default: 0 + add_column :clusters, :fact_checks_count, :integer, null: false, default: 0 + add_column :clusters, :last_request_date, :datetime + add_column :clusters, :last_fact_check_date, :datetime + end +end diff --git a/db/migrate/20240218181609_change_project_media_columns_for_clusters.rb b/db/migrate/20240218181609_change_project_media_columns_for_clusters.rb new file mode 100644 index 0000000000..29ac9b6fea --- /dev/null +++ b/db/migrate/20240218181609_change_project_media_columns_for_clusters.rb @@ -0,0 +1,5 @@ +class ChangeProjectMediaColumnsForClusters < ActiveRecord::Migration[6.1] + def change + remove_column :clusters, :project_medias_count + end +end diff --git a/db/migrate/20240223210914_add_positive_feedback_and_negative_feedback_to_monthly_team_statistic.rb b/db/migrate/20240223210914_add_positive_feedback_and_negative_feedback_to_monthly_team_statistic.rb new file mode 100644 index 0000000000..8b6966a05f --- /dev/null +++ b/db/migrate/20240223210914_add_positive_feedback_and_negative_feedback_to_monthly_team_statistic.rb @@ -0,0 +1,6 @@ +class AddPositiveFeedbackAndNegativeFeedbackToMonthlyTeamStatistic < ActiveRecord::Migration[6.1] + def change + add_column :monthly_team_statistics, :positive_feedback, :integer + add_column :monthly_team_statistics, :negative_feedback, :integer + end +end diff --git a/db/migrate/20240223222532_add_title_to_clusters.rb b/db/migrate/20240223222532_add_title_to_clusters.rb new file mode 100644 index 0000000000..e4ec070586 --- /dev/null +++ b/db/migrate/20240223222532_add_title_to_clusters.rb @@ -0,0 +1,5 @@ +class AddTitleToClusters < ActiveRecord::Migration[6.1] + def change + add_column :clusters, :title, :string + end +end diff --git a/db/migrate/20240228014721_update_cluster_index.rb b/db/migrate/20240228014721_update_cluster_index.rb new file mode 100644 index 0000000000..7e97c9fc43 --- /dev/null +++ b/db/migrate/20240228014721_update_cluster_index.rb @@ -0,0 +1,6 @@ +class UpdateClusterIndex < ActiveRecord::Migration[6.1] + def change + remove_index :clusters, name: 'index_clusters_on_project_media_id' # It should not be unique anymore + add_index :clusters, :project_media_id + end +end diff --git a/db/migrate/20240228203460_setval_for_tipline_requests.rb b/db/migrate/20240228203460_setval_for_tipline_requests.rb new file mode 100644 index 0000000000..3fe2bca588 --- /dev/null +++ b/db/migrate/20240228203460_setval_for_tipline_requests.rb @@ -0,0 +1,9 @@ +class SetvalForTiplineRequests < ActiveRecord::Migration[6.1] + def change + # Set start value for the ID + annotation_id = DynamicAnnotation::Field.where(field_name: 'smooch_data').order('id ASC').last&.id || 0 + tipline_request_id = TiplineRequest.order('id ASC').last&.id || 0 + id = [annotation_id, tipline_request_id].max + execute "SELECT setval('tipline_requests_id_seq', #{id})" if id > 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index b3db55b598..58daa330f3 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_01_14_024701) do +ActiveRecord::Schema.define(version: 2024_02_28_203460) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -220,14 +220,31 @@ t.index ["user_id"], name: "index_claim_descriptions_on_user_id" end + create_table "cluster_project_medias", force: :cascade do |t| + t.bigint "cluster_id" + t.bigint "project_media_id" + t.index ["cluster_id", "project_media_id"], name: "index_cluster_project_medias_on_cluster_id_and_project_media_id", unique: true + t.index ["cluster_id"], name: "index_cluster_project_medias_on_cluster_id" + t.index ["project_media_id"], name: "index_cluster_project_medias_on_project_media_id" + end + create_table "clusters", force: :cascade do |t| - t.integer "project_medias_count", default: 0 t.integer "project_media_id" t.datetime "first_item_at" t.datetime "last_item_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["project_media_id"], name: "index_clusters_on_project_media_id", unique: true + t.bigint "feed_id" + t.integer "team_ids", default: [], null: false, array: true + t.integer "channels", default: [], null: false, array: true + t.integer "media_count", default: 0, null: false + t.integer "requests_count", default: 0, null: false + t.integer "fact_checks_count", default: 0, null: false + t.datetime "last_request_date" + t.datetime "last_fact_check_date" + t.string "title" + t.index ["feed_id"], name: "index_clusters_on_feed_id" + t.index ["project_media_id"], name: "index_clusters_on_project_media_id" end create_table "dynamic_annotation_annotation_types", primary_key: "annotation_type", id: :string, force: :cascade do |t| @@ -276,7 +293,6 @@ t.index ["field_type"], name: "index_dynamic_annotation_fields_on_field_type" t.index ["value"], name: "fetch_unique_id", unique: true, where: "(((field_name)::text = 'external_id'::text) AND (value <> ''::text) AND (value <> '\"\"'::text))" t.index ["value"], name: "index_status", where: "((field_name)::text = 'verification_status_status'::text)" - t.index ["value"], name: "smooch_request_message_id_unique_id", unique: true, where: "(((field_name)::text = 'smooch_message_id'::text) AND (value <> ''::text) AND (value <> '\"\"'::text))" t.index ["value"], name: "smooch_user_unique_id", unique: true, where: "(((field_name)::text = 'smooch_user_id'::text) AND (value <> ''::text) AND (value <> '\"\"'::text))" t.index ["value"], name: "translation_request_id", unique: true, where: "((field_name)::text = 'translation_request_id'::text)" t.index ["value_json"], name: "index_dynamic_annotation_fields_on_value_json", using: :gin @@ -338,9 +354,12 @@ t.integer "licenses", default: [], array: true t.boolean "discoverable", default: false t.integer "data_points", default: [], array: true + t.string "uuid", default: "", null: false + t.datetime "last_clusterized_at" t.index ["saved_search_id"], name: "index_feeds_on_saved_search_id" t.index ["team_id"], name: "index_feeds_on_team_id" t.index ["user_id"], name: "index_feeds_on_user_id" + t.index ["uuid"], name: "index_feeds_on_uuid" end create_table "login_activities", id: :serial, force: :cascade do |t| @@ -404,6 +423,8 @@ t.integer "newsletters_sent" t.integer "whatsapp_conversations_user" t.integer "whatsapp_conversations_business" + t.integer "positive_feedback" + t.integer "negative_feedback" t.index ["team_id", "platform", "language", "start_date"], name: "index_monthly_stats_team_platform_language_start", unique: true t.index ["team_id"], name: "index_monthly_team_statistics_on_team_id" end @@ -447,12 +468,11 @@ 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" t.integer "source_id" - t.integer "cluster_id" t.integer "team_id" t.jsonb "channel", default: {"main"=>0} t.boolean "read", default: false, null: false @@ -466,7 +486,6 @@ t.string "custom_title" t.string "title_field" t.index ["channel"], name: "index_project_medias_on_channel" - t.index ["cluster_id"], name: "index_project_medias_on_cluster_id" t.index ["last_seen"], name: "index_project_medias_on_last_seen" t.index ["media_id"], name: "index_project_medias_on_media_id" t.index ["project_id"], name: "index_project_medias_on_project_id" @@ -669,7 +688,6 @@ t.datetime "updated_at", null: false t.string "state" t.index ["external_id", "state"], name: "index_tipline_messages_on_external_id_and_state", unique: true - t.index ["external_id"], name: "index_tipline_messages_on_external_id" t.index ["team_id"], name: "index_tipline_messages_on_team_id" t.index ["uid"], name: "index_tipline_messages_on_uid" end @@ -715,6 +733,35 @@ t.index ["team_id"], name: "index_tipline_newsletters_on_team_id" end + create_table "tipline_requests", force: :cascade do |t| + t.string "language", null: false + t.string "tipline_user_uid" + t.string "platform", null: false + t.string "smooch_request_type", null: false + t.string "smooch_resource_id" + t.string "smooch_message_id", default: "" + t.string "smooch_conversation_id" + t.jsonb "smooch_data", default: {}, null: false + t.string "associated_type", null: false + t.bigint "associated_id", null: false + t.bigint "team_id", null: false + t.bigint "user_id" + t.integer "smooch_report_received_at", default: 0 + t.integer "smooch_report_update_received_at", default: 0 + t.integer "smooch_report_correction_sent_at", default: 0 + t.integer "smooch_report_sent_at", default: 0 + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["associated_type", "associated_id"], name: "index_tipline_requests_on_associated" + t.index ["associated_type", "associated_id"], name: "index_tipline_requests_on_associated_type_and_associated_id" + t.index ["language"], name: "index_tipline_requests_on_language" + t.index ["platform"], name: "index_tipline_requests_on_platform" + t.index ["smooch_message_id"], name: "index_tipline_requests_on_smooch_message_id", unique: true, where: "((smooch_message_id IS NOT NULL) AND ((smooch_message_id)::text <> ''::text))" + t.index ["team_id"], name: "index_tipline_requests_on_team_id" + t.index ["tipline_user_uid"], name: "index_tipline_requests_on_tipline_user_uid" + t.index ["user_id"], name: "index_tipline_requests_on_user_id" + end + create_table "tipline_resources", id: :serial, force: :cascade do |t| t.string "uuid", default: "", null: false t.string "title", default: "", null: false @@ -799,6 +846,9 @@ t.integer "consumed_timestep" t.boolean "otp_required_for_login" t.string "otp_backup_codes", array: true + t.integer "failed_attempts", default: 0, null: false + t.string "unlock_token" + t.datetime "locked_at" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true, where: "((email IS NOT NULL) AND ((email)::text <> ''::text))" t.index ["invitation_token"], name: "index_users_on_invitation_token", unique: true diff --git a/db/seeds.rb b/db/seeds.rb index f18959fdd6..51da4921a5 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,382 +1,667 @@ include SampleData require "faker" +require "byebug" Rails.env.development? || raise('To run the seeds file you should be in the development environment') -data_users = { - main_user: { - team: - { - name: "#{Faker::Company.name} / Main User: Main Team", - logo: 'rails.png' - }, - name: Faker::Name.first_name.downcase, - password: Faker::Internet.password(min_length: 8), - }, - invited_empty_user: { - team: - [ - { - name: "#{Faker::Company.name} / Invited User: Team #1", - logo: 'maçã.png' - }, - { - name: "#{Faker::Company.name} / Invited User: Team #2", - logo: 'ruby-small.png' - } - ], - name: Faker::Name.first_name.downcase, - password: Faker::Internet.password(min_length: 8), - } -} - -data_items = { - 'Link' => [ - '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/fact-checkers-and-their-mental-health-research-work-in-progress', - 'https://meedan.com/post/meedan-stands-with-rappler-in-the-fight-against-disinformation', - 'https://meedan.com/post/2022-french-elections-meedan-software-supported-agence-france-presse', - 'https://meedan.com/post/how-to-write-longform-git-commits-for-better-software-development', - 'https://meedan.com/post/welcome-smriti-singh-our-research-intern', - 'https://meedan.com/post/countdown-to-u-s-2024-meedan-coalition-to-exchange-critical-election-information-with-overlooked-voters', - 'https://meedan.com/post/a-statement-on-the-israel-gaza-war-by-meedans-ceo', - 'https://meedan.com/post/resources-to-capture-critical-evidence-from-the-israel-gaza-war', - 'https://meedan.com/post/turkeys-largest-fact-checking-group-debunks-election-related-disinformation', - 'https://meedan.com/post/meedan-joins-diverse-cohort-of-partners-committed-to-partnership-on-ais-responsible-practices-for-synthetic-media', - 'https://meedan.com/post/nurturing-equity-diversity-and-inclusion-meedans-people-first-approach', - 'https://meedan.com/post/students-find-top-spreader-of-climate-misinformation-is-most-read-online-news-publisher-in-egypt', - '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' - ], - 'UploadedAudio' => ['e-item.mp3', 'rails.mp3', 'with_cover.mp3', 'with_cover.ogg', 'with_cover.wav']*4, - 'UploadedImage' => ['large-image.jpg', 'maçã.png', 'rails-photo.jpg', 'rails.png', 'ruby-small.png']*4, - 'UploadedVideo' => ['d-item.mp4', 'rails.mp4', 'd-item.mp4', 'rails.mp4', 'd-item.mp4']*4, - 'Claim' => Array.new(20) { Faker::Lorem.paragraph(sentence_count: 10) }, -} - -data_imported_fact_checks = [ - 'https://meedan.com/post/welcome-haramoun-hamieh-our-program-manager-for-nawa', - 'https://meedan.com/post/strengthening-fact-checking-with-media-literacy-technology-and-collaboration', - 'https://meedan.com/post/highlights-from-the-work-of-meedans-partners-on-international-fact-checking', - 'https://meedan.com/post/what-is-gendered-health-misinformation-and-why-is-it-an-equity-problem-worth', - 'https://meedan.com/post/the-case-for-a-public-health-approach-to-moderate-health-misinformation', -] - def open_file(file) File.open(File.join(Rails.root, 'test', 'data', file)) end -def create_media(user, data, model_string) - model = Object.const_get(model_string) - case model_string - when 'Claim' - media = model.create!(user_id: user.id, quote: data) - when 'Link' - media = model.create!(user_id: user.id, url: data+"?timestamp=#{Time.now.to_f}") - else - media = model.create!(user_id: user.id, file: open_file(data)) - end - media -end +# claims and uploaded files can be the same +# links need different timestamps, so they are created for each user +CLAIMS_PARAMS = (Array.new(8) do + { + type: 'Claim', + quote: Faker::Lorem.paragraph(sentence_count: 8) + } +end) -def create_project_medias(user, project, team, data) - data.map { |media| ProjectMedia.create!(user_id: user.id, project: project, team: team, media: media) } +UPLOADED_AUDIO_PARAMS = (['e-item.mp3', 'with_cover.mp3', 'with_cover.ogg', 'with_cover.wav']*2).map do |audio| + { type: 'UploadedAudio', file: open_file(audio) } end -def humanize_link(link) - path = URI.parse(link).path - path.remove('/post/').underscore.humanize +UPLOADED_IMAGE_PARAMS = (['large-image.jpg', 'maçã.png', 'rails-photo.jpg', 'ruby-small.png']*2).map do |image| + { type: 'UploadedImage', file: open_file(image) } end -def create_description(project_media) - Media.last.type == "Link" ? humanize_link(Media.find(project_media.media_id).url) : Faker::Company.catch_phrase +UPLOADED_VIDEO_PARAMS = (['d-item.mp4', 'rails.mp4']*4).map do |video| + { type: 'UploadedVideo', file: open_file(video) } end -def create_claim_descriptions(user, project_medias) - project_medias.map { |project_media| ClaimDescription.create!(description: create_description(project_media), context: Faker::Lorem.sentence, user: user, project_media: project_media) } -end +MEDIAS_PARAMS = [ + *CLAIMS_PARAMS, + *UPLOADED_AUDIO_PARAMS, + *UPLOADED_IMAGE_PARAMS, + *UPLOADED_VIDEO_PARAMS, +].shuffle! -def create_fact_checks(user, claim_descriptions) - claim_descriptions.each { |claim_description| FactCheck.create!(summary: Faker::Company.catch_phrase, title: Faker::Company.name, user: user, claim_description: claim_description, language: 'en') } -end +class Setup -def fact_check_attributes(fact_check_link, user, project, team) - { - summary: Faker::Company.catch_phrase, - url: fact_check_link, - title: Faker::Company.name, - user: user, - claim_description: create_claim_description_for_imported_fact_check(user, project, team) - } -end + private -def create_blank(project, team) - ProjectMedia.create!(project: project, team: team, media: Blank.create!, channel: { main: CheckChannels::ChannelCodes::FETCH }) -end + attr_reader :user_names, :user_passwords, :user_emails, :team_names, :existing_user_email, :main_user_a -def create_claim_description_for_imported_fact_check(user, project, team) - ClaimDescription.create!(description: Faker::Company.catch_phrase, context: Faker::Lorem.sentence, user: user, project_media: create_blank(project, team)) -end + public -def create_confirmed_relationship(parent, children) - [children].flatten.each { |child| Relationship.create!(source_id: parent.id, target_id: child.id, relationship_type: Relationship.confirmed_type) } -end + attr_reader :teams, :users -def create_suggested_relationship(parent, children) - children.each { |child| Relationship.create!(source_id: parent.id, target_id: child.id, relationship_type: Relationship.suggested_type)} -end + def initialize(existing_user_email) + @existing_user_email = existing_user_email + @user_names = Array.new(3) { Faker::Name.first_name.downcase } + @user_passwords = Array.new(3) { Faker::Internet.password(min_length: 8) } + @user_emails = @user_names.map { |name| Faker::Internet.safe_email(name: name) } + @team_names = Array.new(4) { Faker::Company.name } + + users + teams + team_users + end + + def get_users_emails_and_passwords + login_credentials = user_emails.zip(user_passwords) + if existing_user_email && teams.size > 1 + login_credentials[1..] + elsif existing_user_email && teams.size == 1 + ['Added to a user, and did not create any new users.'] + elsif teams.size == 1 + login_credentials[0] + else + login_credentials + end.flatten + end + + def users + @users ||= begin + all_users = {} + all_users.merge!(invited_users) + all_users[:main_user_a] = main_user_a + all_users.each_value { |user| user.confirm && user.save! } + all_users + end + end + + def teams + @teams ||= begin + all_teams = {} + all_teams.merge!(invited_teams) + all_teams[:main_team_a] = main_team_a + all_teams + end + end + + private + + def main_user_a + @main_user_a ||= if existing_user_email + User.find_by(email: existing_user_email) + else + User.create!({ + name: user_names[0] + ' [a / main user]', + login: user_names[0] + ' [a / main user]', + email: user_emails[0], + password: user_passwords[0], + password_confirmation: user_passwords[0], + }) + end + end + + def main_team_a + if main_user_a.teams.first + main_user_a.teams.first + else + Team.create!({ + name: "#{team_names[0]} / [a] Main User: Main Team", + slug: Team.slug_from_name(team_names[0]), + logo: open_file('rails.png') + }) + end + end + + def invited_users + { + invited_user_b: + { + name: user_names[1] + ' [b / invited user]', + login: user_names[1] + ' [b / invited user]', + email: user_emails[1], + password: user_passwords[1], + password_confirmation: user_passwords[1] + }, + invited_user_c: + { + name: user_names[2] + ' [c / invited user]', + login: user_names[2] + ' [c / invited user]', + email: user_emails[2], + password: user_passwords[2], + password_confirmation: user_passwords[2] + } + }.transform_values { |params| User.create!(params) } + end + + def invited_teams + { + invited_team_b1: + { + name: "#{team_names[1]} / [b] Invited User: Team #1", + slug: Team.slug_from_name(team_names[1]), + logo: open_file('maçã.png') + }, + invited_team_b2: + { + name: "#{team_names[2]} / [b] Invited User: Team #2", + slug: Team.slug_from_name(team_names[2]), + logo: open_file('ruby-small.png') + }, + invited_team_c: + { + name: "#{team_names[3]} / [c] Invited User: Team #1", + slug: Team.slug_from_name(team_names[3]), + logo: open_file('maçã.png') + } + }.transform_values { |t| Team.create!(t) } + end -def create_project_medias_with_channel(user, project, team, data) - data.map { |media| ProjectMedia.create!(user_id: user.id, project: project, team: team, media: media, channel: { main: CheckChannels::ChannelCodes::WHATSAPP })} + def team_users + if teams.size > 1 + [ + { + team: teams[:invited_team_b1], + user: users[:invited_user_b], + role: 'admin', + status: 'member' + }, + { + team: teams[:invited_team_b2], + user: users[:invited_user_b], + role: 'admin', + status: 'member' + }, + { + team: teams[:invited_team_c], + user: users[:invited_user_c], + role: 'admin', + status: 'member' + } + ].each { |params| TeamUser.create!(params) } + end + + main_team_a_team_user + end + + def main_team_a_team_user + return if @existing_user_email + TeamUser.create!({ + team: teams[:main_team_a], + user: users[:main_user_a], + role: 'admin', + status: 'member' + }) + end end -def create_tipline_user_and_data(project_media, team) - tipline_message_data = { - link: 'https://www.nytimes.com/interactive/2023/09/28/world/europe/russia-ukraine-war-map-front-line.html', - audio: "#{random_url}/wnHkwjykxOqU3SMWpEpuVzSa.oga", - video: "#{random_url}/AOVFpYOfMm_ssRUizUQhJHDD.mp4", - image: "#{random_url}/bOoeoeV9zNA51ecial0eWDG6.jpeg", - facebook: 'https://www.facebook.com/boomlive/posts/pfbid0ZoZPYTQAAmrrPR2XmpZ2BCPED1UgozxFGxSQiH68Aa6BF1Cvx2uWHyHrFrAwK7RPl', - instagram: 'https://www.instagram.com/p/CxsV1Gcskk8/?img_index=1', - tiktok: 'https://www.tiktok.com/@235flavien/video/7271360629615758597?_r=1&_t=8fFCIWTDWVt', - twitter: 'https://twitter.com/VietFactCheck/status/1697642909883892175', - youtube: 'https://www.youtube.com/watch?v=4EIHB-DG_JA', - text: Faker::Lorem.paragraph(sentence_count: 10) - } +class PopulatedWorkspaces + + private + + attr_reader :teams, :users, :invited_teams + + public + + def initialize(setup) + @teams = setup.teams + @users = setup.users + @invited_teams = teams.size > 1 + end - tipline_user_name = Faker::Name.first_name.downcase - tipline_user_surname = Faker::Name.last_name - tipline_message = tipline_message_data.values.sample((1..10).to_a.sample).join(' ') - phone = [ Faker::PhoneNumber.phone_number, Faker::PhoneNumber.cell_phone, Faker::PhoneNumber.cell_phone_in_e164, Faker::PhoneNumber.phone_number_with_country_code, Faker::PhoneNumber.cell_phone_with_country_code].sample - uid = random_string - - # Tipline user - smooch_user_data = { - 'id': uid, - 'raw': { - '_id': uid, - 'givenName': tipline_user_name, - 'surname': tipline_user_surname, - 'signedUpAt': Time.now.to_s, - 'properties': {}, - 'conversationStarted': true, - 'clients': [ + def populate_projects + projects_params_main_user_a = + { + 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| + { + media_attributes: media_params, + user: users[:main_user_a], + team: teams[:main_team_a], + claim_description_attributes: { + description: claim_title(media_params), + context: Faker::Lorem.sentence, + user: users[:main_user_a], + fact_check_attributes: fact_check_params_for_half_the_claims(index, users[:main_user_a]), + } + } + } + } + + Project.create!(projects_params_main_user_a) + + if invited_teams + project_params_invited_users = + [ { - 'id': random_string, - 'status': 'active', - 'externalId': phone, - 'active': true, - 'lastSeen': Time.now.to_s, - 'platform': 'whatsapp', - 'integrationId': random_string, - 'displayName': phone, - 'raw': { - 'profile': { - 'name': tipline_user_name - }, - 'from': phone + 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| + { + media_attributes: media_params, + user: users[:invited_user_b], + team: teams[:invited_team_b1], + claim_description_attributes: { + description: claim_title(media_params), + context: Faker::Lorem.sentence, + user: users[:invited_user_b], + fact_check_attributes: fact_check_params_for_half_the_claims(index, users[:invited_user_b]), + } + } + } + }, + { + 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| + { + media_attributes: media_params, + user: users[:invited_user_b], + team: teams[:invited_team_b2], + claim_description_attributes: { + description: claim_title(media_params), + context: Faker::Lorem.sentence, + user: users[:invited_user_b], + fact_check_attributes: fact_check_params_for_half_the_claims(index, users[:invited_user_b]), + } + } + } + }, + { + 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| + { + media_attributes: media_params, + user: users[:invited_user_c], + team: teams[:invited_team_c], + claim_description_attributes: { + description: claim_title(media_params), + context: Faker::Lorem.sentence, + user: users[:invited_user_c], + fact_check_attributes: fact_check_params_for_half_the_claims(index, users[:invited_user_c]), + } + } } } - ], - 'pendingClients': [] - }, - 'identifier': random_string, - 'app_name': random_string - } + ] - fields = { - smooch_user_id: uid, - smooch_user_app_id: random_string, - smooch_user_data: smooch_user_data.to_json - } - - Dynamic.create!(annotation_type: 'smooch_user', annotated: team, annotator: BotUser.smooch_user, set_fields: fields.to_json) - - # Tipline request - smooch_data = { - 'role': 'appUser', - 'source': { - 'type': ['whatsapp', 'telegram', 'messenger'].sample, - 'id': random_string, - 'integrationId': random_string, - 'originalMessageId': random_string, - 'originalMessageTimestamp': Time.now.to_i - }, - 'authorId': uid, - 'name': tipline_user_name, - '_id': random_string, - 'type': 'text', - 'received': Time.now.to_f, - 'text': tipline_message, - 'language': 'en', - 'mediaUrl': nil, - 'mediaSize': 0, - 'archived': 3, - 'app_id': random_string - } - - fields = { - smooch_request_type: ['default_requests', 'timeout_search_requests', 'relevant_search_result_requests'].sample, - smooch_data: smooch_data.to_json, - smooch_report_received: [Time.now.to_i, nil].sample - } + project_params_invited_users.each { |params| Project.create!(params) } + end + end - Dynamic.create!(annotation_type: 'smooch', annotated: project_media, annotator: BotUser.smooch_user, set_fields: fields.to_json) -end + def publish_fact_checks + users.each_value do |user| + fact_checks = FactCheck.where(user: user).last(items_total/2) + fact_checks[0, (fact_checks.size/2)].each { |fact_check| verify_fact_check_and_publish_report(fact_check.project_media)} + end + end -def create_tipline_requests(team, project_medias, x_times) - project_medias.each {|pm| x_times.times {create_tipline_user_and_data(pm, team)}} -end + def saved_searches + teams.each_value { |team| saved_search(team) } + end -def verify_fact_check_and_publish_report(project_media, url = '') - status = ['verified', 'false'].sample + def main_user_feed(to_be_shared) + if to_be_shared == "share_factchecks" + data_points = [1] + elsif to_be_shared == "share_everything" + data_points = [1,2] + else + [2] + end - verification_status = project_media.last_status_obj - verification_status.status = status - verification_status.save! + feed_params = { + name: "Feed Test ##{users[:main_user_a].feeds.count + 1}", + user: users[:main_user_a], + team: teams[:main_team_a], + published: true, + saved_search: SavedSearch.where(team: teams[:main_team_a]).first, + licenses: [1], + data_points: data_points + } + Feed.create!(feed_params) + end - report_design = project_media.get_dynamic_annotation('report_design') - report_design.set_fields = { status_label: status, state: 'published' }.to_json - report_design.data[:options][:published_article_url] = url - report_design.action = 'publish' - report_design.save! -end + def share_feed(feed) + return unless invited_teams + invited_users = [ users[:invited_user_b], users[:invited_user_c] ] + invited_users.each { |invited_user| feed_invitation(feed, invited_user)} + end + + def clusters(feed) + teams_project_medias.each_value do |project_medias| + project_medias.each_with_index { |project_media,index| cluster(project_media, index, feed)} + end + end + + def confirm_relationships + teams_project_medias.each_value do |project_medias| + confirmed_relationship(project_medias[0], project_medias[1]) + confirmed_relationship(project_medias[2], project_medias[3..items_total/2]) + end + end + + def suggest_relationships + teams_project_medias.each_value do |project_medias| + suggested_relationship(project_medias[2], project_medias[(items_total/2)+1..items_total-1]) + end + end + + def tipline_requests + teams_project_medias.each_value do |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 + + [ + *CLAIMS_PARAMS, + *UPLOADED_AUDIO_PARAMS, + *UPLOADED_IMAGE_PARAMS, + *UPLOADED_VIDEO_PARAMS, + *links_params, + ].shuffle! + end -def create_team_and_project_related_to_user(user, team_data) - puts 'Making Team / Workspace...' - team = create_team(team_data) - team.set_language('en') + def items_total + @items_total ||= MEDIAS_PARAMS.size + end - puts 'Making Project...' - project = create_project(title: team.name, team_id: team.id, user: user, description: '') + def title_from_link(link) + path = URI.parse(link).path + path.remove('/post/').underscore.humanize + end - puts 'Making Team User...' - create_team_user(team: team, user: user, role: 'admin') + def claim_title(media_params) + media_params[:type] == "Link" ? title_from_link(media_params[:url]) : Faker::Company.catch_phrase + end - return team, project + def fact_check_params_for_half_the_claims(index, user) + if index.even? + { + summary: Faker::Company.catch_phrase, + title: Faker::Company.name, + user: user, + language: 'en', + url: get_url_for_some_fact_checks(index) + } + else + { + summary: '', + } + end + end + + def get_url_for_some_fact_checks(index) + index % 4 == 0 ? "https://www.thespruceeats.com/step-by-step-basic-cake-recipe-304553?timestamp=#{Time.now.to_f}" : nil + end + + def verify_fact_check_and_publish_report(project_media) + status = ['verified', 'false'].sample + + verification_status = project_media.last_status_obj + verification_status.status = status + verification_status.save! + + report_design = project_media.get_dynamic_annotation('report_design') + report_design.set_fields = { status_label: status, state: 'published' }.to_json + report_design.action = 'publish' + report_design.save! + end + + def saved_search(team) + user = team.team_users.find_by(role: 'admin').user + + saved_search_params = { + title: "#{user.name.capitalize}'s list", + team: team, + filters: {created_by: user}, + } + + if team.saved_searches.empty? + SavedSearch.create!(saved_search_params) + else + team.saved_searches.first + end + end + + def feed_invitation(feed, invited_user) + feed_invitation_params = { + email: invited_user.email, + feed: feed, + user: users[:main_user_a], + state: :invited + } + FeedInvitation.create!(feed_invitation_params) + end + + def confirmed_relationship(parent, children) + [children].flatten.each { |child| Relationship.create!(source_id: parent.id, target_id: child.id, relationship_type: Relationship.confirmed_type) } + end + + def suggested_relationship(parent, children) + children.each { |child| Relationship.create!(source_id: parent.id, target_id: child.id, relationship_type: Relationship.suggested_type)} + end + + def teams_project_medias + @teams_project_medias ||= teams.transform_values { |team| team.project_medias.last(items_total).to_a } + end + + def create_tipline_user_and_data(project_media) + tipline_message_data = { + link: 'https://www.nytimes.com/interactive/2023/09/28/world/europe/russia-ukraine-war-map-front-line.html', + audio: "#{random_url}/wnHkwjykxOqU3SMWpEpuVzSa.oga", + video: "#{random_url}/AOVFpYOfMm_ssRUizUQhJHDD.mp4", + image: "#{random_url}/bOoeoeV9zNA51ecial0eWDG6.jpeg", + facebook: 'https://www.facebook.com/boomlive/posts/pfbid0ZoZPYTQAAmrrPR2XmpZ2BCPED1UgozxFGxSQiH68Aa6BF1Cvx2uWHyHrFrAwK7RPl', + instagram: 'https://www.instagram.com/p/CxsV1Gcskk8/?img_index=1', + tiktok: 'https://www.tiktok.com/@235flavien/video/7271360629615758597?_r=1&_t=8fFCIWTDWVt', + twitter: 'https://twitter.com/VietFactCheck/status/1697642909883892175', + youtube: 'https://www.youtube.com/watch?v=4EIHB-DG_JA', + text: Faker::Lorem.paragraph(sentence_count: 10) + } + + tipline_user_name = Faker::Name.first_name.downcase + tipline_user_surname = Faker::Name.last_name + tipline_message = tipline_message_data.values.sample((1..10).to_a.sample).join(' ') + phone = [ Faker::PhoneNumber.phone_number, Faker::PhoneNumber.cell_phone, Faker::PhoneNumber.cell_phone_in_e164, Faker::PhoneNumber.phone_number_with_country_code, Faker::PhoneNumber.cell_phone_with_country_code].sample + uid = random_string + + # Tipline user + smooch_user_data = { + 'id': uid, + 'raw': { + '_id': uid, + 'givenName': tipline_user_name, + 'surname': tipline_user_surname, + 'signedUpAt': Time.now.to_s, + 'properties': {}, + 'conversationStarted': true, + 'clients': [ + { + 'id': random_string, + 'status': 'active', + 'externalId': phone, + 'active': true, + 'lastSeen': Time.now.to_s, + 'platform': 'whatsapp', + 'integrationId': random_string, + 'displayName': phone, + 'raw': { + 'profile': { + 'name': tipline_user_name + }, + 'from': phone + } + } + ], + 'pendingClients': [] + }, + 'identifier': random_string, + 'app_name': random_string + } + + fields = { + smooch_user_id: uid, + smooch_user_app_id: random_string, + smooch_user_data: smooch_user_data.to_json + } + + Dynamic.create!(annotation_type: 'smooch_user', annotated: project_media.team, annotator: BotUser.smooch_user, set_fields: fields.to_json) + + # Tipline request + smooch_data = { + 'role': 'appUser', + 'source': { + 'type': ['whatsapp', 'telegram', 'messenger'].sample, + 'id': random_string, + 'integrationId': random_string, + 'originalMessageId': random_string, + 'originalMessageTimestamp': Time.now.to_i + }, + 'authorId': uid, + 'name': tipline_user_name, + '_id': random_string, + 'type': 'text', + 'received': Time.now.to_f, + 'text': tipline_message, + 'language': 'en', + 'mediaUrl': nil, + 'mediaSize': 0, + 'archived': 3, + 'app_id': random_string + } + + TiplineRequest.create!( + associated: project_media, + team_id: project_media.team_id, + smooch_request_type: ['default_requests', 'timeout_search_requests', 'relevant_search_result_requests'].sample, + smooch_data: smooch_data, + smooch_report_received_at: [Time.now.to_i, nil].sample, + user_id: BotUser.smooch_user&.id + ) + end + + def create_tipline_requests(team_project_medias) + team_project_medias.each_with_index do |project_media, index| + if index.even? + create_tipline_user_and_data(project_media) + elsif index % 3 == 0 + 17.times {create_tipline_user_and_data(project_media)} + end + end + end + + def cluster_teams + if invited_teams + [ + [teams[:main_team_a].id], + [teams[:invited_team_c].id], + [teams[:main_team_a].id, teams[:invited_team_c].id] + ].sample + else + [teams[:main_team_a].id] + end + end + + def random_channels + channels = [5, 6, 7, 8, 9, 10, 13] + channels.sample(rand(channels.size)) + end + + def cluster(project_media, index, feed) + count = index.zero? ? 0 : rand(100) + + cluster_params = { + project_media_id: project_media.id, + feed_id: feed.id, + team_ids: cluster_teams, + channels: random_channels, + media_count: count, + requests_count: count, + fact_checks_count: count, + } + Cluster.create!(cluster_params) + end end -###################### -# 0. Start the script -puts "If you want to create a new user: press 1 then enter" -puts "If you want to add more data to an existing user: press 2 then enter" +puts "If you want to create a new user: press enter" +puts "If you want to add more data to an existing user: Type user email then press enter" print ">> " answer = STDIN.gets.chomp +puts "—————" +puts "Stretch your legs, this might take a while." +puts "On a mac took about 10 minutes to create all populated workspaces." +puts "Keep track of the queues: http://localhost:3000/sidekiq" +puts "The workspaces will be fully finished when those finish running" +puts "—————" + ActiveRecord::Base.transaction do - # 1. Creating what we need for the workspace - # We create a user, team and project OR we fetch one - if answer == "1" - user = create_user(name: data_users[:main_user][:name], login: data_users[:main_user][:name], password: data_users[:main_user][:password], password_confirmation: data_users[:main_user][:password], email: Faker::Internet.safe_email(name: data_users[:main_user][:name])) - team, project = create_team_and_project_related_to_user(user, data_users[:main_user][:team]) - elsif answer == "2" - puts "Type user email then press enter" - print ">> " - email = STDIN.gets.chomp - - puts "Fetching User, Project, Team User and Team..." - user = User.find_by(email: email) - - if user.team_users.first.nil? - team, project = create_team_and_project_related_to_user(user, data_users[:main_user][:team]) - else - team_user = user.team_users.first - team = team_user.team - project = user.projects.first + begin + puts 'Creating users and teams...' + setup = Setup.new(answer.presence) # .presence : returns nil or the string + puts 'Creating projects for all users...' + populated_workspaces = PopulatedWorkspaces.new(setup) + populated_workspaces.populate_projects + puts 'Creating saved searches for all teams...' + populated_workspaces.saved_searches + puts 'Creating feed...' + feed_1 = populated_workspaces.main_user_feed("share_factchecks") + feed_2 = populated_workspaces.main_user_feed("share_everything") + puts 'Making and inviting to Shared Feed... (won\'t run if you are not creating any invited users)' + populated_workspaces.share_feed(feed_1) + populated_workspaces.share_feed(feed_2) + puts 'Making Confirmed Relationships between items...' + populated_workspaces.confirm_relationships + puts 'Making Suggested Relationships between items...' + populated_workspaces.suggest_relationships + puts 'Making Tipline requests...' + populated_workspaces.tipline_requests + puts 'Publishing half of each user\'s Fact Checks...' + populated_workspaces.publish_fact_checks + puts 'Creating Clusters' + populated_workspaces.clusters(feed_2) + rescue RuntimeError => e + if e.message.include?('We could not parse this link') + puts "—————" + puts "Creating Items failed: Couldn't create Links. \nMake sure Pender is running, or comment out Links so they are not created." + puts "—————" + else + raise e end end - # 2. Creating Items in different states - # 2.1 Create medias: claims, audios, images, videos and links - data_items.each do |media_type, medias_data| - begin - puts "Making #{media_type}..." - puts "#{media_type}: Making Medias and Project Medias..." - medias = medias_data.map { |individual_data| create_media(user, individual_data, media_type)} - project_medias = create_project_medias_with_channel(user, project, team, medias) - - puts "#{media_type}: Making Claim Descriptions and Fact Checks..." - # add claim description to all items, don't add fact checks to all - claim_descriptions = create_claim_descriptions(user, project_medias) - claim_descriptions_for_fact_checks = claim_descriptions[0..10] - create_fact_checks(user, claim_descriptions_for_fact_checks) - - puts "#{media_type}: Making Relationship: Confirmed Type and Suggested Type..." - # because we want a lot of state variety between items, we are not creating relationships for 7..13 - # send parent and child index - create_confirmed_relationship(project_medias[0], project_medias[1]) - create_confirmed_relationship(project_medias[2], project_medias[3]) - create_confirmed_relationship(project_medias[4], project_medias[5]) - create_confirmed_relationship(project_medias[6], project_medias[1]) - # send parent and children - create_suggested_relationship(project_medias[6], project_medias[14..19]) - - puts "#{media_type}: Making Relationship: Create item with many confirmed relationships" - # so the center column on the item page has a good size list to check functionality against - # https://github.com/meedan/check-api/pull/1722#issuecomment-1798729043 - # create the children we need for the relationship - confirmed_children_media = data_items.keys.flat_map do |media_type| - data_items[media_type][0..1].map { |data| create_media(user, data , media_type)} - end - confirmed_children_project_medias = create_project_medias(user, project, team, confirmed_children_media) - # send parent and children - create_confirmed_relationship(project_medias[0], confirmed_children_project_medias) - - puts "#{media_type}: Making Tipline requests..." - # we want different ammounts of requests, so we send the item and the number of requests that should be created - # we jump between numbers so it looks more real in the UI (instead of all 1 requests, then all 15 etc) - create_tipline_requests(team, project_medias.values_at(0,3,6,9,12,15,18), 1) - create_tipline_requests(team, project_medias.values_at(1,4,7,10,13,16,19), 15) - create_tipline_requests(team, project_medias.values_at(2,5,8,11,14,17), 17) - - puts "#{media_type}: Publishing Reports..." - # we want some published items to have and some to not have published_article_url, because they behave differently in the feed - # we send the published_article_url when we want one - project_medias[7..8].each { |pm| verify_fact_check_and_publish_report(pm, "https://www.thespruceeats.com/step-by-step-basic-cake-recipe-304553?timestamp=#{Time.now.to_f}")} - project_medias[9..10].each { |pm| verify_fact_check_and_publish_report(pm)} - rescue StandardError => e - if media_type != 'Link' - raise e - else - puts "Couldn't create Links. Other medias will still be created. \nIn order to create Links make sure Pender is running." - end - end + unless e + puts "—————" + puts "Created users:" + setup.get_users_emails_and_passwords.each { |user_info| puts user_info } end - - # 2.2 Create medias: imported Fact Checks - puts 'Making Imported Fact Checks...' - data_imported_fact_checks.map { |fact_check_link| create_fact_check(fact_check_attributes(fact_check_link, user, project, team)) } - - # 3. Create Shared feed - puts 'Making Shared Feed' - if team.saved_searches.empty? - saved_search = SavedSearch.create!(title: "#{user.name.capitalize}'s list", team: team, filters: {created_by: user}) - else - saved_search = team.saved_searches.first - end - feed = Feed.create!(name: "Feed Test ##{user.feeds.count + 1} [User: #{user.name.capitalize} / Team: #{team.name}]", user: user, team: team, published: true, saved_search: saved_search, licenses: [1]) - - # 4.1 Create new user with two empty workspaces - puts 'Making invited user and their 2 empty workspaces...' - invited_empty_user = create_user(name: data_users[:invited_empty_user][:name], login: data_users[:invited_empty_user][:name], password: data_users[:invited_empty_user][:password], password_confirmation: data_users[:invited_empty_user][:password], email: Faker::Internet.safe_email(name: data_users[:invited_empty_user][:name])) - data_users[:invited_empty_user][:team].each { |team| create_team_and_project_related_to_user(invited_empty_user, team) } - - # 4.2 Invite new user/empty workspace - puts 'Inviting user to main user\'s feed...' - create_feed_invitation(email: invited_empty_user.email, feed: feed, user: user) - - # FINAL. Return user information - if answer == "1" - puts "Created user: name: #{data_users[:main_user][:name]} — email: #{user.email} — password : #{data_users[:main_user][:password]}" - elsif answer == "2" - puts "Data added to user: #{user.email}" - end - puts "Created invited user / empty workspace: name: #{data_users[:invited_empty_user][:name]} — email: #{invited_empty_user.email} — password : #{data_users[:invited_empty_user][:password]}" end - + Rails.cache.clear diff --git a/lib/check_basic_abilities.rb b/lib/check_basic_abilities.rb index f9405cdf2b..904e73382b 100644 --- a/lib/check_basic_abilities.rb +++ b/lib/check_basic_abilities.rb @@ -81,12 +81,6 @@ def extra_perms_for_all_users !obj.team.private || @user.cached_teams.include?(obj.team.id) end - can :read, Cluster do |obj| - shared_team_ids = @context_team.shared_teams.map(&:id) - team_ids = (shared_team_ids & @user.cached_teams) - ProjectMedia.where(cluster_id: obj.id, team_id: shared_team_ids).exists? && !team_ids.empty? - end - can :read, BotUser do |obj| obj.get_approved || @user.cached_teams.include?(obj.team_author_id) end @@ -113,6 +107,10 @@ def extra_perms_for_all_users !(@user.cached_teams & obj.team_ids).empty? end + can :read, Cluster do |obj| + !(@user.cached_teams & obj.feed.team_ids).empty? + end + can :read, FeedTeam do |obj| @user.cached_teams.include?(obj.team_id) end diff --git a/lib/check_search.rb b/lib/check_search.rb index 3698af3071..7bf9780c9d 100644 --- a/lib/check_search.rb +++ b/lib/check_search.rb @@ -39,7 +39,8 @@ def initialize(options, file = nil, team_id = Team.current&.id) @options['es_id'] = Base64.encode64("ProjectMedia/#{@options['id']}") if @options['id'] && ['GraphQL::Types::String', 'GraphQL::Types::Int', 'String', 'Integer'].include?(@options['id'].class.name) # Apply feed filters - @options.merge!(@feed.get_feed_filters) if feed_query? + @feed_view = @options['feed_view'] || :fact_check + @options.merge!(@feed.get_feed_filters(@feed_view)) if feed_query? (Project.current ||= Project.where(id: @options['projects'].last).last) if @options['projects'].to_a.size == 1 @file = file @@ -222,10 +223,6 @@ def feed_query? !!@feed end - def clusterized_feed_query? - feed_query? && @options['clusterize'] && !@feed.published - end - def get_pg_results_for_media custom_conditions = {} core_conditions = {} @@ -290,14 +287,12 @@ def medias_query(include_related_items = self.should_include_related_items?) custom_conditions << { terms: { read: @options['read'].map(&:to_i) } } if @options.has_key?('read') custom_conditions << { terms: { cluster_teams: @options['cluster_teams'] } } if @options.has_key?('cluster_teams') core_conditions << { term: { sources_count: 0 } } unless include_related_items - core_conditions << { range: { cluster_size: { gt: 0 } } } if clusterized_feed_query? custom_conditions << { terms: { unmatched: @options['unmatched'] } } if @options.has_key?('unmatched') custom_conditions.concat keyword_conditions custom_conditions.concat tags_conditions custom_conditions.concat report_status_conditions custom_conditions.concat published_by_conditions custom_conditions.concat annotated_by_conditions - custom_conditions.concat cluster_published_reports_conditions custom_conditions.concat integer_terms_query('assigned_user_ids', 'assigned_to') custom_conditions.concat integer_terms_query('channel', 'channels') custom_conditions.concat integer_terms_query('source_id', 'sources') @@ -307,7 +302,7 @@ def medias_query(include_related_items = self.should_include_related_items?) custom_conditions.concat range_filter(:es) custom_conditions.concat numeric_range_filter custom_conditions.concat language_conditions - custom_conditions.concat fact_check_language_conditions + custom_conditions.concat fact_check_language_conditions unless feed_query? custom_conditions.concat request_language_conditions custom_conditions.concat report_language_conditions custom_conditions.concat team_tasks_conditions @@ -601,17 +596,6 @@ def search_tags_query(tags) def report_status_conditions return [] if @options['report_status'].blank? || !@options['report_status'].is_a?(Array) - if clusterized_feed_query? - conditions = [] - if (['published', 'unpublished'] - @options['report_status']).empty? - conditions << { range: { cluster_published_reports_count: { gte: 0 } } } - elsif @options['report_status'].include?('published') - conditions << { range: { cluster_published_reports_count: { gt: 0 } } } - elsif @options['report_status'].include?('unpublished') - conditions << { term: { cluster_published_reports_count: 0 } } - end - return conditions - end statuses = [] @options['report_status'].each do |status_name| status_id = ['unpublished', 'paused', 'published'].index(status_name) || -1 # Invalidate the query if an invalid status is passed @@ -636,11 +620,6 @@ def annotated_by_conditions end end - def cluster_published_reports_conditions - return [] if @options['cluster_published_reports'].blank? - [{ terms: { cluster_published_reports: [@options['cluster_published_reports']].flatten } }] - end - def doc_conditions doc_c = [] unless @options['show'].blank? @@ -747,7 +726,7 @@ def build_feed_conditions conditions = [] @feed.get_team_filters(@options['feed_team_ids']).each do |filters| team_id = filters['team_id'].to_i - conditions << CheckSearch.new(filters.to_json, nil, team_id).medias_query + conditions << CheckSearch.new(filters.merge({ show_similar: !!@options['show_similar'] }).to_json, nil, team_id).medias_query end { bool: { should: conditions } } end diff --git a/lib/check_statistics.rb b/lib/check_statistics.rb index 8b132fbc38..808ea22a62 100644 --- a/lib/check_statistics.rb +++ b/lib/check_statistics.rb @@ -3,43 +3,32 @@ class WhatsAppInsightsApiError < ::StandardError; end class << self def requests(team_id, platform, start_date, end_date, language, type = nil) - relation = Annotation - .where(annotation_type: 'smooch') - .joins("INNER JOIN dynamic_annotation_fields fs ON fs.annotation_id = annotations.id AND fs.field_name = 'smooch_data'") - .where("fs.value_json->'source'->>'type' = ?", platform) - .where("fs.value_json->>'language' = ?", language) - .where('t.id' => team_id) - .where('annotations.created_at' => start_date..end_date) - unless type.nil? - relation = relation - .joins("INNER JOIN dynamic_annotation_fields fs2 ON fs2.annotation_id = annotations.id AND fs2.field_name = 'smooch_request_type'") - .where('fs2.value' => type.to_json) - end - relation + conditions = { + created_at: start_date..end_date, + team_id: team_id, + language: language, + platform: platform + } + conditions[:smooch_request_type] = type unless type.nil? + TiplineRequest.where(conditions) end def reports_received(team_id, platform, start_date, end_date, language) - DynamicAnnotation::Field - .where(field_name: 'smooch_report_received') - .joins("INNER JOIN annotations a ON a.id = dynamic_annotation_fields.annotation_id INNER JOIN project_medias pm ON pm.id = a.annotated_id AND a.annotated_type = 'ProjectMedia' INNER JOIN dynamic_annotation_fields fs ON fs.annotation_id = a.id AND fs.field_name = 'smooch_data'") - .where('pm.team_id' => team_id) - .where("fs.value_json->'source'->>'type' = ?", platform) - .where("fs.value_json->>'language' = ?", language) - .where('dynamic_annotation_fields.created_at' => start_date..end_date) + TiplineRequest.where(team_id: team_id, language: language, smooch_report_received_at: start_date.to_datetime.to_i..end_date.to_datetime.to_i, platform: platform) end def project_media_requests(team_id, platform, start_date, end_date, language, type = nil) base = requests(team_id, platform, start_date, end_date, language, type) - base.joins("INNER JOIN project_medias pm ON pm.id = annotations.annotated_id AND annotations.annotated_type = 'ProjectMedia' INNER JOIN teams t ON t.id = pm.team_id") + base.where(associated_type: 'ProjectMedia') end def team_requests(team_id, platform, start_date, end_date, language) base = requests(team_id, platform, start_date, end_date, language) - base.joins("INNER JOIN teams t ON annotations.annotated_type = 'Team' AND t.id = annotations.annotated_id") + base.where(associated_type: 'Team') end def unique_requests_count(relation) - relation.group("fs.value_json #>> '{source,originalMessageId}'").count.size + relation.group("smooch_data #>> '{source,originalMessageId}'").count.size end def number_of_newsletters_sent(team_id, start_date, end_date, language) @@ -162,12 +151,12 @@ def get_statistics(start_date, end_date, team_id, platform, language, tracing_at uids = [] CheckTracer.in_span('CheckStatistics#unique_users', attributes: tracing_attributes) do # Number of unique users - project_media_requests(team_id, platform, start_date, end_date, language).find_each do |a| - uid = begin JSON.parse(a.load.get_field_value('smooch_data'))['authorId'] rescue nil end + project_media_requests(team_id, platform, start_date, end_date, language).find_each do |tr| + uid = tr.tipline_user_uid uids << uid if !uid.nil? && !uids.include?(uid) end - team_requests(team_id, platform, start_date, end_date, language).find_each do |a| - uid = begin JSON.parse(a.load.get_field_value('smooch_data'))['authorId'] rescue nil end + team_requests(team_id, platform, start_date, end_date, language).find_each do |tr| + uid = tr.tipline_user_uid uids << uid if !uid.nil? && !uids.include?(uid) end statistics[:unique_users] = uids.size @@ -175,7 +164,8 @@ def get_statistics(start_date, end_date, team_id, platform, language, tracing_at CheckTracer.in_span('CheckStatistics#returning_users', attributes: tracing_attributes) do # Number of returning users (at least one session in the current month, and at least one session in the last previous 2 months) - statistics[:returning_users] = DynamicAnnotation::Field.where(field_name: 'smooch_data', created_at: start_date.ago(2.months)..start_date).where("value_json->>'authorId' IN (?) AND value_json->>'language' = ?", uids, language).collect{ |f| f.value_json['authorId'] }.uniq.size + statistics[:returning_users] = TiplineRequest.where(created_at: start_date.ago(2.months)..start_date) + .where(tipline_user_uid: uids, language: language).map(&:tipline_user_uid).uniq.size end CheckTracer.in_span('CheckStatistics#reports_sent_to_users', attributes: tracing_attributes) do @@ -185,17 +175,16 @@ def get_statistics(start_date, end_date, team_id, platform, language, tracing_at CheckTracer.in_span('CheckStatistics#unique_users_who_received_report', attributes: tracing_attributes) do # Number of unique users who received a report - statistics[:unique_users_who_received_report] = [reports_received(team_id, platform, start_date, end_date, language) + project_media_requests(team_id, platform, start_date, end_date, language, 'relevant_search_result_requests')].flatten.collect do |f| - annotation = f.is_a?(Annotation) ? f : f.annotation - JSON.parse(annotation.load.get_field_value('smooch_data'))['authorId'] + statistics[:unique_users_who_received_report] = [reports_received(team_id, platform, start_date, end_date, language) + project_media_requests(team_id, platform, start_date, end_date, language, 'relevant_search_result_requests')].flatten.collect do |tr| + tr.tipline_user_uid end.uniq.size end CheckTracer.in_span('CheckStatistics#median_response_time', attributes: tracing_attributes) do # Average time to publishing times = [] - reports_received(team_id, platform, start_date, end_date, language).find_each do |f| - times << (f.created_at - f.annotation.created_at) + reports_received(team_id, platform, start_date, end_date, language).find_each do |tr| + times << (tr.smooch_report_received_at - tr.created_at.to_i) end median_response_time_in_seconds = times.size == 0 ? nil : times.sum.to_f / times.size statistics[:median_response_time] = median_response_time_in_seconds @@ -246,6 +235,8 @@ def get_statistics(start_date, end_date, team_id, platform, language, tracing_at irrelevant_results = project_media_requests(team_id, platform, start_date, end_date, language, 'irrelevant_search_result_requests').count ignored_results = project_media_requests(team_id, platform, start_date, end_date, language, 'timeout_search_requests').count statistics[:positive_searches] = relevant_results + irrelevant_results + ignored_results + statistics[:positive_feedback] = relevant_results + statistics[:negative_feedback] = irrelevant_results end CheckTracer.in_span('CheckStatistics#negative_searches', attributes: tracing_attributes) do diff --git a/lib/relay.idl b/lib/relay.idl index bc2ab83e63..2c1c049dea 100644 --- a/lib/relay.idl +++ b/lib/relay.idl @@ -479,7 +479,7 @@ type BotUser implements Node { permissions: String settings_as_json_schema(team_slug: String): String settings_ui_schema: String - team_author: Team + team_author: PublicTeam updated_at: String } @@ -635,27 +635,6 @@ type ClaimDescription implements Node { user: User } -""" -The connection type for ClaimDescription. -""" -type ClaimDescriptionConnection { - """ - A list of edges. - """ - edges: [ClaimDescriptionEdge] - - """ - A list of nodes. - """ - nodes: [ClaimDescription] - - """ - Information to aid in pagination. - """ - pageInfo: PageInfo! - totalCount: Int -} - """ An edge in a connection. """ @@ -675,34 +654,21 @@ type ClaimDescriptionEdge { Cluster type """ type Cluster implements Node { - claim_descriptions( - """ - 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 - feed_id: Int! - - """ - Returns the first _n_ elements from the list. - """ - first: Int - - """ - Returns the last _n_ elements from the list. - """ - last: Int - ): ClaimDescriptionConnection + center: ProjectMedia + channels: [Int] created_at: String dbid: Int - fact_checked_by_team_names: JsonStringType + fact_checks_count: Int first_item_at: Int id: ID! - items( + last_fact_check_date: Int + last_item_at: Int + last_request_date: Int + media_count: Int + permissions: String + requests_count: Int + team_ids: [Int] + teams( """ Returns the elements in the list that come after the specified cursor. """ @@ -712,7 +678,6 @@ type Cluster implements Node { Returns the elements in the list that come before the specified cursor. """ before: String - feed_id: Int! """ Returns the first _n_ elements from the list. @@ -723,15 +688,47 @@ type Cluster implements Node { Returns the last _n_ elements from the list. """ last: Int - ): ProjectMediaConnection - last_item_at: Int - permissions: String - requests_count: Int - size: Int - team_names: [String] + ): PublicTeamConnection + title: String updated_at: String } +""" +The connection type for Cluster. +""" +type ClusterConnection { + """ + A list of edges. + """ + edges: [ClusterEdge] + + """ + A list of nodes. + """ + nodes: [Cluster] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int +} + +""" +An edge in a connection. +""" +type ClusterEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Cluster +} + type Comment implements Node { annotated_id: String annotated_type: String @@ -8052,6 +8049,39 @@ type FactCheckEdge { Feed type """ type Feed implements Node { + clusters( + """ + 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 + channels: [Int] = null + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + last_request_date: String + media_type: [String] + medias_count_max: Int + medias_count_min: Int + offset: Int = 0 + requests_count_max: Int + requests_count_min: Int + sort: String = "title" + sort_type: String = "ASC" + team_ids: [Int] = null + ): ClusterConnection + clusters_count(channels: [Int] = null, last_request_date: String, media_type: [String], medias_count_max: Int, medias_count_min: Int, requests_count_max: Int, requests_count_min: Int, team_ids: [Int] = null): Int created_at: String current_feed_team: FeedTeam data_points: [Int] @@ -8143,7 +8173,7 @@ type Feed implements Node { saved_search: SavedSearch saved_search_id: Int tags: [String] - team: Team + team: PublicTeam team_id: Int teams( """ @@ -8165,7 +8195,7 @@ type Feed implements Node { Returns the last _n_ elements from the list. """ last: Int - ): TeamConnection! + ): PublicTeamConnection! teams_count: Int updated_at: String user: User @@ -8276,7 +8306,7 @@ type FeedTeam implements Node { saved_search: SavedSearch saved_search_id: Int shared: Boolean - team: Team + team: PublicTeam team_id: Int updated_at: String } @@ -8463,6 +8493,158 @@ type GenerateTwoFactorBackupCodesPayload { scalar JsonStringType +""" +Me type +""" +type Me implements Node { + accepted_terms: Boolean + annotations( + """ + 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 + type: String + ): AnnotationConnection + assignments( + """ + 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 + team_id: Int + ): ProjectMediaConnection + bot: BotUser + bot_events: String + completed_signup: Boolean + confirmed: Boolean + created_at: String + current_project: Project + current_team: Team + current_team_id: Int + dbid: Int + email: String + feed_invitations( + """ + 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 + ): FeedInvitationConnection! + get_send_email_notifications: Boolean + get_send_failed_login_notifications: Boolean + get_send_successful_login_notifications: Boolean + id: ID! + is_active: Boolean + is_admin: Boolean + is_bot: Boolean + jsonsettings: String + last_accepted_terms_at: String + last_active_at: Int + login: String + name: String + number_of_teams: Int + permissions: String + profile_image: String + providers: JsonStringType + settings: JsonStringType + source: Source + source_id: Int + team_ids: [Int] + team_user(team_slug: String!): TeamUser + team_users( + """ + 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 + status: String + ): TeamUserConnection + teams( + """ + 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 + ): TeamConnection + token: String + two_factor: JsonStringType + unconfirmed_email: String + updated_at: String + user_teams: String + uuid: String +} + """ Media type """ @@ -9859,7 +10041,7 @@ type Project implements Node { pusher_channel: String search: CheckSearch search_id: String - team: Team + team: PublicTeam title: String! updated_at: String url: String @@ -9932,7 +10114,7 @@ type ProjectGroup implements Node { """ last: Int ): ProjectConnection - team: Team + team: PublicTeam team_id: Int title: String updated_at: String @@ -10031,8 +10213,6 @@ type ProjectMedia implements Node { author_role: String channel: JsonStringType claim_description: ClaimDescription - cluster: Cluster - cluster_id: Int comments( """ Returns the elements in the list that come after the specified cursor. @@ -10973,6 +11153,7 @@ type ProjectMedia implements Node { project: Project project_group: ProjectGroup project_id: Int + public_team: PublicTeam published: String pusher_channel: String quote: String @@ -11182,6 +11363,7 @@ type PublicTeam implements Node { dbid: Int description: String id: ID! + medias_count: Int name: String! permissions: String private: Boolean @@ -11194,6 +11376,42 @@ type PublicTeam implements Node { updated_at: String } +""" +The connection type for PublicTeam. +""" +type PublicTeamConnection { + """ + A list of edges. + """ + edges: [PublicTeamEdge] + + """ + A list of nodes. + """ + nodes: [PublicTeam] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int +} + +""" +An edge in a connection. +""" +type PublicTeamEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PublicTeam +} + """ The query root of this schema """ @@ -11207,11 +11425,6 @@ type Query { Information about the bot_user with given id """ bot_user(id: ID!): BotUser - - """ - Information about the cluster with given id - """ - cluster(id: ID!): Cluster dynamic_annotation_field(only_cache: Boolean, query: String!): DynamicAnnotationField """ @@ -11238,7 +11451,7 @@ type Query { """ Information about the current user """ - me: User + me: Me """ Fetches an object given its ID. @@ -11640,7 +11853,7 @@ Unassociated root object queries """ type RootLevel implements Node { current_team: Team - current_user: User + current_user: Me id: ID! team_bots_listed( """ @@ -11697,7 +11910,7 @@ type SavedSearch implements Node { is_part_of_feeds: Boolean items_count: Int permissions: String - team: Team + team: PublicTeam team_id: Int title: String updated_at: String @@ -12891,7 +13104,7 @@ type TeamTask implements Node { show_in_browser_extension: Boolean tasks_count: Int tasks_with_answers_count: Int - team: Team + team: PublicTeam team_id: Int type: String updated_at: String @@ -13121,13 +13334,14 @@ type TiplineNewsletterEdge { TiplineRequest type """ type TiplineRequest implements Node { - annotation: Annotation - annotation_id: Int associated_graphql_id: String + associated_id: Int + associated_type: String created_at: String dbid: Int id: ID! permissions: String + smooch_data: JsonStringType smooch_report_correction_sent_at: Int smooch_report_received_at: Int smooch_report_sent_at: Int @@ -13137,7 +13351,6 @@ type TiplineRequest implements Node { smooch_user_request_language: String smooch_user_slack_channel_url: String updated_at: String - value_json: JsonStringType } """ @@ -15516,6 +15729,7 @@ type UpdateUserPayload { A unique identifier for the client performing the mutation. """ clientMutationId: String + me: Me user: User userEdge: UserEdge } @@ -15524,152 +15738,19 @@ type UpdateUserPayload { User type """ type User implements Node { - accepted_terms: Boolean - annotations( - """ - 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 - type: String - ): AnnotationConnection - assignments( - """ - 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 - team_id: Int - ): ProjectMediaConnection - bot: BotUser - bot_events: String - completed_signup: Boolean - confirmed: Boolean created_at: String - current_project: Project - current_team: Team - current_team_id: Int dbid: Int email: String - feed_invitations( - """ - 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 - ): FeedInvitationConnection! - get_send_email_notifications: Boolean - get_send_failed_login_notifications: Boolean - get_send_successful_login_notifications: Boolean id: ID! is_active: Boolean - is_admin: Boolean is_bot: Boolean - jsonsettings: String - last_accepted_terms_at: String last_active_at: Int - login: String name: String number_of_teams: Int permissions: String profile_image: String - providers: JsonStringType - settings: JsonStringType source: Source - source_id: Int - team_ids: [Int] - team_user(team_slug: String!): TeamUser - team_users( - """ - 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 - status: String - ): TeamUserConnection - teams( - """ - 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 - ): TeamConnection - token: String - two_factor: JsonStringType - unconfirmed_email: String updated_at: String - user_teams: String - uuid: String } """ @@ -15714,7 +15795,7 @@ type UserDisconnectLoginAccountPayload { """ clientMutationId: String success: Boolean - user: User + user: Me } """ @@ -15779,7 +15860,7 @@ type UserTwoFactorAuthenticationPayload { """ clientMutationId: String success: Boolean - user: User + user: Me } """ diff --git a/lib/sample_data.rb b/lib/sample_data.rb index bc7da857c8..a3358b2dd5 100644 --- a/lib/sample_data.rb +++ b/lib/sample_data.rb @@ -506,6 +506,7 @@ def create_project_media(options = {}) options[:skip_autocreate_source] = true unless options.has_key?(:skip_autocreate_source) pm.source = create_source({ team: options[:team], skip_check_ability: true }) if options[:skip_autocreate_source] pm.save! + create_cluster_project_media({ cluster: options[:cluster], project_media: pm}) if options[:cluster] pm.reload end @@ -845,9 +846,39 @@ def create_tipline_subscription(options = {}) }.merge(options)) end + def create_tipline_request(options = {}) + tr = TiplineRequest.new + tr.smooch_data = { language: 'en', authorId: random_string, source: { type: 'whatsapp' } } unless options.has_key?(:smooch_data) + tr.team_id = options[:team_id] || create_team.id unless options.has_key?(:team_id) + tr.associated = options[:associated] || create_project_media + tr.smooch_request_type = 'default_requests' unless options.has_key?(:smooch_request_type) + tr.platform = 'whatsapp' unless options.has_key?(:platform) + tr.language = 'en' unless options.has_key?(:language) + options.each do |key, value| + tr.send("#{key}=", value) if tr.respond_to?("#{key}=") + end + tr.save! + tr.reload + end + def create_cluster(options = {}) - options[:project_media] = create_project_media unless options.has_key?(:project_media) - Cluster.create!(options) + team = options[:project_media]&.team || create_team + options[:feed] = options[:feed] || create_feed({ team: team }) + c = Cluster.new + options.each do |key, value| + c.send("#{key}=", value) if c.respond_to?("#{key}=") + end + c.save! + # Add item to cluster + create_cluster_project_media({ cluster: c, project_media: options[:project_media] }) if options[:project_media] + c.reload + end + + def create_cluster_project_media(options = {}) + ClusterProjectMedia.create!({ + cluster: options[:cluster] || create_cluster, + project_media: options[:project_media] || create_project_media + }.merge(options)) end def create_claim_description(options = {}) @@ -872,7 +903,7 @@ def create_fact_check(options = {}) def create_feed(options = {}) Feed.create!({ name: random_string, - team: create_team, + team: options[:team] || create_team, licenses: [1], }.merge(options)) end diff --git a/lib/tasks/check_clusters.rake b/lib/tasks/check_clusters.rake deleted file mode 100644 index 42f2cacf2b..0000000000 --- a/lib/tasks/check_clusters.rake +++ /dev/null @@ -1,148 +0,0 @@ -def log(text) - puts text - puts -end - -namespace :check do - namespace :clusters do - # bundle exec rake check:clusters:rebuild[list of team slugs] - desc 'Rebuild similarity clusters from scratch by calling Alegre API' - task rebuild: :environment do |_t, args| - - # Get the team IDs from the team slugs passed as input parameters - team_ids = Team.where(slug: args.to_a.map(&:to_s)).all.map(&:id) - - # Delete existing clusters - log 'Resetting all cluster IDs to null...' - centers = Cluster.all.map(&:project_media_id) - Cluster.delete_all - ProjectMedia.where.not(cluster_id: nil).update_all(cluster_id: nil) - # Reset fields in ElasticSearch - es_body = [] - centers.each do |id| - es_body << { - update: { - _index: ::CheckElasticSearchModel.get_index_alias, - _id: Base64.encode64("ProjectMedia/#{id}"), - retry_on_conflict: 3, - data: { - doc: { - cluster_size: 0, - cluster_report_published: 0, - cluster_first_item_at: 0, - cluster_last_item_at: 0, - cluster_published_reports_count: 0, - cluster_requests_count: 0 - } - } - } - } - end - $repository.client.bulk(body: es_body) unless es_body.empty? - log 'Done.' - - # Set the clusters for all media types - ['text', 'image', 'audio', 'video'].each do |type| - log "-----------------------------------\nComputing cluster for #{type}\n-----------------------------------" - - # If there is no existing cluster computation in progress, request a new one and save the ID in a file - key = File.join(Rails.root, 'tmp', "check:clusters:rebuild:job_id:#{type}") - job_id = File.exists?(key) ? File.read(key).chomp.to_i : nil - if job_id.nil? - log "Cache key not found: #{key}" - context = { - team_id: team_ids, - has_custom_id: true - } - context[:field] = ::Bot::Alegre::ALL_TEXT_SIMILARITY_FIELDS if type == 'text' - params = { - threshold: ::Bot::Alegre.get_threshold_for_query(type, ProjectMedia.new(team_id: 0), true)[:value], - data_types: [type], - context: context - } - log "Requesting Alegre: POST /graph/cluster with payload #{params.to_json}" - response = ::Bot::Alegre.request_api('post', '/graph/cluster/', params) - job_id = response['graph_id'] - File.write(key, job_id) - end - - # Loop requests to Alegre until the cluster is computed - params = { graph_id: job_id } - log "Requesting Alegre: GET /graph/cluster with payload #{params.to_json}" - response = ::Bot::Alegre.request_api('get', '/graph/cluster/', params) - while response.dig('graph', 'status') != 'enriched' - log "Cluster not ready for #{job_id}, response was: #{response.to_json}. Retrying in 5 seconds..." - sleep 5 - response = ::Bot::Alegre.request_api('get', '/graph/cluster/', params) - end - log "Cluster is ready for #{job_id}, response was: #{response.to_json}" - - # Iterate through the results: set cluster_id for each relevant item in the response - response['clusters'].each do |cluster| - ids = [] - cluster.each do |node| - id = node.dig('context', 0, 'project_media_id') unless node['context'].blank? - unless id - id = begin Base64.decode64(node['data_type_id']).match(/^check-project_media-([0-9]+)-.*$/)[1].to_i rescue 0 end - end - ids << id if id.to_i > 0 - end - next if ids.empty? - cluster_obj = nil - count = 0 - ids.uniq.sort.each do |id| - pm = ProjectMedia.find_by_id(id) - next if pm.nil? || !pm.cluster_id.nil? - media_type = { - 'UploadedVideo' => 'video', - 'UploadedAudio' => 'audio', - 'UploadedImage' => 'image', - 'Claim' => 'text', - 'Link' => 'text' - }[pm.media.type] - next if pm.archived == ::CheckArchivedFlags::FlagCodes::TRASHED || !team_ids.include?(pm.team_id) || media_type != type - cluster_obj ||= Cluster.create!(project_media: pm) - cluster_obj.project_medias << pm - count += 1 - end - if cluster_obj.nil? - log "No cluster created, since we have #{count} items" - else - log "Updated #{count} items for cluster ID #{cluster_obj.id}" - end - end - FileUtils.rm(key) - end - - # Now handle manually-added items - log "-----------------------------------\nComputing cluster for manually-added relationships\n-----------------------------------" - join = 'INNER JOIN relationships r ON r.target_id = project_medias.id' - relationship_condition = ['((r.relationship_type = ? AND r.user_id != ?) OR (r.confirmed_by IS NOT NULL))', Relationship.confirmed_type.to_yaml, BotUser.alegre_user.id] - ProjectMedia.joins(join).where(team_id: team_ids).where(*relationship_condition).find_each do |pm| - main = Relationship.confirmed_parent(pm) - cluster = pm.cluster - if main != pm && (cluster.nil? || (cluster.size == 1 && cluster.project_media_id == pm.id)) && (main.cluster_id && main.cluster_id != pm.cluster_id) - log "Adding item #{pm.id} to cluster #{main.cluster_id}" - cluster.destroy! unless cluster.nil? - main.cluster.project_medias << pm - end - end - end - - # bundle exec rake check:clusters:add[team slug] - desc 'Add a new workspace to the similarity clusters' - task add: :environment do |_t, args| - slug = args.to_a.first - raise "Missing input parameter! Usage: bundle exec rake check:clusters:add[team slug]" if slug.blank? - puts "Adding workspace #{slug} to the clusters." - team = Team.find_by_slug(slug) - n = ProjectMedia.where(team_id: team.id).count - i = 0 - ProjectMedia.where(team_id: team.id).order('id ASC').find_each do |pm| - i += 1 - c = Bot::Alegre.set_cluster(pm, true) - log "[#{i}/#{n}] [#{Time.now}] Adding item #{pm.id} to the clusters... added to cluster #{c.id}" - end - end - end -end diff --git a/lib/tasks/check_khousheh.rake b/lib/tasks/check_khousheh.rake new file mode 100644 index 0000000000..7eb42d3e4c --- /dev/null +++ b/lib/tasks/check_khousheh.rake @@ -0,0 +1,322 @@ +ActiveRecord::Base.logger = nil +namespace :check do + namespace :khousheh do + + PER_PAGE = Rails.env.development? ? 10 : 2000 + + TIMESTAMP = Time.now.to_f.to_s.gsub('.', '') + + def print_task_title(title) + puts '----------------------------------------------------------------' + puts title.upcase + '...' + puts '----------------------------------------------------------------' + puts + end + + # FIXME: Load only the claims we need + def claim_uuid_for_duplicate_quote + puts 'Collecting claim media UUIDs for duplicate quotes...' + claim_uuid = {} + Media.select('quote, MIN(id) as first').where(type: 'Claim').group(:quote).having('COUNT(id) > 1') + .each do |raw| + claim_uuid[Digest::MD5.hexdigest(raw['quote'])] = raw['first'].to_s + end + claim_uuid + end + + # docker-compose exec -e elasticsearch_log=0 api bundle exec rake check:khousheh:generate_input + desc 'Generate input files in JSON format.' + task generate_input: :environment do + print_task_title 'Generating input files' + FileUtils.mkdir_p(File.join(Rails.root, 'tmp', 'feed-clusters-input')) + started = Time.now.to_i + # Collect claim media UUIDs for duplicate quote + claim_uuid = claim_uuid_for_duplicate_quote + sort = [{ annotated_id: { order: :asc } }] + Feed.find_each do |feed| + # Only feeds that are sharing media + if feed.data_points.to_a.include?(2) + output = { call_id: "#{TIMESTAMP}-#{feed.uuid}", nodes: [], edges: [] } + Team.current = feed.team + query = { feed_id: feed.id, feed_view: 'media', show_similar: true } + es_query = CheckSearch.new(query.to_json).medias_query + total = CheckSearch.new(query.to_json, nil, feed.team.id).number_of_results + pages = (total / PER_PAGE.to_f).ceil + puts "Generating input file for feed #{feed.name} with #{total} item(s)..." + search_after = [0] + page = 0 + while true + page += 1 + result = $repository.search(_source: 'annotated_id', query: es_query, sort: sort, search_after: search_after, size: PER_PAGE).results + pm_ids = result.collect{ |i| i['annotated_id'] }.uniq + break if pm_ids.empty? + pm_media_mapping = {} # Project Media ID => Media ID + uuid = {} + ProjectMedia.where(id: pm_ids).find_in_batches(:batch_size => PER_PAGE) do |pms| + print '.' + # Collect media UUID + pms.each do |pm| + pm_media_mapping[pm.id] = pm.media_id + uuid[pm.media_id] = pm.media_id.to_s + end + m_ids = pms.map(&:media_id) + Media.where(id: m_ids, type: 'Claim').find_each do |m| + print '.' + uuid[m.id] = claim_uuid[Digest::MD5.hexdigest(m.quote)] || m.id.to_s + end + end + pm_ids.each do |pm_id| + m_uuid = uuid[pm_media_mapping[pm_id]] + next if m_uuid.blank? + output[:nodes] << m_uuid unless output[:nodes].include?(m_uuid) + end + + Relationship.where(source_id: pm_ids).where('relationship_type = ?', Relationship.confirmed_type.to_yaml) + .find_in_batches(:batch_size => PER_PAGE) do |relations| + print '.' + spm_m_mapping = {} + tpm_m_mapping = {} + t_uuid = {} + target_ids = relations.map(&:target_id) + # Get ProjectMedia items without Blank medias + ProjectMedia.where(id: pm_ids).joins(:media).where.not('medias.type': 'Blank').find_each{ |pm| spm_m_mapping[pm.id] = pm.media_id } + ProjectMedia.where(id: target_ids).joins(:media).where.not('medias.type': 'Blank').find_each do |pm| + tpm_m_mapping[pm.id] = pm.media_id + t_uuid[pm.media_id] = pm.media_id.to_s + end + Media.where(id: tpm_m_mapping.values, type: 'Claim').find_each do |m| + print '.' + t_uuid[m.id] = claim_uuid[Digest::MD5.hexdigest(m.quote)] || m.id.to_s + end + relations.each do |r| + print '.' + begin + if spm_m_mapping.keys.include?(r.source_id) && tpm_m_mapping.keys.include?(r.target_id) + if !uuid[spm_m_mapping[r.source_id]].blank? && !t_uuid[tpm_m_mapping[r.target_id]].blank? + output[:edges] << [uuid[spm_m_mapping[r.source_id]], t_uuid[tpm_m_mapping[r.target_id]], r.weight] + end + end + rescue StandardError => e + puts "WARNING: Ignoring corrupted relationship with ID #{r.id} (exception: #{e.message})" + end + end + end + search_after = [pm_ids.max] + puts "\nDone for page #{page}/#{pages}\n" + end + file = File.open(File.join(Rails.root, 'tmp', 'feed-clusters-input', "#{TIMESTAMP}-#{feed.uuid}.json"), 'w+') + file.puts output.to_json + file.close + Team.current = nil + end + end + minutes = ((Time.now.to_i - started) / 60).to_i + puts "[#{Time.now}] Done in #{minutes} minutes." + end + + # docker-compose exec -e elasticsearch_log=0 -e CLUSTER_INPUT_BUCKET=bucket-name api bundle exec rake check:khousheh:upload + desc 'Upload input JSON files to S3.' + task upload: [:environment, :generate_input] do + print_task_title 'Uploading input files' + started = Time.now.to_i + bucket_name = ENV.fetch('CLUSTER_INPUT_BUCKET') + region = CheckConfig.get('storage_bucket_region') || 'eu-west-1' + begin + s3_client = Aws::S3::Client.new(region: region) + rescue Aws::Sigv4::Errors::MissingCredentialsError + puts 'Please provide the AWS credentials.' + end + Feed.find_each do |feed| + # Only feeds that are sharing media + if feed.data_points.to_a.include?(2) + filename = "#{TIMESTAMP}-#{feed.uuid}.json" + filepath = File.join(Rails.root, 'tmp', 'feed-clusters-input', filename) + response = s3_client.put_object( + bucket: bucket_name, + key: filename, + body: File.read(filepath) + ) + if response.etag + puts "Uploaded #{filename}." + else + puts "Error uploading #{filename} to S3." + end + end + end + minutes = ((Time.now.to_i - started) / 60).to_i + puts "[#{Time.now}] Done in #{minutes} minutes." + end + + # docker-compose exec -e elasticsearch_log=0 -e CLUSTER_OUTPUT_BUCKET=bucket-name api bundle exec rake check:khousheh:download + desc 'Download json file from S3' + task download: [:environment, :upload] do + print_task_title 'Downloading output files' + FileUtils.mkdir_p(File.join(Rails.root, 'tmp', 'feed-clusters-output')) + started = Time.now.to_i + bucket_name = ENV.fetch('CLUSTER_OUTPUT_BUCKET') + region = CheckConfig.get('storage_bucket_region') || 'eu-west-1' + s3_client = Aws::S3::Client.new(region: region) + Feed.find_each do |feed| + # Only feeds that are sharing media + if feed.data_points.to_a.include?(2) + filename = "#{TIMESTAMP}-#{feed.uuid}.json" + filepath = File.join(Rails.root, 'tmp', 'feed-clusters-output', filename) + # Try during one hour + attempts = 0 + object = nil + while attempts < 60 && object.nil? + begin + object = s3_client.get_object(bucket: bucket_name, key: filename) + rescue StandardError => e + puts "File #{filename} not found in bucket #{bucket_name}, trying again in 1 minute..." + sleep 60 + attempts += 1 + end + end + if object.nil? + puts "Aborting. File #{filename} not found in bucket #{bucket_name}." + else + file = File.open(File.join(Rails.root, 'tmp', 'feed-clusters-output', "#{TIMESTAMP}-#{feed.uuid}.json"), 'w+') + file.puts object.body.read + file.close + end + end + end + minutes = ((Time.now.to_i - started) / 60).to_i + puts "[#{Time.now}] Done in #{minutes} minutes." + end + + # docker-compose exec -e elasticsearch_log=0 api bundle exec rake check:khousheh:parse_output + desc 'Parse output files (JSON format) and recreate clusters.' + task parse_output: [:environment, :download] do + print_task_title 'Parsing output files' + started = Time.now.to_i + claim_uuid = claim_uuid_for_duplicate_quote + sort = [{ annotated_id: { order: :asc } }] + error_logs = [] + Feed.find_each do |feed| + # Only feeds that are sharing media + if feed.data_points.to_a.include?(2) + puts "Parsing feed #{feed.name}..." + begin + last_old_cluster_id = Cluster.where(feed_id: feed.id).order('id ASC').last&.id + clusters = JSON.parse(File.read(File.join(Rails.root, 'tmp', 'feed-clusters-output', "#{TIMESTAMP}-#{feed.uuid}.json"))) + started_at = Time.now.to_f + Cluster.transaction do + # Create clusters + mapping = {} # Media ID => Cluster ID + # Bulk-insert clusters + c_inserted_items = [] + clusters.length.times.each_slice(2500) do |rows| + print '.' + c_items = [] + rows.each { |r| c_items << { feed_id: feed.id, created_at: Time.now, updated_at: Time.now } } + output = Cluster.insert_all(c_items) + c_inserted_items.concat(output.rows.flatten) + end + clusters.each.with_index do |media_ids, i| + cluster_id = c_inserted_items[i] + media_ids.each do |media_id| + mapping[media_id.to_i] = cluster_id + end + end + # Add items to clusters + Team.current = feed.team + query = { feed_id: feed.id, feed_view: 'media', show_similar: true } + es_query = CheckSearch.new(query.to_json).medias_query + total = CheckSearch.new(query.to_json, nil, feed.team.id).number_of_results + pages = (total / PER_PAGE.to_f).ceil + search_after = [0] + page = 0 + while true + page += 1 + puts "\nIterating on page #{page}/#{pages}\n" + result = $repository.search(_source: 'annotated_id', query: es_query, sort: sort, search_after: search_after, size: PER_PAGE).results + pm_ids = result.collect{ |i| i['annotated_id'] }.uniq + break if pm_ids.empty? + pm_media_mapping = {} # Project Media ID => Media ID + uuid = {} + cpm_items = [] + ProjectMedia.where(id: pm_ids).find_in_batches(:batch_size => PER_PAGE) do |pms| + # Collect claim media UUIDs + pms.each do |pm| + pm_media_mapping[pm.id] = pm.media_id + uuid[pm.media_id] = pm.media_id.to_s + end + Media.where(id: pms.map(&:media_id), type: 'Claim').find_each do |m| + print '.' + uuid[m.id] = claim_uuid[Digest::MD5.hexdigest(m.quote)] || m.id.to_s + end + # Fact-checks + pm_fc_mapping = {} # Project Media ID => Fact-Check Updated At + ProjectMedia.select('project_medias.id as id, fc.updated_at as fc_updated_at') + .where(id: pms.map(&:id)) + .joins("INNER JOIN claim_descriptions cd ON project_medias.id = cd.project_media_id") + .joins("INNER JOIN fact_checks fc ON cd.id = fc.claim_description_id") + .find_each do |pm_fc| + print '.' + pm_fc_mapping[pm_fc['id']] = pm_fc['fc_updated_at'] + end + # Local clusters + pms.each do |pm| + print '.' + cluster_id = mapping[uuid[pm_media_mapping[pm.id]].to_i] + next if cluster_id.nil? + cluster = Cluster.find_by_id(cluster_id) + next if cluster.nil? + updated_cluster_attributes = { id: cluster.id, created_at: cluster.created_at, updated_at: Time.now } + updated_cluster_attributes[:first_item_at] = cluster.first_item_at || pm.created_at + updated_cluster_attributes[:last_item_at] = pm.created_at + updated_cluster_attributes[:team_ids] = (cluster.team_ids.to_a + [pm.team_id]).uniq.compact_blank + updated_cluster_attributes[:channels] = (cluster.channels.to_a + pm.channel.to_h['others'].to_a + [pm.channel.to_h['main']]).uniq.compact_blank + updated_cluster_attributes[:media_count] = cluster.media_count + 1 + updated_cluster_attributes[:requests_count] = cluster.requests_count + pm.requests_count + updated_cluster_attributes[:last_request_date] = (pm.last_seen > cluster.last_request_date.to_i) ? Time.at(pm.last_seen) : cluster.last_request_date + updated_cluster_attributes[:fact_checks_count] = cluster.fact_checks_count + updated_cluster_attributes[:last_fact_check_date] = cluster.last_fact_check_date + unless pm_fc_mapping[pm.id].blank? + updated_cluster_attributes[:fact_checks_count] = cluster.fact_checks_count + 1 + updated_cluster_attributes[:last_fact_check_date] = pm_fc_mapping[pm.id] if pm_fc_mapping[pm.id].to_i > cluster.last_fact_check_date.to_i + end + cpm_items << { project_media_id: pm.id, cluster_id: cluster.id } + # FIXME: Set the center of the cluster properly + updated_cluster_attributes[:project_media_id] = cluster.project_media_id || pm.id + updated_cluster_attributes[:title] = cluster.title || pm.title + # Update cluster + # FIXME: Update clusters in batches + cluster.update_columns(updated_cluster_attributes) + end + end + # Bulk-insert ClusterProjectMedia + unless cpm_items.blank? + begin + ClusterProjectMedia.insert_all(cpm_items, unique_by: %i[ cluster_id project_media_id ]) + rescue + error_logs << {feed: "Failed to import ClusterProjectMedia for feed #{feed.id} page #{page}"} + end + end + search_after = [pm_ids.max] + end + Team.current = nil + # Delete old clusters + Cluster.where(feed_id: feed.id).where('id <= ?', last_old_cluster_id).delete_all unless last_old_cluster_id.nil? + feed.update_column(:last_clusterized_at, Time.now) + end + puts "Rebuilding clusters for feed #{feed.name} took #{Time.now.to_f - started_at} seconds." + rescue Errno::ENOENT + puts "Output file not found for feed #{feed.name}." + end + end + end + puts "Logs: #{error_logs.inspect}." unless error_logs.blank? + minutes = ((Time.now.to_i - started) / 60).to_i + puts "[#{Time.now}] Done in #{minutes} minutes." + end + + # docker-compose exec -e elasticsearch_log=0 -e CLUSTER_INPUT_BUCKET=bucket-name -e CLUSTER_OUTPUT_BUCKET=bucket-name api bundle exec rake check:khousheh:rebuild + desc 'Rebuild clusters.' + task rebuild: [:environment, :parse_output] do + print_task_title "[#{TIMESTAMP}] Rebuilding clusters" + end + end +end diff --git a/lib/tasks/data/similarity.rake b/lib/tasks/data/similarity.rake index a85a42d6ba..bc5cb5b4ca 100644 --- a/lib/tasks/data/similarity.rake +++ b/lib/tasks/data/similarity.rake @@ -20,6 +20,7 @@ def write_similarity_relationships_to_disk(query, filename) created_at: r.created_at, source_text_fields: Hash[Bot::Alegre::ALL_TEXT_SIMILARITY_FIELDS.collect{|f| [f, (r.source.send(f) rescue nil)]}], target_text_fields: Hash[Bot::Alegre::ALL_TEXT_SIMILARITY_FIELDS.collect{|f| [f, (r.target.send(f) rescue nil)]}], + user_id: r.user_id, }.to_json+"\n") end f.close diff --git a/lib/tasks/data/similarityunc.rake b/lib/tasks/data/similarityunc.rake index 0d825080c6..a057891e83 100644 --- a/lib/tasks/data/similarityunc.rake +++ b/lib/tasks/data/similarityunc.rake @@ -26,6 +26,7 @@ def write_archived_similarity_relationships_to_disk(object_change, filename) created_at: r["created_at"], source_text_fields: Hash[Bot::Alegre::ALL_TEXT_SIMILARITY_FIELDS.collect{|f| [f, (source.send(f) rescue nil)]}], target_text_fields: Hash[Bot::Alegre::ALL_TEXT_SIMILARITY_FIELDS.collect{|f| [f, (target.send(f) rescue nil)]}], + user_id: r["user_id"], }.to_json+"\n") end end diff --git a/lib/tasks/migrate/20231122054128_migrate_tipline_requests.rake b/lib/tasks/migrate/20231122054128_migrate_tipline_requests.rake new file mode 100644 index 0000000000..5d3981e98c --- /dev/null +++ b/lib/tasks/migrate/20231122054128_migrate_tipline_requests.rake @@ -0,0 +1,452 @@ +namespace :check do + namespace :migrate do + def parse_args(args) + output = {} + return output if args.blank? + args.each do |a| + arg = a.split('&') + arg.each do |pair| + key, value = pair.split(':') + output.merge!({ key => value }) + end + end + output + end + + def migrate_team_tipline_requests(team, batch_size) + total_count = Annotation.where(annotation_type: 'smooch', annotated_type: 'Team', annotated_id: team.id).count + failed_teams = [] + if total_count > 0 + puts "\nMigrating Team requests[#{team.slug}]: #{total_count} requests" + inserts = 0 + Annotation.where(annotation_type: 'smooch', annotated_type: 'Team', annotated_id: team.id) + .find_in_batches(:batch_size => batch_size) do |annotations| + print '.' + smooch_obj = {} + smooch_user = {} + obj_requests = Hash.new {|hash, key| hash[key] = [] } + # Collect request associated id and user id + annotations.each do |d| + smooch_obj[d.id] = d.annotated_id + smooch_user[d.id] = d.annotator_id + end + DynamicAnnotation::Field.where(annotation_type: 'smooch', annotation_id: smooch_obj.keys).find_each do |f| + print '.' + value = f.value + # I mapped `smooch_report_received` field in two columns `smooch_report_received_at` & `smooch_report_update_received_at` + field_name = f.field_name == 'smooch_report_received' ? 'smooch_report_received_at' : f.field_name + if field_name == 'smooch_data' + value.gsub!('\u0000', '') if value.is_a?(String) # Avoid PG::UntranslatableCharacter exception + value = begin JSON.parse(value) rescue {} end + # These fields are indifferent TiplineRequest columns so collect these fields with their values + # N.B: I set smooch_data.id as a primary key for TiplineRequest table + # and set a default values for some columns to avoid PG error + sd_fields = [ + { 'id' => f.id }, + { 'tipline_user_uid' => value.dig('authorId') }, + { 'language' => value.dig('language') || 'en' }, + { 'platform' => value.dig('source', 'type') || 'whatsapp' }, + { 'created_at' => f.created_at }, + { 'updated_at' => f.updated_at }, + ] + obj_requests[f.annotation_id].concat(sd_fields) + end + obj_requests[f.annotation_id] << { field_name => value } + if field_name == 'smooch_report_received_at' && f.created_at != f.updated_at + # Get the value for `smooch_report_update_received_at` column + obj_requests[f.annotation_id] << { 'smooch_report_update_received_at' => value } + end + end + requests = [] + obj_requests.each do |d_id, fields| + # Build TiplineRequest raw and should include all existing columns + r = { + associated_type: 'Team', + associated_id: smooch_obj[d_id], + user_id: smooch_user[d_id], + team_id: team.id, + smooch_request_type: 'default_requests', + smooch_resource_id: nil, + smooch_message_id: '', + smooch_conversation_id: nil, + smooch_report_received_at: 0, + smooch_report_update_received_at: 0, + smooch_report_correction_sent_at: 0, + smooch_report_sent_at: 0, + }.with_indifferent_access + fields.each do |raws| + raws.each{|k, v| r[k] = v } + end + requests << r + end + unless requests.blank? + inserts += requests.count + puts "\nImporting Team requests[#{team.slug}]: #{inserts}/#{total_count}\n" + begin + TiplineRequest.insert_all(requests) + rescue + failed_teams << team.id unless failed_teams.include?(team.id) + end + end + end + end + failed_teams + end + + def migrate_missing_team_tipline_requests(team, batch_size) + failed_teams = [] + puts "\nMigrating missing Team requests[#{team.slug}]" + inserts = 0 + smooch_data_ids = DynamicAnnotation::Field.where(field_name: 'smooch_data') + .joins("INNER JOIN annotations a ON a.id = dynamic_annotation_fields.annotation_id") + .where('a.annotated_type' => 'Team', 'a.annotated_id' => team.id).map(&:id) + existing_smooch_data_ids = TiplineRequest.where(id: smooch_data_ids, team_id: team.id).map(&:id) + diff = smooch_data_ids - existing_smooch_data_ids + if diff.length > 0 + Annotation.where(annotation_type: 'smooch', annotated_type: 'Team', annotated_id: team.id) + .joins("INNER JOIN dynamic_annotation_fields f ON f.annotation_id = annotations.id AND f.field_name = 'smooch_data'") + .where('f.id IN (?)', diff) + .find_in_batches(:batch_size => batch_size) do |annotations| + print '.' + smooch_obj = {} + smooch_user = {} + obj_requests = Hash.new {|hash, key| hash[key] = [] } + # Collect request associated id and user id + annotations.each do |d| + smooch_obj[d.id] = d.annotated_id + smooch_user[d.id] = d.annotator_id + end + DynamicAnnotation::Field.where(annotation_type: 'smooch', annotation_id: smooch_obj.keys).find_each do |f| + print '.' + value = f.value + # I mapped `smooch_report_received` field in two columns `smooch_report_received_at` & `smooch_report_update_received_at` + field_name = f.field_name == 'smooch_report_received' ? 'smooch_report_received_at' : f.field_name + if field_name == 'smooch_data' + value.gsub!('\u0000', '') if value.is_a?(String) # Avoid PG::UntranslatableCharacter exception + value = begin JSON.parse(value) rescue {} end + # These fields are indifferent TiplineRequest columns so collect these fields with their values + # N.B: I set smooch_data.id as a primary key for TiplineRequest table + # and set a default values for some columns to avoid PG error + sd_fields = [ + { 'id' => f.id }, + { 'tipline_user_uid' => value.dig('authorId') }, + { 'language' => value.dig('language') || 'en' }, + { 'platform' => value.dig('source', 'type') || 'whatsapp' }, + { 'created_at' => f.created_at }, + { 'updated_at' => f.updated_at }, + ] + obj_requests[f.annotation_id].concat(sd_fields) + end + obj_requests[f.annotation_id] << { field_name => value } + if field_name == 'smooch_report_received_at' && f.created_at != f.updated_at + # Get the value for `smooch_report_update_received_at` column + obj_requests[f.annotation_id] << { 'smooch_report_update_received_at' => value } + end + end + requests = [] + obj_requests.each do |d_id, fields| + # Build TiplineRequest raw and should include all existing columns + r = { + associated_type: 'Team', + associated_id: smooch_obj[d_id], + user_id: smooch_user[d_id], + team_id: team.id, + smooch_request_type: 'default_requests', + smooch_resource_id: nil, + smooch_message_id: '', + smooch_conversation_id: nil, + smooch_report_received_at: 0, + smooch_report_update_received_at: 0, + smooch_report_correction_sent_at: 0, + smooch_report_sent_at: 0, + }.with_indifferent_access + fields.each do |raws| + raws.each{|k, v| r[k] = v } + end + requests << r + end + unless requests.blank? + inserts += requests.count + puts "\nImporting Team requests[#{team.slug}]: #{inserts}\n" + begin + TiplineRequest.insert_all(requests) + rescue + failed_teams << team.id unless failed_teams.include?(team.id) + end + end + end + end + failed_teams + end + + def bulk_import_requests_items(annotated_type, ids, team_id) + print '.' + smooch_obj = {} + smooch_user = {} + obj_requests = Hash.new {|hash, key| hash[key] = [] } + # Collect request associated id and user id + Annotation.where(annotation_type: 'smooch', annotated_type: annotated_type, annotated_id: ids).find_each do |d| + print '.' + smooch_obj[d.id] = d.annotated_id + smooch_user[d.id] = d.annotator_id + end + DynamicAnnotation::Field.where(annotation_type: 'smooch', annotation_id: smooch_obj.keys).find_each do |f| + print '.' + value = f.value + # I mapped `smooch_report_received` field in two columns `smooch_report_received_at` & `smooch_report_update_received_at` + field_name = f.field_name == 'smooch_report_received' ? 'smooch_report_received_at' : f.field_name + if field_name == 'smooch_data' + value.gsub!('\u0000', '') if value.is_a?(String) # Avoid PG::UntranslatableCharacter exception + value = begin JSON.parse(value) rescue {} end + # These fields are indifferent TiplineRequest columns so collect these fields with their values + # N.B: I set smooch_data.id as a primary key for TiplineRequest table + # and set a default values for some columns to avoid PG error + sd_fields = [ + { 'id' => f.id }, + { 'tipline_user_uid' => value.dig('authorId') }, + { 'language' => value.dig('language') || 'en' }, + { 'platform' => value.dig('source', 'type') || 'whatsapp' }, + { 'created_at' => f.created_at }, + { 'updated_at' => f.updated_at }, + ] + obj_requests[f.annotation_id].concat(sd_fields) + end + obj_requests[f.annotation_id] << { field_name => value } + if field_name == 'smooch_report_received_at' && f.created_at != f.updated_at + # Get the value for `smooch_report_update_received_at` column + obj_requests[f.annotation_id] << { 'smooch_report_update_received_at' => value } + end + end + requests = [] + obj_requests.each do |d_id, fields| + # Build TiplineRequest raw and should include all existing columns + r = { + associated_type: annotated_type, + associated_id: smooch_obj[d_id], + user_id: smooch_user[d_id], + team_id: team_id, + smooch_request_type: 'default_requests', + smooch_resource_id: nil, + smooch_message_id: '', + smooch_conversation_id: nil, + smooch_report_received_at: 0, + smooch_report_update_received_at: 0, + smooch_report_correction_sent_at: 0, + smooch_report_sent_at: 0, + }.with_indifferent_access + fields.each do |raws| + raws.each{|k, v| r[k] = v } + end + requests << r + end + requests + end + # Migrate TiplineRequests + # bundle exec rails check:migrate:migrate_tipline_requests['slug:team_slug&batch_size:batch_size'] + task migrate_tipline_requests: :environment do |_t, args| + started = Time.now.to_i + data_args = parse_args args.extras + batch_size = data_args['batch_size'] || 1500 + batch_size = batch_size.to_i + slug = data_args['slug'] + condition = {} + last_team_id = Rails.cache.read('check:migrate:migrate_tipline_requests:team_id') || 0 + unless slug.blank? + last_team_id = 0 + if slug == 'custom_slugs' + puts "Type team slugs separated by comma then press enter" + print ">> " + slug = begin STDIN.gets.chomp.split(',').map{ |s| s.to_s } rescue [] end + raise "You must call rake task with team slugs" if slug.blank? + end + condition = { slug: slug } + end + failed_project_media_requests = [] + failed_team_requests = [] + failed_tipline_resource_requests = [] + Team.where(condition).where('id > ?', last_team_id).find_each do |team| + print '.' + migrate_teams = Rails.cache.read('check:migrate:migrate_tipline_requests:migrate_teams') || [] + next if migrate_teams.include?(team.id) + # Migrated Team requests + failed_team_requests = migrate_team_tipline_requests(team, batch_size) + # Migrate TiplineResource requests + total_count = team.tipline_resources.joins("INNER JOIN annotations a ON a.annotated_id = tipline_resources.id") + .where("a.annotated_type = ? AND a.annotation_type = ?", 'TiplineResource', 'smooch').count + if total_count > 0 + puts "\nMigrating TiplineResource requests[#{team.slug}]: #{total_count} requests" + inserts = 0 + team.tipline_resources.find_in_batches(:batch_size => batch_size) do |items| + print '.' + ids = items.map(&:id) + requests = bulk_import_requests_items('TiplineResource', ids, team.id) + unless requests.blank? + inserts += requests.count + puts "\nImporting TiplineResource[#{team.slug}]: #{inserts}/#{total_count}\n" + begin + TiplineRequest.insert_all(requests) + rescue + failed_tipline_resource_requests << team.id unless failed_tipline_resource_requests.include?(team.id) + end + end + end + end + # Migrate ProjectMedia requests + # Get the total count for team requests + total_count = team.project_medias.joins("INNER JOIN annotations a ON a.annotated_id = project_medias.id") + .where("a.annotated_type = ? AND a.annotation_type = ?", 'ProjectMedia', 'smooch').count + if total_count > 0 + puts "\nMigrating ProjectMedia requests[#{team.slug}]: #{total_count} requests" + inserts = 0 + team.project_medias.find_in_batches(:batch_size => batch_size) do |pms| + print '.' + ids = pms.map(&:id) + requests = bulk_import_requests_items('ProjectMedia', ids, team.id) + unless requests.blank? + inserts += requests.count + puts "\nImporting ProjectMedia requests[#{team.slug}]: #{inserts}/#{total_count}\n" + begin + TiplineRequest.insert_all(requests) + rescue + failed_teams << team.id unless failed_teams.include?(team.id) + end + end + end + end + unless slug.blank? + migrate_teams << team.id + Rails.cache.write('check:migrate:migrate_tipline_requests:migrate_teams', migrate_teams) + end + Rails.cache.write('check:migrate:migrate_tipline_requests:team_id', team.id) if slug.blank? + end + puts "Failed to import some project media requests related to the following teams #{failed_project_media_requests.inspect}" if failed_project_media_requests.length > 0 + puts "Failed to import some team requests related to the following teams #{failed_team_requests.inspect}" if failed_team_requests.length > 0 + puts "Failed to import some tipline resource requests related to the following teams #{failed_tipline_resource_requests.inspect}" if failed_tipline_resource_requests.length > 0 + minutes = ((Time.now.to_i - started) / 60).to_i + puts "[#{Time.now}] Done in #{minutes} minutes." + end + + # list teams that have a different count between TiplineRequest and smooch annotation (list teams that not fully migrated) + # bundle exec rails check:migrate:migrate_tipline_requests_status[team_slug1, team_slug2, ...] + task migrate_tipline_requests_status: :environment do |_t, args| + # Get missing requests based on a comparison between TiplineRequest.id and smooch_data field id + slugs = args.extras + condition = {} + condition = { slug: slugs } unless slugs.blank? + logs = [] + Team.where(condition).find_each do |team| + print '.' + diff = {} + # Team tequests + tr_team_c = TiplineRequest.where(associated_type: 'Team', team_id: team.id).count + smooch_team_c = Annotation.where(annotation_type: 'smooch', annotated_type: 'Team', annotated_id: team.id).count + diff[:team] = { existing: smooch_team_c, migrated: tr_team_c } if smooch_team_c > tr_team_c + # TiplineResource requests + tr_resource_c = TiplineRequest.where(associated_type: 'TiplineResource', team_id: team.id).count + smooch_resource_c = team.tipline_resources.joins("INNER JOIN annotations a ON a.annotated_id = tipline_resources.id") + .where("a.annotated_type = ? AND a.annotation_type = ?", 'TiplineResource', 'smooch').count + diff[:tipline_resource] = { existing: smooch_resource_c, migrated: tr_resource_c } if smooch_resource_c > tr_resource_c + # ProjectMedia request + tr_pm_c = TiplineRequest.where(associated_type: 'ProjectMedia', team_id: team.id).count + smooch_tr_c = team.project_medias.joins("INNER JOIN annotations a ON a.annotated_id = project_medias.id") + .where("a.annotated_type = ? AND a.annotation_type = ?", 'ProjectMedia', 'smooch').count + diff[:project_media] = { existing: smooch_tr_c, migrated: tr_pm_c } if smooch_tr_c > tr_pm_c + # Add to logs + unless diff.blank? + diff[:team_slug] = team.slug + logs << diff + end + end + puts "List of teams that not fully migrated" + pp logs + end + + # Migrate missing requests related to specific teams + # bundle exec rails check:migrate:migrate_tipline_requests_missing_requests['slug:team_slug&batch_size:batch_size'] + task migrate_tipline_requests_missing_requests: :environment do |_t, args| + started = Time.now.to_i + data_args = parse_args args.extras + batch_size = data_args['batch_size'] || 1500 + batch_size = batch_size.to_i + slug = data_args['slug'] + condition = {} + last_team_id = Rails.cache.read('check:migrate:migrate_tipline_requests_missing_requests:team_id') || 0 + unless slug.blank? + last_team_id = 0 + if slug == 'custom_slugs' + puts "Type team slugs separated by comma then press enter" + print ">> " + slug = begin STDIN.gets.chomp.split(',').map{ |s| s.to_s } rescue [] end + raise "You must call rake task with team slugs" if slug.blank? + end + condition = { slug: slug } + end + failed_project_media_requests = [] + failed_team_requests = [] + failed_tipline_resource_requests = [] + Team.where(condition).where('id > ?', last_team_id).find_each do |team| + print '.' + # Migrated missing Team requests + failed_team_requests = migrate_missing_team_tipline_requests(team, batch_size) + # Migrate missing TiplineResource requests + puts "\nMigrating missing TiplineResource requests[#{team.slug}]" + inserts = 0 + team.tipline_resources.find_in_batches(:batch_size => batch_size) do |items| + print '.' + smooch_data_ids = DynamicAnnotation::Field.where(field_name: 'smooch_data') + .joins("INNER JOIN annotations a ON a.id = dynamic_annotation_fields.annotation_id") + .where('a.annotated_type' => 'TiplineResource', 'a.annotated_id' => items.map(&:id)).map(&:id) + existing_smooch_data_ids = TiplineRequest.where(id: smooch_data_ids, team_id: team.id).map(&:id) + diff = smooch_data_ids - existing_smooch_data_ids + if diff.length > 0 + ids = Annotation.where(annotation_type: 'smooch', annotated_type: 'TiplineResource') + .joins("INNER JOIN dynamic_annotation_fields f ON f.annotation_id = annotations.id AND f.field_name = 'smooch_data'") + .where('f.id IN (?)', diff).map(&:annotated_id) + requests = bulk_import_requests_items('TiplineResource', ids, team.id) + unless requests.blank? + inserts += requests.count + puts "\nImporting missing TiplineResource[#{team.slug}]: #{inserts}\n" + begin + TiplineRequest.insert_all(requests) + rescue + failed_tipline_resource_requests << team.id unless failed_tipline_resource_requests.include?(team.id) + end + end + end + end + # Migrate missing ProjectMedia requests + puts "\nMigrating missing ProjectMedia requests[#{team.slug}]" + inserts = 0 + team.project_medias.find_in_batches(:batch_size => batch_size) do |pms| + print '.' + smooch_data_ids = DynamicAnnotation::Field.where(field_name: 'smooch_data') + .joins("INNER JOIN annotations a ON a.id = dynamic_annotation_fields.annotation_id") + .where('a.annotated_type' => 'ProjectMedia', 'a.annotated_id' => pms.map(&:id)).map(&:id) + existing_smooch_data_ids = TiplineRequest.where(id: smooch_data_ids, team_id: team.id).map(&:id) + diff = smooch_data_ids - existing_smooch_data_ids + if diff.length > 0 + ids = Annotation.where(annotation_type: 'smooch', annotated_type: 'ProjectMedia') + .joins("INNER JOIN dynamic_annotation_fields f ON f.annotation_id = annotations.id AND f.field_name = 'smooch_data'") + .where('f.id IN (?)', diff).map(&:annotated_id) + requests = bulk_import_requests_items('ProjectMedia', ids, team.id) + unless requests.blank? + inserts += requests.count + puts "\nImporting missing ProjectMedia requests[#{team.slug}]: #{inserts}\n" + begin + TiplineRequest.insert_all(requests) + rescue + failed_teams << team.id unless failed_teams.include?(team.id) + end + end + end + end + Rails.cache.write('check:migrate:migrate_tipline_requests_missing_requests:team_id', team.id) if slug.blank? + end + puts "Failed to import some project media requests related to the following teams #{failed_project_media_requests.inspect}" if failed_project_media_requests.length > 0 + puts "Failed to import some team requests related to the following teams #{failed_team_requests.inspect}" if failed_team_requests.length > 0 + puts "Failed to import some tipline resource requests related to the following teams #{failed_tipline_resource_requests.inspect}" if failed_tipline_resource_requests.length > 0 + minutes = ((Time.now.to_i - started) / 60).to_i + puts "[#{Time.now}] Done in #{minutes} minutes." + end + end +end diff --git a/public/relay.json b/public/relay.json index 57418f13c8..4c2a6b4a74 100644 --- a/public/relay.json +++ b/public/relay.json @@ -2328,7 +2328,7 @@ ], "type": { "kind": "OBJECT", - "name": "Team", + "name": "PublicTeam", "ofType": null }, "isDeprecated": false, @@ -3235,87 +3235,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "ClaimDescriptionConnection", - "description": "The connection type for ClaimDescription.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [ - - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ClaimDescriptionEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [ - - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ClaimDescription", - "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": "ClaimDescriptionEdge", @@ -3367,82 +3286,37 @@ "description": "Cluster type", "fields": [ { - "name": "claim_descriptions", + "name": "center", "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": "feed_id", - "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": "ClaimDescriptionConnection", + "name": "ProjectMedia", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, + { + "name": "channels", + "description": null, + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "created_at", "description": null, @@ -3472,14 +3346,14 @@ "deprecationReason": null }, { - "name": "fact_checked_by_team_names", + "name": "fact_checks_count", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "JsonStringType", + "name": "Int", "ofType": null }, "isDeprecated": false, @@ -3518,77 +3392,14 @@ "deprecationReason": null }, { - "name": "items", + "name": "last_fact_check_date", "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": "feed_id", - "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": "ProjectMediaConnection", + "kind": "SCALAR", + "name": "Int", "ofType": null }, "isDeprecated": false, @@ -3608,6 +3419,34 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "last_request_date", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "media_count", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "permissions", "description": null, @@ -3637,31 +3476,141 @@ "deprecationReason": null }, { - "name": "size", + "name": "team_ids", + "description": null, + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "teams", + "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": "PublicTeamConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "Int", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "team_names", + "name": "updated_at", "description": null, "args": [ + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ClusterConnection", + "description": "The connection type for Cluster.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + ], "type": { "kind": "LIST", "name": null, "ofType": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "ClusterEdge", "ofType": null } }, @@ -3669,14 +3618,50 @@ "deprecationReason": null }, { - "name": "updated_at", + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Cluster", + "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": "String", + "name": "Int", "ofType": null }, "isDeprecated": false, @@ -3685,11 +3670,52 @@ ], "inputFields": null, "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ClusterEdge", + "description": "An edge in a connection.", + "fields": [ { - "kind": "INTERFACE", - "name": "Node", - "ofType": null + "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": "Cluster", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } + ], + "inputFields": null, + "interfaces": [ + ], "enumValues": null, "possibleTypes": null @@ -43750,95 +43776,421 @@ "description": "Feed type", "fields": [ { - "name": "created_at", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "current_feed_team", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "FeedTeam", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "data_points", - "description": null, - "args": [ - - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "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": "discoverable", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "feed_invitations", + "name": "clusters", + "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": "offset", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": "0", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sort", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"title\"", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sort_type", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"ASC\"", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "team_ids", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "channels", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "medias_count_min", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "medias_count_max", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requests_count_min", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requests_count_max", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last_request_date", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "media_type", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ClusterConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clusters_count", + "description": null, + "args": [ + { + "name": "team_ids", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "channels", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "medias_count_min", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "medias_count_max", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requests_count_min", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requests_count_max", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last_request_date", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "media_type", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "created_at", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "current_feed_team", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "FeedTeam", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "data_points", + "description": null, + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "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": "discoverable", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "feed_invitations", "description": null, "args": [ { @@ -44334,7 +44686,7 @@ ], "type": { "kind": "OBJECT", - "name": "Team", + "name": "PublicTeam", "ofType": null }, "isDeprecated": false, @@ -44412,7 +44764,7 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "TeamConnection", + "name": "PublicTeamConnection", "ofType": null } }, @@ -45113,7 +45465,7 @@ ], "type": { "kind": "OBJECT", - "name": "Team", + "name": "PublicTeam", "ofType": null }, "isDeprecated": false, @@ -46009,67 +46361,185 @@ }, { "kind": "OBJECT", - "name": "Media", - "description": "Media type", + "name": "Me", + "description": "Me type", "fields": [ { - "name": "account", + "name": "accepted_terms", "description": null, "args": [ ], "type": { - "kind": "OBJECT", - "name": "Account", + "kind": "SCALAR", + "name": "Boolean", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "account_id", + "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": "type", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } ], "type": { - "kind": "SCALAR", - "name": "Int", + "kind": "OBJECT", + "name": "AnnotationConnection", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "created_at", + "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 + }, + { + "name": "team_id", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } ], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "ProjectMediaConnection", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dbid", + "name": "bot", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "Int", + "kind": "OBJECT", + "name": "BotUser", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "domain", + "name": "bot_events", "description": null, "args": [ @@ -46083,109 +46553,105 @@ "deprecationReason": null }, { - "name": "embed_path", + "name": "completed_signup", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "file_path", + "name": "confirmed", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "id", + "name": "created_at", "description": null, "args": [ ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "metadata", + "name": "current_project", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "JsonStringType", + "kind": "OBJECT", + "name": "Project", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "permissions", + "name": "current_team", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "Team", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "picture", + "name": "current_team_id", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Int", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "pusher_channel", + "name": "dbid", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Int", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "quote", + "name": "email", "description": null, "args": [ @@ -46199,90 +46665,124 @@ "deprecationReason": null }, { - "name": "thumbnail_path", + "name": "feed_invitations", "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": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "FeedInvitationConnection", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null }, { - "name": "type", + "name": "get_send_email_notifications", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "updated_at", + "name": "get_send_failed_login_notifications", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "url", + "name": "get_send_successful_login_notifications", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null }, "isDeprecated": false, "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MediaConnection", - "description": "The connection type for Media.", - "fields": [ + }, { - "name": "edges", - "description": "A list of edges.", + "name": "id", + "description": null, "args": [ ], "type": { - "kind": "LIST", + "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "MediaEdge", + "kind": "SCALAR", + "name": "ID", "ofType": null } }, @@ -46290,155 +46790,106 @@ "deprecationReason": null }, { - "name": "nodes", - "description": "A list of nodes.", + "name": "is_active", + "description": null, "args": [ ], "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Media", - "ofType": null - } + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "pageInfo", - "description": "Information to aid in pagination.", + "name": "is_admin", + "description": null, "args": [ ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "totalCount", + "name": "is_bot", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null }, "isDeprecated": false, "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MediaEdge", - "description": "An edge in a connection.", - "fields": [ + }, { - "name": "cursor", - "description": "A cursor for use in pagination.", + "name": "jsonsettings", + "description": null, "args": [ ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "node", - "description": "The item at the end of the edge.", + "name": "last_accepted_terms_at", + "description": null, "args": [ ], "type": { - "kind": "OBJECT", - "name": "Media", + "kind": "SCALAR", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "MoveTeamTaskDownInput", - "description": "Autogenerated input type of MoveTeamTaskDown", - "fields": null, - "inputFields": [ + }, { - "name": "id", + "name": "last_active_at", "description": null, + "args": [ + + ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } + "kind": "SCALAR", + "name": "Int", + "ofType": null }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", + "name": "login", + "description": null, + "args": [ + + ], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MoveTeamTaskDownPayload", - "description": "Autogenerated return type of MoveTeamTaskDown", - "fields": [ + }, { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", + "name": "name", + "description": null, "args": [ ], @@ -46451,130 +46902,948 @@ "deprecationReason": null }, { - "name": "team", + "name": "number_of_teams", "description": null, "args": [ ], "type": { - "kind": "OBJECT", - "name": "Team", + "kind": "SCALAR", + "name": "Int", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "team_task", + "name": "permissions", "description": null, "args": [ ], "type": { - "kind": "OBJECT", - "name": "TeamTask", + "kind": "SCALAR", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "MoveTeamTaskUpInput", - "description": "Autogenerated input type of MoveTeamTaskUp", - "fields": null, - "inputFields": [ + }, { - "name": "id", + "name": "profile_image", "description": null, + "args": [ + + ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", + "name": "providers", + "description": null, + "args": [ + + ], "type": { "kind": "SCALAR", - "name": "String", + "name": "JsonStringType", "ofType": null }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MoveTeamTaskUpPayload", - "description": "Autogenerated return type of MoveTeamTaskUp", - "fields": [ + }, { - "name": "clientMutationId", - "description": "A unique identifier for the client performing the mutation.", + "name": "settings", + "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "String", + "name": "JsonStringType", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "team", + "name": "source", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "Team", + "name": "Source", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "team_task", + "name": "source_id", "description": null, "args": [ ], "type": { - "kind": "OBJECT", - "name": "TeamTask", + "kind": "SCALAR", + "name": "Int", "ofType": null }, "isDeprecated": false, "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ + }, + { + "name": "team_ids", + "description": null, + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "team_user", + "description": null, + "args": [ + { + "name": "team_slug", + "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": "TeamUser", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "team_users", + "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": "status", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TeamUserConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "teams", + "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": "TeamConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "token", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "two_factor", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unconfirmed_email", + "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_teams", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uuid", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Media", + "description": "Media type", + "fields": [ + { + "name": "account", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Account", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "account_id", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "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": "domain", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "embed_path", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "file_path", + "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": "metadata", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "permissions", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "picture", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pusher_channel", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "quote", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "thumbnail_path", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "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": "url", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MediaConnection", + "description": "The connection type for Media.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MediaEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Media", + "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": "MediaEdge", + "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": "Media", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "MoveTeamTaskDownInput", + "description": "Autogenerated input type of MoveTeamTaskDown", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "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": "MoveTeamTaskDownPayload", + "description": "Autogenerated return type of MoveTeamTaskDown", + "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": "team", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Team", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "team_task", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "TeamTask", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "MoveTeamTaskUpInput", + "description": "Autogenerated input type of MoveTeamTaskUp", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "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": "MoveTeamTaskUpPayload", + "description": "Autogenerated return type of MoveTeamTaskUp", + "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": "team", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Team", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "team_task", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "TeamTask", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ ], "enumValues": null, @@ -52628,6 +53897,11 @@ "name": "Flag", "ofType": null }, + { + "kind": "OBJECT", + "name": "Me", + "ofType": null + }, { "kind": "OBJECT", "name": "Media", @@ -53176,7 +54450,7 @@ ], "type": { "kind": "OBJECT", - "name": "Team", + "name": "PublicTeam", "ofType": null }, "isDeprecated": false, @@ -53528,7 +54802,7 @@ ], "type": { "kind": "OBJECT", - "name": "Team", + "name": "PublicTeam", "ofType": null }, "isDeprecated": false, @@ -54031,34 +55305,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "cluster", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "Cluster", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "cluster_id", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "comments", "description": null, @@ -57702,6 +58948,20 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "public_team", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "PublicTeam", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "published", "description": null, @@ -58699,7 +59959,386 @@ ], "type": { "kind": "OBJECT", - "name": "ProjectMediaUser", + "name": "ProjectMediaUser", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PublicTeam", + "description": "Public team type", + "fields": [ + { + "name": "avatar", + "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": "id", + "description": null, + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "medias_count", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "permissions", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "private", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pusher_channel", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "slug", + "description": null, + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "spam_count", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "team_graphql_id", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "trash_count", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unconfirmed_count", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updated_at", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PublicTeamConnection", + "description": "The connection type for PublicTeam.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PublicTeamEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PublicTeam", + "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": "PublicTeamEdge", + "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": "PublicTeam", "ofType": null }, "isDeprecated": false, @@ -58713,245 +60352,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "PublicTeam", - "description": "Public team type", - "fields": [ - { - "name": "avatar", - "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": "id", - "description": null, - "args": [ - - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [ - - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "permissions", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "private", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pusher_channel", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "slug", - "description": null, - "args": [ - - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "spam_count", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "team_graphql_id", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "trash_count", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unconfirmed_count", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updated_at", - "description": null, - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "Query", @@ -59000,35 +60400,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "cluster", - "description": "Information about the cluster with given id", - "args": [ - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "type": { - "kind": "OBJECT", - "name": "Cluster", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "dynamic_annotation_field", "description": null, @@ -59236,7 +60607,7 @@ ], "type": { "kind": "OBJECT", - "name": "User", + "name": "Me", "ofType": null }, "isDeprecated": false, @@ -61218,7 +62589,7 @@ ], "type": { "kind": "OBJECT", - "name": "User", + "name": "Me", "ofType": null }, "isDeprecated": false, @@ -61491,7 +62862,7 @@ ], "type": { "kind": "OBJECT", - "name": "Team", + "name": "PublicTeam", "ofType": null }, "isDeprecated": false, @@ -67424,7 +68795,7 @@ ], "type": { "kind": "OBJECT", - "name": "Team", + "name": "PublicTeam", "ofType": null }, "isDeprecated": false, @@ -68869,21 +70240,21 @@ "description": "TiplineRequest type", "fields": [ { - "name": "annotation", + "name": "associated_graphql_id", "description": null, "args": [ ], "type": { - "kind": "OBJECT", - "name": "Annotation", + "kind": "SCALAR", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "annotation_id", + "name": "associated_id", "description": null, "args": [ @@ -68897,7 +70268,7 @@ "deprecationReason": null }, { - "name": "associated_graphql_id", + "name": "associated_type", "description": null, "args": [ @@ -68971,21 +70342,21 @@ "deprecationReason": null }, { - "name": "smooch_report_correction_sent_at", + "name": "smooch_data", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "Int", + "name": "JsonStringType", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "smooch_report_received_at", + "name": "smooch_report_correction_sent_at", "description": null, "args": [ @@ -68999,7 +70370,7 @@ "deprecationReason": null }, { - "name": "smooch_report_sent_at", + "name": "smooch_report_received_at", "description": null, "args": [ @@ -69013,7 +70384,7 @@ "deprecationReason": null }, { - "name": "smooch_report_update_received_at", + "name": "smooch_report_sent_at", "description": null, "args": [ @@ -69027,21 +70398,21 @@ "deprecationReason": null }, { - "name": "smooch_request_type", + "name": "smooch_report_update_received_at", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Int", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "smooch_user_external_identifier", + "name": "smooch_request_type", "description": null, "args": [ @@ -69055,7 +70426,7 @@ "deprecationReason": null }, { - "name": "smooch_user_request_language", + "name": "smooch_user_external_identifier", "description": null, "args": [ @@ -69069,7 +70440,7 @@ "deprecationReason": null }, { - "name": "smooch_user_slack_channel_url", + "name": "smooch_user_request_language", "description": null, "args": [ @@ -69083,7 +70454,7 @@ "deprecationReason": null }, { - "name": "updated_at", + "name": "smooch_user_slack_channel_url", "description": null, "args": [ @@ -69097,14 +70468,14 @@ "deprecationReason": null }, { - "name": "value_json", + "name": "updated_at", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "JsonStringType", + "name": "String", "ofType": null }, "isDeprecated": false, @@ -84326,477 +85697,47 @@ ], "type": { - "kind": "OBJECT", - "name": "TeamUserEdge", - "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": "UpdateTeamTaskInput", - "description": "Autogenerated input type of UpdateTeamTask", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "label", - "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": "order", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "fieldset", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "required", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "task_type", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "json_options", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "json_schema", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "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": "associated_type", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "show_in_browser_extension", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "conditional_info", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "options_diff", - "description": null, - "type": { - "kind": "SCALAR", - "name": "JsonStringType", - "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": "UpdateTeamTaskPayload", - "description": "Autogenerated return type of UpdateTeamTask", - "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": "team", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "Team", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "team_task", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "TeamTask", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "team_taskEdge", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "TeamTaskEdge", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateTeamUserInput", - "description": "Autogenerated input type of UpdateTeamUser", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "role", - "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": "UpdateTeamUserPayload", - "description": "Autogenerated return type of UpdateTeamUser", - "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": "team", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "Team", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "team_user", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "TeamUser", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "team_userEdge", - "description": null, - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "TeamUserEdge", - "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": "UpdateTiplineNewsletterInput", - "description": "Autogenerated input type of UpdateTiplineNewsletter", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "enabled", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", + "kind": "OBJECT", + "name": "TeamUserEdge", "ofType": null }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "introduction", + "name": "user", "description": null, + "args": [ + + ], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "User", "ofType": null }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateTeamTaskInput", + "description": "Autogenerated input type of UpdateTeamTask", + "fields": null, + "inputFields": [ { - "name": "language", + "name": "id", "description": null, "type": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null }, "defaultValue": null, @@ -84804,19 +85745,23 @@ "deprecationReason": null }, { - "name": "header_type", + "name": "label", "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": "header_overlay_text", + "name": "description", "description": null, "type": { "kind": "SCALAR", @@ -84828,11 +85773,11 @@ "deprecationReason": null }, { - "name": "content_type", + "name": "order", "description": null, "type": { "kind": "SCALAR", - "name": "String", + "name": "Int", "ofType": null }, "defaultValue": null, @@ -84840,7 +85785,7 @@ "deprecationReason": null }, { - "name": "rss_feed_url", + "name": "fieldset", "description": null, "type": { "kind": "SCALAR", @@ -84852,11 +85797,11 @@ "deprecationReason": null }, { - "name": "number_of_articles", + "name": "required", "description": null, "type": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null }, "defaultValue": null, @@ -84864,7 +85809,7 @@ "deprecationReason": null }, { - "name": "first_article", + "name": "task_type", "description": null, "type": { "kind": "SCALAR", @@ -84876,7 +85821,7 @@ "deprecationReason": null }, { - "name": "second_article", + "name": "json_options", "description": null, "type": { "kind": "SCALAR", @@ -84888,7 +85833,7 @@ "deprecationReason": null }, { - "name": "third_article", + "name": "json_schema", "description": null, "type": { "kind": "SCALAR", @@ -84900,11 +85845,11 @@ "deprecationReason": null }, { - "name": "footer", + "name": "keep_completed_tasks", "description": null, "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null }, "defaultValue": null, @@ -84912,11 +85857,11 @@ "deprecationReason": null }, { - "name": "send_every", + "name": "associated_type", "description": null, "type": { "kind": "SCALAR", - "name": "JsonStringType", + "name": "String", "ofType": null }, "defaultValue": null, @@ -84924,11 +85869,11 @@ "deprecationReason": null }, { - "name": "send_on", + "name": "show_in_browser_extension", "description": null, "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null }, "defaultValue": null, @@ -84936,7 +85881,7 @@ "deprecationReason": null }, { - "name": "timezone", + "name": "conditional_info", "description": null, "type": { "kind": "SCALAR", @@ -84948,11 +85893,11 @@ "deprecationReason": null }, { - "name": "time", + "name": "options_diff", "description": null, "type": { "kind": "SCALAR", - "name": "String", + "name": "JsonStringType", "ofType": null }, "defaultValue": null, @@ -84978,8 +85923,8 @@ }, { "kind": "OBJECT", - "name": "UpdateTiplineNewsletterPayload", - "description": "Autogenerated return type of UpdateTiplineNewsletter", + "name": "UpdateTeamTaskPayload", + "description": "Autogenerated return type of UpdateTeamTask", "fields": [ { "name": "clientMutationId", @@ -85010,28 +85955,28 @@ "deprecationReason": null }, { - "name": "tipline_newsletter", + "name": "team_task", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "TiplineNewsletter", + "name": "TeamTask", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "tipline_newsletterEdge", + "name": "team_taskEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "TiplineNewsletterEdge", + "name": "TeamTaskEdge", "ofType": null }, "isDeprecated": false, @@ -85047,8 +85992,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateTiplineResourceInput", - "description": "Autogenerated input type of UpdateTiplineResource", + "name": "UpdateTeamUserInput", + "description": "Autogenerated input type of UpdateTeamUser", "fields": null, "inputFields": [ { @@ -85064,91 +86009,7 @@ "deprecationReason": null }, { - "name": "uuid", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "title", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "content", - "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": "header_type", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "header_overlay_text", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "content_type", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rss_feed_url", + "name": "role", "description": null, "type": { "kind": "SCALAR", @@ -85159,18 +86020,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "number_of_articles", - "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.", @@ -85190,8 +86039,8 @@ }, { "kind": "OBJECT", - "name": "UpdateTiplineResourcePayload", - "description": "Autogenerated return type of UpdateTiplineResource", + "name": "UpdateTeamUserPayload", + "description": "Autogenerated return type of UpdateTeamUser", "fields": [ { "name": "clientMutationId", @@ -85222,28 +86071,42 @@ "deprecationReason": null }, { - "name": "tipline_resource", + "name": "team_user", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "TiplineResource", + "name": "TeamUser", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "tipline_resourceEdge", + "name": "team_userEdge", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "TiplineResourceEdge", + "name": "TeamUserEdge", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "User", "ofType": null }, "isDeprecated": false, @@ -85259,8 +86122,8 @@ }, { "kind": "INPUT_OBJECT", - "name": "UpdateUserInput", - "description": "Autogenerated input type of UpdateUser", + "name": "UpdateTiplineNewsletterInput", + "description": "Autogenerated input type of UpdateTiplineNewsletter", "fields": null, "inputFields": [ { @@ -85276,7 +86139,19 @@ "deprecationReason": null }, { - "name": "profile_image", + "name": "enabled", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "introduction", "description": null, "type": { "kind": "SCALAR", @@ -85288,11 +86163,11 @@ "deprecationReason": null }, { - "name": "current_team_id", + "name": "language", "description": null, "type": { "kind": "SCALAR", - "name": "Int", + "name": "String", "ofType": null }, "defaultValue": null, @@ -85300,7 +86175,7 @@ "deprecationReason": null }, { - "name": "email", + "name": "header_type", "description": null, "type": { "kind": "SCALAR", @@ -85312,7 +86187,7 @@ "deprecationReason": null }, { - "name": "name", + "name": "header_overlay_text", "description": null, "type": { "kind": "SCALAR", @@ -85324,7 +86199,31 @@ "deprecationReason": null }, { - "name": "current_project_id", + "name": "content_type", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rss_feed_url", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_articles", "description": null, "type": { "kind": "SCALAR", @@ -85336,7 +86235,7 @@ "deprecationReason": null }, { - "name": "password", + "name": "first_article", "description": null, "type": { "kind": "SCALAR", @@ -85348,7 +86247,7 @@ "deprecationReason": null }, { - "name": "password_confirmation", + "name": "second_article", "description": null, "type": { "kind": "SCALAR", @@ -85360,11 +86259,11 @@ "deprecationReason": null }, { - "name": "send_email_notifications", + "name": "third_article", "description": null, "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null }, "defaultValue": null, @@ -85372,11 +86271,11 @@ "deprecationReason": null }, { - "name": "send_successful_login_notifications", + "name": "footer", "description": null, "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null }, "defaultValue": null, @@ -85384,11 +86283,11 @@ "deprecationReason": null }, { - "name": "send_failed_login_notifications", + "name": "send_every", "description": null, "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "JsonStringType", "ofType": null }, "defaultValue": null, @@ -85396,11 +86295,11 @@ "deprecationReason": null }, { - "name": "accept_terms", + "name": "send_on", "description": null, "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null }, "defaultValue": null, @@ -85408,11 +86307,23 @@ "deprecationReason": null }, { - "name": "completed_signup", + "name": "timezone", "description": null, "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "time", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null }, "defaultValue": null, @@ -85438,8 +86349,8 @@ }, { "kind": "OBJECT", - "name": "UpdateUserPayload", - "description": "Autogenerated return type of UpdateUser", + "name": "UpdateTiplineNewsletterPayload", + "description": "Autogenerated return type of UpdateTiplineNewsletter", "fields": [ { "name": "clientMutationId", @@ -85456,28 +86367,42 @@ "deprecationReason": null }, { - "name": "user", + "name": "team", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "User", + "name": "Team", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "userEdge", + "name": "tipline_newsletter", "description": null, "args": [ ], "type": { "kind": "OBJECT", - "name": "UserEdge", + "name": "TiplineNewsletter", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tipline_newsletterEdge", + "description": null, + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "TiplineNewsletterEdge", "ofType": null }, "isDeprecated": false, @@ -85492,256 +86417,169 @@ "possibleTypes": null }, { - "kind": "OBJECT", - "name": "User", - "description": "User type", - "fields": [ + "kind": "INPUT_OBJECT", + "name": "UpdateTiplineResourceInput", + "description": "Autogenerated input type of UpdateTiplineResource", + "fields": null, + "inputFields": [ { - "name": "accepted_terms", + "name": "id", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "ID", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "annotations", + "name": "uuid", "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": "type", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], "type": { - "kind": "OBJECT", - "name": "AnnotationConnection", + "kind": "SCALAR", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "assignments", + "name": "title", "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": "team_id", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], "type": { - "kind": "OBJECT", - "name": "ProjectMediaConnection", + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "content", + "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": "bot", + "name": "header_type", "description": null, - "args": [ - - ], "type": { - "kind": "OBJECT", - "name": "BotUser", + "kind": "SCALAR", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "bot_events", + "name": "header_overlay_text", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "completed_signup", + "name": "content_type", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "confirmed", + "name": "rss_feed_url", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "created_at", + "name": "number_of_articles", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Int", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "current_project", - "description": 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": "UpdateTiplineResourcePayload", + "description": "Autogenerated return type of UpdateTiplineResource", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", "args": [ ], "type": { - "kind": "OBJECT", - "name": "Project", + "kind": "SCALAR", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "current_team", + "name": "team", "description": null, "args": [ @@ -85755,394 +86593,348 @@ "deprecationReason": null }, { - "name": "current_team_id", + "name": "tipline_resource", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "Int", + "kind": "OBJECT", + "name": "TiplineResource", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "dbid", + "name": "tipline_resourceEdge", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "Int", + "kind": "OBJECT", + "name": "TiplineResourceEdge", "ofType": null }, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateUserInput", + "description": "Autogenerated input type of UpdateUser", + "fields": null, + "inputFields": [ { - "name": "email", + "name": "id", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "feed_invitations", + "name": "profile_image", "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": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "FeedInvitationConnection", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "get_send_email_notifications", + "name": "current_team_id", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "Int", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "get_send_failed_login_notifications", + "name": "email", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "get_send_successful_login_notifications", + "name": "name", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "id", + "name": "current_project_id", "description": null, - "args": [ - - ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } + "kind": "SCALAR", + "name": "Int", + "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "is_active", + "name": "password", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "is_admin", + "name": "password_confirmation", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "is_bot", + "name": "send_email_notifications", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "jsonsettings", + "name": "send_successful_login_notifications", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "last_accepted_terms_at", + "name": "send_failed_login_notifications", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "last_active_at", + "name": "accept_terms", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "login", + "name": "completed_signup", "description": null, - "args": [ - - ], "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "name", - "description": null, - "args": [ - - ], + "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": "UpdateUserPayload", + "description": "Autogenerated return type of UpdateUser", + "fields": [ { - "name": "number_of_teams", - "description": null, + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", "args": [ ], "type": { "kind": "SCALAR", - "name": "Int", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "permissions", + "name": "me", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "Me", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "profile_image", + "name": "user", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "User", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "providers", + "name": "userEdge", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "JsonStringType", + "kind": "OBJECT", + "name": "UserEdge", "ofType": null }, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "User", + "description": "User type", + "fields": [ { - "name": "settings", + "name": "created_at", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "JsonStringType", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "source", + "name": "dbid", "description": null, "args": [ ], "type": { - "kind": "OBJECT", - "name": "Source", + "kind": "SCALAR", + "name": "Int", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "source_id", + "name": "email", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "Int", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "team_ids", + "name": "id", "description": null, "args": [ ], "type": { - "kind": "LIST", + "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "ID", "ofType": null } }, @@ -86150,170 +86942,49 @@ "deprecationReason": null }, { - "name": "team_user", + "name": "is_active", "description": null, "args": [ - { - "name": "team_slug", - "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": "TeamUser", + "kind": "SCALAR", + "name": "Boolean", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "team_users", + "name": "is_bot", "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": "status", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } + ], "type": { - "kind": "OBJECT", - "name": "TeamUserConnection", + "kind": "SCALAR", + "name": "Boolean", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "teams", + "name": "last_active_at", "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": "TeamConnection", + "kind": "SCALAR", + "name": "Int", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "token", + "name": "name", "description": null, "args": [ @@ -86327,21 +86998,21 @@ "deprecationReason": null }, { - "name": "two_factor", + "name": "number_of_teams", "description": null, "args": [ ], "type": { "kind": "SCALAR", - "name": "JsonStringType", + "name": "Int", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "unconfirmed_email", + "name": "permissions", "description": null, "args": [ @@ -86355,7 +87026,7 @@ "deprecationReason": null }, { - "name": "updated_at", + "name": "profile_image", "description": null, "args": [ @@ -86369,21 +87040,21 @@ "deprecationReason": null }, { - "name": "user_teams", + "name": "source", "description": null, "args": [ ], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "Source", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "uuid", + "name": "updated_at", "description": null, "args": [ @@ -86585,7 +87256,7 @@ ], "type": { "kind": "OBJECT", - "name": "User", + "name": "Me", "ofType": null }, "isDeprecated": false, @@ -86870,7 +87541,7 @@ ], "type": { "kind": "OBJECT", - "name": "User", + "name": "Me", "ofType": null }, "isDeprecated": false, diff --git a/test/controllers/elastic_search_10_test.rb b/test/controllers/elastic_search_10_test.rb index fbeb56e98d..4dd96cee74 100644 --- a/test/controllers/elastic_search_10_test.rb +++ b/test/controllers/elastic_search_10_test.rb @@ -254,7 +254,6 @@ def setup test "should filter by keyword and requests fields" do # Reuests fields are username, identifier and content create_annotation_type_and_fields('Smooch User', { 'Id' => ['Text', false], 'Data' => ['JSON', false] }) - create_annotation_type_and_fields('Smooch', { 'Data' => ['JSON', false] }) t = create_team u = create_user create_team_user team: t, user: u, role: 'admin' @@ -302,9 +301,9 @@ def setup create_dynamic_annotation annotated: pm2, annotation_type: 'smooch_user', set_fields: { smooch_user_id: twitter_uid, smooch_user_data: { raw: twitter_data }.to_json }.to_json with_current_user_and_team(u, t) do wa_smooch_data = { 'authorId' => whatsapp_uid, 'text' => 'smooch_request a', 'name' => 'wa_user', 'language' => 'en' } - smooch_pm = create_dynamic_annotation annotated: pm, annotation_type: 'smooch', set_fields: { smooch_data: wa_smooch_data.to_json }.to_json, disable_es_callbacks: false + smooch_pm = create_tipline_request associated: pm, team_id: t.id, language: 'en', smooch_data: wa_smooch_data, disable_es_callbacks: false twitter_smooch_data = { 'authorId' => twitter_uid, 'text' => 'smooch_request b', 'name' => 'melsawy', 'language' => 'fr' } - smooch_pm2 = create_dynamic_annotation annotated: pm2, annotation_type: 'smooch', set_fields: { smooch_data: twitter_smooch_data.to_json }.to_json, disable_es_callbacks: false + smooch_pm2 = create_tipline_request associated: pm2, team_id: t.id, language: 'fr', smooch_data: twitter_smooch_data, disable_es_callbacks: false sleep 2 result = CheckSearch.new({keyword: 'smooch_request', keyword_fields: {fields: ['request_content']}}.to_json) assert_equal [pm.id, pm2.id], result.medias.map(&:id).sort diff --git a/test/controllers/elastic_search_3_test.rb b/test/controllers/elastic_search_3_test.rb index 77a1db3a20..18d9988f53 100644 --- a/test/controllers/elastic_search_3_test.rb +++ b/test/controllers/elastic_search_3_test.rb @@ -149,146 +149,5 @@ def setup assert_equal [pm.id, pm2.id].sort, result.medias.map(&:id).sort end - test "should sort by cluster_published_reports_count" do - RequestStore.store[:skip_cached_field_update] = false - t = create_team - t2 = create_team - f = create_feed - f.teams << t - f.teams << t2 - FeedTeam.update_all(shared: true) - pm1 = create_project_media team: t - c1 = create_cluster project_media: pm1 - c1.project_medias << pm1 - pm2 = create_project_media team: t - c2 = create_cluster project_media: pm2 - c2.project_medias << pm2 - pm2_t2 = create_project_media team: t2 - c1.project_medias << pm2_t2 - publish_report(pm2) - publish_report(pm2_t2) - publish_report(pm1) - sleep 2 - Team.stubs(:current).returns(t) - query = { clusterize: true, feed_id: f.id, sort: 'cluster_published_reports_count' } - result = CheckSearch.new(query.to_json) - assert_equal [pm1.id, pm2.id], result.medias.map(&:id) - query[:sort_type] = 'asc' - result = CheckSearch.new(query.to_json) - assert_equal [pm2.id, pm1.id], result.medias.map(&:id) - # filter by published_by filter `cluster_published_reports` - query = { clusterize: true, feed_id: f.id, cluster_published_reports: [t.id, t2.id]} - result = CheckSearch.new(query.to_json) - assert_equal [pm1.id, pm2.id], result.medias.map(&:id).sort - query = { clusterize: true, feed_id: f.id, cluster_published_reports: [t2.id]} - result = CheckSearch.new(query.to_json) - assert_equal [pm1.id], result.medias.map(&:id) - Team.unstub(:current) - end - - test "should sort by cluster_first_item_at" do - t = create_team - f = create_feed - f.teams << t - FeedTeam.update_all(shared: true) - Time.stubs(:now).returns(Time.new - 2.week) - pm1 = create_project_media team: t - c1 = create_cluster project_media: pm1 - c1.project_medias << pm1 - Time.stubs(:now).returns(Time.new - 1.week) - pm2 = create_project_media team: t - c2 = create_cluster project_media: pm2 - c2.project_medias << pm2 - Time.stubs(:now).returns(Time.new - 3.week) - pm3 = create_project_media team: t - c3 = create_cluster project_media: pm3 - c3.project_medias << pm3 - Time.unstub(:now) - sleep 2 - Team.stubs(:current).returns(t) - query = { clusterize: true, feed_id: f.id, sort: 'cluster_first_item_at' } - result = CheckSearch.new(query.to_json) - assert_equal [pm2.id, pm1.id, pm3.id], result.medias.map(&:id) - query[:sort_type] = 'asc' - result = CheckSearch.new(query.to_json) - assert_equal [pm3.id, pm1.id, pm2.id], result.medias.map(&:id) - Team.unstub(:current) - end - - test "should sort by clusters requests count" do - RequestStore.store[:skip_cached_field_update] = false - create_annotation_type_and_fields('Smooch', { 'Data' => ['JSON', false] }) - t = create_team - f = create_feed - f.teams << t - FeedTeam.update_all(shared: true) - u = create_user - create_team_user team: t, user: u, role: 'admin' - pm1 = create_project_media team: t - pm1_1 = create_project_media team: t - pm2 = create_project_media team: t - create_dynamic_annotation annotation_type: 'smooch', annotated: pm1 - create_dynamic_annotation annotation_type: 'smooch', annotated: pm2 - c1 = create_cluster project_media: pm1 - c2 = create_cluster project_media: pm2 - c1.project_medias << pm1 - c1.project_medias << pm1_1 - c2.project_medias << pm2 - sleep 2 - with_current_user_and_team(u, t) do - create_dynamic_annotation annotation_type: 'smooch', annotated: pm1 - create_dynamic_annotation annotation_type: 'smooch', annotated: pm1_1 - sleep 2 - es1 = $repository.find(get_es_id(pm1)) - es2 = $repository.find(get_es_id(pm2)) - assert_equal c1.requests_count(true), es1['cluster_requests_count'] - assert_equal c2.requests_count(true), es2['cluster_requests_count'] - query = { clusterize: true, feed_id: f.id, sort: 'cluster_requests_count' } - result = CheckSearch.new(query.to_json) - assert_equal [pm1.id, pm2.id], result.medias.map(&:id) - query[:sort_type] = 'asc' - result = CheckSearch.new(query.to_json) - assert_equal [pm2.id, pm1.id], result.medias.map(&:id) - end - end - - test "should sort by cluster_size" do - t = create_team - f = create_feed - f.teams << t - FeedTeam.update_all(shared: true) - u = create_user - create_team_user team: t, user: u, role: 'admin' - pm1 = create_project_media team: t - pm1_1 = create_project_media team: t - pm1_2 = create_project_media team: t - pm2 = create_project_media team: t - pm2_1 = create_project_media team: t - pm2_2 = create_project_media team: t - pm2_3 = create_project_media team: t - pm3 = create_project_media team: t - pm3_1 = create_project_media team: t - c1 = create_cluster project_media: pm1 - c2 = create_cluster project_media: pm2 - c3 = create_cluster project_media: pm3 - c1.project_medias << pm1 - c1.project_medias << pm1_1 - c1.project_medias << pm1_2 - c2.project_medias << pm2 - c2.project_medias << pm2_1 - c2.project_medias << pm2_2 - c2.project_medias << pm2_3 - c3.project_medias << pm3 - c3.project_medias << pm3_1 - sleep 2 - with_current_user_and_team(u, t) do - query = { clusterize: true, feed_id: f.id, sort: 'cluster_size' } - result = CheckSearch.new(query.to_json) - assert_equal [pm2.id, pm1.id, pm3.id], result.medias.map(&:id) - query[:sort_type] = 'asc' - result = CheckSearch.new(query.to_json) - assert_equal [pm3.id, pm1.id, pm2.id], result.medias.map(&:id) - end - end # Please add new tests to test/controllers/elastic_search_7_test.rb end diff --git a/test/controllers/elastic_search_8_test.rb b/test/controllers/elastic_search_8_test.rb index 11b75fcf52..6936e38a9f 100644 --- a/test/controllers/elastic_search_8_test.rb +++ b/test/controllers/elastic_search_8_test.rb @@ -36,10 +36,8 @@ def setup create_relationship source_id: pm3.id, target_id: t2_pm3.id, relationship_type: Relationship.suggested_type # Add requests - create_annotation_type_and_fields('Smooch', { 'Data' => ['JSON', false] }) - create_dynamic_annotation annotation_type: 'smooch', annotated: pm2 - 2.times { create_dynamic_annotation(annotation_type: 'smooch', annotated: pm3) } - + create_tipline_request team_id: p.team_id, associated: pm2 + 2.times { create_tipline_request(team_id: p.team_id, associated: pm3) } sleep 2 min_mapping = { diff --git a/test/controllers/elastic_search_9_test.rb b/test/controllers/elastic_search_9_test.rb index 1677b435fe..b47980d4e1 100644 --- a/test/controllers/elastic_search_9_test.rb +++ b/test/controllers/elastic_search_9_test.rb @@ -26,69 +26,6 @@ def setup end end - test "should filter feed by workspace" do - RequestStore.store[:skip_cached_field_update] = false - f = create_feed - t1 = create_team ; f.teams << t1 - t2 = create_team ; f.teams << t2 - t3 = create_team ; f.teams << t3 - FeedTeam.update_all(shared: true) - pm1 = create_project_media team: t1 - c1 = create_cluster project_media: pm1 - c1.project_medias << pm1 - pm2 = create_project_media team: t2 - c2 = create_cluster project_media: pm2 - c2.project_medias << pm2 - pm3 = create_project_media team: t3 - c2.project_medias << pm3 - sleep 2 - u = create_user - create_team_user team: t1, user: u, role: 'admin' - with_current_user_and_team(u, t1) do - query = { clusterize: true, feed_id: f.id } - result = CheckSearch.new(query.to_json) - assert_equal [pm1.id, pm2.id], result.medias.map(&:id).sort - query[:cluster_teams] = [t1.id] - result = CheckSearch.new(query.to_json) - assert_equal [pm1.id], result.medias.map(&:id) - query[:cluster_teams] = [t2.id, t3.id] - result = CheckSearch.new(query.to_json) - assert_equal [pm2.id], result.medias.map(&:id) - # Get current team - assert_equal t1, result.team - end - end - - test "should filter feed by report status" do - create_verification_status_stuff - RequestStore.store[:skip_cached_field_update] = false - t = create_team - f = create_feed - f.teams << t - FeedTeam.update_all(shared: true) - pm1 = create_project_media team: t - c1 = create_cluster project_media: pm1 - c1.project_medias << pm1 - pm2 = create_project_media team: t - c2 = create_cluster project_media: pm2 - c2.project_medias << pm2 - sleep 2 - u = create_user - create_team_user team: t, user: u, role: 'admin' - with_current_user_and_team(u, t) do - publish_report(pm1) - query = { clusterize: true, feed_id: f.id, report_status: ['published', 'unpublished'] } - result = CheckSearch.new(query.to_json) - assert_equal [pm1.id, pm2.id], result.medias.map(&:id).sort - query[:report_status] = ['published'] - result = CheckSearch.new(query.to_json) - assert_equal [pm1.id], result.medias.map(&:id) - query[:report_status] = ['unpublished'] - result = CheckSearch.new(query.to_json) - assert_equal [pm2.id], result.medias.map(&:id) - end - end - test "should search for keywords with typos" do t = create_team p = create_project team: t @@ -266,34 +203,6 @@ def setup assert_equal [pm1.id, pm3.id, pm2.id], result.medias.map(&:id) end - test "should filter feed by organization" do - f = create_feed - t1 = create_team ; f.teams << t1 - t2 = create_team ; f.teams << t2 - t3 = create_team ; f.teams << t3 - FeedTeam.update_all(shared: true) - pm1 = create_project_media team: t1, disable_es_callbacks: false - pm2 = create_project_media team: t2, disable_es_callbacks: false - pm3 = create_project_media team: t3, disable_es_callbacks: false - sleep 2 - u = create_user - create_team_user team: t1, user: u, role: 'admin' - with_current_user_and_team(u, t1) do - query = { feed_id: f.id } - result = CheckSearch.new(query.to_json) - assert_equal [pm1.id, pm2.id, pm3.id], result.medias.map(&:id).sort - query[:feed_team_ids] = [t1.id, t3.id] - result = CheckSearch.new(query.to_json) - assert_equal [pm1.id, pm3.id], result.medias.map(&:id).sort - query[:feed_team_ids] = [t2.id] - result = CheckSearch.new(query.to_json) - assert_equal [pm2.id], result.medias.map(&:id) - query[:feed_team_ids] = [] - result = CheckSearch.new(query.to_json) - assert_empty result.medias.map(&:id) - end - end - test "shoud add team filter by default" do t = create_team t2 = create_team diff --git a/test/controllers/graphql_controller_10_test.rb b/test/controllers/graphql_controller_10_test.rb index b053b06e5c..781852d57a 100644 --- a/test/controllers/graphql_controller_10_test.rb +++ b/test/controllers/graphql_controller_10_test.rb @@ -79,14 +79,17 @@ def setup create_team_user team: t, user: u, role: 'admin' create_team_user team: t, user: u2 authenticate_with_user(u) - query = "query GetById { team(id: \"#{t.id}\") { team_users { edges { node { user { dbid, get_send_email_notifications, get_send_successful_login_notifications, get_send_failed_login_notifications, source { medias(first: 1) { edges { node { id } } } }, annotations(first: 1) { edges { node { id } } }, team_users(first: 1) { edges { node { id } } }, bot { get_description, get_role, get_version, get_source_code_url } } } } } } }" + query = "query GetById { team(id: \"#{t.id}\") { team_users { edges { node { user { dbid, source { medias(first: 1) { edges { node { id } } } } } } } } } }" post :create, params: { query: query, team: t.slug } - assert_response :success data = JSON.parse(@response.body)['data']['team']['team_users']['edges'] ids = data.collect{ |i| i['node']['user']['dbid'] } assert_equal 3, data.size assert_equal [u.id, u2.id, u3.id], ids.sort + # Quey bot + query = "query { me { dbid, get_send_email_notifications, get_send_successful_login_notifications, get_send_failed_login_notifications, source { medias(first: 1) { edges { node { id } } } }, annotations(first: 1) { edges { node { id } } }, team_users(first: 1) { edges { node { id } } }, bot { get_description, get_role, get_version, get_source_code_url } } }" + post :create, params: { query: query } + assert_response :success end test "should get team tasks" do @@ -552,11 +555,10 @@ def setup t = create_team create_team_user user: u, team: t, status: 'member' create_team_user user: u2, team: t, status: 'member' - p = create_project team: t - pm1 = create_project_media project: p - pm2 = create_project_media project: p - pm3 = create_project_media project: p - pm4 = create_project_media project: p + pm1 = create_project_media team: t + pm2 = create_project_media team: t + pm3 = create_project_media team: t + pm4 = create_project_media team: t s1 = create_status status: 'in_progress', annotated: pm1 s2 = create_status status: 'in_progress', annotated: pm2 s3 = create_status status: 'in_progress', annotated: pm3 @@ -568,9 +570,8 @@ def setup s3.assign_user(u.id) s4.assign_user(u2.id) authenticate_with_user(u) - post :create, params: { query: "query GetById { user(id: \"#{u.id}\") { assignments(first: 10) { edges { node { dbid, assignments(first: 10, user_id: #{u.id}, annotation_type: \"task\") { edges { node { dbid } } } } } } } }" } - assert_response :success - data = JSON.parse(@response.body)['data']['user'] + post :create, params: { query: "query { me { assignments(first: 10) { edges { node { dbid, assignments(first: 10, user_id: #{u.id}, annotation_type: \"task\") { edges { node { dbid } } } } } } } }" } + data = JSON.parse(@response.body)['data']['me'] assert_equal [pm3.id, pm2.id, pm1.id], data['assignments']['edges'].collect{ |x| x['node']['dbid'] } assert_equal [t2.id], data['assignments']['edges'][0]['node']['assignments']['edges'].collect{ |x| x['node']['dbid'].to_i } assert_equal [], data['assignments']['edges'][1]['node']['assignments']['edges'] @@ -784,10 +785,10 @@ def setup t = create_team create_team_user user: u, team: t, role: 'editor' pm = create_project_media team: t - a = create_dynamic_annotation annotation_type: 'smooch', set_fields: { smooch_data: { authorId: '123', language: 'en', received: Time.now.to_f }.to_json }.to_json, annotated: pm + tr = create_tipline_request team_id: t.id, associated: pm, language: 'en', smooch_data: { authorId: '123', language: 'en', received: Time.now.to_f } authenticate_with_user(u) - query = "mutation { sendTiplineMessage(input: { clientMutationId: \"1\", message: \"Hello\", inReplyToId: #{a.id} }) { success } }" + query = "mutation { sendTiplineMessage(input: { clientMutationId: \"1\", message: \"Hello\", inReplyToId: #{tr.id} }) { success } }" post :create, params: { query: query, team: t.slug } assert_response :success @@ -799,10 +800,10 @@ def setup u = create_user t = create_team pm = create_project_media team: t - a = create_dynamic_annotation annotation_type: 'smooch', set_fields: { smooch_data: { authorId: '123', language: 'en', received: Time.now.to_f }.to_json }.to_json, annotated: pm + tr = create_tipline_request team_id: t.id, associated: pm, language: 'en', smooch_data: { authorId: '123', language: 'en', received: Time.now.to_f } authenticate_with_user(u) - query = "mutation { sendTiplineMessage(input: { clientMutationId: \"1\", message: \"Hello\", inReplyToId: #{a.id} }) { success } }" + query = "mutation { sendTiplineMessage(input: { clientMutationId: \"1\", message: \"Hello\", inReplyToId: #{tr.id} }) { success } }" post :create, params: { query: query, team: t.slug } assert_response :success diff --git a/test/controllers/graphql_controller_12_test.rb b/test/controllers/graphql_controller_12_test.rb index c88d511d7d..46a4aa90d0 100644 --- a/test/controllers/graphql_controller_12_test.rb +++ b/test/controllers/graphql_controller_12_test.rb @@ -276,4 +276,51 @@ def teardown assert_response :success assert_equal [1, 2, 3].sort, JSON.parse(@response.body).dig('data', 'feed', 'data_points').sort end + + test "should return me type after update user" do + user = create_user + authenticate_with_user(user) + post :create, params: { query: 'query Query { me { id } }' } + assert_response :success + id = JSON.parse(@response.body)['data']['me']['id'] + query = 'mutation { updateUser(input: { clientMutationId: "1", id: "' + id + '", name: "update name" }) { user { dbid }, me { dbid } } }' + post :create, params: { query: query } + assert_response :success + data = JSON.parse(@response.body)['data']['updateUser']['me'] + assert_equal user.id, data['dbid'] + end + + test "should ensure graphql introspection is disabled" do + user = create_user + authenticate_with_user(user) + INTROSPECTION_QUERY = <<-GRAPHQL + { + __schema { + queryType { + name + } + } + } + GRAPHQL + + post :create, params: { query: INTROSPECTION_QUERY } + assert_response :success + response_body = JSON.parse(response.body) + assert_equal response_body['errors'][0]['message'], "Field '__schema' doesn't exist on type 'Query'" + end + + test "should return feed clusters" do + n = random_number(5) # In order to avoid N + 1 query problems, we need to be sure that the number of SQL queries is the same regardless the number of clusters + puts "Testing with #{n} clusters" + f = create_feed team: @t + n.times { create_cluster feed: f, team_ids: [@t.id], project_media: create_project_media(team: @t) } + + authenticate_with_user(@u) + query = 'query { feed(id: "' + f.id.to_s + '") { clusters_count, clusters(first: 10) { edges { node { id, dbid, fact_checks_count, first_item_at, last_item_at, last_request_date, last_fact_check_date, center { id }, teams(first: 10) { edges { node { name, avatar } } } } } } } }' + assert_queries 20, '<=' do + post :create, params: { query: query } + end + assert_response :success + assert_equal n, JSON.parse(@response.body)['data']['feed']['clusters']['edges'].size + end end diff --git a/test/controllers/graphql_controller_2_test.rb b/test/controllers/graphql_controller_2_test.rb index 929898e4fc..294139c421 100644 --- a/test/controllers/graphql_controller_2_test.rb +++ b/test/controllers/graphql_controller_2_test.rb @@ -53,7 +53,7 @@ def setup authenticate_with_user tb1 = create_team_bot set_listed: true tb2 = create_team_bot set_listed: false - query = "query read { root { current_user { id }, current_team { id }, team_bots_listed { edges { node { dbid } } } } }" + query = "query read { root { current_user { id }, current_team { id }, team_bots_listed { edges { node { dbid, get_description, get_version, get_source_code_url, get_role } } } } }" post :create, params: { query: query } edges = JSON.parse(@response.body)['data']['root']['team_bots_listed']['edges'] assert_equal [tb1.id], edges.collect{ |e| e['node']['dbid'] } diff --git a/test/controllers/graphql_controller_3_test.rb b/test/controllers/graphql_controller_3_test.rb index e2892e565c..330401c607 100644 --- a/test/controllers/graphql_controller_3_test.rb +++ b/test/controllers/graphql_controller_3_test.rb @@ -235,20 +235,18 @@ def setup test "should retrieve information for grid" do RequestStore.store[:skip_cached_field_update] = false - u = create_user authenticate_with_user(u) t = create_team slug: 'team' create_team_user user: u, team: t p = create_project team: t - m = create_uploaded_image pm = create_project_media project: p, user: create_user, media: m, disable_es_callbacks: false info = { title: random_string, content: random_string }; pm.analysis = info; pm.save! - create_dynamic_annotation(annotation_type: 'smooch', annotated: pm, set_fields: { smooch_data: '{}' }.to_json) + create_tipline_request team_id: t.id, associated: pm, smooch_data: {} pm2 = create_project_media project: p r = create_relationship source_id: pm.id, target_id: pm2.id, relationship_type: Relationship.confirmed_type - create_dynamic_annotation(annotation_type: 'smooch', annotated: pm2, set_fields: { smooch_data: '{}' }.to_json) + create_tipline_request team_id: t.id, associated: pm2, smooch_data: {} create_claim_description project_media: pm, description: 'Test' sleep 10 @@ -348,9 +346,9 @@ def setup pm = create_project_media team: t pm2 = create_project_media team: t authenticate_with_user(u) - create_dynamic_annotation annotation_type: 'smooch', annotated: pm, set_fields: { smooch_data: { 'authorId' => random_string }.to_json }.to_json - create_dynamic_annotation annotation_type: 'smooch', annotated: pm, set_fields: { smooch_data: { 'authorId' => random_string }.to_json }.to_json - create_dynamic_annotation annotation_type: 'smooch', annotated: pm2, set_fields: { smooch_data: { 'authorId' => random_string }.to_json }.to_json + create_tipline_request team_id: t.id, associated: pm, smooch_data: { 'authorId' => random_string } + 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 } } } } }" post :create, params: { query: query, team: t.slug } diff --git a/test/controllers/graphql_controller_5_test.rb b/test/controllers/graphql_controller_5_test.rb index 834f756074..d8b9bee096 100644 --- a/test/controllers/graphql_controller_5_test.rb +++ b/test/controllers/graphql_controller_5_test.rb @@ -151,23 +151,6 @@ def setup end end - test "should get cluster information" do - u = create_user is_admin: true - f = create_feed - t = create_team - f.teams << t - p = create_project team: t - pm = create_project_media project: p - c = create_cluster project_media: pm - c.project_medias << pm - c.project_medias << create_project_media - authenticate_with_user(u) - query = 'query { project_media(ids: "' + [pm.id, p.id, t.id].join(',') + '") { cluster { first_item_at, last_item_at, claim_descriptions(feed_id: ' + f.id.to_s + ') { edges { node { id } } }, items(feed_id: ' + f.id.to_s + ') { edges { node { dbid } } } } } }' - post :create, params: { query: query, team: t.slug } - assert_response :success - assert_equal 1, JSON.parse(@response.body)['data']['project_media']['cluster']['items']['edges'].size - end - test "should paginate folder items" do u = create_user is_admin: true t = create_team diff --git a/test/controllers/graphql_controller_6_test.rb b/test/controllers/graphql_controller_6_test.rb index 8321ba4988..54c94a287e 100644 --- a/test/controllers/graphql_controller_6_test.rb +++ b/test/controllers/graphql_controller_6_test.rb @@ -169,63 +169,6 @@ def teardown assert_response :success end - test "should search by feed" do - setup_elasticsearch - t1 = create_team - t2 = create_team - u = create_user - create_team_user(team: t1, user: u, role: 'admin') - authenticate_with_user(u) - f_ss = create_saved_search team: t1, filters: { keyword: 'apple' } - f = create_feed team_id: t1.id - f.teams = [t1, t2] - f.saved_search = f_ss - f.save! - - # Team 1 content to be shared - ft1 = FeedTeam.where(feed: f, team: t1).last - ft1.shared = false - ft1.save! - pm1a = create_project_media quote: 'I like apple and banana', team: t1 - pm1b = create_project_media quote: 'I like orange and banana', team: t1 - - # Team 2 content to be shared - ft2_ss = create_saved_search team: t2, filters: { keyword: 'orange' } - ft2 = FeedTeam.where(feed: f, team: t2).last - ft2.shared = true - ft2.saved_search = ft2_ss - ft2.save! - pm2a = create_project_media quote: 'I love apple and banana', team: t2 - pm2b = create_project_media quote: 'I love orange and banana', team: t2 - - # Wait for content to be indexed in ElasticSearch - sleep 2 - query = 'query CheckSearch { search(query: "{\"keyword\":\"and\",\"feed_id\":' + f.id.to_s + '}") { medias(first: 20) { edges { node { dbid } } } } }' - - # Can't see anything until content is shared - post :create, params: { query: query, team: t1.slug } - assert_response :success - assert_equal [], JSON.parse(@response.body)['data']['search']['medias']['edges'] - - # See content after content is shared - with_current_user_and_team(u, t1) do - ft1.shared = true - ft1.save! - end - post :create, params: { query: query, team: t1.slug } - assert_response :success - assert_equal [pm1a.id, pm2b.id].sort, JSON.parse(@response.body)['data']['search']['medias']['edges'].collect{ |pm| pm['node']['dbid'] }.sort - - # Filter by published if feed is published - with_current_user_and_team(nil, nil) do - f.published = true - f.save! - end - post :create, params: { query: query, team: t1.slug } - assert_response :success - assert_equal [], JSON.parse(@response.body)['data']['search']['medias']['edges'] - end - test "should send GraphQL queries in batch" do u = create_user is_admin: true authenticate_with_user(u) diff --git a/test/controllers/graphql_controller_7_test.rb b/test/controllers/graphql_controller_7_test.rb index 5ff40f119f..70a48b2e30 100644 --- a/test/controllers/graphql_controller_7_test.rb +++ b/test/controllers/graphql_controller_7_test.rb @@ -401,9 +401,9 @@ def teardown test "should get user confirmed" do u = create_user authenticate_with_user(u) - post :create, params: { query: "query GetById { user(id: \"#{u.id}\") { confirmed } }" } + post :create, params: { query: "query { me { confirmed } }" } assert_response :success - data = JSON.parse(@response.body)['data']['user'] + data = JSON.parse(@response.body)['data']['me'] assert data['confirmed'] end diff --git a/test/controllers/graphql_controller_8_test.rb b/test/controllers/graphql_controller_8_test.rb index 4533ad9def..b7ed4ff6f8 100644 --- a/test/controllers/graphql_controller_8_test.rb +++ b/test/controllers/graphql_controller_8_test.rb @@ -61,7 +61,7 @@ def setup tu2 = create_team_user user: u2, team: t2 authenticate_with_user(u) - query = 'query { me { team_user(team_slug: "' + t.slug + '") { dbid } } }' + query = 'query { me { team_users(first: 1) { edges { node { id } } }, team_user(team_slug: "' + t.slug + '") { dbid, invited_by { dbid } } } }' post :create, params: { query: query } assert_response :success assert_equal tu.id, JSON.parse(@response.body)['data']['me']['team_user']['dbid'] @@ -538,6 +538,7 @@ def setup } public_team { id + medias_count trash_count unconfirmed_count spam_count @@ -692,7 +693,7 @@ def setup test "should get current user" do u = create_user name: 'Test User' authenticate_with_user(u) - post :create, params: { query: 'query Query { me { source_id, token, is_admin, current_project { id }, name, bot { id } } }' } + post :create, params: { query: 'query Query { me { get_send_email_notifications, get_send_successful_login_notifications, get_send_failed_login_notifications, annotations(first: 1) { edges { node { id } } }, source { dbid }, source_id, token, is_admin, current_project { id }, name, bot { id } } }' } assert_response :success data = JSON.parse(@response.body)['data']['me'] assert_equal 'Test User', data['name'] diff --git a/test/controllers/graphql_controller_test.rb b/test/controllers/graphql_controller_test.rb index fbfc71378b..06f5dbcbbf 100644 --- a/test/controllers/graphql_controller_test.rb +++ b/test/controllers/graphql_controller_test.rb @@ -420,10 +420,11 @@ def setup create_team_user user: u, team: t p = create_project team: t pm = create_project_media project: p - query = "query GetById { project_media(ids: \"#{pm.id},#{p.id}\") { team { name } } }" + query = "query GetById { project_media(ids: \"#{pm.id},#{p.id}\") { team { name }, public_team { name } } }" post :create, params: { query: query, team: 'team' } assert_response :success assert_equal t.name, JSON.parse(@response.body)['data']['project_media']['team']['name'] + assert_equal t.name, JSON.parse(@response.body)['data']['project_media']['public_team']['name'] end test "should run few queries to get project data" do @@ -605,11 +606,8 @@ def setup assert_response :success # check invited by u = User.find_by_email 'test1@local.com' - query = "query GetById { user(id: \"#{u.id}\") { team_user(team_slug: \"#{@team.slug}\") { invited_by { dbid } } } }" - post :create, params: { query: query, team: @team.slug } - assert_response :success - data = JSON.parse(@response.body)['data']['user']['team_user'] - assert_equal User.current.id, data['invited_by']['dbid'] + data = u.team_users.where(team_id: @team.id).last + assert_equal User.current.id, data.invited_by_id # resend/cancel invitation query = 'mutation resendCancelInvitation { resendCancelInvitation(input: { clientMutationId: "1", email: "notexist@local.com", action: "resend" }) { success } }' post :create, params: { query: query, team: @team.slug } diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb index 76a9ccf4a6..9e58de0782 100644 --- a/test/controllers/sessions_controller_test.rb +++ b/test/controllers/sessions_controller_test.rb @@ -71,5 +71,32 @@ def setup assert_not_nil @controller.current_api_user end + # test "should lock user after excessive login requests" do + # u = create_user login: 'test', password: '12345678', password_confirmation: '12345678', email: 'test@test.com' + # Devise.maximum_attempts = 2 + # 2.times do + # post :create, params: { api_user: { email: 'test@test.com', password: '12345679' } } + # end + + # u.reload + # assert u.access_locked? + # assert_not_nil u.locked_at + # end + + # test "should unlock locked user accounts after specified time" do + # travel_to Time.zone.local(2023, 12, 12, 01, 04, 44) + # u = create_user login: 'test', password: '12345678', password_confirmation: '12345678', email: 'test@test.com' + # Devise.unlock_in = 10.minutes + + + # u.lock_access! + + # travel 30.minutes + # post :create, params: { api_user: { email: 'test@test.com', password: '12345678' } } + + # u.reload + # assert !u.access_locked? + # assert_nil u.locked_at + # end end diff --git a/test/lib/check_rack_attack_test.rb b/test/lib/check_rack_attack_test.rb new file mode 100644 index 0000000000..ce32666574 --- /dev/null +++ b/test/lib/check_rack_attack_test.rb @@ -0,0 +1,32 @@ +require 'test_helper' + +class ThrottlingTest < ActionDispatch::IntegrationTest + setup do + Rails.cache.clear + end + + test "should throttle excessive requests to /api/graphql" do + stub_configs({ 'api_rate_limit' => 2 }) do + 2.times do + post api_graphql_path + assert_response :unauthorized + end + + post api_graphql_path + assert_response :too_many_requests + end + end + + test "should throttle excessive requests to /api/users/sign_in" do + stub_configs({ 'login_rate_limit' => 2 }) do + user_params = { api_user: { email: 'user@example.com', password: 'password' } } + + 2.times do + post api_user_session_path, params: user_params, as: :json + end + + post api_user_session_path, params: user_params, as: :json + assert_response :too_many_requests + end + end +end diff --git a/test/lib/smooch_nlu_test.rb b/test/lib/smooch_nlu_test.rb index dddcdec083..86e01015f8 100644 --- a/test/lib/smooch_nlu_test.rb +++ b/test/lib/smooch_nlu_test.rb @@ -110,7 +110,7 @@ def create_team_with_smooch_bot_installed team = create_team_with_smooch_bot_installed SmoochNlu.new(team.slug).disable! Bot::Smooch.get_installation('smooch_id', 'test') - assert_equal [], SmoochNlu.menu_options_from_message('I want to subscribe to the newsletter', 'en', @menu_options) + assert_equal [], SmoochNlu.menu_options_from_message('I want to subscribe to the newsletter', 'en', @menu_options, random_string) end test 'should return a menu option if NLU is enabled' do @@ -120,6 +120,31 @@ def create_team_with_smooch_bot_installed team = create_team_with_smooch_bot_installed SmoochNlu.new(team.slug).enable! Bot::Smooch.get_installation('smooch_id', 'test') - assert_not_nil SmoochNlu.menu_options_from_message('I want to subscribe to the newsletter', 'en', @menu_options) + assert_not_nil SmoochNlu.menu_options_from_message('I want to subscribe to the newsletter', 'en', @menu_options, random_string) + end + + test 'should return empty list of matches if global rate limit is reached' do + Bot::Smooch.stubs(:config).returns({ 'nlu_enabled' => true }) + Bot::Alegre.stubs(:request).never + SmoochNlu.increment_global_counter + SmoochNlu.increment_global_counter + stub_configs({ 'nlu_global_rate_limit' => 1 }) do + assert_equal [], SmoochNlu.alegre_matches_from_message('test', 'en', {}, 'test', 'test') + end + end + + test 'should return empty list of matches if user rate limit is reached' do + Bot::Smooch.stubs(:config).returns({ 'nlu_enabled' => true }) + Bot::Alegre.stubs(:request).never + 2.times { create_tipline_message uid: '123', state: 'received' } + stub_configs({ 'nlu_user_rate_limit' => 1 }) do + assert_equal [], SmoochNlu.alegre_matches_from_message('test', 'en', {}, 'test', '123') + end + end + + test 'should return empty list of matches if exception happens' do + Bot::Smooch.stubs(:config).returns({ 'nlu_enabled' => true }) + Bot::Alegre.stubs(:request).never + assert_equal [], SmoochNlu.alegre_matches_from_message('test', 'en', {}, 'test', 'test') end end diff --git a/test/models/ability_test.rb b/test/models/ability_test.rb index 28c7d82a05..222baf4e5d 100644 --- a/test/models/ability_test.rb +++ b/test/models/ability_test.rb @@ -103,6 +103,25 @@ def teardown end end + test "#{role} permissions for tipline request" do + u = create_user + t = create_team + t2 = create_team + tu = create_team_user team: t, user: u, role: role + pm = create_project_media team: t + pm2 = create_project_media team: t2 + tr = create_tipline_request team_id: t.id, associated: pm + tr2 = create_tipline_request team_id: t2.id, associated: pm2 + with_current_user_and_team(u, t) do + ability = Ability.new + assert ability.can?(:create, TiplineRequest) + assert ability.can?(:update, tr) + assert ability.can?(:destroy, tr) + assert ability.cannot?(:update, tr2) + assert ability.cannot?(:destroy, tr2) + end + end + test "#{role} permissions for tag" do u = create_user t = create_team diff --git a/test/models/bot/alegre_2_test.rb b/test/models/bot/alegre_2_test.rb index b64b51a192..e6b812047b 100644 --- a/test/models/bot/alegre_2_test.rb +++ b/test/models/bot/alegre_2_test.rb @@ -665,26 +665,6 @@ def teardown Bot::Alegre.unstub(:get_items_with_similar_description) end - test "should set cluster" do - c1 = create_cluster - c2 = create_cluster - pm1 = create_project_media team: @team, cluster_id: c1.id - pm2 = create_project_media team: @team, cluster_id: c2.id - - ProjectMedia.any_instance.stubs(:similar_items_ids_and_scores).returns({ pm1.id => { score: 0.9 }, pm2.id => { score: 0.8 } }) - pm3 = create_project_media team: @team - Bot::Alegre.set_cluster(pm3) - assert_equal c1.id, pm3.reload.cluster_id - - ProjectMedia.any_instance.stubs(:similar_items_ids_and_scores).returns({}) - pm4 = create_project_media team: @team - assert_difference 'Cluster.count' do - Bot::Alegre.set_cluster(pm4) - end - - ProjectMedia.any_instance.unstub(:similar_items_ids_and_scores) - end - test "should get number of words" do assert_equal 4, Bot::Alegre.get_number_of_words('58 This is a test !!! 123 😊') assert_equal 1, Bot::Alegre.get_number_of_words(random_url) diff --git a/test/models/bot/alegre_3_test.rb b/test/models/bot/alegre_3_test.rb index 778fc47fb8..319c3cc77e 100644 --- a/test/models/bot/alegre_3_test.rb +++ b/test/models/bot/alegre_3_test.rb @@ -72,7 +72,6 @@ def teardown } } create_annotation_type_and_fields('Transcription', {}, json_schema) - create_annotation_type_and_fields('Smooch', { 'Data' => ['JSON', true] }) tbi = Bot::Alegre.get_alegre_tbi(@team.id) tbi.set_transcription_similarity_enabled = false tbi.save! @@ -120,8 +119,8 @@ def teardown assert_nil a # Audio item match all required conditions by verify transcription_minimum_requests count RequestStore.store[:skip_cached_field_update] = false - create_dynamic_annotation annotation_type: 'smooch', annotated: pm1 - create_dynamic_annotation annotation_type: 'smooch', annotated: pm1 + create_tipline_request team: @team.id, associated: pm1 + create_tipline_request team: @team.id, associated: pm1 assert Bot::Alegre.run({ data: { dbid: pm1.id }, event: 'create_project_media' }) a = pm1.annotations('transcription').last assert_equal "", a.data['text'] @@ -139,7 +138,6 @@ def teardown } } create_annotation_type_and_fields('Transcription', {}, json_schema) - create_annotation_type_and_fields('Smooch', { 'Data' => ['JSON', true] }) tbi = Bot::Alegre.get_alegre_tbi(@team.id) tbi.set_transcription_similarity_enabled = false tbi.save! @@ -187,8 +185,8 @@ def teardown assert_nil a # Audio item match all required conditions by verify transcription_minimum_requests count RequestStore.store[:skip_cached_field_update] = false - create_dynamic_annotation annotation_type: 'smooch', annotated: pm1 - create_dynamic_annotation annotation_type: 'smooch', annotated: pm1 + create_tipline_request team_id: @pm.team_id, associated: pm1 + create_tipline_request team_id: @pm.team_id, associated: pm1 assert Bot::Alegre.run({ data: { dbid: pm1.id }, event: 'create_project_media' }) a = pm1.annotations('transcription').last expected_last_response = {"job_status"=>"COMPLETED", "transcription"=>"Foo bar"} diff --git a/test/models/bot/alegre_test.rb b/test/models/bot/alegre_test.rb index 3db5f0dd5b..732166a51c 100644 --- a/test/models/bot/alegre_test.rb +++ b/test/models/bot/alegre_test.rb @@ -18,6 +18,7 @@ def setup create_flag_annotation_type create_extracted_text_annotation_type Sidekiq::Testing.inline! + WebMock.disable_net_connect! allow: /#{CheckConfig.get('elasticsearch_host')}|#{CheckConfig.get('storage_endpoint')}/ end def teardown @@ -101,6 +102,7 @@ def teardown test "should unarchive item after running" do 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 pm.archived = CheckArchivedFlags::FlagCodes::PENDING_SIMILARITY_ANALYSIS pm.save! @@ -210,6 +212,8 @@ def teardown end test "should use OCR data for similarity matching" do + WebMock.stub_request(:post, 'http://alegre:3100/text/langid/').with(body: { text: 'Foo bar' }.to_json).to_return(status: 200, body: '{}') + WebMock.stub_request(:post, 'http://alegre:3100/text/similarity/').to_return(status: 200, body: '{}') pm = create_project_media team: @team pm2 = create_project_media team: @team Bot::Alegre.stubs(:get_items_with_similar_description).returns({ pm2.id => {:score=>0.9, :context=>{"team_id"=>@team.id, "field"=>"original_description", "project_media_id"=>pm2.id, "has_custom_id"=>true}, :model=>"elasticsearch"} }) @@ -256,6 +260,8 @@ def teardown # This test to reproduce errbit error CHECK-1218 test "should match to existing parent" do + WebMock.stub_request(:post, 'http://alegre:3100/text/langid/').with(body: { text: 'Foo bar' }.to_json).to_return(status: 200, body: '{}') + WebMock.stub_request(:post, 'http://alegre:3100/text/similarity/').to_return(status: 200, body: '{}') pm_s = create_project_media team: @team pm = create_project_media team: @team pm2 = create_project_media team: @team @@ -271,6 +277,9 @@ def teardown end test "should use transcription data for similarity matching" do + WebMock.stub_request(:post, 'http://alegre:3100/text/langid/').with(body: { text: 'Foo bar' }.to_json).to_return(status: 200, body: '{}') + WebMock.stub_request(:delete, 'http://alegre:3100/text/similarity/').to_return(status: 200, body: '{}') + WebMock.stub_request(:post, 'http://alegre:3100/text/similarity/').to_return(status: 200, body: '{}') json_schema = { type: 'object', required: ['job_name'], @@ -295,6 +304,8 @@ def teardown end test "should check existing relationship before create a new one" do + WebMock.stub_request(:post, 'http://alegre:3100/text/similarity/').to_return(status: 200, body: '{}') + WebMock.stub_request(:post, 'http://alegre:3100/text/langid/').with(body: { text: 'Foo bar' }.to_json).to_return(status: 200, body: '{}') pm = create_project_media team: @team pm2 = create_project_media team: @team pm3 = create_project_media team: @team diff --git a/test/models/bot/smooch_2_test.rb b/test/models/bot/smooch_2_test.rb index f1812c1cf9..ef996a2609 100644 --- a/test/models/bot/smooch_2_test.rb +++ b/test/models/bot/smooch_2_test.rb @@ -28,6 +28,7 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: c } ] @@ -61,6 +62,7 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: url } ] @@ -99,7 +101,7 @@ def teardown s.save! child = create_project_media project: @project - create_dynamic_annotation annotation_type: 'smooch', annotated: child, set_fields: { smooch_message_id: random_string, smooch_data: { app_id: @app_id, authorId: random_string, language: 'en' }.to_json }.to_json + create_tipline_request team_id: @project.team_id, associated: child, language: 'en', smooch_message_id: random_string, smooch_data: { app_id: @app_id, authorId: random_string, language: 'en' } r = create_relationship source_id: parent.id, target_id: child.id, relationship_type: Relationship.confirmed_type, user: create_user s = child.annotations.where(annotation_type: 'verification_status').last.load assert_equal 'verified', s.status @@ -117,6 +119,7 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: random_string } ] @@ -179,6 +182,7 @@ def aasm(_arg) '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: t } ] @@ -229,6 +233,7 @@ def unique_payload(uid, message_text) '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: message_text } ], @@ -263,6 +268,7 @@ def unique_payload(uid, message_text) '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: random_string } ] @@ -372,6 +378,7 @@ def unique_payload(uid, message_text) '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: random_string } ] @@ -412,6 +419,7 @@ def unique_payload(uid, message_text) '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: random_string } ] @@ -439,6 +447,7 @@ def unique_payload(uid, message_text) '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: random_string } ] @@ -507,7 +516,8 @@ def unique_payload(uid, message_text) body: 'Test' }, timestamp: '1623865510', - type: 'text' + type: 'text', + source: { type: "whatsapp" }, } ] } @@ -570,6 +580,7 @@ def unique_payload(uid, message_text) }, timestamp: '1623865510', type: 'image', + source: { type: "whatsapp" }, image: { id: '123456', mime_type: 'image/png' diff --git a/test/models/bot/smooch_3_test.rb b/test/models/bot/smooch_3_test.rb index 33f6de1a98..b11b372b64 100644 --- a/test/models/bot/smooch_3_test.rb +++ b/test/models/bot/smooch_3_test.rb @@ -46,6 +46,7 @@ def teardown '_id': random_string, authorId: random_string, type: 'text', + source: { type: "whatsapp" }, text: random_string } ] @@ -65,8 +66,7 @@ def teardown pm = ProjectMedia.last assert_equal 'undetermined', pm.last_verification_status # Get requests data - sm = pm.get_annotations('smooch').last - requests = DynamicAnnotation::Field.where(annotation_id: sm.id, field_name: 'smooch_data') + requests = TiplineRequest.where(associated_type: 'ProjectMedia', associated_id: pm.id) assert_equal 1, requests.count end @@ -78,12 +78,14 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: 'foo', }, { '_id': random_string, authorId: uid, type: 'image', + source: { type: "whatsapp" }, text: 'first image', mediaUrl: @media_url }, @@ -91,6 +93,7 @@ def teardown '_id': random_string, authorId: uid, type: 'image', + source: { type: "whatsapp" }, text: 'second image', mediaUrl: @media_url_2 }, @@ -98,7 +101,8 @@ def teardown '_id': random_string, authorId: uid, type: 'text', - text: 'bar' + text: 'bar', + source: { type: "whatsapp" }, } ] messages.each do |message| @@ -138,6 +142,7 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: random_string } ] @@ -182,6 +187,7 @@ def teardown # video message = { type: 'file', + source: { type: "whatsapp" }, text: random_string, mediaUrl: @video_url, mediaType: 'image/jpeg', @@ -189,7 +195,8 @@ def teardown received: 1573082583.219, name: random_string, authorId: random_string, - '_id': random_string + '_id': random_string, + language: 'en', } assert_difference 'ProjectMedia.count' do Bot::Smooch.save_message(message.to_json, @app_id) @@ -201,6 +208,7 @@ def teardown # audio message = { type: 'file', + source: { type: "whatsapp" }, text: random_string, mediaUrl: @audio_url, mediaType: 'image/jpeg', @@ -208,7 +216,8 @@ def teardown received: 1573082583.219, name: random_string, authorId: random_string, - '_id': random_string + '_id': random_string, + language: 'en', } assert_difference 'ProjectMedia.count' do Bot::Smooch.save_message(message.to_json, @app_id) @@ -226,6 +235,7 @@ def teardown '_id': random_string, authorId: random_string, type: 'image', + source: { type: "whatsapp" }, text: random_string, mediaUrl: @media_url_3, mediaSize: UploadedImage.max_size + random_number @@ -235,6 +245,7 @@ def teardown authorId: random_string, type: 'file', mediaType: 'image/jpeg', + source: { type: "whatsapp" }, text: random_string, mediaUrl: @media_url_2, mediaSize: UploadedImage.max_size + random_number @@ -244,6 +255,7 @@ def teardown authorId: random_string, type: 'video', mediaType: 'video/mp4', + source: { type: "whatsapp" }, text: random_string, mediaUrl: @video_url, mediaSize: UploadedVideo.max_size + random_number @@ -253,6 +265,7 @@ def teardown authorId: random_string, type: 'audio', mediaType: 'audio/mpeg', + source: { type: "whatsapp" }, text: random_string, mediaUrl: @audio_url, mediaSize: UploadedAudio.max_size + random_number @@ -288,6 +301,7 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: '2' } ], @@ -309,6 +323,7 @@ def teardown '_id': random_string, authorId: random_string, type: 'text', + source: { type: "whatsapp" }, text: random_string, name: nil } @@ -336,7 +351,8 @@ def teardown type: 'file', text: random_string, mediaUrl: @video_url, - mediaType: 'video/mp4' + mediaType: 'video/mp4', + source: { type: "whatsapp" }, }.with_indifferent_access is_supported = Bot::Smooch.supported_message?(message) assert is_supported.slice(:type, :size).all?{ |_k, v| v } @@ -347,7 +363,8 @@ def teardown type: 'file', text: random_string, mediaUrl: @video_url, - mediaType: 'newtype/ogg' + mediaType: 'newtype/ogg', + source: { type: "whatsapp" }, }.with_indifferent_access is_supported = Bot::Smooch.supported_message?(message) assert !is_supported.slice(:type, :size).all?{ |_k, v| v } @@ -357,7 +374,8 @@ def teardown authorId: random_string, type: 'file', text: random_string, - mediaUrl: @video_url + mediaUrl: @video_url, + source: { type: "whatsapp" }, }.with_indifferent_access is_supported = Bot::Smooch.supported_message?(message) assert is_supported.slice(:type, :size).all?{ |_k, v| v } @@ -368,7 +386,8 @@ def teardown type: 'file', text: random_string, mediaUrl: @audio_url, - mediaType: 'audio/mpeg' + mediaType: 'audio/mpeg', + source: { type: "whatsapp" }, }.with_indifferent_access is_supported = Bot::Smooch.supported_message?(message) assert is_supported.slice(:type, :size).all?{ |_k, v| v } @@ -379,7 +398,8 @@ def teardown type: 'file', text: random_string, mediaUrl: @audio_url, - mediaType: 'newtype/mp4' + mediaType: 'newtype/mp4', + source: { type: "whatsapp" }, }.with_indifferent_access is_supported = Bot::Smooch.supported_message?(message) assert !is_supported.slice(:type, :size).all?{ |_k, v| v } @@ -389,7 +409,8 @@ def teardown authorId: random_string, type: 'file', text: random_string, - mediaUrl: @audio_url + mediaUrl: @audio_url, + source: { type: "whatsapp" }, }.with_indifferent_access is_supported = Bot::Smooch.supported_message?(message) assert is_supported.slice(:type, :size).all?{ |_k, v| v } diff --git a/test/models/bot/smooch_4_test.rb b/test/models/bot/smooch_4_test.rb index 92358f3da7..22e634eb1e 100644 --- a/test/models/bot/smooch_4_test.rb +++ b/test/models/bot/smooch_4_test.rb @@ -89,7 +89,6 @@ def teardown end test "should store Smooch conversation id" do - create_annotation_type_and_fields('Smooch', { 'Conversation Id' => ['Text', true] }) conversation_id = random_string result = OpenStruct.new({ conversation: OpenStruct.new({ id: conversation_id })}) SmoochApi::ConversationApi.any_instance.stubs(:get_messages).returns(result) @@ -100,6 +99,7 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: random_string } ] @@ -115,12 +115,12 @@ def teardown 'conversationStarted': true } }.to_json - assert_difference "DynamicAnnotation::Field.where(field_name: 'smooch_conversation_id').count" do + assert_difference "TiplineRequest.count" do Bot::Smooch.run(payload) end pm = ProjectMedia.last - a = pm.annotations('smooch').last - assert_equal conversation_id, a.load.get_field_value('smooch_conversation_id') + tr = TiplineRequest.last + assert_equal conversation_id, tr.smooch_conversation_id end SmoochApi::ConversationApi.any_instance.unstub(:get_messages) end @@ -134,6 +134,7 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: random_string } ] @@ -175,18 +176,21 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: 'foo', }, { '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: 'bar' }, { '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: 'test' } ] @@ -206,10 +210,10 @@ def teardown Bot::Smooch.run(payload) sleep 1 end - assert_difference 'Dynamic.where(annotation_type: "smooch").count' do + assert_difference 'TiplineRequest.count' do Sidekiq::Worker.drain_all end - assert_equal ['bar', 'foo', 'test'], JSON.parse(Dynamic.where(annotation_type: 'smooch').last.get_field_value('smooch_data'))['text'].split(Bot::Smooch::MESSAGE_BOUNDARY).map(&:chomp).sort + assert_equal ['bar', 'foo', 'test'], TiplineRequest.last.smooch_data['text'].split(Bot::Smooch::MESSAGE_BOUNDARY).map(&:chomp).sort end end @@ -254,7 +258,7 @@ def teardown Rails.cache.unstub(:read) Sidekiq::Worker.drain_all assert_equal 'waiting_for_message', sm.state.value - assert_equal ['Hello for the last time', 'ONE ', '2', 'Query'], JSON.parse(Dynamic.where(annotation_type: 'smooch').last.get_field_value('smooch_data'))['text'].split(Bot::Smooch::MESSAGE_BOUNDARY).map(&:chomp) + assert_equal ['Hello for the last time', 'ONE ', '2', 'Query'], TiplineRequest.last.smooch_data['text'].split(Bot::Smooch::MESSAGE_BOUNDARY).map(&:chomp) assert_equal 'Hello for the last time', ProjectMedia.last.text end @@ -376,16 +380,16 @@ def teardown u = create_user is_admin: true t = create_team with_current_user_and_team(u, t) do - d = create_dynamic_annotation annotation_type: 'smooch', set_fields: { smooch_message_id: random_string, smooch_data: { 'authorId' => whatsapp_uid }.to_json }.to_json - assert_equal '+55 12 3456-7890', d.get_field('smooch_data').smooch_user_external_identifier - d = create_dynamic_annotation annotation_type: 'smooch', set_fields: { smooch_message_id: random_string, smooch_data: { 'authorId' => twitter_uid }.to_json }.to_json - assert_equal '@foobar', d.get_field('smooch_data').smooch_user_external_identifier - d = create_dynamic_annotation annotation_type: 'smooch', set_fields: { smooch_message_id: random_string, smooch_data: { 'authorId' => facebook_uid }.to_json }.to_json - assert_equal '123456', d.get_field('smooch_data').smooch_user_external_identifier - d = create_dynamic_annotation annotation_type: 'smooch', set_fields: { smooch_message_id: random_string, smooch_data: { 'authorId' => telegram_uid }.to_json }.to_json - assert_equal '@barfoo', d.get_field('smooch_data').smooch_user_external_identifier - d = create_dynamic_annotation annotation_type: 'smooch', set_fields: { smooch_message_id: random_string, smooch_data: { 'authorId' => other_uid }.to_json }.to_json - assert_equal '', d.get_field('smooch_data').smooch_user_external_identifier + tr = create_tipline_request team_id: t.id, smooch_message_id: random_string, smooch_data: { 'authorId' => whatsapp_uid } + assert_equal '+55 12 3456-7890', tr.smooch_user_external_identifier + tr = create_tipline_request team_id: t.id,smooch_message_id: random_string, smooch_data: { 'authorId' => twitter_uid } + assert_equal '@foobar', tr.smooch_user_external_identifier + tr = create_tipline_request team_id: t.id, smooch_message_id: random_string, smooch_data: { 'authorId' => facebook_uid } + assert_equal '123456', tr.smooch_user_external_identifier + tr = create_tipline_request team_id: t.id, smooch_message_id: random_string, smooch_data: { 'authorId' => telegram_uid } + assert_equal '@barfoo', tr.smooch_user_external_identifier + tr = create_tipline_request team_id: t.id, smooch_message_id: random_string, smooch_data: { 'authorId' => other_uid } + assert_equal '', tr.smooch_user_external_identifier end end @@ -485,9 +489,9 @@ def teardown send_message_to_smooch_bot('4', uid) end Sidekiq::Worker.drain_all - a = Dynamic.where(annotation_type: 'smooch').last - assert_equal 'TiplineResource', a.annotated_type - assert_not_nil a.get_field('smooch_resource_id') + tr = TiplineRequest.last + assert_equal 'TiplineResource', tr.associated_type + assert_not_nil tr.smooch_resource_id end test "should submit short unconfirmed request" do @@ -502,9 +506,8 @@ def teardown assert_no_difference 'ProjectMedia.count' do Sidekiq::Worker.drain_all end - a = Dynamic.where(annotation_type: 'smooch').last - annotated = a.annotated - assert_equal 'Team', a.annotated_type + tr = TiplineRequest.last + assert_equal 'Team', tr.associated_type end test "should submit long unconfirmed request" do @@ -519,13 +522,13 @@ def teardown assert_difference 'ProjectMedia.count' do Sidekiq::Worker.drain_all end - a = Dynamic.where(annotation_type: 'smooch').last - annotated = a.annotated - assert_equal 'ProjectMedia', a.annotated_type - assert_equal CheckArchivedFlags::FlagCodes::UNCONFIRMED, annotated.archived + tr = TiplineRequest.last + associated = tr.associated + assert_equal 'ProjectMedia', tr.associated_type + assert_equal CheckArchivedFlags::FlagCodes::UNCONFIRMED, associated.archived # Verify requests count & demand - assert_equal 1, annotated.requests_count - assert_equal 1, annotated.demand + assert_equal 1, associated.requests_count + assert_equal 1, associated.demand # Auto confirm the media if the same media is sent as a default request Sidekiq::Testing.fake! do send_message_to_smooch_bot(message, uid) @@ -539,8 +542,8 @@ def teardown end Rails.cache.unstub(:read) Sidekiq::Worker.drain_all - assert_equal CheckArchivedFlags::FlagCodes::NONE, annotated.reload.archived - assert_equal 2, annotated.reload.requests_count + assert_equal CheckArchivedFlags::FlagCodes::NONE, associated.reload.archived + assert_equal 2, associated.reload.requests_count # Test resend the same media (should not update archived column) Sidekiq::Testing.fake! do send_message_to_smooch_bot('Hello', uid) @@ -549,8 +552,8 @@ def teardown assert_no_difference 'ProjectMedia.count' do Sidekiq::Worker.drain_all end - assert_equal CheckArchivedFlags::FlagCodes::NONE, annotated.reload.archived - assert_equal 3, annotated.reload.requests_count + assert_equal CheckArchivedFlags::FlagCodes::NONE, associated.reload.archived + assert_equal 3, associated.reload.requests_count end test "should get default TOS message" do @@ -573,11 +576,13 @@ def teardown text: random_string, mediaUrl: @video_url, mediaType: 'video/mp4', + source: { type: "whatsapp" }, role: 'appUser', received: 1573082583.219, name: random_string, authorId: random_string, - '_id': random_string + '_id': random_string, + language: 'en' } medias_count = Media.count assert_difference 'ProjectMedia.count', 1 do @@ -602,9 +607,11 @@ def teardown text = "\vstring for testing\t\n " message = { type: 'text', + source: { type: "whatsapp" }, text: text, authorId: random_string, - '_id': random_string + '_id': random_string, + language: 'en', } assert_difference 'ProjectMedia.count', 1 do Bot::Smooch.save_message(message.to_json, @app_id) @@ -614,9 +621,11 @@ def teardown text = "\vstring FOR teSTIng\t\n " message = { type: 'text', + source: { type: "whatsapp" }, text: text, authorId: random_string, - '_id': random_string + '_id': random_string, + language: 'en', } assert_no_difference 'ProjectMedia.count' do Bot::Smooch.save_message(message.to_json, @app_id) @@ -626,9 +635,11 @@ def teardown text = "\vTEST with existing item \t\n " message = { type: 'text', + source: { type: "whatsapp" }, text: text, authorId: random_string, - '_id': random_string + '_id': random_string, + language: 'en', } assert_no_difference 'ProjectMedia.count' do Bot::Smooch.save_message(message.to_json, @app_id) diff --git a/test/models/bot/smooch_5_test.rb b/test/models/bot/smooch_5_test.rb index 4a59d5e7cd..2c6aeae8ac 100644 --- a/test/models/bot/smooch_5_test.rb +++ b/test/models/bot/smooch_5_test.rb @@ -43,7 +43,7 @@ def teardown pm4a = create_project_media quote: 'Test 4', team: t4 # Should not be in search results (team is not part of feed) pm4b = create_project_media media: l, team: t4 # Should not be in search results by URL ss = create_saved_search team: t1, filters: { show: ['claims', 'weblink'] } - f1 = create_feed team_id: t1.id, published: true + f1 = create_feed team_id: t1.id, published: true, data_points: [1, 2] f1.teams << t2 FeedTeam.update_all(shared: true) f1.teams << t3 diff --git a/test/models/bot/smooch_6_test.rb b/test/models/bot/smooch_6_test.rb index dcd9f2c76d..2af10953f3 100644 --- a/test/models/bot/smooch_6_test.rb +++ b/test/models/bot/smooch_6_test.rb @@ -62,13 +62,13 @@ def assert_state(expected) end def assert_saved_query_type(type) - assert_difference "DynamicAnnotation::Field.where('value LIKE ?', '%#{type}%').count" do + assert_difference "TiplineRequest.where('smooch_request_type LIKE ?', '%#{type}%').count" do Sidekiq::Worker.drain_all end end def assert_no_saved_query - assert_no_difference "Dynamic.where(annotation_type: 'smooch').count" do + assert_no_difference "TiplineRequest.count" do Sidekiq::Worker.drain_all end end @@ -223,7 +223,7 @@ def send_message_outside_24_hours_window(template, pm = nil) Sidekiq::Testing.inline! do send_message 'hello', '1', '1', 'Foo bar', '1' assert_state 'search_result' - assert_difference 'Dynamic.where(annotation_type: "smooch").count + ProjectMedia.count + Relationship.where(relationship_type: Relationship.suggested_type).count', 3 do + assert_difference 'TiplineRequest.count + ProjectMedia.count', 2 do send_message '1' end assert_state 'main' @@ -240,7 +240,7 @@ def send_message_outside_24_hours_window(template, pm = nil) Sidekiq::Testing.inline! do send_message 'hello', '1', '1', 'Foo bar foo bar foo bar', '1' assert_state 'search_result' - assert_difference 'Dynamic.where(annotation_type: "smooch").count + ProjectMedia.count + Relationship.where(relationship_type: Relationship.suggested_type).count', 3 do + assert_difference 'TiplineRequest.count + ProjectMedia.count', 2 do send_message '1' end assert_state 'main' @@ -257,12 +257,12 @@ def send_message_outside_24_hours_window(template, pm = nil) 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::Smooch.stubs(:bundle_list_of_messages).returns({ 'type' => 'image', 'mediaUrl' => image_url }) + 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 send_message 'hello', '1', '1', 'Image here', '1' assert_state 'search_result' - assert_difference 'Dynamic.where(annotation_type: "smooch").count + ProjectMedia.count + Relationship.where(relationship_type: Relationship.suggested_type).count', 3 do + assert_difference 'TiplineRequest.count + ProjectMedia.count', 2 do send_message '1' end assert_state 'main' @@ -292,7 +292,7 @@ def send_message_outside_24_hours_window(template, pm = nil) Sidekiq::Testing.inline! do send_message 'hello', '1', '1', 'Foo bar', '1' assert_state 'search_result' - assert_difference 'Dynamic.count + ProjectMedia.count', 3 do + assert_difference 'Dynamic.count + TiplineRequest.count + ProjectMedia.count', 3 do send_message '2' end assert_state 'waiting_for_message' @@ -394,6 +394,7 @@ def send_message_outside_24_hours_window(template, pm = nil) type: 'whatsapp', integrationId: random_string }, + language: 'en', } Bot::Smooch.save_message(message.to_json, @app_id, nil, 'menu_options_requests', pm) message = { @@ -410,6 +411,7 @@ def send_message_outside_24_hours_window(template, pm = nil) type: 'messenger', integrationId: random_string }, + language: 'en', } Bot::Smooch.save_message(message.to_json, @app_id, nil, 'menu_options_requests', pm) # verifiy new channel value @@ -429,6 +431,7 @@ def send_message_outside_24_hours_window(template, pm = nil) type: 'messenger', integrationId: random_string }, + language: 'en', } Bot::Smooch.save_message(message.to_json, @app_id, nil, 'menu_options_requests', pm2) # verifiy new channel value @@ -612,8 +615,8 @@ def send_message_outside_24_hours_window(template, pm = nil) send_message url, '1', url, '1' assert_state 'search' Sidekiq::Worker.drain_all - d = Dynamic.where(annotation_type: 'smooch').last - assert_equal 2, JSON.parse(d.get_field_value('smooch_data'))['text'].split("\n#{Bot::Smooch::MESSAGE_BOUNDARY}").select{ |x| x.chomp.strip == url }.size + tr = TiplineRequest.last + assert_equal 2, tr.smooch_data['text'].split("\n#{Bot::Smooch::MESSAGE_BOUNDARY}").select{ |x| x.chomp.strip == url }.size end test "should get search results in different languages" do @@ -871,7 +874,7 @@ def send_message_outside_24_hours_window(template, pm = nil) send_message 'hello', '1' # Sends a first message and confirms language as English send_message 'This is message is so long that it is considered a media' assert_difference 'ProjectMedia.count' do - assert_difference "Dynamic.where(annotation_type: 'smooch').count" do + assert_difference "TiplineRequest.count" do Sidekiq::Worker.drain_all end end @@ -884,7 +887,7 @@ def send_message_outside_24_hours_window(template, pm = nil) send_message 'hello', '1' # Sends a first message and confirms language as English send_message 'Hi, there!' assert_no_difference 'ProjectMedia.count' do - assert_difference "Dynamic.where(annotation_type: 'smooch').count" do + assert_difference "TiplineRequest.count" do Sidekiq::Worker.drain_all end end diff --git a/test/models/bot/smooch_7_test.rb b/test/models/bot/smooch_7_test.rb index e0041231b3..5e3acf755a 100644 --- a/test/models/bot/smooch_7_test.rb +++ b/test/models/bot/smooch_7_test.rb @@ -16,13 +16,13 @@ def teardown test "should update cached field when request is created or deleted" do RequestStore.store[:skip_cached_field_update] = false - create_annotation_type_and_fields('Smooch', { 'Data' => ['JSON', true] }) Sidekiq::Testing.inline! do - pm = create_project_media + t = create_team + pm = create_project_media team: t assert_equal 0, pm.reload.requests_count - d = create_dynamic_annotation annotation_type: 'smooch', annotated: pm + tr = create_tipline_request team_id: t.id, associated: pm assert_equal 1, pm.reload.requests_count - d.destroy + tr.destroy assert_equal 0, pm.reload.requests_count end end @@ -114,7 +114,7 @@ def teardown Time.unstub(:now) end - test "should create smooch annotation for user requests" do + test "should create tipline requests for user requests" do setup_smooch_bot(true) Sidekiq::Testing.fake! do now = Time.now @@ -127,16 +127,14 @@ def teardown assert_equal 'secondary', sm.state.value send_message_to_smooch_bot('1', uid) conditions = { - annotation_type: 'smooch', - annotated_type: @pm_for_menu_option.class.name, - annotated_id: @pm_for_menu_option.id + associated_type: @pm_for_menu_option.class.name, + associated_id: @pm_for_menu_option.id } - assert_difference "Dynamic.where(#{conditions}).count", 1 do + assert_difference "TiplineRequest.where(#{conditions}).count", 1 do Sidekiq::Worker.drain_all end - a = Dynamic.where(conditions).last - f = a.get_field_value('smooch_data') - text = JSON.parse(f)['text'].split("\n#{Bot::Smooch::MESSAGE_BOUNDARY}") + tr = TiplineRequest.where(conditions).last + text = tr.smooch_data['text'].split("\n#{Bot::Smooch::MESSAGE_BOUNDARY}") # Verify that all messages were stored assert_equal 3, text.size assert_equal '1', text.last @@ -147,12 +145,11 @@ def teardown send_message_to_smooch_bot(random_string, uid) send_message_to_smooch_bot(random_string, uid) send_message_to_smooch_bot('1', uid) - assert_difference "Dynamic.where(#{conditions}).count", 1 do + assert_difference "TiplineRequest.where(#{conditions}).count", 1 do Sidekiq::Worker.drain_all end - a = Dynamic.where(conditions).last - f = a.get_field_value('smooch_data') - text = JSON.parse(f)['text'].split("\n#{Bot::Smooch::MESSAGE_BOUNDARY}") + tr = TiplineRequest.where(conditions).last + text = tr.smooch_data['text'].split("\n#{Bot::Smooch::MESSAGE_BOUNDARY}") # verify that all messages stored assert_equal 5, text.size assert_equal '1', text.last @@ -161,13 +158,13 @@ def teardown send_message_to_smooch_bot(random_string, uid) assert_equal 'main', sm.state.value Time.stubs(:now).returns(now + 30.minutes) - assert_difference 'Annotation.where(annotation_type: "smooch").count', 1 do + assert_difference 'TiplineRequest.count', 1 do Sidekiq::Worker.drain_all end send_message_to_smooch_bot(random_string, uid) send_message_to_smooch_bot(random_string, uid) Time.stubs(:now).returns(now + 30.minutes) - assert_difference 'Annotation.where(annotation_type: "smooch").count', 1 do + assert_difference 'TiplineRequest.count', 1 do Sidekiq::Worker.drain_all end end @@ -436,6 +433,7 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, text: text } ] @@ -486,7 +484,8 @@ def teardown originalMessageTimestamp: 1573082582, type: 'whatsapp', integrationId: random_string - } + }, + language: 'en', } end Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'relevant_search_result_requests', pm) @@ -509,6 +508,7 @@ def teardown type: 'whatsapp', integrationId: random_string }, + language: 'en', } end Bot::Smooch.save_message(message.call.to_json, @app_id, nil, 'relevant_search_result_requests', pm2) @@ -526,10 +526,8 @@ def teardown assert_equal 2, es2['tipline_search_results_count'] assert_equal 1, es2['positive_tipline_search_results_count'] # Verify destroy - DynamicAnnotation::Field.where(annotation_type: 'smooch',field_name: 'smooch_request_type') - .where('value IN (?)', ['"irrelevant_search_result_requests"', '"timeout_search_requests"']) - .joins('INNER JOIN annotations a ON a.id = dynamic_annotation_fields.annotation_id') - .where('a.annotated_type = ? AND a.annotated_id = ?', 'ProjectMedia', pm.id).destroy_all + 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 end @@ -541,6 +539,7 @@ def teardown '_id': random_string, authorId: random_string, type: 'text', + source: { type: "whatsapp" }, text: random_string } ] @@ -569,11 +568,10 @@ def teardown r.set_fields = { state: 'published' }.to_json r.action = 'republish_and_resend' r.save! - a = pm.annotations('smooch').last.load - smooch_data = a.get_field('smooch_data') - assert_not_nil smooch_data.smooch_report_sent_at - assert_not_nil smooch_data.smooch_report_correction_sent_at - assert_not_nil smooch_data.smooch_request_type + tr = TiplineRequest.last + assert_not_nil tr.smooch_report_sent_at + assert_not_nil tr.smooch_report_correction_sent_at + assert_not_nil tr.smooch_request_type end test "should include claim_description_content in smooch search" do @@ -591,4 +589,19 @@ def teardown results = Bot::Smooch.search_by_keywords_for_similar_published_fact_checks(query.split(), nil, [t.id]) assert_equal [pm.id], results.map(&:id) end + + test "should rescue when raise error on tipline request creation" do + TiplineRequest.any_instance.stubs(:save!).raises(ActiveRecord::RecordNotUnique) + t = create_team + pm = create_project_media team: t + u = create_user + create_team_user team: t, user: u, role: 'admin' + with_current_user_and_team(u, t) do + fields = { 'smooch_request_type' => 'default_requests', 'smooch_message_id' => random_string, 'smooch_data' => { authorId: random_string, language: 'en', source: { type: "whatsapp" }, } } + assert_no_difference 'TiplineRequest.count' do + Bot::Smooch.create_tipline_requests(pm, nil, fields) + end + end + TiplineRequest.any_instance.unstub(:save!) + end end diff --git a/test/models/bot/smooch_8_test.rb b/test/models/bot/smooch_8_test.rb deleted file mode 100644 index 9e2b985700..0000000000 --- a/test/models/bot/smooch_8_test.rb +++ /dev/null @@ -1,26 +0,0 @@ -require_relative '../../test_helper' -require 'sidekiq/testing' - -class Bot::Smooch8Test < ActiveSupport::TestCase - def setup - end - - def teardown - end - - test "should not store duplicated Smooch requests" do - create_annotation_type_and_fields('Smooch', { - 'Data' => ['JSON', false], - 'Message Id' => ['Text', false] - }) - - pm = create_project_media - fields = { 'smooch_message_id' => random_string, 'smooch_data' => '{}' } - assert_difference 'Annotation.count' do - Bot::Smooch.create_smooch_annotations(pm, nil, fields) - end - assert_no_difference 'Annotation.count' do - Bot::Smooch.create_smooch_annotations(pm, nil, fields) - end - end -end diff --git a/test/models/bot/smooch_rules_test.rb b/test/models/bot/smooch_rules_test.rb index 379fedd6b6..d10c6358b1 100644 --- a/test/models/bot/smooch_rules_test.rb +++ b/test/models/bot/smooch_rules_test.rb @@ -131,6 +131,8 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, + language: 'en', text: ([random_string] * 10).join(' ') } ] @@ -157,6 +159,8 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, + language: 'en', text: ([random_string] * 3).join(' ') + ' pLease?' } ] @@ -182,6 +186,8 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, + language: 'en', text: random_string } ] @@ -211,6 +217,8 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, + language: 'en', text: quote } ] @@ -235,6 +243,8 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, + language: 'en', text: random_number.to_s + ' ' + random_string } ] @@ -259,6 +269,8 @@ def teardown '_id': random_string, authorId: uid, type: 'text', + source: { type: "whatsapp" }, + language: 'en', text: [random_string, random_string, random_string, 'bad word', random_string, random_string].join(' ') } ] diff --git a/test/models/bot/smooch_test.rb b/test/models/bot/smooch_test.rb index c587c90d6d..0bc83003ec 100644 --- a/test/models/bot/smooch_test.rb +++ b/test/models/bot/smooch_test.rb @@ -61,12 +61,14 @@ def teardown authorId: id2, type: 'audio', text: random_string, + source: { type: "whatsapp" }, mediaUrl: @audio_url }, { '_id': random_string, authorId: id, type: 'text', + source: { type: "whatsapp" }, text: 'This is a test claim' }, { @@ -74,18 +76,21 @@ def teardown authorId: id, type: 'image', text: random_string, + source: { type: "whatsapp" }, mediaUrl: @media_url }, { '_id': random_string, authorId: id, type: 'text', + source: { type: "whatsapp" }, text: "#{random_string} #{@link_url} #{random_string}" }, { '_id': random_string, authorId: id, type: 'text', + source: { type: "whatsapp" }, text: 'This is a test claim' }, { @@ -93,18 +98,21 @@ def teardown authorId: id, type: 'image', text: random_string, + source: { type: "whatsapp" }, mediaUrl: @media_url }, { '_id': random_string, authorId: id, type: 'text', + source: { type: "whatsapp" }, text: "#{random_string} #{@link_url} #{random_string}" }, { '_id': random_string, authorId: id2, type: 'text', + source: { type: "whatsapp" }, text: 'This is a test claim' }, { @@ -112,6 +120,7 @@ def teardown authorId: id2, type: 'image', text: random_string, + source: { type: "whatsapp" }, mediaUrl: @media_url }, { @@ -120,6 +129,7 @@ def teardown type: 'file', text: random_string, mediaUrl: @media_url, + source: { type: "whatsapp" }, mediaType: 'image/jpeg' }, { @@ -128,18 +138,21 @@ def teardown type: 'file', text: random_string, mediaUrl: @media_url, + source: { type: "whatsapp" }, mediaType: 'application/pdf' }, { '_id': random_string, authorId: id2, type: 'text', + source: { type: "whatsapp" }, text: "#{random_string} #{@link_url} #{random_string}" }, { '_id': random_string, authorId: id2, type: 'text', + source: { type: "whatsapp" }, text: 'This is a test claim' }, { @@ -147,6 +160,7 @@ def teardown authorId: id2, type: 'image', text: random_string, + source: { type: "whatsapp" }, mediaUrl: @media_url }, { @@ -154,36 +168,42 @@ def teardown authorId: id2, type: 'video', text: random_string, + source: { type: "whatsapp" }, mediaUrl: @video_url }, { '_id': random_string, authorId: id2, type: 'text', + source: { type: "whatsapp" }, text: "#{random_string} #{@link_url} #{random_string}" }, { '_id': random_string, authorId: id2, type: 'text', + source: { type: "whatsapp" }, text: "#{random_string} #{@link_url_2} #montag #{random_string}" }, { '_id': random_string, authorId: id3, type: 'text', + source: { type: "whatsapp" }, text: "#{random_string} #{@link_url_2.gsub(/^https?:\/\//, '')} #teamtag #{random_string}" }, { '_id': random_string, authorId: id2, type: 'text', + source: { type: "whatsapp" }, text: 'This #teamtag is another #hashtag claim' }, { '_id': random_string, authorId: id3, type: 'text', + source: { type: "whatsapp" }, text: 'This #teamtag is another #hashtag CLAIM' }, { @@ -192,6 +212,7 @@ def teardown type: 'file', text: random_string, mediaUrl: @video_url, + source: { type: "whatsapp" }, mediaType: 'video/mp4' }, { @@ -200,6 +221,7 @@ def teardown type: 'file', text: random_string, mediaUrl: @audio_url, + source: { type: "whatsapp" }, mediaType: 'audio/mpeg' } ] @@ -208,7 +230,7 @@ def teardown tt_montag = create_tag_text text: 'montag', team_id: @team.id assert_difference 'ProjectMedia.count', 7 do - assert_difference 'Annotation.where(annotation_type: "smooch").count', 22 do + assert_difference 'TiplineRequest.count', 22 do assert_no_difference 'Comment.length' do messages.each do |message| uid = message[:authorId] @@ -282,7 +304,8 @@ def teardown '_id': random_string, authorId: uid, type: 'text', - text: random_string + text: random_string, + source: { type: "whatsapp" }, } ] payload = { @@ -490,7 +513,8 @@ def teardown '_id': random_string, authorId: uid, type: 'text', - text: random_string + text: random_string, + source: { type: "whatsapp" }, } ] payload = { @@ -535,7 +559,8 @@ def teardown '_id': random_string, authorId: uid, type: 'text', - text: random_string + text: random_string, + source: { type: "whatsapp" }, } ] payload = { @@ -554,11 +579,9 @@ def teardown assert Bot::Smooch.run(payload) pm = ProjectMedia.last - sm = pm.get_annotations('smooch').last - df = DynamicAnnotation::Field.where(annotation_id: sm.id, field_name: 'smooch_data').last - assert_not_nil df - assert_equal 0, df.reload.smooch_report_received_at - assert_nil df.reload.smooch_report_update_received_at + tr = pm.tipline_requests.last + assert_equal 0, tr.reload.smooch_report_received_at + assert_equal 0, tr.reload.smooch_report_update_received_at r = publish_report(pm) assert_equal 0, r.reload.sent_count msg_id = random_string @@ -566,7 +589,7 @@ def teardown fallback_template: 'fact_check_report', project_media_id: pm.id }.to_json) - assert_nil DynamicAnnotation::Field.where(field_name: 'smooch_report_received').last + assert_equal 0, tr.reload.smooch_report_received_at payload = { trigger: 'message:delivery:channel', @@ -585,26 +608,18 @@ def teardown } assert Bot::Smooch.run(payload.to_json) - f1 = DynamicAnnotation::Field.where(field_name: 'smooch_report_received').last - assert_not_nil f1 - t1 = f1.value - assert_equal t1, df.reload.smooch_report_received_at - assert_nil df.reload.smooch_report_update_received_at + assert tr.reload.smooch_report_received_at > 0 + assert_equal 0, tr.reload.smooch_report_update_received_at assert_equal 1, r.reload.sent_count - sleep 1 # Process TiplineMessage creation in background to avoid duplication exception Sidekiq::Testing.fake! do assert Bot::Smooch.run(payload.to_json) - f2 = DynamicAnnotation::Field.where(field_name: 'smooch_report_received').last - assert_equal f1, f2 - t2 = f2.value - assert_equal t2, df.reload.smooch_report_received_at - assert_equal t2, df.reload.smooch_report_update_received_at + tr2 = pm.tipline_requests.last + assert_equal tr, tr2 + assert tr2.smooch_report_update_received_at > 0 assert_equal 1, r.reload.sent_count - - assert t2 > t1 end end @@ -720,7 +735,8 @@ def teardown '_id': random_string, authorId: uid, type: 'text', - text: random_string + text: random_string, + source: { type: "whatsapp" }, } ] payload = { @@ -751,7 +767,8 @@ def teardown '_id': random_string, authorId: uid, type: 'text', - text: random_string + text: random_string, + source: { type: "whatsapp" }, } ] payload = { @@ -768,8 +785,7 @@ def teardown }.to_json assert Bot::Smooch.run(payload) pm = ProjectMedia.last - sm = pm.get_annotations('smooch').last.load - f = sm.get_field('smooch_data') - assert_equal 'en', f.smooch_user_request_language + tr = pm.tipline_requests.last + assert_equal 'en', tr.smooch_user_request_language end end diff --git a/test/models/bot_user_2_test.rb b/test/models/bot_user_2_test.rb index 4198df57e1..2311497080 100644 --- a/test/models/bot_user_2_test.rb +++ b/test/models/bot_user_2_test.rb @@ -127,4 +127,12 @@ class BotUser2Test < ActiveSupport::TestCase b.call({}) end end + + test "should capture error if bot can't be called" do + Bot::Alegre.stubs(:run).raises(StandardError) + b = create_bot_user login: 'alegre' + assert_nothing_raised do + b.call({}) + end + end end diff --git a/test/models/cluster_project_media_test.rb b/test/models/cluster_project_media_test.rb new file mode 100644 index 0000000000..dd71524ee2 --- /dev/null +++ b/test/models/cluster_project_media_test.rb @@ -0,0 +1,25 @@ +require_relative '../test_helper' + +class ClusterProjectMediaTest < ActiveSupport::TestCase + def setup + super + ClusterProjectMedia.delete_all + end + + test "should create cluster project media" do + assert_difference 'ClusterProjectMedia.count' do + create_cluster_project_media + end + end + + test "should validate cluster and project media exists" do + c = create_cluster + pm = create_project_media + assert_raises ActiveRecord::RecordInvalid do + ClusterProjectMedia.create!(cluster: nil, project_media: pm) + end + assert_raises ActiveRecord::RecordInvalid do + ClusterProjectMedia.create!(cluster: c, project_media: nil) + end + end +end diff --git a/test/models/cluster_test.rb b/test/models/cluster_test.rb index ea6db9e2be..2486a18726 100644 --- a/test/models/cluster_test.rb +++ b/test/models/cluster_test.rb @@ -25,118 +25,6 @@ def setup assert_equal [pm1, pm2].sort, c.reload.items.sort end - test "should not create cluster if center is not present" do - assert_no_difference 'Cluster.count' do - assert_raises ActiveRecord::RecordInvalid do - create_cluster project_media: nil - end - end - end - - test "should not have two clusters with same center (Rails validation)" do - pm = create_project_media - create_cluster project_media: pm - assert_no_difference 'Cluster.count' do - assert_raises ActiveRecord::RecordInvalid do - create_cluster project_media: pm - end - end - end - - test "should not have two clusters with same center (database validation)" do - pm = create_project_media - create_cluster project_media: pm - c = create_cluster - assert_raises ActiveRecord::RecordNotUnique do - c.update_column :project_media_id, pm.id - end - end - - test "should remove items from cluster when cluster is deleted" do - c = create_cluster - pm1 = create_project_media cluster: c - pm2 = create_project_media cluster: c - assert_equal c.id, pm1.reload.cluster_id - assert_equal c.id, pm2.reload.cluster_id - c.destroy! - assert_nil pm1.reload.cluster_id - assert_nil pm2.reload.cluster_id - end - - test "should cache number of items in the cluster" do - c = create_cluster - assert_equal 0, c.reload.size - pm1 = create_project_media cluster: c - assert_equal 1, c.reload.size - pm2 = create_project_media cluster: c - assert_equal 2, c.reload.size - pm1.destroy! - assert_equal 1, c.reload.size - pm2.destroy! - assert_equal 0, c.reload.size - end - - test "should not have the center belonging to another cluster" do - pm = create_project_media - c = create_cluster - c.project_medias << pm - assert_no_difference 'Cluster.count' do - assert_raises ActiveRecord::RecordInvalid do - create_cluster project_media: pm - end - end - end - - test "should set cluster" do - t = create_team - pm1 = create_project_media team: t - c = create_cluster - c.project_medias << pm1 - pm2 = create_project_media team: t - ProjectMedia.any_instance.stubs(:similar_items_ids_and_scores).returns({ pm1.id => { score: 0.9, context: {} }, random_number => { score: 0.8, context: { foo: 'bar' } } }) - assert_equal c, Bot::Alegre.set_cluster(pm2) - ProjectMedia.any_instance.unstub(:similar_items_ids_and_scores) - end - - test "should get requests count" do - create_annotation_type_and_fields('Smooch', { 'Data' => ['JSON', true] }) - RequestStore.store[:skip_cached_field_update] = false - t = create_team - Sidekiq::Testing.inline! do - c = create_cluster - pm = create_project_media team: t - 2.times { create_dynamic_annotation(annotation_type: 'smooch', annotated: pm) } - pm2 = create_project_media team: t - 2.times { create_dynamic_annotation(annotation_type: 'smooch', annotated: pm2) } - c.project_medias << pm - c.project_medias << pm2 - assert_equal 4, c.requests_count - assert_equal 4, c.requests_count(true) - d = create_dynamic_annotation annotation_type: 'smooch', annotated: pm - assert_equal 5, c.requests_count - assert_equal 5, c.requests_count(true) - d.destroy! - assert_equal 4, c.requests_count - assert_equal 4, c.requests_count(true) - end - end - - test "should get teams that fact-checked the item" do - c = create_cluster - assert_kind_of Hash, c.get_names_of_teams_that_fact_checked_it - end - - test "should get claim descriptions" do - c = create_cluster - pm1 = create_project_media - cd1 = create_claim_description project_media: pm1 - c.project_medias << pm1 - pm2 = create_project_media - cd2 = create_claim_description project_media: pm2 - c.project_medias << pm2 - assert_equal [cd1, cd2], c.claim_descriptions.sort - end - test "should access cluster" do u = create_user t1 = create_team @@ -147,7 +35,6 @@ def setup # A cluster whose center is from the same team pm1 = create_project_media team: t1 c1 = create_cluster project_media: pm1 - c1.project_medias << pm1 # A cluster from another feed t2 = create_team @@ -155,27 +42,35 @@ def setup f2.teams << t2 pm2 = create_project_media team: t2 c2 = create_cluster project_media: pm2 - c2.project_medias << pm2 # A cluster from the same feed without any item from the team t3 = create_team f1.teams << t3 pm3 = create_project_media team: t3 c3 = create_cluster project_media: pm3 - c3.project_medias << pm3 # A cluster from the same feed with an item from the team t4 = create_team f1.teams << t4 pm4 = create_project_media team: t4 c4 = create_cluster project_media: pm4 - c4.project_medias << pm4 c4.project_medias << create_project_media(team: t1) a = Ability.new(u, t1) + assert a.can?(:read, c1.feed) assert a.can?(:read, c1) + assert !a.can?(:read, c2.feed) assert !a.can?(:read, c2) - assert a.can?(:read, c3) - assert a.can?(:read, c4) + assert !a.can?(:read, c3.feed) + assert !a.can?(:read, c3) + assert !a.can?(:read, c4.feed) + assert !a.can?(:read, c4) + end + + test "should return size" do + c = create_cluster + assert_equal 0, c.size + c.project_medias << create_project_media + assert_equal 1, c.size end end diff --git a/test/models/dynamic_annotation/field_test.rb b/test/models/dynamic_annotation/field_test.rb index ce938c6d3e..c87915efa6 100644 --- a/test/models/dynamic_annotation/field_test.rb +++ b/test/models/dynamic_annotation/field_test.rb @@ -232,7 +232,6 @@ def field_formatter_name_response end test "should get smooch user slack channel url" do - create_annotation_type_and_fields('Smooch', { 'Data' => ['JSON', false] }) create_annotation_type_and_fields('Smooch User', { 'Data' => ['JSON', false], 'Slack Channel Url' => ['Text', true], @@ -247,11 +246,10 @@ def field_formatter_name_response set_fields = { smooch_user_id: author_id, smooch_user_data: { id: author_id }.to_json, smooch_user_slack_channel_url: url }.to_json d = create_dynamic_annotation annotated: t, annotation_type: 'smooch_user', set_fields: set_fields with_current_user_and_team(u, t) do - ds = create_dynamic_annotation annotation_type: 'smooch', annotated: pm, set_fields: { smooch_data: { 'authorId' => author_id }.to_json }.to_json - f = ds.get_field('smooch_data') - assert_equal url, f.smooch_user_slack_channel_url + tr = create_tipline_request team_id: t.id, associated: pm, tipline_user_uid: author_id, smooch_data: { 'authorId' => author_id } + assert_equal url, tr.smooch_user_slack_channel_url assert 1, Rails.cache.delete_matched("SmoochUserSlackChannelUrl:Team:*") - assert_equal url, f.smooch_user_slack_channel_url + assert_equal url, tr.smooch_user_slack_channel_url end end diff --git a/test/models/feed_test.rb b/test/models/feed_test.rb index 1760b35804..7be837b0bf 100755 --- a/test/models/feed_test.rb +++ b/test/models/feed_test.rb @@ -203,4 +203,9 @@ def setup create_feed data_points: [0, 1] end end + + test "should not apply filters when medias are shared" do + f = create_feed data_points: [2], published: true + assert_equal({}, f.get_feed_filters(:media)) + end end diff --git a/test/models/media_test.rb b/test/models/media_test.rb index 19be8cc974..b3b415511b 100644 --- a/test/models/media_test.rb +++ b/test/models/media_test.rb @@ -603,4 +603,13 @@ def setup create_media url: url end end + + test "should have uuid" do + m = create_media + assert_equal m.id, m.uuid + c1 = create_claim_media quote: 'Foo' + assert_equal c1.id, c1.uuid + c2 = create_claim_media quote: 'Foo' + assert_equal c1.id, c2.uuid + end end diff --git a/test/models/project_media_2_test.rb b/test/models/project_media_2_test.rb index 9359059f30..86b13b0b22 100644 --- a/test/models/project_media_2_test.rb +++ b/test/models/project_media_2_test.rb @@ -148,7 +148,6 @@ def setup RequestStore.store[:skip_cached_field_update] = false # sortable fields are [linked_items_count, last_seen and share_count] setup_elasticsearch - create_annotation_type_and_fields('Smooch', { 'Data' => ['JSON', false] }) Rails.stubs(:env).returns('development'.inquiry) team = create_team p = create_project team: team @@ -157,7 +156,7 @@ def setup assert_equal 1, result['linked_items_count'] assert_equal pm.created_at.to_i, result['last_seen'] assert_equal pm.reload.last_seen, pm.read_attribute(:last_seen) - t = t0 = create_dynamic_annotation(annotation_type: 'smooch', annotated: pm).created_at.to_i + t = t0 = create_tipline_request(team_id: team.id, associated: pm).created_at.to_i result = $repository.find(get_es_id(pm)) assert_equal t, result['last_seen'] assert_equal pm.reload.last_seen, pm.read_attribute(:last_seen) @@ -172,7 +171,7 @@ def setup assert_equal t, result['last_seen'] assert_equal pm.reload.last_seen, pm.read_attribute(:last_seen) - t = create_dynamic_annotation(annotation_type: 'smooch', annotated: pm2).created_at.to_i + t = create_tipline_request(team_id: team.id, associated: pm2).created_at.to_i result = $repository.find(get_es_id(pm)) assert_equal t, result['last_seen'] assert_equal pm.reload.last_seen, pm.read_attribute(:last_seen) @@ -205,8 +204,8 @@ def setup t0 = pm.created_at.to_i # pm.last_seen should equal pm.created_at if no tipline request (aka 'smooch' annotation) assert_queries(0, '=') { assert_equal(t0, pm.last_seen) } - t1 = create_dynamic_annotation(annotation_type: 'smooch', annotated: pm).created_at.to_i - # pm.last_seen should equal pm smooch annotation created_at if item is not related + t1 = create_tipline_request(team_id: team.id, associated: pm).created_at.to_i + # pm.last_seen should equal pm tipline request created_at if item is not related assert_queries(0, '=') { assert_equal(t1, pm.last_seen) } pm2 = create_project_media team: team t2 = pm2.created_at.to_i @@ -216,11 +215,11 @@ def setup # pm is now a parent and pm2 its child with no smooch annotation, so pm.last_seen should match pm2.created_at assert_queries(0, '=') { assert_equal(t2, pm.last_seen) } # adding a smooch annotation to pm2 should update parent last_seen - t3 = create_dynamic_annotation(annotation_type: 'smooch', annotated: pm2).created_at.to_i + t3 = create_tipline_request(team_id: team.id, associated: pm2).created_at.to_i assert_queries(0, '=') { assert_equal(t3, pm.last_seen) } # now let's add a second child pm3... pm3 = create_project_media team: team - t4 = create_dynamic_annotation(annotation_type: 'smooch', annotated: pm3).created_at.to_i + t4 = create_tipline_request(team_id: team.id, associated: pm3).created_at.to_i r2 = create_relationship source_id: pm.id, target_id: pm3.id, relationship_type: Relationship.confirmed_type # pm3.last_seen should equal pm3 smooch annotation created_at assert_queries(0, '=') { assert_equal(t4, pm3.last_seen) } @@ -239,4 +238,4 @@ def setup end end -end \ No newline at end of file +end diff --git a/test/models/project_media_6_test.rb b/test/models/project_media_6_test.rb index f560746e0c..d4380406c5 100644 --- a/test/models/project_media_6_test.rb +++ b/test/models/project_media_6_test.rb @@ -142,40 +142,6 @@ def setup assert_equal p.id, result['project_id'] end - test "should get cluster size" do - pm = create_project_media - assert_nil pm.reload.cluster - c = create_cluster - c.project_medias << pm - assert_equal 1, pm.reload.cluster.size - c.project_medias << create_project_media - assert_equal 2, pm.reload.cluster.size - end - - test "should get cluster teams" do - RequestStore.store[:skip_cached_field_update] = false - setup_elasticsearch - t1 = create_team - t2 = create_team - pm1 = create_project_media team: t1 - assert_nil pm1.cluster - c = create_cluster project_media: pm1 - c.project_medias << pm1 - assert_equal [t1.name], pm1.cluster.team_names.values - assert_equal [t1.id], pm1.cluster.team_names.keys - sleep 2 - id = get_es_id(pm1) - es = $repository.find(id) - assert_equal [t1.id], es['cluster_teams'] - pm2 = create_project_media team: t2 - c.project_medias << pm2 - sleep 2 - assert_equal [t1.name, t2.name].sort, pm1.cluster.team_names.values.sort - assert_equal [t1.id, t2.id].sort, pm1.cluster.team_names.keys.sort - es = $repository.find(id) - assert_equal [t1.id, t2.id], es['cluster_teams'] - end - test "should complete media if there are pending tasks" do pm = create_project_media s = pm.last_verification_status_obj @@ -343,16 +309,15 @@ def setup RequestStore.store[:skip_cached_field_update] = false team = create_team p = create_project team: team - create_annotation_type_and_fields('Smooch', { 'Data' => ['JSON', false] }) pm = create_project_media team: team, project_id: p.id, disable_es_callbacks: false ms_pm = get_es_id(pm) assert_queries(0, '=') { assert_equal(0, pm.demand) } - create_dynamic_annotation annotation_type: 'smooch', annotated: pm + create_tipline_request team: team.id, associated: pm assert_queries(0, '=') { assert_equal(1, pm.demand) } pm2 = create_project_media team: team, project_id: p.id, disable_es_callbacks: false ms_pm2 = get_es_id(pm2) assert_queries(0, '=') { assert_equal(0, pm2.demand) } - 2.times { create_dynamic_annotation(annotation_type: 'smooch', annotated: pm2) } + 2.times { create_tipline_request(team_id: team.id, associated: pm2) } assert_queries(0, '=') { assert_equal(2, pm2.demand) } # test sorting result = $repository.find(ms_pm) @@ -371,13 +336,13 @@ def setup pm3 = create_project_media team: team, project_id: p.id ms_pm3 = get_es_id(pm3) assert_queries(0, '=') { assert_equal(0, pm3.demand) } - 2.times { create_dynamic_annotation(annotation_type: 'smooch', annotated: pm3) } + 2.times { create_tipline_request(team_id: team.id, associated: pm3) } assert_queries(0, '=') { assert_equal(2, pm3.demand) } create_relationship source_id: pm.id, target_id: pm3.id, relationship_type: Relationship.confirmed_type assert_queries(0, '=') { assert_equal(5, pm.demand) } assert_queries(0, '=') { assert_equal(5, pm2.demand) } assert_queries(0, '=') { assert_equal(5, pm3.demand) } - create_dynamic_annotation annotation_type: 'smooch', annotated: pm3 + create_tipline_request team_id: team.id, associated: pm3 assert_queries(0, '=') { assert_equal(6, pm.demand) } assert_queries(0, '=') { assert_equal(6, pm2.demand) } assert_queries(0, '=') { assert_equal(6, pm3.demand) } diff --git a/test/models/relationship_test.rb b/test/models/relationship_test.rb index 61acaa14a7..8eca19286e 100644 --- a/test/models/relationship_test.rb +++ b/test/models/relationship_test.rb @@ -61,23 +61,6 @@ def setup assert_equal pm_t.id, es_t['parent_id'] end - test "should set cluster" do - t = create_team - u = create_user - create_team_user team: t, user: u, role: 'admin' - s = create_project_media team: t - t = create_project_media team: t - s_c = create_cluster project_media: s - s_c.project_medias << s - t_c = create_cluster project_media: t - t_c.project_medias << t - User.stubs(:current).returns(u) - create_relationship source_id: s.id, target_id: t.id, relationship_type: Relationship.confirmed_type - assert_nil Cluster.where(id: t_c.id).last - assert_equal [s.id, t.id].sort, s_c.reload.project_media_ids.sort - User.unstub(:current) - end - test "should remove suggested relation when same items added as similar" do team = create_team b = create_bot name: 'Alegre', login: 'alegre' diff --git a/test/models/request_test.rb b/test/models/request_test.rb index 89d0c27871..806eece43d 100644 --- a/test/models/request_test.rb +++ b/test/models/request_test.rb @@ -249,7 +249,7 @@ def setup Bot::Alegre.stubs(:request).returns({}) RequestStore.store[:skip_cached_field_update] = false u = create_user is_admin: true - f = create_feed + f = create_feed data_points: [1, 2], published: true t1 = create_team t2 = create_team name: 'Foo' t3 = create_team name: 'Bar' diff --git a/test/models/tipline_request_test.rb b/test/models/tipline_request_test.rb new file mode 100644 index 0000000000..3bbfdd22c8 --- /dev/null +++ b/test/models/tipline_request_test.rb @@ -0,0 +1,56 @@ +require_relative '../test_helper' + +class TiplineRequestTest < ActiveSupport::TestCase + test "should create tipline request" do + assert_difference 'TiplineRequest.count' do + create_tipline_request + end + # validate smooch_request_type, language and platform + assert_raises ActiveRecord::RecordInvalid do + create_tipline_request smooch_request_type: nil, smooch_data: { language: 'en', authorId: random_string, source: { type: 'whatsapp' } } + end + assert_raises ActiveRecord::RecordInvalid do + create_tipline_request language: nil, smooch_data: { authorId: random_string, source: { type: 'whatsapp' } } + end + assert_raises ActiveRecord::RecordInvalid do + create_tipline_request platform: nil, smooch_data: { language: 'en', authorId: random_string } + end + # validate smooch_request_type and platform values + assert_no_difference 'TiplineRequest.count' do + assert_raises ActiveRecord::RecordInvalid do + create_tipline_request smooch_request_type: 'invalid_type' + end + end + assert_no_difference 'TiplineRequest.count' do + assert_raises ActiveRecord::RecordInvalid do + create_tipline_request platform: random_string, smooch_data: { language: 'en', authorId: random_string } + end + end + end + + test "should set user and team" do + t = create_team + u = create_user + create_team_user team: t, user: u, role: 'admin' + with_current_user_and_team(u, t) do + tr = create_tipline_request team_id: nil + assert_equal t.id, tr.team_id + assert_equal u.id, tr.user_id + end + end + + test "should set smooch data fields" do + author_id = random_string + platform = 'whatsapp' + smooch_data = { language: 'en', authorId: author_id, source: { type: platform } } + tr = create_tipline_request smooch_data: smooch_data + assert_equal 'en', tr.language + assert_equal author_id, tr.tipline_user_uid + assert_equal platform, tr.platform + end + + test "should get associated GraphQL ID" do + tr = create_tipline_request + assert_kind_of String, tr.associated_graphql_id + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 695d08ad75..26e040e76e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -159,7 +159,7 @@ def before_all # This will run before any test def setup - [Account, Media, ProjectMedia, User, Source, Annotation, Team, TeamUser, Relationship, Project, TiplineResource].each{ |klass| klass.delete_all } + [Account, Media, ProjectMedia, User, Source, Annotation, Team, TeamUser, Relationship, Project, TiplineResource, TiplineRequest].each{ |klass| klass.delete_all } # Some of our non-GraphQL tests rely on behavior that this requires. As a result, # we'll keep it around for now and just recreate any needed dynamic annotation data @@ -846,14 +846,6 @@ def setup_smooch_bot(menu = false, extra_settings = {}) DynamicAnnotation::FieldType.delete_all DynamicAnnotation::Field.delete_all create_verification_status_stuff - create_annotation_type_and_fields('Smooch', { - 'Data' => ['JSON', false], - 'Report Received' => ['Timestamp', true], - 'Request Type' => ['Text', true], - 'Resource Id' => ['Text', true], - 'Report correction sent at' => ['Timestamp', true], - 'Report sent at' => ['Timestamp', true] - }) create_annotation_type_and_fields('Smooch Response', { 'Data' => ['JSON', true] }) create_annotation_type annotation_type: 'reverse_image', label: 'Reverse Image' WebMock.disable_net_connect! allow: /#{CheckConfig.get('elasticsearch_host')}|#{CheckConfig.get('storage_endpoint')}/ @@ -1051,7 +1043,9 @@ def send_message_to_smooch_bot(message = random_string, user = random_string, ex '_id': random_string, authorId: user, type: 'text', - text: message + text: message, + source: { type: "whatsapp" }, + language: 'en', }.merge(extra) ] payload = {