├── .editorconfig ├── .github └── dependabot.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── _src ├── index.coffee ├── lib │ ├── mailbuffer.coffee │ ├── main.coffee │ ├── schemas │ │ ├── create-multi.coffee │ │ ├── create-single.coffee │ │ ├── creator.coffee │ │ ├── message-content.coffee │ │ └── user.coffee │ ├── tasks.coffee │ ├── utils.coffee │ └── worker.coffee └── test │ ├── example.coffee │ └── main.coffee ├── coffeelint.json └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = tab 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | 17 | [*.coffee] 18 | indent_style = tab 19 | trim_trailing_whitespace = false 20 | insert_final_newline = false 21 | 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.profile 2 | *.lock 3 | *.conflict 4 | *.DS_Store 5 | *.zip 6 | *.rdb 7 | 8 | .project 9 | .settings 10 | .idea 11 | 12 | *.mo 13 | *.sublime* 14 | config.json 15 | config*.json 16 | deploy.json 17 | /node_modules 18 | /lib 19 | /test 20 | index.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /_src 2 | /_docs 3 | /node_modules 4 | Gruntfile.* 5 | *.sublime* 6 | config.json 7 | config*.json 8 | deploy.json 9 | .editorconfig 10 | .travis.yml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | - redis-server 3 | os: 4 | - linux 5 | language: node_js 6 | node_js: 7 | - 0.10 8 | - 0.12 9 | - 4.0 10 | - 4.4 11 | - 5.1 12 | - 5.5 13 | - 5.11 14 | - 6.0 15 | - 6.1 16 | - iojs 17 | - node 18 | before_script: 19 | - "npm install -g mocha grunt-cli" 20 | - "grunt build" 21 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | # Project configuration. 3 | grunt.initConfig 4 | pkg: grunt.file.readJSON('package.json') 5 | watch: 6 | module: 7 | files: ["_src/**/*.coffee"] 8 | tasks: [ "coffee:base" ] 9 | 10 | coffee: 11 | base: 12 | expand: true 13 | cwd: '_src', 14 | src: ["**/*.coffee"] 15 | dest: '' 16 | ext: '.js' 17 | 18 | clean: 19 | base: 20 | src: [ "index.js", "lib", "test" ] 21 | 22 | includereplace: 23 | pckg: 24 | options: 25 | globals: 26 | version: "0.0.5" 27 | 28 | prefix: "@@" 29 | suffix: '' 30 | 31 | files: 32 | "index.js": ["index.js"] 33 | 34 | 35 | mochacli: 36 | options: 37 | require: [ "should" ] 38 | reporter: "spec" 39 | bail: false 40 | timeout: 3000 41 | slow: 3 42 | 43 | main: 44 | src: [ "test/main.js" ] 45 | options: 46 | env: 47 | severity_heartbeat: "debug" 48 | 49 | 50 | # Load npm modules 51 | grunt.loadNpmTasks "grunt-contrib-watch" 52 | grunt.loadNpmTasks "grunt-contrib-coffee" 53 | grunt.loadNpmTasks "grunt-contrib-clean" 54 | grunt.loadNpmTasks "grunt-mocha-cli" 55 | grunt.loadNpmTasks "grunt-include-replace" 56 | 57 | # just a hack until this issue has been fixed: https://github.com/yeoman/grunt-regarde/issues/3 58 | grunt.option('force', not grunt.option('force')) 59 | 60 | # ALIAS TASKS 61 | grunt.registerTask "default", "build" 62 | grunt.registerTask "clear", [ "clean:base" ] 63 | grunt.registerTask "test", [ "mochacli:main" ] 64 | 65 | # ALIAS SHORT TASKS 66 | grunt.registerTask "b", "build" 67 | grunt.registerTask "t", "test" 68 | grunt.registerTask "w", "watch" 69 | 70 | 71 | # build the project 72 | grunt.registerTask "build", [ "clear", "coffee:base", "includereplace" ] 73 | grunt.registerTask "build-dev", [ "clear", "coffee:base", "test" ] 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 M. Peter 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![redis-notifications](https://trello-attachments.s3.amazonaws.com/5481963992d9ba3848568a1b/600x199/0942ef2e9e86200b258685b0ff02f794/redis-notifications_pxl.png) 3 | 4 | [![Build Status](https://secure.travis-ci.org/mpneuried/redis-notifications.png?branch=master)](http://travis-ci.org/mpneuried/redis-notifications) 5 | [![Build Status](https://david-dm.org/mpneuried/redis-notifications.png)](https://david-dm.org/mpneuried/redis-notifications) 6 | [![NPM version](https://badge.fury.io/js/redis-notifications.png)](http://badge.fury.io/js/redis-notifications) 7 | 8 | A redis based notification engine. 9 | It implements the [**rsmq-worker**](https://github.com/mpneuried/rsmq-worker) to safely create notifications and recurring reports. 10 | 11 | The goal is to define a simple API to be able to send notifications and mails to multiple users. 12 | A user can define a setting to only receive one mail per day as a report. 13 | This is all done within a queuing solution. so it's scalable and failsafe. 14 | 15 | [![NPM](https://nodei.co/npm/redis-notifications.png?downloads=true&stars=true)](https://nodei.co/npm/redis-notifications/) 16 | 17 | 18 | ## Install 19 | 20 | ```sh 21 | npm install redis-notifications 22 | ``` 23 | 24 | ## Initialize 25 | 26 | 27 | **initialize** 28 | 29 | ```js 30 | var RedisNotifications = require( "redis-notifications" ); 31 | 32 | var nf = new RedisNotifications(); 33 | 34 | // REQUIRED EVENT LISTENERS 35 | // listen to errors 36 | nf.on( "error", function( err ){}); 37 | 38 | // Hook to read details of an user 39 | nf.on( "readUser", function( user_id, cb ){ /* ... */ }); 40 | 41 | // Hook to generate the message content for the notification and the mail 42 | nf.on( "getContent", function( type, user, editor, additional, cb ){ /* ... */ }); 43 | 44 | // Hook to write/send the notification to the user. Is is done immediately on every create 45 | nf.on( "createNotification", function( user, editor, message, cb ){ /* ... */ }); 46 | 47 | // Hook to send a report to a user. 48 | nf.on( "sendMail", function( user, messages, isReport, cb ){ /* ... */ }); 49 | 50 | // INIT 51 | nf.init(); 52 | 53 | // METHODS 54 | // define the data of the editor 55 | var editor = { id: "ABCDE", firstname: "William", lastname: "Creator", email: "william.create@example.com" }; 56 | 57 | // create a notification to a single user 58 | nf.create( editor, { type: "notification_type", user: 123 }, function( err, msgid ){ /* ... */ }); 59 | ``` 60 | 61 | **Config** 62 | 63 | - **options** *( `Object` optional )* The configuration object 64 | - **options.maxBufferReadCount**: *( `Number` optional; default = `100` )* Count of users to read at once to send mails 65 | 66 | **[RSMQ-Worker Options](https://github.com/mpneuried/rsmq-worker#initialize)** 67 | 68 | - **options.queuename**: *( `String` optional; default = `rnqueue` )* The queuename to pull the messages 69 | - **options.interval**: *( `Number[]` optional; default = `[ 0, 1, 5, 10 ]` )* An Array of increasing wait times in seconds 70 | - **options.maxReceiveCount**: *( `Number` optional; default = `10` )* Receive count until a message will be exceeded 71 | - **options.invisibletime**: *( `Number` optional; default = `30` )* A time in seconds to hide a message after it has been received. 72 | - **options.defaultDelay**: *( `Number` optional; default = `1` )* The default delay in seconds for for sending new messages to the queue. 73 | - **options.timeout**: *( `Number` optional; default = `3000` )* Message processing timeout in `ms`. So you have to call the `next()` method of `message` at least after e.g. 3000ms. If set to `0` it'll wait until infinity. 74 | - **options.prefix**: *( `String` optional; default = `notifications` )* The redis namespace for rsmq 75 | - **options.client**: *( `RedisClient` optional; default = `null` )* A already existing redis client instance to use. 76 | - **options.host**: *( `String` optional; default = `localhost` )* Host to connect to redis if `redis` instance has been defined 77 | - **options.port**: *( `Number` optional; default = `6379` )* Port to connect to redis if `redis` instance has been defined 78 | - **options.options**: *( `Object` optional; default = `{}` )* Options to connect to redis if `redis` instance has been defined 79 | 80 | ## Event Hooks 81 | 82 | ### `readUser` 83 | 84 | Call to read a user by the given id. So you have to implement the DB/API read yourself 85 | 86 | **Arguments** 87 | 88 | - **user_id** : *( `String|Number` )* The user id to read 89 | - **cb** : *( `Function` )* The callback function 90 | 91 | **Callback Params:** 92 | 93 | - **err** : *( `Null|Error` )* An optional error 94 | - **user** : *( `Object` )* The user result with the following fields. You can also add additional fields you need later 95 | - **user.id** : *( `String|Number`, required )* The user_id 96 | - **user.firstname** : *( `String`, required )* The user's first name. 97 | - **user.lastname** : *( `String`, optional )* The user's last name. 98 | - **user.email** : *( `String`, required )* The user's email. 99 | - **user.timezone** : *( `String`, required )* A timezone string. It has to be a valid [moment timezone](http://momentjs.com/timezone/) string 100 | - **user.sendInterval** : *( `String`, required )* The send interval. This defines if, how and when the user will receive a email 101 | 102 | **Possible values:** 103 | 104 | - **`0`:** The user will never receive a mail 105 | - **`p`:** receive the only prio mails *( created with `high:true` )* immediately 106 | - **`i`:** receive the mail immediately 107 | - **`d{time}`:** receive the mail daily report. The time has to be a 4 digit number. E.g. `0800` = 8 in the morning within his timezone. `2330` = half an hour before midnight. 108 | 109 | ### `getContent` 110 | 111 | Create the notification content 112 | 113 | **Arguments** 114 | 115 | - **type** : *( `String` )* The message type you defined on `.create{Multile}()` 116 | - **user** : *( `Object` )* User result generated by you within the `readUser` hook 117 | - **editor** : *( `Object` )* User editor you added on `.create{Multile}()` 118 | - **additional** : *( `Object` )* User additional data you added on `.create{Multile}()` 119 | - **cb** : *( `Function` )* The callback function 120 | 121 | **Callback Params:** 122 | 123 | - **err** : *( `Null|Error` )* An optional error 124 | - **content** : *( `Object` )* The content result with the following fields. You can also add additional fields you need later 125 | - **content.subject** : *( `String`, required )* The message subject/headline 126 | - **content.body** : *( `String`, required )* The message body 127 | - **content.teaser** : *( `String`, optional )* The message teaser. HTML will be stripped out! If not defined the module truncates the `body` to 100 chars. 128 | 129 | ### `createNotification` 130 | 131 | This will called immediately after `.create{Multile}()`. 132 | Here you have to write the notification to your db and/or send it immediately to the user. 133 | 134 | **Arguments** 135 | 136 | - **user** : *( `Object` )* User result generated by you within the `readUser` hook 137 | - **editor** : *( `Object` )* User editor you added on `.create{Multile}()` 138 | - **message** : *( `Object` )* The message content you created within the `getContent` hook 139 | - **cb** : *( `Function` )* The callback function 140 | 141 | **Callback Params:** 142 | 143 | - **err** : *( `Null|Error` )* An optional error 144 | 145 | ### `sendMail` 146 | 147 | Send a mail to the user. 148 | This is only done if `sendInterval` is `d{time}`, `i` or `p` 149 | Only on `d{time}` a report is generated witch could contain more than one message. 150 | 151 | **Arguments** 152 | 153 | - **user** : *( `Object` )* User result generated by you within the `readUser` hook 154 | - **messages** : *( `Object[]` )* An Array messages you created within the `getContent` hooks 155 | - **isReport** : *( `Boolean` )* A flag that tells you this mail is a report because the user used `sendInterval = "d{time}"`. 156 | - **cb** : *( `Function` )* The callback function 157 | 158 | **Callback Params:** 159 | 160 | - **err** : *( `Null|Error` )* An optional error 161 | 162 | ### `error` 163 | 164 | An error occurred 165 | 166 | **Arguments** 167 | 168 | - **err** : *( `Error` )* A error 169 | 170 | ## Methods 171 | 172 | ### `.init()` 173 | 174 | After you added the required hooks you have the call `.init()` to start the module. 175 | 176 | ### `.create( editor, message [, cb ] )` 177 | 178 | Create a notification for one user 179 | 180 | **Arguments** 181 | 182 | * `editor` : *( `Object` required )*: The sending editor 183 | * `editor.id` : *( `String|Number` required )*: a unique identifier of the notification creator 184 | * `editor.firstname` : *( `String` optional )*: The first name of the creator 185 | * `editor.lastname` : *( `String` optional )*: The last name of the creator 186 | * `editor.email` : *( `String` optional )*: The email of the creator 187 | * `message` : *( `Object` required )*: The message data 188 | * `message.type` : *( `String` required )*: The notification type. This is a key to be able to generate different notification contents 189 | * `message.user` : *( `String|Number` required )*: The id of the user that will receive this message 190 | * `message.high` : *( `Boolean` optional; default = `false` )*: This flag marks this notification as high prio. So mails will send immediately and users with `sendInterval = "p"` will alos get a mail. 191 | * `message.additional` : *( `Object` optional )*: An object you could use to place custom data. This cwill be passed to the hook `getContent` 192 | * `cb` : *( `Function` optional )*: A optional callback method. Callback arguments: `function( err, nid ){}` 193 | 194 | **Return** 195 | 196 | *( Null|Error )*: Null on success or a validation error. 197 | 198 | ### `.createMulti( editor, message [, cb ] )` 199 | 200 | Create a notification for multiple users 201 | 202 | **Arguments** 203 | 204 | * `editor` : *( `Object` required )*: The sending editor 205 | * `editor.id` : *( `String|Number` required )*: a unique identifier of the notification creator 206 | * `editor.firstname` : *( `String` optional )*: The first name of the creator 207 | * `editor.lastname` : *( `String` optional )*: The last name of the creator 208 | * `editor.email` : *( `String` optional )*: The email of the creatore 209 | * `message` : *( `Object` required )*: The message data 210 | * `message.type` : *( `String` required )*: The notification type. This is a key to be able to generate different notification contents 211 | * `message.users` : *( `Array` required )*: An Array of user ids that will receive this notification. 212 | * `message.high` : *( `Boolean` optional; default = `false` )*: This flag marks this notification as high prio. So mails will send immediately and users with `sendInterval = "p"` will alos get a mail. 213 | * `message.additional` : *( `Object` optional; default = `{}` )*: An object you could use to place custom data. This will be passed to the hook `getContent` 214 | * `cb` : *( `Function` optional )*: A optional callback method. Callback arguments: `function( err ){}` 215 | 216 | **Return** 217 | 218 | *( Null|Error )*: Null on success or a validation error. 219 | 220 | ### `.getRsmqWorker()` 221 | 222 | Helper method to get internal used instance 223 | 224 | **Return** 225 | 226 | *( RSMQWorker )*: The internal [rsmq-worker](https://github.com/mpneuried/rsmq-worker) instance. 227 | 228 | ### `.getRsmq()` 229 | 230 | Helper method to get internal used instance 231 | 232 | **Return** 233 | 234 | *( RedisSMQ )*: The internal [rsmq](https://github.com/smrchy/rsmq) instance. 235 | 236 | ### `.getRedis()` 237 | 238 | Helper method to get internal used instance 239 | 240 | **Return** 241 | 242 | *( RedisClient )*: The internal [redis](https://github.com/mranney/node_redis) instance. 243 | 244 | ## Example 245 | 246 | This is a example implementation. 247 | It's up to you to implement the DB read and Write methods and do the notification and mail sending. 248 | 249 | ```js 250 | var RedisNotifications = require( "redis-notifications" ); 251 | 252 | var nf = new RedisNotifications(); 253 | 254 | // REQUIRED EVENT LISTENERS 255 | // listen to errors 256 | nf.on( "error", function( err ){ 257 | console.error( err, err.stack ); 258 | }); 259 | 260 | // Hock to read details of an user 261 | nf.on( "readUser", function( user_id, cb ){ 262 | 263 | // here you have to query your database and return a valid user with at least these fields 264 | var user = { 265 | // required fields 266 | id: user_id, // unique user id 267 | firstname: "John", 268 | email: "john.do@mail.com", 269 | timezone: "CET", // a moment-timezone valid timezone 270 | sendInterval: "i", // when to send a mail 271 | 272 | // optional fields 273 | lastname: "Do", 274 | 275 | // custom fields 276 | custom_lang: "DE" // it is possible to add custom fields 277 | }; 278 | cb( null, user ); 279 | }); 280 | 281 | // Hook to generate the message content for the notification and the mail 282 | nf.on( "getContent", function( type, user, editor, additional, cb ){ 283 | 284 | // here you have to generate your message by type. Usually you would use a template-engine 285 | 286 | var content = { 287 | // required fields 288 | subject: "This is my message subject", 289 | body: "Lorem ipsum dolor ...", 290 | 291 | // custom fields 292 | custom_field: additional.custom_key, // it is possible to add custom fields to the content 293 | custom_lang: user.custom_lang // reuse the custom field from user 294 | }; 295 | cb( null, content ); 296 | }); 297 | 298 | // Hook to write/send the notification to the user. Is is done immediately on every create 299 | nf.on( "createNotification", function( user, editor, message, cb ){ 300 | 301 | // here you have to write the notification to your db and/or send it immediately to the user 302 | 303 | cb( null ); 304 | }); 305 | 306 | // Hook to send a report to a user. 307 | // This is only done if `sendInterval` is `d{time}`, `i` or `p` 308 | // Only on `d{time}` a report is generated witch could contain more than one message 309 | nf.on( "sendMail", function( user, messages, isReport, cb ){ 310 | 311 | // here you have to do create of the mail content and mail send it. 312 | 313 | cb( null ); 314 | }); 315 | 316 | // INIT 317 | // you have to initialize the module until the required listeners has been added 318 | nf.init(); 319 | 320 | // METHODS 321 | 322 | // define the data of the editor 323 | var editor = { 324 | // required fields 325 | id: "ABCDE", 326 | 327 | // optional fields 328 | firstname: "William", 329 | lastname: "Creator", 330 | email: "william.create@example.com", 331 | 332 | // custom fields 333 | custom_lang: "DE" 334 | }; 335 | 336 | 337 | // create a notification for multiple users without callback 338 | var errCrM = nf.createMulti( editor, { 339 | type: "notification_type", // A type to differ the content in the `getContent` hook 340 | users: [ 123, 456, 789 ], // An array of user that will receive this notification 341 | high: true // A flag to define this notification as high prio. 342 | }); 343 | if( errCrM ){ 344 | console.error( errCrM ); 345 | } 346 | // High means: 347 | // - This message will be send by mail immediately. 348 | // - Users with `sendInterval = "p"` (only high prio) will also get this notification. 349 | 350 | // create a notification to a single user 351 | nf.create( editor, { 352 | type: "notification_type", // A type to differ the content in the `getContent` hook 353 | user: 123, // The user id that will receive this notification 354 | high: false, // A flag to define this notification as high prio. 355 | additional: { 356 | custom_icon: "info" // additional data that later can be used with in the `getContent` hook 357 | } 358 | }, function( err, msgid ){ 359 | if( err ){ 360 | console.error( err ); 361 | } 362 | }); 363 | ``` 364 | 365 | ## Todos 366 | 367 | - Tests 368 | - add `sendInterval` variant to send the report weekly and/or monthly. 369 | 370 | ## Release History 371 | |Version|Date|Description| 372 | |:--:|:--:|:--| 373 | |0.2.1|2016-10-27|Small bugfix with default value (Thanks to [Anton Rau](https://github.com/plankter) for [#3](https://github.com/mpneuried/redis-notifications/issues/3)). Updated dependencies| 374 | |0.2.0|2016-07-18|Updated dev env and dependencies| 375 | |0.1.1|2015-01-30|Logo update| 376 | |0.1.0|2015-01-30|Added docs and optimized code and API| 377 | |0.0.2|2015-01-29|moved schema to extra module `obj-schema`| 378 | |0.0.1|2015-01-29|Initial version. No tests and docs until now!| 379 | 380 | [![NPM](https://nodei.co/npm-dl/redis-notifications.png?months=6)](https://nodei.co/npm/redis-notifications/) 381 | 382 | > Initially Generated with [generator-mpnodemodule](https://github.com/mpneuried/generator-mpnodemodule) 383 | 384 | ## Other projects 385 | 386 | |Name|Description| 387 | |:--|:--| 388 | |[**rsmq**](https://github.com/smrchy/rsmq)|A really simple message queue based on Redis| 389 | |[**rsmq-worker**](https://github.com/mpneuried/rsmq-worker)|Helper to simply implement a worker [RSMQ ( Redis Simple Message Queue )](https://github.com/smrchy/rsmq).| 390 | |[**node-cache**](https://github.com/tcs-de/nodecache)|Simple and fast NodeJS internal caching. Node internal in memory cache like memcached.| 391 | |[**obj-schema**](https://github.com/mpneuried/obj-schema)|Simple module to validate an object by a predefined schema| 392 | |[**redis-sessions**](https://github.com/smrchy/redis-sessions)|An advanced session store for NodeJS and Redis| 393 | |[**connect-redis-sessions**](https://github.com/mpneuried/connect-redis-sessions)|A connect or express middleware to simply use the [redis sessions](https://github.com/smrchy/redis-sessions). With [redis sessions](https://github.com/smrchy/redis-sessions) you can handle multiple sessions per user_id.| 394 | |[**systemhealth**](https://github.com/mpneuried/systemhealth)|Node module to run simple custom checks for your machine or it's connections. It will use [redis-heartbeat](https://github.com/mpneuried/redis-heartbeat) to send the current state to redis.| 395 | |[**task-queue-worker**](https://github.com/smrchy/task-queue-worker)|A powerful tool for background processing of tasks that are run by making standard http requests.| 396 | |[**soyer**](https://github.com/mpneuried/soyer)|Soyer is small lib for serverside use of Google Closure Templates with node.js.| 397 | |[**grunt-soy-compile**](https://github.com/mpneuried/grunt-soy-compile)|Compile Goggle Closure Templates ( SOY ) templates inclding the handling of XLIFF language files.| 398 | |[**backlunr**](https://github.com/mpneuried/backlunr)|A solution to bring Backbone Collections together with the browser fulltext search engine Lunr.js| 399 | 400 | 401 | ## The MIT License (MIT) 402 | 403 | Copyright © 2015 Mathias Peter, http://www.tcs.de 404 | 405 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 406 | 407 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 408 | 409 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 410 | -------------------------------------------------------------------------------- /_src/index.coffee: -------------------------------------------------------------------------------- 1 | exports.version = '@@version' 2 | exports.utils = require( "./lib/utils" ) 3 | 4 | module.exports = require './lib/main' 5 | -------------------------------------------------------------------------------- /_src/lib/mailbuffer.coffee: -------------------------------------------------------------------------------- 1 | ## # RNMailBuffer 2 | # ### extends [NPM:MPBasic](https://cdn.rawgit.com/mpneuried/mpbaisc/master/_docs/index.coffee.html) 3 | # 4 | # ### Exports: *Class* 5 | # 6 | # Main Module to init the notifications to redis 7 | # 8 | 9 | # **npm modules** 10 | moment = require( "moment-timezone" ) 11 | 12 | # [utils](./utils.coffee.html) 13 | utils = require( "./utils" ) 14 | 15 | class RNMailBuffer extends require( "mpbasic" )() 16 | 17 | # ## defaults 18 | defaults: => 19 | @extend super, 20 | keyUserlist: "users_with_messages" 21 | keyMsgsPrefix: "msgs" 22 | 23 | maxBufferReadCount: 100 24 | # **options.prefix** *String* A general redis prefix 25 | prefix: "notifications" 26 | 27 | ### 28 | ## constructor 29 | ### 30 | constructor: ( @main, options )-> 31 | @ready = false 32 | 33 | 34 | super 35 | 36 | @write = @_waitUntil( @_write ) 37 | @listUsers = @_waitUntil( @_listUsers ) 38 | 39 | @main.on "ready", @_start 40 | return 41 | 42 | _start: => 43 | @redis = @main.getRedis() 44 | @ready = true 45 | @emit "ready" 46 | return 47 | 48 | _write: ( data, cb )=> 49 | @_getRedisTime ( err, sec, ms )=> 50 | if err 51 | cb( err ) 52 | return 53 | 54 | data.created = sec 55 | 56 | _ud = data.userdata 57 | rM = [] 58 | rM.push( [ "ZADD", @_getKey( @config.keyUserlist ), @_calcSendAt( ms, _ud.sendInterval, _ud.timezone ), _ud.id ] ) 59 | rM.push( [ "LPUSH", @_getKey( @config.keyMsgsPrefix, _ud.id ), JSON.stringify( data )] ) 60 | 61 | @redis.multi( rM ).exec ( err, results )-> 62 | if err 63 | cb( err ) 64 | return 65 | cb() 66 | return 67 | return 68 | 69 | return 70 | 71 | _listUsers: ( cb )=> 72 | @_calcCheckTime ( err, ts )=> 73 | if err 74 | cb( err ) 75 | return 76 | @debug "_listUsers:ts", ts 77 | @redis.zrangebyscore( @_getKey( @config.keyUserlist ), 0, ts, "LIMIT", 0, @config.maxBufferReadCount, cb ) 78 | return 79 | return 80 | 81 | userMsgs: ( uid, cb )=> 82 | @redis.lrange @_getKey( @config.keyMsgsPrefix, uid ), 0, -1, ( err, msgs )-> 83 | if err 84 | cb( err ) 85 | return 86 | try 87 | # concat the messages simulate a array and do a single parse 88 | cb( null, JSON.parse( "[" + msgs.join( "," ) + "]" ) ) 89 | catch _err 90 | cb( _err ) 91 | return 92 | return 93 | 94 | removeUser: ( args..., cb )=> 95 | [ uid, count ] = args 96 | if not count? 97 | _range = [ -1, 0 ] 98 | else 99 | _range = [ 0, ( ( count + 1 ) * -1 ) ] 100 | 101 | rM = [] 102 | rM.push( [ "ZREM", @_getKey( @config.keyUserlist ), uid ] ) 103 | if not count? or count > 0 104 | # only relevent if count is undefined or gt 0 105 | rM.push( [ "LTRIM", @_getKey( @config.keyMsgsPrefix, uid ), _range[ 0 ], _range[ 1 ] ] ) 106 | @redis.multi( rM ).exec ( err, results )-> 107 | if err 108 | cb( err ) 109 | return 110 | cb() 111 | return 112 | return 113 | 114 | _calcCheckTime: ( cb )=> 115 | @_getRedisTime ( err, sec, ms )-> 116 | if err 117 | cb( err ) 118 | return 119 | _n = moment(ms) 120 | _last10Min = ( Math.floor( _n.minute()/10 ) * 10 ) 121 | _n.minutes( _last10Min ).seconds( 1 ).milliseconds( 0 ) 122 | 123 | cb( null, _n.valueOf() ) 124 | return 125 | return 126 | 127 | _calcSendAt: ( now, interval, timezone="CET" )-> 128 | type = interval[0] 129 | # handle daily 130 | if type is "d" 131 | time = interval[1..] 132 | _m = moment( now ).tz( timezone ) 133 | 134 | _next = moment( _m ).hour( parseInt( time[..1], 10 ) ).minute( parseInt( time[2..], 10 ) ).seconds(0).milliseconds(0) 135 | 136 | if _next <= _m 137 | _next.add( 1, "d" ) 138 | 139 | # DEBUGGING 140 | _next.add( -1, "d" ) 141 | 142 | return _next.valueOf() 143 | 144 | 145 | ### 146 | ## _getKey 147 | 148 | `redisconnector._getKey( id, name )` 149 | 150 | Samll helper to prefix and get a redis key. 151 | 152 | @param { String } name the key name 153 | @param { String } id The key 154 | 155 | @return { String } Return The generated key 156 | 157 | @api public 158 | ### 159 | _getKey: ( name, id )=> 160 | _key = @main.getRedisNamespace() or "" 161 | if name? 162 | if _key[ _key.length-1 ] isnt ":" 163 | _key += ":" 164 | _key += "#{name}" 165 | if id? 166 | if _key[ _key.length-1 ] isnt ":" 167 | _key += ":" 168 | _key += "#{id}" 169 | return _key 170 | 171 | _getRedisTime: ( cb )=> 172 | @redis.time ( err, time )-> 173 | if err 174 | cb( err ) 175 | return 176 | 177 | [ s, ns ] = time 178 | ns = utils.lpad( ns, 6, "0" )[0..5] 179 | ms = Math.round( (parseInt( s + ns , 10 ) / 1000 ) ) 180 | 181 | cb( null, parseInt( s, 10 ), ms ) 182 | return 183 | return 184 | 185 | #export this class 186 | module.exports = RNMailBuffer 187 | -------------------------------------------------------------------------------- /_src/lib/main.coffee: -------------------------------------------------------------------------------- 1 | # # RedisNotifications 2 | # ### extends [NPM:MPBasic](https://cdn.rawgit.com/mpneuried/mpbaisc/master/_docs/index.coffee.html) 3 | # 4 | # ### Exports: *Class* 5 | # 6 | # Main Module to init the notifications to redis 7 | 8 | # **npm modules** 9 | _isFunction = require( "lodash/isFunction" ) 10 | 11 | # **internal modules** 12 | # [RNWorker](./worker.coffee.html) 13 | Worker = require( "./worker" ) 14 | # [RNTasks](./tasks.coffee.html) 15 | Tasks = require( "./tasks" ) 16 | # [RNMailBuffer](./mailbuffer.coffee.html) 17 | MailBuffer = require( "./mailbuffer" ) 18 | 19 | # [validateCreator](./schemas/creator.coffee.html) 20 | validateCreator = require( "./schemas/creator" ) 21 | # [validateMultiCreate](./schemas/create-multi.coffee.html) 22 | validateMultiCreate = require( "./schemas/create-multi" ) 23 | # [validateSingleCreate](./schemas/create-single.coffee.html) 24 | validateSingleCreate = require( "./schemas/create-single" ) 25 | 26 | # ** configurations** 27 | # **RequiredEvents** *String[]* A list of events that has to be binded to this module 28 | RequiredEvents = [ "readUser", "getContent", "createNotification", "sendMail", "error" ] 29 | 30 | class RedisNotifications extends require( "mpbasic" )() 31 | 32 | # ## defaults 33 | defaults: => 34 | @extend super, 35 | # **options.maxBufferReadCount** *Number* Count of users to read at once to send mails 36 | maxBufferReadCount: 100 37 | 38 | # RSMW-Worker options 39 | # **options.queuename** *String* The queuename to use for the worker 40 | queuename: "rnqueue" 41 | # **options.interval** *Number[]* An Array of increasing wait times in seconds 42 | interval: [ 0, 1, 5, 10 ] 43 | # **RSMQWorker.maxReceiveCount** *Number* Receive count until a message will be exceeded 44 | maxReceiveCount: 10 45 | # **RSMQWorker.invisibletime** *Number* A time in seconds to hide a message after it has been received. 46 | invisibletime: 30 47 | # **RSMQWorker.defaultDelay** *Number* The default delay in seconds for for sending new messages to the queue. 48 | defaultDelay: 1 49 | # **RSMQWorker.timeout** *Number* Message processing timeout in `ms`. If set to `0` it'll wait until infinity. 50 | timeout: 3000 51 | # **options.host** *String* Redis host name 52 | host: "localhost" 53 | # **options.port** *Number* Redis port 54 | port: 6379 55 | # **options.options** *Object* Redis options 56 | options: {} 57 | # **options.client** *RedisClient* Exsiting redis client instance 58 | client: null 59 | # **options.prefix** *String* A general redis prefix 60 | prefix: "notifications" 61 | 62 | ### 63 | ## constructor 64 | ### 65 | constructor: ( options )-> 66 | @ready = false 67 | super 68 | @worker = new Worker( @config ) 69 | @mailbuffer = new MailBuffer( @, @config ) 70 | @tasks = new Tasks( @, @config ) 71 | 72 | # wrap start method to only be active until the connection is established 73 | @init = @_waitUntil( @_init, "ready", @worker ) 74 | return 75 | 76 | _init: => 77 | if @ready 78 | return 79 | 80 | @_checkListeners() 81 | 82 | @ready = true 83 | @emit "ready" 84 | return 85 | 86 | _checkListeners: => 87 | for evnt in RequiredEvents when not @listeners( evnt ).length 88 | @_handleError( null, "EMISSINGLISTENER", evname: evnt ) 89 | 90 | return 91 | 92 | createMulti: ( creator, options, cb = true )=> 93 | _verrC = validateCreator( creator, cb ) 94 | _verrM = validateMultiCreate( options, cb ) 95 | if _verrC? or _verrM? 96 | @emit "error", ( _verrC or _verrM ) 97 | return 98 | 99 | options.creator = creator 100 | 101 | if _isFunction( cb ) 102 | @worker.send "crNfcns", options, ( err, qmid )-> 103 | if err 104 | cb( err ) 105 | return 106 | cb( null ) 107 | return 108 | else 109 | @worker.send( "crNfcns", options ) 110 | 111 | return null 112 | 113 | create: ( creator, options, cb = true )=> 114 | _verrC = validateCreator( creator, cb ) 115 | _verrS = validateSingleCreate( options, cb ) 116 | if _verrC? or _verrS? 117 | @emit "error", ( _verrC or _verrS ) 118 | return 119 | 120 | options.creator = creator 121 | 122 | @worker.send( "crNfcn", options, cb ) 123 | return null 124 | 125 | getWorker: => 126 | return @worker 127 | 128 | getMailbuffer: => 129 | return @mailbuffer 130 | 131 | getRsmqWorker: => 132 | return @worker.getRsmqWorker() 133 | 134 | getRsmq: => 135 | return @worker.getRsmq() 136 | 137 | getRedis: => 138 | return @worker.getRedis() 139 | 140 | getRedisNamespace: => 141 | return @worker.getRedisNamespace() 142 | 143 | 144 | ERRORS: => 145 | return @extend {}, super, 146 | "EMISSINGLISTENER": [ 404, "Missing Event Listener. Please make sure you've added a event listener to `<%= evname %>`" ] 147 | 148 | #export this class 149 | module.exports = RedisNotifications 150 | -------------------------------------------------------------------------------- /_src/lib/schemas/create-multi.coffee: -------------------------------------------------------------------------------- 1 | module.exports = new ( class CreateMutliSchema extends require( "obj-schema" ) )( 2 | type: 3 | type: "string" 4 | required: true 5 | 6 | users: 7 | type: "array" 8 | required: true 9 | 10 | high: 11 | type: "boolean" 12 | default: false 13 | 14 | additional: 15 | type: "object" 16 | default: {} 17 | 18 | , name: "createmulti" ).validateCb 19 | -------------------------------------------------------------------------------- /_src/lib/schemas/create-single.coffee: -------------------------------------------------------------------------------- 1 | module.exports = new ( class CreateSchema extends require( "obj-schema" ) )( 2 | type: 3 | type: "string" 4 | required: true 5 | 6 | user: 7 | required: true 8 | 9 | high: 10 | type: "boolean" 11 | default: false 12 | 13 | additional: 14 | type: "object" 15 | default: {} 16 | 17 | , name: "create" ).validateCb 18 | -------------------------------------------------------------------------------- /_src/lib/schemas/creator.coffee: -------------------------------------------------------------------------------- 1 | module.exports = new ( class CreatorSchema extends require( "obj-schema" ) )( 2 | id: 3 | required: true 4 | 5 | firstname: 6 | type: "string" 7 | 8 | lastname: 9 | type: "string" 10 | 11 | email: 12 | type: "email" 13 | 14 | , name: "creator" ).validateCb 15 | -------------------------------------------------------------------------------- /_src/lib/schemas/message-content.coffee: -------------------------------------------------------------------------------- 1 | # [utils](./utils.coffee.html) 2 | utils = require( "../utils" ) 3 | 4 | module.exports = new ( class MessageContentSchema extends require( "obj-schema" ) )( 5 | subject: 6 | type: "string" 7 | required: true 8 | 9 | body: 10 | type: "string" 11 | required: true 12 | sanitize: true 13 | 14 | teaser: 15 | type: "string" 16 | sanitize: true 17 | striphtml: true 18 | default: ( data, def )-> 19 | return utils.truncate( data?.body or "", 100 ) 20 | 21 | , name: "messagecontent" ).validateCb 22 | -------------------------------------------------------------------------------- /_src/lib/schemas/user.coffee: -------------------------------------------------------------------------------- 1 | module.exports = new ( class UserSchema extends require( "obj-schema" ) )( 2 | id: 3 | required: true 4 | 5 | firstname: 6 | type: "string" 7 | required: true 8 | 9 | lastname: 10 | type: "string" 11 | 12 | email: 13 | type: "email" 14 | required: true 15 | 16 | timezone: 17 | type: "timezone" 18 | required: true 19 | 20 | sendInterval: 21 | type: "string" 22 | required: true 23 | regexp: /^(0|i|p|d\d{4})$/ 24 | 25 | , name: "user" ).validateCb 26 | -------------------------------------------------------------------------------- /_src/lib/tasks.coffee: -------------------------------------------------------------------------------- 1 | ## # RNTasks 2 | # ### extends [NPM:MPBasic](https://cdn.rawgit.com/mpneuried/mpbaisc/master/_docs/index.coffee.html) 3 | # 4 | # ### Exports: *Class* 5 | # 6 | 7 | # **npm modules** 8 | _omit = require( "lodash/omit" ) 9 | _map = require( "lodash/map" ) 10 | 11 | # **internal modules** 12 | 13 | # [validateUser](./schemas/user.coffee.html) 14 | validateUser = require( "./schemas/user" ) 15 | # [validateUser](./schemas/user.coffee.html) 16 | validateMessageContent = require( "./schemas/message-content" ) 17 | 18 | class RNTasks extends require( "mpbasic" )() 19 | 20 | # ## defaults 21 | defaults: => 22 | @extend super, {} 23 | 24 | constructor: ( @main, options )-> 25 | @worker = @main.getWorker() 26 | @mailbuffer = @main.getMailbuffer() 27 | 28 | # wire tasks to worker 29 | @worker.on "crNfcns", @dispatchUsers 30 | @worker.on "crNfcn", @createNotification 31 | @worker.on "chShdl", @checkSchedule 32 | @worker.on "sndMsg", @sendMail 33 | @worker.on "crMsg", @composeMessageReport 34 | @worker.on "checkMailBuffer", @checkMailBuffer 35 | super 36 | 37 | checkSchedule: ( data, next )=> 38 | @warning "checkSchedule", data 39 | next() 40 | return 41 | 42 | dispatchUsers: ( data, next )=> 43 | #@info "dispatchUsers", data 44 | for user_id in data.users 45 | @worker.send( "crNfcn", @extend( {}, _omit( data, "users" ), user: user_id ) ) 46 | 47 | next() 48 | return 49 | 50 | createNotification: ( data, next, msgid )=> 51 | #@info "createNotification", data, id 52 | @main.emit "readUser", data.user, ( err, userdata )=> 53 | if err 54 | @main.emit "error", err 55 | @warning "readUser", err 56 | next( false ) 57 | return 58 | 59 | _verr = validateUser( userdata, true ) 60 | if _verr 61 | @warning "validated user", _verr 62 | @main.emit "error", _verr 63 | next( false ) 64 | return 65 | 66 | @_getMessageContent( msgid, data, userdata, next ) 67 | return 68 | return 69 | 70 | checkMailBuffer: => 71 | @mailbuffer.listUsers ( err, users )=> 72 | if err 73 | @error "checkMailBuffer:listusers", err 74 | return 75 | @debug "checkMailBuffer:listUsers", users 76 | for user_id in users 77 | @worker.send( "crMsg", user: user_id ) 78 | return 79 | return 80 | 81 | composeMessageReport: ( data, next )=> 82 | uid = data.user 83 | @mailbuffer.userMsgs uid, ( err, msgs )=> 84 | if err 85 | @main.emit "error", err 86 | @warning "composeMessageReport", err 87 | next( false ) 88 | return 89 | @debug "composeMessageReport", msgs 90 | if not msgs.length 91 | @warning "User with no messgaes" 92 | @mailbuffer.removeUser uid, 0, ( err )=> 93 | if err 94 | @main.emit "error", err 95 | @warning "sendMail", err 96 | next( false ) 97 | return 98 | next() 99 | return 100 | return 101 | 102 | # get the userdata out of the first and newest message 103 | userdata = msgs[ 0 ].userdata 104 | @worker.send "sndMsg", { user: userdata, messages: _map( msgs, "message" ), composed: true }, ( err )=> 105 | if err 106 | @main.emit "error", err 107 | @warning "getMessageContent:sndMsg", err 108 | next( false ) 109 | return 110 | next() 111 | return 112 | return 113 | return 114 | 115 | sendMail: ( data, next )=> 116 | #@info "sendMail", data 117 | @main.emit "sendMail", data.user, data.messages, data.composed, ( err )=> 118 | if err 119 | @main.emit "error", err 120 | @warning "sendMail", err 121 | next( false ) 122 | return 123 | 124 | if not data.composed 125 | # if it's an immediate send 126 | next() 127 | return 128 | 129 | # on composed send remove the data after successful send 130 | @mailbuffer.removeUser data.user.id, data.messages.length, ( err )=> 131 | if err 132 | @main.emit "error", err 133 | @warning "sendMail:removeUser", err 134 | next( false ) 135 | return 136 | next() 137 | return 138 | return 139 | return 140 | 141 | _getMessageContent: ( msgid, data, userdata, next )=> 142 | #@info "_getMessageContent", data 143 | @main.emit "getContent", data.type, userdata, data.creator, data.additional, ( err, message )=> 144 | if err 145 | @main.emit "error", err 146 | @warning "getMessageContent", err 147 | next( false ) 148 | return 149 | 150 | _verr = validateMessageContent( message, true ) 151 | if _verr 152 | @warning "validated messagecontent", _verr 153 | @main.emit "error", _verr 154 | next( false ) 155 | return 156 | 157 | # set the message id to set a unique identifier 158 | message.id = msgid 159 | 160 | @main.emit "createNotification", userdata, data.creator, message, ( err )=> 161 | if err 162 | @main.emit "error", err 163 | @warning "createNotification", err 164 | next( false ) 165 | return 166 | 167 | # do not send mails if sendInterval is deactivated 168 | if userdata.sendInterval is "0" 169 | next() 170 | return 171 | 172 | # if the user has set "only prio" and the message is a non prio so stop here 173 | if not data.high and userdata.sendInterval is "p" 174 | next() 175 | return 176 | 177 | data.userdata = userdata 178 | 179 | # Send immediately if sendInterval ist set to i (immediately) or p (only prio) 180 | if userdata.sendInterval in [ "i", "p" ] 181 | 182 | @worker.send "sndMsg", { user: userdata, messages: [ message ], composed: false }, ( err )=> 183 | if err 184 | @main.emit "error", err 185 | @warning "getMessageContent:sndMsg", err 186 | next( false ) 187 | return 188 | next() 189 | return 190 | return 191 | 192 | data.message = message 193 | 194 | @mailbuffer.write data, ( err )=> 195 | if err 196 | @main.emit "error", err 197 | @warning "createNotification", err 198 | next( false ) 199 | return 200 | next() 201 | return 202 | 203 | return 204 | return 205 | return 206 | 207 | 208 | #export this class 209 | module.exports = RNTasks 210 | -------------------------------------------------------------------------------- /_src/lib/utils.coffee: -------------------------------------------------------------------------------- 1 | # # Utils 2 | # 3 | # ### Exports: *Object* 4 | # 5 | # A collection of helper functions 6 | 7 | # export the functions 8 | module.exports = 9 | ### 10 | ## randomString 11 | 12 | `utils.randomString( string_length, speciallevel )` 13 | 14 | Generate a random string 15 | 16 | @param { Number } string_length string length to generate 17 | @param { Number } speciallevel Level of complexity. 18 | * 0 = only letters upper and lowercase, 52 possible chars; 19 | * 1 = 0 + Numbers, 62 possible chars; 20 | * 2 = 1 + "_-@:.", 67 possible chars; 21 | * 3 = 2 + may speacial chars, 135 possible chars; 22 | 23 | @return { String } The gerated string 24 | ### 25 | randomString: ( string_length = 5, specialLevel = 0 ) -> 26 | chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 27 | chars += "0123456789" if specialLevel >= 1 28 | chars += "_-@:." if specialLevel >= 2 29 | chars += "!\"§$%&/()=?*'_:;,.-#+¬”#£fi^\\˜·¯˙˚«∑€®†Ω¨⁄øπ•‘æœ@∆ºª©ƒ∂‚å–…∞µ~∫√ç≈¥" if specialLevel >= 3 30 | 31 | randomstring = "" 32 | i = 0 33 | 34 | while i < string_length 35 | rnum = Math.floor(Math.random() * chars.length) 36 | randomstring += chars.substring(rnum, rnum + 1) 37 | i++ 38 | randomstring 39 | 40 | ### 41 | ## randRange 42 | 43 | `utils.randRange( lowVal, highVal )` 44 | 45 | Create a random number bewtween two values 46 | 47 | @param { Number } lowVal Min number 48 | @param { Number } highVal Max number 49 | 50 | @return { Number } A random number 51 | ### 52 | randRange: randRange = ( lowVal, highVal )-> 53 | return Math.floor( Math.random()*(highVal-lowVal+1 ))+lowVal 54 | 55 | ### 56 | ## randPick 57 | 58 | `utils.randPick( lowVal, highVal )` 59 | 60 | creates a function that picks a random element out of the given array 61 | 62 | @param { Number } arr The array of values to pick out 63 | 64 | @return { Function } A Function that returns a ranom value out of the given array 65 | ### 66 | randPick: ( arr )-> 67 | _l = arr.length - 1 68 | return -> 69 | return arr[ randRange( 0, _l ) ] 70 | 71 | ### 72 | ## lpad 73 | 74 | `utils.lpad( lowVal, highVal )` 75 | 76 | Left pad string 77 | 78 | @param { String } value The value to pad 79 | @param { Number } [padding=2] The padding size 80 | @param { String } [fill="0"] The filler value. 81 | 82 | @return { String } the padded value 83 | ### 84 | lpad: (value, padding = 2, fill = "0") -> 85 | fill += fill for i in [1..padding] 86 | return (fill + value).slice(padding * -1) 87 | 88 | ### 89 | ## truncate 90 | 91 | `utils.truncate( str [, len] [, tolerance] )` 92 | 93 | Truncate a string to the given `len` of chars and cut at the nearest following space. 94 | 95 | @param { String } value The string to truncate 96 | @param { Number } [len=100] The char count 97 | @param { String } [tolerance=5] The tolerance in percent to not truncate a string if it's length exceeds up to x%. 98 | 99 | @return { String } the truncated string 100 | ### 101 | truncate: ( str, len = 100, add = "...", tolerance = 5 )-> 102 | if not str? 103 | return "" 104 | if str.length > ( len * ( 1 + tolerance/100 ) ) 105 | return str.substr( 0, str.indexOf( " ", len ) ) + ( add or "" ) 106 | return str 107 | -------------------------------------------------------------------------------- /_src/lib/worker.coffee: -------------------------------------------------------------------------------- 1 | ## # RNWorker 2 | # ### extends [NPM:MPBasic](https://cdn.rawgit.com/mpneuried/mpbaisc/master/_docs/index.coffee.html) 3 | # 4 | # ### Exports: *Class* 5 | # 6 | # Main Module to init the notifications to redis 7 | # 8 | 9 | # **npm modules** 10 | # [NPM:rsmq-worker](https://cdn.rawgit.com/mpneuried/rsmq-worker/master/_docs/README.md.html) 11 | RSMQWorker = require( "rsmq-worker" ) 12 | 13 | class RNWorker extends require( "mpbasic" )() 14 | 15 | # ## defaults 16 | defaults: => 17 | @extend super, 18 | 19 | # **options.queuename** *String* The queuename to use for the worker 20 | queuename: "notifications" 21 | # **options.interval** *Number[]* An Array of increasing wait times in seconds 22 | interval: [ 0, 1, 5, 10 ] 23 | 24 | # **options.host** *String* Redis host name 25 | host: "localhost" 26 | # **options.port** *Number* Redis port 27 | port: 6379 28 | # **options.options** *Object* Redis options 29 | options: {} 30 | # **options.client** *RedisClient* Exsiting redis client instance 31 | client: null 32 | # **options.prefix** *String* A general redis prefix 33 | prefix: "notifications" 34 | 35 | ### 36 | ## constructor 37 | ### 38 | constructor: ( options )-> 39 | @ready = false 40 | super 41 | @worker = new RSMQWorker @config.queuename, 42 | interval: @config.interval 43 | maxReceiveCount: @config.maxReceiveCount 44 | invisibletime: @config.invisibletime 45 | defaultDelay: @config.defaultDelay 46 | timeout: @config.timeout 47 | customExceedCheck: @_customExceedCheck 48 | 49 | redis: @config.client 50 | redisPrefix: @config.prefix 51 | host: @config.host 52 | port: @config.port 53 | options: @config.options 54 | 55 | # wrap start method to only be active until the connection is established 56 | @worker.on "ready", @_start 57 | 58 | @worker.on "message", @_onMessage 59 | 60 | @worker.on "timeout", ( msg )=> 61 | @warning "task timeout", msg 62 | return 63 | 64 | @worker.start() 65 | return 66 | 67 | _start: => 68 | @ready = true 69 | @emit "ready" 70 | return 71 | 72 | _customExceedCheck: ( msg )-> 73 | if msg.message is "check" 74 | return true 75 | return false 76 | 77 | _doCheck: ( next )=> 78 | next( false ) 79 | @emit "checkMailBuffer" 80 | return 81 | 82 | send: ( type, msg, cb )=> 83 | @debug "send", type, msg 84 | @worker.send( JSON.stringify( mt: type, md: msg ), cb ) 85 | return 86 | 87 | _onMessage: ( msg, next, id )=> 88 | @debug "_onMessage", msg 89 | 90 | if msg is "check" 91 | @_doCheck( next ) 92 | return 93 | 94 | # dispatch the message 95 | _data = JSON.parse( msg ) 96 | try 97 | @emit _data.mt, _data.md, next, id 98 | catch _err 99 | @error "execute message", _err, _err.stack 100 | 101 | next( false ) 102 | return 103 | 104 | return 105 | 106 | getRsmqWorker: => 107 | return @worker 108 | 109 | getRsmq: => 110 | return @worker._getRsmq() 111 | 112 | getRedisNamespace: => 113 | return @getRsmq().redisns 114 | 115 | getRedis: => 116 | return @getRsmq().redis 117 | 118 | 119 | #export this class 120 | module.exports = RNWorker 121 | -------------------------------------------------------------------------------- /_src/test/example.coffee: -------------------------------------------------------------------------------- 1 | _map = require( "lodash/map" ) 2 | 3 | RedisNotifications = require( "../." ) 4 | utils = require( "../lib/utils" ) 5 | 6 | 7 | nf = new RedisNotifications() 8 | 9 | nf.init() 10 | # Hooks 11 | 12 | nf.on "error", ( err )-> 13 | console.log "\n\n --- EXAMPLE - READ USER --- \n", err, err.stack, "\n --- \n\n" 14 | 15 | console.trace() 16 | return 17 | 18 | 19 | pickSendIv = utils.randPick( [ "i", "d1700", "d1600", "d0900", "p", "0" ] ) 20 | nf.on "readUser", ( id, cb )-> 21 | # read the users settings 22 | console.log "\n\n --- EXAMPLE - READ USER --- \nUSER_ID", id, "\n --- \n\n" 23 | _user = 24 | id: id 25 | firstname: "John" 26 | lastname: "Do" 27 | email: "john.do@example.com" 28 | timezone: "CET" # possible timezones can be viewed at http://momentjs.com/timezone/docs/#/data-loading/getting-zone-names/ 29 | sendInterval: pickSendIv() 30 | custom_data: "ABC" 31 | 32 | ### `sendInterval` {i|d|p|0}{time} 33 | eg: d:1700 34 | 35 | i = immediately; 36 | d = daily 37 | p = only prio 38 | 0 = off 39 | 40 | time = 1700 - at 17:00 o clock. Required for `d` and optional for `p` 41 | ### 42 | 43 | cb( null, _user ) 44 | return 45 | 46 | nf.on "getContent", ( type, user, creator, additional, cb )-> 47 | console.log "\n\n --- EXAMPLE - GET CONTENT --- \nTYPE:", type, "\n --- \nUSER:", user, "\n --- \nCREATOR:", creator, "\n --- \nADDITIONAL:", additional, "\n --- \n\n" 48 | 49 | _content = 50 | subject: "This is my subject" 51 | body: "This is the whole content abc of this message." 52 | icon: additional.icon 53 | sid: additional.sid 54 | #teaser: "This is a short abstract of the content" # If not defined it will be generated by reducing the `body` to 100 words 55 | 56 | cb( null, _content ) 57 | return 58 | 59 | nf.on "createNotification", ( user, creator, message, cb )-> 60 | console.log "\n\n --- EXAMPLE - SEND NOTIFICATION --- \nID", message.id, "\n --- \nUSER:", user, "\n --- \nMSG:", message, "\n --- \nCREATOR:", creator, "\n --- \n\n" 61 | cb( null ) 62 | return 63 | 64 | nf.on "sendMail", ( user, messages, isReport, cb )-> 65 | console.log "\n\n --- EXAMPLE - SEND MAIL --- \nIDs", _map( messages, "id" ), "\n --- \nUSER:", user, "\n --- \nMSG:", messages,"\n --- \nISREPORT:", isReport, "\n --- \n\n" 66 | cb( null ) 67 | return 68 | 69 | pickPrio = utils.randPick( [ true, false ] ) 70 | # define a creator and write some messages 71 | _creator = 72 | id: "ABCDE" 73 | firstname: "William" 74 | lastname: "Creator" 75 | email: "william.create@example.com" 76 | 77 | _optionsMulti = 78 | type: "foo" 79 | users: [ "ABCD1", "ABCD2", "ABCD3", "ABCD4", "ABCD5" ] 80 | high: pickPrio() 81 | additional: 82 | sid: 1 83 | icon: "warning" 84 | 85 | _err = nf.createMulti( _creator, _optionsMulti ) 86 | console.log( "\n\n --- EXAMPLE - ERROR --- \n", _err, "\n --- \n\n" ) if _err? 87 | 88 | _optionsSingle = 89 | type: "bar" 90 | user: "WXYZ1" 91 | high: pickPrio() 92 | additional: 93 | sid: 1 94 | icon: "user" 95 | 96 | nf.create _creator, _optionsSingle, ( err, data )-> 97 | console.log "\n\n --- EXAMPLE - create --- \n", err, data, "\n --- \n\n" 98 | return 99 | 100 | 101 | #nf.getRsmqWorker().send( "check", 30 ) 102 | 103 | # Init and start the module 104 | nf.init() 105 | -------------------------------------------------------------------------------- /_src/test/main.coffee: -------------------------------------------------------------------------------- 1 | should = require('should') 2 | 3 | Module = require( "../." ) 4 | 5 | _moduleInst = null 6 | 7 | describe "----- redis-notifications TESTS -----", -> 8 | 9 | before ( done )-> 10 | _moduleInst = new Module() 11 | # TODO add initialisation Code 12 | done() 13 | return 14 | 15 | after ( done )-> 16 | # TODO teardown 17 | done() 18 | return 19 | 20 | describe 'Main Tests', -> 21 | 22 | # Implement tests cases here 23 | it "first test", ( done )-> 24 | done() 25 | return 26 | 27 | return 28 | return 29 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrow_spacing": { 3 | "level": "ignore" 4 | }, 5 | "braces_spacing": { 6 | "level": "ignore", 7 | "spaces": 0, 8 | "empty_object_spaces": 0 9 | }, 10 | "camel_case_classes": { 11 | "level": "error" 12 | }, 13 | "coffeescript_error": { 14 | "level": "error" 15 | }, 16 | "colon_assignment_spacing": { 17 | "level": "ignore", 18 | "spacing": { 19 | "left": 0, 20 | "right": 0 21 | } 22 | }, 23 | "cyclomatic_complexity": { 24 | "value": 10, 25 | "level": "ignore" 26 | }, 27 | "duplicate_key": { 28 | "level": "error" 29 | }, 30 | "empty_constructor_needs_parens": { 31 | "level": "ignore" 32 | }, 33 | "ensure_comprehensions": { 34 | "level": "warn" 35 | }, 36 | "indentation": { 37 | "value": 1, 38 | "level": "error" 39 | }, 40 | "line_endings": { 41 | "level": "ignore", 42 | "value": "unix" 43 | }, 44 | "max_line_length": { 45 | "value": 120, 46 | "level": "ignore", 47 | "limitComments": false 48 | }, 49 | "missing_fat_arrows": { 50 | "level": "ignore", 51 | "is_strict": false 52 | }, 53 | "newlines_after_classes": { 54 | "value": 3, 55 | "level": "ignore" 56 | }, 57 | "no_backticks": { 58 | "level": "error" 59 | }, 60 | "no_debugger": { 61 | "level": "warn" 62 | }, 63 | "no_empty_functions": { 64 | "level": "ignore" 65 | }, 66 | "no_empty_param_list": { 67 | "level": "ignore" 68 | }, 69 | "no_implicit_braces": { 70 | "level": "ignore", 71 | "strict": true 72 | }, 73 | "no_implicit_parens": { 74 | "strict": true, 75 | "level": "ignore" 76 | }, 77 | "no_interpolation_in_single_quotes": { 78 | "level": "ignore" 79 | }, 80 | "no_plusplus": { 81 | "level": "ignore" 82 | }, 83 | "no_stand_alone_at": { 84 | "level": "ignore" 85 | }, 86 | "no_tabs": { 87 | "level": "ignore" 88 | }, 89 | "no_throwing_strings": { 90 | "level": "error" 91 | }, 92 | "no_trailing_semicolons": { 93 | "level": "error" 94 | }, 95 | "no_trailing_whitespace": { 96 | "level": "error", 97 | "allowed_in_comments": true, 98 | "allowed_in_empty_lines": true 99 | }, 100 | "no_unnecessary_double_quotes": { 101 | "level": "ignore" 102 | }, 103 | "no_unnecessary_fat_arrows": { 104 | "level": "warn" 105 | }, 106 | "non_empty_constructor_needs_parens": { 107 | "level": "ignore" 108 | }, 109 | "prefer_english_operator": { 110 | "level": "ignore", 111 | "doubleNotLevel": "ignore" 112 | }, 113 | "space_operators": { 114 | "level": "ignore" 115 | }, 116 | "spacing_after_comma": { 117 | "level": "ignore" 118 | }, 119 | "transform_messes_up_line_numbers": { 120 | "level": "warn" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-notifications", 3 | "version": "0.2.1", 4 | "description": "A redis based notification engine. It implements the rsmq-worker to savely create notifications and recurring reports", 5 | "keywords": [], 6 | "homepage": "https://github.com/mpneuried/redis-notifications", 7 | "bugs": "https://github.com/mpneuried/redis-notifications/issues", 8 | "author": { 9 | "name": "mpneuried", 10 | "email": "", 11 | "url": "https://github.com/redis-notifications" 12 | }, 13 | "main": "./index.js", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/mpneuried/redis-notifications.git" 17 | }, 18 | "scripts": { 19 | "test": "grunt test" 20 | }, 21 | "engines": { 22 | "node": ">= 0.10.0" 23 | }, 24 | "license": "MIT", 25 | "dependencies": { 26 | "mpbasic": "0.0.x", 27 | "rsmq-worker": "0.5.x", 28 | "lodash": "4.x", 29 | "moment-timezone": "0.5.x", 30 | "obj-schema": "1.2.x" 31 | }, 32 | "devDependencies": { 33 | "should": "11.x", 34 | "grunt": "1.0.x", 35 | "grunt-contrib-watch": "1.0.x", 36 | "grunt-contrib-coffee": "1.x", 37 | "grunt-include-replace": "5.x", 38 | "grunt-mocha-cli": "2.x", 39 | "grunt-contrib-clean": "1.x" 40 | } 41 | } 42 | --------------------------------------------------------------------------------