├── .meteor ├── .gitignore ├── release ├── platforms ├── cordova-plugins ├── .cordova-plugins.1bgpkk3 ├── .id ├── .finished-upgraders ├── packages └── versions ├── client ├── body.html ├── stylesheets │ ├── status.css │ ├── intro.css │ ├── radar.scss │ └── stingwatch.css └── main.js ├── public ├── logo.gif ├── favicon.ico ├── logo64.gif ├── apple-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── intro │ ├── radar.png │ ├── map-radar.jpg │ ├── nopd-portrait.jpg │ └── cameras-portrait.jpg ├── logo64Inverse.gif ├── ms-icon-70x70.png ├── splash │ ├── screen.png │ └── screen-land.png ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png ├── stingwatch_logo.jpg ├── stingwatch_logo.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── android-icon-144x144.png ├── android-icon-192x192.png └── apple-icon-precomposed.png ├── stingwatch ├── imports ├── startup │ ├── client │ │ ├── mapbox.js │ │ ├── reactive-local-store.js │ │ ├── device-id.js │ │ └── routes.jsx │ └── cordova │ │ ├── sim.js │ │ ├── notifications.js │ │ └── telephony.js ├── lib │ ├── hammer.js │ └── trigger-danger.js ├── ui │ ├── components │ │ ├── status │ │ │ ├── LearnButton.jsx │ │ │ ├── NavBar.jsx │ │ │ ├── StatusScanning.jsx │ │ │ ├── TweetButton.jsx │ │ │ ├── Factoid.jsx │ │ │ ├── Radar.jsx │ │ │ ├── TweetComposer.jsx │ │ │ └── StatusDanger.jsx │ │ ├── intro │ │ │ ├── IntroSlide2.jsx │ │ │ ├── IntroSlide1.jsx │ │ │ └── IntroSlide3.jsx │ │ └── GeekMode.jsx │ ├── pages │ │ ├── IntroPage.jsx │ │ ├── TermsRejectedPage.jsx │ │ ├── StatusPage.jsx │ │ └── TermsPage.jsx │ └── App.jsx └── globals.js ├── settings-example.json ├── cordova-build-override └── platforms │ └── android │ └── build-extras.gradle ├── .gitignore ├── server └── main.js ├── cordova └── main.js ├── package.json ├── mobile-config.js └── README.md /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3.4.1 2 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | android 2 | browser 3 | server 4 | -------------------------------------------------------------------------------- /client/body.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | -------------------------------------------------------------------------------- /client/stylesheets/status.css: -------------------------------------------------------------------------------- 1 | #map { 2 | height: 20rem; 3 | } 4 | -------------------------------------------------------------------------------- /public/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/logo.gif -------------------------------------------------------------------------------- /stingwatch: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | meteor run android-device -p 4000 --settings settings.json 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo64.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/logo64.gif -------------------------------------------------------------------------------- /public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/apple-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/intro/radar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/intro/radar.png -------------------------------------------------------------------------------- /public/logo64Inverse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/logo64Inverse.gif -------------------------------------------------------------------------------- /public/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/splash/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/splash/screen.png -------------------------------------------------------------------------------- /public/intro/map-radar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/intro/map-radar.jpg -------------------------------------------------------------------------------- /public/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/stingwatch_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/stingwatch_logo.jpg -------------------------------------------------------------------------------- /public/stingwatch_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/stingwatch_logo.png -------------------------------------------------------------------------------- /public/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/android-icon-36x36.png -------------------------------------------------------------------------------- /public/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/android-icon-48x48.png -------------------------------------------------------------------------------- /public/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/android-icon-72x72.png -------------------------------------------------------------------------------- /public/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/android-icon-96x96.png -------------------------------------------------------------------------------- /public/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/splash/screen-land.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/splash/screen-land.png -------------------------------------------------------------------------------- /public/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/android-icon-144x144.png -------------------------------------------------------------------------------- /public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/android-icon-192x192.png -------------------------------------------------------------------------------- /public/intro/nopd-portrait.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/intro/nopd-portrait.jpg -------------------------------------------------------------------------------- /public/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/apple-icon-precomposed.png -------------------------------------------------------------------------------- /public/intro/cameras-portrait.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marvinmarnold/stingwatch/HEAD/public/intro/cameras-portrait.jpg -------------------------------------------------------------------------------- /imports/startup/client/mapbox.js: -------------------------------------------------------------------------------- 1 | export function configMapbox() { 2 | Mapbox.load({ 3 | plugins: ['heat', 'label'] 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /settings-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "public": { 3 | "TWITTER_CONSUMER_KEY": "", 4 | "MAPBOX_TOKEN": "" 5 | }, 6 | "TWITTER_CONSUMER_SECRET": "" 7 | } 8 | -------------------------------------------------------------------------------- /imports/lib/hammer.js: -------------------------------------------------------------------------------- 1 | export const HammerHelper = { 2 | isNext(dir) { 3 | return dir === 2; 4 | }, 5 | 6 | isPrev(dir) { 7 | return dir === 4; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /cordova-build-override/platforms/android/build-extras.gradle: -------------------------------------------------------------------------------- 1 | cdvVersionCode = '36' 2 | android { 3 | lintOptions { 4 | disable 'MissingTranslation' 5 | disable 'ExtraTranslation' 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.meteor/cordova-plugins: -------------------------------------------------------------------------------- 1 | cordova-plugin-dialogs@1.2.1 2 | cordova-plugin-sim@1.2.1 3 | cordova-plugin-telephony@file://packages/cordova-plugin-telephony 4 | cordova-plugin-vibration@2.1.1 5 | de.appplant.cordova.plugin.local-notification@0.8.4 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | packages/bootstrap 3 | scripts 4 | settings 5 | .meteor/local 6 | packages/meteor-imsi-catcher-catcher 7 | packages/meteor-device-id 8 | packages/cordova-plugin-telephony 9 | packages/meteor-reactive-local-store 10 | settings.json 11 | -------------------------------------------------------------------------------- /imports/startup/client/reactive-local-store.js: -------------------------------------------------------------------------------- 1 | import { RLS } from 'meteor/marvin:reactive-local-store'; 2 | import { SETTINGS } from '../../globals.js'; 3 | 4 | export function configRLS() { 5 | RLS.setRegisteredKeys([ 6 | SETTINGS.TERMS_ACCEPTED 7 | ]); 8 | 9 | RLS.init(); 10 | } 11 | -------------------------------------------------------------------------------- /.meteor/.cordova-plugins.1bgpkk3: -------------------------------------------------------------------------------- 1 | cordova-plugin-dialogs@1.2.1 2 | cordova-plugin-file@4.2.0 3 | cordova-plugin-filee@4.2.0 4 | cordova-plugin-sim@1.2.1 5 | cordova-plugin-telephony@file://../cordova-plugin-telephony 6 | cordova-plugin-vibration@2.1.1 7 | de.appplant.cordova.plugin.local-notification@0.8.4 8 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 2sgvkysqas4g1t60v7b 8 | -------------------------------------------------------------------------------- /imports/startup/client/device-id.js: -------------------------------------------------------------------------------- 1 | import { DeviceId } from 'meteor/marvin:device-id'; 2 | 3 | export function startupDeviceId() { 4 | // Generate a deviceId client side 5 | console.log('startupDeviceId'); 6 | DeviceId.gen((error, deviceId) => { 7 | console.log('generated id: ' + deviceId); 8 | // console.log('deviceId error'); 9 | // console.log(error); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | -------------------------------------------------------------------------------- /client/stylesheets/intro.css: -------------------------------------------------------------------------------- 1 | .intro-slide-2 { 2 | min-height: 100%; 3 | background-size: cover; 4 | background-repeat: no-repeat; 5 | background-position: center center; 6 | background-image: url('/intro/nopd-portrait.jpg'); 7 | color: #fff; 8 | } 9 | 10 | .intro-slide-3 { 11 | min-height: 100%; 12 | background-size: cover; 13 | background-repeat: no-repeat; 14 | background-position: center center; 15 | background-image: url('/intro/cameras-portrait.jpg'); 16 | color: #fff; 17 | } 18 | -------------------------------------------------------------------------------- /imports/ui/components/status/LearnButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class LearnButton extends React.Component { 4 | handleClick() { 5 | window.open("https://www.stingraymappingproject.org", "_system"); 6 | } 7 | 8 | render() { 9 | const url = "https://www.stingraymappingproject.org"; 10 | 11 | return ( 12 | 14 | 15 |   Learn 16 | 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | // import '../imports/startup/server/twitter.js'; 3 | // import '../imports/api/twitter.js'; 4 | 5 | // Meteor.startup(() => { 6 | // // if (Meteor.isServer) { 7 | // // Meteor.startup(function () { 8 | // // // code to run on server at startup 9 | // // process.env.MOBILE_DDP_URL = 'http://http://192.168.88.167:4000'; 10 | // // process.env.MOBILE_ROOT_URL = 'http://http://192.168.88.167:4000'; 11 | // // }); 12 | // // } 13 | // console.log("Starting client"); 14 | // configTwitter(); 15 | // }); 16 | -------------------------------------------------------------------------------- /cordova/main.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | 3 | import { startupSim } from '../imports/startup/cordova/sim.js'; 4 | // import { startupDeviceId } from '../imports/startup/cordova/device-id.js'; 5 | import { startupNotifications } from '../imports/startup/cordova/notifications.js'; 6 | import { startupTelephony } from '../imports/startup/cordova/telephony.js'; 7 | 8 | Meteor.startup(() => { 9 | if(Meteor.isCordova) { 10 | console.log("Starting Cordova"); 11 | // startupDeviceId(); 12 | startupSim(); 13 | startupNotifications(); 14 | startupTelephony(); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { render } from 'react-dom'; 3 | 4 | import { renderRoutes } from '../imports/startup/client/routes.jsx'; 5 | import { configRLS } from '../imports/startup/client/reactive-local-store.js'; 6 | import { configMapbox } from '../imports/startup/client/mapbox.js'; 7 | import { startupDeviceId } from '../imports/startup/client/device-id.js'; 8 | 9 | Meteor.startup(() => { 10 | // console.log("Starting client"); 11 | configRLS(); 12 | configMapbox(); 13 | startupDeviceId(); 14 | render(renderRoutes(), document.getElementById('app')); 15 | }); 16 | -------------------------------------------------------------------------------- /imports/ui/components/status/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { APP_NAME } from '../../../globals.js'; 4 | 5 | export default class NavBar extends React.Component { 6 | handleClick() { 7 | this.props.toggleGeekMode(); 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 | 14 | 15 | {APP_NAME} 16 | 17 | 22 | 23 |
24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stingwatch", 3 | "private": true, 4 | "scripts": { 5 | "lint": "eslint .", 6 | "pretest": "npm run lint --silent" 7 | }, 8 | "dependencies": { 9 | "meteor-node-stubs": "~0.2.0", 10 | "react": "^15.0.2", 11 | "react-addons-pure-render-mixin": "^15.0.2", 12 | "react-dom": "^15.0.2", 13 | "react-hammerjs": "^0.4.6", 14 | "react-router": "^2.4.0", 15 | "twitter": "^1.2.5" 16 | }, 17 | "devDependencies": { 18 | "eslint": "^2.6.0", 19 | "eslint-config-airbnb": "^6.2.0", 20 | "eslint-plugin-meteor": "^3.4.0", 21 | "eslint-plugin-react": "^4.2.3" 22 | }, 23 | "eslintConfig": { 24 | "plugins": [ 25 | "meteor" 26 | ], 27 | "extends": [ 28 | "airbnb", 29 | "plugin:meteor/guide" 30 | ], 31 | "rules": {} 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/stylesheets/radar.scss: -------------------------------------------------------------------------------- 1 | $size: 321px; 2 | 3 | .h-center { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | #radar { 10 | position: relative; 11 | width: $size; 12 | height: $size; 13 | background:#222 url('/intro/map-radar.jpg'); 14 | /** background:#222 url(http://smartphone-attack-vector.de/wp-content/uploads/2016/04/map-radar.jpg); **/ 15 | border-radius: 320px; 16 | overflow:hidden; 17 | } 18 | 19 | #rad { 20 | position:absolute; 21 | width: $size; 22 | height: $size; 23 | background:url('/intro/radar.png'); 24 | } 25 | 26 | .radarBlip { 27 | background:#cf5; 28 | position:absolute; 29 | border-radius:10px; 30 | width:4px; 31 | height:4px; 32 | margin-top:-2px; 33 | margin-left:-2px; 34 | box-shadow:0 0 10px 5px rgba(100,255,0,0.5); 35 | opacity:0.3; 36 | } 37 | -------------------------------------------------------------------------------- /imports/startup/client/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route, IndexRoute, browserHistory } from 'react-router'; 3 | 4 | // route components 5 | import App from '../../ui/App.jsx'; 6 | import IntroPage from '../../ui/pages/IntroPage.jsx'; 7 | import StatusPage from '../../ui/pages/StatusPage.jsx'; 8 | import TermsPage from '../../ui/pages/TermsPage.jsx'; 9 | import TermsRejectedPage from '../../ui/pages/TermsRejectedPage.jsx'; 10 | 11 | export const renderRoutes = () => ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /client/stylesheets/stingwatch.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .logo { 8 | display: inline-block; 9 | height: 4rem; 10 | } 11 | 12 | #status_scanning-map { 13 | width: 100%; 14 | } 15 | 16 | .full-screen { 17 | height: 100%; 18 | width: 100%; 19 | min-width: 100%; 20 | min-height: 100%; 21 | /*background-color: blue;*/ 22 | } 23 | 24 | body, html, #app, #app-base { 25 | height: 100%; 26 | padding: 0 !important; 27 | margin: 0 !important; 28 | -webkit-box-sizing: border-box; 29 | -moz-box-sizing: border-box; 30 | box-sizing: border-box; 31 | font-family: 'Lato', sans-serif; 32 | font-weight: 400; 33 | } 34 | 35 | .v-middle { 36 | display: flex; 37 | align-items: center; 38 | } 39 | 40 | .btn-wrap { 41 | white-space:normal !important; 42 | word-wrap:break-word; 43 | } 44 | -------------------------------------------------------------------------------- /imports/ui/components/status/StatusScanning.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Factoid from './Factoid.jsx'; 3 | import LearnButton from './LearnButton.jsx'; 4 | import NavBar from './NavBar.jsx'; 5 | import Radar from './Radar.jsx'; 6 | import TweetButton from './TweetButton.jsx'; 7 | 8 | export default class StatusPage extends React.Component { 9 | render() { 10 | return ( 11 |
12 | 13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | 25 |
26 |
27 |
28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /imports/lib/trigger-danger.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { Session } from 'meteor/session'; 3 | import { SESSION_STATUS, STATUSES } from '../globals.js'; 4 | import { DeviceId } from 'meteor/marvin:device-id'; 5 | 6 | const triggerDuration = 3510; // ms 7 | 8 | export function triggerDanger() { 9 | Session.set(SESSION_STATUS, STATUSES.DANGER); 10 | Session.set(STATUSES.DANGER_TRIGGERED, true); 11 | 12 | Meteor.setTimeout(() => { 13 | Session.set(STATUSES.DANGER_TRIGGERED, false) 14 | }, triggerDuration); 15 | 16 | // Create a fake detection so something will be displayed on Danger Page 17 | // If testing in web browser, deviceId not set 18 | const deviceId = DeviceId.get() || Random.id() 19 | Meteor.call("catcher.simulate-detection", deviceId, (error, result) => { 20 | // console.log('Got result from simulate-detection'); 21 | if(error) { 22 | console.log("error", error); 23 | } 24 | if(result) { 25 | // console.log(result); 26 | } 27 | }); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /imports/ui/components/status/TweetButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createContainer } from 'meteor/react-meteor-data'; 3 | 4 | export default class TweetButton extends React.Component { 5 | login() { 6 | console.log('TweetButton#login'); 7 | Meteor.loginWithTwitter(err => { 8 | if(err) { 9 | console.log("TweetButton#login.error"); 10 | console.log(err); 11 | } else { 12 | console.log("Success"); 13 | } 14 | }) 15 | } 16 | 17 | compose() { 18 | this.props.setComposingTweet(true); 19 | } 20 | 21 | render() { 22 | let onClick; 23 | 24 | if(this.props.loggedIn) { 25 | onClick = this.compose; 26 | } else { 27 | onClick = this.login; 28 | } 29 | 30 | return ( 31 | 34 | ); 35 | } 36 | }; 37 | 38 | export default createContainer(() => { 39 | return { 40 | loggedIn: !!Meteor.user() 41 | }; 42 | }, TweetButton); 43 | -------------------------------------------------------------------------------- /imports/ui/components/status/Factoid.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { FACTOIDS } from '../../../globals.js'; 4 | 5 | export default class Factoid extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | currentFactoidNum: 0, 11 | timer: undefined 12 | }; 13 | } 14 | 15 | componentDidMount() { 16 | this.resetTimer(); 17 | } 18 | 19 | componentWillUnmount() { 20 | Meteor.clearTimeout(this.state.timer); 21 | } 22 | 23 | resetTimer() { 24 | const thiz = this; 25 | 26 | this.setState({ 27 | timer: setTimeout(() => { 28 | thiz.changeFactoid(); 29 | thiz.resetTimer(); 30 | }, 10 * 1000) 31 | }); 32 | } 33 | 34 | changeFactoid() { 35 | const i = (this.state.currentFactoidNum + 1) % FACTOIDS.length; 36 | this.setState({currentFactoidNum: i}); 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 |

Stingray Facts and Tips

43 |

{FACTOIDS[this.state.currentFactoidNum]}

44 |
45 | ); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /imports/ui/components/intro/IntroSlide2.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Hammer from 'react-hammerjs'; 3 | 4 | import { HammerHelper } from '../../../lib/hammer.js'; 5 | 6 | export default class IntroSlide2 extends React.Component { 7 | 8 | // https://github.com/hammerjs/hammer.js/wiki/Getting-Started 9 | handleSwipe(event) { 10 | var dir = event.direction 11 | 12 | if(HammerHelper.isPrev(dir)) { 13 | // console.log("IntroSlide2, Prev page"); 14 | this.props.introPrev(); 15 | } else if(HammerHelper.isNext(dir)) { 16 | // console.log("IntroSlide2, Next page"); 17 | this.props.introNext(); 18 | } 19 | } 20 | 21 | render() { 22 | return ( 23 | 24 |
25 |
26 |
27 |

For years, a device called a Stingray has allowed police to listen to our calls and read our text messages without search warrants.

28 |
29 |
30 |
31 |
32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /imports/globals.js: -------------------------------------------------------------------------------- 1 | export const SESSION_STATUS = "session-status"; 2 | 3 | export const STATUSES = { 4 | SCANNING: 'STATUSES.SCANNING', 5 | DANGER: 'STATUSES.DANGER', 6 | DANGER_TRIGGERED: 'STATUSES.DANGER_TRIGGERED' 7 | }; 8 | 9 | export const APP_NAME = 'StingWatch'; 10 | 11 | export const DEFAULT_TWEETS = { 12 | SCANNING: "Looks like you are safe", 13 | DANGER: "You are in grave danger" 14 | } 15 | 16 | export const FACTOIDS = [ 17 | "Baltimore police testified that they have used Stingray technology 4300 times since 2007.", 18 | "Police have believed themselves unable to comply with subpoenas to produce Stingray devices in court, due to nondisclosure agreements with the FBI.", 19 | "If a Stingray is used against a Verizon customer, identifying information from every Verizon phone in the area will be swept up.", 20 | "Civilians recently filed a lawsuit against the Chicago Police Department based on evidence that police had been eavesdropping on protesters’ cell phones.", 21 | "At least 53 agencies in 21 states and the District of Columbia own Stingrays. Many more continue to hide their purchase and use of the technology." 22 | ] 23 | 24 | export const SETTINGS = { 25 | TERMS_ACCEPTED: 'settings-terms-accepted' 26 | } 27 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base # Packages every Meteor app needs to have 8 | mobile-experience # Packages for a great mobile UX 9 | mongo # The database Meteor supports right now 10 | reactive-var # Reactive variable for tracker 11 | jquery # Helpful client-side library 12 | tracker # Meteor's client-side reactive programming library 13 | random 14 | 15 | standard-minifier-css # CSS minifier run for production mode 16 | standard-minifier-js # JS minifier run for production mode 17 | es5-shim # ECMAScript 5 compatibility for older browsers. 18 | ecmascript # Enable ECMAScript2015+ syntax in app code 19 | static-html 20 | twbs:bootstrap 21 | crosswalk 22 | react-meteor-data 23 | aldeed:simple-schema 24 | fortawesome:fontawesome 25 | fourseven:scss 26 | session 27 | marvin:reactive-local-store 28 | underscore 29 | pauloborges:mapbox 30 | marvin:device-id 31 | marvin:imsi-catcher-catcher 32 | accounts-twitter 33 | mdg:geolocation 34 | -------------------------------------------------------------------------------- /imports/startup/cordova/sim.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { RLS } from 'meteor/marvin:reactive-local-store'; 3 | import { SETTINGS } from '../../globals.js'; 4 | import { Catcher } from 'meteor/marvin:imsi-catcher-catcher'; 5 | import { DeviceId } from 'meteor/marvin:device-id'; 6 | 7 | const refreshPeriod = 1000 * 30 * 1; 8 | 9 | export function startupSim() { 10 | readValues(); 11 | 12 | Meteor.setTimeout(() => { 13 | startupSim() 14 | }, refreshPeriod); 15 | } 16 | 17 | 18 | function readValues() { 19 | if(RLS.get(SETTINGS.TERMS_ACCEPTED)) { 20 | window.plugins.sim.getSimInfo(result => { 21 | 22 | var simReading = { 23 | commonReading: { 24 | deviceId: DeviceId.get(), 25 | readingType: Catcher.READING_TYPES.ANDROID_V1_SIM, 26 | deviceScannerId: 1, 27 | }, 28 | mcc: parseInt(result.mcc), 29 | mnc: parseInt(result.mnc), 30 | carrierName: result.carrierName, 31 | countryCode: result.countryCode 32 | } 33 | 34 | console.log('Going to insert SIM reading:'); 35 | console.log(simReading); 36 | 37 | Meteor.call('catcher.readings.insert', simReading, (error, result) => { 38 | // console.log(result); 39 | }); 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /imports/ui/components/intro/IntroSlide1.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Hammer from 'react-hammerjs'; 3 | 4 | import { HammerHelper } from '../../../lib/hammer.js'; 5 | 6 | export default class IntroSlide1 extends React.Component { 7 | 8 | // https://github.com/hammerjs/hammer.js/wiki/Getting-Started 9 | handleSwipe(event) { 10 | var dir = event.direction 11 | 12 | if(HammerHelper.isPrev(dir)) { 13 | // console.log("IntroSlide1, Prev page"); 14 | } else if(HammerHelper.isNext(dir)) { 15 | // console.log("IntroSlide1, Next page"); 16 | this.props.introNext(); 17 | } 18 | } 19 | 20 | 21 | render() { 22 | return ( 23 | 24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 |

StingWatch

32 |

Swipe to get started

33 |
34 |
35 |
36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /imports/ui/components/intro/IntroSlide3.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Hammer from 'react-hammerjs'; 3 | import { browserHistory, Link } from 'react-router'; 4 | 5 | import { HammerHelper } from '../../../lib/hammer.js'; 6 | 7 | export default class IntroSlide3 extends React.Component { 8 | 9 | // https://github.com/hammerjs/hammer.js/wiki/Getting-Started 10 | handleSwipe(event) { 11 | var dir = event.direction 12 | 13 | if(HammerHelper.isPrev(dir)) { 14 | // console.log("IntroSlide3, Prev page"); 15 | this.props.introPrev(); 16 | } else if(HammerHelper.isNext(dir)) { 17 | // Do nothing on last page 18 | } 19 | } 20 | 21 | render() { 22 | return ( 23 | 24 |
25 |
26 |
27 |

StingWatch is fighting back. Help us to expose unconstitutional police surveillance.

28 | 29 | Start StingWatch 30 | 31 |
32 |
33 |
34 |
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /imports/ui/components/status/Radar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | let timer; 4 | export default class Radar extends React.Component { 5 | componentDidMount() { 6 | const $rad = $('#rad'); 7 | const $blips = $('.radarBlip'); 8 | 9 | let deg = 0; 10 | let rad = 160.5; // = 321/2 11 | 12 | $blips.each(function() { 13 | const data = $(this).data(); 14 | const pos = { X: data.x, Y: data.y }; 15 | const getAtan = Math.atan2(pos.X - rad, pos.Y - rad); 16 | const getDeg = ~~(-getAtan / (Math.PI / 180) + 180); 17 | 18 | $(this).css({ left: pos.X, top: pos.Y }).attr('data-atDeg', getDeg); 19 | }); 20 | 21 | (function rotate() { 22 | $rad.css({transform: 'rotate('+ deg +'deg)'}); 23 | $('[data-atDeg='+deg+']').stop().fadeTo(0, 1).fadeTo(1700, 0.2); 24 | 25 | timer = setTimeout(function() { 26 | deg = ++deg % 360; 27 | rotate(); 28 | }, 25); 29 | })(); 30 | } 31 | 32 | componentWillUnmount() { 33 | clearTimeout(timer) 34 | } 35 | 36 | render() { 37 | return ( 38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /imports/startup/cordova/notifications.js: -------------------------------------------------------------------------------- 1 | import { Tracker } from 'meteor/tracker'; 2 | import { STATUSES } from '../../globals.js'; 3 | import { Catcher } from 'meteor/marvin:imsi-catcher-catcher'; 4 | import { triggerDanger } from '../../lib/trigger-danger.js'; 5 | 6 | export function startupNotifications() { 7 | const thiz = this; 8 | 9 | // Trigger Danger if any detections found 10 | Tracker.autorun(() => { 11 | if(!!Catcher.inDanger()) 12 | triggerDanger(); 13 | }); 14 | 15 | // Vibrate, beep, show notification if Detection triggered 16 | Tracker.autorun(() => { 17 | if (Session.get(STATUSES.DANGER_TRIGGERED)) { 18 | console.log('Notifications triggered'); 19 | 20 | // Vibrate 21 | navigator.vibrate([2000, 1000, 2500, 500, 500]) 22 | 23 | // Make noise 24 | navigator.notification.beep(2); 25 | 26 | // Display notification 27 | const notificationId = 1; 28 | // const notificationIcon = 'http://localhost:12544/local-filesystem/app/android-icon-36x36.png' 29 | const notificationIcon = 'res://icon.png' 30 | 31 | cordova.plugins.notification.local.schedule({ 32 | id: notificationId, 33 | text: "Stingray Detected", 34 | led: "FFFFFF", 35 | icon: notificationIcon 36 | }); 37 | 38 | // Do something on click 39 | // cordova.plugins.notification.local.on("click", notification => { 40 | // if (notification.id === notificationId) { 41 | // 42 | // } 43 | // }); 44 | } 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /mobile-config.js: -------------------------------------------------------------------------------- 1 | App.info({ 2 | id: 'org.stingraymappingproject.sting_watch', 3 | name: 'StingWatch', 4 | description: 'StingWatch', 5 | author: 'The Stingray Mapping Project', 6 | email: 'marvin@unplugged.im', 7 | website: 'https://www.stingraymappingproject.org', 8 | version: '0.2.36', 9 | }); 10 | 11 | App.icons({ 12 | android_mdpi: 'public/android-icon-48x48.png', 13 | android_hdpi: 'public/android-icon-72x72.png', 14 | android_xhdpi: 'public/android-icon-96x96.png', 15 | android_xxhdpi: 'public/android-icon-144x144.png', 16 | android_xxxhdpi: 'public/android-icon-192x192.png' 17 | }); 18 | 19 | App.accessRule('https://*.amazonaws.com'); 20 | App.accessRule('https://*.mapbox.com'); 21 | App.accessRule('http://*.mapbox.com'); 22 | App.accessRule('https://*.stingraymappingproject.org'); 23 | App.accessRule('https://*.twitter.com'); 24 | 25 | App.launchScreens({ 26 | android_mdpi_portrait: 'public/splash/screen.png', 27 | android_mdpi_landscape: 'public/splash/screen-land.png', 28 | android_hdpi_portrait: 'public/splash/screen.png', 29 | android_hdpi_landscape: 'public/splash/screen-land.png', 30 | android_xhdpi_portrait: 'public/splash/screen.png', 31 | android_xhdpi_landscape: 'public/splash/screen-land.png', 32 | android_xxhdpi_portrait: 'public/splash/screen.png', 33 | android_xxhdpi_landscape: 'public/splash/screen-land.png' 34 | }); 35 | 36 | App.setPreference('android-versionCode', '36'); 37 | // App.setPreference('BackgroundColor', '0xff0000ff'); 38 | // App.setPreference('Orientation', 'default'); 39 | // App.setPreference('Orientation', 'all', 'ios'); 40 | -------------------------------------------------------------------------------- /imports/ui/pages/IntroPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import IntroSlide1 from '../components/intro/IntroSlide1.jsx'; 4 | import IntroSlide2 from '../components/intro/IntroSlide2.jsx'; 5 | import IntroSlide3 from '../components/intro/IntroSlide3.jsx'; 6 | 7 | export default class IntroPage extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | page: 1 13 | }; 14 | } 15 | 16 | introPrev() { 17 | let page = this.state.page; 18 | switch (page) { 19 | case 1: 20 | // Do nothing if on first page 21 | break; 22 | case 2: 23 | this.setState({ page: --page }); 24 | break; 25 | case 3: 26 | this.setState({ page: --page }); 27 | break; 28 | } 29 | } 30 | 31 | introNext() { 32 | let page = this.state.page; 33 | switch (page) { 34 | case 1: 35 | this.setState({ page: ++page }); 36 | break; 37 | case 2: 38 | this.setState({ page: ++page }); 39 | break; 40 | case 3: 41 | // Do nothing if on last page 42 | break; 43 | } 44 | } 45 | 46 | render() { 47 | switch (this.state.page) { 48 | case 1: 49 | return 50 | break; 51 | case 2: 52 | return 53 | break; 54 | case 3: 55 | return 56 | break; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /imports/ui/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Session } from 'meteor/session'; 3 | import { browserHistory } from 'react-router'; 4 | import { createContainer } from 'meteor/react-meteor-data'; 5 | 6 | import { RLS } from 'meteor/marvin:reactive-local-store'; 7 | import { SESSION_STATUS, STATUSES, SETTINGS } from '../globals.js'; 8 | 9 | import GeekMode from './components/GeekMode.jsx'; 10 | 11 | class App extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | geekModeEnabled: false 17 | }; 18 | } 19 | 20 | componentDidMount () { 21 | // If Terms already reviewed, skip intro and redirect 22 | if((RLS.get(SETTINGS.TERMS_ACCEPTED) !== undefined) && 23 | (RLS.get(SETTINGS.TERMS_ACCEPTED) !== null)) { 24 | 25 | browserHistory.push('/status'); 26 | } 27 | } 28 | 29 | toggleGeekMode() { 30 | const geekModeEnabled = this.state.geekModeEnabled; 31 | this.setState({ geekModeEnabled: !geekModeEnabled }); 32 | } 33 | 34 | render() { 35 | return ( 36 |
37 | { 38 | React.cloneElement(this.props.children, { 39 | toggleGeekMode: this.toggleGeekMode.bind(this), 40 | status: this.props.status 41 | }) 42 | } 43 | 44 | { 45 | (this.state.geekModeEnabled) ? 46 | :
47 | } 48 |
49 | ); 50 | } 51 | } 52 | 53 | export default createContainer(() => { 54 | Session.setDefault(SESSION_STATUS, STATUSES.SCANNING); 55 | 56 | return { 57 | status: Session.get(SESSION_STATUS) 58 | }; 59 | }, App); 60 | -------------------------------------------------------------------------------- /imports/ui/pages/TermsRejectedPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Session } from 'meteor/session'; 3 | import { Link, browserHistory } from 'react-router'; 4 | import { RLS } from 'meteor/marvin:reactive-local-store'; 5 | import { SETTINGS } from '../../globals.js'; 6 | 7 | export default class TermsRejectedPage extends React.Component { 8 | handleReject() { 9 | RLS.set(SETTINGS.TERMS_ACCEPTED, false); 10 | browserHistory.push('/status'); 11 | } 12 | 13 | renderButtons() { 14 | return ( 15 |
16 |
17 | 18 | Back to terms 19 | 20 |
21 |
22 | 26 |
27 |
28 | ); 29 | } 30 | 31 | render() { 32 | return ( 33 |
34 |
35 |
36 |

Are you sure?

37 | 38 |

Even if you don't accept, you can still use StingWatch and receive warnings from detections recorded by users nearby you.

39 | 40 |

But unless you enable data collection StingWatch cannot detect any unsual activity on your device.

41 | 42 | {this.renderButtons()} 43 |
44 |
45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /imports/ui/components/GeekMode.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Session } from 'meteor/session'; 3 | import { DeviceId } from 'meteor/marvin:device-id'; 4 | 5 | import { triggerDanger } from '../../lib/trigger-danger.js'; 6 | import { SESSION_STATUS, STATUSES } from '../../globals.js'; 7 | 8 | export default class GeekMode extends React.Component { 9 | 10 | handleDetection() { 11 | triggerDanger(); 12 | } 13 | 14 | handleScan() { 15 | Session.set(SESSION_STATUS, STATUSES.SCANNING); 16 | } 17 | 18 | refreshDeviceId() { 19 | const thiz = this; 20 | DeviceId.regen(() => { 21 | thiz.forceUpdate(); 22 | }); 23 | } 24 | 25 | renderStatusButton() { 26 | if(this.props.status === STATUSES.DANGER) { 27 | return ( 28 | 33 | ); 34 | } else { // if(Session.get(SESSION_STATUS) === STATUSES.SCANNING) { 35 | return ( 36 | 41 | ); 42 | } 43 | } 44 | 45 | renderDeviceIdButton() { 46 | return ( 47 | 50 | ); 51 | } 52 | 53 | render() { 54 | return ( 55 |
56 |
57 |
Geek Mode lets you change your settings and access advanced features.
58 |

Only use this if you know what you are doing.

59 | 60 | {this.renderDeviceIdButton()} 61 | {this.renderStatusButton()} 62 | 63 |
64 | ); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /imports/ui/pages/StatusPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Session } from 'meteor/session'; 3 | import { createContainer } from 'meteor/react-meteor-data'; 4 | import { Catcher } from 'meteor/marvin:imsi-catcher-catcher'; 5 | 6 | import { DeviceId } from 'meteor/marvin:device-id'; 7 | 8 | import { SESSION_STATUS, STATUSES } from '../../globals.js'; 9 | 10 | import StatusScanning from '../components/status/StatusScanning.jsx'; 11 | import StatusDanger from '../components/status/StatusDanger.jsx'; 12 | import TweetComposer from '../components/status/TweetComposer.jsx'; 13 | 14 | class StatusPage extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.state = { 19 | composingTweet: false 20 | }; 21 | } 22 | 23 | setComposingTweet(isComposing) { 24 | this.setState({composingTweet: isComposing}) 25 | } 26 | 27 | renderStatus() { 28 | if (this.props.status === STATUSES.SCANNING) { 29 | return ( 30 | 33 | ); 34 | } else { 35 | return ( 36 | 40 | ); 41 | } 42 | } 43 | 44 | render() { 45 | if(this.state.composingTweet) { 46 | return ( 47 | 50 | ); 51 | } else { 52 | return this.renderStatus(); 53 | } 54 | } 55 | } 56 | 57 | export default createContainer(() => { 58 | const deviceId = DeviceId.get(); 59 | const detectionHandle = Meteor.subscribe('catcher.detections.relevant', deviceId); 60 | const detection = Catcher.Detections.findOne(); 61 | const isMine = detection && (detection.deviceId === deviceId); 62 | 63 | return { 64 | detection: detection, 65 | isMine: isMine 66 | }; 67 | }, StatusPage); 68 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.8 2 | accounts-oauth@1.1.13 3 | accounts-twitter@1.0.10 4 | aldeed:collection2@2.8.0 5 | aldeed:collection2-core@1.0.0 6 | aldeed:schema-deny@1.0.1 7 | aldeed:schema-index@1.0.1 8 | aldeed:simple-schema@1.5.3 9 | allow-deny@1.0.5 10 | autoupdate@1.2.10 11 | babel-compiler@6.8.3 12 | babel-runtime@0.1.9_1 13 | base64@1.0.9 14 | binary-heap@1.0.9 15 | blaze@2.1.8 16 | blaze-tools@1.0.9 17 | boilerplate-generator@1.0.9 18 | caching-compiler@1.0.5_1 19 | caching-html-compiler@1.0.6 20 | callback-hook@1.0.9 21 | check@1.2.3 22 | crosswalk@1.6.2 23 | ddp@1.2.5 24 | ddp-client@1.2.8_1 25 | ddp-common@1.2.6 26 | ddp-rate-limiter@1.0.5 27 | ddp-server@1.2.8_1 28 | deps@1.0.12 29 | diff-sequence@1.0.6 30 | ecmascript@0.4.6_1 31 | ecmascript-runtime@0.2.11_1 32 | ejson@1.0.12 33 | es5-shim@4.5.12_1 34 | fastclick@1.0.12 35 | fortawesome:fontawesome@4.5.0 36 | fourseven:scss@3.8.0_1 37 | geojson-utils@1.0.9 38 | hot-code-push@1.0.4 39 | html-tools@1.0.10 40 | htmljs@1.0.10 41 | http@1.1.7 42 | id-map@1.0.8 43 | jquery@1.11.9 44 | launch-screen@1.0.12 45 | livedata@1.0.18 46 | localstorage@1.0.11 47 | logging@1.0.13_1 48 | marvin:device-id@0.0.1 49 | marvin:imsi-catcher-catcher@0.0.1 50 | marvin:reactive-local-store@0.0.1 51 | matb33:collection-hooks@0.8.1 52 | mdg:geolocation@1.3.0 53 | mdg:validation-error@0.5.1 54 | meteor@1.1.15_1 55 | meteor-base@1.0.4 56 | minifier-css@1.1.12_1 57 | minifier-js@1.1.12_1 58 | minimongo@1.0.17 59 | mobile-experience@1.0.4 60 | mobile-status-bar@1.0.12 61 | modules@0.6.4 62 | modules-runtime@0.6.4_1 63 | mongo@1.1.9_1 64 | mongo-id@1.0.5 65 | npm-mongo@1.4.44_1 66 | oauth@1.1.11 67 | oauth1@1.1.10 68 | observe-sequence@1.0.12 69 | ordered-dict@1.0.8 70 | pauloborges:mapbox@2.2.3_2 71 | promise@0.7.2_1 72 | raix:eventemitter@0.1.3 73 | random@1.0.10 74 | rate-limit@1.0.5 75 | react-meteor-data@0.2.9 76 | reactive-dict@1.1.8 77 | reactive-var@1.0.10 78 | reload@1.1.10 79 | retry@1.0.8 80 | routepolicy@1.0.11 81 | service-configuration@1.0.10 82 | session@1.1.6 83 | spacebars@1.0.12 84 | spacebars-compiler@1.0.12 85 | standard-minifier-css@1.0.7_1 86 | standard-minifier-js@1.0.7_1 87 | static-html@1.0.10_1 88 | templating@1.1.12_1 89 | templating-tools@1.0.4 90 | tmeasday:check-npm-versions@0.3.1 91 | tracker@1.0.14 92 | twbs:bootstrap@4.0.0-alpha.2 93 | twitter@1.1.11 94 | ui@1.0.11 95 | underscore@1.0.9 96 | url@1.0.10 97 | webapp@1.2.9_1 98 | webapp-hashing@1.0.9 99 | -------------------------------------------------------------------------------- /imports/ui/components/status/TweetComposer.jsx: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { createContainer } from 'meteor/react-meteor-data'; 5 | 6 | import { STATUSES } from '../../../globals.js'; 7 | import { DEFAULT_TWEETS } from '../../../globals.js'; 8 | 9 | export default class TweetComposer extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | charactersRemaining: 140, 15 | }; 16 | } 17 | 18 | componentDidMount() { 19 | this.handleKeyUp(); 20 | } 21 | 22 | tweet() { 23 | const thiz = this; 24 | thiz.handleKeyUp(); 25 | 26 | if(thiz.state.charactersRemaining >= 0) { 27 | Meteor.call('twitter.tweet', thiz.tweetText(), (error, response) => { 28 | if(error) { 29 | console.log('tweet failed'); 30 | console.log(error); 31 | } else { 32 | thiz.cancelComposingTweet(); 33 | } 34 | }) 35 | } 36 | } 37 | 38 | defaultTweet() { 39 | return (this.props.status == STATUSES.SCANNING) ? DEFAULT_TWEETS.SCANNING : DEFAULT_TWEETS.DANGER; 40 | } 41 | 42 | cancelComposingTweet() { 43 | this.props.setComposingTweet(false); 44 | } 45 | 46 | tweetText() { 47 | return ReactDOM.findDOMNode(this.refs.tweetTextArea).value.trim(); 48 | } 49 | 50 | handleKeyUp() { 51 | this.setState({charactersRemaining: (140 - this.tweetText().length)}); 52 | } 53 | 54 | render() { 55 | return ( 56 |
57 |
58 |
59 | 60 | 63 |
64 |
65 | 66 |

67 | {this.state.charactersRemaining} characters remaining 68 |

69 | 70 | 75 | 76 | 81 |
82 | ); 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /imports/ui/pages/TermsPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Session } from 'meteor/session'; 3 | import { Link, browserHistory } from 'react-router'; 4 | import { RLS } from 'meteor/marvin:reactive-local-store'; 5 | import { SETTINGS } from '../../globals.js'; 6 | 7 | export default class TermsPage extends React.Component { 8 | handleAccept() { 9 | RLS.set(SETTINGS.TERMS_ACCEPTED, true); 10 | browserHistory.push('/status'); 11 | } 12 | 13 | renderPolicyText() { 14 | return ( 15 |
16 |

Data Policy

17 |

In order to work, StingWatch records information about the cellphone towers you connect to and stores your GPS coordinates in the cloud.

18 |

To keep you anonymous, your phone will generate a random Device ID that will be sent in along with your data. No other identifying information is collected. No IPs, no names, no email addresses.

19 |

Data is being encrypted in motion with TLS.

20 |

Although we plan on adding encryption at rest, this information is currently stored in cleartext on AWS. We will never sell your information to anybody. But after performing further data anonymization, we will make subsets of this information available to other IMSI-catcher researchers.

21 |

Most importantly, data and especially detections will be mapped on our public website, stingraymappingproject.org, with 500m accuracy from where the reading actually occured.

22 |

We recognize that this level of data collection may be prohibitive for some users but it is essential for us to be able to detect Stingrays and map their usage.

23 |

If this is an issue for you, consider using another similar tool like SnoopSnitch, AIMSICD, Darshak, or IMSI-Catcher-Catcher. Or just, submit a pull request @ github.com/marvinmarnold/stingwatch

24 |
25 | ); 26 | } 27 | 28 | renderButtons() { 29 | return ( 30 |
31 |
32 | 35 |
36 |
37 | 38 | 39 | Reject 40 | 41 |
42 |
43 | ); 44 | } 45 | 46 | render() { 47 | return ( 48 |
49 |
50 |
51 | {this.renderPolicyText()} 52 | {this.renderButtons()} 53 |
54 |
55 |
56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About StingWatch 2 | StingWatch is an Android app built with Meteor (using React + Cordova) to catch IMSI-catchers (aka Stingrays or cell site simulators). These are electronic devices for mass surveillance used by many local police departments and other groups around the world. [Learn more](https://www.stingraymappingproject.org). 3 | 4 | In a nutshell, StingWatch: 5 | - is based on modular components that in the long run should make the code reusable across many types of devices (PCs, iPhones, IoT, RaspberryPi, etc.) 6 | - will contribute and pull from a shared database for advanced detections 7 | - is more limited functionality and reliability than other well established apps like [SnoopSnitch](https://opensource.srlabs.de/projects/snoopsnitch) and [AIMSICD](https://github.com/CellularPrivacy/Android-IMSI-Catcher-Detector) 8 | - does not require rooting 9 | 10 | ## imsi-catcher-catcher 11 | The main package that StingWatch relies on is [imsi-catcher-catcher](https://github.com/marvinmarnold/meteor-imsi-catcher-catcher). 12 | imsi-catcher-catcher is also being incorporated into another project at the same time, [StingWatch Desktop](https://github.com/marvinmarnold/stingwatch-desktop), a version of StingWatch that can run on anything that supports gnuradio and Meteor, like an [Intel Compute Stick](http://www.intel.com/content/www/us/en/compute-stick/intel-compute-stick.html). 13 | 14 | # Get StingWatch 15 | ## APKs 16 | * [Google Play](https://play.google.com/apps/testing/org.stingraymappingproject.sting_watch) (in beta testing) 17 | * FDroid coming soon 18 | * Direct APK link coming soon 19 | 20 | ## Install from source 21 | Make sure the following are installed: 22 | - git, curl: `sudo apt-get install git curl` 23 | - Meteor: https://www.meteor.com/install 24 | - Android SDK: https://guide.meteor.com/mobile.html 25 | 26 | ### Get the code and plugins 27 | Both these methods are assuming an installation into `~/.stingwatch`. 28 | #### Option 1: Script 29 | `curl https://stingraymappingproject.org/install.sh | sh` 30 | 31 | #### Option 2: Manual 32 | These instructions are copied from the install script above: 33 | ```` 34 | # Get StingWatch 35 | git clone git@github.com:marvinmarnold/stingwatch.git 36 | cd stingwatch 37 | 38 | # Install NPM packages 39 | sudo meteor npm install --save react react-addons-pure-render-mixin react-dom react-hammerjs react-router twitter 40 | 41 | # Setup location for packages 42 | mkdir packages 43 | cd packages 44 | 45 | # Link to Bootstrap 46 | git clone git@github.com:marvinmarnold/bootstrap.git 47 | cd bootstrap 48 | git checkout stingwatch 49 | cd .. 50 | 51 | git clone git@github.com:marvinmarnold/cordova-plugin-telephony.git 52 | git clone git@github.com:marvinmarnold/meteor-device-id.git 53 | git clone git@github.com:marvinmarnold/meteor-imsi-catcher-catcher.git 54 | git clone git@github.com:marvinmarnold/meteor-reactive-local-store.git 55 | cd .. 56 | 57 | # Copy over settings 58 | cp settings-example.json settings.json 59 | 60 | chmod +x stingwatch.sh 61 | ```` 62 | 63 | ### Configure settings 64 | Register with Mapbox and Twitter. 65 | Fill in `settings.json` with keys. 66 | 67 | # Use StingWatch 68 | Make sure your Android device is plugged in and that `adb devices` shows it. 69 | 70 | ## Local server 71 | The fastest way to get started it to make your computer the server the app talks to. 72 | ```` 73 | cd ~/.stingwatch 74 | ./stingwatch 75 | ```` 76 | The server will start the server on `localhost:4000` and is equivalent to running `meteor run android-device -p 4000 --settings settings.json 77 | `. 78 | -------------------------------------------------------------------------------- /imports/ui/components/status/StatusDanger.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { createContainer } from 'meteor/react-meteor-data'; 3 | 4 | import { DeviceId } from 'meteor/marvin:device-id'; 5 | 6 | import LearnButton from './LearnButton.jsx'; 7 | import NavBar from './NavBar.jsx'; 8 | import TweetButton from './TweetButton.jsx'; 9 | 10 | class StatusDanger extends React.Component { 11 | componentDidMount() { 12 | // console.log('componentDidMount'); 13 | this.props.initMap(); 14 | } 15 | 16 | componentWillUnmount() { 17 | // console.log('componentWillUnmount'); 18 | this.props.unmountMap(); 19 | } 20 | 21 | // Display different warnings depending on if: 22 | // 1) there is a recent dectection, or 23 | // 2) the detction is a test or not, or 24 | // 3) if detection was recorded by current user 25 | renderThreatDescription() { 26 | const detection = this.props.detection; 27 | if(detection) { 28 | // is test 29 | if(detection.isTest) { 30 | 31 | } else { 32 | // current user 33 | if(detection.deviceId === DeviceId.get()) { 34 | 35 | } else { 36 | 37 | } 38 | } 39 | } else { 40 | return

All Detections are too old

41 | } 42 | } 43 | 44 | render() { 45 | return ( 46 |
47 | 48 |
49 |
50 | {this.renderThreatDescription()} 51 |

Threat detected

52 |

StingWatch has detected a Stingray within 500m of you.

53 |
54 |
55 | 56 |
57 |
58 | 61 |
62 |
63 |
64 |
65 | ); 66 | } 67 | } 68 | 69 | let frameLoaded = false; 70 | let map, threatsLayer; 71 | export default createContainer(({detection}) => { 72 | 73 | 74 | const unmountMap = () => { 75 | // console.log("unmountMap"); 76 | map = undefined; 77 | threatsLayer = undefined; 78 | frameLoaded = false; 79 | } 80 | 81 | const initMap = () => { 82 | frameLoaded = true; 83 | // console.log("initMap"); 84 | if (Mapbox.loaded()) { 85 | if(!map && !!detection) { 86 | 87 | L.mapbox.accessToken = Meteor.settings.public.MAPBOX_TOKEN; 88 | 89 | map = L.mapbox.map('map', 'mapbox.streets').setView([ 90 | detection.latitude, 91 | detection.longitude 92 | ], 7); 93 | 94 | threatsLayer = L.mapbox.featureLayer().addTo(map); 95 | 96 | L.circleMarker([detection.latitude, detection.longitude], { 97 | fillColor: '#ff0000', 98 | fillOpacity: 0.8, 99 | stroke: false 100 | }).addTo(threatsLayer); 101 | 102 | } 103 | } 104 | }; 105 | 106 | Tracker.autorun(() => { 107 | if (Mapbox.loaded()) { 108 | if(!map && !!detection && frameLoaded) { 109 | initMap(); 110 | } 111 | } 112 | }); 113 | 114 | return { 115 | map: map, 116 | threatsLayer: threatsLayer, 117 | initMap: initMap, 118 | unmountMap: unmountMap 119 | }; 120 | }, StatusDanger); 121 | -------------------------------------------------------------------------------- /imports/startup/cordova/telephony.js: -------------------------------------------------------------------------------- 1 | // Primary class for collecting readings to be used in detections 2 | // Tries to read values from cordova-plugin-telephony every refreshPeriod 3 | import { Meteor } from 'meteor/meteor'; 4 | import { RLS } from 'meteor/marvin:reactive-local-store'; 5 | import { SETTINGS } from '../../globals.js'; 6 | import { Catcher } from 'meteor/marvin:imsi-catcher-catcher'; 7 | import { DeviceId } from 'meteor/marvin:device-id'; 8 | 9 | const refreshPeriod = 1000 * 15; 10 | let maximumAge = 60000; 11 | 12 | export function startupTelephony() { 13 | readValues(); 14 | 15 | Meteor.setTimeout(function () { 16 | startupTelephony() 17 | }, refreshPeriod); 18 | } 19 | 20 | function readLocation(telephonyResult) { 21 | // Different phones work better with different values, so trade off 22 | if(maximumAge === 60000) { 23 | maximumAge = 0; 24 | } else { 25 | maximumAge = 60000; 26 | } 27 | console.log('reading location with max age: ' + maximumAge); 28 | // Geolocation.latLng((lat, lng) => { 29 | // console.log('received some sort of response geolocation'); 30 | // console.log('meteor ' + lat + ", " + lng); 31 | // }) 32 | let geo = Geolocation.latLng() 33 | console.log('meteor geo'); 34 | console.log(geo); 35 | 36 | navigator.geolocation.getCurrentPosition(pos => { 37 | // console.log('gps data received'); 38 | 39 | insertGSMReading(telephonyResult, pos) 40 | }, 41 | error => { 42 | console.log('error getting gps data'); 43 | console.log(error); 44 | 45 | insertGSMReading(telephonyResult, {coords: {latitude: -99999, longitude: -99999}}) 46 | }, { maximumAge: maximumAge, timeout: 10000, enableHighAccuracy: true }); 47 | } 48 | 49 | function readValues() { 50 | // console.log("Telephony readValues"); 51 | 52 | // Read telephony values from device through cordova-plugin-telephony 53 | if(RLS.get(SETTINGS.TERMS_ACCEPTED)) { 54 | 55 | window.plugins.telephony.requestReadPermission( () => { 56 | // Start listener for telephony changes 57 | window.plugins.telephony.listenTelephonyInfo( () => { 58 | console.log("Started to listen for telephony changes"); 59 | }); 60 | 61 | // Read telephony values currently available 62 | // Note: Seems like some values are only available on state change 63 | window.plugins.telephony.getTelephonyInfo(result => { 64 | console.log('Telephony received data'); 65 | console.log(result); 66 | createNeighborReadings(result); 67 | 68 | readLocation(result); 69 | }, error => { 70 | console.log('telephony error'); 71 | }); 72 | }) 73 | } 74 | } 75 | 76 | function insertGSMReading (result, pos) { 77 | // console.log('insertGPS reading'); 78 | // console.log(result); 79 | 80 | var gsmReading = { 81 | commonReading: { 82 | deviceId: DeviceId.get(), 83 | readingType: result.phoneType, 84 | deviceScannerId: 2 85 | }, 86 | mcc: parseInt(result.mcc) || -1, 87 | mnc: parseInt(result.mnc) || -1, 88 | lac: parseInt(result.lac) || -1, 89 | cid: parseInt(result.cid) || -1, 90 | psc: parseInt(result.psc) || -1, 91 | latitude: pos.coords.latitude || -1, 92 | longitude: pos.coords.longitude || -1, 93 | signalStrengthDBM: parseInt(result.signalStrength) || -1, 94 | hasNeighbors: result.neighbors && result.neighbors.length > 0 95 | } 96 | 97 | console.log('about to insert GSMReading'); 98 | console.log(gsmReading); 99 | 100 | Meteor.call('catcher.readings.insert', gsmReading, (error, result) => { 101 | // console.log('after insert'); 102 | // console.log(error); 103 | // console.log(result); 104 | }) 105 | } 106 | 107 | function createNeighborReadings(result) { 108 | // console.log('createNeighborReadings'); 109 | var neighbors = result.neighbors 110 | 111 | if(neighbors) { 112 | // console.log('Creating neighbors'); 113 | _.each(neighbors, neighbor => { 114 | var neighborReading = { 115 | commonReading: { 116 | deviceId: DeviceId.get(), 117 | readingType: Catcher.READING_TYPES.ANDROID_V1_NEIGHBOR, 118 | deviceScannerId: 2 119 | }, 120 | networkType: neighbor.networkType, 121 | lac: parseInt(neighbor.lac) || -1, 122 | cid: parseInt(neighbor.cid) || -1, 123 | psc: parseInt(neighbor.psc) || -1, 124 | signalStrengthDBM: parseInt(result.signalStrength) || -1 125 | } 126 | 127 | // console.log(neighborReading); 128 | Meteor.call('catcher.readings.insert', neighborReading) 129 | }) 130 | } 131 | } 132 | --------------------------------------------------------------------------------