From cf06b577babb692991e50ee138106eaccfb04038 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Thu, 15 Aug 2024 00:19:17 -0400 Subject: [PATCH 01/27] docker local postgres --- docker-compose.dev.yml | 8 +++---- docker-compose.yml | 48 +++++++++++++++++++++--------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index d9bb4a030e..2c2d290a20 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -32,10 +32,10 @@ services: environment: CHOKIDAR_USEPOLLING: "true" - postgres: - restart: no - ports: - - "${POSTGRES_PORT:-5432}:5432" + # postgres: + # restart: no + # ports: + # - "${POSTGRES_PORT:-5432}:5432" file-server: build: diff --git a/docker-compose.yml b/docker-compose.yml index 4382451795..4d67511280 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,20 +34,20 @@ services: labels: polis_tag: ${TAG:-dev} depends_on: - - "postgres" + # - "postgres" - "file-server" networks: - "polis-net" # Scale the server container to a given number of instances. scale: 1 volumes: - # Persist logs to a volume, so they can be accessed after the container is stopped. + # Persist logs to a volume, so they can be accessed after the container is stopped. - server-logs:/app/logs math: image: docker.io/compdem/polis-math:${TAG:-dev} - depends_on: - - "postgres" + # depends_on: + # - "postgres" build: context: ./math labels: @@ -60,23 +60,23 @@ services: networks: - "polis-net" - postgres: - image: docker.io/compdem/polis-postgres:${TAG:-dev} - restart: always - build: - context: ./server - dockerfile: Dockerfile-db - labels: - polis_tag: ${TAG:-dev} - environment: - - POSTGRES_DB=${POSTGRES_DB:?error} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?error} - - POSTGRES_USER=${POSTGRES_USER:?error} - networks: - - "polis-net" - volumes: - - "backups:/backups" - - "postgres:/var/lib/postgresql/data" + # postgres: + # image: docker.io/compdem/polis-postgres:${TAG:-dev} + # restart: always + # build: + # context: ./server + # dockerfile: Dockerfile-db + # labels: + # polis_tag: ${TAG:-dev} + # environment: + # - POSTGRES_DB=${POSTGRES_DB:?error} + # - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?error} + # - POSTGRES_USER=${POSTGRES_USER:?error} + # networks: + # - "polis-net" + # volumes: + # - "backups:/backups" + # - "postgres:/var/lib/postgresql/data" nginx-proxy: image: docker.io/compdem/polis-nginx-proxy:${TAG:-dev} @@ -120,9 +120,9 @@ volumes: backups: labels: polis_tag: ${TAG:-dev} - postgres: - labels: - polis_tag: ${TAG:-dev} + # postgres: + # labels: + # polis_tag: ${TAG:-dev} server-logs: labels: polis_tag: ${TAG:-dev} From 02375c6c4962f591df10d65a97df62f5ee9f0550 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Thu, 15 Aug 2024 14:45:31 -0400 Subject: [PATCH 02/27] fix Constants capitalization bug in participationview --- client-participation/js/views/comment-form.js | 250 ++++++++++-------- 1 file changed, 138 insertions(+), 112 deletions(-) diff --git a/client-participation/js/views/comment-form.js b/client-participation/js/views/comment-form.js index 39147a91c8..4aa052f51d 100644 --- a/client-participation/js/views/comment-form.js +++ b/client-participation/js/views/comment-form.js @@ -1,21 +1,21 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . var autosize = require("autosize"); -var constants = require("../util/constants"); +var Constants = require("../util/constants"); var CurrentUserModel = require("../stores/currentUser"); var template = require("../templates/comment-form.handlebars"); var display = require("../util/display"); var eb = require("../eventBus"); var Handlebones = require("handlebones"); var M = require("../util/metrics"); -var PolisFacebookUtils = require('../util/facebookButton'); -var ProfilePicView = require('../views/profilePicView'); +var PolisFacebookUtils = require("../util/facebookButton"); +var ProfilePicView = require("../views/profilePicView"); var serialize = require("../util/serialize"); var Strings = require("../strings"); var Utils = require("../util/utils"); var $ = require("jquery"); -var CHARACTER_LIMIT = constants.CHARACTER_LIMIT; +var CHARACTER_LIMIT = Constants.CHARACTER_LIMIT; // var CommentsByMeView = Handlebones.CollectionView.extend({ // modelView: CommentView @@ -32,15 +32,16 @@ module.exports = Handlebones.ModelView.extend({ name: "comment-form", template: template, - // needed to prevent double submissions, which are annoying because they trigger a duplicate alert buttonActive: true, - context: function() { + context: function () { var ctx = Handlebones.ModelView.prototype.context.apply(this, arguments); - ctx = _.extend(ctx, this, this.model&&this.model.attributes); + ctx = _.extend(ctx, this, this.model && this.model.attributes); ctx.is_active = this.parent.model.get("is_active"); - ctx.shouldAutofocusOnTextarea = this.shouldAutofocusOnTextarea || Utils.shouldFocusOnTextareaWhenWritePaneShown(); + ctx.shouldAutofocusOnTextarea = + this.shouldAutofocusOnTextarea || + Utils.shouldFocusOnTextareaWhenWritePaneShown(); ctx.hasTwitter = userObject.hasTwitter; ctx.hasFacebook = userObject.hasFacebook && Constants.FB_APP_ID; ctx.auth_opt_tw = preload.firstConv.auth_opt_tw; @@ -48,7 +49,7 @@ module.exports = Handlebones.ModelView.extend({ ctx.s = Strings; ctx.desktop = !display.xs(); ctx.hideHelp = !Utils.userCanSeeHelp() || preload.firstConv.help_type === 0; - ctx.direction = Strings.direction ? Strings.direction : 'ltr' + ctx.direction = Strings.direction ? Strings.direction : "ltr"; ctx.no_write_hint = false; //preload.firstConv.write_hint_type === 0; @@ -56,38 +57,41 @@ module.exports = Handlebones.ModelView.extend({ if (btnBg) { ctx.customBtnStyles = "background-color: " + btnBg + ";"; } - ctx.charLimitString = Strings.tipCharLimit.replace("{{char_limit}}", constants.CHARACTER_LIMIT); + ctx.charLimitString = Strings.tipCharLimit.replace( + "{{char_limit}}", + Constants.CHARACTER_LIMIT + ); ctx.is_anon = window.preload.firstConv.is_anon; return ctx; }, - hideMessage: function(id) { + hideMessage: function (id) { this.$(id).hide(); }, - showMessage: function(id) { + showMessage: function (id) { // since there are now other warnings/tips showing, hide the default one this.$(".low_priority_tip").hide(); this.$(id).show(); }, - updateOneIdeaPerCommentMessage: function(formText) { + updateOneIdeaPerCommentMessage: function (formText) { // TODO I18N // Tests to see if there is non-punctuation that follows the end of a sentence. - if ((formText||"").match(/[\?\.\!].*[a-zA-Z0-9]+/)) { + if ((formText || "").match(/[\?\.\!].*[a-zA-Z0-9]+/)) { this.showMessage("#one_idea_per_comment_message"); } else { this.hideMessage("#one_idea_per_comment_message"); this.maybeShowBasicTip(); } }, - updateCharLimitExceededMessage: function(formText) { - if (formText.length > constants.CHARACTER_LIMIT) { + updateCharLimitExceededMessage: function (formText) { + if (formText.length > Constants.CHARACTER_LIMIT) { this.showMessage("#char_limit_exceeded_message"); } else { this.hideMessage("#char_limit_exceeded_message"); this.maybeShowBasicTip(); } }, - updateCommentNotQuestionAlert: function(formText) { + updateCommentNotQuestionAlert: function (formText) { if (formText.indexOf("?") >= 0) { this.showMessage("#commentNotQuestionAlert"); } else { @@ -95,14 +99,14 @@ module.exports = Handlebones.ModelView.extend({ this.maybeShowBasicTip(); } }, - maybeShowBasicTip: function() { + maybeShowBasicTip: function () { // if there are no other warnings/tips showing, show the default one if (this.$(".protip:visible").length === 0) { this.chooseBasicTip(); } }, - chooseBasicTip: function() { - var form = $("#comment_form_textarea"); + chooseBasicTip: function () { + var form = $("#comment_form_textarea"); var formText = form.val(); if (formText.length) { this.showMessage("#not_a_reply_message"); @@ -112,15 +116,14 @@ module.exports = Handlebones.ModelView.extend({ this.hideMessage("#not_a_reply_message"); } }, - textChange: function() { - + textChange: function () { this.hideMessage("#comment_sent_message"); this.hideMessage("#comment_send_failed_message"); this.hideMessage("#comment_send_failed_empty_message"); this.hideMessage("#comment_send_failed_too_long_message"); this.hideMessage("#comment_send_failed_duplicate_message"); this.hideMessage("#comment_send_failed_conversation_closed_message"); - var form = $(arguments[0].target); + var form = $(arguments[0].target); var formText = form.val(); var len = formText.length; var remaining = CHARACTER_LIMIT - len; @@ -136,7 +139,9 @@ module.exports = Handlebones.ModelView.extend({ // this.buttonActive = false; this.$("#commentCharCount").text(""); this.$("#commentCharCount").hide(); - this.$("#commentCharCountExceeded").text(txt.replace("{{CHARACTERS_COUNT}}", num)); + this.$("#commentCharCountExceeded").text( + txt.replace("{{CHARACTERS_COUNT}}", num) + ); this.$("#commentCharCountExceeded").show(); } else { num = remaining; @@ -148,7 +153,9 @@ module.exports = Handlebones.ModelView.extend({ // if (remaining > 0) { // } else { // } - this.$("#commentCharCount").text(txt.replace("{{CHARACTERS_COUNT}}", num)); + this.$("#commentCharCount").text( + txt.replace("{{CHARACTERS_COUNT}}", num) + ); this.$("#commentCharCount").show(); this.$("#commentCharCountExceeded").text(""); this.$("#commentCharCountExceeded").hide(); @@ -160,15 +167,15 @@ module.exports = Handlebones.ModelView.extend({ this.updateCommentNotQuestionAlert(formText); eb.trigger(eb.interacted); }, - showFormControls: function() { + showFormControls: function () { // this.$(".alert").hide(); this.$(".comment_form_control_hideable").show(); }, - hideFormControls: function() { + hideFormControls: function () { // this.$(".comment_form_control_hideable").hide(); // this.$("#commentCharCount").text(""); }, - reloadPagePreservingCommentText: function() { + reloadPagePreservingCommentText: function () { var wipCommentFormText = $("#comment_form_textarea").val(); var params = {}; if (wipCommentFormText.length) { @@ -177,10 +184,11 @@ module.exports = Handlebones.ModelView.extend({ eb.trigger(eb.reloadWithMoreParams, params); }, events: { - "focus #comment_form_textarea": function(e) { // maybe on keyup ? + "focus #comment_form_textarea": function (e) { + // maybe on keyup ? this.showFormControls(); }, - "blur #comment_form_textarea": function(e) { + "blur #comment_form_textarea": function (e) { var txt = this.$("#comment_form_textarea").val(); if (!txt || !txt.length) { this.hideFormControls(); @@ -189,15 +197,15 @@ module.exports = Handlebones.ModelView.extend({ "change #comment_form_textarea": "textChange", "keyup #comment_form_textarea": "textChange", "paste #comment_form_textarea": "textChange", - "click #facebookButtonCommentForm" : "facebookClicked", - "click #twitterButtonCommentForm" : "twitterClicked", + "click #facebookButtonCommentForm": "facebookClicked", + "click #twitterButtonCommentForm": "twitterClicked", "click #comment_button": "onSubmitClicked", }, - onSubmitClicked: function(e) { + onSubmitClicked: function (e) { e.preventDefault(); this.submitComment(); }, - submitComment: function(e){ + submitComment: function (e) { var that = this; this.hideMessage("#comment_sent_message"); this.hideMessage("#comment_send_failed_message"); @@ -206,11 +214,10 @@ module.exports = Handlebones.ModelView.extend({ this.hideMessage("#comment_send_failed_duplicate_message"); this.hideMessage("#comment_send_failed_conversation_closed_message"); - function doSubmitComment() { if (that.buttonActive) { that.buttonActive = false; - serialize(that, function(attrs){ + serialize(that, function (attrs) { if (attrs.txt.length === 0) { that.showMessage("#comment_send_failed_empty_message"); that.buttonActive = true; @@ -221,21 +228,29 @@ module.exports = Handlebones.ModelView.extend({ that.buttonActive = true; return; } - that.participantCommented(attrs).then(function() { - that.$("#comment_form_textarea").val(""); - that.hideFormControls(); - that.showMessage("#comment_sent_message"); - - }, function(err) { - // that.showMessage("#comment_send_failed_message"); - }).always(function() { - that.buttonActive = true; - }); + that + .participantCommented(attrs) + .then( + function () { + that.$("#comment_form_textarea").val(""); + that.hideFormControls(); + that.showMessage("#comment_sent_message"); + }, + function (err) { + // that.showMessage("#comment_send_failed_message"); + } + ) + .always(function () { + that.buttonActive = true; + }); }); } } var xid = Utils.getXid(); - var hasSocial = window.userObject.hasFacebook || window.userObject.hasTwitter || !_.isUndefined(xid); + var hasSocial = + window.userObject.hasFacebook || + window.userObject.hasTwitter || + !_.isUndefined(xid); var needsSocial = preload.firstConv.auth_needed_to_write; M.add(M.COMMENT_SUBMIT_CLICK); if (hasSocial || !needsSocial) { @@ -246,35 +261,38 @@ module.exports = Handlebones.ModelView.extend({ this.showSocialAuthChoices(); } }, - onAuthSuccess: function() { + onAuthSuccess: function () { this.reloadPagePreservingCommentText(); // $("#socialButtonsCommentForm").hide(); // $("#comment_form_controls").show(); }, - facebookClicked: function(e) { + facebookClicked: function (e) { e.preventDefault(); var that = this; M.addAndSend(M.COMMENT_SUBMIT_FB_INIT); - PolisFacebookUtils.connect().then(function() { - M.addAndSend(M.COMMENT_SUBMIT_FB_OK); - // wait a bit for new cookies to be ready, or something, then submit comment. - setTimeout(function() { - that.onAuthSuccess(); - // CurrentUserModel.update(); - }, 100); - }, function(err) { - M.addAndSend(M.COMMENT_SUBMIT_FB_ERR); - // alert("facebook error"); - }); + PolisFacebookUtils.connect().then( + function () { + M.addAndSend(M.COMMENT_SUBMIT_FB_OK); + // wait a bit for new cookies to be ready, or something, then submit comment. + setTimeout(function () { + that.onAuthSuccess(); + // CurrentUserModel.update(); + }, 100); + }, + function (err) { + M.addAndSend(M.COMMENT_SUBMIT_FB_ERR); + // alert("facebook error"); + } + ); }, - twitterClicked: function(e) { + twitterClicked: function (e) { var that = this; e.preventDefault(); - eb.on(eb.twitterConnectedCommentForm, function() { + eb.on(eb.twitterConnectedCommentForm, function () { M.addAndSend(M.COMMENT_SUBMIT_TW_OK); // wait a bit for new cookies to be ready, or something, then submit comment. - setTimeout(function() { + setTimeout(function () { that.onAuthSuccess(); // CurrentUserModel.update(); }, 100); @@ -284,19 +302,24 @@ module.exports = Handlebones.ModelView.extend({ // open a new window where the twitter auth screen will show. // that window will redirect back to a simple page that calls window.opener.twitterStatus("ok") - var params = 'location=0,status=0,width=800,height=400'; - window.open(document.location.origin + "/api/v3/twitterBtn?owner=false&dest=/twitterAuthReturn/CommentForm", 'twitterWindow', params); + var params = "location=0,status=0,width=800,height=400"; + window.open( + document.location.origin + + "/api/v3/twitterBtn?owner=false&dest=/twitterAuthReturn/CommentForm", + "twitterWindow", + params + ); }, - showSocialAuthChoices: function() { + showSocialAuthChoices: function () { $("#comment_form_controls").hide(); $("#socialButtonsCommentForm").show(); $("#socialButtonsUnderReadReact").hide(); }, - participantCommented: function(attrs) { + participantCommented: function (attrs) { var that = this; //that = the view attrs.pid = "mypid"; attrs.conversation_id = this.conversation_id; - attrs.vote = constants.REACTIONS.AGREE; // participants' comments are automatically agreed to. Needed for now since math assumes every comment has at least one vote. + attrs.vote = Constants.REACTIONS.AGREE; // participants' comments are automatically agreed to. Needed for now since math assumes every comment has at least one vote. if (/^\s*$/.exec(attrs.txt)) { alert(Strings.commentIsEmpty); @@ -320,47 +343,49 @@ module.exports = Handlebones.ModelView.extend({ if (!promise) { return reject(); } else { - promise.then(function() { - that.trigger("commentSubmitted"); // view.trigger - // $("#comment_form_textarea").hide(); - // $("#commentSentAlert").fadeIn(300); - // setTimeout(function() { - // $("#commentSentAlert").fadeOut(500, function() { - // $("#comment_form_textarea").fadeIn(400); - // }); - // }, 1500); - }, function(err) { - if (err.status === 409) { - - // that.model.set({ - // error: "Duplicate!", - // errorExtra: "That comment already exists.", - // }); - // alert(Strings.commentErrorDuplicate); - that.showMessage("#comment_send_failed_duplicate_message"); - } else if (err.responseText === "polis_err_conversation_is_closed"){ - - // that.model.set({ - // error: "This conversation is closed.", - // errorExtra: "No further commenting is allowed.", - // }); - // alert(Strings.commentErrorConversationClosed); - that.showMessage("#comment_send_failed_conversation_closed_message"); - } else { - - // that.model.set({ - // error: "Error sending comment.", - // errorExtra: "Please try again later.", - // }); - // alert(Strings.commentSendFailed); - that.showMessage("#comment_send_failed_message"); - // this.showMessage("#comment_send_failed_message"); + promise.then( + function () { + that.trigger("commentSubmitted"); // view.trigger + // $("#comment_form_textarea").hide(); + // $("#commentSentAlert").fadeIn(300); + // setTimeout(function() { + // $("#commentSentAlert").fadeOut(500, function() { + // $("#comment_form_textarea").fadeIn(400); + // }); + // }, 1500); + }, + function (err) { + if (err.status === 409) { + // that.model.set({ + // error: "Duplicate!", + // errorExtra: "That comment already exists.", + // }); + // alert(Strings.commentErrorDuplicate); + that.showMessage("#comment_send_failed_duplicate_message"); + } else if (err.responseText === "polis_err_conversation_is_closed") { + // that.model.set({ + // error: "This conversation is closed.", + // errorExtra: "No further commenting is allowed.", + // }); + // alert(Strings.commentErrorConversationClosed); + that.showMessage( + "#comment_send_failed_conversation_closed_message" + ); + } else { + // that.model.set({ + // error: "Error sending comment.", + // errorExtra: "Please try again later.", + // }); + // alert(Strings.commentSendFailed); + that.showMessage("#comment_send_failed_message"); + // this.showMessage("#comment_send_failed_message"); + } } - }); + ); return promise; } }, - initialize: function(options) { + initialize: function (options) { Handlebones.ModelView.prototype.initialize.apply(this, arguments); this.model = options.model; this.conversation_id = options.conversation_id; @@ -370,25 +395,26 @@ module.exports = Handlebones.ModelView.extend({ // })); this.serverClient = options.serverClient; - this.profilePicView = this.addChild(new ProfilePicView({ - model: CurrentUserModel, - })); + this.profilePicView = this.addChild( + new ProfilePicView({ + model: CurrentUserModel, + }) + ); if (options.wipCommentFormText) { this.shouldAutofocusOnTextarea = true; } var that = this; - this.listenTo(this, "render", function(){ - setTimeout(function() { + this.listenTo(this, "render", function () { + setTimeout(function () { if (!_.isUndefined(options.wipCommentFormText)) { $("#comment_form_textarea").val(options.wipCommentFormText); eb.trigger(eb.doneUsingWipCommentFormText); that.submitComment(); } autosize($("#comment_form_textarea")); - },100); + }, 100); }); - }, }); From ae7d3c97599967fe5000aa5dcbef2043db823fe8 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Mon, 19 Aug 2024 16:04:13 -0700 Subject: [PATCH 03/27] add perspective to example.env --- example.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example.env b/example.env index 6b1cbf80be..009be62b4d 100644 --- a/example.env +++ b/example.env @@ -117,7 +117,7 @@ AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= # This value is written by the server app if SHOULD_USE_TRANSLATION_API is true. GOOGLE_APPLICATION_CREDENTIALS= - +GOOGLE_JIGSAW_PERSPECTIVE_API_KEY= ###### DEPRECATED ###### # (Deprecated) Used internally by Node.Crypto to encrypt/decrypt IP addresses. From 22d200dde6b326b9a02786404f1acdd87ae70521 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Mon, 19 Aug 2024 16:04:31 -0700 Subject: [PATCH 04/27] add google apis to package.json --- server/package-lock.json | 851 ++++++++++++++++++++++++++++++++++++--- server/package.json | 1 + 2 files changed, 793 insertions(+), 59 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 1438d87970..92428cdfb1 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -21,6 +21,7 @@ "dotenv": "^16.0.3", "express": "~3.21.2", "fb": "~1.0.2", + "googleapis": "^142.0.0", "html-entities": "^2.4.0", "http-proxy": "~1.18.1", "lru-cache": "~3.0.0", @@ -2490,6 +2491,15 @@ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2668,13 +2678,19 @@ "integrity": "sha512-k9VSlRfRi5JYyQWMylSOgjld96ta1qaQUIvmn+na0BzViclH04PBumewv4z5aeXNkn6Z/gAN5FtPeBLvV20F9w==" }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3311,6 +3327,23 @@ "node": ">=0.10.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/degenerator": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz", @@ -3559,6 +3592,27 @@ "node": ">= 0.6" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -4420,10 +4474,79 @@ "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } }, "node_modules/gcp-metadata": { "version": "0.3.1", @@ -4456,14 +4579,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "dev": true, + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4627,6 +4755,204 @@ "gp12-pem": "bin/gp12-pem" } }, + "node_modules/googleapis": { + "version": "142.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-142.0.0.tgz", + "integrity": "sha512-LsU1ynez4/KNPwnFMSDI93pBEsETNdQPCrT3kz2qgiNg5H2pW4dKW+1VmENMkZ4u9lMxA89nnXD3nqWBJ0rruQ==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.0.0", + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common/node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis-common/node_modules/google-auth-library": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.13.0.tgz", + "integrity": "sha512-p9Y03Uzp/Igcs36zAaB0XTSwZ8Y0/tpYiz5KIde5By+H9DCVUSYtDWZu6aFXsWTqENMb8BD/pDT3hR8NVrPkfA==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis-common/node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/googleapis-common/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/googleapis-common/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/googleapis-common/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/googleapis/node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis/node_modules/google-auth-library": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.13.0.tgz", + "integrity": "sha512-p9Y03Uzp/Igcs36zAaB0XTSwZ8Y0/tpYiz5KIde5By+H9DCVUSYtDWZu6aFXsWTqENMb8BD/pDT3hR8NVrPkfA==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis/node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/googleapis/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -4692,11 +5018,35 @@ "node": ">=4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4704,6 +5054,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", @@ -5890,6 +6252,15 @@ "node": ">=4" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -6447,6 +6818,26 @@ "node": ">= 0.4.0" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", @@ -6585,10 +6976,13 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true, + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7970,6 +8364,23 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz", "integrity": "sha512-pVEuxHdSGrt8QmQ3LOZXLhSA6MP/iPqKzZeO6Squ7PNGkA/9MBsSfV0/L+bIxkoDmjF4tZcLpcVq/fkqoHvuKg==" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", @@ -7997,14 +8408,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8700,6 +9115,12 @@ "node": ">=0.8" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/triple-beam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", @@ -9012,6 +9433,12 @@ "querystring": "0.2.0" } }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, "node_modules/url/node_modules/punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", @@ -9115,6 +9542,22 @@ "makeerror": "1.0.12" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11275,6 +11718,11 @@ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, + "bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -11420,13 +11868,15 @@ "integrity": "sha512-k9VSlRfRi5JYyQWMylSOgjld96ta1qaQUIvmn+na0BzViclH04PBumewv4z5aeXNkn6Z/gAN5FtPeBLvV20F9w==" }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "callsites": { @@ -11927,6 +12377,16 @@ "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==", "dev": true }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, "degenerator": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz", @@ -12139,6 +12599,19 @@ } } }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -12795,10 +13268,50 @@ } }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "requires": { + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + } + } }, "gcp-metadata": { "version": "0.3.1", @@ -12822,14 +13335,15 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "dev": true, + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, "get-package-type": { @@ -12958,6 +13472,153 @@ "node-forge": "^0.7.1" } }, + "googleapis": { + "version": "142.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-142.0.0.tgz", + "integrity": "sha512-LsU1ynez4/KNPwnFMSDI93pBEsETNdQPCrT3kz2qgiNg5H2pW4dKW+1VmENMkZ4u9lMxA89nnXD3nqWBJ0rruQ==", + "requires": { + "google-auth-library": "^9.0.0", + "googleapis-common": "^7.0.0" + }, + "dependencies": { + "gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "requires": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.13.0.tgz", + "integrity": "sha512-p9Y03Uzp/Igcs36zAaB0XTSwZ8Y0/tpYiz5KIde5By+H9DCVUSYtDWZu6aFXsWTqENMb8BD/pDT3hR8NVrPkfA==", + "requires": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + } + }, + "gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "requires": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + } + } + }, + "googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "requires": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "dependencies": { + "gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "requires": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.13.0.tgz", + "integrity": "sha512-p9Y03Uzp/Igcs36zAaB0XTSwZ8Y0/tpYiz5KIde5By+H9DCVUSYtDWZu6aFXsWTqENMb8BD/pDT3hR8NVrPkfA==", + "requires": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + } + }, + "gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "requires": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" + } + }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + } + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -13010,11 +13671,31 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } }, "hexoid": { "version": "1.0.0", @@ -13927,6 +14608,14 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -14390,6 +15079,14 @@ "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", "integrity": "sha512-3DWDqAtIiPSkBXZyYEjwebfK56nrlQfRGt642fu8RPaL+ePu750+HCMHxjJCG3iEHq/0aeMvX6KIzlv7nuhfrA==" }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, "node-forge": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", @@ -14496,10 +15193,9 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "on-finished": { "version": "2.3.0", @@ -15560,6 +16256,19 @@ } } }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, "setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", @@ -15581,14 +16290,14 @@ "dev": true }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "signal-exit": { @@ -16139,6 +16848,11 @@ "punycode": "^2.1.1" } }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "triple-beam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", @@ -16348,6 +17062,11 @@ } } }, + "url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -16431,6 +17150,20 @@ "makeerror": "1.0.12" } }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/server/package.json b/server/package.json index 7f8b4e8793..c243498560 100644 --- a/server/package.json +++ b/server/package.json @@ -37,6 +37,7 @@ "dotenv": "^16.0.3", "express": "~3.21.2", "fb": "~1.0.2", + "googleapis": "^142.0.0", "html-entities": "^2.4.0", "http-proxy": "~1.18.1", "lru-cache": "~3.0.0", From 60738cb9ed15358497c557239bacd0a939a5e523 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Mon, 19 Aug 2024 16:04:59 -0700 Subject: [PATCH 05/27] add perspective api key to config --- server/src/config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/config.ts b/server/src/config.ts index ad74b5f3a1..44a346ac86 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -62,6 +62,8 @@ export default { adminEmails: process.env.ADMIN_EMAILS || '[]' as string, adminUIDs: process.env.ADMIN_UIDS || '[]' as string, akismetAntispamApiKey: process.env.AKISMET_ANTISPAM_API_KEY || null as string | null, + googleJigsawPerspectiveApiKey: + process.env.GOOGLE_JIGSAW_PERSPECTIVE_API_KEY || (null as string | null), awsRegion: process.env.AWS_REGION as string, backfillCommentLangDetection: isTrue(process.env.BACKFILL_COMMENT_LANG_DETECTION) as boolean, cacheMathResults: isTrueOrBlank(process.env.CACHE_MATH_RESULTS) as boolean, From 8fbbab54340b495a93ad84ef1685477d507fe03e Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Mon, 19 Aug 2024 16:05:06 -0700 Subject: [PATCH 06/27] config prettier --- server/src/config.ts | 74 ++++++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/server/src/config.ts b/server/src/config.ts index 44a346ac86..c33fc8cd07 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -3,13 +3,18 @@ import isTrue from "boolean"; const devHostname = process.env.API_DEV_HOSTNAME || "localhost:5000"; const devMode = isTrue(process.env.DEV_MODE) as boolean; -const domainOverride = process.env.DOMAIN_OVERRIDE || null as string | null; +const domainOverride = process.env.DOMAIN_OVERRIDE || (null as string | null); const prodHostname = process.env.API_PROD_HOSTNAME || "pol.is"; -const serverPort = parseInt(process.env.API_SERVER_PORT || process.env.PORT || "5000", 10) as number; -const shouldUseTranslationAPI = isTrue(process.env.SHOULD_USE_TRANSLATION_API) as boolean; - -import('source-map-support').then((sourceMapSupport) => { - sourceMapSupport.install(); +const serverPort = parseInt( + process.env.API_SERVER_PORT || process.env.PORT || "5000", + 10 +) as number; +const shouldUseTranslationAPI = isTrue( + process.env.SHOULD_USE_TRANSLATION_API +) as boolean; + +import("source-map-support").then((sourceMapSupport) => { + sourceMapSupport.install(); }); export default { @@ -59,18 +64,22 @@ export default { adminEmailDataExport: process.env.ADMIN_EMAIL_DATA_EXPORT as string, adminEmailDataExportTest: process.env.ADMIN_EMAIL_DATA_EXPORT_TEST as string, adminEmailEmailTest: process.env.ADMIN_EMAIL_EMAIL_TEST as string, - adminEmails: process.env.ADMIN_EMAILS || '[]' as string, - adminUIDs: process.env.ADMIN_UIDS || '[]' as string, - akismetAntispamApiKey: process.env.AKISMET_ANTISPAM_API_KEY || null as string | null, + adminEmails: process.env.ADMIN_EMAILS || ("[]" as string), + adminUIDs: process.env.ADMIN_UIDS || ("[]" as string), + akismetAntispamApiKey: + process.env.AKISMET_ANTISPAM_API_KEY || (null as string | null), googleJigsawPerspectiveApiKey: process.env.GOOGLE_JIGSAW_PERSPECTIVE_API_KEY || (null as string | null), awsRegion: process.env.AWS_REGION as string, - backfillCommentLangDetection: isTrue(process.env.BACKFILL_COMMENT_LANG_DETECTION) as boolean, + backfillCommentLangDetection: isTrue( + process.env.BACKFILL_COMMENT_LANG_DETECTION + ) as boolean, cacheMathResults: isTrueOrBlank(process.env.CACHE_MATH_RESULTS) as boolean, databaseURL: process.env.DATABASE_URL as string, - emailTransportTypes: process.env.EMAIL_TRANSPORT_TYPES || null as string | null, + emailTransportTypes: + process.env.EMAIL_TRANSPORT_TYPES || (null as string | null), encryptionPassword: process.env.ENCRYPTION_PASSWORD_00001 as string, - fbAppId: process.env.FB_APP_ID || null as string | null, + fbAppId: process.env.FB_APP_ID || (null as string | null), logLevel: process.env.SERVER_LOG_LEVEL as string, logToFile: isTrue(process.env.SERVER_LOG_TO_FILE) as boolean, mailgunApiKey: process.env.MAILGUN_API_KEY || (null as string | null), @@ -80,14 +89,29 @@ export default { maxmindUserID: process.env.MAXMIND_USER_ID as string, nodeEnv: process.env.NODE_ENV as string, polisFromAddress: process.env.POLIS_FROM_ADDRESS as string, - readOnlyDatabaseURL: process.env.READ_ONLY_DATABASE_URL || process.env.DATABASE_URL as string, - runPeriodicExportTests: isTrue(process.env.RUN_PERIODIC_EXPORT_TESTS) as boolean, + readOnlyDatabaseURL: + process.env.READ_ONLY_DATABASE_URL || (process.env.DATABASE_URL as string), + runPeriodicExportTests: isTrue( + process.env.RUN_PERIODIC_EXPORT_TESTS + ) as boolean, shouldUseTranslationAPI: setGoogleApplicationCredentials() as boolean, - staticFilesAdminPort: parseInt(process.env.STATIC_FILES_ADMIN_PORT || process.env.STATIC_FILES_PORT || '8080', 10) as number, - staticFilesParticipationPort: parseInt(process.env.STATIC_FILES_PARTICIPATION_PORT || process.env.STATIC_FILES_PORT || '8080', 10) as number, + staticFilesAdminPort: parseInt( + process.env.STATIC_FILES_ADMIN_PORT || + process.env.STATIC_FILES_PORT || + "8080", + 10 + ) as number, + staticFilesParticipationPort: parseInt( + process.env.STATIC_FILES_PARTICIPATION_PORT || + process.env.STATIC_FILES_PORT || + "8080", + 10 + ) as number, staticFilesHost: process.env.STATIC_FILES_HOST as string, - twitterConsumerKey: process.env.TWITTER_CONSUMER_KEY || null as string | null, - twitterConsumerSecret: process.env.TWITTER_CONSUMER_SECRET || null as string | null, + twitterConsumerKey: + process.env.TWITTER_CONSUMER_KEY || (null as string | null), + twitterConsumerSecret: + process.env.TWITTER_CONSUMER_SECRET || (null as string | null), webserverPass: process.env.WEBSERVER_PASS as string, webserverUsername: process.env.WEBSERVER_USERNAME as string, @@ -100,12 +124,12 @@ export default { process.env.DOMAIN_WHITELIST_ITEM_06 || null, process.env.DOMAIN_WHITELIST_ITEM_07 || null, process.env.DOMAIN_WHITELIST_ITEM_08 || null, - ].filter(item => item !== null) as string[], + ].filter((item) => item !== null) as string[], }; // Use this function when a value shuould default to true if not set. function isTrueOrBlank(val: string | boolean | undefined): boolean { - return val === undefined || val === '' || isTrue(val); + return val === undefined || val === "" || isTrue(val); } function setGoogleApplicationCredentials(): boolean { @@ -113,13 +137,17 @@ function setGoogleApplicationCredentials(): boolean { return false; } - const googleCredentialsBase64: string | undefined = process.env.GOOGLE_CREDENTIALS_BASE64; - const googleCredsStringified: string | undefined = process.env.GOOGLE_CREDS_STRINGIFIED; + const googleCredentialsBase64: string | undefined = + process.env.GOOGLE_CREDENTIALS_BASE64; + const googleCredsStringified: string | undefined = + process.env.GOOGLE_CREDS_STRINGIFIED; try { // TODO: Consider deprecating GOOGLE_CREDS_STRINGIFIED in future. if (!googleCredentialsBase64 && !googleCredsStringified) { - throw new Error("Missing Google credentials. Translation API will be disabled."); + throw new Error( + "Missing Google credentials. Translation API will be disabled." + ); } const creds_string = googleCredentialsBase64 From 72dfe235263d7cbac90e7bad8551560c9d04aa1b Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Tue, 20 Aug 2024 14:02:20 -0700 Subject: [PATCH 07/27] import google --- server/src/server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/server.ts b/server/src/server.ts index e448a2bacd..e8e0b6ea8e 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -13,6 +13,7 @@ import async from "async"; // npm list types-at-fb // @ts-ignore import FB from "fb"; +import { google } from "googleapis"; import fs from "fs"; import bcrypt from "bcryptjs"; import crypto from "crypto"; From f4d6d83935dd617c7d7322cb217b3066785bdf75 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Tue, 20 Aug 2024 14:21:03 -0700 Subject: [PATCH 08/27] prettier server.ts --- server/src/server.ts | 210 ++++++++++++++++++++++++------------------- 1 file changed, 118 insertions(+), 92 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index e8e0b6ea8e..1324da2640 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -115,7 +115,6 @@ const resolveWith = (x: { body?: { user_id: string } }) => { return Promise.resolve(x); }; - //var SegfaultHandler = require('segfault-handler'); //SegfaultHandler.registerHandler("segfault.log"); @@ -131,17 +130,13 @@ if (devMode) { // Bluebird uncaught error handler. Promise.onPossiblyUnhandledRejection(function (err: any) { - logger.error('onPossiblyUnhandledRejection', err); + logger.error("onPossiblyUnhandledRejection", err); // throw err; // not throwing since we're printing stack traces anyway }); -const adminEmails = Config.adminEmails - ? JSON.parse(Config.adminEmails) - : []; +const adminEmails = Config.adminEmails ? JSON.parse(Config.adminEmails) : []; -const polisDevs = Config.adminUIDs - ? JSON.parse(Config.adminUIDs) - : []; +const polisDevs = Config.adminUIDs ? JSON.parse(Config.adminUIDs) : []; function isPolisDev(uid?: any) { return polisDevs.indexOf(uid) >= 0; @@ -612,7 +607,7 @@ function initializePolisHelpers() { conv?: { zid: any }, tid?: any, voteType?: any, - weight?: number, + weight?: number ) { let zid = conv?.zid; weight = weight || 0; @@ -652,7 +647,7 @@ function initializePolisHelpers() { tid?: any, xid?: any, voteType?: any, - weight?: number, + weight?: number ) { return ( pgQueryP_readOnly("select * from conversations where zid = ($1);", [zid]) @@ -696,25 +691,20 @@ function initializePolisHelpers() { }); } if (conv.use_xid_whitelist) { - return isXidWhitelisted(conv.owner, xid).then((is_whitelisted: boolean) => { - if (is_whitelisted) { - return conv; - } else { - throw 'polis_err_xid_not_whitelisted'; + return isXidWhitelisted(conv.owner, xid).then( + (is_whitelisted: boolean) => { + if (is_whitelisted) { + return conv; + } else { + throw "polis_err_xid_not_whitelisted"; + } } - }); + ); } return conv; }) .then(function (conv: any) { - return doVotesPost( - uid, - pid, - conv, - tid, - voteType, - weight, - ); + return doVotesPost(uid, pid, conv, tid, voteType, weight); }) ); } @@ -777,10 +767,17 @@ function initializePolisHelpers() { } function redirectIfNotHttps( - req: { headers: { [x: string]: string; host: string }; method: string; path: string; url: string }, + req: { + headers: { [x: string]: string; host: string }; + method: string; + path: string; + url: string; + }, res: { end: () => any; - status: (arg0: number) => { + status: ( + arg0: number + ) => { send: (arg0: string) => any; }; writeHead: (arg0: number, arg1: { Location: string }) => void; @@ -788,23 +785,23 @@ function initializePolisHelpers() { next: () => any ) { // Exempt dev mode or healthcheck path from HTTPS check - if (devMode || req.path === '/api/v3/testConnection') { + if (devMode || req.path === "/api/v3/testConnection") { return next(); } // Check if the request is already HTTPS - const isHttps = req.headers['x-forwarded-proto'] === 'https'; + const isHttps = req.headers["x-forwarded-proto"] === "https"; if (!isHttps) { - logger.debug('redirecting to https', { headers: req.headers }); + logger.debug("redirecting to https", { headers: req.headers }); // Only redirect GET requests; otherwise, send a 400 error for non-GET methods - if (req.method === 'GET') { + if (req.method === "GET") { res.writeHead(302, { - Location: `https://${req.headers.host}${req.url}` + Location: `https://${req.headers.host}${req.url}`, }); return res.end(); } else { - res.status(400).send('Please use HTTPS when submitting data.'); + res.status(400).send("Please use HTTPS when submitting data."); } } return next(); @@ -1681,7 +1678,7 @@ function initializePolisHelpers() { }); } function redirectIfHasZidButNoConversationId( - req: { body: { zid: any; conversation_id: any }, headers?: any }, + req: { body: { zid: any; conversation_id: any }; headers?: any }, res: { writeHead: (arg0: number, arg1: { Location: string }) => void; end: () => any; @@ -2369,7 +2366,8 @@ function initializePolisHelpers() { ); } ); - }); + } + ); } const getServerNameWithProtocol = Config.getServerNameWithProtocol; @@ -2408,7 +2406,12 @@ function initializePolisHelpers() { server, function (err: any) { if (err) { - fail(res, 500, "Error: Couldn't send password reset email.", err); + fail( + res, + 500, + "Error: Couldn't send password reset email.", + err + ); return; } finish(); @@ -2471,7 +2474,7 @@ Feel free to reply to this email if you need help.`; ) { res?.clearCookie?.(cookieName, { path: "/", - domain: cookies.cookieDomain(req) + domain: cookies.cookieDomain(req), }); } @@ -3720,12 +3723,14 @@ ${serverName}/pwreset/${pwresettoken} userInfo.email, "Polis Password Reset", body - ).then(function () { - callback?.(); - }).catch(function (err: any) { - logger.error("polis_err_failed_to_email_password_reset_code", err); - callback?.(err); - }); + ) + .then(function () { + callback?.(); + }) + .catch(function (err: any) { + logger.error("polis_err_failed_to_email_password_reset_code", err); + callback?.(err); + }); } ); } @@ -4742,9 +4747,10 @@ Email verified! You can close this tab or hit the back button. // lti_user_image: any; lti_context_id: any; tool_consumer_instance_guid?: any; afterJoinRedirectUrl: any; }; }' but required in type // '{ cookies: { [x: string]: any; }; }'.ts(2345) // @ts-ignore - addCookies(req, res, token, uid).then(function () { - res.json(response_data); - }) + addCookies(req, res, token, uid) + .then(function () { + res.json(response_data); + }) .catch(function (err: any) { fail(res, 500, "polis_err_adding_cookies", err); }); @@ -5272,7 +5278,11 @@ Email verified! You can close this tab or hit the back button. // Type 'unknown' is not assignable to type 'any[]'.ts(2345) // @ts-ignore ).then(function (rows: string | any[]) { - logger.debug("isParentDomainWhitelisted", { domain, zid, isWithinIframe }); + logger.debug("isParentDomainWhitelisted", { + domain, + zid, + isWithinIframe, + }); if (!rows || !rows.length || !rows[0].domain_whitelist.length) { // there is no whitelist, so any domain is ok. logger.debug("isParentDomainWhitelisted : no whitelist"); @@ -6374,7 +6384,6 @@ Email verified! You can close this tab or hit the back button. const _getCommentsList = Comment._getCommentsList; const getNumberOfCommentsRemaining = Comment.getNumberOfCommentsRemaining; - function handle_GET_participation( req: { p: { zid: any; uid?: any; strict: any } }, res: { @@ -7121,7 +7130,7 @@ Email verified! You can close this tab or hit the back button. logger.debug("Post comments quote_txt", { txt, quote_txt }); txt = quote_txt; } else { - logger.debug("Post comments txt", {zid, pid, txt}); + logger.debug("Post comments txt", { zid, pid, txt }); } let ip = @@ -7158,26 +7167,28 @@ Email verified! You can close this tab or hit the back button. !_.isUndefined(xid) && !_.isNull(xid) ? getXidStuff(xid, zid) : Promise.resolve(); - pidPromise = xidUserPromise.then((xidUser: UserType | "noXidRecord") => { - shouldCreateXidRecord = xidUser === "noXidRecord"; - if (typeof xidUser === 'object') { - uid = xidUser.uid; - pid = xidUser.pid; - return pid; - } else { - return doGetPid().then((pid: any) => { - if (shouldCreateXidRecord) { - // Expected 6 arguments, but got 3.ts(2554) - // conversation.ts(34, 3): An argument for 'x_profile_image_url' was not provided. - // @ts-ignore - return createXidRecordByZid(zid, uid, xid).then(() => { - return pid; - }); - } + pidPromise = xidUserPromise.then( + (xidUser: UserType | "noXidRecord") => { + shouldCreateXidRecord = xidUser === "noXidRecord"; + if (typeof xidUser === "object") { + uid = xidUser.uid; + pid = xidUser.pid; return pid; - }); + } else { + return doGetPid().then((pid: any) => { + if (shouldCreateXidRecord) { + // Expected 6 arguments, but got 3.ts(2554) + // conversation.ts(34, 3): An argument for 'x_profile_image_url' was not provided. + // @ts-ignore + return createXidRecordByZid(zid, uid, xid).then(() => { + return pid; + }); + } + return pid; + }); + } } - }); + ); } let commentExistsPromise = commentExists(zid, txt); @@ -7238,16 +7249,22 @@ Email verified! You can close this tab or hit the back button. if (bad && conv.profanity_filter) { active = false; classifications.push("bad"); - logger.info("active=false because (bad && conv.profanity_filter)"); + logger.info( + "active=false because (bad && conv.profanity_filter)" + ); } if (spammy && conv.spam_filter) { active = false; classifications.push("spammy"); - logger.info("active=false because (spammy && conv.spam_filter)"); + logger.info( + "active=false because (spammy && conv.spam_filter)" + ); } if (conv.strict_moderation) { active = false; - logger.info("active=false because (conv.strict_moderation)"); + logger.info( + "active=false because (conv.strict_moderation)" + ); } let mod = 0; // hasn't yet been moderated. @@ -7376,7 +7393,12 @@ Email verified! You can close this tab or hit the back button. function (err: { code: string | number }) { if (err.code === "23505" || err.code === 23505) { // duplicate comment - fail(res, 409, "polis_err_post_comment_duplicate", err); + fail( + res, + 409, + "polis_err_post_comment_duplicate", + err + ); } else { fail(res, 500, "polis_err_post_comment", err); } @@ -7504,8 +7526,11 @@ Email verified! You can close this tab or hit the back button. let comments = results[0]; let math = results[1]; let numberOfCommentsRemainingRows = results[2]; - logger.debug("getNextPrioritizedComment intermediate results:", - {zid, pid, numberOfCommentsRemainingRows}); + logger.debug("getNextPrioritizedComment intermediate results:", { + zid, + pid, + numberOfCommentsRemainingRows, + }); if (!comments || !comments.length) { return null; } else if ( @@ -7960,7 +7985,7 @@ Email verified! You can close this tab or hit the back button. req.p.tid, req.p.xid, req.p.vote, - req.p.weight, + req.p.weight ); }) .then(function (o: { vote: any }) { @@ -7984,7 +8009,11 @@ Email verified! You can close this tab or hit the back button. return getNextComment(zid, pid, [], true, lang); }) .then(function (nextComment: any) { - logger.debug("handle_POST_votes nextComment:", {zid, pid, nextComment}); + logger.debug("handle_POST_votes nextComment:", { + zid, + pid, + nextComment, + }); let result: PidReadyResult = {}; if (nextComment) { result.nextComment = nextComment; @@ -8023,8 +8052,8 @@ Email verified! You can close this tab or hit the back button. fail(res, 403, "polis_err_conversation_is_closed", err); } else if (err === "polis_err_post_votes_social_needed") { fail(res, 403, "polis_err_post_votes_social_needed", err); - } else if (err === 'polis_err_xid_not_whitelisted') { - fail(res, 403, 'polis_err_xid_not_whitelisted', err); + } else if (err === "polis_err_xid_not_whitelisted") { + fail(res, 403, "polis_err_xid_not_whitelisted", err); } else { fail(res, 500, "polis_err_vote", err); } @@ -8461,7 +8490,7 @@ Email verified! You can close this tab or hit the back button. pgQueryP( "update conversations set is_active = false where zid = ($1);", [conv.zid] - ) + ); }) .catch(function (err: any) { fail(res, 500, "polis_err_closing_conversation", err); @@ -12511,7 +12540,6 @@ Thanks for using Polis! }); } - function handle_POST_einvites( req: { p: { email: any } }, res: { @@ -13537,7 +13565,8 @@ Thanks for using Polis! // serve up index.html in response to anything starting with a number let hostname: string = Config.staticFilesHost; - let staticFilesParticipationPort: number = Config.staticFilesParticipationPort; + let staticFilesParticipationPort: number = + Config.staticFilesParticipationPort; let staticFilesAdminPort: number = Config.staticFilesAdminPort; let fetchUnsupportedBrowserPage = makeFileFetcher( hostname, @@ -13612,14 +13641,16 @@ Thanks for using Polis! function ifDefinedFirstElseSecond(first: any, second: boolean) { return _.isUndefined(first) ? second : first; } - let fetch404Page = makeFileFetcher(hostname, staticFilesAdminPort, "/404.html", { - "Content-Type": "text/html", - }); + let fetch404Page = makeFileFetcher( + hostname, + staticFilesAdminPort, + "/404.html", + { + "Content-Type": "text/html", + } + ); - function fetchIndexForConversation( - req: { path: string; }, - res: any - ) { + function fetchIndexForConversation(req: { path: string }, res: any) { logger.debug("fetchIndexForConversation", req.path); let match = req.path.match(/[0-9][0-9A-Za-z]+/); let conversation_id: any; @@ -13655,12 +13686,7 @@ Thanks for using Polis! conversation: x, // Nothing user-specific can go here, since we want to cache these per-conv index files on the CDN. }; - fetchIndex( - req, - res, - preloadData, - staticFilesParticipationPort - ); + fetchIndex(req, res, preloadData, staticFilesParticipationPort); }) .catch(function (err: any) { logger.error("polis_err_fetching_conversation_info", err); From dd390e154b73229f64b8545f10076b821fe09866 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Tue, 20 Aug 2024 15:16:03 -0700 Subject: [PATCH 09/27] fix typescript bluebird promise to enable async await --- server/src/server.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index 1324da2640..240230ec09 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -5,7 +5,7 @@ import akismetLib from "akismet"; import AWS from "aws-sdk"; import badwords from "badwords/object"; -import Promise from "bluebird"; +import { Promise as BluebirdPromise } from "bluebird"; import http from "http"; import httpProxy from "http-proxy"; // const Promise = require('es6-promise').Promise, @@ -125,11 +125,11 @@ const resolveWith = (x: { body?: { user_id: string } }) => { // }; if (devMode) { - Promise.longStackTraces(); + BluebirdPromise.longStackTraces(); } // Bluebird uncaught error handler. -Promise.onPossiblyUnhandledRejection(function (err: any) { +BluebirdPromise.onPossiblyUnhandledRejection(function (err: any) { logger.error("onPossiblyUnhandledRejection", err); // throw err; // not throwing since we're printing stack traces anyway }); @@ -4309,7 +4309,7 @@ Email verified! You can close this tab or hit the back button. // @ts-ignore pid_to_ptpt[c.pid] = c; }); - return Promise.mapSeries( + return BluebirdPromise.mapSeries( candidates, (item: { zid: any; pid: any }, index: any, length: any) => { return getNumberOfCommentsRemaining(item.zid, item.pid).then( @@ -4409,7 +4409,7 @@ Email verified! You can close this tab or hit the back button. } ); - return Promise.each( + return BluebirdPromise.each( needNotification, ( item: { pid: string | number; remaining: any }, From fe504bbf9cd4cc48d6be4022b94d188828a42729 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Tue, 20 Aug 2024 18:21:59 -0700 Subject: [PATCH 10/27] refactor post_comment route async, remove paths --- server/src/server.ts | 626 ++++++++++++++++--------------------------- 1 file changed, 238 insertions(+), 388 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index 240230ec09..0bb2970c0c 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -7020,413 +7020,262 @@ Email verified! You can close this tab or hit the back button. }); } - function handle_POST_comments( - req: { - p: { - zid?: any; - xid?: any; - uid?: any; - txt?: any; - pid?: any; - vote?: any; - twitter_tweet_id?: any; - quote_twitter_screen_name?: any; - quote_txt?: any; - quote_src_url?: any; - anon?: any; - is_seed?: any; - }; - headers?: Headers; - connection?: { remoteAddress: any; socket: { remoteAddress: any } }; - socket?: { remoteAddress: any }; - }, - res: { json: (arg0: { tid: any; currentPid: any }) => void } - ) { - let zid = req.p.zid; - let xid = req.p.xid; - let uid = req.p.uid; - let txt = req.p.txt; - let pid = req.p.pid; // PID_FLOW may be undefined + interface RequestParams { + zid?: string; + xid?: string; + uid?: string; + txt?: string; + pid?: string; + vote?: number; + anon?: boolean; + is_seed?: boolean; + } + + interface CustomRequest extends Request { + p: RequestParams; + } + + async function handle_POST_comments( + req: CustomRequest, + res: Response + ): Promise { + const { zid, xid, uid, txt, pid: initialPid, vote, anon, is_seed } = req.p; + + console.log("============= handle_POST_comments ==========="); + console.log(zid, xid, uid, txt, initialPid, vote, anon, is_seed); + /* + 2024-08-20 15:44:25 ============= handle_POST_comments =========== + 2024-08-20 15:44:25 37436 undefined 186 a lovely comment 3 undefined -1 undefined undefined + */ + + let pid = initialPid; let currentPid = pid; - let vote = req.p.vote; - let twitter_tweet_id = req.p.twitter_tweet_id; - let quote_twitter_screen_name = req.p.quote_twitter_screen_name; - let quote_txt = req.p.quote_txt; - let quote_src_url = req.p.quote_src_url; - let anon = req.p.anon; - let is_seed = req.p.is_seed; - let mustBeModerator = !!quote_txt || !!twitter_tweet_id || anon; - - // either include txt, or a tweet id - if ( - (_.isUndefined(txt) || txt === "") && - (_.isUndefined(twitter_tweet_id) || twitter_tweet_id === "") && - (_.isUndefined(quote_txt) || quote_txt === "") - ) { + const mustBeModerator = anon; + + if (!txt || txt === "") { fail(res, 400, "polis_err_param_missing_txt"); return; } - if (quote_txt && _.isUndefined(quote_src_url)) { - fail(res, 400, "polis_err_param_missing_quote_src_url"); - return; + async function doGetPid(): Promise { + if (_.isUndefined(pid)) { + const newPid = await getPidPromise(zid!, uid!, true); + if (newPid === -1) { + const rows = await addParticipant(zid!, uid!); + const ptpt = rows[0]; + pid = ptpt.pid; + currentPid = pid; + return pid; + } else { + return newPid; + } + } + return Number(pid); } - function doGetPid() { - // PID_FLOW - if (_.isUndefined(pid)) { - return getPidPromise(req.p.zid, req.p.uid, true).then((pid: number) => { - if (pid === -1) { - // Argument of type '(rows: any[]) => number' is not assignable to parameter of type '(value: unknown) => number | PromiseLike'. - // Types of parameters 'rows' and 'value' are incompatible. - // Type 'unknown' is not assignable to type 'any[]'.ts(2345) - // @ts-ignore - return addParticipant(req.p.zid, req.p.uid).then(function ( - rows: any[] - ) { - let ptpt = rows[0]; - pid = ptpt.pid; - currentPid = pid; - return pid; - }); - } else { + try { + logger.debug("Post comments txt", { zid, pid, txt }); + + const ip = + req.headers["x-forwarded-for"] || + req.connection?.remoteAddress || + req.socket?.remoteAddress || + req.connection?.socket?.remoteAddress; + + const isSpamPromise = isSpam({ + comment_content: txt, + comment_author: uid!, + permalink: `https://pol.is/${zid}`, + user_ip: ip as string, + user_agent: req.headers["user-agent"], + referrer: req.headers.referer, + }).catch((err) => { + logger.error("isSpam failed", err); + return false; + }); + + const isModeratorPromise = isModerator(zid!, uid!); + const conversationInfoPromise = getConversationInfo(zid!); + + let shouldCreateXidRecord = false; + + const pidPromise = (async () => { + if (xid) { + const xidUser = await getXidStuff(xid, zid!); + shouldCreateXidRecord = xidUser === "noXidRecord"; + if (typeof xidUser === "object") { + uid = xidUser.uid; + pid = xidUser.pid; return pid; } - }); - } - return Promise.resolve(pid); - } - let twitterPrepPromise = Promise.resolve(); - if (twitter_tweet_id) { - twitterPrepPromise = prepForTwitterComment(twitter_tweet_id, zid); - } else if (quote_twitter_screen_name) { - twitterPrepPromise = prepForQuoteWithTwitterUser( - quote_twitter_screen_name, - zid - ); - } + } + const newPid = await doGetPid(); + if (shouldCreateXidRecord) { + await createXidRecordByZid(zid!, uid!, xid!); + } + return newPid; + })(); - twitterPrepPromise - .then( - // No overload matches this call. - // Overload 1 of 2, '(onFulfill?: ((value: void) => any) | undefined, onReject?: ((error: any) => any) | undefined): Bluebird', gave the following error. - // Argument of type '(info: { ptpt: any; tweet: any; }) => Bluebird' is not assignable to parameter of type '(value: void) => any'. - // Types of parameters 'info' and 'value' are incompatible. - // Type 'void' is not assignable to type '{ ptpt: any; tweet: any; }'. - // Overload 2 of 2, '(onfulfilled?: ((value: void) => any) | null | undefined, onrejected?: ((reason: any) => Resolvable) | null | undefined): Bluebird', gave the following error. - // Argument of type '(info: { ptpt: any; tweet: any; }) => Bluebird' is not assignable to parameter of type '(value: void) => any'. - // Types of parameters 'info' and 'value' are incompatible. - // Type 'void' is not assignable to type '{ ptpt: any; tweet: any; }'.ts(2769) - // @ts-ignore - function (info: { ptpt: any; tweet: any }) { - let ptpt = info && info.ptpt; - // let twitterUser = info && info.twitterUser; - let tweet = info && info.tweet; - - if (tweet) { - logger.debug("Post comments tweet", { txt, tweetTxt: tweet.txt }); - txt = tweet.text; - } else if (quote_txt) { - logger.debug("Post comments quote_txt", { txt, quote_txt }); - txt = quote_txt; - } else { - logger.debug("Post comments txt", { zid, pid, txt }); - } - - let ip = - req?.headers?.["x-forwarded-for"] || // TODO This header may contain multiple IP addresses. Which should we report? - req?.connection?.remoteAddress || - req?.socket?.remoteAddress || - req?.connection?.socket.remoteAddress; - - let isSpamPromise = isSpam({ - comment_content: txt, - comment_author: uid, - permalink: "https://pol.is/" + zid, - user_ip: ip, - user_agent: req?.headers?.["user-agent"], - referrer: req?.headers?.referer, - }); - isSpamPromise.catch(function (err: any) { - logger.error("isSpam failed", err); - }); - // let isSpamPromise = Promise.resolve(false); - let isModeratorPromise = isModerator(zid, uid); + const commentExistsPromise = commentExists(zid!, txt); - let conversationInfoPromise = getConversationInfo(zid); + const [ + finalPid, + conv, + is_moderator, + commentExistsAlready, + spammy, + ] = await Promise.all([ + pidPromise, + conversationInfoPromise, + isModeratorPromise, + commentExistsPromise, + isSpamPromise, + ]); - // return xidUserPromise.then(function(xidUser) { + if (!is_moderator && mustBeModerator) { + fail(res, 403, "polis_err_post_comment_auth"); + return; + } - let shouldCreateXidRecord = false; + if (finalPid < 0) { + fail(res, 500, "polis_err_post_comment_bad_pid"); + return; + } - let pidPromise; - if (ptpt) { - pidPromise = Promise.resolve(ptpt.pid); - } else { - let xidUserPromise = - !_.isUndefined(xid) && !_.isNull(xid) - ? getXidStuff(xid, zid) - : Promise.resolve(); - pidPromise = xidUserPromise.then( - (xidUser: UserType | "noXidRecord") => { - shouldCreateXidRecord = xidUser === "noXidRecord"; - if (typeof xidUser === "object") { - uid = xidUser.uid; - pid = xidUser.pid; - return pid; - } else { - return doGetPid().then((pid: any) => { - if (shouldCreateXidRecord) { - // Expected 6 arguments, but got 3.ts(2554) - // conversation.ts(34, 3): An argument for 'x_profile_image_url' was not provided. - // @ts-ignore - return createXidRecordByZid(zid, uid, xid).then(() => { - return pid; - }); - } - return pid; - }); - } - } - ); - } + if (commentExistsAlready) { + fail(res, 409, "polis_err_post_comment_duplicate"); + return; + } - let commentExistsPromise = commentExists(zid, txt); + if (!conv.is_active) { + fail(res, 403, "polis_err_conversation_is_closed"); + return; + } - return Promise.all([ - pidPromise, - conversationInfoPromise, - isModeratorPromise, - commentExistsPromise, - ]).then( - function (results: any[]) { - let pid = results[0]; - let conv = results[1]; - let is_moderator = results[2]; - let commentExists = results[3]; - - if (!is_moderator && mustBeModerator) { - fail(res, 403, "polis_err_post_comment_auth"); - return; - } + const bad = hasBadWords(txt); - if (pid < 0) { - // NOTE: this API should not be called in /demo mode - fail(res, 500, "polis_err_post_comment_bad_pid"); - return; - } + const velocity = 1; + let active = true; + const classifications = []; - if (commentExists) { - fail(res, 409, "polis_err_post_comment_duplicate"); - return; - } + if (bad && conv.profanity_filter) { + active = false; + classifications.push("bad"); + logger.info("active=false because (bad && conv.profanity_filter)"); + } + if (spammy && conv.spam_filter) { + active = false; + classifications.push("spammy"); + logger.info("active=false because (spammy && conv.spam_filter)"); + } + if (conv.strict_moderation) { + active = false; + logger.info("active=false because (conv.strict_moderation)"); + } - if (!conv.is_active) { - fail(res, 403, "polis_err_conversation_is_closed"); - return; - } + let mod = 0; + if (is_moderator && is_seed) { + mod = polisTypes.mod.ok; + active = true; + } - if (_.isUndefined(txt)) { - logger.error("polis_err_post_comments_missing_txt"); - throw "polis_err_post_comments_missing_txt"; - } - let bad = hasBadWords(txt); + const [detections] = await Promise.all([detectLanguage(txt)]); + const detection = Array.isArray(detections) ? detections[0] : detections; + const lang = detection.language; + const lang_confidence = detection.confidence; - return isSpamPromise - .then( - function (spammy: any) { - return spammy; - }, - function (err: any) { - logger.error("spam check failed", err); - return false; // spam check failed, continue assuming "not spammy". - } - ) - .then(function (spammy: any) { - let velocity = 1; - let active = true; - let classifications = []; - if (bad && conv.profanity_filter) { - active = false; - classifications.push("bad"); - logger.info( - "active=false because (bad && conv.profanity_filter)" - ); - } - if (spammy && conv.spam_filter) { - active = false; - classifications.push("spammy"); - logger.info( - "active=false because (spammy && conv.spam_filter)" - ); - } - if (conv.strict_moderation) { - active = false; - logger.info( - "active=false because (conv.strict_moderation)" - ); - } + const insertedComment = await pgQueryP( + `INSERT INTO COMMENTS + (pid, zid, txt, velocity, active, mod, uid, anon, is_seed, created, tid, lang, lang_confidence) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, default, null, $10, $11) + RETURNING *;`, + [ + finalPid, + zid, + txt, + velocity, + active, + mod, + uid, + anon || false, + is_seed || false, + lang, + lang_confidence, + ] + ); - let mod = 0; // hasn't yet been moderated. + const comment = insertedComment[0]; + const tid = comment.tid; - // moderators' comments are automatically in (when prepopulating). - if (is_moderator && is_seed) { - mod = polisTypes.mod.ok; - active = true; - } - let authorUid = ptpt ? ptpt.uid : uid; - - Promise.all([detectLanguage(txt)]).then((a: any[]) => { - let detections = a[0]; - let detection = Array.isArray(detections) - ? detections[0] - : detections; - let lang = detection.language; - let lang_confidence = detection.confidence; - - return pgQueryP( - "INSERT INTO COMMENTS " + - "(pid, zid, txt, velocity, active, mod, uid, tweet_id, quote_src_url, anon, is_seed, created, tid, lang, lang_confidence) VALUES " + - "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, default, null, $12, $13) RETURNING *;", - [ - pid, - zid, - txt, - velocity, - active, - mod, - authorUid, - twitter_tweet_id || null, - quote_src_url || null, - anon || false, - is_seed || false, - lang, - lang_confidence, - ] - ).then( - // Argument of type '(docs: any[]) => any' is not assignable to parameter of type '(value: unknown) => any'. - // Types of parameters 'docs' and 'value' are incompatible. - // Type 'unknown' is not assignable to type 'any[]'.ts(2345) - // @ts-ignore - function (docs: any[]) { - let comment = docs && docs[0]; - let tid = comment && comment.tid; - // let createdTime = comment && comment.created; - - if (bad || spammy || conv.strict_moderation) { - getNumberOfCommentsWithModerationStatus( - zid, - polisTypes.mod.unmoderated - ) - .catch(function (err: any) { - logger.error( - "polis_err_getting_modstatus_comment_count", - err - ); - return void 0; - }) - .then(function (n: number) { - if (n === 0) { - return; - } - pgQueryP_readOnly( - "select * from users where site_id = (select site_id from page_ids where zid = ($1)) UNION select * from users where uid = ($2);", - [zid, conv.owner] - ).then(function (users: any) { - let uids = _.pluck(users, "uid"); - // also notify polis team for moderation - uids.forEach(function (uid?: any) { - sendCommentModerationEmail(req, uid, zid, n); - }); - }); - }); - } else { - addNotificationTask(zid); - } + if (bad || spammy || conv.strict_moderation) { + try { + const n = await getNumberOfCommentsWithModerationStatus( + zid!, + polisTypes.mod.unmoderated + ); + if (n !== 0) { + const users = await pgQueryP_readOnly( + "SELECT * FROM users WHERE site_id = (SELECT site_id FROM page_ids WHERE zid = $1) UNION SELECT * FROM users WHERE uid = $2;", + [zid, conv.owner] + ); + const uids = users.map((user: { uid: string }) => user.uid); + uids.forEach((uid: string) => + sendCommentModerationEmail(req, uid, zid!, n) + ); + } + } catch (err) { + logger.error("polis_err_getting_modstatus_comment_count", err); + } + } else { + addNotificationTask(zid!); + } - // It should be safe to delete this. Was added to postpone the no-auto-vote change for old conversations. - if (is_seed && _.isUndefined(vote) && zid <= 17037) { - vote = 0; - } + if (is_seed && _.isUndefined(vote) && Number(zid) <= 17037) { + vote = 0; + } - let createdTime = comment.created; - let votePromise = _.isUndefined(vote) - ? Promise.resolve() - : votesPost(uid, pid, zid, tid, xid, vote, 0); - - return ( - votePromise - // This expression is not callable. - //Each member of the union type '{ (onFulfill?: ((value: void) => Resolvable) | undefined, onReject?: ((error: any) => Resolvable) | undefined): Bluebird; (onfulfilled?: ((value: void) => Resolvable<...>) | ... 1 more ... | undefined, onrejected?: ((reason: any) => Resolvable<...>) | ... 1 more ... | u...' has signatures, but none of those signatures are compatible with each other.ts(2349) - // @ts-ignore - .then( - function (o: { vote: { created: any } }) { - if (o && o.vote && o.vote.created) { - createdTime = o.vote.created; - } - - setTimeout(function () { - updateConversationModifiedTime( - zid, - createdTime - ); - updateLastInteractionTimeForConversation( - zid, - uid - ); - if (!_.isUndefined(vote)) { - updateVoteCount(zid, pid); - } - }, 100); - - res.json({ - tid: tid, - currentPid: currentPid, - }); - }, - function (err: any) { - fail(res, 500, "polis_err_vote_on_create", err); - } - ) - ); - }, - function (err: { code: string | number }) { - if (err.code === "23505" || err.code === 23505) { - // duplicate comment - fail( - res, - 409, - "polis_err_post_comment_duplicate", - err - ); - } else { - fail(res, 500, "polis_err_post_comment", err); - } - } - ); // insert - }); // lang - }); - }, - function (errors: any[]) { - if (errors[0]) { - fail(res, 500, "polis_err_getting_pid", errors[0]); - return; - } - if (errors[1]) { - fail(res, 500, "polis_err_getting_conv_info", errors[1]); - return; - } - } + let createdTime = comment.created; + + if (!_.isUndefined(vote)) { + try { + const voteResult = await votesPost( + uid!, + finalPid, + zid!, + tid, + xid!, + vote, + 0 ); - }, - function (err: any) { - fail(res, 500, "polis_err_fetching_tweet", err); + if (voteResult?.vote?.created) { + createdTime = voteResult.vote.created; + } + } catch (err) { + fail(res, 500, "polis_err_vote_on_create", err); + return; } - ) - .catch(function (err: any) { - fail(res, 500, "polis_err_post_comment_misc", err); + } + + setTimeout(() => { + updateConversationModifiedTime(zid!, createdTime); + updateLastInteractionTimeForConversation(zid!, uid!); + if (!_.isUndefined(vote)) { + updateVoteCount(zid!, finalPid); + } + }, 100); + + res.json({ + tid: tid, + currentPid: currentPid, }); - } // end POST /api/v3/comments + } catch (err: any) { + if (err.code === "23505" || err.code === 23505) { + fail(res, 409, "polis_err_post_comment_duplicate", err); + } else { + fail(res, 500, "polis_err_post_comment", err); + } + } + } function handle_GET_votes_me( req: { p: { zid: any; uid?: any; pid: any } }, @@ -10943,17 +10792,18 @@ Thanks for using Polis! // * create a twitter record } - function addParticipant(zid: any, uid?: any) { - return pgQueryP( + const addParticipant = async (zid: string, uid?: string): Promise => { + await pgQueryP( "INSERT INTO participants_extended (zid, uid) VALUES ($1, $2);", [zid, uid] - ).then(() => { - return pgQueryP( - "INSERT INTO participants (pid, zid, uid, created) VALUES (NULL, $1, $2, default) RETURNING *;", - [zid, uid] - ); - }); - } + ); + + return pgQueryP( + "INSERT INTO participants (pid, zid, uid, created) VALUES (NULL, $1, $2, default) RETURNING *;", + [zid, uid] + ); + }; + function getAndInsertTwitterUser(o: any, uid?: any) { return getTwitterUserInfo(o, false).then(function (userString: string) { const u: UserType = JSON.parse(userString)[0]; From 893178a62de20baf2316e9274c137d336c6bbae4 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Tue, 20 Aug 2024 18:42:32 -0700 Subject: [PATCH 11/27] perspective call --- server/src/server.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/server/src/server.ts b/server/src/server.ts index 0bb2970c0c..73f125a8c4 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -7020,6 +7020,34 @@ Email verified! You can close this tab or hit the back button. }); } + const GOOGLE_DISCOVERY_URL = + "https://commentanalyzer.googleapis.com/$discovery/rest?version=v1alpha1"; + + async function analyzeComment(txt: string) { + try { + const client = await google.discoverAPI(GOOGLE_DISCOVERY_URL); + + const analyzeRequest = { + comment: { + text: txt, + }, + requestedAttributes: { + TOXICITY: {}, + INCOHERENT: {}, + }, + }; + + const response = await client.comments.analyze({ + key: Config.googleJigsawPerspectiveApiKey, + resource: analyzeRequest, + }); + + console.log(JSON.stringify(response.data, null, 2)); + } catch (err) { + console.error("Error:", err); + } + } + interface RequestParams { zid?: string; xid?: string; @@ -7094,6 +7122,8 @@ Email verified! You can close this tab or hit the back button. return false; }); + const jigsawModerationPromise = analyzeComment(txt); + const isModeratorPromise = isModerator(zid!, uid!); const conversationInfoPromise = getConversationInfo(zid!); @@ -7124,14 +7154,18 @@ Email verified! You can close this tab or hit the back button. is_moderator, commentExistsAlready, spammy, + jigsawResponse, ] = await Promise.all([ pidPromise, conversationInfoPromise, isModeratorPromise, commentExistsPromise, isSpamPromise, + jigsawModerationPromise, ]); + console.log("JIGSAW RESPONSE", jigsawResponse); + if (!is_moderator && mustBeModerator) { fail(res, 403, "polis_err_post_comment_auth"); return; From 7e8c49f706c5207e1e642ff2960f9622a1f4ce7b Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Sun, 25 Aug 2024 15:52:15 -0700 Subject: [PATCH 12/27] jigsaw toxicity threshold under flag. --- server/src/server.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index 73f125a8c4..4adff278d0 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -7033,7 +7033,6 @@ Email verified! You can close this tab or hit the back button. }, requestedAttributes: { TOXICITY: {}, - INCOHERENT: {}, }, }; @@ -7042,13 +7041,14 @@ Email verified! You can close this tab or hit the back button. resource: analyzeRequest, }); - console.log(JSON.stringify(response.data, null, 2)); + return response.data; } catch (err) { console.error("Error:", err); } } - interface RequestParams { + /* this is a concept and can be generalized to other handlers */ + interface PolisRequestParams { zid?: string; xid?: string; uid?: string; @@ -7059,12 +7059,12 @@ Email verified! You can close this tab or hit the back button. is_seed?: boolean; } - interface CustomRequest extends Request { - p: RequestParams; + interface PolisRequest extends Request { + p: PolisRequestParams; } async function handle_POST_comments( - req: CustomRequest, + req: PolisRequest, res: Response ): Promise { const { zid, xid, uid, txt, pid: initialPid, vote, anon, is_seed } = req.p; @@ -7164,8 +7164,6 @@ Email verified! You can close this tab or hit the back button. jigsawModerationPromise, ]); - console.log("JIGSAW RESPONSE", jigsawResponse); - if (!is_moderator && mustBeModerator) { fail(res, 403, "polis_err_post_comment_auth"); return; @@ -7189,10 +7187,20 @@ Email verified! You can close this tab or hit the back button. const bad = hasBadWords(txt); const velocity = 1; + const jigsawToxicityThreshold = 0.8; let active = true; const classifications = []; - if (bad && conv.profanity_filter) { + console.log("JIGSAW RESPONSE", txt); + console.log( + `Jigsaw toxicty Score for comment "${txt}": ${jigsawResponse?.attributeScores?.TOXICITY?.summaryScore?.value}` + ); + + if ( + conv.profanity_filter && + jigsawResponse?.attributeScores?.TOXICITY?.summaryScore?.value > + jigsawToxicityThreshold + ) { active = false; classifications.push("bad"); logger.info("active=false because (bad && conv.profanity_filter)"); From c39e1265134f0ff31a86453a31215f7ce4206730 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Mon, 26 Aug 2024 18:01:54 -0700 Subject: [PATCH 13/27] text flag for toxic --- .../components/conversation-admin/comment-moderation/comment.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client-admin/src/components/conversation-admin/comment-moderation/comment.js b/client-admin/src/components/conversation-admin/comment-moderation/comment.js index af51b377d5..953a2d290b 100644 --- a/client-admin/src/components/conversation-admin/comment-moderation/comment.js +++ b/client-admin/src/components/conversation-admin/comment-moderation/comment.js @@ -27,6 +27,7 @@ class Comment extends React.Component { return ( + {this.props.comment.active ? null : 'Comment flagged as toxic. Comment not active / not shown in conversation. Human moderation action will override this flag.'} {this.props.comment.txt} Date: Wed, 28 Aug 2024 11:28:31 -0700 Subject: [PATCH 14/27] add perspective to privacy policy --- client-admin/src/content/privacy.md | 1 + 1 file changed, 1 insertion(+) diff --git a/client-admin/src/content/privacy.md b/client-admin/src/content/privacy.md index 80ae289cc3..057478c19a 100644 --- a/client-admin/src/content/privacy.md +++ b/client-admin/src/content/privacy.md @@ -100,6 +100,7 @@ Conversation participants may opt to use third party data collector and processo * We use Logentries for server logs, which may include personal information such as IP address necessary for debugging production issues. Information about how Logentries uses this data when you use our Site can be found at: * We use Amazon Web Services Simple Email Service for sending account confirmation and notification emails to users. We also use AWS Simple Storage Service for. Information about how AWS handles: * We use Google Translate for automatic machine translation of comments, and Google Analytics for site usage statistics. Information about how Google uses this data when you use our Site can be found at: +* We use Jigsaw Perspective API for comment moderation. This service analyzes the content of comments to detect potentially toxic or inappropriate language. Information about how Perspective API handles data can be found at: * We pass all Site web requests through a Cloudflare caching proxy. No personal information is cached at this level. Information about how Cloudflare uses your data can be found at: ## Public Areas and Syndicated Services From 8d9f8b1129fa17f08004bde5e86f1cf10d618948 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Wed, 28 Aug 2024 11:32:05 -0700 Subject: [PATCH 15/27] jigsaw TOS --- client-admin/src/content/tos.md | 1 + 1 file changed, 1 insertion(+) diff --git a/client-admin/src/content/tos.md b/client-admin/src/content/tos.md index cab7a6ef06..f4546a0b03 100644 --- a/client-admin/src/content/tos.md +++ b/client-admin/src/content/tos.md @@ -52,6 +52,7 @@ These Terms of Use are a legally binding contract between you and The Computatio 5. **User Content Disclaimer.**   We are under no obligation to edit or control User Content that you or other users post or publish, and will not be in any way responsible or liable for User Content. The Computational Democracy Project may, however, at any time and without prior notice, screen, remove, edit, or block any User Content that in our sole judgment violates these Terms or is otherwise objectionable. + We may use automated systems, including but not limited to the Jigsaw Perspective API, to moderate User Content for potentially toxic or inappropriate language. You understand that when using the Service you will be exposed to User Content from a variety of sources and acknowledge that User Content may be inaccurate, offensive, indecent or objectionable. You agree to waive, and hereby do waive, any legal or equitable rights or remedies you have or may have against The Computational Democracy Project with respect to User Content. We expressly disclaim any and all liability in connection with User Content. From 89fdcfbc400875a37c81efc2caa593ce0ed5c683 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Thu, 29 Aug 2024 09:36:58 -0700 Subject: [PATCH 16/27] Better type for getPca. Also removed promotion of error to the return value. Let the error propagate as an error. --- server/src/server.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index 4adff278d0..5b41a0b781 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1297,8 +1297,15 @@ function initializePolisHelpers() { } res.status(200).json({}); } + + type PcaCacheItem = { + asPOJO: any; + asJSON: string; + asBufferOfGzippedJson: any; + expiration: number; + } let pcaCacheSize = Config.cacheMathResults ? 300 : 1; - let pcaCache = new LruCache({ + let pcaCache = new LruCache({ max: pcaCacheSize, }); @@ -1560,12 +1567,12 @@ function initializePolisHelpers() { return o; } - function getPca(zid?: any, math_tick?: number) { + function getPca(zid?: any, math_tick?: number) :Promise { let cached = pcaCache.get(zid); // Object is of type 'unknown'.ts(2571) // @ts-ignore if (cached && cached.expiration < Date.now()) { - cached = null; + cached = undefined; } // Object is of type 'unknown'.ts(2571) // @ts-ignore @@ -1577,7 +1584,7 @@ function initializePolisHelpers() { cached_math_tick: cachedPOJO.math_tick, query_math_tick: math_tick, }); - return Promise.resolve(null); + return Promise.resolve(undefined); } else { logger.info("math from cache", { zid, math_tick }); return Promise.resolve(cached); @@ -1614,7 +1621,7 @@ function initializePolisHelpers() { math_env: Config.mathEnv, } ); - return null; + return undefined; } let item = rows[0].data; @@ -1627,7 +1634,7 @@ function initializePolisHelpers() { zid, math_tick, }); - return null; + return undefined; } logger.info("after cache miss, found item, adding to cache", { zid, @@ -1636,18 +1643,11 @@ function initializePolisHelpers() { processMathObject(item); - return updatePcaCache(zid, item).then( - function (o: any) { - return o; - }, - function (err: any) { - return err; - } - ); + return updatePcaCache(zid, item); }); } - function updatePcaCache(zid: any, item: { zid: any }) { + function updatePcaCache(zid: any, item: { zid: any }) :Promise { return new Promise(function ( resolve: (arg0: { asPOJO: any; From de1816573bf62afa3158a57f34d317c99559614a Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Thu, 29 Aug 2024 09:37:45 -0700 Subject: [PATCH 17/27] Tell TypeScript to use a less ancient library target. --- server/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/tsconfig.json b/server/tsconfig.json index c38392e11b..5e35cc18d6 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -4,7 +4,7 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "target": "es2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, // "lib": [], /* Specify library files to be included in the compilation. */ "allowJs": true /* Allow javascript files to be compiled. */, From 63a1ffe39de307a1abb0e72ec7b06967eb2c371c Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 30 Aug 2024 15:15:13 -0700 Subject: [PATCH 18/27] Trim trailing whitespace. --- server/src/server.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index 5b41a0b781..6f4c1c945f 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -7227,9 +7227,9 @@ Email verified! You can close this tab or hit the back button. const lang_confidence = detection.confidence; const insertedComment = await pgQueryP( - `INSERT INTO COMMENTS - (pid, zid, txt, velocity, active, mod, uid, anon, is_seed, created, tid, lang, lang_confidence) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, default, null, $10, $11) + `INSERT INTO COMMENTS + (pid, zid, txt, velocity, active, mod, uid, anon, is_seed, created, tid, lang, lang_confidence) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, default, null, $10, $11) RETURNING *;`, [ finalPid, From b317fbaf0cfc9a2ec03a2fa2e65715421c242e35 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 30 Aug 2024 15:15:31 -0700 Subject: [PATCH 19/27] Add data export endpoint. This is a simple first pass which just reads the data from the database and delivers it. No caching, no fancy business. The endpoints are based on the report identifier and provide three separate .csv exports. For example: /api/v3/reportExport/r6ke7cdzte2jrsxctfyt9/summary.csv /api/v3/reportExport/r6ke7cdzte2jrsxctfyt9/comments.csv /api/v3/reportExport/r6ke7cdzte2jrsxctfyt9/votes.csv The format is made to match that of the old command line exporter as much as possible. --- server/app.ts | 14 +++++ server/src/server.ts | 138 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/server/app.ts b/server/app.ts index 765f31407b..52bd8c3b82 100644 --- a/server/app.ts +++ b/server/app.ts @@ -84,6 +84,7 @@ helpersInitialized.then( handle_GET_math_correlationMatrix, handle_GET_dataExport, handle_GET_dataExport_results, + handle_GET_reportExport, handle_GET_domainWhitelist, handle_GET_dummyButton, handle_GET_einvites, @@ -300,6 +301,19 @@ helpersInitialized.then( handle_GET_dataExport ); + app.get( + "/api/v3/reportExport/:report_id/:report_type", + moveToBody, + need( + "report_id", + getReportIdFetchRid, + assignToPCustom("rid") + ), + need("report_id", getStringLimitLength(1, 1000), assignToP), + need("report_type", getStringLimitLength(1, 1000), assignToP), + handle_GET_reportExport + ); + app.get( "/api/v3/dataExport/results", moveToBody, diff --git a/server/src/server.ts b/server/src/server.ts index 6f4c1c945f..750a21edf4 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -2072,6 +2072,143 @@ function initializePolisHelpers() { // }); // return res.end(); } + + async function handle_GET_reportExport( + req: { + p: { rid: string, report_type: string }, + headers: { host: string, "x-forwarded-proto": string } + }, + res: { send: (data :string) => void } + ) { + function formatCSV(colFns :Record string>, rows: object[]) :string { + const fns = Object.values(colFns); + const sep = "\n"; + let csv = Object.keys(colFns).join(",") + sep; + if (rows.length > 0) { + for (const row of rows) { + // we append to a single string here (instead of creating an array of strings and joining + // them) to reduce the amount of garbage created; we may have millions of rows, I wish we + // could stream directly to the response... + for (let ii = 0; ii < fns.length; ii += 1) { + if (ii > 0) csv += ","; + csv += fns[ii](row); + } + csv += sep + } + } + return csv; + } + + async function loadConversationSummary (zid :number) { + const [zinvite, convoRows, commentersRow, pca] = await Promise.all([ + getZinvite(zid), + pgQueryP_readOnly( + `SELECT topic, description FROM conversations WHERE zid = $1`, [zid] + ), + pgQueryP_readOnly( + `SELECT COUNT(DISTINCT pid) FROM comments WHERE zid = $1`, [zid] + ), + getPca(zid) + ]); + if (!zinvite || !convoRows || !commentersRow || !pca) { + throw new Error("polis_error_data_unknown_report"); + } + + const convo = (convoRows as {topic: string, description: string}[])[0]; + const commenters = (commentersRow as { count: number }[])[0].count; + + type PcaData = { + "in-conv": number[], + "user-vote-counts": Record, + "group-clusters": Record, + "n-cmts": number, + } + const data = pca.asPOJO as PcaData + const siteUrl = `${req.headers["x-forwarded-proto"]}://${req.headers.host}` + + const escapeQuotes = (s: string) => s.replace(/"/g, "\"\""); + return [ + [ "topic", `"${escapeQuotes(convo.topic)}"` ], + [ "url", `${siteUrl}/${zinvite}` ], + [ "voters", Object.keys(data["user-vote-counts"]).length ], + [ "voters-in-conv", data["in-conv"].length ], + [ "commenters", commenters ], + [ "comments", data["n-cmts"] ], + [ "groups", Object.keys(data["group-clusters"]).length ], + [ "conversation-description", `"${escapeQuotes(convo.description)}"` ], + ].map(row => row.join(",")); + } + + const loadCommentSummary = (zid: number) => pgQueryP_readOnly( + `SELECT + created, + tid, + pid, + COALESCE((SELECT count(*) FROM votes WHERE votes.tid = comments.tid AND vote = 1), 0) as agrees, + COALESCE((SELECT count(*) FROM votes WHERE votes.tid = comments.tid AND vote = -1), 0) as disagrees, + mod, + txt + FROM comments + WHERE zid = $1`, + [zid]); + + const loadVotes = (zid: number) => pgQueryP_readOnly( + `SELECT created as timestamp, tid, pid, vote FROM votes WHERE zid = $1 order by tid, pid`, + [zid]); + + const formatDatetime = (timestamp: string) => new Date(parseInt(timestamp)).toString(); + + const { rid, report_type } = req.p; + try { + const zid = await getZidForRid(rid); + if (!zid) { + fail(res, 404, "polis_error_data_unknown_report"); + return; + } + + switch (report_type) { + case "summary.csv": + res.send((await loadConversationSummary(zid)).join("\n")); + break; + + case "comments.csv": + const rows = await loadCommentSummary(zid) as object[] | undefined; + if (rows) res.send(formatCSV({ + "timestamp": (row) => String(Math.floor(row.timestamp/1000)), + "datetime": (row) => formatDatetime(row.timestamp), + "comment-id": (row) => String(row.tid), + "author-id": (row) => String(row.pid), + agrees: (row) => String(row.agrees), + disagrees: (row) => String(row.disagrees), + moderated: (row) => String(row.mod), + "comment-body": (row) => String(row.txt), + }, rows)); + else fail(res, 500, "polis_err_data_export"); + break; + + case "votes.csv": + const votes = await loadVotes(zid) as object[] | undefined; + if (votes) res.send(formatCSV({ + timestamp: (row) => String(Math.floor(row.timestamp/1000)), + datetime: (row) => formatDatetime(row.timestamp), + "comment-id": (row) => String(row.tid), + "voter-id": (row) => String(row.pid), + vote: (row) => String(row.vote), + }, votes)); + else fail(res, 500, "polis_err_data_export"); + break; + + default: + fail(res, 404, "polis_error_data_unknown_report"); + break; + } + } catch (err) { + const msg = err instanceof Error && err.message && err.message.startsWith("polis_") ? + err.message : "polis_err_data_export"; + fail(res, 500, msg, err); + } + } + function getBidIndexToPidMapping(zid: number, math_tick: number) { math_tick = math_tick || -1; return pgQueryP_readOnly( @@ -13867,6 +14004,7 @@ Thanks for using Polis! handle_GET_math_correlationMatrix, handle_GET_dataExport, handle_GET_dataExport_results, + handle_GET_reportExport, handle_GET_domainWhitelist, handle_GET_dummyButton, handle_GET_einvites, From 6c3ef93fda451dddc101f7e46b2a4df086e2e5aa Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Fri, 30 Aug 2024 17:40:49 -0700 Subject: [PATCH 20/27] smaller font size --- .../components/conversation-admin/comment-moderation/comment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-admin/src/components/conversation-admin/comment-moderation/comment.js b/client-admin/src/components/conversation-admin/comment-moderation/comment.js index 953a2d290b..abafbbb8fd 100644 --- a/client-admin/src/components/conversation-admin/comment-moderation/comment.js +++ b/client-admin/src/components/conversation-admin/comment-moderation/comment.js @@ -27,7 +27,7 @@ class Comment extends React.Component { return ( - {this.props.comment.active ? null : 'Comment flagged as toxic. Comment not active / not shown in conversation. Human moderation action will override this flag.'} + {this.props.comment.active ? null : 'Comment flagged as toxic by Jigsaw Perspective API (value > .8). Comment not active / not shown in conversation. Human moderation action will override this flag.'} {this.props.comment.txt} Date: Fri, 30 Aug 2024 17:56:03 -0700 Subject: [PATCH 21/27] Use the correct column name here. --- server/src/server.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index 750a21edf4..ab69b5ba01 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -2173,9 +2173,10 @@ function initializePolisHelpers() { case "comments.csv": const rows = await loadCommentSummary(zid) as object[] | undefined; + console.log(rows) if (rows) res.send(formatCSV({ - "timestamp": (row) => String(Math.floor(row.timestamp/1000)), - "datetime": (row) => formatDatetime(row.timestamp), + "timestamp": (row) => String(Math.floor(row.created/1000)), + "datetime": (row) => formatDatetime(row.created), "comment-id": (row) => String(row.tid), "author-id": (row) => String(row.pid), agrees: (row) => String(row.agrees), From 6a9ee2797b71e57dafe1ed2759d0f90a2334d879 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Sat, 31 Aug 2024 14:04:05 -0700 Subject: [PATCH 22/27] Set text/csv content type on responses. --- server/src/server.ts | 45 ++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index ab69b5ba01..fe4148d5c9 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -2078,7 +2078,7 @@ function initializePolisHelpers() { p: { rid: string, report_type: string }, headers: { host: string, "x-forwarded-proto": string } }, - res: { send: (data :string) => void } + res: { send: (data :string) => void, setHeader: (key: string, value: string) => void } ) { function formatCSV(colFns :Record string>, rows: object[]) :string { const fns = Object.values(colFns); @@ -2168,35 +2168,40 @@ function initializePolisHelpers() { switch (report_type) { case "summary.csv": + res.setHeader('content-type', 'text/csv'); res.send((await loadConversationSummary(zid)).join("\n")); break; case "comments.csv": const rows = await loadCommentSummary(zid) as object[] | undefined; console.log(rows) - if (rows) res.send(formatCSV({ - "timestamp": (row) => String(Math.floor(row.created/1000)), - "datetime": (row) => formatDatetime(row.created), - "comment-id": (row) => String(row.tid), - "author-id": (row) => String(row.pid), - agrees: (row) => String(row.agrees), - disagrees: (row) => String(row.disagrees), - moderated: (row) => String(row.mod), - "comment-body": (row) => String(row.txt), - }, rows)); - else fail(res, 500, "polis_err_data_export"); + if (rows) { + res.setHeader('content-type', 'text/csv'); + res.send(formatCSV({ + "timestamp": (row) => String(Math.floor(row.created/1000)), + "datetime": (row) => formatDatetime(row.created), + "comment-id": (row) => String(row.tid), + "author-id": (row) => String(row.pid), + agrees: (row) => String(row.agrees), + disagrees: (row) => String(row.disagrees), + moderated: (row) => String(row.mod), + "comment-body": (row) => String(row.txt), + }, rows)); + } else fail(res, 500, "polis_err_data_export"); break; case "votes.csv": const votes = await loadVotes(zid) as object[] | undefined; - if (votes) res.send(formatCSV({ - timestamp: (row) => String(Math.floor(row.timestamp/1000)), - datetime: (row) => formatDatetime(row.timestamp), - "comment-id": (row) => String(row.tid), - "voter-id": (row) => String(row.pid), - vote: (row) => String(row.vote), - }, votes)); - else fail(res, 500, "polis_err_data_export"); + if (votes) { + res.setHeader('content-type', 'text/csv'); + res.send(formatCSV({ + timestamp: (row) => String(Math.floor(row.timestamp/1000)), + datetime: (row) => formatDatetime(row.timestamp), + "comment-id": (row) => String(row.tid), + "voter-id": (row) => String(row.pid), + vote: (row) => String(row.vote), + }, votes)); + } else fail(res, 500, "polis_err_data_export"); break; default: From 27d703353f9848b717fd0eb8824f1db6a4d91fc8 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Thu, 5 Sep 2024 19:04:36 -0700 Subject: [PATCH 23/27] export fonts from globals --- client-report/src/components/globals.js | 32 +++++++++++-------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/client-report/src/components/globals.js b/client-report/src/components/globals.js index d9cb881009..c5effedcf6 100644 --- a/client-report/src/components/globals.js +++ b/client-report/src/components/globals.js @@ -21,17 +21,16 @@ export const brandColors = { export const allCommentsSortDefault = "tid"; - -const fontSizes = { +export const fontSizes = { largest: 36, large: 24, medium: 18, -} +}; const fontWeights = { boldest: 700, - normal: 400 -} + normal: 400, +}; export const primaryHeading = { fontSize: fontSizes.largest, @@ -40,49 +39,46 @@ export const primaryHeading = { export const secondaryHeading = { fontSize: fontSizes.large, - fontWeight: fontWeights.normal -} + fontWeight: fontWeights.normal, +}; export const groupHeader = { fontSize: fontSizes.largest, fontWeight: fontWeights.boldest, -} +}; export const overviewNumber = { fontSize: fontSizes.largest, fontWeight: fontWeights.normal, marginBottom: 0, -} +}; export const overviewLabel = { fontSize: fontSizes.medium, fontWeight: fontWeights.normal, marginTop: 0, - maxWidth: 190 -} + maxWidth: 190, +}; export const tidGrey = "rgb(200,200,200)"; export const paragraph = { width: paragraphWidth, fontFamily: serif, lineHeight: paragraphLineHeight, -} - +}; // Duplicated in: // polisClientParticipation/vis2/components/globals.js // polisReport/src/components/globals.js -export const groupLabels = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"] -export const groupSymbols = ["○", "◆", "+", "-", "◬", "▮", ] - +export const groupLabels = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]; +export const groupSymbols = ["○", "◆", "+", "-", "◬", "▮"]; export const enableMatrix = false; // export const maxCommentExtremityToShow = 2; /* naive & may not be solid. should be dynamically generated from extremity array probably to pick top 20 or something */ -export const maxCommentExtremityToShow = .6; /* naive & may not be solid. should be dynamically generated from extremity array probably to pick top 20 or something */ +export const maxCommentExtremityToShow = 0.6; /* naive & may not be solid. should be dynamically generated from extremity array probably to pick top 20 or something */ export const labelPadding = 40; export const shouldColorizeTidsByRepfulness = true; - // ====== REMEMBER: gid's start at zero, (0, 1, 2) but we show them as group 1, 2, 3 ======= export const groupColor = (gid) => { From 2e34aeae31ae9c20e1dd6584fe7fccf4cadee97d Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Thu, 5 Sep 2024 19:04:51 -0700 Subject: [PATCH 24/27] data export info in report view --- client-report/src/components/overview.js | 200 ++++++++++++++++++----- 1 file changed, 159 insertions(+), 41 deletions(-) diff --git a/client-report/src/components/overview.js b/client-report/src/components/overview.js index 160fd9ebbb..0fda40a318 100644 --- a/client-report/src/components/overview.js +++ b/client-report/src/components/overview.js @@ -8,26 +8,39 @@ const computeVoteTotal = (users) => { let voteTotal = 0; _.each(users, (count) => { - voteTotal += count + voteTotal += count; }); return voteTotal; -} +}; // const computeUniqueCommenters = (comments) => { // } -const Number = ({number, label}) => ( -
-

- {number.toLocaleString()} -

-

- {label} -

+const Number = ({ number, label }) => ( +
+

{number.toLocaleString()}

+

{label}

-) +); + +const pathname = window.location.pathname; // "/report/2arcefpshi" +const report_id = pathname.split("/")[2]; + +const getCurrentTimestamp = () => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + const hours = String(now.getHours()).padStart(2, "0"); + const minutes = String(now.getMinutes()).padStart(2, "0"); + return `${year}-${month}-${day}-${hours}${minutes}`; +}; + +const getDownloadFilename = (file, conversation) => { + return `${getCurrentTimestamp()}-${conversation.conversation_id}-${file}.csv`; +}; const Overview = ({ conversation, @@ -40,40 +53,146 @@ const Overview = ({ computedStats, }) => { return ( -
-

Overview

-

- Pol.is is a real-time survey system that helps identify the different ways a large group of people think about a divisive or complicated topic. Here’s a basic breakdown of some terms you’ll need to know in order to understand this report. -

-

- Participants: These are the people who participated in the conversation by voting and writing statements. Based on how they voted, each participant is sorted into an opinion group. -

-

- Statements: Participants may submit statements for other participants to vote on. Statements are assigned a number in the order they’re submitted. -

-

- Opinion groups: Groups are made of participants who voted similarly to each other, and differently from the other groups. -

- -

- {conversation && conversation.ownername ? "This pol.is conversation was run by "+conversation.ownername+". " : null} - {conversation && conversation.topic ? "The topic was '"+conversation.topic+"'. " : null} -

-
+
+
+
+

Overview

+

+ Pol.is is a real-time survey system that helps identify the different ways a large group + of people think about a divisive or complicated topic. Here's a basic breakdown of some + terms you'll need to know in order to understand this report. +

+

+ Participants: These are the people who participated in the conversation + by voting and writing statements. Based on how they voted, each participant is sorted + into an opinion group. +

+

+ Statements: Participants may submit statements for other participants + to vote on. Statements are assigned a number in the order they're submitted. +

+

+ Opinion groups: Groups are made of participants who voted similarly to + each other, and differently from the other groups. +

+ +

+ {conversation && conversation.ownername + ? "This pol.is conversation was run by " + conversation.ownername + ". " + : null} + {conversation && conversation.topic + ? "The topic was '" + conversation.topic + "'. " + : null} +

+
+ +
+

+ Raw Data Export (Anonymous) +

+

+ {`The following data exports are anonymized. Participants are identifed by an integer representing the order in which they first voted. For a full description of files and columns, please see: `} + https://compdemocracy.org/export/ +

+

+ {`--------Summary: `} + + {getDownloadFilename("summary", conversation)} + +

+

+ {`-------Comments: `} + + {getDownloadFilename("comments", conversation)} + + {` (may take up to several minutes)`} +

+

+ {`--Votes history: `} + + {getDownloadFilename("votes", conversation)} + + {` (as event log)`} +

+
+

+ Public API endpoints (read only, Jupyter notebook friendly) +

+

+ {`$ curl http://${window.location.hostname}/api/v3/reportExport/${report_id}/summary.csv`} +

+

+ {`$ curl http://${window.location.hostname}/api/v3/reportExport/${report_id}/comments.csv`} +

+

+ {`$ curl http://${window.location.hostname}/api/v3/reportExport/${report_id}/votes.csv`} +

+
+ {window.location.hostname === "pol.is" || + (window.location.hostname === "localhost" && ( +
+

+ Attribution of Polis Data +

+ +

+ All Polis data is licensed under a Creative Commons Attribution 4.0 International + license: https://creativecommons.org/licenses/by/4.0/ +

+

+ --------------- BEGIN STATEMENT --------------- +

+

{`Data was gathered using the Polis software (see: compdemocracy.org/polis and github.com/compdemocracy/polis) and is sub-licensed + under CC BY 4.0 with Attribution to The Computational Democracy Project. The data and more + information about how the data was collected can be found at the following link: ${window.location.href}`}

+

+ --------------- END STATEMENT--------------- +

+

+ For further information on best practices for Attribution of CC 4.0 licensed + content Please see: + https://wiki.creativecommons.org/wiki/Best_practices_for_attribution#Title.2C_Author.2C_Source.2C_License +

+
+ ))} +
+
+ +
- + {/* Leaving this out for now until we get smarter conversationStats */} {/* */} - - - + +
-
); }; @@ -82,9 +201,8 @@ export default Overview; //

{conversation && conversation.participant_count ? "A total of "+ptptCount+" people participated. " : null}

- // It was presented {conversation ? conversation.medium : "loading"} to an audience of {conversation ? conversation.audiences : "loading"}. // The conversation was run for {conversation ? conversation.duration : "loading"}. - // {demographics ? demographics.foo : "loading"} were women +// {demographics ? demographics.foo : "loading"} were women - // {conversation && conversation.description ? "The specific question was '"+conversation.description+"'. ": null} +// {conversation && conversation.description ? "The specific question was '"+conversation.description+"'. ": null} From 8529c94b276146041df109ebfebf3d95f4935140 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Tue, 17 Sep 2024 14:25:55 -0700 Subject: [PATCH 25/27] prettier app.js --- client-report/src/components/app.js | 530 +++++++++++++++------------- 1 file changed, 285 insertions(+), 245 deletions(-) diff --git a/client-report/src/components/app.js b/client-report/src/components/app.js index 2617aa73ca..44f2ca2273 100644 --- a/client-report/src/components/app.js +++ b/client-report/src/components/app.js @@ -16,7 +16,7 @@ import Footer from "./framework/Footer"; import Overview from "./overview"; import MajorityStrict from "./lists/majorityStrict"; import Uncertainty from "./lists/uncertainty"; -import AllCommentsModeratedIn from "./lists/allCommentsModeratedIn" +import AllCommentsModeratedIn from "./lists/allCommentsModeratedIn"; import ParticipantGroups from "./lists/participantGroups"; // import CommentsGraph from "./commentsGraph/commentsGraph"; import ParticipantsGraph from "./participantsGraph/participantsGraph"; @@ -24,14 +24,13 @@ import ParticipantsGraph from "./participantsGraph/participantsGraph"; import Beeswarm from "./beeswarm/beeswarm"; import Controls from "./controls/controls"; -import net from "../util/net" +import net from "../util/net"; -import $ from 'jquery'; +import $ from "jquery"; var pathname = window.location.pathname; // "/report/2arcefpshi" var report_id = pathname.split("/")[2]; - function assertExists(obj, key) { if (typeof obj[key] === "undefined") { console.error("assertExists failed. Missing: ", key); @@ -39,7 +38,6 @@ function assertExists(obj, key) { } class App extends React.Component { - constructor(props) { super(props); this.state = { @@ -52,7 +50,7 @@ class App extends React.Component { colorBlindMode: false, dimensions: { width: window.innerWidth, - height: window.innerHeight + height: window.innerHeight, }, shouldPoll: false, voteColors: { @@ -64,15 +62,17 @@ class App extends React.Component { } getMath(conversation_id) { - return net.polisGet("/api/v3/math/pca2", { - lastVoteTimestamp: 0, - conversation_id: conversation_id, - }).then((data) => { - if (!data) { - return {}; - } - return data; - }); + return net + .polisGet("/api/v3/math/pca2", { + lastVoteTimestamp: 0, + conversation_id: conversation_id, + }) + .then((data) => { + if (!data) { + return {}; + } + return data; + }); } getComments(conversation_id, isStrictMod) { @@ -98,14 +98,16 @@ class App extends React.Component { }); } getReport(report_id) { - return net.polisGet("/api/v3/reports", { - report_id: report_id, - }).then((reports) => { - if (reports.length) { - return reports[0]; - } - return null; - }); + return net + .polisGet("/api/v3/reports", { + report_id: report_id, + }) + .then((reports) => { + if (reports.length) { + return reports[0]; + } + return null; + }); } getGroupDemographics(conversation_id) { return net.polisGet("/api/v3/group_demographics", { @@ -128,30 +130,40 @@ class App extends React.Component { }); return new Promise((resolve, reject) => { - attemptResponse.then((response) => { - if (response.status && response.status === "pending") { - this.corMatRetries = _.isNumber(this.corMatRetries) ? this.corMatRetries + 1 : 1; - setTimeout(() => { - this.getCorrelationMatrix(math_tick).then(resolve, reject); - }, this.corMatRetries < 10 ? 200 : 3000); // try to get a quick response, but don't keep polling at that rate for more than 10 seconds. - } else if (globals.enableMatrix && response && response.status === "polis_report_needs_comment_selection") { - this.setState({ - errorText: "Select some comments", - }); - reject("Currently, No comments are selected for display in the matrix."); - } else { - resolve(response); + attemptResponse.then( + (response) => { + if (response.status && response.status === "pending") { + this.corMatRetries = _.isNumber(this.corMatRetries) ? this.corMatRetries + 1 : 1; + setTimeout( + () => { + this.getCorrelationMatrix(math_tick).then(resolve, reject); + }, + this.corMatRetries < 10 ? 200 : 3000 + ); // try to get a quick response, but don't keep polling at that rate for more than 10 seconds. + } else if ( + globals.enableMatrix && + response && + response.status === "polis_report_needs_comment_selection" + ) { + this.setState({ + errorText: "Select some comments", + }); + reject("Currently, No comments are selected for display in the matrix."); + } else { + resolve(response); + } + }, + (err) => { + reject(err); } - }, (err) => { - reject(err); - }); + ); }); } getData() { const reportPromise = this.getReport(report_id); // debug initial report data fetch - reportPromise.then((report) => console.log("report received:", report)) + reportPromise.then((report) => console.log("report received:", report)); const mathPromise = reportPromise.then((report) => { return this.getMath(report.conversation_id); }); @@ -164,15 +176,17 @@ class App extends React.Component { return this.getGroupDemographics(report.conversation_id); }); //const conversationStatsPromise = reportPromise.then((report) => { - //return this.getConversationStats(report.conversation_id) + //return this.getConversationStats(report.conversation_id) //}); const participantsOfInterestPromise = reportPromise.then((report) => { return this.getParticipantsOfInterest(report.conversation_id); }); - const matrixPromise = globals.enableMatrix ? mathPromise.then((math) => { - const math_tick = math.math_tick; - return this.getCorrelationMatrix(math_tick); - }) : Promise.resolve(); + const matrixPromise = globals.enableMatrix + ? mathPromise.then((math) => { + const math_tick = math.math_tick; + return this.getCorrelationMatrix(math_tick); + }) + : Promise.resolve(); const conversationPromise = reportPromise.then((report) => { return this.getConversation(report.conversation_id); }); @@ -186,191 +200,197 @@ class App extends React.Component { matrixPromise, conversationPromise, //conversationStatsPromise, - ]).then((a) => { - let [ - report, - mathResult, - comments, - groupDemographics, - participants, - correlationHClust, - conversation, - //conversationstats, - ] = a; - - assertExists(mathResult, "base-clusters"); - assertExists(mathResult, "consensus"); - assertExists(mathResult, "group-aware-consensus"); - assertExists(mathResult, "group-clusters"); - assertExists(mathResult, "group-votes"); - assertExists(mathResult, "n-cmts"); - assertExists(mathResult, "repness"); - assertExists(mathResult, "pca"); - assertExists(mathResult, "tids"); - assertExists(mathResult, "user-vote-counts"); - assertExists(mathResult, "votes-base"); - assertExists(mathResult.pca, "center"); - assertExists(mathResult.pca, "comment-extremity"); - assertExists(mathResult.pca, "comment-projection"); - assertExists(mathResult.pca, "comps"); - - let indexToTid = mathResult.tids; - - // # ptpts that voted - var ptptCountTotal = conversation.participant_count; - - - // # ptpts that voted enough to be included in math - var ptptCount = 0; - _.each(mathResult["group-votes"], (val/*, key*/) => { - ptptCount += val["n-members"]; - }); + ]) + .then((a) => { + let [ + report, + mathResult, + comments, + groupDemographics, + participants, + correlationHClust, + conversation, + //conversationstats, + ] = a; + + assertExists(mathResult, "base-clusters"); + assertExists(mathResult, "consensus"); + assertExists(mathResult, "group-aware-consensus"); + assertExists(mathResult, "group-clusters"); + assertExists(mathResult, "group-votes"); + assertExists(mathResult, "n-cmts"); + assertExists(mathResult, "repness"); + assertExists(mathResult, "pca"); + assertExists(mathResult, "tids"); + assertExists(mathResult, "user-vote-counts"); + assertExists(mathResult, "votes-base"); + assertExists(mathResult.pca, "center"); + assertExists(mathResult.pca, "comment-extremity"); + assertExists(mathResult.pca, "comment-projection"); + assertExists(mathResult.pca, "comps"); + + let indexToTid = mathResult.tids; + + // # ptpts that voted + var ptptCountTotal = conversation.participant_count; + + // # ptpts that voted enough to be included in math + var ptptCount = 0; + _.each(mathResult["group-votes"], (val /*, key*/) => { + ptptCount += val["n-members"]; + }); - var badTids = {}; - var filteredTids = {}; - var filteredProbabilities = {}; - - // prep Correlation matrix. - if (globals.enableMatrix) { - var probabilities = correlationHClust.matrix; - var tids = correlationHClust.comments; - for (let row = 0; row < probabilities.length; row++) { - if (probabilities[row][0] === "NaN") { - let tid = correlationHClust.comments[row]; - badTids[tid] = true; - // console.log("bad", tid); + var badTids = {}; + var filteredTids = {}; + var filteredProbabilities = {}; + + // prep Correlation matrix. + if (globals.enableMatrix) { + var probabilities = correlationHClust.matrix; + var tids = correlationHClust.comments; + for (let row = 0; row < probabilities.length; row++) { + if (probabilities[row][0] === "NaN") { + let tid = correlationHClust.comments[row]; + badTids[tid] = true; + // console.log("bad", tid); + } } - } - filteredProbabilities = probabilities.map((row) => { - return row.filter((cell, colNum) => { - let colTid = correlationHClust.comments[colNum]; - return badTids[colTid] !== true; + filteredProbabilities = probabilities + .map((row) => { + return row.filter((cell, colNum) => { + let colTid = correlationHClust.comments[colNum]; + return badTids[colTid] !== true; + }); + }) + .filter((row, rowNum) => { + let rowTid = correlationHClust.comments[rowNum]; + return badTids[rowTid] !== true; + }); + filteredTids = tids.filter((tid /*, index*/) => { + return badTids[tid] !== true; }); - }).filter((row, rowNum) => { - let rowTid = correlationHClust.comments[rowNum]; - return badTids[rowTid] !== true; - }); - filteredTids = tids.filter((tid/*, index*/) => { - return badTids[tid] !== true; - }); - } + } - var maxTid = -1; - for (let i = 0; i < comments.length; i++) { - if (comments[i].tid > maxTid) { - maxTid = comments[i].tid; + var maxTid = -1; + for (let i = 0; i < comments.length; i++) { + if (comments[i].tid > maxTid) { + maxTid = comments[i].tid; + } } - } - var tidWidth = ("" + maxTid).length + var tidWidth = ("" + maxTid).length; - function pad(n, width, z) { - z = z || '0'; - n = n + ''; - return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; - } - function formatTid(tid) { - // let padded = "" + tid; - // return '#' + pad(""+tid, tidWidth); - return pad(""+tid, tidWidth); - } + function pad(n, width, z) { + z = z || "0"; + n = n + ""; + return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; + } + function formatTid(tid) { + // let padded = "" + tid; + // return '#' + pad(""+tid, tidWidth); + return pad("" + tid, tidWidth); + } - let repfulAgreeTidsByGroup = {}; - let repfulDisageeTidsByGroup = {}; - if (mathResult.repness) { - _.each(mathResult.repness, (entries, gid) => { - entries.forEach((entry) => { - if (entry['repful-for'] === 'agree') { - repfulAgreeTidsByGroup[gid] = repfulAgreeTidsByGroup[gid] || []; - repfulAgreeTidsByGroup[gid].push(entry.tid); - } else if (entry['repful-for'] === 'disagree') { - repfulDisageeTidsByGroup[gid] = repfulDisageeTidsByGroup[gid] || []; - repfulDisageeTidsByGroup[gid].push(entry.tid); - } + let repfulAgreeTidsByGroup = {}; + let repfulDisageeTidsByGroup = {}; + if (mathResult.repness) { + _.each(mathResult.repness, (entries, gid) => { + entries.forEach((entry) => { + if (entry["repful-for"] === "agree") { + repfulAgreeTidsByGroup[gid] = repfulAgreeTidsByGroup[gid] || []; + repfulAgreeTidsByGroup[gid].push(entry.tid); + } else if (entry["repful-for"] === "disagree") { + repfulDisageeTidsByGroup[gid] = repfulDisageeTidsByGroup[gid] || []; + repfulDisageeTidsByGroup[gid].push(entry.tid); + } + }); }); - }); - } - - // ====== REMEMBER: gid's start at zero, (0, 1, 2) but we show them as group 1, 2, 3 in participation view ====== - let groupNames = {}; - for (let i = 0; i <= 9; i++) { - let label = report["label_group_" + i]; - if (label) { - groupNames[i] = label; } - } - - let uncertainty = []; - // let maxCount = _.reduce(comments, (memo, c) => { return Math.max(c.count, memo);}, 1); - comments.map((c) => { - var unc = c.pass_count / c.count - if (unc > .3) { - c.unc = unc; - uncertainty.push(c); + // ====== REMEMBER: gid's start at zero, (0, 1, 2) but we show them as group 1, 2, 3 in participation view ====== + let groupNames = {}; + for (let i = 0; i <= 9; i++) { + let label = report["label_group_" + i]; + if (label) { + groupNames[i] = label; + } } - }); - uncertainty.sort((a, b) => { - return (b.unc*b.unc*b.pass_count - a.unc*a.unc*a.pass_count); - }); - uncertainty = uncertainty.slice(0, 5); - - let extremity = {}; - _.each(mathResult.pca["comment-extremity"], function(e, index) { - extremity[indexToTid[index]] = e; - }); - var uniqueCommenters = {}; - var voteTotals = DataUtils.getVoteTotals(mathResult); - comments = comments.map((c) => { - c["group-aware-consensus"] = mathResult["group-aware-consensus"][c.tid]; - uniqueCommenters[c.pid] = 1; - c = Object.assign(c, voteTotals[c.tid]); - return c; - }); - var numUniqueCommenters = _.keys(uniqueCommenters).length; - var totalVotes = _.reduce(_.values(mathResult["user-vote-counts"]), function(memo, num) { - return memo + num; - }, 0); - const computedStats = { - votesPerVoterAvg: totalVotes / ptptCountTotal, - commentsPerCommenterAvg: comments.length / numUniqueCommenters, - }; + let uncertainty = []; + // let maxCount = _.reduce(comments, (memo, c) => { return Math.max(c.count, memo);}, 1); + comments.map((c) => { + var unc = c.pass_count / c.count; + if (unc > 0.3) { + c.unc = unc; + uncertainty.push(c); + } + }); + uncertainty.sort((a, b) => { + return b.unc * b.unc * b.pass_count - a.unc * a.unc * a.pass_count; + }); + uncertainty = uncertainty.slice(0, 5); - this.setState({ - loading: false, - math: mathResult, - consensus: mathResult.consensus, - extremity: extremity, - uncertainty: uncertainty.map((c) => {return c.tid;}), - comments: comments, - demographics: groupDemographics, - participants: participants, - conversation: conversation, - ptptCount: ptptCount, - ptptCountTotal: ptptCountTotal, - filteredCorrelationMatrix: filteredProbabilities, - filteredCorrelationTids: filteredTids, - badTids: badTids, - groupNames: groupNames, - repfulAgreeTidsByGroup: repfulAgreeTidsByGroup, - repfulDisageeTidsByGroup: repfulDisageeTidsByGroup, - formatTid: formatTid, - report: report, - //conversationStats: conversationstats, - computedStats: computedStats, - nothingToShow: !comments.length || !groupDemographics.length - }); + let extremity = {}; + _.each(mathResult.pca["comment-extremity"], function (e, index) { + extremity[indexToTid[index]] = e; + }); - }).catch((err) => { - this.setState({ - error: true, - errorText: String(err), + var uniqueCommenters = {}; + var voteTotals = DataUtils.getVoteTotals(mathResult); + comments = comments.map((c) => { + c["group-aware-consensus"] = mathResult["group-aware-consensus"][c.tid]; + uniqueCommenters[c.pid] = 1; + c = Object.assign(c, voteTotals[c.tid]); + return c; + }); + var numUniqueCommenters = _.keys(uniqueCommenters).length; + var totalVotes = _.reduce( + _.values(mathResult["user-vote-counts"]), + function (memo, num) { + return memo + num; + }, + 0 + ); + const computedStats = { + votesPerVoterAvg: totalVotes / ptptCountTotal, + commentsPerCommenterAvg: comments.length / numUniqueCommenters, + }; + + this.setState({ + loading: false, + math: mathResult, + consensus: mathResult.consensus, + extremity: extremity, + uncertainty: uncertainty.map((c) => { + return c.tid; + }), + comments: comments, + demographics: groupDemographics, + participants: participants, + conversation: conversation, + ptptCount: ptptCount, + ptptCountTotal: ptptCountTotal, + filteredCorrelationMatrix: filteredProbabilities, + filteredCorrelationTids: filteredTids, + badTids: badTids, + groupNames: groupNames, + repfulAgreeTidsByGroup: repfulAgreeTidsByGroup, + repfulDisageeTidsByGroup: repfulDisageeTidsByGroup, + formatTid: formatTid, + report: report, + //conversationStats: conversationstats, + computedStats: computedStats, + nothingToShow: !comments.length || !groupDemographics.length, + }); + }) + .catch((err) => { + this.setState({ + error: true, + errorText: String(err), + }); }); - }); } - UNSAFE_componentWillMount() { this.getData(); @@ -380,14 +400,17 @@ class App extends React.Component { } }, 20 * 1000); - window.addEventListener("resize", _.throttle(() => { - this.setState({ - dimensions: { - width: window.innerWidth, - height: window.innerHeight - } - }) - }, 500)); + window.addEventListener( + "resize", + _.throttle(() => { + this.setState({ + dimensions: { + width: window.innerWidth, + height: window.innerHeight, + }, + }); + }, 500) + ); } onAutoRefreshEnabled() { @@ -402,7 +425,7 @@ class App extends React.Component { }); } - handleColorblindModeClick () { + handleColorblindModeClick() { var colorBlind = !this.state.colorBlindMode; if (colorBlind) { this.setState({ @@ -423,36 +446,45 @@ class App extends React.Component { render() { if (this.state.error) { - return (
-
Error Loading
-
{this.state.errorText}
-
); + return ( +
+
Error Loading
+
{this.state.errorText}
+
+ ); } if (this.state.nothingToShow) { - return (
-
Nothing to show yet
-
); + return ( +
+
Nothing to show yet
+
+ ); } if (this.state.loading) { - return (
-
Loading ...
-
); + return ( +
+
Loading ...
+
+ ); } - console.log('top level app state and props', this.state, this.props) + console.log("top level app state and props", this.state, this.props); return ( -
- -
+ +
+ }} + > + voteColors={this.state.voteColors} + /> {/* This may eventually need to go back in below */} {/* stats={this.state.conversationStats} */} @@ -464,7 +496,8 @@ class App extends React.Component { ptptCountTotal={this.state.ptptCountTotal} demographics={this.state.demographics} conversation={this.state.conversation} - voteColors={this.state.voteColors}/> + voteColors={this.state.voteColors} + /> + voteColors={this.state.voteColors} + /> {/*

Consensus

Inclusive Majority

*/} + + voteColors={this.state.voteColors} + /> + voteColors={this.state.voteColors} + /> + voteColors={this.state.voteColors} + /> {/* {false ? + voteColors={this.state.voteColors} + /> {/* */} -
+ voteColors={this.state.voteColors} + /> +
); From 5658bd5d2211416932281b2f8025b19c78b1f638 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Tue, 17 Sep 2024 14:26:29 -0700 Subject: [PATCH 26/27] move overview down --- client-report/src/components/overview.js | 238 +++++++++++------------ 1 file changed, 118 insertions(+), 120 deletions(-) diff --git a/client-report/src/components/overview.js b/client-report/src/components/overview.js index 0fda40a318..470448864e 100644 --- a/client-report/src/components/overview.js +++ b/client-report/src/components/overview.js @@ -54,126 +54,35 @@ const Overview = ({ }) => { return (
-
-
-

Overview

-

- Pol.is is a real-time survey system that helps identify the different ways a large group - of people think about a divisive or complicated topic. Here's a basic breakdown of some - terms you'll need to know in order to understand this report. -

-

- Participants: These are the people who participated in the conversation - by voting and writing statements. Based on how they voted, each participant is sorted - into an opinion group. -

-

- Statements: Participants may submit statements for other participants - to vote on. Statements are assigned a number in the order they're submitted. -

-

- Opinion groups: Groups are made of participants who voted similarly to - each other, and differently from the other groups. -

- -

- {conversation && conversation.ownername - ? "This pol.is conversation was run by " + conversation.ownername + ". " - : null} - {conversation && conversation.topic - ? "The topic was '" + conversation.topic + "'. " - : null} -

-
- -
-

- Raw Data Export (Anonymous) -

-

- {`The following data exports are anonymized. Participants are identifed by an integer representing the order in which they first voted. For a full description of files and columns, please see: `} - https://compdemocracy.org/export/ -

-

- {`--------Summary: `} - - {getDownloadFilename("summary", conversation)} - -

-

- {`-------Comments: `} - - {getDownloadFilename("comments", conversation)} - - {` (may take up to several minutes)`} -

-

- {`--Votes history: `} - - {getDownloadFilename("votes", conversation)} - - {` (as event log)`} -

-
-

- Public API endpoints (read only, Jupyter notebook friendly) -

-

- {`$ curl http://${window.location.hostname}/api/v3/reportExport/${report_id}/summary.csv`} -

-

- {`$ curl http://${window.location.hostname}/api/v3/reportExport/${report_id}/comments.csv`} -

-

- {`$ curl http://${window.location.hostname}/api/v3/reportExport/${report_id}/votes.csv`} -

-
- {window.location.hostname === "pol.is" || - (window.location.hostname === "localhost" && ( -
-

- Attribution of Polis Data -

- -

- All Polis data is licensed under a Creative Commons Attribution 4.0 International - license: https://creativecommons.org/licenses/by/4.0/ -

-

- --------------- BEGIN STATEMENT --------------- -

-

{`Data was gathered using the Polis software (see: compdemocracy.org/polis and github.com/compdemocracy/polis) and is sub-licensed - under CC BY 4.0 with Attribution to The Computational Democracy Project. The data and more - information about how the data was collected can be found at the following link: ${window.location.href}`}

-

- --------------- END STATEMENT--------------- -

-

- For further information on best practices for Attribution of CC 4.0 licensed - content Please see: - https://wiki.creativecommons.org/wiki/Best_practices_for_attribution#Title.2C_Author.2C_Source.2C_License -

-
- ))} -
+
+

Overview

+

+ Pol.is is a real-time survey system that helps identify the different ways a large group + of people think about a divisive or complicated topic. Here's a basic breakdown of some + terms you'll need to know in order to understand this report. +

+

+ Participants: These are the people who participated in the conversation + by voting and writing statements. Based on how they voted, each participant is sorted into + an opinion group. +

+

+ Statements: Participants may submit statements for other participants to + vote on. Statements are assigned a number in the order they're submitted. +

+

+ Opinion groups: Groups are made of participants who voted similarly to + each other, and differently from the other groups. +

+ +

+ {conversation && conversation.ownername + ? "This pol.is conversation was run by " + conversation.ownername + ". " + : null} + {conversation && conversation.topic + ? "The topic was '" + conversation.topic + "'. " + : null} +

@@ -193,6 +102,95 @@ const Overview = ({ label={"statements per author on average"} />
+ +
+

+ Raw Data Export (Anonymous) +

+

+ {`The following data exports are anonymized. Participants are identifed by an integer representing the order in which they first voted. For a full description of files and columns, please see: `} + https://compdemocracy.org/export/ +

+

+ {`--------Summary: `} + + {getDownloadFilename("summary", conversation)} + +

+

+ {`-------Comments: `} + + {getDownloadFilename("comments", conversation)} + + {` (may take up to several minutes)`} +

+

+ {`--Votes history: `} + + {getDownloadFilename("votes", conversation)} + + {` (as event log)`} +

+
+

+ Public API endpoints (read only, Jupyter notebook friendly) +

+

+ {`$ curl http://${window.location.hostname}/api/v3/reportExport/${report_id}/summary.csv`} +

+

+ {`$ curl http://${window.location.hostname}/api/v3/reportExport/${report_id}/comments.csv`} +

+

+ {`$ curl http://${window.location.hostname}/api/v3/reportExport/${report_id}/votes.csv`} +

+
+ {window.location.hostname === "pol.is" || + (window.location.hostname === "localhost" && ( +
+

+ Attribution of Polis Data +

+ +

+ All Polis data is licensed under a Creative Commons Attribution 4.0 International + license: https://creativecommons.org/licenses/by/4.0/ +

+

+ --------------- BEGIN STATEMENT --------------- +

+

{`Data was gathered using the Polis software (see: compdemocracy.org/polis and github.com/compdemocracy/polis) and is sub-licensed + under CC BY 4.0 with Attribution to The Computational Democracy Project. The data and more + information about how the data was collected can be found at the following link: ${window.location.href}`}

+

+ --------------- END STATEMENT--------------- +

+

+ For further information on best practices for Attribution of CC 4.0 licensed content + Please see: + https://wiki.creativecommons.org/wiki/Best_practices_for_attribution#Title.2C_Author.2C_Source.2C_License +

+
+ ))} +
); }; From 0f7f4b590f1eaf2cc408141f0c9c612134d53d4b Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Tue, 17 Sep 2024 14:26:38 -0700 Subject: [PATCH 27/27] shorten toxic text --- .../components/conversation-admin/comment-moderation/comment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-admin/src/components/conversation-admin/comment-moderation/comment.js b/client-admin/src/components/conversation-admin/comment-moderation/comment.js index abafbbb8fd..ec9a5e4787 100644 --- a/client-admin/src/components/conversation-admin/comment-moderation/comment.js +++ b/client-admin/src/components/conversation-admin/comment-moderation/comment.js @@ -27,7 +27,7 @@ class Comment extends React.Component { return ( - {this.props.comment.active ? null : 'Comment flagged as toxic by Jigsaw Perspective API (value > .8). Comment not active / not shown in conversation. Human moderation action will override this flag.'} + {this.props.comment.active ? null : 'Comment flagged as toxic by Jigsaw Perspective API. Comment not shown to participants. Accept to override.'} {this.props.comment.txt}