├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── codeql.yml │ ├── comment-issue.yml │ ├── lint.yml │ └── testsuite.yml ├── .gitignore ├── .jsbeautifyrc ├── .vscode ├── settings.json └── typings │ └── meteor.d.ts ├── Contributing.md ├── History.md ├── LICENSE ├── README.md ├── client └── monitor.js ├── demo ├── .gitignore ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── identifier │ ├── packages │ ├── platforms │ ├── release │ └── versions ├── README.md ├── client │ ├── main.css │ ├── main.html │ └── main.js ├── deploy.sh ├── package-lock.json ├── package.json ├── packages │ ├── .gitignore │ └── meteor-user-status │ │ ├── client │ │ └── monitor.js │ │ ├── package.js │ │ └── server │ │ └── status.js ├── server │ └── main.js └── settings-example.json ├── docs └── example.png ├── package-lock.json ├── package.js ├── package.json ├── server └── status.js └── tests ├── client_tests.js ├── insecure_login.js ├── monitor_tests.js ├── server_tests.js ├── setup.js └── status_tests.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | packages 3 | phantom_runner.js 4 | start_test.js 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parser": "@babel/eslint-parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module", 16 | "allowImportExportEverywhere": true, 17 | "requireConfigFile": false 18 | }, 19 | "rules": { 20 | "linebreak-style": [ 21 | "error", 22 | "unix" 23 | ], 24 | "quotes": [ 25 | "error", 26 | "single" 27 | ], 28 | "semi": [ 29 | "error", 30 | "always" 31 | ], 32 | "no-console": "off" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '16 20 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v2 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v2 75 | with: 76 | category: "/language:${{matrix.language}}" 77 | -------------------------------------------------------------------------------- /.github/workflows/comment-issue.yml: -------------------------------------------------------------------------------- 1 | name: Add immediate comment on new issues 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | createComment: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Create Comment 12 | uses: peter-evans/create-or-update-comment@v1.4.2 13 | with: 14 | issue-number: ${{ github.event.issue.number }} 15 | body: | 16 | Thank you for submitting this issue! 17 | 18 | We, the Members of Meteor Community Packages take every issue seriously. 19 | Our goal is to provide long-term lifecycles for packages and keep up 20 | with the newest changes in Meteor and the overall NodeJs/JavaScript ecosystem. 21 | 22 | However, we contribute to these packages mostly in our free time. 23 | Therefore, we can't guarantee you issues to be solved within certain time. 24 | 25 | If you think this issue is trivial to solve, don't hesitate to submit 26 | a pull request, too! We will accompany you in the process with reviews and hints 27 | on how to get development set up. 28 | 29 | Please also consider sponsoring the maintainers of the package. 30 | If you don't know who is currently maintaining this package, just leave a comment 31 | and we'll let you know 32 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: "Lint test" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | name: Javascript standard lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 18 18 | - name: cache dependencies 19 | uses: actions/cache@v3 20 | with: 21 | path: ~/.npm 22 | key: ${{ runner.os }}-node-18-${{ hashFiles('**/package-lock.json') }} 23 | restore-keys: | 24 | ${{ runner.os }}-node-18- 25 | - name: Install dependencies 26 | run: npm ci 27 | - name: Run lint 28 | run: npm run lint 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/testsuite.yml: -------------------------------------------------------------------------------- 1 | # the test suite runs the tests (headless, server+client) for multiple Meteor releases 2 | name: Test suite 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | meteorRelease: 15 | - "--release 2.7" 16 | - "--release 2.8.1" 17 | - "--release 2.15" 18 | # Latest version 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Install Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: "14.x" 27 | 28 | - name: Install Dependencies 29 | run: | 30 | curl https://install.meteor.com | /bin/sh 31 | npm i -g @zodern/mtest 32 | - name: Run Tests 33 | run: | 34 | # Fix using old versions of Meteor 35 | export NODE_TLS_REJECT_UNAUTHORIZED=0 36 | 37 | mtest --package ./ --once ${{ matrix.meteorRelease }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | .versions 3 | node_modules 4 | 5 | .idea/ 6 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "js": { 3 | "allowed_file_extensions": ["js", "json", "jshintrc", "jsbeautifyrc"], 4 | "indent_size": 2, 5 | "jslint_happy": true, 6 | "indent_with_tabs": false, 7 | "brace_style": "collapse, preserve-inline", 8 | "max_preserve_newlines": 4, 9 | "end_with_newline": true, 10 | "preserve_newlines": true 11 | }, 12 | "html": { 13 | "allowed_file_extensions": ["htm", "html", "xhtml", "shtml", "xml", "svg", "dust"], 14 | "indent_size": 2, 15 | "indent_with_tabs": false, 16 | "end_with_newline": true, 17 | "preserve_newlines": true 18 | }, 19 | "css": { 20 | "allowed_file_extensions": ["css", "scss", "sass", "less"], 21 | "indent_size": 2, 22 | "indent_with_tabs": false, 23 | "newline_between_rules": true, 24 | "end_with_newline": true, 25 | "selector_separator_newline": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "powermode.enabled": true, 3 | "editor.formatOnSave": true, 4 | "cSpell.enabled": true, 5 | "jshint.options": { 6 | "esversion": 6 7 | }, 8 | "git.confirmSync": false, 9 | "html.format.wrapLineLength": 0, 10 | "files.eol": "\n" 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/typings/meteor.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/meteor-typings/meteor/241d0e5335025e64fc3ea064de80a026a08f3f06/1.3/header.d.ts 3 | 4 | // Generated by typings 5 | // Source: https://raw.githubusercontent.com/meteor-typings/meteor/241d0e5335025e64fc3ea064de80a026a08f3f06/1.3/main.d.ts 6 | declare module Accounts { 7 | function user(): Meteor.User; 8 | 9 | function userId(): string; 10 | 11 | function createUser(options: { 12 | username ? : string; 13 | email ? : string; 14 | password ? : string; 15 | profile ? : Object; 16 | }, callback ? : Function): string; 17 | 18 | function config(options: { 19 | sendVerificationEmail ? : boolean; 20 | forbidClientAccountCreation ? : boolean; 21 | restrictCreationByEmailDomain ? : string | Function; 22 | loginExpirationInDays ? : number; 23 | oauthSecretKey ? : string; 24 | }): void; 25 | 26 | function onLogin(func: Function): { 27 | stop: () => void 28 | }; 29 | 30 | function onLoginFailure(func: Function): { 31 | stop: () => void 32 | }; 33 | 34 | function loginServicesConfigured(): boolean; 35 | 36 | function onPageLoadLogin(func: Function): void; 37 | } 38 | 39 | declare module "meteor/accounts-base" { 40 | /// 41 | 42 | module Accounts { 43 | function user(): Meteor.User; 44 | 45 | function userId(): string; 46 | 47 | function createUser(options: { 48 | username ? : string; 49 | email ? : string; 50 | password ? : string; 51 | profile ? : Object; 52 | }, callback ? : Function): string; 53 | 54 | function config(options: { 55 | sendVerificationEmail ? : boolean; 56 | forbidClientAccountCreation ? : boolean; 57 | restrictCreationByEmailDomain ? : string | Function; 58 | loginExpirationInDays ? : number; 59 | oauthSecretKey ? : string; 60 | }): void; 61 | 62 | function onLogin(func: Function): { 63 | stop: () => void 64 | }; 65 | 66 | function onLoginFailure(func: Function): { 67 | stop: () => void 68 | }; 69 | 70 | function loginServicesConfigured(): boolean; 71 | 72 | function onPageLoadLogin(func: Function): void; 73 | } 74 | } 75 | declare module Accounts { 76 | function changePassword(oldPassword: string, newPassword: string, callback ? : Function): void; 77 | 78 | function forgotPassword(options: { 79 | email ? : string; 80 | }, callback ? : Function): void; 81 | 82 | function resetPassword(token: string, newPassword: string, callback ? : Function): void; 83 | 84 | function verifyEmail(token: string, callback ? : Function): void; 85 | 86 | function onEmailVerificationLink(callback: Function): void; 87 | 88 | function onEnrollmentLink(callback: Function): void; 89 | 90 | function onResetPasswordLink(callback: Function): void; 91 | 92 | function loggingIn(): boolean; 93 | 94 | function logout(callback ? : Function): void; 95 | 96 | function logoutOtherClients(callback ? : Function): void; 97 | 98 | var ui: { 99 | config(options: { 100 | requestPermissions ? : Object; 101 | requestOfflineToken ? : Object; 102 | forceApprovalPrompt ? : Object; 103 | passwordSignupFields ? : string; 104 | }): void; 105 | }; 106 | } 107 | 108 | declare module "meteor/accounts-base" { 109 | module Accounts { 110 | function changePassword(oldPassword: string, newPassword: string, callback ? : Function): void; 111 | 112 | function forgotPassword(options: { 113 | email ? : string; 114 | }, callback ? : Function): void; 115 | 116 | function resetPassword(token: string, newPassword: string, callback ? : Function): void; 117 | 118 | function verifyEmail(token: string, callback ? : Function): void; 119 | 120 | function onEmailVerificationLink(callback: Function): void; 121 | 122 | function onEnrollmentLink(callback: Function): void; 123 | 124 | function onResetPasswordLink(callback: Function): void; 125 | 126 | function loggingIn(): boolean; 127 | 128 | function logout(callback ? : Function): void; 129 | 130 | function logoutOtherClients(callback ? : Function): void; 131 | 132 | var ui: { 133 | config(options: { 134 | requestPermissions ? : Object; 135 | requestOfflineToken ? : Object; 136 | forceApprovalPrompt ? : Object; 137 | passwordSignupFields ? : string; 138 | }): void; 139 | }; 140 | } 141 | } 142 | 143 | interface EmailFields { 144 | from ? : () => string; 145 | subject ? : (user: Meteor.User) => string; 146 | text ? : (user: Meteor.User, url: string) => string; 147 | html ? : (user: Meteor.User, url: string) => string; 148 | } 149 | 150 | interface Header { 151 | [id: string]: string; 152 | } 153 | 154 | interface EmailTemplates { 155 | from: string; 156 | siteName: string; 157 | headers ? : Header; 158 | resetPassword: EmailFields; 159 | enrollAccount: EmailFields; 160 | verifyEmail: EmailFields; 161 | } 162 | 163 | declare module Accounts { 164 | var emailTemplates: EmailTemplates; 165 | 166 | function addEmail(userId: string, newEmail: string, verified ? : boolean): void; 167 | 168 | function removeEmail(userId: string, email: string): void; 169 | 170 | function onCreateUser(func: Function): void; 171 | 172 | function findUserByEmail(email: string): Object; 173 | 174 | function findUserByUsername(username: string): Object; 175 | 176 | function sendEnrollmentEmail(userId: string, email ? : string): void; 177 | 178 | function sendResetPasswordEmail(userId: string, email ? : string): void; 179 | 180 | function sendVerificationEmail(userId: string, email ? : string): void; 181 | 182 | function setUsername(userId: string, newUsername: string): void; 183 | 184 | function setPassword(userId: string, newPassword: string, options ? : { 185 | logout ? : Object; 186 | }): void; 187 | 188 | function validateNewUser(func: Function): boolean; 189 | 190 | function validateLoginAttempt(func: Function): { 191 | stop: () => void 192 | }; 193 | 194 | interface IValidateLoginAttemptCbOpts { 195 | type: string; 196 | allowed: boolean; 197 | error: Meteor.Error; 198 | user: Meteor.User; 199 | connection: Meteor.Connection; 200 | methodName: string; 201 | methodArguments: any[]; 202 | } 203 | } 204 | 205 | declare module "meteor/accounts-base" { 206 | /// 207 | /// 208 | 209 | interface EmailFields { 210 | from ? : () => string; 211 | subject ? : (user: Meteor.User) => string; 212 | text ? : (user: Meteor.User, url: string) => string; 213 | html ? : (user: Meteor.User, url: string) => string; 214 | } 215 | 216 | interface Header { 217 | [id: string]: string; 218 | } 219 | 220 | interface EmailTemplates { 221 | from: string; 222 | siteName: string; 223 | headers ? : Header; 224 | resetPassword: EmailFields; 225 | enrollAccount: EmailFields; 226 | verifyEmail: EmailFields; 227 | } 228 | 229 | module Accounts { 230 | var emailTemplates: EmailTemplates; 231 | 232 | function addEmail(userId: string, newEmail: string, verified ? : boolean): void; 233 | 234 | function removeEmail(userId: string, email: string): void; 235 | 236 | function onCreateUser(func: Function): void; 237 | 238 | function findUserByEmail(email: string): Object; 239 | 240 | function findUserByUsername(username: string): Object; 241 | 242 | function sendEnrollmentEmail(userId: string, email ? : string): void; 243 | 244 | function sendResetPasswordEmail(userId: string, email ? : string): void; 245 | 246 | function sendVerificationEmail(userId: string, email ? : string): void; 247 | 248 | function setUsername(userId: string, newUsername: string): void; 249 | 250 | function setPassword(userId: string, newPassword: string, options ? : { 251 | logout ? : Object; 252 | }): void; 253 | 254 | function validateNewUser(func: Function): boolean; 255 | 256 | function validateLoginAttempt(func: Function): { 257 | stop: () => void 258 | }; 259 | 260 | interface IValidateLoginAttemptCbOpts { 261 | type: string; 262 | allowed: boolean; 263 | error: Meteor.Error; 264 | user: Meteor.User; 265 | connection: Meteor.Connection; 266 | methodName: string; 267 | methodArguments: any[]; 268 | } 269 | } 270 | } 271 | 272 | declare module Blaze { 273 | var View: ViewStatic; 274 | 275 | interface ViewStatic { 276 | new(name ? : string, renderFunction ? : Function): View; 277 | } 278 | 279 | interface View { 280 | name: string; 281 | parentView: View; 282 | isCreated: boolean; 283 | isRendered: boolean; 284 | isDestroyed: boolean; 285 | renderCount: number; 286 | autorun(runFunc: Function): void; 287 | onViewCreated(func: Function): void; 288 | onViewReady(func: Function): void; 289 | onViewDestroyed(func: Function): void; 290 | firstNode(): Node; 291 | lastNode(): Node; 292 | template: Template; 293 | templateInstance(): TemplateInstance; 294 | } 295 | var currentView: View; 296 | 297 | function isTemplate(value: any): boolean; 298 | 299 | interface HelpersMap { 300 | [key: string]: Function; 301 | } 302 | 303 | interface EventsMap { 304 | [key: string]: Function; 305 | } 306 | 307 | var Template: TemplateStatic; 308 | 309 | interface TemplateStatic { 310 | new(viewName ? : string, renderFunction ? : Function): Template; 311 | 312 | registerHelper(name: string, func: Function): void; 313 | instance(): TemplateInstance; 314 | currentData(): any; 315 | parentData(numLevels: number): any; 316 | } 317 | 318 | interface Template { 319 | viewName: string; 320 | renderFunction: Function; 321 | constructView(): View; 322 | head: Template; 323 | find(selector: string): Template; 324 | findAll(selector: string): Template[]; 325 | $: any; 326 | onCreated(cb: Function): void; 327 | onRendered(cb: Function): void; 328 | onDestroyed(cb: Function): void; 329 | created: Function; 330 | rendered: Function; 331 | destroyed: Function; 332 | helpers(helpersMap: HelpersMap): void; 333 | events(eventsMap: EventsMap): void; 334 | } 335 | 336 | var TemplateInstance: TemplateInstanceStatic; 337 | 338 | interface TemplateInstanceStatic { 339 | new(view: View): TemplateInstance; 340 | } 341 | 342 | interface TemplateInstance { 343 | $(selector: string): any; 344 | autorun(runFunc: Function): Object; 345 | data: Object; 346 | find(selector ? : string): TemplateInstance; 347 | findAll(selector: string): TemplateInstance[]; 348 | firstNode: Object; 349 | lastNode: Object; 350 | subscribe(name: string, ...args: any[]): Meteor.SubscriptionHandle; 351 | subscriptionsReady(): boolean; 352 | view: Object; 353 | } 354 | 355 | function Each(argFunc: Function, contentFunc: Function, elseFunc ? : Function): View; 356 | 357 | function Unless(conditionFunc: Function, contentFunc: Function, elseFunc ? : Function): View; 358 | 359 | function If(conditionFunc: Function, contentFunc: Function, elseFunc ? : Function): View; 360 | 361 | function Let(bindings: Function, contentFunc: Function): View; 362 | 363 | function With(data: Object | Function, contentFunc: Function): View; 364 | 365 | function getData(elementOrView ? : HTMLElement | View): Object; 366 | 367 | function getView(element ? : HTMLElement): View; 368 | 369 | function remove(renderedView: View): void; 370 | 371 | function render(templateOrView: Template | View, parentNode: Node, nextNode ? : Node, parentView ? : View): View; 372 | 373 | function renderWithData(templateOrView: Template | View, data: Object | Function, parentNode: Node, nextNode ? : Node, parentView ? : View): View; 374 | 375 | function toHTML(templateOrView: Template | View): string; 376 | 377 | function toHTMLWithData(templateOrView: Template | View, data: Object | Function): string; 378 | } 379 | 380 | declare module "meteor/blaze" { 381 | /// 382 | 383 | module Blaze { 384 | var View: ViewStatic; 385 | 386 | interface ViewStatic { 387 | new(name ? : string, renderFunction ? : Function): View; 388 | } 389 | 390 | interface View { 391 | name: string; 392 | parentView: View; 393 | isCreated: boolean; 394 | isRendered: boolean; 395 | isDestroyed: boolean; 396 | renderCount: number; 397 | autorun(runFunc: Function): void; 398 | onViewCreated(func: Function): void; 399 | onViewReady(func: Function): void; 400 | onViewDestroyed(func: Function): void; 401 | firstNode(): Node; 402 | lastNode(): Node; 403 | template: Template; 404 | templateInstance(): TemplateInstance; 405 | } 406 | var currentView: View; 407 | 408 | function isTemplate(value: any): boolean; 409 | 410 | interface HelpersMap { 411 | [key: string]: Function; 412 | } 413 | 414 | interface EventsMap { 415 | [key: string]: Function; 416 | } 417 | 418 | var Template: TemplateStatic; 419 | 420 | interface TemplateStatic { 421 | new(viewName ? : string, renderFunction ? : Function): Template; 422 | 423 | registerHelper(name: string, func: Function): void; 424 | instance(): TemplateInstance; 425 | currentData(): any; 426 | parentData(numLevels: number): any; 427 | } 428 | 429 | interface Template { 430 | viewName: string; 431 | renderFunction: Function; 432 | constructView(): View; 433 | head: Template; 434 | find(selector: string): Template; 435 | findAll(selector: string): Template[]; 436 | $: any; 437 | onCreated(cb: Function): void; 438 | onRendered(cb: Function): void; 439 | onDestroyed(cb: Function): void; 440 | created: Function; 441 | rendered: Function; 442 | destroyed: Function; 443 | helpers(helpersMap: HelpersMap): void; 444 | events(eventsMap: EventsMap): void; 445 | } 446 | 447 | var TemplateInstance: TemplateInstanceStatic; 448 | 449 | interface TemplateInstanceStatic { 450 | new(view: View): TemplateInstance; 451 | } 452 | 453 | interface TemplateInstance { 454 | $(selector: string): any; 455 | autorun(runFunc: Function): Object; 456 | data: Object; 457 | find(selector ? : string): TemplateInstance; 458 | findAll(selector: string): TemplateInstance[]; 459 | firstNode: Object; 460 | lastNode: Object; 461 | subscribe(name: string, ...args: any[]): Meteor.SubscriptionHandle; 462 | subscriptionsReady(): boolean; 463 | view: Object; 464 | } 465 | 466 | function Each(argFunc: Function, contentFunc: Function, elseFunc ? : Function): View; 467 | 468 | function Unless(conditionFunc: Function, contentFunc: Function, elseFunc ? : Function): View; 469 | 470 | function If(conditionFunc: Function, contentFunc: Function, elseFunc ? : Function): View; 471 | 472 | function Let(bindings: Function, contentFunc: Function): View; 473 | 474 | function With(data: Object | Function, contentFunc: Function): View; 475 | 476 | function getData(elementOrView ? : HTMLElement | View): Object; 477 | 478 | function getView(element ? : HTMLElement): View; 479 | 480 | function remove(renderedView: View): void; 481 | 482 | function render(templateOrView: Template | View, parentNode: Node, nextNode ? : Node, parentView ? : View): View; 483 | 484 | function renderWithData(templateOrView: Template | View, data: Object | Function, parentNode: Node, nextNode ? : Node, parentView ? : View): View; 485 | 486 | function toHTML(templateOrView: Template | View): string; 487 | 488 | function toHTMLWithData(templateOrView: Template | View, data: Object | Function): string; 489 | } 490 | } 491 | declare module BrowserPolicy { 492 | interface framing { 493 | disallow(): void; 494 | restrictToOrigin(origin: string): void; 495 | allowAll(): void; 496 | } 497 | 498 | interface content { 499 | allowEval(): void; 500 | allowInlineStyles(): void; 501 | allowInlineScripts(): void; 502 | allowSameOriginForAll(): void; 503 | allowDataUrlForAll(): void; 504 | allowOriginForAll(origin: string): void; 505 | allowImageOrigin(origin: string): void; 506 | allowFrameOrigin(origin: string): void; 507 | allowContentTypeSniffing(): void; 508 | allowAllContentOrigin(): void; 509 | allowAllContentDataUrl(): void; 510 | allowAllContentSameOrigin(): void; 511 | 512 | disallowAll(): void; 513 | disallowInlineStyles(): void; 514 | disallowEval(): void; 515 | disallowInlineScripts(): void; 516 | disallowFont(): void; 517 | disallowObject(): void; 518 | disallowAllContent(): void; 519 | } 520 | } 521 | 522 | declare module "meteor/browser-policy" { 523 | module BrowserPolicy { 524 | interface framing { 525 | disallow(): void; 526 | restrictToOrigin(origin: string): void; 527 | allowAll(): void; 528 | } 529 | 530 | interface content { 531 | allowEval(): void; 532 | allowInlineStyles(): void; 533 | allowInlineScripts(): void; 534 | allowSameOriginForAll(): void; 535 | allowDataUrlForAll(): void; 536 | allowOriginForAll(origin: string): void; 537 | allowImageOrigin(origin: string): void; 538 | allowFrameOrigin(origin: string): void; 539 | allowContentTypeSniffing(): void; 540 | allowAllContentOrigin(): void; 541 | allowAllContentDataUrl(): void; 542 | allowAllContentSameOrigin(): void; 543 | 544 | disallowAll(): void; 545 | disallowInlineStyles(): void; 546 | disallowEval(): void; 547 | disallowInlineScripts(): void; 548 | disallowFont(): void; 549 | disallowObject(): void; 550 | disallowAllContent(): void; 551 | } 552 | } 553 | } 554 | declare module Match { 555 | var Any: any; 556 | var String: any; 557 | var Integer: any; 558 | var Boolean: any; 559 | var undefined: any; 560 | var Object: any; 561 | 562 | function Optional(pattern: any): boolean; 563 | 564 | function ObjectIncluding(dico: any): boolean; 565 | 566 | function OneOf(...patterns: any[]): any; 567 | 568 | function Where(condition: any): any; 569 | 570 | function test(value: any, pattern: any): boolean; 571 | } 572 | 573 | declare function check(value: any, pattern: any): void; 574 | 575 | declare module "meteor/check" { 576 | module Match { 577 | var Any: any; 578 | var String: any; 579 | var Integer: any; 580 | var Boolean: any; 581 | var undefined: any; 582 | var Object: any; 583 | 584 | function Optional(pattern: any): boolean; 585 | 586 | function ObjectIncluding(dico: any): boolean; 587 | 588 | function OneOf(...patterns: any[]): any; 589 | 590 | function Where(condition: any): any; 591 | 592 | function test(value: any, pattern: any): boolean; 593 | } 594 | 595 | function check(value: any, pattern: any): void; 596 | } 597 | 598 | declare module DDP { 599 | interface DDPStatic { 600 | subscribe(name: string, ...rest: any[]): Meteor.SubscriptionHandle; 601 | call(method: string, ...parameters: any[]): void; 602 | apply(method: string, ...parameters: any[]): void; 603 | methods(IMeteorMethodsDictionary: any): any; 604 | status(): DDPStatus; 605 | reconnect(): void; 606 | disconnect(): void; 607 | onReconnect(): void; 608 | } 609 | 610 | function _allSubscriptionsReady(): boolean; 611 | 612 | interface DDPStatus { 613 | connected: boolean; 614 | /** 615 | * connected, 616 | * connecting, 617 | * failed, 618 | * waiting, 619 | * offline 620 | */ 621 | status: string; 622 | retryCount: number; 623 | retryTime ? : number; 624 | reason ? : string; 625 | } 626 | 627 | function connect(url: string): DDPStatic; 628 | } 629 | 630 | declare module DDPCommon { 631 | interface MethodInvocation { 632 | new(options: {}): MethodInvocation; 633 | 634 | unblock(): void; 635 | 636 | setUserId(userId: number): void; 637 | } 638 | } 639 | 640 | declare module "meteor/ddp" { 641 | /// 642 | 643 | module DDP { 644 | interface DDPStatic { 645 | subscribe(name: string, ...rest: any[]): Meteor.SubscriptionHandle; 646 | call(method: string, ...parameters: any[]): void; 647 | apply(method: string, ...parameters: any[]): void; 648 | methods(IMeteorMethodsDictionary: any): any; 649 | status(): DDPStatus; 650 | reconnect(): void; 651 | disconnect(): void; 652 | onReconnect(): void; 653 | } 654 | 655 | function _allSubscriptionsReady(): boolean; 656 | 657 | interface DDPStatus { 658 | connected: boolean; 659 | /** 660 | * connected, 661 | * connecting, 662 | * failed, 663 | * waiting, 664 | * offline 665 | */ 666 | status: string; 667 | retryCount: number; 668 | retryTime ? : number; 669 | reason ? : string; 670 | } 671 | 672 | function connect(url: string): DDPStatic; 673 | } 674 | 675 | module DDPCommon { 676 | interface MethodInvocation { 677 | new(options: {}): MethodInvocation; 678 | 679 | unblock(): void; 680 | 681 | setUserId(userId: number): void; 682 | } 683 | } 684 | } 685 | interface EJSONableCustomType { 686 | clone(): EJSONableCustomType; 687 | equals(other: Object): boolean; 688 | toJSONValue(): JSONable; 689 | typeName(): string; 690 | } 691 | interface EJSONable { 692 | [key: string]: number | string | boolean | Object | number[] | string[] | Object[] | Date | Uint8Array | EJSONableCustomType; 693 | } 694 | interface JSONable { 695 | [key: string]: number | string | boolean | Object | number[] | string[] | Object[]; 696 | } 697 | interface EJSON extends EJSONable {} 698 | 699 | declare module EJSON { 700 | function addType(name: string, factory: (val: JSONable) => EJSONableCustomType): void; 701 | 702 | function clone < T > (val: T): T; 703 | 704 | function equals(a: EJSON, b: EJSON, options ? : { 705 | keyOrderSensitive ? : boolean; 706 | }): boolean; 707 | 708 | function fromJSONValue(val: JSONable): any; 709 | 710 | function isBinary(x: Object): boolean; 711 | var newBinary: any; 712 | 713 | function parse(str: string): EJSON; 714 | 715 | function stringify(val: EJSON, options ? : { 716 | indent ? : boolean | number | string; 717 | canonical ? : boolean; 718 | }): string; 719 | 720 | function toJSONValue(val: EJSON): JSONable; 721 | } 722 | 723 | declare module "meteor/ejson" { 724 | interface EJSONableCustomType { 725 | clone(): EJSONableCustomType; 726 | equals(other: Object): boolean; 727 | toJSONValue(): JSONable; 728 | typeName(): string; 729 | } 730 | interface EJSONable { 731 | [key: string]: number | string | boolean | Object | number[] | string[] | Object[] | Date | Uint8Array | EJSONableCustomType; 732 | } 733 | interface JSONable { 734 | [key: string]: number | string | boolean | Object | number[] | string[] | Object[]; 735 | } 736 | interface EJSON extends EJSONable {} 737 | 738 | module EJSON { 739 | function addType(name: string, factory: (val: JSONable) => EJSONableCustomType): void; 740 | 741 | function clone < T > (val: T): T; 742 | 743 | function equals(a: EJSON, b: EJSON, options ? : { 744 | keyOrderSensitive ? : boolean; 745 | }): boolean; 746 | 747 | function fromJSONValue(val: JSONable): any; 748 | 749 | function isBinary(x: Object): boolean; 750 | var newBinary: any; 751 | 752 | function parse(str: string): EJSON; 753 | 754 | function stringify(val: EJSON, options ? : { 755 | indent ? : boolean | number | string; 756 | canonical ? : boolean; 757 | }): string; 758 | 759 | function toJSONValue(val: EJSON): JSONable; 760 | } 761 | } 762 | declare module Email { 763 | function send(options: { 764 | from ? : string; 765 | to ? : string | string[]; 766 | cc ? : string | string[]; 767 | bcc ? : string | string[]; 768 | replyTo ? : string | string[]; 769 | subject ? : string; 770 | text ? : string; 771 | html ? : string; 772 | headers ? : Object; 773 | attachments ? : Object[]; 774 | mailComposer ? : MailComposer; 775 | }): void; 776 | } 777 | 778 | interface MailComposerOptions { 779 | escapeSMTP: boolean; 780 | encoding: string; 781 | charset: string; 782 | keepBcc: boolean; 783 | forceEmbeddedImages: boolean; 784 | } 785 | 786 | declare var MailComposer: MailComposerStatic; 787 | interface MailComposerStatic { 788 | new(options: MailComposerOptions): MailComposer; 789 | } 790 | interface MailComposer { 791 | addHeader(name: string, value: string): void; 792 | setMessageOption(from: string, to: string, body: string, html: string): void; 793 | streamMessage(): void; 794 | pipe(stream: any /** fs.WriteStream **/ ): void; 795 | } 796 | 797 | declare module "meteor/email" { 798 | module Email { 799 | function send(options: { 800 | from ? : string; 801 | to ? : string | string[]; 802 | cc ? : string | string[]; 803 | bcc ? : string | string[]; 804 | replyTo ? : string | string[]; 805 | subject ? : string; 806 | text ? : string; 807 | html ? : string; 808 | headers ? : Object; 809 | attachments ? : Object[]; 810 | mailComposer ? : MailComposer; 811 | }): void; 812 | } 813 | 814 | interface MailComposerOptions { 815 | escapeSMTP: boolean; 816 | encoding: string; 817 | charset: string; 818 | keepBcc: boolean; 819 | forceEmbeddedImages: boolean; 820 | } 821 | 822 | var MailComposer: MailComposerStatic; 823 | interface MailComposerStatic { 824 | new(options: MailComposerOptions): MailComposer; 825 | } 826 | interface MailComposer { 827 | addHeader(name: string, value: string): void; 828 | setMessageOption(from: string, to: string, body: string, html: string): void; 829 | streamMessage(): void; 830 | pipe(stream: any /** fs.WriteStream **/ ): void; 831 | } 832 | } 833 | declare module HTTP { 834 | interface HTTPRequest { 835 | content ? : string; 836 | data ? : any; 837 | query ? : string; 838 | params ? : { 839 | [id: string]: string 840 | }; 841 | auth ? : string; 842 | headers ? : { 843 | [id: string]: string 844 | }; 845 | timeout ? : number; 846 | followRedirects ? : boolean; 847 | } 848 | 849 | interface HTTPResponse { 850 | statusCode ? : number; 851 | headers ? : { 852 | [id: string]: string 853 | }; 854 | content ? : string; 855 | data ? : any; 856 | } 857 | 858 | function call(method: string, url: string, options ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse; 859 | 860 | function del(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse; 861 | 862 | function get(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse; 863 | 864 | function post(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse; 865 | 866 | function put(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse; 867 | 868 | function call(method: string, url: string, options ? : { 869 | content ? : string; 870 | data ? : Object; 871 | query ? : string; 872 | params ? : Object; 873 | auth ? : string; 874 | headers ? : Object; 875 | timeout ? : number; 876 | followRedirects ? : boolean; 877 | npmRequestOptions ? : Object; 878 | beforeSend ? : Function; 879 | }, asyncCallback ? : Function): HTTP.HTTPResponse; 880 | } 881 | 882 | declare module "meteor/http" { 883 | module HTTP { 884 | interface HTTPRequest { 885 | content ? : string; 886 | data ? : any; 887 | query ? : string; 888 | params ? : { 889 | [id: string]: string 890 | }; 891 | auth ? : string; 892 | headers ? : { 893 | [id: string]: string 894 | }; 895 | timeout ? : number; 896 | followRedirects ? : boolean; 897 | } 898 | 899 | interface HTTPResponse { 900 | statusCode ? : number; 901 | headers ? : { 902 | [id: string]: string 903 | }; 904 | content ? : string; 905 | data ? : any; 906 | } 907 | 908 | function call(method: string, url: string, options ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse; 909 | 910 | function del(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse; 911 | 912 | function get(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse; 913 | 914 | function post(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse; 915 | 916 | function put(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse; 917 | 918 | function call(method: string, url: string, options ? : { 919 | content ? : string; 920 | data ? : Object; 921 | query ? : string; 922 | params ? : Object; 923 | auth ? : string; 924 | headers ? : Object; 925 | timeout ? : number; 926 | followRedirects ? : boolean; 927 | npmRequestOptions ? : Object; 928 | beforeSend ? : Function; 929 | }, asyncCallback ? : Function): HTTP.HTTPResponse; 930 | } 931 | } 932 | 933 | declare module Meteor { 934 | /** Global props **/ 935 | var isClient: boolean; 936 | var isCordova: boolean; 937 | var isServer: boolean; 938 | var release: string; 939 | var settings: { 940 | [id: string]: any 941 | }; 942 | /** props **/ 943 | 944 | /** User **/ 945 | interface UserEmail { 946 | address: string; 947 | verified: boolean; 948 | } 949 | interface User { 950 | _id ? : string; 951 | username ? : string; 952 | emails ? : UserEmail[]; 953 | createdAt ? : number; 954 | profile ? : any; 955 | services ? : any; 956 | } 957 | 958 | function user(): User; 959 | 960 | function userId(): string; 961 | var users: Mongo.Collection < User > ; 962 | /** User **/ 963 | 964 | /** Error **/ 965 | var Error: ErrorStatic; 966 | interface ErrorStatic { 967 | new(error: string | number, reason ? : string, details ? : string): Error; 968 | } 969 | interface Error { 970 | error: string | number; 971 | reason ? : string; 972 | details ? : string; 973 | } 974 | /** Error **/ 975 | 976 | /** Method **/ 977 | function methods(methods: Object): void; 978 | 979 | function call(name: string, ...args: any[]): any; 980 | 981 | function apply(name: string, args: EJSONable[], options ? : { 982 | wait ? : boolean; 983 | onResultReceived ? : Function; 984 | }, asyncCallback ? : Function): any; 985 | /** Method **/ 986 | 987 | /** Url **/ 988 | function absoluteUrl(path ? : string, options ? : { 989 | secure ? : boolean; 990 | replaceLocalhost ? : boolean; 991 | rootUrl ? : string; 992 | }): string; 993 | /** Url **/ 994 | 995 | /** Timeout **/ 996 | function setInterval(func: Function, delay: number): number; 997 | 998 | function setTimeout(func: Function, delay: number): number; 999 | 1000 | function clearInterval(id: number): void; 1001 | 1002 | function clearTimeout(id: number): void; 1003 | 1004 | function defer(func: Function): void; 1005 | /** Timeout **/ 1006 | 1007 | /** utils **/ 1008 | function startup(func: Function): void; 1009 | 1010 | function wrapAsync(func: Function, context ? : Object): any; 1011 | /** utils **/ 1012 | 1013 | /** Pub/Sub **/ 1014 | interface SubscriptionHandle { 1015 | stop(): void; 1016 | ready(): boolean; 1017 | } 1018 | interface LiveQueryHandle { 1019 | stop(): void; 1020 | } 1021 | /** Pub/Sub **/ 1022 | } 1023 | 1024 | declare module "meteor/meteor" { 1025 | /// 1026 | /// 1027 | 1028 | module Meteor { 1029 | /** Global props **/ 1030 | var isClient: boolean; 1031 | var isCordova: boolean; 1032 | var isServer: boolean; 1033 | var release: string; 1034 | var settings: { 1035 | [id: string]: any 1036 | }; 1037 | /** props **/ 1038 | 1039 | /** User **/ 1040 | interface UserEmail { 1041 | address: string; 1042 | verified: boolean; 1043 | } 1044 | interface User { 1045 | _id ? : string; 1046 | username ? : string; 1047 | emails ? : UserEmail[]; 1048 | createdAt ? : number; 1049 | profile ? : any; 1050 | services ? : any; 1051 | } 1052 | 1053 | function user(): User; 1054 | 1055 | function userId(): string; 1056 | var users: Mongo.Collection < User > ; 1057 | /** User **/ 1058 | 1059 | /** Error **/ 1060 | var Error: ErrorStatic; 1061 | interface ErrorStatic { 1062 | new(error: string | number, reason ? : string, details ? : string): Error; 1063 | } 1064 | interface Error { 1065 | error: string | number; 1066 | reason ? : string; 1067 | details ? : string; 1068 | } 1069 | /** Error **/ 1070 | 1071 | /** Method **/ 1072 | function methods(methods: Object): void; 1073 | 1074 | function call(name: string, ...args: any[]): any; 1075 | 1076 | function apply(name: string, args: EJSONable[], options ? : { 1077 | wait ? : boolean; 1078 | onResultReceived ? : Function; 1079 | }, asyncCallback ? : Function): any; 1080 | /** Method **/ 1081 | 1082 | /** Url **/ 1083 | function absoluteUrl(path ? : string, options ? : { 1084 | secure ? : boolean; 1085 | replaceLocalhost ? : boolean; 1086 | rootUrl ? : string; 1087 | }): string; 1088 | /** Url **/ 1089 | 1090 | /** Timeout **/ 1091 | function setInterval(func: Function, delay: number): number; 1092 | 1093 | function setTimeout(func: Function, delay: number): number; 1094 | 1095 | function clearInterval(id: number): void; 1096 | 1097 | function clearTimeout(id: number): void; 1098 | 1099 | function defer(func: Function): void; 1100 | /** Timeout **/ 1101 | 1102 | /** utils **/ 1103 | function startup(func: Function): void; 1104 | 1105 | function wrapAsync(func: Function, context ? : Object): any; 1106 | /** utils **/ 1107 | 1108 | /** Pub/Sub **/ 1109 | interface SubscriptionHandle { 1110 | stop(): void; 1111 | ready(): boolean; 1112 | } 1113 | interface LiveQueryHandle { 1114 | stop(): void; 1115 | } 1116 | /** Pub/Sub **/ 1117 | } 1118 | } 1119 | 1120 | declare module Meteor { 1121 | /** Login **/ 1122 | interface LoginWithExternalServiceOptions { 1123 | requestPermissions ? : string[]; 1124 | requestOfflineToken ? : Boolean; 1125 | forceApprovalPrompt ? : Boolean; 1126 | loginUrlParameters ? : Object; 1127 | redirectUrl ? : string; 1128 | loginHint ? : string; 1129 | loginStyle ? : string; 1130 | } 1131 | 1132 | function loginWithMeteorDeveloperAccount(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1133 | 1134 | function loginWithFacebook(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1135 | 1136 | function loginWithGithub(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1137 | 1138 | function loginWithGoogle(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1139 | 1140 | function loginWithMeetup(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1141 | 1142 | function loginWithTwitter(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1143 | 1144 | function loginWithWeibo(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1145 | 1146 | function loggingIn(): boolean; 1147 | 1148 | function loginWith < ExternalService > (options ? : { 1149 | requestPermissions ? : string[]; 1150 | requestOfflineToken ? : boolean; 1151 | loginUrlParameters ? : Object; 1152 | userEmail ? : string; 1153 | loginStyle ? : string; 1154 | redirectUrl ? : string; 1155 | }, callback ? : Function): void; 1156 | 1157 | function loginWithPassword(user: Object | string, password: string, callback ? : Function): void; 1158 | 1159 | function logout(callback ? : Function): void; 1160 | 1161 | function logoutOtherClients(callback ? : Function): void; 1162 | /** Login **/ 1163 | 1164 | /** Event **/ 1165 | interface Event { 1166 | type: string; 1167 | target: HTMLElement; 1168 | currentTarget: HTMLElement; 1169 | which: number; 1170 | stopPropagation(): void; 1171 | stopImmediatePropagation(): void; 1172 | preventDefault(): void; 1173 | isPropagationStopped(): boolean; 1174 | isImmediatePropagationStopped(): boolean; 1175 | isDefaultPrevented(): boolean; 1176 | } 1177 | interface EventHandlerFunction extends Function { 1178 | (event ? : Meteor.Event, templateInstance ? : Blaze.TemplateInstance): void; 1179 | } 1180 | interface EventMap { 1181 | [id: string]: Meteor.EventHandlerFunction; 1182 | } 1183 | /** Event **/ 1184 | 1185 | /** Connection **/ 1186 | function reconnect(): void; 1187 | 1188 | function disconnect(): void; 1189 | /** Connection **/ 1190 | 1191 | /** Status **/ 1192 | function status(): DDP.DDPStatus; 1193 | /** Status **/ 1194 | 1195 | /** Pub/Sub **/ 1196 | function subscribe(name: string, ...args: any[]): Meteor.SubscriptionHandle; 1197 | /** Pub/Sub **/ 1198 | } 1199 | 1200 | declare module "meteor/meteor" { 1201 | /// 1202 | 1203 | module Meteor { 1204 | /** Login **/ 1205 | interface LoginWithExternalServiceOptions { 1206 | requestPermissions ? : string[]; 1207 | requestOfflineToken ? : Boolean; 1208 | forceApprovalPrompt ? : Boolean; 1209 | loginUrlParameters ? : Object; 1210 | redirectUrl ? : string; 1211 | loginHint ? : string; 1212 | loginStyle ? : string; 1213 | } 1214 | 1215 | function loginWithMeteorDeveloperAccount(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1216 | 1217 | function loginWithFacebook(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1218 | 1219 | function loginWithGithub(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1220 | 1221 | function loginWithGoogle(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1222 | 1223 | function loginWithMeetup(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1224 | 1225 | function loginWithTwitter(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1226 | 1227 | function loginWithWeibo(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void; 1228 | 1229 | function loggingIn(): boolean; 1230 | 1231 | function loginWith < ExternalService > (options ? : { 1232 | requestPermissions ? : string[]; 1233 | requestOfflineToken ? : boolean; 1234 | loginUrlParameters ? : Object; 1235 | userEmail ? : string; 1236 | loginStyle ? : string; 1237 | redirectUrl ? : string; 1238 | }, callback ? : Function): void; 1239 | 1240 | function loginWithPassword(user: Object | string, password: string, callback ? : Function): void; 1241 | 1242 | function logout(callback ? : Function): void; 1243 | 1244 | function logoutOtherClients(callback ? : Function): void; 1245 | /** Login **/ 1246 | 1247 | /** Event **/ 1248 | interface Event { 1249 | type: string; 1250 | target: HTMLElement; 1251 | currentTarget: HTMLElement; 1252 | which: number; 1253 | stopPropagation(): void; 1254 | stopImmediatePropagation(): void; 1255 | preventDefault(): void; 1256 | isPropagationStopped(): boolean; 1257 | isImmediatePropagationStopped(): boolean; 1258 | isDefaultPrevented(): boolean; 1259 | } 1260 | interface EventHandlerFunction extends Function { 1261 | (event ? : Meteor.Event, templateInstance ? : Blaze.TemplateInstance): void; 1262 | } 1263 | interface EventMap { 1264 | [id: string]: Meteor.EventHandlerFunction; 1265 | } 1266 | /** Event **/ 1267 | 1268 | /** Connection **/ 1269 | function reconnect(): void; 1270 | 1271 | function disconnect(): void; 1272 | /** Connection **/ 1273 | 1274 | /** Status **/ 1275 | function status(): DDP.DDPStatus; 1276 | /** Status **/ 1277 | 1278 | /** Pub/Sub **/ 1279 | function subscribe(name: string, ...args: any[]): Meteor.SubscriptionHandle; 1280 | /** Pub/Sub **/ 1281 | } 1282 | } 1283 | declare module Meteor { 1284 | /** Connection **/ 1285 | interface Connection { 1286 | id: string; 1287 | close: Function; 1288 | onClose: Function; 1289 | clientAddress: string; 1290 | httpHeaders: Object; 1291 | } 1292 | 1293 | function onConnection(callback: Function): void; 1294 | /** Connection **/ 1295 | 1296 | function publish(name: string, func: Function): void; 1297 | } 1298 | 1299 | interface Subscription { 1300 | added(collection: string, id: string, fields: Object): void; 1301 | changed(collection: string, id: string, fields: Object): void; 1302 | connection: Meteor.Connection; 1303 | error(error: Error): void; 1304 | onStop(func: Function): void; 1305 | ready(): void; 1306 | removed(collection: string, id: string): void; 1307 | stop(): void; 1308 | userId: string; 1309 | } 1310 | 1311 | declare module "meteor/meteor" { 1312 | module Meteor { 1313 | /** Connection **/ 1314 | interface Connection { 1315 | id: string; 1316 | close: Function; 1317 | onClose: Function; 1318 | clientAddress: string; 1319 | httpHeaders: Object; 1320 | } 1321 | 1322 | function onConnection(callback: Function): void; 1323 | /** Connection **/ 1324 | 1325 | function publish(name: string, func: Function): void; 1326 | } 1327 | 1328 | interface Subscription { 1329 | added(collection: string, id: string, fields: Object): void; 1330 | changed(collection: string, id: string, fields: Object): void; 1331 | connection: Meteor.Connection; 1332 | error(error: Error): void; 1333 | onStop(func: Function): void; 1334 | ready(): void; 1335 | removed(collection: string, id: string): void; 1336 | stop(): void; 1337 | userId: string; 1338 | } 1339 | } 1340 | declare module Mongo { 1341 | interface Selector { 1342 | [key: string]: any; 1343 | } 1344 | interface Selector extends Object {} 1345 | interface Modifier {} 1346 | interface SortSpecifier {} 1347 | interface FieldSpecifier { 1348 | [id: string]: Number; 1349 | } 1350 | 1351 | var Collection: CollectionStatic; 1352 | interface CollectionStatic { 1353 | new < T > (name: string, options ? : { 1354 | connection ? : Object; 1355 | idGeneration ? : string; 1356 | transform ? : Function; 1357 | }): Collection < T > ; 1358 | } 1359 | interface Collection < T > { 1360 | allow(options: { 1361 | insert ? : (userId: string, doc: T) => boolean; 1362 | update ? : (userId: string, doc: T, fieldNames: string[], modifier: any) => boolean; 1363 | remove ? : (userId: string, doc: T) => boolean; 1364 | fetch ? : string[]; 1365 | transform ? : Function; 1366 | }): boolean; 1367 | deny(options: { 1368 | insert ? : (userId: string, doc: T) => boolean; 1369 | update ? : (userId: string, doc: T, fieldNames: string[], modifier: any) => boolean; 1370 | remove ? : (userId: string, doc: T) => boolean; 1371 | fetch ? : string[]; 1372 | transform ? : Function; 1373 | }): boolean; 1374 | find(selector ? : Selector | ObjectID | string, options ? : { 1375 | sort ? : SortSpecifier; 1376 | skip ? : number; 1377 | limit ? : number; 1378 | fields ? : FieldSpecifier; 1379 | reactive ? : boolean; 1380 | transform ? : Function; 1381 | }): Cursor < T > ; 1382 | findOne(selector ? : Selector | ObjectID | string, options ? : { 1383 | sort ? : SortSpecifier; 1384 | skip ? : number; 1385 | fields ? : FieldSpecifier; 1386 | reactive ? : boolean; 1387 | transform ? : Function; 1388 | }): T; 1389 | insert(doc: T, callback ? : Function): string; 1390 | rawCollection(): any; 1391 | rawDatabase(): any; 1392 | remove(selector: Selector | ObjectID | string, callback ? : Function): number; 1393 | update(selector: Selector | ObjectID | string, modifier: Modifier, options ? : { 1394 | multi ? : boolean; 1395 | upsert ? : boolean; 1396 | }, callback ? : Function): number; 1397 | upsert(selector: Selector | ObjectID | string, modifier: Modifier, options ? : { 1398 | multi ? : boolean; 1399 | }, callback ? : Function): { 1400 | numberAffected ? : number;insertedId ? : string; 1401 | }; 1402 | _ensureIndex(indexName: string, options ? : { 1403 | [key: string]: any 1404 | }): void; 1405 | } 1406 | 1407 | var Cursor: CursorStatic; 1408 | interface CursorStatic { 1409 | new < T > (): Cursor < T > ; 1410 | } 1411 | interface ObserveCallbacks { 1412 | added ? (document: Object) : void; 1413 | addedAt ? (document: Object, atIndex: number, before: Object) : void; 1414 | changed ? (newDocument: Object, oldDocument: Object) : void; 1415 | changedAt ? (newDocument: Object, oldDocument: Object, indexAt: number) : void; 1416 | removed ? (oldDocument: Object) : void; 1417 | removedAt ? (oldDocument: Object, atIndex: number) : void; 1418 | movedTo ? (document: Object, fromIndex: number, toIndex: number, before: Object) : void; 1419 | } 1420 | interface ObserveChangesCallbacks { 1421 | added ? (id: string, fields: Object) : void; 1422 | addedBefore ? (id: string, fields: Object, before: Object) : void; 1423 | changed ? (id: string, fields: Object) : void; 1424 | movedBefore ? (id: string, before: Object) : void; 1425 | removed ? (id: string) : void; 1426 | } 1427 | interface Cursor < T > { 1428 | count(): number; 1429 | fetch(): Array < T > ; 1430 | forEach(callback: < T > (doc: T, index: number, cursor: Cursor < T > ) => void, thisArg ? : any): void; 1431 | map < U > (callback: (doc: T, index: number, cursor: Cursor < T > ) => U, thisArg ? : any): Array < U > ; 1432 | observe(callbacks: ObserveCallbacks): Meteor.LiveQueryHandle; 1433 | observeChanges(callbacks: ObserveChangesCallbacks): Meteor.LiveQueryHandle; 1434 | } 1435 | 1436 | var ObjectID: ObjectIDStatic; 1437 | interface ObjectIDStatic { 1438 | new(hexString ? : string): ObjectID; 1439 | } 1440 | interface ObjectID {} 1441 | } 1442 | 1443 | declare module "meteor/mongo" { 1444 | module Mongo { 1445 | interface Selector { 1446 | [key: string]: any; 1447 | } 1448 | interface Selector extends Object {} 1449 | interface Modifier {} 1450 | interface SortSpecifier {} 1451 | interface FieldSpecifier { 1452 | [id: string]: Number; 1453 | } 1454 | 1455 | var Collection: CollectionStatic; 1456 | interface CollectionStatic { 1457 | new < T > (name: string, options ? : { 1458 | connection ? : Object; 1459 | idGeneration ? : string; 1460 | transform ? : Function; 1461 | }): Collection < T > ; 1462 | } 1463 | interface Collection < T > { 1464 | allow(options: { 1465 | insert ? : (userId: string, doc: T) => boolean; 1466 | update ? : (userId: string, doc: T, fieldNames: string[], modifier: any) => boolean; 1467 | remove ? : (userId: string, doc: T) => boolean; 1468 | fetch ? : string[]; 1469 | transform ? : Function; 1470 | }): boolean; 1471 | deny(options: { 1472 | insert ? : (userId: string, doc: T) => boolean; 1473 | update ? : (userId: string, doc: T, fieldNames: string[], modifier: any) => boolean; 1474 | remove ? : (userId: string, doc: T) => boolean; 1475 | fetch ? : string[]; 1476 | transform ? : Function; 1477 | }): boolean; 1478 | find(selector ? : Selector | ObjectID | string, options ? : { 1479 | sort ? : SortSpecifier; 1480 | skip ? : number; 1481 | limit ? : number; 1482 | fields ? : FieldSpecifier; 1483 | reactive ? : boolean; 1484 | transform ? : Function; 1485 | }): Cursor < T > ; 1486 | findOne(selector ? : Selector | ObjectID | string, options ? : { 1487 | sort ? : SortSpecifier; 1488 | skip ? : number; 1489 | fields ? : FieldSpecifier; 1490 | reactive ? : boolean; 1491 | transform ? : Function; 1492 | }): T; 1493 | insert(doc: T, callback ? : Function): string; 1494 | rawCollection(): any; 1495 | rawDatabase(): any; 1496 | remove(selector: Selector | ObjectID | string, callback ? : Function): number; 1497 | update(selector: Selector | ObjectID | string, modifier: Modifier, options ? : { 1498 | multi ? : boolean; 1499 | upsert ? : boolean; 1500 | }, callback ? : Function): number; 1501 | upsert(selector: Selector | ObjectID | string, modifier: Modifier, options ? : { 1502 | multi ? : boolean; 1503 | }, callback ? : Function): { 1504 | numberAffected ? : number;insertedId ? : string; 1505 | }; 1506 | _ensureIndex(indexName: string, options ? : { 1507 | [key: string]: any 1508 | }): void; 1509 | } 1510 | 1511 | var Cursor: CursorStatic; 1512 | interface CursorStatic { 1513 | new < T > (): Cursor < T > ; 1514 | } 1515 | interface ObserveCallbacks { 1516 | added ? (document: Object) : void; 1517 | addedAt ? (document: Object, atIndex: number, before: Object) : void; 1518 | changed ? (newDocument: Object, oldDocument: Object) : void; 1519 | changedAt ? (newDocument: Object, oldDocument: Object, indexAt: number) : void; 1520 | removed ? (oldDocument: Object) : void; 1521 | removedAt ? (oldDocument: Object, atIndex: number) : void; 1522 | movedTo ? (document: Object, fromIndex: number, toIndex: number, before: Object) : void; 1523 | } 1524 | interface ObserveChangesCallbacks { 1525 | added ? (id: string, fields: Object) : void; 1526 | addedBefore ? (id: string, fields: Object, before: Object) : void; 1527 | changed ? (id: string, fields: Object) : void; 1528 | movedBefore ? (id: string, before: Object) : void; 1529 | removed ? (id: string) : void; 1530 | } 1531 | interface Cursor < T > { 1532 | count(): number; 1533 | fetch(): Array < T > ; 1534 | forEach(callback: < T > (doc: T, index: number, cursor: Cursor < T > ) => void, thisArg ? : any): void; 1535 | map < U > (callback: (doc: T, index: number, cursor: Cursor < T > ) => U, thisArg ? : any): Array < U > ; 1536 | observe(callbacks: ObserveCallbacks): Meteor.LiveQueryHandle; 1537 | observeChanges(callbacks: ObserveChangesCallbacks): Meteor.LiveQueryHandle; 1538 | } 1539 | 1540 | var ObjectID: ObjectIDStatic; 1541 | interface ObjectIDStatic { 1542 | new(hexString ? : string): ObjectID; 1543 | } 1544 | interface ObjectID {} 1545 | } 1546 | } 1547 | declare module Mongo { 1548 | interface AllowDenyOptions { 1549 | insert ? : (userId: string, doc: any) => boolean; 1550 | update ? : (userId: string, doc: any, fieldNames: string[], modifier: any) => boolean; 1551 | remove ? : (userId: string, doc: any) => boolean; 1552 | fetch ? : string[]; 1553 | transform ? : Function; 1554 | } 1555 | } 1556 | 1557 | declare module "meteor/mongo" { 1558 | module Mongo { 1559 | interface AllowDenyOptions { 1560 | insert ? : (userId: string, doc: any) => boolean; 1561 | update ? : (userId: string, doc: any, fieldNames: string[], modifier: any) => boolean; 1562 | remove ? : (userId: string, doc: any) => boolean; 1563 | fetch ? : string[]; 1564 | transform ? : Function; 1565 | } 1566 | } 1567 | } 1568 | declare module Random { 1569 | function id(numberOfChars ? : number): string; 1570 | 1571 | function secret(numberOfChars ? : number): string; 1572 | 1573 | function fraction(): number; 1574 | // @param numberOfDigits, @returns a random hex string of the given length 1575 | function hexString(numberOfDigits: number): string; 1576 | // @param array, @return a random element in array 1577 | function choice(array: any[]): string; 1578 | // @param str, @return a random char in str 1579 | function choice(str: string): string; 1580 | } 1581 | 1582 | declare module "meteor/random" { 1583 | module Random { 1584 | function id(numberOfChars ? : number): string; 1585 | 1586 | function secret(numberOfChars ? : number): string; 1587 | 1588 | function fraction(): number; 1589 | // @param numberOfDigits, @returns a random hex string of the given length 1590 | function hexString(numberOfDigits: number): string; 1591 | // @param array, @return a random element in array 1592 | function choice(array: any[]): string; 1593 | // @param str, @return a random char in str 1594 | function choice(str: string): string; 1595 | } 1596 | } 1597 | declare var ReactiveVar: ReactiveVarStatic; 1598 | interface ReactiveVarStatic { 1599 | new < T > (initialValue: T, equalsFunc ? : Function): ReactiveVar < T > ; 1600 | } 1601 | interface ReactiveVar < T > { 1602 | get(): T; 1603 | set(newValue: T): void; 1604 | } 1605 | 1606 | declare module "meteor/reactive-var" { 1607 | var ReactiveVar: ReactiveVarStatic; 1608 | interface ReactiveVarStatic { 1609 | new < T > (initialValue: T, equalsFunc ? : Function): ReactiveVar < T > ; 1610 | } 1611 | interface ReactiveVar < T > { 1612 | get(): T; 1613 | set(newValue: T): void; 1614 | } 1615 | } 1616 | 1617 | declare module Session { 1618 | function equals(key: string, value: string | number | boolean | any): boolean; 1619 | 1620 | function get(key: string): any; 1621 | 1622 | function set(key: string, value: EJSONable | any): void; 1623 | 1624 | function setDefault(key: string, value: EJSONable | any): void; 1625 | } 1626 | 1627 | declare module "meteor/session" { 1628 | /// 1629 | 1630 | module Session { 1631 | function equals(key: string, value: string | number | boolean | any): boolean; 1632 | 1633 | function get(key: string): any; 1634 | 1635 | function set(key: string, value: EJSONable | any): void; 1636 | 1637 | function setDefault(key: string, value: EJSONable | any): void; 1638 | } 1639 | } 1640 | 1641 | declare var Template: TemplateStatic; 1642 | interface TemplateStatic extends Blaze.TemplateStatic { 1643 | new(viewName ? : string, renderFunction ? : Function): Blaze.Template; 1644 | body: Blaze.Template; 1645 | [index: string]: any | Blaze.Template; 1646 | } 1647 | 1648 | declare module "meteor/templating" { 1649 | /// 1650 | 1651 | var Template: TemplateStatic; 1652 | interface TemplateStatic extends Blaze.TemplateStatic { 1653 | new(viewName ? : string, renderFunction ? : Function): Blaze.Template; 1654 | body: Blaze.Template; 1655 | [index: string]: any | Blaze.Template; 1656 | } 1657 | } 1658 | interface ILengthAble { 1659 | length: number; 1660 | } 1661 | 1662 | interface ITinytestAssertions { 1663 | ok(doc: Object): void; 1664 | expect_fail(): void; 1665 | fail(doc: Object): void; 1666 | runId(): string; 1667 | equal < T > (actual: T, expected: T, message ? : string, not ? : boolean): void; 1668 | notEqual < T > (actual: T, expected: T, message ? : string): void; 1669 | instanceOf(obj: Object, klass: Function, message ? : string): void; 1670 | notInstanceOf(obj: Object, klass: Function, message ? : string): void; 1671 | matches(actual: any, regexp: RegExp, message ? : string): void; 1672 | notMatches(actual: any, regexp: RegExp, message ? : string): void; 1673 | throws(f: Function, expected ? : string | RegExp): void; 1674 | isTrue(v: boolean, msg ? : string): void; 1675 | isFalse(v: boolean, msg ? : string): void; 1676 | isNull(v: any, msg ? : string): void; 1677 | isNotNull(v: any, msg ? : string): void; 1678 | isUndefined(v: any, msg ? : string): void; 1679 | isNotUndefined(v: any, msg ? : string): void; 1680 | isNan(v: any, msg ? : string): void; 1681 | isNotNan(v: any, msg ? : string): void; 1682 | include < T > (s: Array < T > | Object | string, value: any, msg ? : string, not ? : boolean): void; 1683 | 1684 | notInclude < T > (s: Array < T > | Object | string, value: any, msg ? : string, not ? : boolean): void; 1685 | length(obj: ILengthAble, expected_length: number, msg ? : string): void; 1686 | _stringEqual(actual: string, expected: string, msg ? : string): void; 1687 | } 1688 | 1689 | declare module Tinytest { 1690 | function add(description: string, func: (test: ITinytestAssertions) => void): void; 1691 | 1692 | function addAsync(description: string, func: (test: ITinytestAssertions) => void): void; 1693 | } 1694 | 1695 | declare module "meteor/tiny-test" { 1696 | interface ILengthAble { 1697 | length: number; 1698 | } 1699 | 1700 | interface ITinytestAssertions { 1701 | ok(doc: Object): void; 1702 | expect_fail(): void; 1703 | fail(doc: Object): void; 1704 | runId(): string; 1705 | equal < T > (actual: T, expected: T, message ? : string, not ? : boolean): void; 1706 | notEqual < T > (actual: T, expected: T, message ? : string): void; 1707 | instanceOf(obj: Object, klass: Function, message ? : string): void; 1708 | notInstanceOf(obj: Object, klass: Function, message ? : string): void; 1709 | matches(actual: any, regexp: RegExp, message ? : string): void; 1710 | notMatches(actual: any, regexp: RegExp, message ? : string): void; 1711 | throws(f: Function, expected ? : string | RegExp): void; 1712 | isTrue(v: boolean, msg ? : string): void; 1713 | isFalse(v: boolean, msg ? : string): void; 1714 | isNull(v: any, msg ? : string): void; 1715 | isNotNull(v: any, msg ? : string): void; 1716 | isUndefined(v: any, msg ? : string): void; 1717 | isNotUndefined(v: any, msg ? : string): void; 1718 | isNan(v: any, msg ? : string): void; 1719 | isNotNan(v: any, msg ? : string): void; 1720 | include < T > (s: Array < T > | Object | string, value: any, msg ? : string, not ? : boolean): void; 1721 | 1722 | notInclude < T > (s: Array < T > | Object | string, value: any, msg ? : string, not ? : boolean): void; 1723 | length(obj: ILengthAble, expected_length: number, msg ? : string): void; 1724 | _stringEqual(actual: string, expected: string, msg ? : string): void; 1725 | } 1726 | 1727 | module Tinytest { 1728 | function add(description: string, func: (test: ITinytestAssertions) => void): void; 1729 | 1730 | function addAsync(description: string, func: (test: ITinytestAssertions) => void): void; 1731 | } 1732 | } 1733 | declare module App { 1734 | function accessRule(pattern: string, options ? : { 1735 | type ? : string; 1736 | launchExternal ? : boolean; 1737 | }): void; 1738 | 1739 | function configurePlugin(id: string, config: Object): void; 1740 | 1741 | function icons(icons: Object): void; 1742 | 1743 | function info(options: { 1744 | id ? : string; 1745 | version ? : string; 1746 | name ? : string; 1747 | description ? : string; 1748 | author ? : string; 1749 | email ? : string; 1750 | website ? : string; 1751 | }): void; 1752 | 1753 | function launchScreens(launchScreens: Object): void; 1754 | 1755 | function setPreference(name: string, value: string, platform ? : string): void; 1756 | } 1757 | 1758 | declare function execFileAsync(command: string, args ? : any[], options ? : { 1759 | cwd ? : Object; 1760 | env ? : Object; 1761 | stdio ? : any[] | string; 1762 | destination ? : any; 1763 | waitForClose ? : string; 1764 | }): any; 1765 | declare function execFileSync(command: string, args ? : any[], options ? : { 1766 | cwd ? : Object; 1767 | env ? : Object; 1768 | stdio ? : any[] | string; 1769 | destination ? : any; 1770 | waitForClose ? : string; 1771 | }): String; 1772 | 1773 | declare module Assets { 1774 | function getBinary(assetPath: string, asyncCallback ? : Function): EJSON; 1775 | 1776 | function getText(assetPath: string, asyncCallback ? : Function): string; 1777 | } 1778 | 1779 | declare module Cordova { 1780 | function depends(dependencies: { 1781 | [id: string]: string 1782 | }): void; 1783 | } 1784 | 1785 | declare module Npm { 1786 | function depends(dependencies: { 1787 | [id: string]: string 1788 | }): void; 1789 | 1790 | function require(name: string): any; 1791 | } 1792 | 1793 | declare namespace Package { 1794 | function describe(options: { 1795 | summary ? : string; 1796 | version ? : string; 1797 | name ? : string; 1798 | git ? : string; 1799 | documentation ? : string; 1800 | debugOnly ? : boolean; 1801 | prodOnly ? : boolean; 1802 | testOnly ? : boolean; 1803 | }): void; 1804 | 1805 | function onTest(func: (api: PackageAPI) => void): void; 1806 | 1807 | function onUse(func: (api: PackageAPI) => void): void; 1808 | 1809 | function registerBuildPlugin(options ? : { 1810 | name ? : string; 1811 | use ? : string | string[]; 1812 | sources ? : string[]; 1813 | npmDependencies ? : Object; 1814 | }): void; 1815 | } 1816 | 1817 | interface PackageAPI { 1818 | new(): PackageAPI; 1819 | addAssets(filenames: string | string[], architecture: string | string[]): void; 1820 | addFiles(filenames: string | string[], architecture ? : string | string[], options ? : { 1821 | bare ? : boolean; 1822 | }): void; 1823 | export (exportedObjects: string | string[], architecture ? : string | string[], exportOptions ? : Object, testOnly ? : boolean): void; 1824 | imply(packageNames: string | string[], architecture ? : string | string[]): void; 1825 | use(packageNames: string | string[], architecture ? : string | string[], options ? : { 1826 | weak ? : boolean; 1827 | unordered ? : boolean; 1828 | }): void; 1829 | versionsFrom(meteorRelease: string | string[]): void; 1830 | } 1831 | 1832 | declare var console: Console; 1833 | 1834 | declare module "meteor/tools" { 1835 | module App { 1836 | function accessRule(pattern: string, options ? : { 1837 | type ? : string; 1838 | launchExternal ? : boolean; 1839 | }): void; 1840 | 1841 | function configurePlugin(id: string, config: Object): void; 1842 | 1843 | function icons(icons: Object): void; 1844 | 1845 | function info(options: { 1846 | id ? : string; 1847 | version ? : string; 1848 | name ? : string; 1849 | description ? : string; 1850 | author ? : string; 1851 | email ? : string; 1852 | website ? : string; 1853 | }): void; 1854 | 1855 | function launchScreens(launchScreens: Object): void; 1856 | 1857 | function setPreference(name: string, value: string, platform ? : string): void; 1858 | } 1859 | 1860 | function execFileAsync(command: string, args ? : any[], options ? : { 1861 | cwd ? : Object; 1862 | env ? : Object; 1863 | stdio ? : any[] | string; 1864 | destination ? : any; 1865 | waitForClose ? : string; 1866 | }): any; 1867 | 1868 | function execFileSync(command: string, args ? : any[], options ? : { 1869 | cwd ? : Object; 1870 | env ? : Object; 1871 | stdio ? : any[] | string; 1872 | destination ? : any; 1873 | waitForClose ? : string; 1874 | }): String; 1875 | 1876 | module Assets { 1877 | function getBinary(assetPath: string, asyncCallback ? : Function): EJSON; 1878 | 1879 | function getText(assetPath: string, asyncCallback ? : Function): string; 1880 | } 1881 | 1882 | module Cordova { 1883 | function depends(dependencies: { 1884 | [id: string]: string 1885 | }): void; 1886 | } 1887 | 1888 | module Npm { 1889 | function depends(dependencies: { 1890 | [id: string]: string 1891 | }): void; 1892 | 1893 | function require(name: string): any; 1894 | } 1895 | 1896 | namespace Package { 1897 | function describe(options: { 1898 | summary ? : string; 1899 | version ? : string; 1900 | name ? : string; 1901 | git ? : string; 1902 | documentation ? : string; 1903 | debugOnly ? : boolean; 1904 | prodOnly ? : boolean; 1905 | testOnly ? : boolean; 1906 | }): void; 1907 | 1908 | function onTest(func: (api: PackageAPI) => void): void; 1909 | 1910 | function onUse(func: (api: PackageAPI) => void): void; 1911 | 1912 | function registerBuildPlugin(options ? : { 1913 | name ? : string; 1914 | use ? : string | string[]; 1915 | sources ? : string[]; 1916 | npmDependencies ? : Object; 1917 | }): void; 1918 | } 1919 | 1920 | interface PackageAPI { 1921 | new(): PackageAPI; 1922 | addAssets(filenames: string | string[], architecture: string | string[]): void; 1923 | addFiles(filenames: string | string[], architecture ? : string | string[], options ? : { 1924 | bare ? : boolean; 1925 | }): void; 1926 | export (exportedObjects: string | string[], architecture ? : string | string[], exportOptions ? : Object, testOnly ? : boolean): void; 1927 | imply(packageNames: string | string[], architecture ? : string | string[]): void; 1928 | use(packageNames: string | string[], architecture ? : string | string[], options ? : { 1929 | weak ? : boolean; 1930 | unordered ? : boolean; 1931 | }): void; 1932 | versionsFrom(meteorRelease: string | string[]): void; 1933 | } 1934 | 1935 | var console: Console; 1936 | } 1937 | declare module Tracker { 1938 | function Computation(): void; 1939 | interface Computation { 1940 | firstRun: boolean; 1941 | invalidate(): void; 1942 | invalidated: boolean; 1943 | onInvalidate(callback: Function): void; 1944 | onStop(callback: Function): void; 1945 | stop(): void; 1946 | stopped: boolean; 1947 | } 1948 | var currentComputation: Computation; 1949 | 1950 | var Dependency: DependencyStatic; 1951 | interface DependencyStatic { 1952 | new(): Dependency; 1953 | } 1954 | interface Dependency { 1955 | changed(): void; 1956 | depend(fromComputation ? : Computation): boolean; 1957 | hasDependents(): boolean; 1958 | } 1959 | 1960 | var active: boolean; 1961 | 1962 | function afterFlush(callback: Function): void; 1963 | 1964 | function autorun(runFunc: (computation: Computation) => void, options ? : { 1965 | onError ? : Function; 1966 | }): Computation; 1967 | 1968 | function flush(): void; 1969 | 1970 | function nonreactive(func: Function): void; 1971 | 1972 | function onInvalidate(callback: Function): void; 1973 | } 1974 | 1975 | declare module "meteor/tracker" { 1976 | module Tracker { 1977 | function Computation(): void; 1978 | interface Computation { 1979 | firstRun: boolean; 1980 | invalidate(): void; 1981 | invalidated: boolean; 1982 | onInvalidate(callback: Function): void; 1983 | onStop(callback: Function): void; 1984 | stop(): void; 1985 | stopped: boolean; 1986 | } 1987 | var currentComputation: Computation; 1988 | 1989 | var Dependency: DependencyStatic; 1990 | interface DependencyStatic { 1991 | new(): Dependency; 1992 | } 1993 | interface Dependency { 1994 | changed(): void; 1995 | depend(fromComputation ? : Computation): boolean; 1996 | hasDependents(): boolean; 1997 | } 1998 | 1999 | var active: boolean; 2000 | 2001 | function afterFlush(callback: Function): void; 2002 | 2003 | function autorun(runFunc: (computation: Computation) => void, options ? : { 2004 | onError ? : Function; 2005 | }): Computation; 2006 | 2007 | function flush(): void; 2008 | 2009 | function nonreactive(func: Function): void; 2010 | 2011 | function onInvalidate(callback: Function): void; 2012 | } 2013 | } -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # Publishing a new version 2 | 3 | - [ ] Pick an appropriate incremented version number v0.x.y (e.g. 0.4.0 becomes 0.4.1 or 0.5.0 depending on how much changed) 4 | - [ ] Update the `History.md` file to reflect all changes that will be in the new version. If you've been keeping it up to date (and it appears you have), this just involves inserting the above version number below vNEXT. 5 | - [ ] Update this version number in `package.js`. 6 | - [ ] git commit (but don't push yet). 7 | - [ ] Try `npm run publish` 8 | - [ ] If everything worked, `git tag v0.x.y` and push the tag (`git push origin v0.x.y`; this allows others to find the code for this version) and merge into master and push that too. (If you aren't rebasing the feature branch you may want to merge first before publishing) 9 | - [ ] If publishing didn't work, you can fix things, amend the commit as necessary, then tag and push after verifying that it went through. 10 | 11 | In order to `meteor publish`, you will need to be added as a maintainer to this package. You can see a list of the current maintainers with: 12 | 13 | ``` 14 | meteor admin maintainers mizzao:user-status --list 15 | ``` 16 | 17 | # Travis CI 18 | 19 | To be written. 20 | 21 | See https://travis-ci.org/Meteor-Community-Packages/meteor-user-status 22 | 23 | # Pushing a demo 24 | 25 | To be written. 26 | 27 | We host the demo at https://user-status.meteorapp.com. 28 | 29 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | ## vNEXT 2 | 3 | ## v1.1.1 4 | * Add newer Meteor versions to `versionsFrom` 5 | 6 | ## v1.1.0 7 | 8 | * Bumped dependency versions 9 | * Updated tests 10 | * `Meteor.settings?.packages?.['mizzao:user-status']?.startupQuerySelector` option added to allow for custom startup selector 11 | 12 | ## v1.0.1 13 | 14 | * Bumped dependency versions 15 | 16 | ## v1.0.0 17 | 18 | * Decaffeinated! 19 | * BREAKING: User status now needs to be properly imported: `import { UserStatus } from 'meteor/mizzao:user-status'` 20 | * Added ESLint and jsbeautify 21 | * Added husky for pre-commit lint checking 22 | * Don't try to stop monitor in testing if it's not started 23 | * Upgrade demo to Meteor 1.8.1 24 | * Upgraded timesync to 0.5.1 25 | * Formatted all JS, CSS, and HTML files to jsbeautify specs 26 | * Updated all references of github.com/mizzao to github.com/Meteor-Community-Packages 27 | * Changed TravisCI to use Node 8.15.1 and added lint checks 28 | * Added Visual Studio Code settings.json 29 | * Changed all tabs to 2 spaces 30 | * Minimum meteor requirement now 1.7.0.5 31 | 32 | ## v0.6.8 33 | 34 | * Updated Coffeescript dependency to allow both v1 and v2. 35 | 36 | ## v0.6.7 37 | 38 | * Remove jQuery dependency 39 | 40 | ## v0.6.6 41 | 42 | * Declare dependent packages explicitly for Meteor 1.2. 43 | 44 | ## v0.6.5 45 | 46 | * Detect pause/resume events in Cordova. (#47, #64) 47 | 48 | ## v0.6.4 49 | 50 | * Improve consistency of the `status.online` and `status.idle` fields. (#31) 51 | 52 | ## v0.6.3 53 | 54 | * Update usage of the Mongo Collection API for Meteor 0.9.1+. 55 | * Add compatibility for the `audit-argument-checks` package. 56 | 57 | ## v0.6.2 58 | 59 | * Fix constraint syntax for the released Meteor 0.9.0. 60 | 61 | ## v0.6.1 62 | 63 | * **Updated for Meteor 0.9.** 64 | 65 | ## v0.6.0 66 | 67 | * Connections now record user agents - useful for diagnostic purposes. See the demo at http://user-status.meteor.com/. 68 | * The `lastLogin` field of documents in `Meteor.users` is **no longer a date**; it is an object with fields `date`, `ipAddr`, and `userAgent`. **Use `lastLogin.date` instead of simply `lastlogin` if you were depending on this behavior.** This provides a quick way to display connected users' IPs and UAs for administration or diagnostic purposes. 69 | * Better handling of idle/active events if TimeSync loses its computed offset temporarily due to a clock change. 70 | 71 | ## v0.5.0 72 | 73 | * All connections are tracked, including anonymous (not authenticated) ones. Idle monitoring is supported on anonymous connections, and idle state will persist across a login/logout. (#22) 74 | * The `Meteor.onConnection`, `connection.onClose`, and `Accounts.onLogin` functions are used to handle most changes in user state, except for logging out which is not directly supported in Meteor. This takes advantage of Meteor's new DDP heartbeats, and should improve issues with dangling connections caused by unclosed SockJS sockets. (#26) 75 | * Ensure that the last activity timestamp is reset when restarting the idle monitor after stopping it. (#27) 76 | * Add several unit tests for client-side idle monitoring logic. 77 | 78 | ## v0.4.1 79 | 80 | * Ensure that the latest activity timestamp updates when focusing into the app's window. 81 | 82 | ## v0.4.0 83 | 84 | * Store all dates as native `Date` objects in the database (#20). Most usage will be unaffected due to many libraries supporting the interchangeable use of `Date` objects or integer timestamps, but **some behavior may change when using operations with automatic type coercion, such as addition**. 85 | 86 | ## v0.3.5 87 | 88 | * Add some shim code for better compatibility with fast-render. (#24) 89 | 90 | ## v0.3.4 91 | 92 | * Fix an issue with properly recording the user's latest idle time across a reconnection. 93 | * Ignore actions generated while the window is blurred with `idleOnBlur` enabled. 94 | 95 | ## v0.3.3 96 | 97 | * Refactored server-side code so that it was more testable, and added multiplexing tests. 98 | * Fixed an issue where idle state would not be maintained if a connection was interrupted. 99 | 100 | ## v0.3.2 101 | 102 | * Fixed an issue where stopping the idle monitor could leave the client in an idle state. 103 | 104 | ## v0.3.1 105 | 106 | * Added multiplexing of idle status to `Meteor.users` as requested by @timhaines. 107 | 108 | ## v0.3.0 109 | 110 | * Added opt-in automatic **idle monitoring** on the client, which is reported to the server. See the demo app. 111 | * Export a single `UserStatus` variable on the server and the client, that contains all operations. **Breaks compatibility with previous usage:** the previous `UserStatus` variable is now `UserStatus.events`. 112 | * The `sessionLogin` and `sessionLogout` events have been renamed `connectionLogin` and `connectionLogout` along with the new `connectionIdle` and `connectionActive` events. **Breaks compatibility with previous usage.** 113 | * In callbacks, `sessionId` has also been renamed `connectionId` as per the Meteor change. 114 | 115 | ## v0.2.0 116 | 117 | * Exported `UserStatus` and `UserSessions` using the new API. 118 | * Moved the user status information into the `status` field instead of the `profile` field, which is user editable by default. **NOTE: this introduces a breaking change.** (#8) 119 | * Introduced a last login time for each session, and a combined time for the status field. (#8) 120 | * Added some basic tests. 121 | 122 | ## v0.1.7 123 | 124 | * Fixed a nuanced bug with the use of `upsert`. 125 | 126 | ## v0.1.6 127 | 128 | * Changed `find`/`insert`/`update` to a single upsert instead (#3, #6). 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andrew Mao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | user-status 2 | =========== 3 | 4 | ## What's this do? 5 | 6 | Keeps track of user connection data, such as IP addresses, user agents, and 7 | client-side activity, and tracks this information in `Meteor.users` as well as 8 | some other objects. This allows you to easily see users that are online, for 9 | applications such as rendering the users box below showing online users in green 10 | and idle users in orange. 11 | 12 | ![User online states](https://raw.github.com/Meteor-Community-Packages/meteor-user-status/master/docs/example.png) 13 | 14 | For a complete example of what can be tracked, including inactivity, IP 15 | addresses, and user agents, check out a demo app at 16 | http://user-status.meteor.com, or its 17 | [source](https://github.com/Meteor-Community-Packages/meteor-user-status/tree/master/demo). 18 | 19 | ## Install 20 | 21 | Install using Meteor: 22 | 23 | ```sh 24 | $ meteor add mizzao:user-status 25 | ``` 26 | 27 | Additionally, note that to read client IP addresses properly, you must set the 28 | `HTTP_FORWARDED_COUNT` environment variable for your app, and make sure that IP 29 | address headers are forwarded for any reverse proxy installed in front of the 30 | app. See the [Meteor docs on this](http://docs.meteor.com/#meteor_onconnection) 31 | for more details. 32 | 33 | ## Basic Usage - Online State 34 | 35 | This package maintains two types of status: a general user online flag in `Meteor.users`, and some additional data for each session. It uses [timesync](https://github.com/Meteor-Community-Packages/meteor-timesync) to maintain the server's time across all clients, regardless of whether they have the correct time. 36 | 37 | `Meteor.users` receives a `status` field will be updated automatically if the user logs in or logs out, closes their browser, or otherwise disconnects. A user is online if at least one connection with that `userId` is logged in. It contains the following fields: 38 | 39 | - `online`: `true` if there is at least one connection online for this user 40 | - `lastLogin`: information about the most recent login of the user, with the fields `date`, `ipAddr`, and `userAgent`. 41 | - `idle`: `true` if all connections for this user are idle. Requires idle tracking to be turned on for all connections, as below. 42 | - `lastActivity`: if the user was idle, the last time an action was observed. This field is only available when the user is online and idle. It does not maintain the user's last activity in real time or a stored value indefinitely - `lastLogin` is a coarse approximation to that. For more information, see https://github.com/Meteor-Community-Packages/meteor-user-status/issues/80. 43 | 44 | To make this available on the client, use a reactive cursor, such as by creating a publication on the server: 45 | 46 | ```javascript 47 | Meteor.publish("userStatus", function() { 48 | return Meteor.users.find({ "status.online": true }, { fields: { ... } }); 49 | }); 50 | ``` 51 | 52 | or you can use this to do certain actions when users go online and offline. 53 | 54 | ```javascript 55 | Meteor.users.find({ "status.online": true }).observe({ 56 | added: function(id) { 57 | // id just came online 58 | }, 59 | removed: function(id) { 60 | // id just went offline 61 | } 62 | }); 63 | ``` 64 | 65 | You can use a reactive cursor to select online users in a client-side template helper: 66 | 67 | ```javascript 68 | Template.foo.usersOnline = function() { 69 | return Meteor.users.find({ "status.online": true }) 70 | }; 71 | ``` 72 | 73 | Making this directly available on the client allows for useful template renderings of user state. For example, with something like the following you get the picture above (using bootstrap classes). 74 | 75 | ``` 76 | 79 | ``` 80 | 81 | ```javascript 82 | Template.userPill.labelClass = function() { 83 | if (this.status.idle) 84 | return "label-warning" 85 | else if (this.status.online) 86 | return "label-success" 87 | else 88 | return "label-default" 89 | }; 90 | ``` 91 | 92 | ## Advanced Usage and Idle Tracking 93 | 94 | ### Client API 95 | 96 | On the client, the `UserStatus` object provides for seamless automatic monitoring of a client's idle state. By default, it will listen for all clicks and keypresses in `window` as signals of a user's action. It has the following functions: 97 | 98 | - `startMonitor`: a function taking an object with fields `threshold` (the amount of time before a user is counted as idle), `interval` (how often to check if a client has gone idle), and `idleOnBlur` (whether to count a window blur as a user going idle.) This function enables idle tracking on the client. 99 | - `stopMonitor`: stops the running monitor. 100 | - `pingMonitor`: if the automatic event handlers aren't catching what you need, you can manually ping the monitor to signal that a user is doing something and reset the idle monitor. 101 | - `isIdle`: a reactive variable signifying whether the user is currently idle or not. 102 | - `isMonitoring`: a reactive variable for whether the monitor is running. 103 | - `lastActivity`: a reactive variable for the last action recorded by the user (according to [server time](https://github.com/Meteor-Community-Packages/meteor-timesync)). Since this variable will be invalidated a lot and cause many recomputations, it's best only used for debugging or diagnostics (as in the demo). 104 | 105 | For an example of how the above functions are used, see the demo. 106 | 107 | ### Server API 108 | 109 | The `UserStatus.connections` (in-memory) collection contains information for all connections on the server, in the following fields: 110 | 111 | - `_id`: the connection id. 112 | - `userId`: the user id, if the connection is authenticated. 113 | - `ipAddr`: the remote address of the connection. A user logged in from different places will have one document per connection. 114 | - `userAgent`: the user agent of the connection. 115 | - `loginTime`: if authenticated, when the user logged in with this connection. 116 | - `idle`: `true` if idle monitoring is enabled on this connection and the client has gone idle. 117 | 118 | #### startupQuerySelector (Optional) 119 | 120 | On startup `meteor-user-status` automatically resets all users to `offline` and then marks each `online` as connections are reestablished. 121 | 122 | To customize this functionality you can use the `startupQuerySelector` [Meteor package option](https://docs.meteor.com/api/packagejs.html#options) like this: 123 | ```javascript 124 | "packages": { 125 | "mizzao:user-status": { 126 | "startupQuerySelector": { "$or": [{ "status.online": true }, { "status.idle": { "$exists": true } }, { "status.lastActivity": { "$exists": true } }] } 127 | } 128 | } 129 | ``` 130 | The above example reduces server/database load during startup if you have a large number of users. 131 | 132 | #### Usage with collection2 133 | 134 | If your project is using `aldeed:collection2` with a schema attached to `Meteor.users`, you need to add the following items to the schema to allow modifications of the status: 135 | 136 | ```javascript 137 | import SimpleSchema from 'simpl-schema'; 138 | 139 | const userSchema = new SimpleSchema({ 140 | status: { 141 | type: Object, 142 | optional: true, 143 | }, 144 | 'status.lastLogin': { 145 | type: Object, 146 | optional: true, 147 | }, 148 | 'status.lastLogin.date': { 149 | type: Date, 150 | optional: true, 151 | }, 152 | 'status.lastLogin.ipAddr': { 153 | type: String, 154 | optional: true, 155 | }, 156 | 'status.lastLogin.userAgent': { 157 | type: String, 158 | optional: true, 159 | }, 160 | 'status.idle': { 161 | type: Boolean, 162 | optional: true, 163 | }, 164 | 'status.lastActivity': { 165 | type: Date, 166 | optional: true, 167 | }, 168 | 'status.online': { 169 | type: Boolean, 170 | optional: true, 171 | }, 172 | }); 173 | 174 | // attaching the Schema to Meteor.users will extend it 175 | Meteor.users.attachSchema(userSchema); 176 | 177 | ``` 178 | 179 | #### Events 180 | 181 | The `UserStatus.events` object is an `EventEmitter` on which you can listen for connections logging in and out. Logging out includes closing the browser; reopening the browser will trigger a new login event. The following events are supported: 182 | 183 | #### `UserStatus.events.on("connectionLogin", function(fields) { ... })` 184 | 185 | `fields` contains `userId`, `connectionId`, `ipAddr`, `userAgent`, and `loginTime`. 186 | 187 | #### `UserStatus.events.on("connectionLogout", function(fields) { ... })` 188 | 189 | `fields` contains `userId`, `connectionId`, `lastActivity`, and `logoutTime`. 190 | 191 | #### `UserStatus.events.on("connectionIdle", function(fields) { ... })` 192 | 193 | `fields` contains `userId`, `connectionId`, and `lastActivity`. 194 | 195 | #### `UserStatus.events.on("connectionActive", function(fields) { ... })` 196 | 197 | `fields` contains `userId`, `connectionId`, and `lastActivity`. 198 | 199 | Check out https://github.com/mizzao/meteor-accounts-testing for a simple accounts drop-in that you can use to test your app - this is also used in the demo. 200 | 201 | #### Startup selector 202 | By default, the startup selector for resetting user status is `{}`. 203 | If you want to change that you can set the default selector in your settings.json file: 204 | 205 | ```json 206 | { 207 | "packages": { 208 | "mizzao:user-status": { 209 | "startupQuerySelector": { 210 | // your selector here, for example: 211 | "profile.name": "admin" 212 | } 213 | } 214 | } 215 | } 216 | ``` 217 | 218 | ## Testing 219 | 220 | There are some `Tinytest` unit tests that are used to test the logic in this package, but general testing with many users and connections is hard. Hence, we have set up a demo app (http://user-status.meteor.com) for testing that is also hosted as a proof of concept. If you think you've found a bug in the package, try to replicate it on the demo app and post an issue with steps to reproduce. 221 | 222 | ## Contributing 223 | 224 | See [Contributing.md](Contributing.md). 225 | -------------------------------------------------------------------------------- /client/monitor.js: -------------------------------------------------------------------------------- 1 | /* globals window, document */ 2 | 3 | import { Meteor } from 'meteor/meteor'; 4 | import { TimeSync } from 'meteor/mizzao:timesync'; 5 | import { Tracker } from 'meteor/tracker'; 6 | /* 7 | The idle monitor watches for mouse, keyboard, and blur events, 8 | and reports idle status to the server. 9 | 10 | It uses TimeSync to report accurate time. 11 | 12 | Everything is reactive, of course! 13 | */ 14 | 15 | // State variables 16 | let monitorId = null; 17 | let idle = false; 18 | let lastActivityTime = undefined; 19 | 20 | const monitorDep = new Tracker.Dependency; 21 | const idleDep = new Tracker.Dependency; 22 | const activityDep = new Tracker.Dependency; 23 | 24 | let focused = true; 25 | 26 | // These settings are internal or exported for test only 27 | export const MonitorInternals = { 28 | idleThreshold: null, 29 | idleOnBlur: false, 30 | 31 | computeState(lastActiveTime, currentTime, isWindowFocused) { 32 | const inactiveTime = currentTime - lastActiveTime; 33 | if (MonitorInternals.idleOnBlur && !isWindowFocused) { 34 | return true; 35 | } 36 | if (inactiveTime > MonitorInternals.idleThreshold) { 37 | return true; 38 | } else { 39 | return false; 40 | } 41 | }, 42 | 43 | connectionChange(isConnected, wasConnected) { 44 | // We only need to do something if we reconnect and we are idle 45 | // Don't get idle status reactively, as this function only 46 | // takes care of reconnect status and doesn't care if it changes. 47 | 48 | // Note that userId does not change during a resume login, as designed by Meteor. 49 | // However, the idle state is tied to the connection and not the userId. 50 | if (isConnected && !wasConnected && idle) { 51 | return MonitorInternals.reportIdle(lastActivityTime); 52 | } 53 | }, 54 | 55 | onWindowBlur() { 56 | focused = false; 57 | return monitor(); 58 | }, 59 | 60 | onWindowFocus() { 61 | focused = true; 62 | // Focusing should count as an action, otherwise "active" event may be 63 | // triggered at some point in the past! 64 | return monitor(true); 65 | }, 66 | 67 | reportIdle(time) { 68 | return Meteor.call('user-status-idle', time); 69 | }, 70 | 71 | reportActive(time) { 72 | return Meteor.call('user-status-active', time); 73 | } 74 | 75 | }; 76 | 77 | const start = (settings) => { 78 | if (!TimeSync.isSynced()) { 79 | throw new Error('Can\'t start idle monitor until synced to server'); 80 | } 81 | if (monitorId) { 82 | throw new Error('Idle monitor is already active. Stop it first.'); 83 | } 84 | 85 | settings = settings || {}; 86 | 87 | // The amount of time before a user is marked idle 88 | MonitorInternals.idleThreshold = settings.threshold || 60000; 89 | 90 | // Don't check too quickly; it doesn't matter anyway: http://stackoverflow.com/q/15871942/586086 91 | const interval = Math.max(settings.interval || 1000, 1000); 92 | 93 | // Whether blurring the window should immediately cause the user to go idle 94 | MonitorInternals.idleOnBlur = (settings.idleOnBlur != null) ? settings.idleOnBlur : false; 95 | 96 | // Set new monitoring interval 97 | monitorId = Meteor.setInterval(monitor, interval); 98 | monitorDep.changed(); 99 | 100 | // Reset last activity; can't count inactivity from some arbitrary time 101 | if (lastActivityTime == null) { 102 | lastActivityTime = Tracker.nonreactive(() => TimeSync.serverTime()); 103 | activityDep.changed(); 104 | } 105 | 106 | monitor(); 107 | }; 108 | 109 | const stop = () => { 110 | if (!monitorId) { 111 | throw new Error('Idle monitor is not running.'); 112 | } 113 | 114 | Meteor.clearInterval(monitorId); 115 | monitorId = null; 116 | lastActivityTime = undefined; // If monitor started again, we shouldn't re-use this time 117 | monitorDep.changed(); 118 | 119 | if (idle) { // Un-set any idleness 120 | idle = false; 121 | idleDep.changed(); 122 | // need to run this because the Tracker below won't re-run when monitor is off 123 | MonitorInternals.reportActive(Tracker.nonreactive(() => TimeSync.serverTime())); 124 | } 125 | 126 | }; 127 | 128 | const monitor = (setAction) => { 129 | // Ignore focus/blur events when we aren't monitoring 130 | if (!monitorId) { 131 | return; 132 | } 133 | 134 | // We use setAction here to not have to call serverTime twice. Premature optimization? 135 | const currentTime = Tracker.nonreactive(() => TimeSync.serverTime()); 136 | // Can't monitor if we haven't synced with server yet, or lost our sync. 137 | if (currentTime == null) { 138 | return; 139 | } 140 | 141 | // Update action as long as we're not blurred and idling on blur 142 | // We ignore actions that happen while a client is blurred, if idleOnBlur is set. 143 | if (setAction && (focused || !MonitorInternals.idleOnBlur)) { 144 | lastActivityTime = currentTime; 145 | activityDep.changed(); 146 | } 147 | 148 | const newIdle = MonitorInternals.computeState(lastActivityTime, currentTime, focused); 149 | 150 | if (newIdle !== idle) { 151 | idle = newIdle; 152 | idleDep.changed(); 153 | } 154 | }; 155 | 156 | const touch = () => { 157 | if (!monitorId) { 158 | Meteor._debug('Cannot touch as idle monitor is not running.'); 159 | return; 160 | } 161 | return monitor(true); // Check for an idle state change right now 162 | }; 163 | 164 | const isIdle = () => { 165 | idleDep.depend(); 166 | return idle; 167 | }; 168 | 169 | const isMonitoring = () => { 170 | monitorDep.depend(); 171 | return (monitorId != null); 172 | }; 173 | 174 | const lastActivity = () => { 175 | if (!isMonitoring()) { 176 | return; 177 | } 178 | activityDep.depend(); 179 | return lastActivityTime; 180 | }; 181 | 182 | Meteor.startup(() => { 183 | // Listen for mouse and keyboard events on window 184 | // TODO other stuff - e.g. touch events? 185 | window.addEventListener('click', () => monitor(true)); 186 | window.addEventListener('keydown', () => monitor(true)); 187 | 188 | // catch window blur events when requested and where supported 189 | // We'll use jQuery here instead of window.blur so that other code can attach blur events: 190 | // http://stackoverflow.com/q/22415296/586086 191 | window.addEventListener('blur', MonitorInternals.onWindowBlur); 192 | window.addEventListener('focus', MonitorInternals.onWindowFocus); 193 | 194 | // Catch Cordova "pause" and "resume" events: 195 | // https://github.com/mizzao/meteor-user-status/issues/47 196 | if (Meteor.isCordova) { 197 | document.addEventListener('pause', MonitorInternals.onWindowBlur); 198 | document.addEventListener('resume', MonitorInternals.onWindowFocus); 199 | } 200 | 201 | // First check initial state if window loaded while blurred 202 | // Some browsers don't fire focus on load: http://stackoverflow.com/a/10325169/586086 203 | focused = document.hasFocus(); 204 | 205 | // Report idle status whenever connection changes 206 | Tracker.autorun(() => { 207 | // Don't report idle state unless we're monitoring 208 | if (!isMonitoring()) { 209 | return; 210 | } 211 | 212 | // XXX These will buffer across a disconnection - do we want that? 213 | // The idle report will result in a duplicate message (with below) 214 | // The active report will result in a null op. 215 | if (isIdle()) { 216 | MonitorInternals.reportIdle(lastActivityTime); 217 | } else { 218 | // If we were inactive, report that we are active again to the server 219 | MonitorInternals.reportActive(lastActivityTime); 220 | } 221 | }); 222 | 223 | // If we reconnect and we were idle, make sure we send that upstream 224 | let wasConnected = Meteor.status().connected; 225 | return Tracker.autorun(() => { 226 | const { 227 | connected 228 | } = Meteor.status(); 229 | MonitorInternals.connectionChange(connected, wasConnected); 230 | 231 | wasConnected = connected; 232 | }); 233 | }); 234 | 235 | // export functions for starting and stopping idle monitor 236 | export const UserStatus = { 237 | startMonitor: start, 238 | stopMonitor: stop, 239 | pingMonitor: touch, 240 | isIdle, 241 | isMonitoring, 242 | lastActivity 243 | }; 244 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | settings.json 2 | -------------------------------------------------------------------------------- /demo/.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 | 1.4.0-remove-old-dev-bundle-link 15 | 1.4.1-add-shell-server-package 16 | 1.4.3-split-account-service-packages 17 | 1.5-add-dynamic-import-package 18 | 1.7-split-underscore-from-meteor-base 19 | 1.8.3-split-jquery-from-blaze 20 | -------------------------------------------------------------------------------- /demo/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /demo/.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 | 1s8s711noc9bq16pktns 8 | -------------------------------------------------------------------------------- /demo/.meteor/identifier: -------------------------------------------------------------------------------- 1 | hz6wab1631fff885otl -------------------------------------------------------------------------------- /demo/.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@1.5.1 # Packages every Meteor app needs to have 8 | mobile-experience@1.1.1 # Packages for a great mobile UX 9 | mongo@1.16.8 # The database Meteor supports right now 10 | blaze-html-templates # Compile .html files into Meteor Blaze views 11 | reactive-var@1.0.12 # Reactive variable for tracker 12 | tracker@1.3.3 # Meteor's client-side reactive programming library 13 | 14 | standard-minifier-css@1.9.2 # CSS minifier run for production mode 15 | standard-minifier-js@2.8.1 # JS minifier run for production mode 16 | es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers 17 | ecmascript@0.16.8 # Enable ECMAScript2015+ syntax in app code 18 | shell-server@0.5.0 # Server-side component of the `meteor shell` command 19 | 20 | # communitypackages:accounts-testing 21 | mizzao:timesync 22 | mizzao:user-status 23 | twbs:bootstrap 24 | dynamic-import@0.7.3 25 | underscore@1.6.0 26 | jquery 27 | -------------------------------------------------------------------------------- /demo/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /demo/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@2.15 2 | -------------------------------------------------------------------------------- /demo/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@2.2.10 2 | allow-deny@1.1.1 3 | autoupdate@1.8.0 4 | babel-compiler@7.10.5 5 | babel-runtime@1.5.1 6 | base64@1.0.12 7 | binary-heap@1.0.11 8 | blaze@2.8.0 9 | blaze-html-templates@2.0.1 10 | blaze-tools@1.1.4 11 | boilerplate-generator@1.7.2 12 | caching-compiler@1.2.2 13 | caching-html-compiler@1.2.2 14 | callback-hook@1.5.1 15 | check@1.3.2 16 | ddp@1.4.1 17 | ddp-client@2.6.1 18 | ddp-common@1.4.0 19 | ddp-rate-limiter@1.2.1 20 | ddp-server@2.7.0 21 | diff-sequence@1.1.2 22 | dynamic-import@0.7.3 23 | ecmascript@0.16.8 24 | ecmascript-runtime@0.8.1 25 | ecmascript-runtime-client@0.12.1 26 | ecmascript-runtime-server@0.11.0 27 | ejson@1.1.3 28 | es5-shim@4.8.0 29 | fetch@0.1.4 30 | geojson-utils@1.0.11 31 | hot-code-push@1.0.4 32 | html-tools@1.1.4 33 | htmljs@1.2.0 34 | http@2.0.0 35 | id-map@1.1.1 36 | inter-process-messaging@0.1.1 37 | jquery@1.11.11 38 | launch-screen@2.0.0 39 | localstorage@1.2.0 40 | logging@1.3.3 41 | meteor@1.11.5 42 | meteor-base@1.5.1 43 | minifier-css@1.6.4 44 | minifier-js@2.7.5 45 | minimongo@1.9.3 46 | mizzao:timesync@0.5.4 47 | mizzao:user-status@1.2.0 48 | mobile-experience@1.1.1 49 | mobile-status-bar@1.1.0 50 | modern-browsers@0.1.10 51 | modules@0.20.0 52 | modules-runtime@0.13.1 53 | mongo@1.16.8 54 | mongo-decimal@0.1.3 55 | mongo-dev-server@1.1.0 56 | mongo-id@1.0.8 57 | npm-mongo@4.17.2 58 | observe-sequence@1.0.22 59 | ordered-dict@1.1.0 60 | promise@0.12.2 61 | random@1.2.1 62 | rate-limit@1.1.1 63 | react-fast-refresh@0.2.8 64 | reactive-var@1.0.12 65 | reload@1.3.1 66 | retry@1.1.0 67 | routepolicy@1.1.1 68 | shell-server@0.5.0 69 | socket-stream-client@0.5.2 70 | spacebars@1.4.1 71 | spacebars-compiler@1.3.2 72 | standard-minifier-css@1.9.2 73 | standard-minifier-js@2.8.1 74 | templating@1.4.3 75 | templating-compiler@1.4.2 76 | templating-runtime@1.6.4 77 | templating-tools@1.2.3 78 | tracker@1.3.3 79 | twbs:bootstrap@3.3.6 80 | typescript@4.9.5 81 | underscore@1.6.1 82 | url@1.3.2 83 | webapp@1.13.8 84 | webapp-hashing@1.1.1 85 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | ## Demo/test app for user-status 2 | 3 | Note the following cool features: 4 | 5 | - All clients are synced to server time using [timesync](https://github.com/mizzao/meteor-timesync), and can make local comparisons even if their actual time is way off. 6 | - Each connection maintains its own idle (or not) state. 7 | - When a client goes idle (by crossing some threshold), the last activity time is recorded for when it started being idle. 8 | -------------------------------------------------------------------------------- /demo/client/main.css: -------------------------------------------------------------------------------- 1 | tr.bold>td { 2 | font-weight: bold; 3 | text-decoration: underline; 4 | } 5 | -------------------------------------------------------------------------------- /demo/client/main.html: -------------------------------------------------------------------------------- 1 | 2 | User-Status Demonstration and Testing 3 | 4 | 5 | 6 | 7 | Fork me on GitHub 8 | 9 |
10 |
11 |
12 |

Instructions

13 |
    14 |
  • Log in with any username.
  • 15 |
  • Log in from other computers with the same username or open multiple tabs.
  • 16 | These will appear as sessions/connections under the same user. 17 |
  • Click or keypress events reset the idle timer. The default idle threshold is 30 seconds.
  • 18 |
  • Idle session information is transmitted to the server and multiplexed into the user document.
  • 19 |
  • Play with the idle timer to try different client-side settings.
  • 20 |
  • Click 'Resync Time' a few times to see the variance in TimeSync.
  • 21 |
22 | {{> login}} 23 | {{> status userStatus}} 24 |
25 |
26 | {{> serverStatus }} 27 |
28 |
29 |
30 | 31 | 32 | 45 | 46 | 85 | 86 | 123 | 124 | 138 | 139 | 144 | -------------------------------------------------------------------------------- /demo/client/main.js: -------------------------------------------------------------------------------- 1 | import { Handlebars } from 'meteor/blaze'; 2 | import { Meteor } from 'meteor/meteor'; 3 | import { TimeSync } from 'meteor/mizzao:timesync'; 4 | import { Mongo } from 'meteor/mongo'; 5 | import { Template } from 'meteor/templating'; 6 | import { Tracker } from 'meteor/tracker'; 7 | import { UserStatus } from 'meteor/mizzao:user-status'; 8 | import moment from 'moment'; 9 | 10 | import './main.html'; 11 | 12 | const UserConnections = new Mongo.Collection('user_status_sessions'); 13 | 14 | const relativeTime = (timeAgo) => { 15 | const diff = moment.utc(TimeSync.serverTime() - timeAgo); 16 | const time = diff.format('H:mm:ss'); 17 | const days = +diff.format('DDD') - 1; 18 | const ago = (days ? days + 'd ' : '') + time; 19 | return `${ago} ago`; 20 | }; 21 | 22 | Handlebars.registerHelper('userStatus', UserStatus); 23 | Handlebars.registerHelper('localeTime', date => date != null ? date.toLocaleString() : undefined); 24 | Handlebars.registerHelper('relativeTime', relativeTime); 25 | 26 | Template.login.helpers({ 27 | loggedIn() { 28 | return Meteor.userId(); 29 | } 30 | }); 31 | 32 | Template.status.events = { 33 | 'submit form.start-monitor'(e, tmpl) { 34 | e.preventDefault(); 35 | return UserStatus.startMonitor({ 36 | threshold: tmpl.find('input[name=threshold]').valueAsNumber, 37 | interval: tmpl.find('input[name=interval]').valueAsNumber, 38 | idleOnBlur: tmpl.find('select[name=idleOnBlur]').value === 'true' 39 | }); 40 | }, 41 | 42 | 'click .stop-monitor'() { 43 | return UserStatus.stopMonitor(); 44 | }, 45 | 'click .resync'() { 46 | return TimeSync.resync(); 47 | } 48 | }; 49 | 50 | Template.status.helpers({ 51 | lastActivity() { 52 | const lastActivity = this.lastActivity(); 53 | if (lastActivity != null) { 54 | return relativeTime(lastActivity); 55 | } else { 56 | return 'undefined'; 57 | } 58 | } 59 | }); 60 | 61 | Template.status.helpers({ 62 | serverTime() { 63 | return new Date(TimeSync.serverTime()).toLocaleString(); 64 | }, 65 | serverOffset: TimeSync.serverOffset, 66 | serverRTT: TimeSync.roundTripTime, 67 | 68 | // Falsy values aren't rendered in templates, so let's render them ourself 69 | isIdleText() { 70 | return this.isIdle() || 'false'; 71 | }, 72 | isMonitoringText() { 73 | return this.isMonitoring() || 'false'; 74 | } 75 | }); 76 | 77 | Template.serverStatus.helpers({ 78 | anonymous() { 79 | return UserConnections.find({ 80 | userId: { 81 | $exists: false 82 | } 83 | }); 84 | }, 85 | users() { 86 | return Meteor.users.find(); 87 | }, 88 | userClass() { 89 | if ((this.status != null ? this.status.idle : undefined)) { 90 | return 'warning'; 91 | } else { 92 | return 'success'; 93 | } 94 | }, 95 | connections() { 96 | return UserConnections.find({ 97 | userId: this._id 98 | }); 99 | } 100 | }); 101 | 102 | Template.serverConnection.helpers({ 103 | connectionClass() { 104 | if (this.idle) { 105 | return 'warning'; 106 | } else { 107 | return 'success'; 108 | } 109 | }, 110 | loginTime() { 111 | if (this.loginTime == null) { 112 | return; 113 | } 114 | return new Date(this.loginTime).toLocaleString(); 115 | } 116 | }); 117 | 118 | Template.login.events = { 119 | 'submit form'(e, tmpl) { 120 | e.preventDefault(); 121 | const input = tmpl.find('input[name=username]'); 122 | input.blur(); 123 | return Meteor.insecureUserLogin(input.value, (err) => { 124 | if (err) { 125 | return console.log(err); 126 | } 127 | }); 128 | } 129 | }; 130 | 131 | // Start monitor as soon as we got a signal, captain! 132 | Tracker.autorun((c) => { 133 | try { // May be an error if time is not synced 134 | UserStatus.startMonitor({ 135 | threshold: 30000, 136 | idleOnBlur: true 137 | }); 138 | return c.stop(); 139 | } catch (error) { 140 | console.error(error); 141 | } 142 | }); 143 | -------------------------------------------------------------------------------- /demo/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DEPLOY_HOSTNAME=galaxy.meteor.com meteor deploy user-status.meteorapp.com --settings settings.json 3 | -------------------------------------------------------------------------------- /demo/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user-status-demo", 3 | "requires": true, 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "@babel/code-frame": { 7 | "version": "7.10.3", 8 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", 9 | "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", 10 | "dev": true, 11 | "requires": { 12 | "@babel/highlight": "^7.10.3" 13 | } 14 | }, 15 | "@babel/helper-environment-visitor": { 16 | "version": "7.22.20", 17 | "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", 18 | "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", 19 | "dev": true 20 | }, 21 | "@babel/helper-hoist-variables": { 22 | "version": "7.22.5", 23 | "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", 24 | "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", 25 | "dev": true, 26 | "requires": { 27 | "@babel/types": "^7.22.5" 28 | }, 29 | "dependencies": { 30 | "@babel/helper-validator-identifier": { 31 | "version": "7.22.20", 32 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", 33 | "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", 34 | "dev": true 35 | }, 36 | "@babel/types": { 37 | "version": "7.23.0", 38 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", 39 | "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", 40 | "dev": true, 41 | "requires": { 42 | "@babel/helper-string-parser": "^7.22.5", 43 | "@babel/helper-validator-identifier": "^7.22.20", 44 | "to-fast-properties": "^2.0.0" 45 | } 46 | } 47 | } 48 | }, 49 | "@babel/helper-string-parser": { 50 | "version": "7.22.5", 51 | "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", 52 | "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", 53 | "dev": true 54 | }, 55 | "@babel/helper-validator-identifier": { 56 | "version": "7.10.3", 57 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", 58 | "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", 59 | "dev": true 60 | }, 61 | "@babel/highlight": { 62 | "version": "7.10.3", 63 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", 64 | "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", 65 | "dev": true, 66 | "requires": { 67 | "@babel/helper-validator-identifier": "^7.10.3", 68 | "chalk": "^2.0.0", 69 | "js-tokens": "^4.0.0" 70 | } 71 | }, 72 | "@babel/parser": { 73 | "version": "7.10.3", 74 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", 75 | "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", 76 | "dev": true 77 | }, 78 | "@babel/runtime": { 79 | "version": "7.23.9", 80 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", 81 | "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", 82 | "requires": { 83 | "regenerator-runtime": "^0.14.0" 84 | } 85 | }, 86 | "@babel/traverse": { 87 | "version": "7.23.2", 88 | "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", 89 | "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", 90 | "dev": true, 91 | "requires": { 92 | "@babel/code-frame": "^7.22.13", 93 | "@babel/generator": "^7.23.0", 94 | "@babel/helper-environment-visitor": "^7.22.20", 95 | "@babel/helper-function-name": "^7.23.0", 96 | "@babel/helper-hoist-variables": "^7.22.5", 97 | "@babel/helper-split-export-declaration": "^7.22.6", 98 | "@babel/parser": "^7.23.0", 99 | "@babel/types": "^7.23.0", 100 | "debug": "^4.1.0", 101 | "globals": "^11.1.0" 102 | }, 103 | "dependencies": { 104 | "@babel/code-frame": { 105 | "version": "7.22.13", 106 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", 107 | "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", 108 | "dev": true, 109 | "requires": { 110 | "@babel/highlight": "^7.22.13", 111 | "chalk": "^2.4.2" 112 | } 113 | }, 114 | "@babel/generator": { 115 | "version": "7.23.0", 116 | "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", 117 | "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", 118 | "dev": true, 119 | "requires": { 120 | "@babel/types": "^7.23.0", 121 | "@jridgewell/gen-mapping": "^0.3.2", 122 | "@jridgewell/trace-mapping": "^0.3.17", 123 | "jsesc": "^2.5.1" 124 | } 125 | }, 126 | "@babel/helper-function-name": { 127 | "version": "7.23.0", 128 | "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", 129 | "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", 130 | "dev": true, 131 | "requires": { 132 | "@babel/template": "^7.22.15", 133 | "@babel/types": "^7.23.0" 134 | } 135 | }, 136 | "@babel/helper-split-export-declaration": { 137 | "version": "7.22.6", 138 | "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", 139 | "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", 140 | "dev": true, 141 | "requires": { 142 | "@babel/types": "^7.22.5" 143 | } 144 | }, 145 | "@babel/helper-validator-identifier": { 146 | "version": "7.22.20", 147 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", 148 | "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", 149 | "dev": true 150 | }, 151 | "@babel/highlight": { 152 | "version": "7.22.20", 153 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", 154 | "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", 155 | "dev": true, 156 | "requires": { 157 | "@babel/helper-validator-identifier": "^7.22.20", 158 | "chalk": "^2.4.2", 159 | "js-tokens": "^4.0.0" 160 | } 161 | }, 162 | "@babel/parser": { 163 | "version": "7.23.0", 164 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", 165 | "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", 166 | "dev": true 167 | }, 168 | "@babel/template": { 169 | "version": "7.22.15", 170 | "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", 171 | "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", 172 | "dev": true, 173 | "requires": { 174 | "@babel/code-frame": "^7.22.13", 175 | "@babel/parser": "^7.22.15", 176 | "@babel/types": "^7.22.15" 177 | } 178 | }, 179 | "@babel/types": { 180 | "version": "7.23.0", 181 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", 182 | "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", 183 | "dev": true, 184 | "requires": { 185 | "@babel/helper-string-parser": "^7.22.5", 186 | "@babel/helper-validator-identifier": "^7.22.20", 187 | "to-fast-properties": "^2.0.0" 188 | } 189 | } 190 | } 191 | }, 192 | "@babel/types": { 193 | "version": "7.10.3", 194 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", 195 | "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", 196 | "dev": true, 197 | "requires": { 198 | "@babel/helper-validator-identifier": "^7.10.3", 199 | "lodash": "^4.17.13", 200 | "to-fast-properties": "^2.0.0" 201 | } 202 | }, 203 | "@jridgewell/gen-mapping": { 204 | "version": "0.3.3", 205 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", 206 | "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", 207 | "dev": true, 208 | "requires": { 209 | "@jridgewell/set-array": "^1.0.1", 210 | "@jridgewell/sourcemap-codec": "^1.4.10", 211 | "@jridgewell/trace-mapping": "^0.3.9" 212 | } 213 | }, 214 | "@jridgewell/resolve-uri": { 215 | "version": "3.1.1", 216 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", 217 | "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", 218 | "dev": true 219 | }, 220 | "@jridgewell/set-array": { 221 | "version": "1.1.2", 222 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", 223 | "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", 224 | "dev": true 225 | }, 226 | "@jridgewell/sourcemap-codec": { 227 | "version": "1.4.15", 228 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 229 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", 230 | "dev": true 231 | }, 232 | "@jridgewell/trace-mapping": { 233 | "version": "0.3.19", 234 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", 235 | "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", 236 | "dev": true, 237 | "requires": { 238 | "@jridgewell/resolve-uri": "^3.1.0", 239 | "@jridgewell/sourcemap-codec": "^1.4.14" 240 | } 241 | }, 242 | "@mapbox/node-pre-gyp": { 243 | "version": "1.0.11", 244 | "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", 245 | "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", 246 | "requires": { 247 | "detect-libc": "^2.0.0", 248 | "https-proxy-agent": "^5.0.0", 249 | "make-dir": "^3.1.0", 250 | "node-fetch": "^2.6.7", 251 | "nopt": "^5.0.0", 252 | "npmlog": "^5.0.1", 253 | "rimraf": "^3.0.2", 254 | "semver": "^7.3.5", 255 | "tar": "^6.1.11" 256 | } 257 | }, 258 | "abbrev": { 259 | "version": "1.1.1", 260 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 261 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 262 | }, 263 | "agent-base": { 264 | "version": "6.0.2", 265 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 266 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 267 | "requires": { 268 | "debug": "4" 269 | } 270 | }, 271 | "ansi-regex": { 272 | "version": "5.0.1", 273 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 274 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 275 | }, 276 | "ansi-styles": { 277 | "version": "3.2.1", 278 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 279 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 280 | "dev": true, 281 | "requires": { 282 | "color-convert": "^1.9.0" 283 | } 284 | }, 285 | "aproba": { 286 | "version": "2.0.0", 287 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", 288 | "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" 289 | }, 290 | "are-we-there-yet": { 291 | "version": "2.0.0", 292 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", 293 | "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", 294 | "requires": { 295 | "delegates": "^1.0.0", 296 | "readable-stream": "^3.6.0" 297 | } 298 | }, 299 | "autoprefixer": { 300 | "version": "10.4.17", 301 | "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", 302 | "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", 303 | "dev": true, 304 | "requires": { 305 | "browserslist": "^4.22.2", 306 | "caniuse-lite": "^1.0.30001578", 307 | "fraction.js": "^4.3.7", 308 | "normalize-range": "^0.1.2", 309 | "picocolors": "^1.0.0", 310 | "postcss-value-parser": "^4.2.0" 311 | } 312 | }, 313 | "babel-eslint": { 314 | "version": "10.1.0", 315 | "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", 316 | "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", 317 | "dev": true, 318 | "requires": { 319 | "@babel/code-frame": "^7.0.0", 320 | "@babel/parser": "^7.7.0", 321 | "@babel/traverse": "^7.7.0", 322 | "@babel/types": "^7.7.0", 323 | "eslint-visitor-keys": "^1.0.0", 324 | "resolve": "^1.12.0" 325 | } 326 | }, 327 | "balanced-match": { 328 | "version": "1.0.2", 329 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 330 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 331 | }, 332 | "bcrypt": { 333 | "version": "5.1.1", 334 | "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", 335 | "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", 336 | "requires": { 337 | "@mapbox/node-pre-gyp": "^1.0.11", 338 | "node-addon-api": "^5.0.0" 339 | } 340 | }, 341 | "brace-expansion": { 342 | "version": "1.1.11", 343 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 344 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 345 | "requires": { 346 | "balanced-match": "^1.0.0", 347 | "concat-map": "0.0.1" 348 | } 349 | }, 350 | "browserslist": { 351 | "version": "4.23.0", 352 | "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", 353 | "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", 354 | "dev": true, 355 | "requires": { 356 | "caniuse-lite": "^1.0.30001587", 357 | "electron-to-chromium": "^1.4.668", 358 | "node-releases": "^2.0.14", 359 | "update-browserslist-db": "^1.0.13" 360 | } 361 | }, 362 | "caniuse-lite": { 363 | "version": "1.0.30001591", 364 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", 365 | "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==", 366 | "dev": true 367 | }, 368 | "chalk": { 369 | "version": "2.4.2", 370 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 371 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 372 | "dev": true, 373 | "requires": { 374 | "ansi-styles": "^3.2.1", 375 | "escape-string-regexp": "^1.0.5", 376 | "supports-color": "^5.3.0" 377 | }, 378 | "dependencies": { 379 | "supports-color": { 380 | "version": "5.5.0", 381 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 382 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 383 | "dev": true, 384 | "requires": { 385 | "has-flag": "^3.0.0" 386 | } 387 | } 388 | } 389 | }, 390 | "chownr": { 391 | "version": "2.0.0", 392 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", 393 | "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" 394 | }, 395 | "color-convert": { 396 | "version": "1.9.3", 397 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 398 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 399 | "dev": true, 400 | "requires": { 401 | "color-name": "1.1.3" 402 | } 403 | }, 404 | "color-name": { 405 | "version": "1.1.3", 406 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 407 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 408 | "dev": true 409 | }, 410 | "color-support": { 411 | "version": "1.1.3", 412 | "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", 413 | "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" 414 | }, 415 | "concat-map": { 416 | "version": "0.0.1", 417 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 418 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 419 | }, 420 | "console-control-strings": { 421 | "version": "1.1.0", 422 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 423 | "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" 424 | }, 425 | "debug": { 426 | "version": "4.3.4", 427 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 428 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 429 | "requires": { 430 | "ms": "2.1.2" 431 | } 432 | }, 433 | "delegates": { 434 | "version": "1.0.0", 435 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 436 | "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" 437 | }, 438 | "detect-libc": { 439 | "version": "2.0.2", 440 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", 441 | "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==" 442 | }, 443 | "electron-to-chromium": { 444 | "version": "1.4.685", 445 | "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.685.tgz", 446 | "integrity": "sha512-yDYeobbTEe4TNooEzOQO6xFqg9XnAkVy2Lod1C1B2it8u47JNLYvl9nLDWBamqUakWB8Jc1hhS1uHUNYTNQdfw==", 447 | "dev": true 448 | }, 449 | "emoji-regex": { 450 | "version": "8.0.0", 451 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 452 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 453 | }, 454 | "escalade": { 455 | "version": "3.1.2", 456 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", 457 | "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", 458 | "dev": true 459 | }, 460 | "escape-string-regexp": { 461 | "version": "1.0.5", 462 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 463 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 464 | "dev": true 465 | }, 466 | "eslint-visitor-keys": { 467 | "version": "1.3.0", 468 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", 469 | "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", 470 | "dev": true 471 | }, 472 | "fraction.js": { 473 | "version": "4.3.7", 474 | "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", 475 | "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", 476 | "dev": true 477 | }, 478 | "fs-minipass": { 479 | "version": "2.1.0", 480 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", 481 | "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", 482 | "requires": { 483 | "minipass": "^3.0.0" 484 | }, 485 | "dependencies": { 486 | "minipass": { 487 | "version": "3.3.6", 488 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", 489 | "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", 490 | "requires": { 491 | "yallist": "^4.0.0" 492 | } 493 | } 494 | } 495 | }, 496 | "fs.realpath": { 497 | "version": "1.0.0", 498 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 499 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" 500 | }, 501 | "gauge": { 502 | "version": "3.0.2", 503 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", 504 | "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", 505 | "requires": { 506 | "aproba": "^1.0.3 || ^2.0.0", 507 | "color-support": "^1.1.2", 508 | "console-control-strings": "^1.0.0", 509 | "has-unicode": "^2.0.1", 510 | "object-assign": "^4.1.1", 511 | "signal-exit": "^3.0.0", 512 | "string-width": "^4.2.3", 513 | "strip-ansi": "^6.0.1", 514 | "wide-align": "^1.1.2" 515 | } 516 | }, 517 | "glob": { 518 | "version": "7.2.3", 519 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 520 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 521 | "requires": { 522 | "fs.realpath": "^1.0.0", 523 | "inflight": "^1.0.4", 524 | "inherits": "2", 525 | "minimatch": "^3.1.1", 526 | "once": "^1.3.0", 527 | "path-is-absolute": "^1.0.0" 528 | } 529 | }, 530 | "globals": { 531 | "version": "11.12.0", 532 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", 533 | "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", 534 | "dev": true 535 | }, 536 | "has-flag": { 537 | "version": "3.0.0", 538 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 539 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 540 | "dev": true 541 | }, 542 | "has-unicode": { 543 | "version": "2.0.1", 544 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 545 | "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" 546 | }, 547 | "https-proxy-agent": { 548 | "version": "5.0.1", 549 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", 550 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", 551 | "requires": { 552 | "agent-base": "6", 553 | "debug": "4" 554 | } 555 | }, 556 | "inflight": { 557 | "version": "1.0.6", 558 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 559 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 560 | "requires": { 561 | "once": "^1.3.0", 562 | "wrappy": "1" 563 | } 564 | }, 565 | "inherits": { 566 | "version": "2.0.4", 567 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 568 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 569 | }, 570 | "is-fullwidth-code-point": { 571 | "version": "3.0.0", 572 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 573 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 574 | }, 575 | "jquery": { 576 | "version": "3.7.1", 577 | "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", 578 | "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" 579 | }, 580 | "js-tokens": { 581 | "version": "4.0.0", 582 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 583 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 584 | "dev": true 585 | }, 586 | "jsesc": { 587 | "version": "2.5.2", 588 | "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", 589 | "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", 590 | "dev": true 591 | }, 592 | "lodash": { 593 | "version": "4.17.21", 594 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 595 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", 596 | "dev": true 597 | }, 598 | "lru-cache": { 599 | "version": "6.0.0", 600 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 601 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 602 | "requires": { 603 | "yallist": "^4.0.0" 604 | } 605 | }, 606 | "make-dir": { 607 | "version": "3.1.0", 608 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", 609 | "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", 610 | "requires": { 611 | "semver": "^6.0.0" 612 | }, 613 | "dependencies": { 614 | "semver": { 615 | "version": "6.3.1", 616 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 617 | "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" 618 | } 619 | } 620 | }, 621 | "minimatch": { 622 | "version": "3.1.2", 623 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 624 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 625 | "requires": { 626 | "brace-expansion": "^1.1.7" 627 | } 628 | }, 629 | "minipass": { 630 | "version": "5.0.0", 631 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", 632 | "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" 633 | }, 634 | "minizlib": { 635 | "version": "2.1.2", 636 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", 637 | "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", 638 | "requires": { 639 | "minipass": "^3.0.0", 640 | "yallist": "^4.0.0" 641 | }, 642 | "dependencies": { 643 | "minipass": { 644 | "version": "3.3.6", 645 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", 646 | "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", 647 | "requires": { 648 | "yallist": "^4.0.0" 649 | } 650 | } 651 | } 652 | }, 653 | "mkdirp": { 654 | "version": "1.0.4", 655 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", 656 | "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" 657 | }, 658 | "moment": { 659 | "version": "2.30.1", 660 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", 661 | "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==" 662 | }, 663 | "ms": { 664 | "version": "2.1.2", 665 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 666 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 667 | }, 668 | "node-addon-api": { 669 | "version": "5.1.0", 670 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", 671 | "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" 672 | }, 673 | "node-fetch": { 674 | "version": "2.7.0", 675 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", 676 | "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", 677 | "requires": { 678 | "whatwg-url": "^5.0.0" 679 | } 680 | }, 681 | "node-releases": { 682 | "version": "2.0.14", 683 | "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", 684 | "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", 685 | "dev": true 686 | }, 687 | "nopt": { 688 | "version": "5.0.0", 689 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", 690 | "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", 691 | "requires": { 692 | "abbrev": "1" 693 | } 694 | }, 695 | "normalize-range": { 696 | "version": "0.1.2", 697 | "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", 698 | "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", 699 | "dev": true 700 | }, 701 | "npmlog": { 702 | "version": "5.0.1", 703 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", 704 | "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", 705 | "requires": { 706 | "are-we-there-yet": "^2.0.0", 707 | "console-control-strings": "^1.1.0", 708 | "gauge": "^3.0.0", 709 | "set-blocking": "^2.0.0" 710 | } 711 | }, 712 | "object-assign": { 713 | "version": "4.1.1", 714 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 715 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" 716 | }, 717 | "once": { 718 | "version": "1.4.0", 719 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 720 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 721 | "requires": { 722 | "wrappy": "1" 723 | } 724 | }, 725 | "path-is-absolute": { 726 | "version": "1.0.1", 727 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 728 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" 729 | }, 730 | "path-parse": { 731 | "version": "1.0.7", 732 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 733 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 734 | "dev": true 735 | }, 736 | "picocolors": { 737 | "version": "1.0.0", 738 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 739 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", 740 | "dev": true 741 | }, 742 | "postcss-value-parser": { 743 | "version": "4.2.0", 744 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", 745 | "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", 746 | "dev": true 747 | }, 748 | "readable-stream": { 749 | "version": "3.6.2", 750 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 751 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 752 | "requires": { 753 | "inherits": "^2.0.3", 754 | "string_decoder": "^1.1.1", 755 | "util-deprecate": "^1.0.1" 756 | } 757 | }, 758 | "regenerator-runtime": { 759 | "version": "0.14.1", 760 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", 761 | "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" 762 | }, 763 | "resolve": { 764 | "version": "1.17.0", 765 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", 766 | "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", 767 | "dev": true, 768 | "requires": { 769 | "path-parse": "^1.0.6" 770 | } 771 | }, 772 | "rimraf": { 773 | "version": "3.0.2", 774 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 775 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 776 | "requires": { 777 | "glob": "^7.1.3" 778 | } 779 | }, 780 | "safe-buffer": { 781 | "version": "5.2.1", 782 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 783 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 784 | }, 785 | "semver": { 786 | "version": "7.5.4", 787 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", 788 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", 789 | "requires": { 790 | "lru-cache": "^6.0.0" 791 | } 792 | }, 793 | "set-blocking": { 794 | "version": "2.0.0", 795 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 796 | "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" 797 | }, 798 | "signal-exit": { 799 | "version": "3.0.7", 800 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", 801 | "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" 802 | }, 803 | "string-width": { 804 | "version": "4.2.3", 805 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 806 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 807 | "requires": { 808 | "emoji-regex": "^8.0.0", 809 | "is-fullwidth-code-point": "^3.0.0", 810 | "strip-ansi": "^6.0.1" 811 | } 812 | }, 813 | "string_decoder": { 814 | "version": "1.3.0", 815 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 816 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 817 | "requires": { 818 | "safe-buffer": "~5.2.0" 819 | } 820 | }, 821 | "strip-ansi": { 822 | "version": "6.0.1", 823 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 824 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 825 | "requires": { 826 | "ansi-regex": "^5.0.1" 827 | } 828 | }, 829 | "tar": { 830 | "version": "6.2.1", 831 | "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", 832 | "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", 833 | "requires": { 834 | "chownr": "^2.0.0", 835 | "fs-minipass": "^2.0.0", 836 | "minipass": "^5.0.0", 837 | "minizlib": "^2.1.1", 838 | "mkdirp": "^1.0.3", 839 | "yallist": "^4.0.0" 840 | } 841 | }, 842 | "to-fast-properties": { 843 | "version": "2.0.0", 844 | "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", 845 | "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", 846 | "dev": true 847 | }, 848 | "tr46": { 849 | "version": "0.0.3", 850 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 851 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 852 | }, 853 | "update-browserslist-db": { 854 | "version": "1.0.13", 855 | "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", 856 | "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", 857 | "dev": true, 858 | "requires": { 859 | "escalade": "^3.1.1", 860 | "picocolors": "^1.0.0" 861 | } 862 | }, 863 | "util-deprecate": { 864 | "version": "1.0.2", 865 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 866 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 867 | }, 868 | "webidl-conversions": { 869 | "version": "3.0.1", 870 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 871 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 872 | }, 873 | "whatwg-url": { 874 | "version": "5.0.0", 875 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 876 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 877 | "requires": { 878 | "tr46": "~0.0.3", 879 | "webidl-conversions": "^3.0.0" 880 | } 881 | }, 882 | "wide-align": { 883 | "version": "1.1.5", 884 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", 885 | "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", 886 | "requires": { 887 | "string-width": "^1.0.2 || 2 || 3 || 4" 888 | } 889 | }, 890 | "wrappy": { 891 | "version": "1.0.2", 892 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 893 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 894 | }, 895 | "yallist": { 896 | "version": "4.0.0", 897 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 898 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 899 | } 900 | } 901 | } 902 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user-status-demo", 3 | "private": false, 4 | "scripts": { 5 | "lint": "./node_modules/.bin/eslint .", 6 | "test": "meteor test --once --driver-package meteortesting:mocha", 7 | "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", 8 | "visualize": "meteor --production --extra-packages bundle-visualizer" 9 | }, 10 | "dependencies": { 11 | "@babel/runtime": "^7.23.9", 12 | "bcrypt": "^5.1.1", 13 | "jquery": "^3.7.1", 14 | "moment": "^2.30.1" 15 | }, 16 | "devDependencies": { 17 | "autoprefixer": "^10.4.17", 18 | "babel-eslint": "^10.1.0" 19 | }, 20 | "meteor": { 21 | "mainModule": { 22 | "client": "client/main.js", 23 | "server": "server/main.js" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demo/packages/.gitignore: -------------------------------------------------------------------------------- 1 | /mizzao:timesync 2 | /mizzao:user-status 3 | -------------------------------------------------------------------------------- /demo/packages/meteor-user-status/client/monitor.js: -------------------------------------------------------------------------------- 1 | ../../../../client/monitor.js -------------------------------------------------------------------------------- /demo/packages/meteor-user-status/package.js: -------------------------------------------------------------------------------- 1 | ../../../package.js -------------------------------------------------------------------------------- /demo/packages/meteor-user-status/server/status.js: -------------------------------------------------------------------------------- 1 | ../../../../server/status.js -------------------------------------------------------------------------------- /demo/server/main.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { UserStatus } from 'meteor/mizzao:user-status'; 3 | 4 | // Try setting this so it works on meteor.com 5 | // (https://github.com/oortcloud/unofficial-meteor-faq) 6 | process.env.HTTP_FORWARDED_COUNT = 1; 7 | 8 | Meteor.publish(null, () => [ 9 | Meteor.users.find({ 10 | 'status.online': true 11 | }, { // online users only 12 | fields: { 13 | status: 1, 14 | username: 1 15 | } 16 | }), 17 | UserStatus.connections.find() 18 | ]); 19 | -------------------------------------------------------------------------------- /demo/settings-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "galaxy.meteor.com": { 3 | "env": { 4 | "ROOT_URL": "https://user-status.meteorapp.com", 5 | "MONGO_URL": "mongodb://xxxxx:xxxxxx@xxxxx.mlab.com:37283/xxxxx" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meteor-Community-Packages/meteor-user-status/8cb45620af218d042e99846393ae3e5a0b973b33/docs/example.png -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | Package.describe({ 4 | name: 'mizzao:user-status', 5 | summary: 'User connection and idle state tracking for Meteor', 6 | version: '1.2.0', 7 | git: 'https://github.com/Meteor-Community-Packages/meteor-user-status.git' 8 | }); 9 | 10 | Package.onUse((api) => { 11 | api.versionsFrom(['1.7.0.5', '2.7', '2.8.1', '2.15']); 12 | 13 | api.use('ecmascript'); 14 | api.use('accounts-base'); 15 | api.use('check'); 16 | api.use('mongo'); 17 | api.use('ddp'); 18 | api.use('tracker', 'client'); 19 | api.use('mizzao:timesync@0.5.4'); 20 | 21 | api.export('MonitorInternals', 'client', { 22 | testOnly: true 23 | }); 24 | api.export('StatusInternals', 'server', { 25 | testOnly: true 26 | }); 27 | 28 | api.mainModule('client/monitor.js', 'client'); 29 | api.mainModule('server/status.js', 'server'); 30 | 31 | }); 32 | 33 | Package.onTest((api) => { 34 | api.use('ecmascript'); 35 | api.use('mizzao:user-status'); 36 | api.use('mizzao:timesync@0.5.4'); 37 | 38 | api.use(['accounts-base', 'accounts-password']); 39 | 40 | api.use(['random', 'tracker']); 41 | 42 | api.use('test-helpers'); 43 | api.use('tinytest'); 44 | 45 | api.addFiles('tests/insecure_login.js'); 46 | api.addFiles('tests/setup.js'); 47 | // Just some unit tests here. Use the test app otherwise. 48 | api.addFiles('tests/monitor_tests.js', 'client'); 49 | api.addFiles('tests/status_tests.js', 'server'); 50 | 51 | api.addFiles('tests/server_tests.js', 'server'); 52 | api.addFiles('tests/client_tests.js', 'client'); 53 | }); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meteor-user-status", 3 | "version": "1.1.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/Meteor-Community-Packages/meteor-user-status.git" 7 | }, 8 | "bugs": { 9 | "url": "https://github.com/Meteor-Community-Packages/meteor-user-status/issues" 10 | }, 11 | "homepage": "https://github.com/Meteor-Community-Packages/meteor-user-status#readme", 12 | "private": false, 13 | "scripts": { 14 | "lint": "./node_modules/.bin/eslint .", 15 | "test": "make test", 16 | "test-local": "meteor test-packages --once --driver-package test-in-console", 17 | "publish": "meteor npm i && npm prune --production && meteor publish && meteor npm i" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.23.9", 21 | "@babel/eslint-parser": "^7.23.10", 22 | "autoprefixer": "^10.4.17", 23 | "eslint": "^8.56.0", 24 | "husky": "^8.0.3", 25 | "mocha": "^10.2.0" 26 | }, 27 | "husky": { 28 | "hooks": { 29 | "pre-commit": "npm run lint" 30 | } 31 | }, 32 | "dependencies": {} 33 | } 34 | -------------------------------------------------------------------------------- /server/status.js: -------------------------------------------------------------------------------- 1 | /* 2 | Apparently, the new api.export takes care of issues here. No need to attach to global namespace. 3 | See http://shiggyenterprises.wordpress.com/2013/09/09/meteor-packages-in-coffeescript-0-6-5/ 4 | 5 | We may want to make UserSessions a server collection to take advantage of indices. 6 | Will implement if someone has enough online users to warrant it. 7 | */ 8 | import { Accounts } from 'meteor/accounts-base'; 9 | import { check, Match } from 'meteor/check'; 10 | import { Meteor } from 'meteor/meteor'; 11 | import { Mongo } from 'meteor/mongo'; 12 | import { EventEmitter } from 'events'; 13 | 14 | const UserConnections = new Mongo.Collection('user_status_sessions', { 15 | connection: null 16 | }); 17 | 18 | const statusEvents = new (EventEmitter)(); 19 | 20 | /* 21 | Multiplex login/logout events to status.online 22 | 23 | 'online' field is "true" if user is online, and "false" otherwise 24 | 25 | 'idle' field is tri-stated: 26 | - "true" if user is online and not idle 27 | - "false" if user is online and idle 28 | - null if user is offline 29 | */ 30 | statusEvents.on('connectionLogin', (advice) => { 31 | const update = { 32 | $set: { 33 | 'status.online': true, 34 | 'status.lastLogin': { 35 | date: advice.loginTime, 36 | ipAddr: advice.ipAddr, 37 | userAgent: advice.userAgent 38 | } 39 | } 40 | }; 41 | 42 | // unless ALL existing connections are idle (including this new one), 43 | // the user connection becomes active. 44 | const conns = UserConnections.find({ 45 | userId: advice.userId 46 | }).fetch(); 47 | if (!conns.every(c => c.idle)) { 48 | update.$set['status.idle'] = false; 49 | update.$unset = { 50 | 'status.lastActivity': null 51 | }; 52 | } 53 | // in other case, idle field remains true and no update to lastActivity. 54 | 55 | Meteor.users.update(advice.userId, update); 56 | }); 57 | 58 | statusEvents.on('connectionLogout', (advice) => { 59 | const conns = UserConnections.find({ 60 | userId: advice.userId 61 | }).fetch(); 62 | if (conns.length === 0) { 63 | // Go offline if we are the last connection for this user 64 | // This includes removing all idle information 65 | Meteor.users.update(advice.userId, { 66 | $set: { 67 | 'status.online': false 68 | }, 69 | $unset: { 70 | 'status.idle': null, 71 | 'status.lastActivity': null 72 | } 73 | }); 74 | } else if (conns.every(c => c.idle)) { 75 | /* 76 | All remaining connections are idle: 77 | - If the last active connection quit, then we should go idle with the most recent activity 78 | 79 | - If an idle connection quit, nothing should happen; specifically, if the 80 | most recently active idle connection quit, we shouldn't tick the value backwards. 81 | This may result in a no-op so we can be smart and skip the update. 82 | */ 83 | if (advice.lastActivity != null) { 84 | return; 85 | } // The dropped connection was already idle 86 | 87 | const latestLastActivity = new Date(Math.max(...conns.map(conn => conn.lastActivity))); 88 | Meteor.users.update(advice.userId, { 89 | $set: { 90 | 'status.idle': true, 91 | 'status.lastActivity': latestLastActivity 92 | } 93 | }); 94 | } 95 | }); 96 | 97 | /* 98 | Multiplex idle/active events to status.idle 99 | TODO: Hopefully this is quick because it's all in memory, but we can use indices if it turns out to be slow 100 | 101 | TODO: There is a race condition when switching between tabs, leaving the user inactive while idle goes from one tab to the other. 102 | It can probably be smoothed out. 103 | */ 104 | statusEvents.on('connectionIdle', (advice) => { 105 | const conns = UserConnections.find({ 106 | userId: advice.userId 107 | }).fetch(); 108 | if (!conns.every(c => c.idle)) { 109 | return; 110 | } 111 | // Set user to idle if all the connections are idle 112 | // This will not be the most recent idle across a disconnection, so we use max 113 | 114 | // TODO: the race happens here where everyone was idle when we looked for them but now one of them isn't. 115 | const latestLastActivity = new Date(Math.max(...conns.map(conn => conn.lastActivity))); 116 | Meteor.users.update(advice.userId, { 117 | $set: { 118 | 'status.idle': true, 119 | 'status.lastActivity': latestLastActivity 120 | } 121 | }); 122 | }); 123 | 124 | statusEvents.on('connectionActive', (advice) => { 125 | Meteor.users.update(advice.userId, { 126 | $set: { 127 | 'status.idle': false 128 | }, 129 | $unset: { 130 | 'status.lastActivity': null 131 | } 132 | }); 133 | }); 134 | 135 | // Reset online status on startup (users will reconnect) 136 | const onStartup = (selector) => { 137 | if (selector == null) { 138 | selector = Meteor?.settings?.packages?.['mizzao:user-status']?.startupQuerySelector || {}; 139 | } 140 | return Meteor.users.update(selector, { 141 | $set: { 142 | 'status.online': false 143 | }, 144 | $unset: { 145 | 'status.idle': null, 146 | 'status.lastActivity': null 147 | } 148 | }, { 149 | multi: true 150 | }); 151 | }; 152 | 153 | /* 154 | Local session modification functions - also used in testing 155 | */ 156 | 157 | const addSession = (connection) => { 158 | UserConnections.upsert(connection.id, { 159 | $set: { 160 | ipAddr: connection.clientAddress, 161 | userAgent: connection.httpHeaders['user-agent'] 162 | } 163 | }); 164 | }; 165 | 166 | const loginSession = (connection, date, userId) => { 167 | UserConnections.upsert(connection.id, { 168 | $set: { 169 | userId, 170 | loginTime: date 171 | } 172 | }); 173 | 174 | statusEvents.emit('connectionLogin', { 175 | userId, 176 | connectionId: connection.id, 177 | ipAddr: connection.clientAddress, 178 | userAgent: connection.httpHeaders['user-agent'], 179 | loginTime: date 180 | }); 181 | }; 182 | 183 | // Possibly trigger a logout event if this connection was previously associated with a user ID 184 | const tryLogoutSession = (connection, date) => { 185 | let conn; 186 | if ((conn = UserConnections.findOne({ 187 | _id: connection.id, 188 | userId: { 189 | $exists: true 190 | } 191 | })) == null) { 192 | return false; 193 | } 194 | 195 | // Yes, this is actually a user logging out. 196 | UserConnections.upsert(connection.id, { 197 | $unset: { 198 | userId: null, 199 | loginTime: null 200 | } 201 | }); 202 | 203 | return statusEvents.emit('connectionLogout', { 204 | userId: conn.userId, 205 | connectionId: connection.id, 206 | lastActivity: conn.lastActivity, // If this connection was idle, pass the last activity we saw 207 | logoutTime: date 208 | }); 209 | }; 210 | 211 | const removeSession = (connection, date) => { 212 | tryLogoutSession(connection, date); 213 | UserConnections.remove(connection.id); 214 | }; 215 | 216 | const idleSession = (connection, date, userId) => { 217 | UserConnections.update(connection.id, { 218 | $set: { 219 | idle: true, 220 | lastActivity: date 221 | } 222 | }); 223 | 224 | statusEvents.emit('connectionIdle', { 225 | userId, 226 | connectionId: connection.id, 227 | lastActivity: date 228 | }); 229 | }; 230 | 231 | const activeSession = (connection, date, userId) => { 232 | UserConnections.update(connection.id, { 233 | $set: { 234 | idle: false 235 | }, 236 | $unset: { 237 | lastActivity: null 238 | } 239 | }); 240 | 241 | statusEvents.emit('connectionActive', { 242 | userId, 243 | connectionId: connection.id, 244 | lastActivity: date 245 | }); 246 | }; 247 | 248 | /* 249 | Handlers for various client-side events 250 | */ 251 | Meteor.startup(onStartup); 252 | 253 | // Opening and closing of DDP connections 254 | Meteor.onConnection((connection) => { 255 | addSession(connection); 256 | 257 | return connection.onClose(() => removeSession(connection, new Date())); 258 | }); 259 | 260 | // Authentication of a DDP connection 261 | Accounts.onLogin(info => loginSession(info.connection, new Date(), info.user._id)); 262 | 263 | // pub/sub trick as referenced in http://stackoverflow.com/q/10257958/586086 264 | // We used this in the past, but still need this to detect logouts on the same connection. 265 | Meteor.publish(null, function () { 266 | // Return null explicitly if this._session is not available, i.e.: 267 | // https://github.com/arunoda/meteor-fast-render/issues/41 268 | if (this._session == null) { 269 | return []; 270 | } 271 | 272 | // We're interested in logout events - re-publishes for which a past connection exists 273 | if (this.userId == null) { 274 | tryLogoutSession(this._session.connectionHandle, new Date()); 275 | } 276 | 277 | return []; 278 | }); 279 | 280 | // We can use the client's timestamp here because it was sent from a TimeSync 281 | // value, however we should never trust it for something security dependent. 282 | // If timestamp is not provided (probably due to a desync), use server time. 283 | Meteor.methods({ 284 | 'user-status-idle'(timestamp) { 285 | check(timestamp, Match.OneOf(null, undefined, Date, Number)); 286 | 287 | const date = (timestamp != null) ? new Date(timestamp) : new Date(); 288 | idleSession(this.connection, date, this.userId); 289 | }, 290 | 291 | 'user-status-active'(timestamp) { 292 | check(timestamp, Match.OneOf(null, undefined, Date, Number)); 293 | 294 | // We only use timestamp because it's when we saw activity *on the client* 295 | // as opposed to just being notified it. It is probably more accurate even if 296 | // a dozen ms off due to the latency of sending it to the server. 297 | const date = (timestamp != null) ? new Date(timestamp) : new Date(); 298 | activeSession(this.connection, date, this.userId); 299 | } 300 | }); 301 | 302 | // Exported variable 303 | export const UserStatus = { 304 | connections: UserConnections, 305 | events: statusEvents 306 | }; 307 | 308 | // Internal functions, exported for testing 309 | export const StatusInternals = { 310 | onStartup, 311 | addSession, 312 | removeSession, 313 | loginSession, 314 | tryLogoutSession, 315 | idleSession, 316 | activeSession, 317 | }; 318 | -------------------------------------------------------------------------------- /tests/client_tests.js: -------------------------------------------------------------------------------- 1 | /* globals navigator, Package, Tinytest */ 2 | 3 | import { Meteor } from 'meteor/meteor'; 4 | import { TimeSync } from 'meteor/mizzao:timesync'; 5 | import { InsecureLogin } from './insecure_login'; 6 | 7 | // The maximum tolerance we expect in client-server tests 8 | // TODO why must this be so large? 9 | const timeTol = 1000; 10 | let loginTime = null; 11 | let idleTime = null; 12 | 13 | // Monitor tests will wait for timesync, so we don't need to here. 14 | Tinytest.addAsync('status - login', (test, next) => InsecureLogin.ready(() => { 15 | test.ok(); 16 | loginTime = new Date(TimeSync.serverTime()); 17 | return next(); 18 | })); 19 | 20 | // Check that initialization is empty 21 | Tinytest.addAsync('status - online recorded on server', (test, next) => Meteor.call('grabStatus', function (err, res) { 22 | test.isUndefined(err); 23 | test.length(res, 1); 24 | 25 | const user = res[0]; 26 | test.equal(user._id, Meteor.userId()); 27 | test.equal(user.status.online, true); 28 | 29 | test.isTrue(Math.abs(user.status.lastLogin.date - loginTime) < timeTol); 30 | 31 | // TODO: user-agent doesn't seem to match up in phantomjs for some reason 32 | if (Package['test-in-console'] == null) { 33 | test.equal(user.status.lastLogin.userAgent, navigator.userAgent); 34 | } 35 | 36 | test.equal(user.status.idle, false); 37 | test.isFalse(user.status.lastActivity != null); 38 | 39 | return next(); 40 | })); 41 | 42 | Tinytest.addAsync('status - session recorded on server', (test, next) => Meteor.call('grabSessions', function (err, res) { 43 | test.isUndefined(err); 44 | test.length(res, 1); 45 | 46 | const doc = res[0]; 47 | test.equal(doc.userId, Meteor.userId()); 48 | test.isTrue(doc.ipAddr != null); 49 | test.isTrue(Math.abs(doc.loginTime - loginTime) < timeTol); 50 | 51 | // This shit doesn't seem to work properly in PhantomJS on Travis 52 | if (Package['test-in-console'] == null) { 53 | test.equal(doc.userAgent, navigator.userAgent); 54 | } 55 | 56 | test.isFalse(doc.idle != null); // connection record, not user 57 | test.isFalse(doc.lastActivity != null); 58 | 59 | return next(); 60 | })); 61 | 62 | Tinytest.addAsync('status - online recorded on client', (test, next) => { 63 | test.equal(Meteor.user().status.online, true); 64 | return next(); 65 | }); 66 | 67 | Tinytest.addAsync('status - idle report to server', (test, next) => { 68 | const now = TimeSync.serverTime(); 69 | idleTime = new Date(now); 70 | 71 | return Meteor.call('user-status-idle', now, (err) => { 72 | test.isUndefined(err); 73 | 74 | // Testing grabStatus should be sufficient to ensure that sessions work 75 | return Meteor.call('grabStatus', function (err, res) { 76 | test.isUndefined(err); 77 | test.length(res, 1); 78 | 79 | const user = res[0]; 80 | test.equal(user._id, Meteor.userId()); 81 | test.equal(user.status.online, true); 82 | test.equal(user.status.idle, true); 83 | test.isTrue(user.status.lastLogin != null); 84 | // This should be the exact date we sent to the server 85 | test.equal(user.status.lastActivity, idleTime); 86 | 87 | return next(); 88 | }); 89 | }); 90 | }); 91 | 92 | Tinytest.addAsync('status - active report to server', (test, next) => { 93 | const now = TimeSync.serverTime(); 94 | 95 | return Meteor.call('user-status-active', now, (err) => { 96 | test.isUndefined(err); 97 | 98 | return Meteor.call('grabStatus', function (err, res) { 99 | test.isUndefined(err); 100 | test.length(res, 1); 101 | 102 | const user = res[0]; 103 | test.equal(user._id, Meteor.userId()); 104 | test.equal(user.status.online, true); 105 | test.isTrue(user.status.lastLogin != null); 106 | 107 | test.equal(user.status.idle, false); 108 | test.isFalse(user.status.lastActivity != null); 109 | 110 | return next(); 111 | }); 112 | }); 113 | }); 114 | 115 | Tinytest.addAsync('status - idle report with no timestamp', (test, next) => { 116 | const now = TimeSync.serverTime(); 117 | idleTime = new Date(now); 118 | 119 | return Meteor.call('user-status-idle', undefined, (err) => { 120 | test.isUndefined(err); 121 | 122 | return Meteor.call('grabStatus', function (err, res) { 123 | test.isUndefined(err); 124 | test.length(res, 1); 125 | 126 | const user = res[0]; 127 | test.equal(user._id, Meteor.userId()); 128 | test.equal(user.status.online, true); 129 | test.equal(user.status.idle, true); 130 | test.isTrue(user.status.lastLogin != null); 131 | // This will be approximate 132 | test.isTrue(Math.abs(user.status.lastActivity - idleTime) < timeTol); 133 | 134 | return next(); 135 | }); 136 | }); 137 | }); 138 | 139 | Tinytest.addAsync('status - active report with no timestamp', (test, next) => Meteor.call('user-status-active', undefined, (err) => { 140 | test.isUndefined(err); 141 | 142 | return Meteor.call('grabStatus', function (err, res) { 143 | test.isUndefined(err); 144 | test.length(res, 1); 145 | 146 | const user = res[0]; 147 | test.equal(user._id, Meteor.userId()); 148 | test.equal(user.status.online, true); 149 | test.isTrue(user.status.lastLogin != null); 150 | 151 | test.equal(user.status.idle, false); 152 | test.isFalse(user.status.lastActivity != null); 153 | 154 | return next(); 155 | }); 156 | })); 157 | 158 | Tinytest.addAsync('status - logout', (test, next) => Meteor.logout((err) => { 159 | test.isUndefined(err); 160 | return next(); 161 | })); 162 | 163 | Tinytest.addAsync('status - offline recorded on server', (test, next) => Meteor.call('grabStatus', function (err, res) { 164 | test.isUndefined(err); 165 | test.length(res, 1); 166 | 167 | const user = res[0]; 168 | test.isTrue(user._id != null); 169 | test.equal(user.status.online, false); 170 | // logintime is still maintained 171 | test.isTrue(user.status.lastLogin != null); 172 | 173 | test.isFalse(user.status.idle != null); 174 | test.isFalse(user.status.lastActivity != null); 175 | 176 | return next(); 177 | })); 178 | 179 | Tinytest.addAsync('status - session userId deleted on server', (test, next) => Meteor.call('grabSessions', function (err, res) { 180 | test.isUndefined(err); 181 | test.length(res, 1); 182 | 183 | const doc = res[0]; 184 | test.isFalse(doc.userId != null); 185 | test.isTrue(doc.ipAddr != null); 186 | test.isFalse(doc.loginTime != null); 187 | 188 | test.isFalse(doc.idle); // === false 189 | test.isFalse(doc.lastActivity != null); 190 | 191 | return next(); 192 | })); 193 | -------------------------------------------------------------------------------- /tests/insecure_login.js: -------------------------------------------------------------------------------- 1 | import { Accounts } from 'meteor/accounts-base'; 2 | import { Meteor } from 'meteor/meteor'; 3 | 4 | /* 5 | * Created by https://github.com/matb33 for testing packages that make user of userIds 6 | * Original file https://github.com/matb33/meteor-collection-hooks/blob/master/tests/insecure_login.js 7 | */ 8 | export const InsecureLogin = { 9 | queue: [], 10 | ran: false, 11 | ready: (callback) => { 12 | InsecureLogin.queue.push(callback); 13 | if (InsecureLogin.ran) InsecureLogin.unwind(); 14 | }, 15 | run: () => { 16 | InsecureLogin.ran = true; 17 | InsecureLogin.unwind(); 18 | }, 19 | unwind: () => { 20 | for (const callback of InsecureLogin.queue) { 21 | callback(); 22 | } 23 | InsecureLogin.queue = []; 24 | } 25 | }; 26 | 27 | if (Meteor.isClient) { 28 | Accounts.callLoginMethod({ 29 | methodArguments: [{ 30 | username: 'InsecureLogin' 31 | }], 32 | userCallback: (err) => { 33 | if (err) throw err; 34 | console.info('Insecure login successful!'); 35 | InsecureLogin.run(); 36 | } 37 | }); 38 | } else { 39 | InsecureLogin.run(); 40 | } 41 | 42 | if (Meteor.isServer) { 43 | if (!Meteor.users.find({ 44 | 'username': 'InsecureLogin' 45 | }).count()) { 46 | Accounts.createUser({ 47 | username: 'InsecureLogin', 48 | email: 'test@test.com', 49 | password: 'password', 50 | profile: { 51 | name: 'InsecureLogin' 52 | } 53 | }); 54 | } 55 | 56 | Accounts.registerLoginHandler((options) => { 57 | if (!options.username) return; 58 | 59 | var user = Meteor.users.findOne({ 60 | 'username': options.username 61 | }); 62 | if (!user) return; 63 | 64 | return { 65 | userId: user._id 66 | }; 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /tests/monitor_tests.js: -------------------------------------------------------------------------------- 1 | /* globals Tinytest */ 2 | import { Meteor } from 'meteor/meteor'; 3 | import { TimeSync } from 'meteor/mizzao:timesync'; 4 | import { Tracker } from 'meteor/tracker'; 5 | import { MonitorInternals, UserStatus } from '../client/monitor'; 6 | import { getCleanupWrapper } from './setup'; 7 | 8 | const tolMs = 100; 9 | const reportedEvents = []; 10 | 11 | // Stub out reporting methods for testing 12 | MonitorInternals.reportIdle = time => reportedEvents.push({ 13 | status: 'idle', 14 | time 15 | }); 16 | 17 | MonitorInternals.reportActive = time => reportedEvents.push({ 18 | status: 'active', 19 | time 20 | }); 21 | 22 | // Test function wrapper that cleans up reported events array 23 | const withCleanup = getCleanupWrapper({ 24 | before() { 25 | MonitorInternals.onWindowFocus(); 26 | MonitorInternals.idleThreshold = null; 27 | MonitorInternals.idleOnBlur = false; 28 | // http://stackoverflow.com/a/1232046/586086 29 | return reportedEvents.length = 0; 30 | }, 31 | 32 | after() { 33 | if (UserStatus.isMonitoring()) { 34 | try { 35 | return UserStatus.stopMonitor(); 36 | } catch (error) { 37 | console.error(error); 38 | } 39 | } 40 | } 41 | }); 42 | 43 | // This is a 2x2 test of all possible cases 44 | 45 | Tinytest.add('monitor - idleOnBlur and window blurred', (test) => { 46 | MonitorInternals.idleThreshold = 60000; 47 | MonitorInternals.idleOnBlur = true; 48 | 49 | const activity = Date.now(); 50 | const newTime = activity + 120000; 51 | 52 | test.equal(MonitorInternals.computeState(activity, activity, false), true); 53 | // Should not change if we go idle 54 | return test.equal(MonitorInternals.computeState(activity, newTime, false), true); 55 | }); 56 | 57 | Tinytest.add('monitor - idleOnBlur and window focused', (test) => { 58 | MonitorInternals.idleThreshold = 60000; 59 | MonitorInternals.idleOnBlur = true; 60 | 61 | const activity = Date.now(); 62 | const newTime = activity + 120000; 63 | 64 | test.equal(MonitorInternals.computeState(activity, activity, true), false); 65 | // Should change if we go idle 66 | return test.equal(MonitorInternals.computeState(activity, newTime, true), true); 67 | }); 68 | 69 | Tinytest.add('monitor - idle below threshold', (test) => { 70 | MonitorInternals.idleThreshold = 60000; 71 | MonitorInternals.idleOnBlur = false; 72 | 73 | const activity = Date.now(); 74 | test.equal(MonitorInternals.computeState(activity, activity, true), false); 75 | // Shouldn't change if we go out of focus 76 | return test.equal(MonitorInternals.computeState(activity, activity, false), false); 77 | }); 78 | 79 | Tinytest.add('monitor - idle above threshold', (test) => { 80 | MonitorInternals.idleThreshold = 60000; 81 | MonitorInternals.idleOnBlur = false; 82 | 83 | const activity = Date.now(); 84 | const newTime = activity + 120000; 85 | 86 | test.equal(MonitorInternals.computeState(activity, newTime, true), true); 87 | // Shouldn't change if we go out of focus 88 | return test.equal(MonitorInternals.computeState(activity, newTime, false), true); 89 | }); 90 | 91 | // We need to wait for TimeSync to run or errors will ensue. 92 | Tinytest.addAsync('monitor - wait for timesync', (test, next) => Tracker.autorun((c) => { 93 | if (TimeSync.isSynced()) { 94 | c.stop(); 95 | return next(); 96 | } 97 | })); 98 | 99 | Tinytest.add('monitor - start with default settings', withCleanup((test) => { 100 | UserStatus.startMonitor(); 101 | 102 | test.equal(UserStatus.isMonitoring(), true); 103 | test.isTrue(UserStatus.lastActivity()); 104 | 105 | test.equal(MonitorInternals.idleThreshold, 60000); 106 | return test.equal(MonitorInternals.idleOnBlur, false); 107 | })); 108 | 109 | Tinytest.add('monitor - start with window focused, and idleOnBlur = false', withCleanup((test) => { 110 | UserStatus.startMonitor({ 111 | threshold: 30000, 112 | idleOnBlur: false 113 | }); 114 | 115 | const timestamp = Tracker.nonreactive(() => TimeSync.serverTime()); 116 | 117 | test.equal(MonitorInternals.idleThreshold, 30000); 118 | test.equal(MonitorInternals.idleOnBlur, false); 119 | 120 | Tracker.flush(); 121 | 122 | test.equal(UserStatus.isIdle(), false); 123 | test.isTrue(Math.abs(UserStatus.lastActivity() - timestamp) < tolMs); 124 | test.length(reportedEvents, 1); 125 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'active'); 126 | return test.isTrue(Math.abs((reportedEvents[0] != null ? reportedEvents[0].time : undefined) - timestamp) < tolMs); 127 | })); 128 | 129 | Tinytest.add('monitor - start with window focused, and idleOnBlur = true', withCleanup((test) => { 130 | UserStatus.startMonitor({ 131 | threshold: 30000, 132 | idleOnBlur: true 133 | }); 134 | 135 | const timestamp = Tracker.nonreactive(() => TimeSync.serverTime()); 136 | 137 | test.equal(MonitorInternals.idleThreshold, 30000); 138 | test.equal(MonitorInternals.idleOnBlur, true); 139 | 140 | Tracker.flush(); 141 | 142 | test.equal(UserStatus.isIdle(), false); 143 | test.isTrue(Math.abs(UserStatus.lastActivity() - timestamp) < tolMs); 144 | test.length(reportedEvents, 1); 145 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'active'); 146 | return test.isTrue(Math.abs((reportedEvents[0] != null ? reportedEvents[0].time : undefined) - timestamp) < tolMs); 147 | })); 148 | 149 | Tinytest.add('monitor - start with window blurred, and idleOnBlur = false', withCleanup((test) => { 150 | MonitorInternals.onWindowBlur(); 151 | 152 | UserStatus.startMonitor({ 153 | threshold: 30000, 154 | idleOnBlur: false 155 | }); 156 | 157 | const timestamp = Tracker.nonreactive(() => TimeSync.serverTime()); 158 | 159 | test.equal(MonitorInternals.idleThreshold, 30000); 160 | test.equal(MonitorInternals.idleOnBlur, false); 161 | 162 | Tracker.flush(); 163 | 164 | test.equal(UserStatus.isIdle(), false); 165 | test.isTrue(Math.abs(UserStatus.lastActivity() - timestamp) < tolMs); 166 | test.length(reportedEvents, 1); 167 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'active'); 168 | return test.isTrue(Math.abs((reportedEvents[0] != null ? reportedEvents[0].time : undefined) - timestamp) < tolMs); 169 | })); 170 | 171 | Tinytest.add('monitor - start with window blurred, and idleOnBlur = true', withCleanup((test) => { 172 | MonitorInternals.onWindowBlur(); 173 | 174 | UserStatus.startMonitor({ 175 | threshold: 30000, 176 | idleOnBlur: true 177 | }); 178 | 179 | const timestamp = Tracker.nonreactive(() => TimeSync.serverTime()); 180 | 181 | test.equal(MonitorInternals.idleThreshold, 30000); 182 | test.equal(MonitorInternals.idleOnBlur, true); 183 | 184 | Tracker.flush(); 185 | 186 | test.equal(UserStatus.isIdle(), true); 187 | test.isTrue(Math.abs(UserStatus.lastActivity() - timestamp) < tolMs); 188 | test.length(reportedEvents, 1); 189 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'idle'); 190 | return test.isTrue(Math.abs((reportedEvents[0] != null ? reportedEvents[0].time : undefined) - timestamp) < tolMs); 191 | })); 192 | 193 | Tinytest.add('monitor - ignore actions when window is blurred with idleOnBlur = true', withCleanup((test) => { 194 | 195 | UserStatus.startMonitor({ 196 | idleOnBlur: true 197 | }); 198 | Tracker.flush(); 199 | 200 | const startTime = UserStatus.lastActivity(); 201 | 202 | test.length(reportedEvents, 1); 203 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'active'); 204 | test.isTrue(Math.abs((reportedEvents[0] != null ? reportedEvents[0].time : undefined) - startTime) < tolMs); 205 | 206 | MonitorInternals.onWindowBlur(); 207 | Tracker.flush(); 208 | 209 | test.length(reportedEvents, 2); 210 | test.equal(reportedEvents[1] != null ? reportedEvents[1].status : undefined, 'idle'); 211 | 212 | UserStatus.pingMonitor(); 213 | 214 | // Shouldn't have changed 215 | test.length(reportedEvents, 2); 216 | return test.equal(UserStatus.lastActivity(), startTime); 217 | })); 218 | 219 | Tinytest.add('monitor - stopping while idle unsets idle state', withCleanup((test) => { 220 | 221 | UserStatus.startMonitor({ 222 | idleOnBlur: true 223 | }); 224 | Tracker.flush(); 225 | 226 | test.length(reportedEvents, 1); 227 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'active'); 228 | 229 | const startTime = UserStatus.lastActivity(); 230 | 231 | // Simulate going idle immediately using blur 232 | MonitorInternals.onWindowBlur(); 233 | Tracker.flush(); 234 | 235 | test.length(reportedEvents, 2); 236 | test.equal(reportedEvents[1] != null ? reportedEvents[1].status : undefined, 'idle'); 237 | 238 | test.equal(UserStatus.isIdle(), true); 239 | test.equal(UserStatus.lastActivity(), startTime); 240 | 241 | UserStatus.stopMonitor(); 242 | Tracker.flush(); 243 | 244 | test.length(reportedEvents, 3); 245 | test.equal(reportedEvents[2] != null ? reportedEvents[2].status : undefined, 'active'); 246 | 247 | test.equal(UserStatus.isIdle(), false); 248 | return test.equal(UserStatus.lastActivity(), undefined); 249 | })); 250 | 251 | Tinytest.addAsync('monitor - stopping and restarting grabs new start time', withCleanup((test, next) => { 252 | 253 | UserStatus.startMonitor({ 254 | idleOnBlur: true 255 | }); 256 | Tracker.flush(); 257 | 258 | const start1 = UserStatus.lastActivity(); 259 | 260 | UserStatus.stopMonitor(); 261 | Tracker.flush(); 262 | 263 | return Meteor.setTimeout(() => { 264 | UserStatus.startMonitor({ 265 | idleOnBlur: true 266 | }); 267 | Tracker.flush(); 268 | 269 | const start2 = UserStatus.lastActivity(); 270 | 271 | test.isTrue(start2 > start1); 272 | return next(); 273 | }, 50); 274 | })); 275 | 276 | Tinytest.add('monitor - idle state reported across a disconnect', withCleanup((test) => { 277 | 278 | UserStatus.startMonitor({ 279 | idleOnBlur: true 280 | }); 281 | Tracker.flush(); 282 | 283 | test.length(reportedEvents, 1); 284 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'active'); 285 | 286 | // Simulate going idle immediately using blur 287 | MonitorInternals.onWindowBlur(); 288 | Tracker.flush(); 289 | 290 | test.length(reportedEvents, 2); 291 | test.equal(reportedEvents[1] != null ? reportedEvents[1].status : undefined, 'idle'); 292 | 293 | MonitorInternals.connectionChange(false, true); 294 | MonitorInternals.connectionChange(true, false); 295 | 296 | test.length(reportedEvents, 3); 297 | return test.equal(reportedEvents[2] != null ? reportedEvents[2].status : undefined, 'idle'); 298 | })); 299 | -------------------------------------------------------------------------------- /tests/server_tests.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { UserStatus } from '../server/status'; 3 | import { TEST_IP, TEST_userId } from './setup'; 4 | 5 | /* 6 | Manual tests to do: 7 | 8 | logged out -> logged in 9 | logged in -> logged out 10 | logged in -> close session -> reopen 11 | logged in -> connection timeout 12 | */ 13 | 14 | // Publish status to client 15 | Meteor.publish(null, () => Meteor.users.find({}, { 16 | fields: { 17 | status: 1 18 | } 19 | })); 20 | 21 | Meteor.methods({ 22 | 'grabStatus'() { 23 | return Meteor.users.find({ 24 | _id: { 25 | $ne: TEST_userId 26 | } 27 | }, { 28 | fields: { 29 | status: 1 30 | } 31 | }).fetch(); 32 | }, 33 | 'grabSessions'() { 34 | // Only grab sessions not generated by server-side tests. 35 | return UserStatus.connections.find({ 36 | $and: [{ 37 | userId: { 38 | $ne: TEST_userId 39 | } 40 | }, 41 | { 42 | ipAddr: { 43 | $ne: TEST_IP 44 | } 45 | } 46 | ] 47 | }).fetch(); 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-catch */ 2 | import { Meteor } from 'meteor/meteor'; 3 | 4 | export const TEST_username = 'status_test'; 5 | export let TEST_userId = ''; 6 | export const TEST_IP = '255.255.255.0'; 7 | 8 | if (Meteor.isServer) { 9 | const testUserExists = Meteor.users.findOne({ 10 | username: TEST_username 11 | }); 12 | 13 | if (!testUserExists) { 14 | TEST_userId = Meteor.users.insert({ 15 | username: TEST_username 16 | }); 17 | console.log('Inserted test user id: ', TEST_userId); 18 | } else { 19 | TEST_userId = testUserExists._id; 20 | } 21 | } 22 | 23 | // Get a wrapper that runs a before and after function wrapping some test function. 24 | export const getCleanupWrapper = function (settings) { 25 | const { before } = settings; 26 | const { after } = settings; 27 | // Take a function... 28 | return fn => // Return a function that, when called, executes the hooks around the function. 29 | (function () { 30 | const next = arguments[1]; 31 | if (typeof before === 'function') { 32 | before(); 33 | } 34 | 35 | if (next == null) { 36 | // Synchronous version - Tinytest.add 37 | try { 38 | return fn.apply(this, arguments); 39 | } catch (error) { 40 | throw error; 41 | } finally { 42 | if (typeof after === 'function') { 43 | after(); 44 | } 45 | } 46 | } else { 47 | // Asynchronous version - Tinytest.addAsync 48 | const hookedNext = function () { 49 | if (typeof after === 'function') { 50 | after(); 51 | } 52 | return next(); 53 | }; 54 | return fn.call(this, arguments[0], hookedNext); 55 | } 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /tests/status_tests.js: -------------------------------------------------------------------------------- 1 | /* globals Tinytest */ 2 | import { Meteor } from 'meteor/meteor'; 3 | import { Random } from 'meteor/random'; 4 | import { Tracker } from 'meteor/tracker'; 5 | import { StatusInternals, UserStatus } from '../server/status'; 6 | import { getCleanupWrapper, TEST_IP, TEST_userId } from './setup'; 7 | 8 | let lastLoginAdvice = null; 9 | let lastLogoutAdvice = null; 10 | let lastIdleAdvice = null; 11 | let lastActiveAdvice = null; 12 | 13 | // Record events for tests 14 | UserStatus.events.on('connectionLogin', advice => lastLoginAdvice = advice); 15 | UserStatus.events.on('connectionLogout', advice => lastLogoutAdvice = advice); 16 | UserStatus.events.on('connectionIdle', advice => lastIdleAdvice = advice); 17 | UserStatus.events.on('connectionActive', advice => lastActiveAdvice = advice); 18 | 19 | const TEST_UA = 'old-ass browser'; 20 | 21 | // Make sure repeated calls to this return different values 22 | const delayedDate = () => Meteor.wrapAsync(cb => Meteor.setTimeout((() => cb(undefined, new Date())), 1))(); 23 | 24 | const randomConnection = () => ({ 25 | id: Random.id(), 26 | clientAddress: TEST_IP, 27 | 28 | httpHeaders: { 29 | 'user-agent': TEST_UA 30 | } 31 | }); 32 | 33 | // Delete the entire status field and sessions after each test 34 | const withCleanup = getCleanupWrapper({ 35 | after() { 36 | lastLoginAdvice = null; 37 | lastLogoutAdvice = null; 38 | lastIdleAdvice = null; 39 | lastActiveAdvice = null; 40 | 41 | Meteor.users.update(TEST_userId, { 42 | $unset: { 43 | status: null 44 | } 45 | }); 46 | UserStatus.connections.remove({ 47 | $or: [{ 48 | userId: TEST_userId 49 | }, 50 | { 51 | ipAddr: TEST_IP 52 | } 53 | ] 54 | }); 55 | 56 | return Tracker.flush(); 57 | } 58 | }); 59 | 60 | // Clean up before we add any tests just in case some crap left over from before 61 | withCleanup(function () {}); 62 | 63 | Tinytest.add('status - adding anonymous session', withCleanup((test) => { 64 | const conn = randomConnection(); 65 | 66 | StatusInternals.addSession(conn); 67 | 68 | const doc = UserStatus.connections.findOne(conn.id); 69 | 70 | test.isTrue(doc != null); 71 | test.equal(doc._id, conn.id); 72 | test.equal(doc.ipAddr, TEST_IP); 73 | test.equal(doc.userAgent, TEST_UA); 74 | test.isFalse(doc.userId); 75 | return test.isFalse(doc.loginTime); 76 | })); 77 | 78 | Tinytest.add('status - adding and removing anonymous session', withCleanup((test) => { 79 | const conn = randomConnection(); 80 | 81 | StatusInternals.addSession(conn); 82 | StatusInternals.removeSession(conn, delayedDate()); 83 | 84 | return test.isFalse(UserStatus.connections.findOne(conn.id)); 85 | })); 86 | 87 | Tinytest.add('status - adding one authenticated session', withCleanup((test) => { 88 | const conn = randomConnection(); 89 | const ts = delayedDate(); 90 | 91 | StatusInternals.addSession(conn); 92 | StatusInternals.loginSession(conn, ts, TEST_userId); 93 | 94 | const doc = UserStatus.connections.findOne(conn.id); 95 | const user = Meteor.users.findOne(TEST_userId); 96 | 97 | test.isTrue(doc != null); 98 | test.equal(doc._id, conn.id); 99 | test.equal(doc.userId, TEST_userId); 100 | test.equal(doc.loginTime, ts); 101 | test.equal(doc.ipAddr, TEST_IP); 102 | test.equal(doc.userAgent, TEST_UA); 103 | 104 | test.equal(lastLoginAdvice.userId, TEST_userId); 105 | test.equal(lastLoginAdvice.connectionId, conn.id); 106 | test.equal(lastLoginAdvice.loginTime, ts); 107 | test.equal(lastLoginAdvice.ipAddr, TEST_IP); 108 | test.equal(lastLoginAdvice.userAgent, TEST_UA); 109 | 110 | test.equal(user.status.online, true); 111 | test.equal(user.status.idle, false); 112 | test.equal(user.status.lastLogin.date, ts); 113 | return test.equal(user.status.lastLogin.userAgent, TEST_UA); 114 | })); 115 | 116 | Tinytest.add('status - adding and removing one authenticated session', withCleanup((test) => { 117 | const conn = randomConnection(); 118 | const ts = delayedDate(); 119 | 120 | StatusInternals.addSession(conn); 121 | StatusInternals.loginSession(conn, ts, TEST_userId); 122 | 123 | const logoutTime = delayedDate(); 124 | StatusInternals.removeSession(conn, logoutTime); 125 | 126 | const doc = UserStatus.connections.findOne(conn.id); 127 | const user = Meteor.users.findOne(TEST_userId); 128 | 129 | test.isFalse(doc != null); 130 | 131 | test.equal(lastLogoutAdvice.userId, TEST_userId); 132 | test.equal(lastLogoutAdvice.connectionId, conn.id); 133 | test.equal(lastLogoutAdvice.logoutTime, logoutTime); 134 | test.isFalse(lastLogoutAdvice.lastActivity != null); 135 | 136 | test.equal(user.status.online, false); 137 | test.isFalse(user.status.idle != null); 138 | return test.equal(user.status.lastLogin.date, ts); 139 | })); 140 | 141 | Tinytest.add('status - logout and then close one authenticated session', withCleanup((test) => { 142 | const conn = randomConnection(); 143 | const ts = delayedDate(); 144 | 145 | StatusInternals.addSession(conn); 146 | StatusInternals.loginSession(conn, ts, TEST_userId); 147 | 148 | const logoutTime = delayedDate(); 149 | StatusInternals.tryLogoutSession(conn, logoutTime); 150 | 151 | test.equal(lastLogoutAdvice.userId, TEST_userId); 152 | test.equal(lastLogoutAdvice.connectionId, conn.id); 153 | test.equal(lastLogoutAdvice.logoutTime, logoutTime); 154 | test.isFalse(lastLogoutAdvice.lastActivity != null); 155 | 156 | lastLogoutAdvice = null; 157 | // After logging out, the user closes the browser, which triggers a close callback 158 | // However, the event should not be emitted again 159 | const closeTime = delayedDate(); 160 | StatusInternals.removeSession(conn, closeTime); 161 | 162 | const doc = UserStatus.connections.findOne(conn.id); 163 | const user = Meteor.users.findOne(TEST_userId); 164 | 165 | test.isFalse(doc != null); 166 | test.isFalse(lastLogoutAdvice != null); 167 | 168 | test.equal(user.status.online, false); 169 | test.isFalse(user.status.idle != null); 170 | return test.equal(user.status.lastLogin.date, ts); 171 | })); 172 | 173 | Tinytest.add('status - idling one authenticated session', withCleanup((test) => { 174 | const conn = randomConnection(); 175 | const ts = delayedDate(); 176 | 177 | StatusInternals.addSession(conn); 178 | StatusInternals.loginSession(conn, ts, TEST_userId); 179 | 180 | const idleTime = delayedDate(); 181 | 182 | StatusInternals.idleSession(conn, idleTime, TEST_userId); 183 | 184 | const doc = UserStatus.connections.findOne(conn.id); 185 | const user = Meteor.users.findOne(TEST_userId); 186 | 187 | test.isTrue(doc != null); 188 | test.equal(doc._id, conn.id); 189 | test.equal(doc.userId, TEST_userId); 190 | test.equal(doc.loginTime, ts); 191 | test.equal(doc.ipAddr, TEST_IP); 192 | test.equal(doc.userAgent, TEST_UA); 193 | test.equal(doc.idle, true); 194 | test.equal(doc.lastActivity, idleTime); 195 | 196 | test.equal(lastIdleAdvice.userId, TEST_userId); 197 | test.equal(lastIdleAdvice.connectionId, conn.id); 198 | test.equal(lastIdleAdvice.lastActivity, idleTime); 199 | 200 | test.equal(user.status.online, true); 201 | test.equal(user.status.lastLogin.date, ts); 202 | test.equal(user.status.idle, true); 203 | return test.equal(user.status.lastActivity, idleTime); 204 | })); 205 | 206 | Tinytest.add('status - idling and reactivating one authenticated session', withCleanup((test) => { 207 | const conn = randomConnection(); 208 | const ts = delayedDate(); 209 | 210 | StatusInternals.addSession(conn); 211 | StatusInternals.loginSession(conn, ts, TEST_userId); 212 | 213 | const idleTime = delayedDate(); 214 | StatusInternals.idleSession(conn, idleTime, TEST_userId); 215 | const activeTime = delayedDate(); 216 | StatusInternals.activeSession(conn, activeTime, TEST_userId); 217 | 218 | const doc = UserStatus.connections.findOne(conn.id); 219 | const user = Meteor.users.findOne(TEST_userId); 220 | 221 | test.isTrue(doc != null); 222 | test.equal(doc._id, conn.id); 223 | test.equal(doc.userId, TEST_userId); 224 | test.equal(doc.loginTime, ts); 225 | test.equal(doc.ipAddr, TEST_IP); 226 | test.equal(doc.userAgent, TEST_UA); 227 | test.equal(doc.idle, false); 228 | test.isFalse(doc.lastActivity != null); 229 | 230 | test.equal(lastActiveAdvice.userId, TEST_userId); 231 | test.equal(lastActiveAdvice.connectionId, conn.id); 232 | test.equal(lastActiveAdvice.lastActivity, activeTime); 233 | 234 | test.equal(user.status.online, true); 235 | test.equal(user.status.lastLogin.date, ts); 236 | test.equal(user.status.idle, false); 237 | return test.isFalse(user.status.lastActivity != null); 238 | })); 239 | 240 | Tinytest.add('status - idling and removing one authenticated session', withCleanup((test) => { 241 | const conn = randomConnection(); 242 | const ts = delayedDate(); 243 | 244 | StatusInternals.addSession(conn); 245 | StatusInternals.loginSession(conn, ts, TEST_userId); 246 | const idleTime = delayedDate(); 247 | StatusInternals.idleSession(conn, idleTime, TEST_userId); 248 | const logoutTime = delayedDate(); 249 | StatusInternals.removeSession(conn, logoutTime); 250 | 251 | const doc = UserStatus.connections.findOne(conn.id); 252 | const user = Meteor.users.findOne(TEST_userId); 253 | 254 | test.isFalse(doc != null); 255 | 256 | test.equal(lastLogoutAdvice.userId, TEST_userId); 257 | test.equal(lastLogoutAdvice.connectionId, conn.id); 258 | test.equal(lastLogoutAdvice.logoutTime, logoutTime); 259 | test.equal(lastLogoutAdvice.lastActivity, idleTime); 260 | 261 | test.equal(user.status.online, false); 262 | return test.equal(user.status.lastLogin.date, ts); 263 | })); 264 | 265 | Tinytest.add('status - idling and reconnecting one authenticated session', withCleanup((test) => { 266 | const conn = randomConnection(); 267 | const ts = delayedDate(); 268 | 269 | StatusInternals.addSession(conn); 270 | StatusInternals.loginSession(conn, ts, TEST_userId); 271 | const idleTime = delayedDate(); 272 | StatusInternals.idleSession(conn, idleTime, TEST_userId); 273 | 274 | // Session reconnects but was idle 275 | 276 | const discTime = delayedDate(); 277 | StatusInternals.removeSession(conn, discTime); 278 | 279 | const reconn = randomConnection(); 280 | const reconnTime = delayedDate(); 281 | 282 | StatusInternals.addSession(reconn); 283 | StatusInternals.loginSession(reconn, reconnTime, TEST_userId); 284 | StatusInternals.idleSession(reconn, idleTime, TEST_userId); 285 | 286 | const doc = UserStatus.connections.findOne(reconn.id); 287 | const user = Meteor.users.findOne(TEST_userId); 288 | 289 | test.isTrue(doc != null); 290 | test.equal(doc._id, reconn.id); 291 | test.equal(doc.userId, TEST_userId); 292 | test.equal(doc.loginTime, reconnTime); 293 | test.equal(doc.ipAddr, TEST_IP); 294 | test.equal(doc.userAgent, TEST_UA); 295 | test.equal(doc.idle, true); 296 | test.equal(doc.lastActivity, idleTime); 297 | 298 | test.equal(user.status.online, true); 299 | test.equal(user.status.lastLogin.date, reconnTime); 300 | test.equal(user.status.idle, true); 301 | return test.equal(user.status.lastActivity, idleTime); 302 | })); 303 | 304 | Tinytest.add('multiplex - two online sessions', withCleanup((test) => { 305 | const conn = randomConnection(); 306 | 307 | const conn2 = randomConnection(); 308 | 309 | const ts = delayedDate(); 310 | const ts2 = delayedDate(); 311 | 312 | StatusInternals.addSession(conn); 313 | StatusInternals.loginSession(conn, ts, TEST_userId); 314 | 315 | StatusInternals.addSession(conn2); 316 | StatusInternals.loginSession(conn2, ts2, TEST_userId); 317 | 318 | const user = Meteor.users.findOne(TEST_userId); 319 | 320 | test.equal(user.status.online, true); 321 | return test.equal(user.status.lastLogin.date, ts2); 322 | })); 323 | 324 | Tinytest.add('multiplex - two online sessions with one going offline', withCleanup((test) => { 325 | let user; 326 | const conn = randomConnection(); 327 | 328 | const conn2 = randomConnection(); 329 | 330 | const ts = delayedDate(); 331 | const ts2 = delayedDate(); 332 | 333 | StatusInternals.addSession(conn); 334 | StatusInternals.loginSession(conn, ts, TEST_userId); 335 | 336 | StatusInternals.addSession(conn2); 337 | StatusInternals.loginSession(conn2, ts2, TEST_userId); 338 | 339 | StatusInternals.removeSession(conn, delayedDate(), 340 | 341 | (user = Meteor.users.findOne(TEST_userId))); 342 | 343 | test.equal(user.status.online, true); 344 | return test.equal(user.status.lastLogin.date, ts2); 345 | })); 346 | 347 | Tinytest.add('multiplex - two online sessions to offline', withCleanup((test) => { 348 | const conn = randomConnection(); 349 | 350 | const conn2 = randomConnection(); 351 | 352 | const ts = delayedDate(); 353 | const ts2 = delayedDate(); 354 | 355 | StatusInternals.addSession(conn); 356 | StatusInternals.loginSession(conn, ts, TEST_userId); 357 | 358 | StatusInternals.addSession(conn2); 359 | StatusInternals.loginSession(conn2, ts2, TEST_userId); 360 | 361 | StatusInternals.removeSession(conn, delayedDate()); 362 | StatusInternals.removeSession(conn2, delayedDate()); 363 | 364 | const user = Meteor.users.findOne(TEST_userId); 365 | 366 | test.equal(user.status.online, false); 367 | return test.equal(user.status.lastLogin.date, ts2); 368 | })); 369 | 370 | Tinytest.add('multiplex - idling one of two online sessions', withCleanup((test) => { 371 | const conn = randomConnection(); 372 | 373 | const conn2 = randomConnection(); 374 | 375 | const ts = delayedDate(); 376 | const ts2 = delayedDate(); 377 | 378 | StatusInternals.addSession(conn); 379 | StatusInternals.loginSession(conn, ts, TEST_userId); 380 | 381 | StatusInternals.addSession(conn2); 382 | StatusInternals.loginSession(conn2, ts2, TEST_userId); 383 | 384 | const idle1 = delayedDate(); 385 | StatusInternals.idleSession(conn, idle1, TEST_userId); 386 | 387 | const user = Meteor.users.findOne(TEST_userId); 388 | 389 | test.equal(user.status.online, true); 390 | test.equal(user.status.lastLogin.date, ts2); 391 | return test.equal(user.status.idle, false); 392 | })); 393 | 394 | Tinytest.add('multiplex - idling two online sessions', withCleanup((test) => { 395 | const conn = randomConnection(); 396 | 397 | const conn2 = randomConnection(); 398 | 399 | const ts = delayedDate(); 400 | const ts2 = delayedDate(); 401 | 402 | StatusInternals.addSession(conn); 403 | StatusInternals.loginSession(conn, ts, TEST_userId); 404 | 405 | StatusInternals.addSession(conn2); 406 | StatusInternals.loginSession(conn2, ts2, TEST_userId); 407 | 408 | const idle1 = delayedDate(); 409 | const idle2 = delayedDate(); 410 | StatusInternals.idleSession(conn, idle1, TEST_userId); 411 | StatusInternals.idleSession(conn2, idle2, TEST_userId); 412 | 413 | const user = Meteor.users.findOne(TEST_userId); 414 | 415 | test.equal(user.status.online, true); 416 | test.equal(user.status.lastLogin.date, ts2); 417 | test.equal(user.status.idle, true); 418 | return test.equal(user.status.lastActivity, idle2); 419 | })); 420 | 421 | Tinytest.add('multiplex - idling two then reactivating one session', withCleanup((test) => { 422 | const conn = randomConnection(); 423 | 424 | const conn2 = randomConnection(); 425 | 426 | const ts = delayedDate(); 427 | const ts2 = delayedDate(); 428 | 429 | StatusInternals.addSession(conn); 430 | StatusInternals.loginSession(conn, ts, TEST_userId); 431 | 432 | StatusInternals.addSession(conn2); 433 | StatusInternals.loginSession(conn2, ts2, TEST_userId); 434 | 435 | const idle1 = delayedDate(); 436 | const idle2 = delayedDate(); 437 | StatusInternals.idleSession(conn, idle1, TEST_userId); 438 | StatusInternals.idleSession(conn2, idle2, TEST_userId); 439 | 440 | StatusInternals.activeSession(conn, delayedDate(), TEST_userId); 441 | 442 | const user = Meteor.users.findOne(TEST_userId); 443 | 444 | test.equal(user.status.online, true); 445 | test.equal(user.status.lastLogin.date, ts2); 446 | test.equal(user.status.idle, false); 447 | return test.isFalse(user.status.lastActivity != null); 448 | })); 449 | 450 | Tinytest.add('multiplex - logging in while an existing session is idle', withCleanup((test) => { 451 | const conn = randomConnection(); 452 | 453 | const conn2 = randomConnection(); 454 | 455 | const ts = delayedDate(); 456 | const ts2 = delayedDate(); 457 | 458 | StatusInternals.addSession(conn); 459 | StatusInternals.loginSession(conn, ts, TEST_userId); 460 | 461 | const idle1 = delayedDate(); 462 | StatusInternals.idleSession(conn, idle1, TEST_userId); 463 | 464 | StatusInternals.addSession(conn2); 465 | StatusInternals.loginSession(conn2, ts2, TEST_userId); 466 | 467 | const user = Meteor.users.findOne(TEST_userId); 468 | 469 | test.equal(user.status.online, true); 470 | test.equal(user.status.lastLogin.date, ts2); 471 | test.equal(user.status.idle, false); 472 | return test.isFalse(user.status.lastActivity != null); 473 | })); 474 | 475 | Tinytest.add('multiplex - simulate tab switch', withCleanup((test) => { 476 | const conn = randomConnection(); 477 | 478 | const conn2 = randomConnection(); 479 | 480 | const ts = delayedDate(); 481 | const ts2 = delayedDate(); 482 | 483 | // open first tab then becomes idle 484 | StatusInternals.addSession(conn); 485 | StatusInternals.loginSession(conn, ts, TEST_userId); 486 | 487 | const idle1 = delayedDate(); 488 | StatusInternals.idleSession(conn, idle1, TEST_userId); 489 | 490 | // open second tab then becomes idle 491 | StatusInternals.addSession(conn2); 492 | StatusInternals.loginSession(conn2, ts2, TEST_userId); 493 | const idle2 = delayedDate(); 494 | StatusInternals.idleSession(conn2, idle2, TEST_userId); 495 | 496 | // go back to first tab 497 | StatusInternals.activeSession(conn, delayedDate(), TEST_userId); 498 | 499 | const user = Meteor.users.findOne(TEST_userId); 500 | 501 | test.equal(user.status.online, true); 502 | test.equal(user.status.lastLogin.date, ts2); 503 | test.equal(user.status.idle, false); 504 | return test.isFalse(user.status.lastActivity != null); 505 | })); 506 | 507 | // Test for idling one session across a disconnection; not most recent idle time 508 | Tinytest.add('multiplex - disconnection and reconnection while idle', withCleanup((test) => { 509 | const conn = randomConnection(); 510 | 511 | const conn2 = randomConnection(); 512 | 513 | const ts = delayedDate(); 514 | const ts2 = delayedDate(); 515 | 516 | StatusInternals.addSession(conn); 517 | StatusInternals.loginSession(conn, ts, TEST_userId); 518 | 519 | StatusInternals.addSession(conn2); 520 | StatusInternals.loginSession(conn2, ts2, TEST_userId); 521 | 522 | const idle1 = delayedDate(); 523 | StatusInternals.idleSession(conn, idle1, TEST_userId); 524 | const idle2 = delayedDate(); 525 | StatusInternals.idleSession(conn2, idle2, TEST_userId); 526 | 527 | // Second session, which connected later, reconnects but remains idle 528 | StatusInternals.removeSession(conn2, delayedDate(), TEST_userId); 529 | 530 | let user = Meteor.users.findOne(TEST_userId); 531 | 532 | test.equal(user.status.online, true); 533 | test.equal(user.status.lastLogin.date, ts2); 534 | test.equal(user.status.idle, true); 535 | test.equal(user.status.lastActivity, idle2); 536 | 537 | const reconn2 = randomConnection(); 538 | 539 | const ts3 = delayedDate(); 540 | StatusInternals.addSession(reconn2); 541 | StatusInternals.loginSession(reconn2, ts3, TEST_userId); 542 | 543 | StatusInternals.idleSession(reconn2, idle2, TEST_userId); 544 | 545 | user = Meteor.users.findOne(TEST_userId); 546 | 547 | test.equal(user.status.online, true); 548 | test.equal(user.status.lastLogin.date, ts3); 549 | test.equal(user.status.idle, true); 550 | return test.equal(user.status.lastActivity, idle2); 551 | })); 552 | 553 | Tinytest.add('status - user online set to false on startup', withCleanup((test) => { 554 | // special argument to onStartup prevents this from affecting client tests 555 | StatusInternals.onStartup(TEST_userId); 556 | 557 | const userAfterStartup = Meteor.users.findOne(TEST_userId); 558 | // Check reset status 559 | test.equal(userAfterStartup.status.online, false); 560 | test.equal(userAfterStartup.status.idle, undefined); 561 | test.equal(userAfterStartup.status.lastActivity, undefined); 562 | 563 | // Make user come online, then restart the server 564 | const conn = randomConnection(); 565 | const ts = delayedDate(); 566 | 567 | StatusInternals.addSession(conn); 568 | StatusInternals.loginSession(conn, ts, TEST_userId); 569 | 570 | const userAfterLogin = Meteor.users.findOne(TEST_userId); 571 | 572 | test.equal(userAfterLogin.status.online, true); 573 | test.isFalse(userAfterLogin.status.idle); 574 | 575 | StatusInternals.onStartup(TEST_userId); 576 | 577 | const userAfterRestart = Meteor.users.findOne(TEST_userId); 578 | // Check reset status again 579 | test.equal(userAfterRestart.status.online, false); 580 | test.equal(userAfterRestart.status.idle, undefined); 581 | return test.equal(userAfterRestart.status.lastActivity, undefined); 582 | })); 583 | --------------------------------------------------------------------------------