diff --git a/.meteor/.finished-upgraders b/.meteor/.finished-upgraders index c07b6ff..910574c 100644 --- a/.meteor/.finished-upgraders +++ b/.meteor/.finished-upgraders @@ -15,5 +15,3 @@ notices-for-facebook-graph-api-2 1.4.1-add-shell-server-package 1.4.3-split-account-service-packages 1.5-add-dynamic-import-package -1.7-split-underscore-from-meteor-base -1.8.3-split-jquery-from-blaze diff --git a/.meteor/packages b/.meteor/packages index a9f060c..5cca156 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -4,34 +4,34 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. -meteor-base@1.5.1 # Packages every Meteor app needs to have -mobile-experience@1.1.0 # Packages for a great mobile UX -mongo@1.13.0 # The database Meteor supports right now +meteor-base@1.3.0 # Packages every Meteor app needs to have +mobile-experience@1.0.5 # Packages for a great mobile UX +mongo@1.4.7 # The database Meteor supports right now reactive-var@1.0.11 # Reactive variable for tracker -tracker@1.2.0 # Meteor's client-side reactive programming library +tracker@1.1.3 # Meteor's client-side reactive programming library -standard-minifier-css@1.7.4 # CSS minifier run for production mode -standard-minifier-js@2.7.1 # JS minifier run for production mode -es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers. -ecmascript@0.16.0 # Enable ECMAScript2015+ syntax in app code -shell-server@0.5.0 # Server-side component of the `meteor shell` command +standard-minifier-css@1.4.0 # CSS minifier run for production mode +standard-minifier-js@2.3.1 # JS minifier run for production mode +es5-shim@4.7.0 # ECMAScript 5 compatibility for older browsers. +ecmascript@0.10.6 # Enable ECMAScript2015+ syntax in app code +shell-server@0.3.1 # Server-side component of the `meteor shell` command react-meteor-data alanning:roles -fourseven:scss@4.14.1 -accounts-base@2.2.0 -accounts-password@2.2.0 -service-configuration@1.3.0 -accounts-facebook@1.3.3 -accounts-github@1.5.0 -accounts-google@1.4.0 +fourseven:scss +accounts-base@1.4.2 +accounts-password@1.5.0 +service-configuration@1.0.11 +accounts-facebook@1.3.1 +accounts-github@1.4.1 +accounts-google@1.3.1 themeteorchef:bert fortawesome:fontawesome aldeed:collection2-core@2.0.1 audit-argument-checks@1.0.7 -ddp-rate-limiter@1.1.0 -dynamic-import@0.7.2 -static-html@1.3.2 +ddp-rate-limiter@1.0.7 +dynamic-import@0.3.0 +static-html alexwine:bootstrap-4 selaias:cookie-consent # Cookie consent @@ -42,19 +42,28 @@ flowkey:raven # js errors vjrj:piwik # Stats mdg:geolocation natestrauser:publish-performant-counts +maximum:server-transform mys:fonts # fonst in npm ostrio:mailer # mailer # babrahams:constellation # dev utility percolate:migrations # db migrations ostrio:meteor-root +meteortesting:mocha +practicalmeteor:chai aldeed:schema-deny +aldeed:template-extension dburles:collection-helpers +less@2.7.11 +markdown@1.0.12 mongo-livedata@1.0.12 reywood:publish-composite +barbatus:stars-rating +arkham:comments-ui facts@1.0.9 gadicohen:sitemaps nspangler:autoreconnect saucecode:timezoned-synced-cron # lmachens:kadira -nimble:restivus@0.8.12 -underscore@1.0.10 +meteorhacks:zones +nimble:restivus +xolvio:cleaner diff --git a/.meteor/release b/.meteor/release index 5599529..011385b 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@2.5 +METEOR@1.6.1.1 diff --git a/.meteor/versions b/.meteor/versions index 0acd86e..9cbf1c3 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,128 +1,155 @@ 255kb:meteor-status@1.5.0 -accounts-base@2.2.0 -accounts-facebook@1.3.3 -accounts-github@1.5.0 -accounts-google@1.4.0 -accounts-oauth@1.4.0 -accounts-password@2.2.0 -alanning:roles@1.3.0 +accounts-base@1.4.2 +accounts-facebook@1.3.1 +accounts-github@1.4.1 +accounts-google@1.3.1 +accounts-oauth@1.1.15 +accounts-password@1.5.1 +alanning:roles@1.2.16 aldeed:collection2-core@2.1.2 aldeed:schema-deny@2.0.1 +aldeed:template-extension@4.1.0 alexwine:bootstrap-4@4.1.0 allow-deny@1.1.0 +arkham:comments-ui@1.4.4 audit-argument-checks@1.0.7 -autoupdate@1.8.0 -babel-compiler@7.7.0 -babel-runtime@1.5.0 -base64@1.0.12 -binary-heap@1.0.11 -blaze@2.5.0 -blaze-tools@1.1.2 -boilerplate-generator@1.7.1 -caching-compiler@1.2.2 -caching-html-compiler@1.2.1 -callback-hook@1.4.0 +autoupdate@1.4.0 +babel-compiler@7.0.7 +babel-runtime@1.2.2 +barbatus:stars-rating@1.1.1 +base64@1.0.11 +binary-heap@1.0.10 +blaze@2.3.2 +blaze-tools@1.0.10 +boilerplate-generator@1.4.0 +browser-policy@1.1.0 +browser-policy-common@1.0.11 +browser-policy-content@1.1.0 +browser-policy-framing@1.1.0 +caching-compiler@1.1.11 +caching-html-compiler@1.1.2 +callback-hook@1.1.0 check@1.3.1 +coffeescript@1.0.17 dburles:collection-helpers@1.1.0 +dburles:mongo-collection-instances@0.3.5 ddp@1.4.0 -ddp-client@2.5.0 +ddp-client@2.3.2 ddp-common@1.4.0 -ddp-rate-limiter@1.1.0 -ddp-server@2.5.0 -diff-sequence@1.1.1 -dynamic-import@0.7.2 -ecmascript@0.16.0 -ecmascript-runtime@0.8.0 -ecmascript-runtime-client@0.12.1 -ecmascript-runtime-server@0.11.0 -ejson@1.1.1 -email@2.2.0 -es5-shim@4.8.0 -facebook-oauth@1.10.0 +ddp-rate-limiter@1.0.7 +ddp-server@2.1.2 +deps@1.0.12 +diff-sequence@1.1.0 +dynamic-import@0.3.0 +ecmascript@0.10.7 +ecmascript-runtime@0.5.0 +ecmascript-runtime-client@0.6.2 +ecmascript-runtime-server@0.5.0 +ejson@1.1.0 +email@1.2.3 +es5-shim@4.7.3 +facebook-oauth@1.4.0 facts@1.0.9 -fetch@0.1.1 flowkey:raven@1.1.0 fortawesome:fontawesome@4.7.0 -fourseven:scss@4.14.1 +fourseven:scss@4.5.4 gadicc:blaze-react-component@1.4.0 gadicohen:robots-txt@0.0.10 gadicohen:sitemaps@0.0.26 geojson-utils@1.0.10 -github-oauth@1.3.1 -google-oauth@1.4.1 +github-oauth@1.2.0 +google-oauth@1.2.5 hot-code-push@1.0.4 -html-tools@1.1.2 -htmljs@1.1.0 -http@2.0.0 -id-map@1.1.1 -inter-process-messaging@0.1.1 +html-tools@1.0.11 +htmljs@1.0.11 +http@1.4.0 +id-map@1.1.0 jquery@1.11.11 -launch-screen@1.3.0 +lai:collection-extensions@0.2.1_1 +launch-screen@1.1.1 +less@2.7.12 +livedata@1.0.18 localstorage@1.2.0 -logging@1.3.1 +logging@1.1.20 +markdown@1.0.12 +maximum:package-base@1.1.2 +maximum:server-transform@0.5.0 mdg:geolocation@1.3.0 -meteor@1.10.0 -meteor-base@1.5.1 -minifier-css@1.6.0 -minifier-js@2.7.1 -minimongo@1.7.0 -mobile-experience@1.1.0 -mobile-status-bar@1.1.0 -modern-browsers@0.1.7 -modules@0.17.0 -modules-runtime@0.12.0 -mongo@1.13.0 -mongo-decimal@0.1.2 +meteor@1.8.6 +meteor-base@1.3.0 +meteorhacks:inject-initial@1.0.4 +meteorhacks:zones@1.6.0 +meteortesting:browser-tests@0.2.0 +meteortesting:mocha@0.5.1 +minifier-css@1.3.1 +minifier-js@2.3.4 +minimongo@1.4.4 +mobile-experience@1.0.5 +mobile-status-bar@1.0.14 +modules@0.11.6 +modules-runtime@0.9.2 +mongo@1.4.7 mongo-dev-server@1.1.0 -mongo-id@1.0.8 +mongo-id@1.0.7 mongo-livedata@1.0.12 mys:fonts@0.0.2 natestrauser:publish-performant-counts@0.1.2 nimble:restivus@0.8.12 -npm-mongo@3.9.1 +npm-bcrypt@0.9.3 +npm-mongo@2.2.34 nspangler:autoreconnect@0.0.1 -oauth@2.1.0 -oauth2@1.3.1 +oauth@1.2.3 +oauth2@1.2.0 observe-sequence@1.0.16 ordered-dict@1.1.0 -ostrio:cookies@2.7.0 +ostrio:cookies@2.1.3 ostrio:mailer@2.1.0 ostrio:meteor-root@1.0.7 +peerlibrary:assert@0.2.5 +peerlibrary:fiber-utils@0.6.0 +peerlibrary:reactive-mongo@0.1.1 +peerlibrary:server-autorun@0.5.2 percolate:migrations@1.0.2 -promise@0.12.0 +practicalmeteor:chai@2.1.0_1 +practicalmeteor:mocha-core@1.0.1 +promise@0.10.2 raix:eventemitter@0.1.3 -random@1.2.0 +random@1.1.0 rate-limit@1.0.9 -react-fast-refresh@0.2.0 react-meteor-data@0.2.16 -reactive-dict@1.3.0 +reactive-dict@1.2.0 reactive-var@1.0.11 -reload@1.3.1 +reload@1.2.0 retry@1.1.0 reywood:publish-composite@1.6.0 -routepolicy@1.1.1 +routepolicy@1.0.13 saucecode:timezoned-synced-cron@1.2.11 selaias:cookie-consent@0.4.0 -service-configuration@1.3.0 -session@1.2.0 +server-render@0.3.0 +service-configuration@1.0.11 +session@1.1.7 sha@1.0.9 -shell-server@0.5.0 -simple:json-routes@1.0.4 -socket-stream-client@0.4.0 -spacebars@1.2.0 -spacebars-compiler@1.3.0 -standard-minifier-css@1.7.4 -standard-minifier-js@2.7.1 -static-html@1.3.2 -templating@1.4.1 -templating-compiler@1.4.1 -templating-runtime@1.5.0 -templating-tools@1.2.1 +shell-server@0.3.1 +shim-common@0.1.0 +simple:json-routes@2.1.0 +socket-stream-client@0.1.0 +spacebars@1.0.15 +spacebars-compiler@1.1.3 +srp@1.0.10 +standard-minifier-css@1.4.1 +standard-minifier-js@2.3.3 +static-html@1.2.2 +templating@1.3.2 +templating-compiler@1.3.3 +templating-runtime@1.3.2 +templating-tools@1.1.2 themeteorchef:bert@2.1.3 tmeasday:check-npm-versions@0.3.2 -tracker@1.2.0 +tracker@1.1.3 +ui@1.0.13 underscore@1.0.10 -url@1.3.2 +url@1.2.0 vjrj:piwik@0.3.1 -webapp@1.13.0 -webapp-hashing@1.1.0 +webapp@1.5.0 +webapp-hashing@1.0.9 +xolvio:cleaner@0.3.3 diff --git a/.meteorignore b/.meteorignore index c013f6f..b25101c 100644 --- a/.meteorignore +++ b/.meteorignore @@ -1,5 +1,3 @@ # Don't reload meteor server when we modify or do test cucumber output.json -# REST smoke-test harness runs standalone (Node), never as Meteor server code -smoke diff --git a/UPGRADE.md b/UPGRADE.md deleted file mode 100644 index 2f7b040..0000000 --- a/UPGRADE.md +++ /dev/null @@ -1,278 +0,0 @@ -# Meteor upgrade — 1.6.1.1 → 2.x (→ 3.x blocked) - -Incremental, verified upgrade of `todos-contra-el-fuego-web`. Branch: -`meteor3-upgrade` (base `tcef-master`). Local commits only — no push until -authorized. - -## ⚠️ Hard blocker for the final 3.x jump: MongoDB 3.2 - -Production runs against the shared replica set **rsmain on MongoDB 3.2.11**. -Meteor 3 ships the modern `mongodb` Node driver, which requires a server -**≥ 4.2/4.4**. Meteor **2.x still works against Mongo 3.2**. - -Therefore this branch lands on **Meteor 2.5** — empirically the highest release -whose bundled `mongodb` driver still connects to Mongo 3.2 (2.6 bumps the driver -to 4.x and fails; see "Escala ceiling" below) — with everything green. The -`meteor update --release 3.x` step (and even 2.6+) is **deliberately not taken** -until the Comunes infra team upgrades rsmain (shared infra — coordinated in a -separate phase, not unilaterally). Server code should still be migrated to -async/await before the final 3.x jump so it is mechanical (tracked as debt). - -## ⚠️ Deploy-ordering dependency: notifications cutover - -The old push/email notification code was removed from the web (see the -notifications commit) because it now lives in the **tcef-notifications** -microservice. **Production must not run this build until tcef-notifications is -emitting in prod** — otherwise users stop receiving notifications. The web -deploy and the notifications cutover must be coordinated. - -## Safety net: REST smoke test - -`smoke/` is a standalone Node harness that calls every REST endpoint the Flutter -app consumes, against a seeded local dev server, and diffs the responses against -committed snapshots (`smoke/snapshots/`). See `smoke/README.md`. - -**Run after every escala — it must stay byte-identical:** - -```bash -docker run -d --name tcef-mongo32 -p 27018:27017 mongo:3.2 # once -export PATH="$HOME/.meteor:$PATH" MONGO_URL="mongodb://localhost:27018/fuegos" -meteor --settings settings-development.json --port 3100 & # dev server -./smoke/smoke.sh # seed + compare -``` - -## Escalas - -### Baseline — Meteor 1.6.1.1 (starting point) - -- Boots against Mongo 3.2 with `settings-development.json`. -- Dev-enabling changes (do not affect production behavior): - - `IPGeocoder.js`: degrade gracefully when the MaxMind GeoLite2 DB is absent - (provisioned by cron in prod) instead of crashing at boot. - - `settings-development.json`: added dev-only `private.internalApiToken` - (`dev-smoke-token`) — the REST API only registers its routes when this is - set. -- **Notifications code removed** (migrated to tcef-notifications): deleted - `notificationsObserver.js`, `notificationsProcess.js`, the "Process pending - notif" SyncedCron job, and the `node-gcm` dependency. `subsUnion.js` kept (it - feeds `subs-public-union`, consumed by the app). -- REST smoke baseline captured here. ✅ green. - -### Escala 1 — 1.6.1.1 → 1.8.3 ✅ green - -```bash -meteor update --release 1.8.3 -meteor npm install --save @babel/runtime@^7.26.0 -``` - -Core packages bumped (highlights): `meteor` 1.8.6→1.9.3, `ecmascript` -0.10.7→0.13.2, `modules` 0.11.6→0.14.0, `mongo` 1.4.7→1.7.0, `npm-mongo` -2.2.34→**3.2.0** (mongodb driver 3.x — still fine against Mongo 3.2 server), -`webapp` 1.5.0→1.7.5, `standard-minifier-js` →2.5.2. `underscore` auto-added -(1.7 dropped it from meteor-base), `fetch` + `modern-browsers` added. - -**Breaking change — Babel beta → stable.** Boot failed with -`Cannot find module '@babel/runtime/helpers/objectSpread2'`: the pinned -`@babel/runtime@7.0.0-beta.44` predates helpers that 1.8's `ecmascript` emits. -Fixed by upgrading to stable **7.x** (`@babel/runtime@^7.29.7`). Note: `@latest` -resolves to `8.x`, which is too new for Meteor's 7-style helper layout — pin to -`^7`. - -REST smoke test: **byte-identical to baseline.** ✅ - -### Escala 2 — 1.8.3 → 1.11 (version resolution only) - -```bash -meteor update --release 1.11 -``` - -Resolution reached 1.11 (`ecmascript` 0.14.3, `mongo` 1.10.0, `npm-mongo` -**3.8.0** — still Mongo-3.2-compatible, `accounts-*` 1.x, `email` 2.0). But this -release surfaced the two big blockers below. The app was ultimately driven to -**2.3** (see next section); 1.11 was a transit point, not a landing. - -### Escala 3 — → Meteor 2.3 ✅ green (builds + runs against Mongo 3.2, smoke byte-identical) - -`.meteor/release = METEOR@2.3`. `npm-mongo` **3.9.0** (mongodb driver 3.x) — the -last driver line that still speaks to a Mongo 3.2 server. Core bumps: -`accounts-*` 2.0, `ecmascript` 0.15.2, `mongo` 1.12.0, HMR added. - -This escala was dominated by **dead Atmosphere packages** (the plan's #1 risk). -Root causes and fixes, in the order they were hit: - -1. **`arkham:comments-ui` — no Meteor 2.x build.** The vendored local package - used `Npm.depends`, and rebuilding a *local* package's npm deps crashes - meteor-tool ≥1.11 on this host (`node_contextify.cc … Assertion - args[1]->IsString()` → SIGABRT; reproducible with any `Npm.depends`, even - `is-number`). Switching to the Atmosphere isopack avoided the local rebuild - but the package is abandoned: its newest 2.x-solvable version (0.2.15) pins - `accounts-password@1.0.1`, conflicting with Meteor 2.0's accounts-password 2.0. - **Decision (user-approved): reimplement comments as a small React feature** - (`imports/api/Comments/*`, `imports/ui/components/Comments/CommentsBox.js`), - dropping the package entirely. Fire-page comments only (the sole usage), with - likes/dislikes, image/YouTube embed, owner edit/remove, and the - "email other commenters" side-effect preserved. Comment text is now rendered - as **safe plain text** (+ media embed) instead of markdown-to-HTML — a small - scope reduction and a security improvement (no raw-HTML injection of user - content). The old local fork is preserved in its nested git repo - (commit `4761da9`). - -2. **`coffeescript@1.0.17` build plugin — crashes the build.** The same - `node_contextify` assertion fired at *build start* on this host whenever the - coffeescript compiler plugin loaded. It was pulled transitively by the dead - **test stack** (`meteortesting:mocha`, `practicalmeteor:chai`, - `xolvio:cleaner`) and by the old auto-downgraded `nimble:restivus@0.6.6` - (which needs `iron:router`). Removing those removed coffeescript and - unblocked the build. A trivial `meteor create` app builds fine on the same - tool, confirming the crash is package-specific, not a broken tool. - -3. **`nimble:restivus` — the REST API package — pins `accounts-password@1.3.3`** - (incompatible with 2.x) and is CoffeeScript (needs the crashing plugin). - **Vendored** as `packages/nimble-restivus`: the `.coffee` sources were - precompiled to plain JS (`lib/restivus-all.js`, single translation unit so - the `Auth`/`Route`/`Restivus` classes share scope; `Restivus` left - un-`var`'d so Meteor's `api.export` picks it up), and `package.js` loosens - `accounts-password` to 2.x and drops the coffeescript dependency. This also - let restivus resolve to 0.8.12 (json-routes based), which **removed the whole - `iron:router` stack**. REST behavior unchanged (smoke byte-identical). - -4. **`maximum:server-transform` (unused) removal** broke - `Meteor.publishTransformed` in `FalsePositives/server/publications.js`; no - transform was actually configured, so replaced with plain `Meteor.publish`. - -5. **`fourseven:scss` 4.5.4 → 4.14.1** — `node-sass@4.5.3` won't build on node 12 - (Meteor ≥1.9); 4.14.1 uses a node-12-compatible node-sass with prebuilt - binaries. (Meteor 1.8 built it on node 8.) `4.15.0` needs ecmascript ≥ 0.15.1 - → too new for 1.11. - -6. **`less`, `markdown` removed** — no `.less`/`.md` source files; their build - plugins were dead weight. - -### ⚠️ Local build-host caveat - -Meteor-tool's bundled node (12.x/14.x) SIGABRTs (`node_contextify` assertion) on -this machine (kernel 6.12) when certain old build plugins load — this is an -environment interaction, not a code defect (the deploy host built these fine -historically). It was fully worked around by removing the dead build plugins -above. If a future escala hits it again, build in a container matching the -deploy target (Debian) rather than on this host. - -### Escalas 2.4 & 2.5 — ✅ green - -`meteor update --release 2.4`, then `--release 2.5`. Both trivial; `npm-mongo` -stays at **3.9.1** (mongodb driver 3.x). REST smoke byte-identical against -`mongo:3.2` on both. - -## Escala ceiling vs Mongo 3.2 — **landing on Meteor 2.5** (empirically verified) - -Meteor bumps the bundled `mongodb` Node driver to **4.3.1 at Meteor 2.6**, which -requires server wire version ≥ 6 (**MongoDB ≥ 3.6**). Production's rsmain is -**Mongo 3.2** (wire version 4). Verified directly by booting 2.6 against a real -`mongo:3.2`: - -``` -MongoCompatibilityError: Server at localhost:27018 reports maximum wire -version 4, but this version of the Node.js Driver requires at least 6 -(MongoDB 3.6) -=> Exited with code: 1 -``` - -So **Meteor 2.5 is the highest release that runs against the production Mongo -3.2** (npm-mongo 3.9.1). That is where this branch lands: everything green, REST -smoke byte-identical. Going past 2.5 (to 2.16 or 3.x) is gated on upgrading the -shared rsmain replica set to Mongo ≥ 4.4 first — the same DB blocker as 3.x. - -## 🚧 3.x — blocked (do not run `meteor update --release 3.x`) - -Two independent blockers, both external/coordination: -1. **MongoDB 3.2 → ≥ 4.4.** Meteor 3's driver requires it. Shared Comunes infra - (rsmain) — coordinate the DB upgrade in a separate phase. This *also* gates - Meteor 2.5+. -2. **Notifications cutover.** The old notif code is already removed from this - branch; production must not run this build until `tcef-notifications` is - emitting in prod. - -When Mongo is ≥ 4.4 and the notifications service is live in prod, the remaining -3.x work is: `meteor update --release 3.x`, finish the async/await migration of -server code (collections/methods/publications `*Async`, no Fibers), React 18 -render root, and re-run the smoke test. - -## 3.x groundwork — done on branch `meteor3-wip` (in progress, not deployable) - -To de-risk the eventual 3.x jump, the migration was driven as far as it goes -against a **modern dockerized MongoDB** (the "mongo superior" decision: give -fuegos its own Mongo 7 instead of the shared rsmain 3.2, which is what actually -unblocks Meteor 3's driver). This lives on `meteor3-wip`; `meteor3-upgrade` stays -at the deployable Meteor 2.5. - -**Modern Mongo (local, reproducible):** -```bash -docker run -d --name tcef-mongo7 -p 27019:27019 mongo:7 --replSet rs0 --port 27019 --bind_ip_all -docker exec tcef-mongo7 mongosh --port 27019 --quiet --eval 'rs.initiate({_id:"rs0",members:[{_id:0,host:"localhost:27019"}]})' -export MONGO_URL="mongodb://localhost:27019/fuegos?replicaSet=rs0" -# meteor-tool node 22 needs this to reach warehouse.meteor.com (else Happy-Eyeballs -# picks an unreachable IPv6): -export NODE_OPTIONS="--dns-result-order=ipv4first --no-network-family-autoselection" -``` - -**Reached `METEOR@3.1` with all package-version conflicts resolved.** Dead -packages swapped for Meteor-3 equivalents: - -| Old (mongo@1.x / dead) | New | -|---|---| -| `mongo-livedata` | removed (deprecated alias) | -| `facts` | `facts-base` | -| `saucecode:timezoned-synced-cron` | `quave:synced-cron` | -| `aldeed:collection2-core` | `aldeed:collection2@4.0.4` | -| `mys:fonts` (pinned caching-compiler@1.0.0) | removed | -| `fourseven:scss@4.14.1` (node-sass, fails on node 22) | `@5.0.0` (Dart Sass) + `sass` npm | -| `nimble:restivus` (vendored) | accounts-password loosened to `2.0.0 \|\| 3.0.0` | - -SCSS then compiles (Dart Sass; only `@import`/`lighten`/`darken` deprecation -warnings — cosmetic debt: migrate to `@use` + `color.adjust`). - -**Blocked at the client-bundle linker:** `Runtime is not available, but it uses -features needing the runtime: underscore` — one of the old client atmosphere -packages (bert / blaze-react / cookie-consent / meteor-status / bootstrap-4 / -etc.) needs replacing for Meteor 3's linker. Beyond that, the **server -async/await migration is the main remaining effort** (unavoidable for 3.x): - -- `Rest.js` + its helpers (`countRealFires`, `firesUnion`, `whichAreFalsePositives`, - `fireFromHash`, `subscriptionsInsert`, `upsertFalsePositive`) use sync Mongo - (`findOne`, `find().fetch()/.count()`, `insert/update/remove/upsert`) → must - become `*Async`. -- The vendored **restivus must be patched to `await`** async endpoint handlers - (its `_callEndpoint` uses the return value synchronously). -- `methods.js`/`publications.js` across the app, `subsUnion.js`, `migrations.js`, - the Comments feature, and startup files → async. -- `cron.js` `SyncedCron` import (quave export) and `facts.js` `Facts` import - (facts-base export). -- React 16 → 18 render root (`createRoot`) in `startup/client/index.js`. - -This is a multi-hour migration; the smoke test (now `mongosh`-capable, see -`smoke/smoke.sh` on that branch) is the gate. Resume on `meteor3-wip`. - -## Dependency debt (tracked, to resolve in the noted escala) - -| Dep | From | Target | Status | -|---|---|---|---| -| React / react-dom | 16.0 | 18 | **debt** — deferred; ancient react-* libs (react-bootstrap 0.31, reactstrap 5-alpha, react-leaflet 1.8) break on 18. React 16 works through Meteor 2.x/3.x. Own front-end project. | -| Server async/await | Fibers | `*Async` APIs | **pending** — prep for 3.x; 2.x still runs on Fibers so not yet required. | -| raven / flowkey:raven | 2.4 | @sentry/node + @sentry/react | debt — DSN off until GlitchTip/Sentry back (sentry.comunes.org dead → harmless boot-log timeout noise) | -| nodemailer | 4 | 6+ | pending | -| Babel | 7 beta | 7 stable | ✅ done (escala 1) | -| i18next | 10 | current | debt — pin; not on the critical path | -| Leaflet | 1.3.1 | current | debt — pin; not on the critical path | -| arkham:comments-ui | 1.4.x | — | ✅ replaced with in-repo React feature | -| nimble:restivus | Atmosphere | vendored | ✅ local precompiled package, accounts-password 2.x | -| maximum:server-transform, meteorhacks:zones, less, markdown, test stack | — | — | ✅ removed (dead/unused) | -| fourseven:scss | 4.5.4 | 4.14.1 | ✅ node-12 compatible | -| node-gcm | 1.0.2 | — | ✅ removed (dead API, moved to microservice) | - -### Comments React feature — verification note - -The server builds and the REST smoke test is green, and the client bundle -compiles, but the comments UI itself is **not exercised by the smoke test** -(it's not part of the Flutter REST contract). It needs manual staging -verification: on a fire archive page — logged-in post/edit/remove, like/dislike, -and image/YouTube embed. diff --git a/imports/api/Comments/Comments.js b/imports/api/Comments/Comments.js deleted file mode 100644 index a45a80f..0000000 --- a/imports/api/Comments/Comments.js +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-disable import/no-absolute-path */ -import { Mongo } from 'meteor/mongo'; - -/* - * Comments collection (fire-page comments). - * - * Replaces the abandoned `arkham:comments-ui` Atmosphere package (no Meteor 2.x - * build) with a small React + methods implementation. The collection name - * ('comments') and the core document shape are kept so existing production - * documents keep rendering: - * - * { referenceId: 'fire-', content, userId, username, - * media: { type, content }, likes: [userId], dislikes: [userId], - * status: 'approved', createdAt, updatedAt } - * - * Legacy documents may also carry replies[]/starRatings[]/ratingScore/ - * isAnonymous — those features were disabled in this app's config and are - * ignored by the new UI. - */ -const Comments = new Mongo.Collection('comments'); - -// Writes only through the vetted server methods. -Comments.deny({ - insert: () => true, - update: () => true, - remove: () => true -}); - -export default Comments; diff --git a/imports/api/Comments/mediaAnalyzers.js b/imports/api/Comments/mediaAnalyzers.js deleted file mode 100644 index 7b7a10b..0000000 --- a/imports/api/Comments/mediaAnalyzers.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Media analyzers ported from the old arkham:comments-ui package - * (lib/services/media-analyzers). Given a comment's text, detect an embeddable - * image or YouTube URL and return { type, content }. First match wins. - */ - -const imageAnalyzer = { - name: 'image', - getMediaFromContent(content) { - if (content) { - const urls = content.match(/(\S+\.[^/\s]+(\/\S+|\/|))(.jpg|.png|.gif)/g); - if (urls && urls[0]) return urls[0]; - } - return ''; - } -}; - -const youtubeAnalyzer = { - name: 'youtube', - getMediaFromContent(content) { - const parts = (content || '').match(/(?:https?:\/\/)?(?:www\.youtube\.com|youtu\.?be)\/([\w=?]+)/); - let mediaContent = ''; - if (parts && parts[1]) { - let id = parts[1]; - if (id.indexOf('v=') > -1) { - const subParts = id.match(/v=([\w]+)/); - if (subParts && subParts[1]) id = subParts[1]; - } - mediaContent = `https://www.youtube.com/embed/${id}`; - } - return mediaContent; - } -}; - -const analyzers = [imageAnalyzer, youtubeAnalyzer]; - -// Returns { type, content } or {}. -export default function getMediaFromContent(content) { - for (let i = 0; i < analyzers.length; i += 1) { - const mediaContent = analyzers[i].getMediaFromContent(content); - if (mediaContent) return { type: analyzers[i].name, content: mediaContent }; - } - return {}; -} diff --git a/imports/api/Comments/server/index.js b/imports/api/Comments/server/index.js deleted file mode 100644 index 40b4368..0000000 --- a/imports/api/Comments/server/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// Server-side registration for the Comments feature. -import './methods'; -import './publications'; diff --git a/imports/api/Comments/server/methods.js b/imports/api/Comments/server/methods.js deleted file mode 100644 index 49ea3f0..0000000 --- a/imports/api/Comments/server/methods.js +++ /dev/null @@ -1,102 +0,0 @@ -/* eslint-disable import/no-absolute-path */ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import Comments from '/imports/api/Comments/Comments'; -import getMediaFromContent from '/imports/api/Comments/mediaAnalyzers'; -import rateLimit from '/imports/modules/rate-limit'; -import onCommentAdd from '/imports/api/Comments/server/onCommentAdd'; - -const MAX_LEN = 5000; - -function usernameOf(user) { - return (user && user.profile && user.profile.name && user.profile.name.first) - ? user.profile.name.first - : ''; -} - -function requireOwner(commentId, userId) { - const comment = Comments.findOne(commentId); - if (!comment) throw new Meteor.Error('404', 'Comment not found'); - if (comment.userId !== userId) throw new Meteor.Error('403', 'Not your comment'); - return comment; -} - -function toggle(commentId, field, otherField, userId) { - check(commentId, String); - if (!userId) throw new Meteor.Error('403', 'Login required'); - const comment = Comments.findOne(commentId); - if (!comment) throw new Meteor.Error('404', 'Comment not found'); - const has = (comment[field] || []).includes(userId); - const modifier = has - ? { $pull: { [field]: userId } } - : { $addToSet: { [field]: userId }, $pull: { [otherField]: userId } }; - Comments.update(commentId, modifier); -} - -Meteor.methods({ - 'comments.insert': function commentsInsert(referenceId, content) { - check(referenceId, String); - check(content, String); - if (!this.userId) throw new Meteor.Error('403', 'Login required to comment'); - const text = content.trim(); - if (!text) throw new Meteor.Error('400', 'Empty comment'); - if (text.length > MAX_LEN) throw new Meteor.Error('400', 'Comment too long'); - - const now = new Date(); - const doc = { - referenceId, - content: text, - userId: this.userId, - username: usernameOf(Meteor.users.findOne(this.userId)), - media: getMediaFromContent(text), - likes: [], - dislikes: [], - status: 'approved', - createdAt: now, - updatedAt: now - }; - const _id = Comments.insert(doc); - // Fire-and-forget: notify other commenters of this fire. Never let a mail - // failure break the insert. - try { - onCommentAdd({ ...doc, _id }); - } catch (e) { - console.warn(`comments onCommentAdd failed: ${e}`); - } - return _id; - }, - - 'comments.edit': function commentsEdit(commentId, content) { - check(commentId, String); - check(content, String); - if (!this.userId) throw new Meteor.Error('403', 'Login required'); - requireOwner(commentId, this.userId); - const text = content.trim(); - if (!text) throw new Meteor.Error('400', 'Empty comment'); - if (text.length > MAX_LEN) throw new Meteor.Error('400', 'Comment too long'); - Comments.update(commentId, { - $set: { content: text, media: getMediaFromContent(text), updatedAt: new Date() } - }); - }, - - 'comments.remove': function commentsRemove(commentId) { - check(commentId, String); - if (!this.userId) throw new Meteor.Error('403', 'Login required'); - requireOwner(commentId, this.userId); - Comments.remove(commentId); - }, - - 'comments.like': function commentsLike(commentId) { - toggle(commentId, 'likes', 'dislikes', this.userId); - }, - - 'comments.dislike': function commentsDislike(commentId) { - toggle(commentId, 'dislikes', 'likes', this.userId); - } -}); - -rateLimit({ - methods: ['comments.insert', 'comments.edit', 'comments.remove', 'comments.like', 'comments.dislike'], - limit: 5, - timeRange: 1000 -}); diff --git a/imports/api/Comments/server/onCommentAdd.js b/imports/api/Comments/server/onCommentAdd.js deleted file mode 100644 index f0349c8..0000000 --- a/imports/api/Comments/server/onCommentAdd.js +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable import/no-absolute-path */ -import { Meteor } from 'meteor/meteor'; -import i18n from 'i18next'; -import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email'; -import getEmailOf from '/imports/modules/get-email-of-user'; -import Comments from '/imports/api/Comments/Comments'; - -/* - * When a comment is added to a fire, email the other users who commented on the - * same fire (uniq, excluding the author). Ported from the old - * startup/server/comments.js `onEvent` handler. - */ -export default function onCommentAdd(payload) { - const { referenceId, userId } = payload; - const query = { referenceId, userId: { $ne: userId } }; - - Comments.rawCollection().distinct('userId', query).then((users) => { - const path = referenceId.replace(/fire-/, 'fire/archive/'); - const fireUrl = Meteor.absoluteUrl(path); - Meteor.users.find({ _id: { $in: users } }).forEach((user) => { - const { firstName, emailAddress } = getEmailOf(user); - if (emailAddress) { - const emailOpts = { - to: emailAddress, - subject: subjectTruncate.apply(i18n.t('Hay más información sobre un fuego')), - lang: user.lang, - template: 'new-fire-comment', - templateVars: { - applicationName: i18n.t('AppName'), - firstName, - fireUrl - } - }; - sendEmail(emailOpts).catch((error) => { - console.warn(`comments new-fire-comment mail failed: ${error}`); - }); - } - }); - }).catch((error) => { - console.warn(`comments distinct() failed: ${error}`); - }); -} diff --git a/imports/api/Comments/server/publications.js b/imports/api/Comments/server/publications.js deleted file mode 100644 index dbd2b0b..0000000 --- a/imports/api/Comments/server/publications.js +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable import/no-absolute-path */ -/* eslint-disable prefer-arrow-callback */ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import Comments from '/imports/api/Comments/Comments'; - -// Comments for one reference (a fire page), oldest first. -Meteor.publish('comments.forReference', function commentsForReference(referenceId) { - check(referenceId, String); - return Comments.find( - { referenceId }, - { sort: { createdAt: 1 } } - ); -}); diff --git a/imports/api/FalsePositives/server/publications.js b/imports/api/FalsePositives/server/publications.js index b6ca42b..a79ae1a 100644 --- a/imports/api/FalsePositives/server/publications.js +++ b/imports/api/FalsePositives/server/publications.js @@ -84,7 +84,7 @@ const find = (collection, northEastLng, northEastLat, southWestLng, southWestLat return fires; }; -Meteor.publish('falsePositivesMyloc', function falsePositivesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) { +Meteor.publishTransformed('falsePositivesMyloc', function falsePositivesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) { // latitude -90 and 90 and the longitude between -180 and 180 check(northEastLng, NumberBetween(-180, 180)); check(southWestLat, NumberBetween(-90, 90)); @@ -94,7 +94,7 @@ Meteor.publish('falsePositivesMyloc', function falsePositivesInMyLoc(northEastLn return find(FalsePositives, northEastLng, northEastLat, southWestLng, southWestLat); }); -Meteor.publish('industriesMyloc', function industriesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) { +Meteor.publishTransformed('industriesMyloc', function industriesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) { // latitude -90 and 90 and the longitude between -180 and 180 check(northEastLng, NumberBetween(-180, 180)); check(southWestLat, NumberBetween(-90, 90)); diff --git a/imports/modules/server/notificationsProcess.js b/imports/modules/server/notificationsProcess.js new file mode 100644 index 0000000..657aa2a --- /dev/null +++ b/imports/modules/server/notificationsProcess.js @@ -0,0 +1,153 @@ +/* eslint-disable import/no-absolute-path */ + +import { Meteor } from 'meteor/meteor'; +import Notifications from '/imports/api/Notifications/Notifications'; +import i18n from 'i18next'; +import moment from 'moment'; +import { dateLongFormat } from '/imports/api/Common/dates'; +// import sendMail from '/imports/startup/server/email'; +import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email'; +import { isMailServerMaster } from '/imports/startup/server/email'; +// import { hr } from '/imports/startup/server/email'; +import getEmailOf from '/imports/modules/get-email-of-user'; +import image from 'google-maps-image-api-url'; +import gcm from 'node-gcm'; +import { trim } from '/imports/ui/components/NotificationsObserver/util.js'; +import ravenLogger from '/imports/startup/server/ravenLogger'; + +let validFcmSender = true; + +if (!(Meteor.settings.private.fcmApiToken && Meteor.settings.private.fcmApiToken.length > 0)) { + console.warn('Missing settings.private.fcmApiToken key, mobile notifications will not work'); + validFcmSender = false; +} + +// https://www.npmjs.com/package/google-maps-image-api-url +// https://stackoverflow.com/questions/24355007/is-there-no-way-to-embed-a-google-map-into-an-html-email +// https://developers.google.com/maps/documentation/static-maps/intro +function imgUrl(lat, lng) { + return image({ + key: Meteor.settings.gmaps.key, + type: 'staticmap', + center: `${lat},${lng}`, + size: '640x480', + zoom: 16, + maptype: 'hybrid', + language: 'es', + markers: `icon: ${Meteor.settings.private.fireIconUrl}|${lat},${lng}` + }); +} + +/* + function imgEl(lat, lng) { + return ``; + } */ + +// Cutover flag (fase 1a): channels listed in +// Meteor.settings.private.notifDisabledChannels (e.g. ['mobile','web']) are +// handled by the tcef-notifications microservice, so the old observer/cron must +// NOT send them. Mutual exclusion per channel — reversible by editing settings, +// no code deploy needed. ES5-compatible on purpose (Meteor 1.6 / Node 8). +const notifDisabledChannels = (Meteor.settings.private && Meteor.settings.private.notifDisabledChannels) || []; + +const processNotif = (notif) => { + if (notifDisabledChannels.indexOf(notif.type) !== -1) { + // This channel was migrated to tcef-notifications; do nothing here. + return; + } + if (isMailServerMaster && validFcmSender && notif.type === 'mobile' && notif.notified !== true) { + const fcmSender = new gcm.Sender(Meteor.settings.private.fcmApiToken); + const user = Meteor.users.findOne({ _id: notif.userId }); + moment.locale(user.lang); + // duplicate code below + const body = `${trim(notif.content)}`; + + // https://firebase.google.com/docs/cloud-messaging/concept-options + const msg = new gcm.Message(); + + msg.addNotification({ + title: i18n.t('Alerta de fuego'), + body, + click_action: 'FLUTTER_NOTIFICATION_CLICK', + tag: notif._id, // prevent duplication of fire notifications + sound: 'default', // Indicates sound to be played. Supports only default currently. + icon: 'launch_image' // 'ic_launcher' + }); + + msg.addData('id', notif._id._str); + msg.addData('description', body); + msg.addData('lat', notif.geo.coordinates[1]); + msg.addData('lon', notif.geo.coordinates[0]); + msg.addData('when', notif.when); + msg.addData('subsId', notif.subsId._str); + msg.addData('sealed', notif.sealed); + + const registrationTokens = []; + if (!user.fireBaseToken) { + console.warn('This mobile user doesn\'t have a firebase registration token'); + } else { + registrationTokens.push(user.fireBaseToken); + // FIXME: better join users + if (validFcmSender) { + fcmSender.send(msg, { registrationTokens }, Meteor.bindEnvironment(function processResult(err, response) { + if (err) { + console.error(`FCM error: ${err}`); + // FIXME send to sentry + ravenLogger.log(err); + } else { + // console.log(`FCM response: ${response}`); + Notifications.update(notif._id, { $set: { notified: true, notifiedAt: new Date() } }); + } + })); + } + } + } + if (isMailServerMaster && notif.type === 'web' && notif.emailNotified !== true) { + const user = Meteor.users.findOne({ _id: notif.userId }); + const { firstName, emailAddress } = getEmailOf(user); + + if (emailAddress) { + const img = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]); + // const url = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]); + const fireUrl = `${Meteor.absoluteUrl('fire/')}${notif.sealed}`; + // const fireHtmlUrl = `${i18n.t('Más información sobre este fuego')}`; + // TODO get _id of fire + // const fireTextUrl = `${i18n.t('Más información sobre este fuego')}:\n${fireUrl}`; + // FIXME use our map as url and static map as img + moment.locale(user.lang); + // moment user tz ? + const message = `${trim(notif.content)} (${i18n.t('fireDetectedAt', { when: dateLongFormat(notif.when) })}).`; + + // TODO Comunes Address + + const emailOpts = { + to: emailAddress, + // userName: firstName, + // sendAt: new Date(), + subject: subjectTruncate.apply(message), + // text: `${message}\n\n${fireTextUrl}\n\n`, + // template: '

{{appName}}

{{{html}}}', + lang: user.lang, + template: 'new-fire', + templateVars: { + applicationName: i18n.t('AppName'), + firstName, + message, + fireUrl, + img, + subsUrl: Meteor.absoluteUrl('subscriptions') + } + }; + sendEmail(emailOpts).catch((error) => { + throw new Meteor.Error('500', `${error}`); + }); + // sendMail(emailOpts, true); + Notifications.update(notif._id, { $set: { emailNotified: true, emailNotifiedAt: new Date() } }); + } else { + // Not email or not verified -> remove notif so we don't retry to send it + Notifications.remove({ _id: notif._id }); + } + } +}; + +export default processNotif; diff --git a/imports/startup/client/comments.js b/imports/startup/client/comments.js new file mode 100644 index 0000000..94cbe90 --- /dev/null +++ b/imports/startup/client/comments.js @@ -0,0 +1,42 @@ +/* global Comments */ +/* eslint-disable import/no-absolute-path */ + +import i18n from '/imports/startup/client/i18n'; +import '/imports/startup/common/comments'; + +import './comments.scss'; + +i18n.init((err, t) => { + Comments.ui.setContent({ + title: ' ', // i18n.t('Comentarios'), + save: t('Guardar'), + reply: t('Responder'), + edit: t('Editar'), + remove: t('Borrar'), + 'placeholder-textarea': t('Añadir un comentario'), + 'add-button-reply': t('Añadir una respuesta'), + 'add-button': t('Añadir comentario'), + 'you-need-to-login': t('Necesitas iniciar sesión para'), + 'add comments': t('añadir comentarios'), + 'like comments': t('puntuar comentarios'), + 'rate comments': t('puntuar comentarios'), + 'add replies': t('responder'), + 'load-more': t('Más comentarios') + }); + + /* This in client side */ + Comments.ui.config({ + limit: 20, // default 10 + loadMoreCount: 20, // default 20 + /* generateAvatar: function genAvatar(user, isAnonymous) { + if (isAnonymous) { + return i18n.t('Anónimo'); + } + return user.profile && user.profile.name && user.profile.name.first ? user.profile.name.first : null; + }, */ + template: 'bootstrap', // default 'semantic-ui' + // default 'http://s3.amazonaws.com/37assets/svn/765-default-avatar.png' + defaultAvatar: '/default-avatar.png', + markdown: true + }); +}); diff --git a/imports/startup/client/comments.scss b/imports/startup/client/comments.scss new file mode 100644 index 0000000..de93e65 --- /dev/null +++ b/imports/startup/client/comments.scss @@ -0,0 +1,29 @@ +.comments-section { + width: 100%; +} + +.comments-box { + max-width: inherit; + padding: 0; +} + +p.comment-content { + /* white-space: pre-line; */ +} + +/* remove comment btn danger style */ + +div.media-body.comment> div> div> div.btn.btn-danger.remove-action { + background-color: white; + border: 1px solid rgb(204, 204, 204); + color: rgb(51, 51, 51); +} + +/* text-area height */ +.form-control .create-comment { + height: 9em; +} + +.img-avatar { + margin-right: 5px; +} \ No newline at end of file diff --git a/imports/startup/common/comments.js b/imports/startup/common/comments.js new file mode 100644 index 0000000..7b9ab18 --- /dev/null +++ b/imports/startup/common/comments.js @@ -0,0 +1,21 @@ +/* global Comments */ + +// Client and Server +Comments.config({ + rating: 'likes-and-dislikes', + allowAnonymous: () => false, + allowReplies: () => false, // disabled right now: I don't know hot to get referenceId of replies + anonymousSalt: 'klasddl3lala0l3lasdlas0ol3lasdlao3lasdoaslaldal3lasdclasdlal3lasdladlaq', + publishUserFields: { + profile: 1 + }, + mediaAnalyzers: [ + Comments.analyzers.image, + Comments.analyzers.youtube + ], + generateUsername: function genUser(user) { + // console.log(JSON.stringify(user)); + // FIXME + return user.profile && user.profile.name && user.profile.name.first ? user.profile.name.first : ''; + } +}); diff --git a/imports/startup/server/IPGeocoder.js b/imports/startup/server/IPGeocoder.js index 59a99af..96c5ef1 100644 --- a/imports/startup/server/IPGeocoder.js +++ b/imports/startup/server/IPGeocoder.js @@ -21,16 +21,11 @@ function isPrivateIP(ip) { const dbpath = '/usr/local/share/maxmind-geolite2/GeoLite2-City.mmdb'; // const dbpath = `${process.env.PWD}/private/GeoLite2-City.mmdb`; -let IPGeocoder; -if (fs.existsSync(dbpath)) { - IPGeocoder = maxmind.openSync(dbpath); -} else { - // In production the DB is provisioned via cron; in local dev it may be - // absent. Degrade gracefully instead of crashing at boot: localize() below - // already falls back to a default location when no geo data is available. +if (!fs.existsSync(dbpath)) { console.error(`Maxmind db not found ${dbpath}, download via cron with https://www.npmjs.com/package/maxmind-geolite2-mirror`); - IPGeocoder = { get() { return null; } }; } + +const IPGeocoder = maxmind.openSync(dbpath); export default IPGeocoder; // Warning: Meteor cannot access to this.connection with arrow functions @@ -52,7 +47,7 @@ export function localize() { // http://dev.maxmind.com/geoip/geoip2/geolite2/ const geo = IPGeocoder.get(clientIP); // console.warn(geo); - if (geo && geo.location && geo.location.latitude && geo.location.longitude) { + if (geo.location && geo.location.latitude && geo.location.longitude) { return geo; } // geoIP fallback, Madrid diff --git a/imports/startup/server/comments.js b/imports/startup/server/comments.js new file mode 100644 index 0000000..b010405 --- /dev/null +++ b/imports/startup/server/comments.js @@ -0,0 +1,80 @@ +/* global Comments */ +/* eslint-disable import/no-absolute-path */ + +import { Meteor } from 'meteor/meteor'; +import i18n from 'i18next'; +import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email'; +import getEmailOf from '/imports/modules/get-email-of-user'; +import '../common/comments'; + +/* import i18n from 'i18next'; +import moment from 'moment'; +import { dateLongFormat } from '/imports/api/Common/dates'; +import Notifications from '/imports/api/Notifications/Notifications'; +// import sendMail from '/imports/startup/server/email'; +// import { hr } from '/imports/startup/server/email'; +import getOAuthProfile from '/imports/modules/get-oauth-profile'; +import image from 'google-maps-image-api-url'; +import { trim } from '/imports/ui/components/NotificationsObserver/util.js'; */ + +Meteor.startup(() => { + // On Server or Client (preferably on Server) + Comments.config({ + onEvent: (name, action, payload) => { + // e.g send a mail + // console.log(`name: ${name}, action: ${action}, payload: ${JSON.stringify(payload)}`); + + // Samples: + + // name: comment, action: add, payload: {"referenceId":"fire-56a9cc5a5586879eb9cc6dd7","content":"asdfasdf asd fasd fas","userId":"tdK4e43ikjDXct7Gj","isAnonymous":false,"createdAt":"2018-03-04T14:53:40.319Z","likes":[],"dislikes":[],"replies":[],"media":{},"status":"approved","starRatings":[],"ratingScore":0,"_id":"mfAe3HL9qNofhiAnh"} + // name: reply, action: add, payload: {"replyId":"teSLaz4f8RxHjptBm","content":"sas fasdfasdasdf","userId":"tdK4e43ikjDXct7Gj","createdAt":"2018-03-04T14:54:56.965Z","replies":[],"likes":[],"lastUpdatedAt":"2018-03-04T14:54:56.965Z","isAnonymous":false,"status":"approved","media":{},"ratingScore":0,"_id":"DyHt28rF6rTDnC5fP","rootUserId":"tdK4e43ikjDXct7Gj"} + // name: reply, action: like, payload: {"replyId":"teSLaz4f8RxHjptBm","content":"sas fasdfasdasdf","userId":"tdK4e43ikjDXct7Gj","createdAt":"2018-03-04T14:54:56.965Z","replies":[],"likes":["tdK4e43ikjDXct7Gj"],"lastUpdatedAt":"2018-03-04T14:54:56.965Z","isAnonymous":false,"status":"approved","media":{},"ratingScore":1,"_id":"DyHt28rF6rTDnC5fP","ratedUserId":"tdK4e43ikjDXct7Gj","rootUserId":"tdK4e43ikjDXct7Gj"} + // name: comment, action: like, payload: {"_id":"mfAe3HL9qNofhiAnh","ratedUserId":"tdK4e43ikjDXct7Gj"} + // name: reply, action: edit, payload: {"_id":"DyHt28rF6rTDnC5fP","replyId":"teSLaz4f8RxHjptBm","content":"sas fasdfasdasdf dasd fadsfa","userId":"tdK4e43ikjDXct7Gj","createdAt":"2018-03-04T14:54:56.965Z","replies":[],"likes":[],"lastUpdatedAt":"2018-03-04T14:54:56.965Z","isAnonymous":false,"status":"approved","media":{},"ratingScore":0,"starRatings":[],"ratedUserId":"tdK4e43ikjDXct7Gj","rootUserId":"tdK4e43ikjDXct7Gj"} + // name: reply, action: remove, payload: {"_id":"DyHt28rF6rTDnC5fP","rootUserId":"tdK4e43ikjDXct7Gj"} + + // name: (comment|reply) + // action: (add|like|edit|remove) + + if (name === 'comment' && action.match(/add|like|edit/)) { + if (action === 'add') { + // Check for other users that did comments and send an email (uniq users, and not this user) + // console.log(payload.referenceId); + const query = payload.isAnonymous ? + { referenceId: payload.referenceId } : + { referenceId: payload.referenceId, userId: { $ne: payload.userId } }; + Comments.getCollection().rawCollection().distinct('userId', query).then((users) => { + // console.log(users); + const path = payload.referenceId.replace(/fire-/, 'fire/archive/'); + const fireUrl = Meteor.absoluteUrl(path); + // console.log(fireUrl); + Meteor.users.find({ _id: { $in: users } }).forEach((user) => { + const { firstName, emailAddress } = getEmailOf(user); + // console.log(JSON.stringify(user)); + if (emailAddress) { + const emailOpts = { + to: emailAddress, + subject: subjectTruncate.apply(i18n.t('Hay más información sobre un fuego')), + lang: user.lang, + template: 'new-fire-comment', + templateVars: { + applicationName: i18n.t('AppName'), + firstName, + fireUrl + } + }; + sendEmail(emailOpts).catch((error) => { + throw new Meteor.Error('500', `${error}`); + }); + } + }); + }); + } + } + if (name === 'reply' && action.match(/add|like|edit/)) { + // Check for other previous users to this thread and send an email + // console.log('Reply op'); + } + } + }); +}); diff --git a/imports/startup/server/cron.js b/imports/startup/server/cron.js index 0004916..843e55b 100644 --- a/imports/startup/server/cron.js +++ b/imports/startup/server/cron.js @@ -4,6 +4,8 @@ import { Meteor } from 'meteor/meteor'; import { tweetIberiaFires, tweetEuropeFires } from '/imports/api/ActiveFires/server/tweetFiresInZone'; import { isMailServerMaster, sendEmailsFromQueue } from '/imports/startup/server/email'; +import Notifications from '/imports/api/Notifications/Notifications'; +import processNotif from '/imports/modules/server/notificationsProcess.js'; // https://github.com/thesaucecode/meteor-synced-cron/ @@ -53,11 +55,29 @@ Meteor.startup(() => { job: () => sendEmailsFromQueue() }); - // NOTE: the "Process pending notif" job (push via node-gcm + notification - // emails) was removed here — that responsibility now lives in the - // tcef-notifications microservice (fases 1a-1c). See UPGRADE.md for the - // deploy-ordering dependency (the old code must not be shut off in - // production until the new service is emitting). + SyncedCron.add({ + name: 'Process pending notif', + timezone: 'Europe/Madrid', + schedule: (parser) => { + // http://bunkat.github.io/later/ + const sched = parser.text('every 15 min'); + if (sched.error !== -1) { + console.error(`Mail cron 'when' field parsed with errors: ${sched.error}`); + } + return sched; + }, + job: () => { + const mobileNotif = Notifications.find({ nofitied: null, type: 'mobile' }); + mobileNotif.forEach((notif) => { + processNotif(notif); + }); + const emailNotif = Notifications.find({ emailNofitied: null, type: 'web' }); + emailNotif.forEach((notif) => { + processNotif(notif); + }); + return { mobile: mobileNotif.count(), web: emailNotif.count() }; + } + }); } const esEn = Meteor.settings.private.twitter.es.enabled; diff --git a/imports/startup/server/index.js b/imports/startup/server/index.js index 52d2b81..7f84dde 100644 --- a/imports/startup/server/index.js +++ b/imports/startup/server/index.js @@ -6,10 +6,11 @@ import './accounts'; import './api'; import './fixtures'; import './email'; -import '/imports/api/Comments/server'; import './IPGeocoder'; import './migrations'; +import './notificationsObserver'; import './facts'; +import './comments'; import './sitemaps'; import './subsUnion'; import './prerender'; diff --git a/imports/startup/server/migrations.js b/imports/startup/server/migrations.js index 746a894..e3bfac9 100644 --- a/imports/startup/server/migrations.js +++ b/imports/startup/server/migrations.js @@ -1,7 +1,6 @@ -/* global Migrations */ +/* global Migrations, Comments */ /* eslint-disable import/no-absolute-path */ import { Meteor } from 'meteor/meteor'; -import Comments from '/imports/api/Comments/Comments'; import { Accounts } from 'meteor/accounts-base'; import randomHex from 'crypto-random-hex'; import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions'; @@ -164,7 +163,7 @@ Meteor.startup(() => { Migrations.add({ version: 11, up: function noAnonComments() { - Comments.remove({ isAnonymous: true }); + Comments.getCollection().remove({ isAnonymous: true }); } }); diff --git a/imports/startup/server/notificationsObserver.js b/imports/startup/server/notificationsObserver.js new file mode 100644 index 0000000..70be8e0 --- /dev/null +++ b/imports/startup/server/notificationsObserver.js @@ -0,0 +1,19 @@ +/* eslint-disable import/no-absolute-path */ + +import { Meteor } from 'meteor/meteor'; +import Notifications from '/imports/api/Notifications/Notifications'; +import processNotif from '/imports/modules/server/notificationsProcess.js'; +import { isMailServerMaster } from '/imports/startup/server/email'; + +Meteor.startup(() => { + if (isMailServerMaster) { + Notifications.find().observe({ + added: function notifAdded(notif) { + processNotif(notif); + }, + changed: function notifChanged(updatedNotif) { // , oldNotif) { + processNotif(updatedNotif); + } + }); + } +}); diff --git a/imports/startup/server/sitemaps.js b/imports/startup/server/sitemaps.js index 52d6008..8026449 100644 --- a/imports/startup/server/sitemaps.js +++ b/imports/startup/server/sitemaps.js @@ -1,8 +1,8 @@ -/* global sitemaps */ +/* global sitemaps Comments */ /* eslint-disable import/no-absolute-path */ import Fires from '/imports/api/Fires/Fires'; -import Comments from '/imports/api/Comments/Comments'; +// import Comments from '/imports/api/Comments/Comments'; const firesMapEnabled = false; @@ -32,7 +32,7 @@ sitemaps.add('/sitemap.xml', () => { if (firesMapEnabled) { Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => { // Search the last comment of tha fire - const lastComment = Comments.findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } }); + const lastComment = Comments.getCollection().findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } }); out.push({ page: `/fire/archive/${fire._id._str}`, lastmod: lastComment ? lastComment.createdAt : fire.updatedAt diff --git a/imports/ui/components/Comments/Comments.scss b/imports/ui/components/Comments/Comments.scss deleted file mode 100644 index c84305b..0000000 --- a/imports/ui/components/Comments/Comments.scss +++ /dev/null @@ -1,76 +0,0 @@ -.comments-section { - width: 100%; -} - -.comments-box { - max-width: inherit; - padding: 0; -} - -.comments-list { - list-style: none; - padding: 0; - margin: 0; -} - -.comment { - padding: 0.75em 0; - border-bottom: 1px solid rgba(0, 0, 0, 0.08); -} - -.comment-header { - display: flex; - justify-content: space-between; - font-size: 0.9em; -} - -.comment-author { - font-weight: bold; -} - -.comment-date { - color: #888; -} - -p.comment-content { - white-space: pre-line; - margin: 0.4em 0; -} - -.comment-media-image { - max-width: 100%; -} - -.comment-media-youtube iframe { - width: 100%; - min-height: 240px; - border: 0; -} - -.comment-actions .btn { - padding-left: 0; - padding-right: 0.75em; -} - -.comment-actions .btn.active { - font-weight: bold; -} - -/* keep the previous "remove" button neutral (not danger red) */ -.comment-owner-actions .remove-action { - color: rgb(51, 51, 51); -} - -.comment-form .create-comment { - height: 9em; - margin-bottom: 0.5em; -} - -.comment-login-hint { - color: #888; - font-style: italic; -} - -.img-avatar { - margin-right: 5px; -} diff --git a/imports/ui/components/Comments/CommentsBox.js b/imports/ui/components/Comments/CommentsBox.js deleted file mode 100644 index bf005a9..0000000 --- a/imports/ui/components/Comments/CommentsBox.js +++ /dev/null @@ -1,152 +0,0 @@ -/* eslint-disable import/no-absolute-path */ -/* eslint-disable jsx-a11y/media-has-caption */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { withTracker } from 'meteor/react-meteor-data'; -import { translate } from 'react-i18next'; -import { Meteor } from 'meteor/meteor'; -import moment from 'moment'; -import CommentsCollection from '/imports/api/Comments/Comments'; -import './Comments.scss'; - -function MediaEmbed({ media }) { - if (!media || !media.content) return null; - if (media.type === 'image') { - return ; - } - if (media.type === 'youtube') { - return ( -
-