diff --git a/.gitignore b/.gitignore index 603fb12..0f4ef0a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,11 +9,3 @@ npm-debug.log .deploy/ public/locales/undefined/ .deploy-staging -.screenshots -.vscode -output.json - -# fase-0 higiene: variantes de settings con secretos (no versionar) -settings-production-*.json -settings-tests.json -*.json~ diff --git a/README.md b/README.md index 4417536..8d7f41e 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,7 @@ This is web service that notifies about fires detected in an area of your intere Prerrequisites: a running meteor (we share the `fuegos` database with the telegram bot). -Install `meteor` and run `meteor npm install` and `npm start` - -Some other development deps (in debian & ubuntu): -``` -apt-get install libcairo2-dev libjpeg-dev libgif-dev pkg-config -``` +Install `meteor` and run `npm start` ### GeoIP @@ -29,23 +24,18 @@ More platforms and services in the future... We do tests via: ``` -TEST_PORT=3000 TEST_WATCH=1 TEST_CLIENT=0 MONGO_URL=mongodb://localhost:27017/fuegostest meteor --settings settings-development.json test --driver-package meteortesting:mocha --port 3010 +TEST_WATCH=1 MONGO_URL=mongodb://localhost:27017/fuegostest meteor --settings settings-development.json test --driver-package meteortesting:mocha --port 3010 # and -node_modules/.bin/chimp --watch --ddp=http://localhost:3000 --path=cucumber +chimp --watch --ddp=http://localhost:3000 --path=cucumber # and -node_modules/.bin/chimp --ddp=http://localhost:3000 --path=cucumber +chimp --ddp=http://localhost:3000 --path=cucumber ``` -### FAQ & Troubleshooting - -**Q** - I get something like `(...) /node_modules/fibers/future.js:280 (...) Error: Could not locate the bindings file.` What can I do? -**A** - Try `meteor npm rebuild` - ## Data source acknowledgements *We acknowledge the use of data and imagery from LANCE FIRMS operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ*. @@ -56,5 +46,4 @@ GNU APLv3. See our [LICENSE](https://github.com/comunes/todos-contra-el-fuego-we ## Thanks & other acknowlegments -Thanks indeed to: -- Lupe Bao Reixa for Galician translation +(...) diff --git a/cucumber/chimp-config.js b/cucumber/chimp-config.js deleted file mode 100644 index 7ec19dc..0000000 --- a/cucumber/chimp-config.js +++ /dev/null @@ -1,29 +0,0 @@ -module.exports = { - // - - - - CHIMP - - - - - /* watch: false, - * watchTags: '@watch', - * offline: false, - */ - // - - - - CUCUMBER - - - - - /* path: './features', - * jsonOutput: 'output.json', */ - - // '- - - - DEBUGGING - - - - - log: 'info', - debug: false, - seleniumDebug: false, - webdriverLogLevel: false, - // debugBrkCucumber: 5858, - // - - - - WEBDRIVER-IO - - - - - webdriverio: { - waitforTimeout: 10000, - waitforInterval: 250, // KEEP SMALL (!!!) this is the INTERVAL in which waitFor* is looped fo - desiredCapabilities: { - chromeOptions: { - // args: ["headless", "disable-gpu"] - args: ['--disable-gpu', '--no-sandbox', '--headless'] - }, - isHeadless: true - } - } -}; diff --git a/cucumber/features/pages.feature b/cucumber/features/pages.feature index 0807c2c..fd2193b 100644 --- a/cucumber/features/pages.feature +++ b/cucumber/features/pages.feature @@ -40,6 +40,7 @@ Feature: Test all secundary pages | fire/Fe26.2**1a0361ed0384f741403682e26b4bbc3850bee24e775da13c9782b22365ea895f*r_3gmVad5vzkeyqPpo6UcA*1s5fFz3iDGKTYP2RCvXMshof00QCHf4ErDl9K2dxoX0u-J-t6scyOWG8pGp3ehg_FfEtyR_kYcEKU3rE0jaSlZbD09TIvhiIJeS3C6Uc8YD-rit0XBrgsVKfYSxKzTRoOYiYFJ8JYd298hMtfiASePjS05Z58hhicyCcJYYRlarqDScG3LiVY3lL5y2nfcdIMNuSjCiKOJWuMkxwd9nR1UHMudLl0hEoy56mPdnHpDYtP9IYUlIOk1LlWBxcmHKifbXeqHu94p8j13Kk20dh2R49Hw3KsSoE9UbWmGQA9wAZXT82301i3rGF5GPAKjlTlRYcWisQurnPwHSVmx3DhUdiYwKGxt4KeaM5QVI4BE9octvE41OOprB_-Il105diQEh2Y9vdvX51ZVWIRfCboICPM6rJb0Oin7U7F1iM-oD_5s3DGnelfM5LGBcKwiB5paMo5M5vdBMaO-zR216cW9yGVXw9IZqHx8xDQWnoHAZjt8NLHeiGF2QOmIGtEUH7qnwhGpkcvszajmAZzR8saZgoH1qfBfvpVA41YfV14gU**4d030f05e23ad75409cebc609107467fc60be5077d52cf041087cd024fc4dc45*GY97aGFc1MyAsoO3Qxqtgwk9j-MbAPdEGBmEHq6r8VU | Additional information | false | Then I check that all page urls works properly + @watch Scenario: Check that other non visible pages work well Given I logout if logged And a list of non visible pages ids and contents diff --git a/designs/icons-tcef.svg b/designs/icons-tcef.svg index 0835ab8..a35fdfa 100644 --- a/designs/icons-tcef.svg +++ b/designs/icons-tcef.svg @@ -247,7 +247,7 @@ inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.23559314" - inkscape:cx="-2890.4884" + inkscape:cx="-1825.0924" inkscape:cy="-486.23766" inkscape:document-units="mm" inkscape:current-layer="layer1" @@ -621,39 +621,5 @@ y="0" x="0" /> - - - - - - - diff --git a/imports/api/ActiveFires/ActiveFires.js b/imports/api/ActiveFires/ActiveFires.js index 16e2760..79437af 100644 --- a/imports/api/ActiveFires/ActiveFires.js +++ b/imports/api/ActiveFires/ActiveFires.js @@ -1,6 +1,5 @@ /* eslint-disable consistent-return */ -import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import SimpleSchema from 'simpl-schema'; import firesCommonSchema from '../Common/FiresSchema'; @@ -19,11 +18,8 @@ ActiveFires.deny({ remove: () => true }); -const activeFiresSchema = firesCommonSchema; -activeFiresSchema.fireUnion = { type: Meteor.Collection.ObjectID, optional: true, blackbox: true }; - -ActiveFires.schema = new SimpleSchema(activeFiresSchema); +ActiveFires.schema = new SimpleSchema(firesCommonSchema); ActiveFires.attachSchema(ActiveFires.schema); diff --git a/imports/api/ActiveFires/server/countFires.js b/imports/api/ActiveFires/server/countFires.js index b3b8511..39008da 100644 --- a/imports/api/ActiveFires/server/countFires.js +++ b/imports/api/ActiveFires/server/countFires.js @@ -32,9 +32,7 @@ const findFiresInRegion = (zone) => { export const countRealFires = (firesCursor) => { const realFires = []; firesCursor.forEach((fire) => { - if (debug) console.log(`${JSON.stringify(fire)} -----`); const union = firesUnion([fire]); - if (debug) console.log(`${JSON.stringify(union)} -----`); const falsePos = whichAreFalsePositives(FalsePositives, union); const industries = whichAreFalsePositives(Industries, union); if (falsePos.count() === 0 && industries.count() === 0) { diff --git a/imports/api/ActiveFires/server/publications.js b/imports/api/ActiveFires/server/publications.js index 03878b3..21e4e5a 100644 --- a/imports/api/ActiveFires/server/publications.js +++ b/imports/api/ActiveFires/server/publications.js @@ -31,8 +31,7 @@ const activefires = (northEastLng, northEastLat, southWestLng, southWestLat, wit lat: 1, lon: 1, when: 1, - scan: 1, - track: 1 + scan: 1 } }); diff --git a/imports/api/ActiveFiresUnion/ActiveFiresUnion.js b/imports/api/ActiveFiresUnion/ActiveFiresUnion.js deleted file mode 100644 index 2437e54..0000000 --- a/imports/api/ActiveFiresUnion/ActiveFiresUnion.js +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-disable consistent-return */ -/* eslint-disable import/no-absolute-path */ - -import { Mongo } from 'meteor/mongo'; -import SimpleSchema from 'simpl-schema'; -import LocationSchema from '/imports/api/Utility/LocationSchema.js'; -import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js'; - -const ActiveFiresUnion = new Mongo.Collection('activefiresunion', { idGeneration: 'MONGO' }); - -/* Sample: - * - * TODO - * - * */ - -// We have to store polygons and their centroids -// {"type":"Polygon","coordinates":[[[-5.8645,43.5524],[-5.8621,43.5524],[-5.8621,43.5546],[-5.8645,43.5546],[-5.8645,43.5524]]]} - -ActiveFiresUnion.allow({ - insert: () => false, - update: () => false, - remove: () => false -}); - -ActiveFiresUnion.deny({ - insert: () => true, - update: () => true, - remove: () => true -}); - -// https://www.npmjs.com/package/simpl-schema -const GeoJsonSchema = new SimpleSchema({ - type: { - type: String, - allowedValues: ['Polygon'] - }, - coordinates: { - type: Array - }, - 'coordinates.$': { - type: Array - }, - 'coordinates.$.$': { - type: Array - }, - 'coordinates.$.$.$': { - type: Number - } -}); - -const firesUnionSchema = { - centerid: LocationSchema, - - // https://docs.mongodb.com/manual/reference/geojson/ - // : { type: , coordinates: } - shape: GeoJsonSchema, - history: { type: Array, optional: true }, - 'history.$': GeoJsonSchema, - - when: Date, // First date - - createdAt: defaultCreatedAt, - updatedAt: defaultUpdateAt -}; - -ActiveFiresUnion.schema = new SimpleSchema(firesUnionSchema); - -ActiveFiresUnion.attachSchema(ActiveFiresUnion.schema); - -export default ActiveFiresUnion; diff --git a/imports/api/ActiveFiresUnion/server/publications.js b/imports/api/ActiveFiresUnion/server/publications.js deleted file mode 100644 index 6a126d8..0000000 --- a/imports/api/ActiveFiresUnion/server/publications.js +++ /dev/null @@ -1,80 +0,0 @@ -/* global Counter */ -/* eslint-disable import/no-absolute-path */ -/* eslint-disable prefer-arrow-callback */ - -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import { NumberBetween } from '/imports/modules/server/other-checks'; -// import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications'; -// import FalsePositives from '/imports/api/FalsePositives/FalsePositives'; -// import Industries from '/imports/api/Industries/Industries'; -import ActiveFiresUnion from '../ActiveFiresUnion'; - -const counter = new Counter('countActiveFiresUnion', ActiveFiresUnion.find({})); - -Meteor.publish('activefiresuniontotal', function total() { - return counter; -}); - -const activeFiresUnion = (b1, b2, c1, c2, withMarks) => { - // a --- b - // c --- d - const geometry = { - $geometry: { - type: 'Polygon', - coordinates: [[ - [c1, c2], - [c1, b2], - [b1, b2], - [b1, c2], - [c1, c2] - ]] - } - }; - // console.log(JSON.stringify(geometry)); - const fires = ActiveFiresUnion.find({ - shape: { - $geoIntersects: geometry - } - }, { - fields: { - shape: 1, - centerid: 1, - when: 1, - createdAt: 1, - updatedAt: 1 - } - }); - - if (Meteor.isDevelopment) console.log(`Active fires union total: ${fires.count()}`); - - /* - if (withMarks && fires.fetch().length > 0) { - const union = firesUnion(fires); - const falsePos = whichAreFalsePositives(FalsePositives, union); - const industries = whichAreFalsePositives(Industries, union); - return [fires, falsePos, industries]; - } */ - - return fires; -}; - -Meteor.publish('activefiresunionmyloc', function activeInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat, withMarks) { - // latitude -90 and 90 and the longitude between -180 and 180 - check(northEastLng, NumberBetween(-180, 180)); - check(southWestLat, NumberBetween(-90, 90)); - check(southWestLng, NumberBetween(-180, 180)); - check(northEastLat, NumberBetween(-90, 90)); - check(withMarks, Boolean); - - // I use this for fire stats - // if (!Meteor.isDevelopment) return this.ready(); // empty - return activeFiresUnion(northEastLng, northEastLat, southWestLng, southWestLat, withMarks); -}); - -// Warning: this increase always by one the fire stats -Meteor.publish('lastFireUnionDetected', function lastFireDetected() { - // I use this for fire stats - // if (!Meteor.isDevelopment) return this.ready(); // empty - return ActiveFiresUnion.find({}, { limit: 1, sort: { when: -1 } }); -}); diff --git a/imports/api/FalsePositives/methods.js b/imports/api/FalsePositives/methods.js index 85005ba..7aa4ef1 100644 --- a/imports/api/FalsePositives/methods.js +++ b/imports/api/FalsePositives/methods.js @@ -6,29 +6,6 @@ import FalsePositiveTypes from './FalsePositiveTypes'; import Fires from '../Fires/Fires'; import rateLimit from '../../modules/rate-limit'; -export function upsertFalsePositive(keyType, owner, fire) { - const fireType = fire.type; - if (fireType === 'vecinal') { - throw new Meteor.Error('500', 'No se puede marcar este tipo de fuego'); - } - const date = new Date(); - const formated = moment(date).format('YYYYMMDD'); - try { - const set = { - owner, - type: keyType, - when: date, - whendateformat: formated, - fireId: fire._id, - geo: fire.ourid - }; - return FalsePositives.upsert({ geo: fire.ourid, owner }, { $set: set }, { multi: false, upsert: true }); - } catch (exception) { - console.log(exception); - throw new Meteor.Error('500', exception); - } -} - Meteor.methods({ 'falsePositives.insert': function falsePositivesInsert(fireId, keyType) { check(fireId, Meteor.Collection.ObjectID); @@ -40,9 +17,27 @@ Meteor.methods({ } check(this.userId, String); const fire = Fires.findOne(fireId); - const owner = this.userId; if (fire) { - upsertFalsePositive(keyType, owner, fire); + const fireType = fire.type; + if (fireType === 'vecinal') { + throw new Meteor.Error('500', 'No se puede marcar este tipo de fuego'); + } + const date = new Date(); + const formated = moment(date).format('YYYYMMDD'); + try { + const set = { + owner: this.userId, + type: keyType, + when: date, + whendateformat: formated, + fireId, + geo: fire.ourid + }; + return FalsePositives.upsert({ geo: fire.ourid, owner: this.userId }, { $set: set }, { multi: false, upsert: true }); + } catch (exception) { + console.log(exception); + throw new Meteor.Error('500', exception); + } } else { throw new Meteor.Error('500', 'Fuego no encontrado'); } diff --git a/imports/api/FalsePositives/server/publications.js b/imports/api/FalsePositives/server/publications.js index a79ae1a..ca66105 100644 --- a/imports/api/FalsePositives/server/publications.js +++ b/imports/api/FalsePositives/server/publications.js @@ -5,8 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { NumberBetween } from '/imports/modules/server/other-checks'; -import '/imports/startup/server/leaflet-workaround.js'; -import L from 'leaflet'; +import L from 'leaflet-headless'; import calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify'; import Industries from '/imports/api/Industries/Industries'; import FalsePositives from '../FalsePositives'; @@ -18,26 +17,13 @@ Meteor.publish('falsePositivesTotal', function total() { }); export const firesUnion = (fires) => { + const group = new L.FeatureGroup(); const firesArray = Array.isArray(fires) ? fires : fires.fetch(); // if not is a cursor const remap = firesArray.map(function remap(doc) { - const isNASA = doc.type === 'modis' || doc.type === 'viirs'; - const pixelSize = doc.type === 'viirs' ? 0.375 : 1; // viirs has 375m pixel size, modis 1000m - // default 1 km for neightbor alerts - const map = { - location: { lat: doc.lat, lon: doc.lon }, - distance: isNASA ? doc.scan * pixelSize : 1, - distanceY: isNASA ? doc.track * pixelSize : 1 - }; - // console.log(map); - return map; + // default scan: 1 for neightbor alerts + return { location: { lat: doc.lat, lon: doc.lon }, distance: doc.scan || 1 }; }); - const union = calcUnion(L, remap, sub => sub, false); - return union; -}; - -export const zoneToUnion = (lat, lon, distance) => { - const remap = [{ location: { lat, lon }, distance }]; - const union = calcUnion(L, remap, sub => sub, true); + const union = calcUnion(remap, group, sub => sub); return union; }; diff --git a/imports/api/FireAlerts/server/publications.js b/imports/api/FireAlerts/server/publications.js index 6642f35..b0e9412 100644 --- a/imports/api/FireAlerts/server/publications.js +++ b/imports/api/FireAlerts/server/publications.js @@ -33,8 +33,7 @@ Meteor.publish('fireAlerts', function fireAlerts(northEastLng, northEastLat, sou fields: { lat: 1, lon: 1, - scan: 1, - track: 1 + scan: 1 } }); if (Meteor.isDevelopment) console.log(`Neighbour alerts total: ${fires.count()}`); diff --git a/imports/api/Fires/server/publications.js b/imports/api/Fires/server/publications.js index 3862beb..fe315b0 100644 --- a/imports/api/Fires/server/publications.js +++ b/imports/api/Fires/server/publications.js @@ -139,40 +139,35 @@ function logUrl(fireEnc, params) { ravenLogger.log(message); } -export function fireFromHash(fireEnc, params) { - check(fireEnc, String); - check(params, Object); - - // console.log(fireEnc); - // const unsealed = Promise.await(urlEnc.decrypt(fireEnc)); - const unsealed = Promise.await(unsealW(fireEnc)); - if (unsealed === undefined) { - throw Error(`Fail to unseal '${fireEnc}'`); - } - const w = unsealed.when; - unsealed.when = new Date(w); - const c = unsealed.createdAt; - unsealed.createdAt = !c ? new Date() : new Date(c); - const u = unsealed.updatedAt; - unsealed.updatedAt = !u ? new Date() : new Date(u); - // console.log(unsealed); - // FIXME: - const unsealedFix = fixConfidence(unsealed); - FiresCollection.schema.validate(unsealedFix); - return findOrCreateFire(unsealedFix); - /* console.log(`fires: ${fire.count()}`); - * return fire; */ -} - -Meteor.publish('fireFromHash', function fireFromHashFunc(fireEnc, params) { +Meteor.publish('fireFromHash', function fireFromHash(fireEnc, params) { check(fireEnc, String); check(params, Object); try { - return fireFromHash(fireEnc, params); + // console.log(fireEnc); + // const unsealed = Promise.await(urlEnc.decrypt(fireEnc)); + const unsealed = Promise.await(unsealW(fireEnc)); + if (unsealed === undefined) { + logUrl(fireEnc, params); + // https://guide.meteor.com/data-loading.html + return this.ready(); + } + const w = unsealed.when; + unsealed.when = new Date(w); + const c = unsealed.createdAt; + unsealed.createdAt = !c ? new Date() : new Date(c); + const u = unsealed.updatedAt; + unsealed.updatedAt = !u ? new Date() : new Date(u); + // console.log(unsealed); + // FIXME: + const unsealedFix = fixConfidence(unsealed); + FiresCollection.schema.validate(unsealedFix); + return findOrCreateFire(unsealedFix); + /* console.log(`fires: ${fire.count()}`); + * return fire; */ } catch (e) { console.warn(e); logUrl(fireEnc, params); - // https://guide.meteor.com/data-loading.html + return this.ready(); } }); diff --git a/imports/api/Notifications/Notifications.js b/imports/api/Notifications/Notifications.js index 0355540..0c89c55 100644 --- a/imports/api/Notifications/Notifications.js +++ b/imports/api/Notifications/Notifications.js @@ -2,7 +2,6 @@ /* eslint-disable import/no-absolute-path */ import { Mongo } from 'meteor/mongo'; -import { Meteor } from 'meteor/meteor'; import SimpleSchema from 'simpl-schema'; import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js'; import LocationSchema from '/imports/api/Utility/LocationSchema.js'; @@ -23,12 +22,11 @@ Notifications.deny({ Notifications.schema = new SimpleSchema({ userId: String, - subsId: { type: Meteor.Collection.ObjectID, optional: true, blackbox: true }, content: String, geo: LocationSchema, type: String, - notified: { type: Boolean, optional: true }, - notifiedAt: { type: Date, optional: true }, + webNotified: { type: Boolean, optional: true }, + webNotifiedAt: { type: Date, optional: true }, emailNotified: { type: Boolean, optional: true }, emailNotifiedAt: { type: Date, optional: true }, when: Date, diff --git a/imports/api/Notifications/methods.js b/imports/api/Notifications/methods.js index 05d7d08..604c6e0 100644 --- a/imports/api/Notifications/methods.js +++ b/imports/api/Notifications/methods.js @@ -8,7 +8,7 @@ Meteor.methods({ check(notifId, Meteor.Collection.ObjectID); try { - Notifications.update(notifId, { $set: { notified: true, notifiedAt: new Date() } }); + Notifications.update(notifId, { $set: { webNotified: true, webNotifiedAt: new Date() } }); return notifId; } catch (exception) { throw new Meteor.Error('500', exception); diff --git a/imports/api/Notifications/server/publications.js b/imports/api/Notifications/server/publications.js index d868692..83ceaaf 100644 --- a/imports/api/Notifications/server/publications.js +++ b/imports/api/Notifications/server/publications.js @@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor'; import Notifications from '../Notifications'; Meteor.publish('mynotifications', function notifications() { - const notif = Notifications.find({ userId: this.userId, type: 'web', notified: null }); + const notif = Notifications.find({ userId: this.userId, type: 'web', webNotified: null }); // console.log(`Notifications for user ${this.userId}: ${notif.count()}`); return notif; }); diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js deleted file mode 100644 index ae5e383..0000000 --- a/imports/api/Rest/Rest.js +++ /dev/null @@ -1,457 +0,0 @@ -/* global Restivus */ -/* eslint-disable import/no-absolute-path */ - -import { Meteor } from 'meteor/meteor'; -import { NumberBetween } from '/imports/modules/server/other-checks'; -import Fires from '/imports/api/Fires/Fires'; -import { check } from 'meteor/check'; -import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; -import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires'; -import { countRealFires } from '/imports/api/ActiveFires/server/countFires'; -import { whichAreFalsePositives, firesUnion, zoneToUnion } from '/imports/api/FalsePositives/server/publications'; -import { fireFromHash } from '/imports/api/Fires/server/publications.js'; -import FalsePositives from '/imports/api/FalsePositives/FalsePositives'; -import Industries from '/imports/api/Industries/Industries'; -import Subscriptions from '/imports/api/Subscriptions/Subscriptions'; -import jsend from 'jsend'; -import { Random } from 'meteor/random'; -import { subscriptionsInsert } from '/imports/api/Subscriptions/methods.js'; -import { Mongo } from 'meteor/mongo'; -import { upsertFalsePositive } from '/imports/api/FalsePositives/methods.js'; - -const debug = false; - -const uptime = new Date(); - -const restivusError = (code, message) => ({ status: 'error', statusCode: code, body: message }); - - -function failMsg(msg) { - return restivusError(500, `Unexpected error in REST call: ${msg}`); -} - -function fail(e) { - return restivusError(500, `Unexpected error in REST call: ${e}`); -} - -function defaultFailParams(e) { - return restivusError(400, `Wrong REST params: ${e}`); -} - -function checkAuthToken(token) { - if (!Meteor.settings.private.internalApiToken || token !== Meteor.settings.private.internalApiToken) { - const message = 'Unauthorized auth token in REST API'; - console.warn(message); - return restivusError(401, message); - } - return undefined; -} - -function checkLatLonDist(km, lat, lng) { - check(lng, NumberBetween(-180, 180)); - check(lat, NumberBetween(-90, 90)); - check(km, NumberBetween(0, Meteor.isDevelopment ? 1000 : 100)); -} - -if (!Meteor.settings.private.internalApiToken) { - console.warn('Meteor.settings.private.internalApiToken is not configured so we don\'t enable our REST API'); -} else { -// export - const apiV1 = new Restivus({ - useDefaultAuth: true, - apiPath: 'api', - version: 'v1', - prettyJson: true - }); - - // Generates: POST on /api/users and GET, DELETE /api/users/:id for - // Meteor.users collection - /* apiV1.addCollection(Meteor.users, { - * excludedEndpoints: ['getAll', 'put', 'delete', 'patch', 'get'], - * routeOptions: { - * authRequired: true - * }, - * endpoints: { - * post: { - * authRequired: false - * }, - * delete: { - * roleRequired: 'admin' - * } - * } - * }); */ - - apiV1.addCollection(Fires, { - excludedEndpoints: ['put', 'post', 'patch', 'delete'], - // excludedEndpoints: ['getAll', 'put', 'post', 'patch', 'delete'], - routeOptions: { - authRequired: false - }, - endpoints: { - get: { - action: function getFire() { - return Fires.findOne(new Meteor.Collection.ObjectID(this.urlParams.id)); - } - } - } - }); - - // Maps to: /api/v1/status/last-fire-check - apiV1.addRoute('status/last-fire-check', { authRequired: false }, { - get: function get() { - return SiteSettings.findOne({ name: 'last-fire-check' }); - } - }); - - // Maps to: /api/v1/status/last-fire-detected - apiV1.addRoute('status/last-fire-detected', { authRequired: false }, { - get: function get() { - return ActiveFiresCollection.findOne({}, { sort: { when: -1 } }); - } - }); - - // Maps to: /api/v1/status/active-fires-count - apiV1.addRoute('status/active-fires-count', { authRequired: false }, { - get: function get() { - return { total: ActiveFiresCollection.find({}).count() }; - } - }); - - // Maps to: /api/v1/status/uptime - apiV1.addRoute('status/uptime', { authRequired: false }, { - get: function get() { - return { ms: new Date() - uptime }; - } - }); - - function getFires(route, full) { - const lat = Number(route.urlParams.lat); - const lng = Number(route.urlParams.lng); - const km = Number(route.urlParams.km); - const { token } = route.urlParams; - try { - checkLatLonDist(km, lat, lng); - check(token, String); - } catch (e) { - return defaultFailParams(e); - } - - const failed = checkAuthToken(token); - if (failed) return failed; - - if (debug) console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`); - - const fires = ActiveFiresCollection.find({ - ourid: { - $near: { - $geometry: { - type: 'Point', - coordinates: [lng, lat] - }, - $maxDistance: km * 1000, - $minDistance: 0 - } - } - }, { - fields: { - lat: 1, - lon: 1, - when: 1, - type: 1, // modis, viirs, vecinal - scan: 1, - track: 1 - } - }); - - const result = { total: fires.count(), real: countRealFires(fires) }; - if (debug) console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius ${result}`); - - if (!full) { - return result; - } - - let union; - - // TODO only get real - const firesA = fires.fetch(); - - if (firesA.length > 0) { - union = firesUnion(fires); - } else { - union = zoneToUnion(lat, lng, km); - } - const falsePos = whichAreFalsePositives(FalsePositives, union); - const industries = whichAreFalsePositives(Industries, union); - result.fires = firesA; - result.industries = industries.fetch(); - result.falsePos = falsePos.fetch(); - return result; - } - - // Maps to: /api/v1/fires-in/:lat/:lng/:km - // 100 km max - // Ex: http://127.0.0.1:3000/api/v1/fires-in/token/38.736946/-9.142685/100 - apiV1.addRoute('fires-in/:token/:lat/:lng/:km', { authRequired: false }, { - get: function get() { - return getFires(this, false); - } - }); - - apiV1.addRoute('fires-in-full/:token/:lat/:lng/:km', { authRequired: false }, { - get: function get() { - return getFires(this, true); - } - }); - - // Add mobile user: - // curl -X POST http://localhost:3000/api/v1/users/mobile -d "token: thisAppAutToken" -d "mobileToken: user-mobile-firebase-token" - // Response: - // - // https://docs.meteor.com/api/passwords.html#Accounts-createUser - apiV1.addRoute('mobile/users', { authRequired: false }, { - post: function post() { - const { token, mobileToken, lang } = this.bodyParams; - try { - check(token, String); - check(lang, String); - check(mobileToken, String); - } catch (e) { - return defaultFailParams(e); - } - - const failed = checkAuthToken(token); - if (failed) return failed; - - let username; - - const already = Meteor.users.find({ fireBaseToken: mobileToken }); - - if (already.count() > 1) { - return restivusError(500, 'Unexpected error in REST call: several users with that mobile token?'); - } else if (already.count() === 1) { - username = already.fetch()[0].username; - } else { - do { - username = Random.id(15); - } while (Meteor.users.find({ username }).count() !== 0); - } - - // FIXME check valid lang - - const now = new Date(); - - const result = Meteor.users.upsert({ - fireBaseToken: mobileToken - }, { - $set: { - username, - fireBaseToken: mobileToken, - lang, - profile: { }, - createdAt: now, - updatedAt: now - } - }); - - - if (debug) { - console.log(this.bodyParams); - console.log(this.urlParams); - console.log(this.queryParams); - } - - const upsertUser = Meteor.users.findOne({ username }); - - if (debug) console.log(upsertUser); - - return jsend.success({ - upsertResult: result, - username, - userId: upsertUser._id, - lang: upsertUser.lang, - mobileToken: upsertUser.fireBaseToken - }); - } - - }); - - // max sitance: 100km - apiV1.addRoute('mobile/subscriptions', { authRequired: false }, { - post: function post() { - const { - token, - mobileToken, - id, - lat, - lon, - distance - } = this.bodyParams; - let oid; - try { - check(token, String); - check(mobileToken, String); - check(id, String); - oid = new Mongo.ObjectID(id); - check(oid, Meteor.Collection.ObjectID); - check(lat, Number); - check(lon, Number); - check(distance, Number); - checkLatLonDist(distance, lat, lon); - } catch (e) { - if (debug) console.log(e); - return defaultFailParams(e); - } - - const failed = checkAuthToken(token); - if (failed) return failed; - - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failMsg('User not found'); - - const newSubs = {}; - newSubs._id = new Meteor.Collection.ObjectID(id); - newSubs.location = {}; - newSubs.location.lat = lat; - newSubs.location.lon = lon; - newSubs.distance = distance; - - let result; - try { - result = subscriptionsInsert(newSubs, user._id, 'mobile'); - } catch (e) { - return fail(e); - } - return jsend.success({ subsId: result._str }); - } - }); - - apiV1.addRoute('mobile/subscriptions/:token/:mobileToken/:subsId', { authRequired: false }, { - delete: function del() { - const { - token, - mobileToken, - subsId - } = this.urlParams; - try { - check(token, String); - check(mobileToken, String); - check(subsId, String); - } catch (e) { - return defaultFailParams(e); - } - - const failed = checkAuthToken(token); - if (failed) return failed; - - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failMsg('User not found'); - - try { - Subscriptions.remove({ owner: user._id, _id: new Meteor.Collection.ObjectID(subsId) }); - } catch (e) { - return fail(e); - } - - return jsend.success({}); - } - }); - - apiV1.addRoute('mobile/subscriptions/all/:token/:mobileToken', { authRequired: false }, { - get: function get() { - const { token, mobileToken } = this.urlParams; - try { - check(token, String); - check(mobileToken, String); - } catch (e) { - return defaultFailParams(e); - } - - const failed = checkAuthToken(token); - if (failed) return failed; - - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failMsg('User not found'); - - const result = Subscriptions.find({ owner: user._id }); - - return jsend.success({ subscriptions: result.fetch(), count: result.count() }); - }, - delete: function delAll() { - const { token, mobileToken } = this.urlParams; - try { - check(token, String); - check(mobileToken, String); - } catch (e) { - return defaultFailParams(e); - } - - const failed = checkAuthToken(token); - if (failed) return failed('Auth api check failed'); - - if (Meteor.users.find({ fireBaseToken: mobileToken }).count() !== 1) return fail; - - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failMsg('User not found'); - - const toRemove = Subscriptions.find({ owner: user._id }).count(); - Subscriptions.remove({ owner: user._id }); - - return jsend.success({ count: toRemove }); - } - }); - - apiV1.addRoute('status/subs-public-union/:token', { authRequired: false }, { - get: function get() { - const { token } = this.urlParams; - try { - check(token, String); - } catch (e) { - return defaultFailParams(e); - } - - const failed = checkAuthToken(token); - if (failed) return failed; - - const currentUnion = SiteSettings.findOne({ name: 'subs-public-union' }); - const userSubsBounds = SiteSettings.findOne({ name: 'subs-public-union-bounds' }); - - return jsend.success({ union: currentUnion, bounds: userSubsBounds }); - } - }); - - apiV1.addRoute('mobile/falsepositive', { authRequired: false }, { - post: function post() { - const { - token, - mobileToken, - sealed, - type - } = this.bodyParams; - try { - check(token, String); - check(mobileToken, String); - check(sealed, String); - check(type, String); - } catch (e) { - if (debug) console.log(e); - return defaultFailParams(e); - } - - const failed = checkAuthToken(token); - if (failed) return failed; - - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failMsg('User not found'); - - console.log(`Marking hash fire as '${type}' false positive: ${sealed}`); - const fireC = fireFromHash(sealed, { id: sealed }); - if (fireC && typeof fireC === 'object') { - const fire = fireC.fetch()[0]; - console.log(`Marking fire as false positive: ${fire._id}`); - const result = upsertFalsePositive(type, user._id, fire); - return jsend.success({ upsert: result }); - } - return failMsg('Cannot mark fire as false positive'); - } - }); -} diff --git a/imports/api/Subscriptions/Subscriptions.js b/imports/api/Subscriptions/Subscriptions.js index c69e20b..eb4be16 100644 --- a/imports/api/Subscriptions/Subscriptions.js +++ b/imports/api/Subscriptions/Subscriptions.js @@ -1,7 +1,6 @@ /* eslint-disable consistent-return */ /* eslint-disable import/no-absolute-path */ -import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import SimpleSchema from 'simpl-schema'; import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js'; @@ -43,7 +42,6 @@ Subscriptions.deny({ Subscriptions.schema = new SimpleSchema({ - _id: { type: Meteor.Collection.ObjectID, optional: true, blackbox: true }, location: Object, 'location.lat': Number, 'location.lon': Number, diff --git a/imports/api/Subscriptions/methods.js b/imports/api/Subscriptions/methods.js index cf927e0..618ed21 100644 --- a/imports/api/Subscriptions/methods.js +++ b/imports/api/Subscriptions/methods.js @@ -10,49 +10,30 @@ function geo(doc) { }; } -export function subscriptionsInsert(doc, userId, type) { - check(doc, { - _id: Match.Maybe(Meteor.Collection.ObjectID), - location: Match.ObjectIncluding({ lat: Number, lon: Number }), - distance: Number - }); - const newDoc = { - owner: userId, - type, - geo: geo(doc), - ...doc - }; - // console.log(newDoc); - const already = Subscriptions.findOne(newDoc); - if (already) { - throw new Meteor.Error('on-already-subscribed', 'The user is already subscribed to this area'); - } - try { - return Subscriptions.insert(newDoc); - } catch (exception) { - // console.error(exception); - throw new Meteor.Error('500', exception); - } -} - -export function subscriptionsRemove(subscriptionId) { - check(subscriptionId, Meteor.Collection.ObjectID); - - try { - return Subscriptions.remove(subscriptionId); - } catch (exception) { - console.error(exception); - throw new Meteor.Error('500', exception); - } -} - Meteor.methods({ - 'subscriptions.insert': function subscriptionInsertMethod(doc) { + 'subscriptions.insert': function subscriptionsInsert(doc) { check(doc, { location: Match.ObjectIncluding({ lat: Number, lon: Number }), distance: Number }); - return subscriptionsInsert(doc, this.userId, 'web'); + const type = 'web'; + const newDoc = { + owner: this.userId, + type, + geo: geo(doc), + ...doc + }; + // console.log(newDoc); + const already = Subscriptions.findOne(newDoc); + if (already) { + throw new Meteor.Error('on-already-subscribed', 'The user is already subscribed to this area'); + } + try { + return Subscriptions.insert(newDoc); + } catch (exception) { + // console.error(exception); + throw new Meteor.Error('500', exception); + } }, 'subscriptions.update': function subscriptionsUpdate(doc) { check(doc, { @@ -71,9 +52,15 @@ Meteor.methods({ throw new Meteor.Error('500', exception); } }, - 'subscriptions.remove': function subscriptionsRemoveMethod(subscriptionId) { + 'subscriptions.remove': function subscriptionsRemove(subscriptionId) { check(subscriptionId, Meteor.Collection.ObjectID); - return subscriptionsRemove(subscriptionId); + + try { + return Subscriptions.remove(subscriptionId); + } catch (exception) { + console.error(exception); + throw new Meteor.Error('500', exception); + } } }); diff --git a/imports/api/Users/Users.js b/imports/api/Users/Users.js index 8911a47..c6858ae 100644 --- a/imports/api/Users/Users.js +++ b/imports/api/Users/Users.js @@ -107,7 +107,6 @@ const schemaUser = new SimpleSchema({ telegramUsername: { type: String, optional: true }, telegramFirstName: { type: String, optional: true }, telegramLanguageCode: { type: String, optional: true }, - fireBaseToken: { type: String, optional: true }, createdAt: defaultCreatedAt, updatedAt: defaultUpdateAt }); diff --git a/imports/modules/server/notificationsProcess.js b/imports/modules/server/notificationsProcess.js deleted file mode 100644 index 657aa2a..0000000 --- a/imports/modules/server/notificationsProcess.js +++ /dev/null @@ -1,153 +0,0 @@ -/* 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/i18n.js b/imports/startup/client/i18n.js index 384053f..2fdc6b4 100644 --- a/imports/startup/client/i18n.js +++ b/imports/startup/client/i18n.js @@ -17,7 +17,7 @@ import i18nOpts from '../common/i18n'; const detectorOptions = { // order and from where user language should be detected - order: ['querystring', 'navigator', 'cookie', 'localStorage', 'htmlTag'], + order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'], // keys or params to lookup language from lookupQuerystring: 'lng', @@ -61,14 +61,6 @@ if (sendMissing && Meteor.isDevelopment) { }; } -function setT9(lang) { - if (lang === 'gl') { - T9n.setLanguage('es'); - } else { - T9n.setLanguage(lang); - } -} - i18n.use(backend) .use(LngDetector) .use(Cache) @@ -83,7 +75,7 @@ i18n.use(backend) // document.title = t('AppName'); // Accounts translation // https://github.com/softwarerero/meteor-accounts-t9n - setT9(i18n.language); + T9n.setLanguage(i18n.language); // console.log(T9n.get('error.accounts.User not found')); moment.locale(i18n.language); @@ -112,8 +104,7 @@ Meteor.subscribe('userData'); // lang is there i18n.on('languageChanged', (lng) => { moment.locale(lng); - // TODO fix this when gl is translated - setT9(lng); + T9n.setLanguage(lng); Tracker.autorun((computation) => { if (Meteor.userId()) { // logged diff --git a/imports/startup/common/i18n.js b/imports/startup/common/i18n.js index 218e46d..9be09fd 100644 --- a/imports/startup/common/i18n.js +++ b/imports/startup/common/i18n.js @@ -3,8 +3,6 @@ import moment from 'moment'; // Load the js langs import es from 'meteor-accounts-t9n/build/es'; import en from 'meteor-accounts-t9n/build/en'; -// TODO ask for translation of this -// import gl from 'meteor-accounts-t9n/build/gl'; const backOpts = { // path where resources get loaded from @@ -23,10 +21,7 @@ const shouldDebug = (forceDebug && !Meteor.isProduction); const i18nOpts = { backend: backOpts, // lng: 'es', - fallbackLng: { - gl: ['es'], - default: ['en'] - }, + fallbackLng: ['es', 'en'], sendMissingTo: 'fallback', interpolation: { escapeValue: false, // not needed for react!! @@ -40,7 +35,7 @@ const i18nOpts = { return value; } }, - whitelist: ['es', 'en', 'gl'], // allowed languages + whitelist: ['es', 'en'], // allowed languages load: 'languageOnly', // 'es' o 'en', previously: 'all', // es-ES -> es, en-US -> en debug: shouldDebug, ns: 'common', diff --git a/imports/startup/server/api.js b/imports/startup/server/api.js index a6044b4..73f1fa2 100644 --- a/imports/startup/server/api.js +++ b/imports/startup/server/api.js @@ -9,7 +9,6 @@ import '../../api/Users/server/publications'; import '../../api/Utility/server/methods'; import '../../api/ActiveFires/server/publications'; -import '../../api/ActiveFiresUnion/server/publications'; import '../../api/FireAlerts/server/publications'; import '../../api/Subscriptions/methods'; diff --git a/imports/startup/server/cron.js b/imports/startup/server/cron.js index 843e55b..9b0fa77 100644 --- a/imports/startup/server/cron.js +++ b/imports/startup/server/cron.js @@ -4,8 +4,6 @@ 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/ @@ -46,7 +44,7 @@ Meteor.startup(() => { timezone: 'Europe/Madrid', schedule: (parser) => { // http://bunkat.github.io/later/ - const sched = parser.text('every 10 min'); + const sched = parser.text('every 1 min'); if (sched.error !== -1) { console.error(`Mail cron 'when' field parsed with errors: ${sched.error}`); } @@ -54,30 +52,6 @@ Meteor.startup(() => { }, job: () => sendEmailsFromQueue() }); - - 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 7f84dde..dd4f913 100644 --- a/imports/startup/server/index.js +++ b/imports/startup/server/index.js @@ -17,4 +17,5 @@ import './prerender'; import './feedback'; import './cron'; import './isMaster'; +import './segfaults'; import './rest'; diff --git a/imports/startup/server/leaflet-workaround.js b/imports/startup/server/leaflet-workaround.js deleted file mode 100644 index 740aa3b..0000000 --- a/imports/startup/server/leaflet-workaround.js +++ /dev/null @@ -1,19 +0,0 @@ -// imports/startup/server/leaflet-workaround.js -global.window = { - screen: { - deviceXDPI: 1, - logicalXDPI: 1 - } -}; -global.document = { - documentElement: { - style: {} - }, - createElement: () => { - return {}; - } -}; -global.navigator = { - userAgent: 'foo', - platform: 'bar' -}; diff --git a/imports/startup/server/migrations.js b/imports/startup/server/migrations.js index e3bfac9..158a75c 100644 --- a/imports/startup/server/migrations.js +++ b/imports/startup/server/migrations.js @@ -9,8 +9,6 @@ import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; import FalsePositives from '/imports/api/FalsePositives/FalsePositives'; import Industries from '/imports/api/Industries/Industries'; import IndustryRegistries from '/imports/api/Industries/IndustryRegistries'; -import Notifications from '/imports/api/Notifications/Notifications'; -import ActiveFiresUnion from '/imports/api/ActiveFiresUnion/ActiveFiresUnion'; import { Mongo } from 'meteor/mongo'; Meteor.startup(() => { @@ -229,30 +227,6 @@ Meteor.startup(() => { } }); - Migrations.add({ - version: 17, - up: function renameWebNotifiedField() { - Notifications.update({ webNotified: { $exists: true } }, { - $rename: { webNotifiedAt: 'notifiedAt' } - }, { upsert: false, multi: true }); - Notifications.update({ webNotified: { $exists: true } }, { - $rename: { webNotified: 'notified' } - }, { upsert: false, multi: true }); - } - }); - - Migrations.add({ - version: 18, - up: () => { - const raw = ActiveFiresUnion.rawCollection(); - raw.createIndex({ centerid: '2dsphere' }); - raw.createIndex({ shape: '2dsphere' }); - raw.createIndex({ when: 1 }); - raw.createIndex({ createdAt: 1 }); - raw.createIndex({ updatedAt: 1 }); - } - }); - // Set createdAt in users & subs Migrations.migrateTo('latest'); diff --git a/imports/startup/server/notificationsObserver.js b/imports/startup/server/notificationsObserver.js index 70be8e0..85cd531 100644 --- a/imports/startup/server/notificationsObserver.js +++ b/imports/startup/server/notificationsObserver.js @@ -1,19 +1,92 @@ /* eslint-disable import/no-absolute-path */ +import i18n from 'i18next'; import { Meteor } from 'meteor/meteor'; +import moment from 'moment'; +import { dateLongFormat } from '/imports/api/Common/dates'; import Notifications from '/imports/api/Notifications/Notifications'; -import processNotif from '/imports/modules/server/notificationsProcess.js'; -import { isMailServerMaster } from '/imports/startup/server/email'; +// import sendMail from '/imports/startup/server/email'; +import sendEmail, { subjectTruncate } from '/imports/modules/server/send-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 { trim } from '/imports/ui/components/NotificationsObserver/util.js'; Meteor.startup(() => { - if (isMailServerMaster) { - Notifications.find().observe({ - added: function notifAdded(notif) { - processNotif(notif); - }, - changed: function notifChanged(updatedNotif) { // , oldNotif) { - processNotif(updatedNotif); - } + // 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 ``; + } */ + + function process(notif) { + if (notif.type === 'web' && !notif.emailNotified) { + 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() } }); + } + } + } + + Notifications.find().observe({ + added: function notifAdded(notif) { + process(notif); + }, + changed: function notifChanged(updatedNotif, oldNotif) { + process(updatedNotif); + } + }); }); diff --git a/imports/startup/server/rest.js b/imports/startup/server/rest.js index 64e8172..423922f 100644 --- a/imports/startup/server/rest.js +++ b/imports/startup/server/rest.js @@ -1 +1,118 @@ -import '../../api/Rest/Rest.js'; +/* global Restivus */ +/* eslint-disable import/no-absolute-path */ + +import { Meteor } from 'meteor/meteor'; +import { NumberBetween } from '/imports/modules/server/other-checks'; +import Fires from '/imports/api/Fires/Fires'; +import { check } from 'meteor/check'; +import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; +import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires'; +import { countRealFires } from '/imports/api/ActiveFires/server/countFires'; + +Meteor.startup(() => { + const uptime = new Date(); + + const apiV1 = new Restivus({ + useDefaultAuth: true, + apiPath: 'api', + version: 'v1', + prettyJson: true + }); + + // Generates: POST on /api/users and GET, DELETE /api/users/:id for + // Meteor.users collection + /* apiV1.addCollection(Meteor.users, { + * excludedEndpoints: ['getAll', 'put', 'delete', 'patch', 'get'], + * routeOptions: { + * authRequired: true + * }, + * endpoints: { + * post: { + * authRequired: false + * }, + * delete: { + * roleRequired: 'admin' + * } + * } + * }); */ + + apiV1.addCollection(Fires, { + excludedEndpoints: ['put', 'post', 'patch', 'delete'], + // excludedEndpoints: ['getAll', 'put', 'post', 'patch', 'delete'], + routeOptions: { + authRequired: false + }, + endpoints: { + get: { + action: function getFire() { + return Fires.findOne(new Meteor.Collection.ObjectID(this.urlParams.id)); + } + } + } + }); + + // Maps to: /api/v1/status/last-fire-check + apiV1.addRoute('status/last-fire-check', { authRequired: false }, { + get: function get() { + return SiteSettings.findOne({ name: 'last-fire-check' }); + } + }); + + // Maps to: /api/v1/status/last-fire-detected + apiV1.addRoute('status/last-fire-detected', { authRequired: false }, { + get: function get() { + return ActiveFiresCollection.findOne({}, { sort: { when: -1 } }); + } + }); + + // Maps to: /api/v1/status/last-fire-detected + apiV1.addRoute('status/active-fires-count', { authRequired: false }, { + get: function get() { + return { total: ActiveFiresCollection.find({}).count() }; + } + }); + + // Maps to: /api/v1/status/uptime + apiV1.addRoute('status/uptime', { authRequired: false }, { + get: function get() { + return { ms: new Date() - uptime }; + } + }); + + // Maps to: /api/v1/fires-in/:lat/:lng/:km + // Ex: http://127.0.0.1:3000/api/v1/fires-in/38.736946/-9.142685/100 + apiV1.addRoute('fires-in/:token/:lat/:lng/:km', { authRequired: false }, { + get: function get() { + const lat = Number(this.urlParams.lat); + const lng = Number(this.urlParams.lng); + const km = Number(this.urlParams.km); + const { token } = this.urlParams; + check(lng, NumberBetween(-180, 180)); + check(lat, NumberBetween(-90, 90)); + check(km, NumberBetween(0, 100)); + check(token, String); + + if (token !== Meteor.settings.private.internalApiToken) { + console.warn(`WARNING: Query for fires in ${lat}, ${lng} in ${km} km radius with wrong token`); + return {}; + } + + console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`); + + const fires = ActiveFiresCollection.find({ + ourid: { + $near: { + $geometry: { + type: 'Point', + coordinates: [lng, lat] + }, + $maxDistance: km * 1000, + $minDistance: 0 + } + } + }); + + return { total: fires.count(), real: countRealFires(fires) }; + } + }); +}); diff --git a/imports/startup/server/segfaults.js b/imports/startup/server/segfaults.js new file mode 100644 index 0000000..b976f0b --- /dev/null +++ b/imports/startup/server/segfaults.js @@ -0,0 +1,25 @@ +import SegfaultHandler from 'segfault-handler'; +import moment from 'moment'; +// import ravenLogger from './ravenLogger'; + +// https://github.com/ddopson/node-segfault-handler + +const dateFileFormat = moment().format('YYYYMMDD_HH:mm:ss'); + +SegfaultHandler.registerHandler(`/var/tmp/tcef-stacktrace-${dateFileFormat}.log`); + + +// This callaback does not work: +// https://github.com/ddopson/node-segfault-handler/issues/49 + +/* SegfaultHandler.registerHandler('/var/tmp/tcef-crash.log', (signal, address, stack) => { + * // Do what you want with the signal, address, or stack (array) + * // This callback will execute before the signal is forwarded on. + * ravenLogger.log(stack, signal); + * }); */ + +// Only uncomment for segv tests +// SegfaultHandler.causeSegfault(); + +// Or this for normal error (should go to raven in configured) +// throw new Error('This is just a test of error in server'); diff --git a/imports/startup/server/sitemaps.js b/imports/startup/server/sitemaps.js index 8026449..c1fe573 100644 --- a/imports/startup/server/sitemaps.js +++ b/imports/startup/server/sitemaps.js @@ -4,8 +4,6 @@ import Fires from '/imports/api/Fires/Fires'; // import Comments from '/imports/api/Comments/Comments'; -const firesMapEnabled = false; - sitemaps.add('/sitemap.xml', () => { const today = new Date(); @@ -22,23 +20,21 @@ sitemaps.add('/sitemap.xml', () => { // When user has public page /* const users = Meteor.users.find().fetch(); - _.each(users, function(user) { - out.push({ - page: '/persona/' + user.username, - lastmod: user.updatedAt - }); + _.each(users, function(user) { + out.push({ + page: '/persona/' + user.username, + lastmod: user.updatedAt + }); }); */ - if (firesMapEnabled) { - Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => { - // Search the last comment of tha fire - 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 - }); + Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => { + // Search the last comment of tha fire + 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 }); - } + }); return out; }); diff --git a/imports/startup/server/subsUnion.js b/imports/startup/server/subsUnion.js index 2643359..96c6c20 100644 --- a/imports/startup/server/subsUnion.js +++ b/imports/startup/server/subsUnion.js @@ -4,8 +4,7 @@ import { Meteor } from 'meteor/meteor'; import Subscriptions from '/imports/api/Subscriptions/Subscriptions'; import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; import Perlin from 'loms.perlin'; -import './leaflet-workaround'; -import L from 'leaflet'; +import L from 'leaflet-headless'; import calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify'; import { isMailServerMaster } from '/imports/startup/server/email'; @@ -36,8 +35,9 @@ Meteor.startup(() => { const noNoisy = sub => sub; const process = (isPublic) => { + const group = new L.FeatureGroup(); const subscribers = Subscriptions.find().fetch(); - const result = calcUnion(L, subscribers, isPublic ? addNoisy : noNoisy, true); + const result = calcUnion(subscribers, group, isPublic ? addNoisy : noNoisy); const union = result[0]; const bounds = result[1]; diff --git a/imports/ui/components/Loading/LoadingBar.js b/imports/ui/components/Loading/LoadingBar.js index 5dab00c..93bba69 100644 --- a/imports/ui/components/Loading/LoadingBar.js +++ b/imports/ui/components/Loading/LoadingBar.js @@ -3,7 +3,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Meteor } from 'meteor/meteor'; -import { Line } from 'react-progress-bar.js'; +import { Line } from 'react-progressbar.js'; import './LoadingBar.scss'; diff --git a/imports/ui/components/Maps/DefMapLayers.js b/imports/ui/components/Maps/DefMapLayers.js index 6e3ed56..713b499 100644 --- a/imports/ui/components/Maps/DefMapLayers.js +++ b/imports/ui/components/Maps/DefMapLayers.js @@ -35,8 +35,7 @@ class DefMapLayers extends Component { ); const osmlayer = ( diff --git a/imports/ui/components/Maps/FireCircleMark.js b/imports/ui/components/Maps/FireCircleMark.js index 80b9826..e4a3460 100644 --- a/imports/ui/components/Maps/FireCircleMark.js +++ b/imports/ui/components/Maps/FireCircleMark.js @@ -1,10 +1,10 @@ import React, { Component } from 'react'; -import { GeoJSON } from 'react-leaflet'; +import { Circle } from 'react-leaflet'; import PropTypes from 'prop-types'; -import { rectangleAround } from 'map-common-utils'; import { onMarkClick } from './MarkListeners'; import FirePopup from './FirePopup'; + class FireCircleMark extends Component { constructor(props) { super(props); @@ -22,26 +22,22 @@ class FireCircleMark extends Component { lat, lon, scan, - track, nasa, id, history, when, t } = this.props; - const rect = rectangleAround({ lat, lon }, track, track); return ( - + - + ); - /* */ } } FireCircleMark.propTypes = { scan: PropTypes.number.isRequired, - track: PropTypes.number.isRequired, lat: PropTypes.number.isRequired, lon: PropTypes.number.isRequired, nasa: PropTypes.bool.isRequired, diff --git a/imports/ui/components/Maps/FireIconMark.js b/imports/ui/components/Maps/FireIconMark.js index ab24da4..94d93a0 100644 --- a/imports/ui/components/Maps/FireIconMark.js +++ b/imports/ui/components/Maps/FireIconMark.js @@ -3,8 +3,8 @@ import React, { Component, Fragment } from 'react'; import { CircleMarker, Marker, Tooltip } from 'react-leaflet'; import PropTypes from 'prop-types'; import { fireIconS, fireIconM, fireIconL, nFireIcon, industryIcon, regIndustryIcon } from '/imports/ui/components/Maps/Icons'; -import { translate } from 'react-i18next'; import { onMarkClick } from './MarkListeners'; +import { translate } from 'react-i18next'; import FirePopup from './FirePopup'; class FireIconMark extends Component { @@ -30,7 +30,17 @@ class FireIconMark extends Component { render() { const { - lat, lon, scan, track, nasa, id, history, falsePositives, industries, neighbour, when, t + lat, + lon, + scan, + nasa, + id, + history, + falsePositives, + industries, + neighbour, + when, + t } = this.props; return (
@@ -69,7 +79,6 @@ FireIconMark.propTypes = { lat: PropTypes.number.isRequired, lon: PropTypes.number.isRequired, scan: PropTypes.number, - track: PropTypes.number, nasa: PropTypes.bool, falsePositives: PropTypes.bool.isRequired, industries: PropTypes.bool.isRequired, diff --git a/imports/ui/components/Maps/FireListUnion.js b/imports/ui/components/Maps/FireListUnion.js deleted file mode 100644 index 3b094c0..0000000 --- a/imports/ui/components/Maps/FireListUnion.js +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-disable import/no-absolute-path */ -/* eslint-disable react/jsx-indent-props */ -/* eslint-disable react/jsx-indent */ -import React from 'react'; -import PropTypes from 'prop-types'; -import FirePolygonMark from '/imports/ui/components/Maps/FirePolygonMark'; - -export default function FireListUnion(props) { - const { - firesUnion, nasa, t, history - } = props; - /* console.log(`Using marks: ${useMarks}, using pixels: ${usePixel}`); */ - const items = firesUnion.map(({ _id, ...otherProps }) => ()); - return (
{items}
); -} - -FireListUnion.propTypes = { - firesUnion: PropTypes.array.isRequired, - nasa: PropTypes.bool.isRequired, - history: PropTypes.object.isRequired, - t: PropTypes.func.isRequired -}; diff --git a/imports/ui/components/Maps/FirePixel.js b/imports/ui/components/Maps/FirePixel.js index 4efcabe..c81b547 100644 --- a/imports/ui/components/Maps/FirePixel.js +++ b/imports/ui/components/Maps/FirePixel.js @@ -3,32 +3,31 @@ import React from 'react'; import { CircleMarker } from 'react-leaflet'; import PropTypes from 'prop-types'; -import { translate } from 'react-i18next'; import FirePopup from './FirePopup'; +import { translate } from 'react-i18next'; /* Less acurate (1 pixel per fire) but faster */ const FirePixel = ({ - lat, lon, nasa, id, when, t, history, track + lat, lon, nasa, id, when, t, history }) => ( ); + FirePixel.propTypes = { lat: PropTypes.number.isRequired, lon: PropTypes.number.isRequired, id: PropTypes.object.isRequired, history: PropTypes.object.isRequired, - scan: PropTypes.number, - track: PropTypes.number, when: PropTypes.instanceOf(Date), nasa: PropTypes.bool.isRequired, t: PropTypes.func.isRequired diff --git a/imports/ui/components/Maps/FirePolygonMark.js b/imports/ui/components/Maps/FirePolygonMark.js deleted file mode 100644 index 92e3640..0000000 --- a/imports/ui/components/Maps/FirePolygonMark.js +++ /dev/null @@ -1,48 +0,0 @@ -import React, { Component } from 'react'; -import { GeoJSON } from 'react-leaflet'; -import PropTypes from 'prop-types'; -import { onMarkClick } from './MarkListeners'; -import FirePopup from './FirePopup'; - -class FirePolygonMark extends Component { - constructor(props) { - super(props); - this.t = props.t; - this.onClick = this.onClick.bind(this); - } - - onClick() { - const { history, nasa, id } = this.props; - onMarkClick(history, nasa, id); - } - - render() { - const { - nasa, - id, - shape, - centerid, - history, - when, - t - } = this.props; - const lon = centerid.coordinates[0]; - const lat = centerid.coordinates[1]; - return ( - - ); - /* */ - } -} - -FirePolygonMark.propTypes = { - id: PropTypes.object.isRequired, - nasa: PropTypes.bool.isRequired, - shape: PropTypes.object.isRequired, - centerid: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, - when: PropTypes.instanceOf(Date).isRequired, - t: PropTypes.func.isRequired -}; - -export default FirePolygonMark; diff --git a/imports/ui/components/Maps/SubsUnion/SubsUnion.js b/imports/ui/components/Maps/SubsUnion/SubsUnion.js index 41a4d81..dab127a 100644 --- a/imports/ui/components/Maps/SubsUnion/SubsUnion.js +++ b/imports/ui/components/Maps/SubsUnion/SubsUnion.js @@ -33,7 +33,8 @@ const subsUnion = (union, options) => { options.map.leafletElement.fitBounds(L.latLngBounds(bounds._northEast, bounds._southWest)); } } else if (options.subs.length > 0) { - const result = calcUnion(L, options.subs, sub => sub, true); + const unionGroup = new L.FeatureGroup(); + const result = calcUnion(options.subs, unionGroup, sub => sub); const unionJson = result[0]; const bounds = result[1]; diff --git a/imports/ui/components/Maps/SubsUnion/Unify.js b/imports/ui/components/Maps/SubsUnion/Unify.js index 873dcbf..46586b7 100644 --- a/imports/ui/components/Maps/SubsUnion/Unify.js +++ b/imports/ui/components/Maps/SubsUnion/Unify.js @@ -1,9 +1,48 @@ -import { calcUnion as calcUnionWoBounds } from 'map-common-utils'; +import { check } from 'meteor/check'; +import LGeo from 'leaflet-geodesy'; +import tunion from '@turf/union'; +import ttrunc from '@turf/truncate'; -const calcUnion = (L, subs, decorated, typeCircle) => { - const unionJson = calcUnionWoBounds(subs, decorated, typeCircle); - // return [unionJson, L.geoJSON(unionJson).getBounds()]; - return [unionJson, unionJson === null ? null : L.geoJSON(unionJson).getBounds()]; +// https://stackoverflow.com/questions/35394577/leaflet-js-union-merge-circles +const truncOptions = { precision: 6, coordinates: 2 }; + +const unify = (polyList) => { + let unionTemp; + for (let i = 0; i < polyList.length; i += 1) { + const pol = polyList[i].toGeoJSON(); + const cleanPol = ttrunc(pol, truncOptions); + if (i === 0) { + unionTemp = cleanPol; + } else { + unionTemp = ttrunc(tunion(unionTemp, cleanPol), truncOptions); + } + } + return unionTemp; +}; + +const calcUnion = (subs, group, decorated) => { + const unionGroup = group; + const copts = { + parts: 144 + }; + subs.forEach((osub) => { + try { + if (osub.location && osub.location.lat && osub.location.lon && osub.distance) { + check(osub.location.lon, Number); + check(osub.location.lat, Number); + check(osub.distance, Number); + const dsub = decorated(osub); + const circle = LGeo.circle([dsub.location.lat, dsub.location.lon], dsub.distance * 1000, copts); + circle.addTo(unionGroup); + } else { + console.info(`Wrong subscription ${JSON.stringify(osub)}`); + } + } catch (e) { + console.error(e, `Wrong subscription trying to make union ${JSON.stringify(osub)}`); + } + }); + const unionJson = unify(unionGroup.getLayers()); + return [unionJson, unionGroup.getBounds()]; }; export default calcUnion; diff --git a/imports/ui/components/NotificationsObserver/NotificationsObserver.js b/imports/ui/components/NotificationsObserver/NotificationsObserver.js index ac374b5..e68b1ef 100644 --- a/imports/ui/components/NotificationsObserver/NotificationsObserver.js +++ b/imports/ui/components/NotificationsObserver/NotificationsObserver.js @@ -12,7 +12,7 @@ import { trim } from './util.js'; function process(notif) { // No already notified if (Push.Permission.has()) { - if (!notif.notified && notif.type === 'web') { + if (!notif.webNotified) { Push.create(i18n.t('AppName'), { body: `${trim(notif.content)} (${i18n.t('fireDetected', { when: dateFromNow(notif.when) })})`, icon: '/n-fire-marker.png', @@ -47,7 +47,7 @@ Meteor.startup(() => { if (Meteor.userId()) { Meteor.subscribe('mynotifications'); // Check for notifications not processed at startup - Notifications.find({ notified: null, type: 'web' }).forEach((notif) => { + Notifications.find({ webNotified: null }).forEach((notif) => { process(notif); }); } diff --git a/imports/ui/layouts/App/App.js b/imports/ui/layouts/App/App.js index 4aeef3a..d991262 100644 --- a/imports/ui/layouts/App/App.js +++ b/imports/ui/layouts/App/App.js @@ -98,66 +98,66 @@ const App = props => ( /* https://react.i18next.com/components/i18nextprovider.html */
{props.i18nReady.get() && - - - - - - { !props.loading && -
- - - {i18n.t('AppName')} - - - - - - + + + + + + { !props.loading && +
+ + + {i18n.t('AppName')} + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + + - - - -
- - - {props.i18nReady.get() && } -
} -
-
-
-
-
} + + + +
+ + + {props.i18nReady.get() && } +
} +
+
+
+
+
}
); @@ -186,9 +186,7 @@ export default withTracker(() => { const loading = !Roles.subscription.ready() || !i18nReady.get(); const name = user && user.profile && user.profile.name && getUserName(user.profile.name); const emailAddress = user && user.emails && user.emails[0].address; - Meteor.autorun(() => { - console.log(`i18n ready?: ${i18nReady.get()}`); - }); + // console.log(`i18n ready?: ${i18nReady.get()}`); return { loading, loggingIn, diff --git a/imports/ui/pages/Fires/Fires.js b/imports/ui/pages/Fires/Fires.js index 2647992..0e4271a 100644 --- a/imports/ui/pages/Fires/Fires.js +++ b/imports/ui/pages/Fires/Fires.js @@ -10,8 +10,7 @@ import { Row, Col, Alert, FormGroup } from 'react-bootstrap'; import { Meteor } from 'meteor/meteor'; import { Bert } from 'meteor/themeteorchef:bert'; import { Helmet } from 'react-helmet'; -import { Map, GeoJSON } from 'react-leaflet'; -import { rectangleAround } from 'map-common-utils'; +import { Map, Circle } from 'react-leaflet'; import Blaze from 'meteor/gadicc:blaze-react-component'; import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers'; import NotFound from '/imports/ui/pages/NotFound/NotFound'; @@ -116,9 +115,8 @@ class Fire extends React.Component { t('Información adicional sobre fuego detectado en {{where}} el {{when}}', { where: fire.address, when: this.dateLongFormat }) : t('Información adicional sobre fuego detectado el {{when}}', { when: this.dateLongFormat }); } - const ready = fire && !loading; - const rect = ready ? rectangleAround({ lat: fire.lat, lon: fire.lon }, fire.track, fire.track) : null; - return (ready ? ( + + return (fire && !loading ? (
{t('AppName')}: {t('Información adicional sobre fuego')} @@ -139,13 +137,14 @@ class Fire extends React.Component { zoom={13} > - { this.circle = circle; this.handleLeafletCircleLoad(circle); }} - data={rect} + { this.circle = circle; this.handleLeafletCircleLoad(circle); }} /> { (this.props.activefires.length + this.props.firealerts.length) === 0 ? No hay fuegos activos en esta zona del mapa. {{ countTotal: this.props.activefirestotal }} fuegos activos en el mundo. : - En rojo, {{ count: this.props.activefiresunion.length + this.props.firealerts.length }} fuegos activos. {{ countTotal: this.props.activefirestotal }} fuegos activos en el mundo. + En rojo, {{ count: this.props.activefires.length + this.props.firealerts.length }} fuegos activos. {{ countTotal: this.props.activefirestotal }} fuegos activos en el mundo. }

{isNotHomeAndMobile() && this.props.firealerts.length > 0 && @@ -297,14 +307,6 @@ class FiresMap extends React.Component { neighbour={false} industries={false} /> - { Meteor.isDevelopment && - - } { // console.log(`With industries: ${withIndustries}`); let subscription; - let subscriptionUnion; let alertSubscription; const centerStored = store.get('firesmap_center'); @@ -423,14 +422,6 @@ export default translate([], { wait: true })(withTracker(() => { mapSize.get()[1].lat, marks.get() && zoom.get() >= MAXZOOM ); - subscriptionUnion = Meteor.subscribe( - 'activefiresunionmyloc', - mapSize.get()[0].lng, - mapSize.get()[0].lat, - mapSize.get()[1].lng, - mapSize.get()[1].lat, - false - ); alertSubscription = Meteor.subscribe( 'fireAlerts', mapSize.get()[0].lng, @@ -451,7 +442,6 @@ export default translate([], { wait: true })(withTracker(() => { }); Meteor.subscribe('activefirestotal'); - Meteor.subscribe('activefiresuniontotal'); const settingsSubs = Meteor.subscribe('settings'); const lastCheck = SiteSettings.findOne({ name: 'last-fire-check' }); @@ -463,15 +453,13 @@ export default translate([], { wait: true })(withTracker(() => { const lastFireDetected = ActiveFiresCollection.findOne({}, { sort: { when: -1 } }); return { - loading: Meteor.status().status !== 'connected' || !subscription ? true : !(subscription.ready() && settingsSubs.ready() && alertSubscription.ready() && settingsSubs.ready() && subscriptionUnion.ready()), + loading: Meteor.status().status !== 'connected' || !subscription ? true : !(subscription.ready() && settingsSubs.ready() && alertSubscription.ready()), userSubs: userSubs ? userSubs.value : null, userSubsBounds: userSubs ? userSubsBounds.value : null, subsready: settingsSubs.ready(), // Not reactive query depending on zoom level activefires: ActiveFiresCollection.find({}, { reactive: zoom.get() >= MAXZOOMREACTIVE }).fetch(), - activefiresunion: ActiveFiresUnion.find({}, { reactive: zoom.get() >= MAXZOOMREACTIVE }).fetch(), activefirestotal: Counter.get('countActiveFires') + fireAlerts.length, - activefiresuniontotal: Counter.get('countActiveFiresUnion') + fireAlerts.length, firealerts: fireAlerts, falsePositives, industries, diff --git a/imports/ui/pages/Index/Index.js b/imports/ui/pages/Index/Index.js index 9986fba..5d6b130 100644 --- a/imports/ui/pages/Index/Index.js +++ b/imports/ui/pages/Index/Index.js @@ -11,7 +11,6 @@ import PropTypes from 'prop-types'; // import { Link } from 'react-router-dom'; // https://www.npmjs.com/package/react-resize-detector import ReactResizeDetector from 'react-resize-detector'; -import MobileStoreButton from 'react-mobile-store-button'; import _ from 'lodash'; import 'html5-device-mockups/dist/device-mockups.min.css'; import 'bootstrap-carousel-swipe/carousel-swipe'; @@ -21,6 +20,7 @@ import SubscriptionsMap from '/imports/ui/pages/Subscriptions/SubscriptionsMap'; import ShareIt from '/imports/ui/components/ShareIt/ShareIt'; import getFallbackLang from '/imports/modules/lang-fallback'; import FiresMap from '../FiresMap/FiresMap'; + import './Index.scss'; import './Index-custom.scss'; @@ -102,7 +102,6 @@ class Index extends Component { render() { const { t } = this.props; const title = `${t('AppName')}: ${t('Inicio')}`; - const androidUrl = 'https://play.google.com/store/apps/details?id=org.comunes.fires'; return (
@@ -182,9 +181,6 @@ class Index extends Component {

Siempre alerta a los fuegos en nuestro vecindario

{/* {this.props.t('Participa')} */} -
- -
diff --git a/imports/ui/pages/Index/Index.scss b/imports/ui/pages/Index/Index.scss index 527aa93..3fb18b4 100644 --- a/imports/ui/pages/Index/Index.scss +++ b/imports/ui/pages/Index/Index.scss @@ -77,7 +77,3 @@ } } } - -.android-btn > div { - height: 50px !important; -} diff --git a/imports/ui/pages/Profile/Profile.js b/imports/ui/pages/Profile/Profile.js index 6d9b24b..665e9d7 100644 --- a/imports/ui/pages/Profile/Profile.js +++ b/imports/ui/pages/Profile/Profile.js @@ -9,17 +9,15 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import { Bert } from 'meteor/themeteorchef:bert'; import { testId } from '/imports/ui/components/Utils/TestUtils'; -import { translate } from 'react-i18next'; -import { T9n } from 'meteor-accounts-t9n'; -import { withTracker } from 'meteor/react-meteor-data'; import Col from '../../components/Col/Col'; +import { withTracker } from 'meteor/react-meteor-data'; import InputHint from '../../components/InputHint/InputHint'; import validate from '../../../modules/validate'; +import { translate } from 'react-i18next'; +import { T9n } from 'meteor-accounts-t9n'; import './Profile.scss'; -// List of languages: https://github.com/i18next/i18next/issues/1068 - class Profile extends React.Component { constructor(props) { super(props); @@ -157,7 +155,6 @@ class Profile extends React.Component { renderPasswordUser(loading, user) { const { t, i18n } = this.props; - const enabledLangs = ['en', 'es', 'gl']; const langName = { en: 'English', es: 'Español', gl: 'Galego', ast: 'Asturianu', ca: 'Català' }; @@ -205,7 +202,7 @@ class Profile extends React.Component { {langName[i18n.language]}
- {enabledLangs.map(lang => ( + {i18n.languages.map(lang => (