├── README.md ├── GroupMeteor.html ├── LICENSE ├── GroupMeteor.css └── GroupMeteor.js /README.md: -------------------------------------------------------------------------------- 1 | A group messaging application built using the Meteor framework, and MongoDB. 2 | Powered by Twilio. 3 | -------------------------------------------------------------------------------- /GroupMeteor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | GroupMeteor 4 | 5 | 6 | 7 |
8 |
9 | {{> loginButtons}} 10 | {{#if currentUser}} 11 |
12 | 13 |
14 | 15 |
16 | 17 |
18 | {{/if}} 19 |
20 | 25 |
26 | 27 | 28 | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Christopher Hranj 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of GroupMeteor nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /GroupMeteor.css: -------------------------------------------------------------------------------- 1 | /* GroupMeteor.css */ 2 | body { 3 | font-family: sans-serif; 4 | background-color: #315481; 5 | background-image: linear-gradient(to bottom, #6C0003, #F06366 100%); 6 | background-attachment: fixed; 7 | 8 | position: absolute; 9 | top: 0; 10 | bottom: 0; 11 | left: 0; 12 | right: 0; 13 | 14 | padding: 0; 15 | margin: 0; 16 | 17 | font-size: 14px; 18 | } 19 | 20 | .container { 21 | max-width: 600px; 22 | margin: 0 auto; 23 | min-height: 100%; 24 | background: white; 25 | } 26 | 27 | header { 28 | background: #E12127; 29 | padding: 20px 15px 15px 15px; 30 | position: relative; 31 | } 32 | 33 | #login-buttons { 34 | 35 | } 36 | 37 | h1 { 38 | font-size: 1.5em; 39 | margin: 0; 40 | margin-bottom: 10px; 41 | display: inline-block; 42 | margin-right: 1em; 43 | color: #FFFFFF; 44 | } 45 | 46 | .bold{ 47 | font-weight: bold; 48 | } 49 | 50 | form { 51 | margin-top: 10px; 52 | margin-bottom: -10px; 53 | position: relative; 54 | color:white; 55 | } 56 | 57 | .new-group input { 58 | box-sizing: border-box; 59 | padding: 10px 0; 60 | background: transparent; 61 | border: none; 62 | width: 100%; 63 | padding-right: 80px; 64 | font-size: 1em; 65 | color: white; 66 | } 67 | 68 | .new-group input:focus{ 69 | outline: 0; 70 | 71 | } 72 | 73 | .new-number input { 74 | box-sizing: border-box; 75 | padding: 10px 0; 76 | background: transparent; 77 | border: none; 78 | width: 100%; 79 | padding-right: 80px; 80 | font-size: 1em; 81 | } 82 | 83 | .new-number input:focus{ 84 | outline: 0; 85 | } 86 | 87 | .new-text input { 88 | box-sizing: border-box; 89 | padding: 10px 0; 90 | background: transparent; 91 | border: none; 92 | width: 100%; 93 | padding-right: 80px; 94 | font-size: 1em; 95 | color: white; 96 | 97 | } 98 | 99 | .new-text input:focus{ 100 | outline: 0; 101 | } 102 | 103 | ul { 104 | margin: 0; 105 | padding: 0; 106 | background: white; 107 | } 108 | 109 | .delete-group { 110 | float: right; 111 | font-weight: bold; 112 | background: none; 113 | font-size: 1em; 114 | border: none; 115 | position: relative; 116 | } 117 | 118 | .delete-number { 119 | float: right; 120 | margin-right: 20px; 121 | font-weight: bold; 122 | background: none; 123 | font-size: 1em; 124 | border: none; 125 | position: relative; 126 | } 127 | 128 | li { 129 | position: relative; 130 | list-style: none; 131 | padding: 15px; 132 | border-bottom: #eee solid 1px; 133 | } 134 | 135 | li .text { 136 | margin-left: 10px; 137 | } 138 | 139 | @media (max-width: 600px) { 140 | li { 141 | padding: 12px 15px; 142 | } 143 | 144 | .new-task input { 145 | padding-bottom: 5px; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /GroupMeteor.js: -------------------------------------------------------------------------------- 1 | // GroupMeteor.js 2 | Groups = new Mongo.Collection("groups"); 3 | 4 | if (Meteor.isClient) { 5 | // Specify which collections from the server the client subscribes to 6 | Meteor.subscribe("groups"); 7 | 8 | Template.body.helpers({ 9 | groups: function () { 10 | // Find all groups and list the newest groups first 11 | return Groups.find({}, {sort: {createdAt: -1}}); 12 | } 13 | }); 14 | 15 | Template.body.events({ 16 | "submit .new-group": function (event) { 17 | // Grab group name from text field 18 | var newGroup = event.target.group.value; 19 | // Check that text field is not blank before adding group 20 | if (newGroup !== '') { 21 | Meteor.call("addGroup", newGroup); 22 | } 23 | // Clear the text field for next entry 24 | event.target.group.value = ""; 25 | // Prevent default form submit 26 | return false; 27 | }, 28 | "submit .new-number": function (event) { 29 | // Grab phone number from text field 30 | var newNumber = event.target.number.value; 31 | // Check that text field is not blank before adding number 32 | if (newNumber !== '') { 33 | Meteor.call("addNumber", this._id, newNumber); 34 | } 35 | // Clear the text field for next entry 36 | event.target.number.value = ""; 37 | // Prevent default form submit 38 | return false; 39 | }, 40 | "submit .new-text": function (event) { 41 | // Grab text message from text field 42 | var newMessage = event.target.message.value; 43 | // Check that message field is not blank before sending texts 44 | if (newMessage !== '') { 45 | Meteor.call("sendMessage", newMessage); 46 | } 47 | // Clear the text field 48 | event.target.message.value = ""; 49 | alert('Your message is being sent!'); 50 | // Prevent default form submit 51 | return false; 52 | } 53 | }); 54 | 55 | Template.group.events({ 56 | "click .toggle-group": function () { 57 | // Set the checked property to the opposite of its current value 58 | Meteor.call("toggleGroup", this._id, !this.checked); 59 | }, 60 | "click .toggle-number": function () { 61 | // Get the number's group data 62 | var data = Template.instance().data; 63 | // Set the checked property to the opposite of its current value 64 | Meteor.call("toggleNumber", data._id, this.number, !this.checked); 65 | }, 66 | "click .delete-group": function () { 67 | // Remove a group from our collection 68 | Meteor.call("deleteGroup", this._id); 69 | }, 70 | "click .delete-number": function () { 71 | // Get the number's group data 72 | var group = Template.instance().data; 73 | // Remove a number from a particular group 74 | Meteor.call("deleteNumber", group._id, this.number); 75 | } 76 | }); 77 | // Configure Accounts to require username instead of email 78 | Accounts.ui.config({ 79 | passwordSignupFields: "USERNAME_ONLY" 80 | }); 81 | } 82 | 83 | if (Meteor.isServer) { 84 | // Specify which collections are sent to the client 85 | Meteor.publish("groups", function () { 86 | return Groups.find({ 87 | owner: this.userId 88 | }); 89 | }); 90 | 91 | Meteor.methods({ 92 | addGroup: function (name) { 93 | Groups.insert({ 94 | name: name, 95 | createdAt: new Date(), 96 | owner: Meteor.userId(), 97 | checked: false, 98 | numbers: [] 99 | }); 100 | }, 101 | addNumber: function (groupId, number) { 102 | Groups.update( 103 | {_id: groupId}, 104 | {$addToSet: {numbers: {"number": number, "checked": true }}} 105 | ); 106 | }, 107 | deleteGroup: function (groupId) { 108 | Groups.remove( 109 | {_id: groupId} 110 | ); 111 | }, 112 | deleteNumber: function (groupId, number) { 113 | Groups.update( 114 | {_id: groupId}, 115 | { $pull: { numbers: {"number": number}}} 116 | ); 117 | }, 118 | toggleGroup: function (groupId, toggle) { 119 | Groups.update( 120 | {_id: groupId}, 121 | { $set: { checked: toggle}} 122 | ); 123 | // Find every number that differs from Group's "checked" boolean 124 | var numbers = 125 | Groups.find( 126 | {numbers: { $elemMatch: {"checked": !toggle}}} 127 | ); 128 | // Set all numbers to match Group's "checked" boolean 129 | numbers.forEach(function (setter) { 130 | for (var index in setter.numbers) { 131 | Groups.update( 132 | { _id: groupId, "numbers.number": setter.numbers[index].number }, 133 | { $set: {"numbers.$.checked": toggle} } 134 | ); 135 | } 136 | }); 137 | }, 138 | toggleNumber: function (groupId, number, toggle) { 139 | Groups.update( 140 | { _id: groupId, "numbers.number": number }, 141 | { $set: {"numbers.$.checked": toggle} } 142 | ); 143 | }, 144 | sendMessage: function (outgoingMessage) { 145 | var phonebook = []; 146 | // Find all checked numbers across all groups 147 | var recipients = 148 | Groups.find( 149 | {numbers: { $elemMatch: {"checked": true}}} 150 | ); 151 | // Add each number from our query to our phonebook 152 | recipients.forEach(function (recipient) { 153 | for (var index in recipient.numbers) { 154 | phonebook.push(recipient.numbers[index].number); 155 | } 156 | }); 157 | // Place all numbers in a Set so no number is texted more than once 158 | var uniquePhoneBook = new Set(phonebook); 159 | // Use Twilio REST API to text each number in the unique phonebook 160 | uniquePhoneBook.forEach(function (number) { 161 | HTTP.call( 162 | "POST", 163 | 'https://api.twilio.com/2010-04-01/Accounts/' + 164 | process.env.TWILIO_ACCOUNT_SID + '/SMS/Messages.json', { 165 | params: { 166 | From: process.env.TWILIO_NUMBER, // Your Twilio number. Use environment variable 167 | To: number, 168 | Body: outgoingMessage 169 | }, 170 | // Set your credentials as environment variables 171 | // so that they are not loaded on the client 172 | auth: 173 | process.env.TWILIO_ACCOUNT_SID + ':' + 174 | process.env.TWILIO_AUTH_TOKEN 175 | }, 176 | // Print error or success to console 177 | function (error) { 178 | if (error) { 179 | console.log(error); 180 | } 181 | else { 182 | console.log('SMS sent successfully.'); 183 | } 184 | } 185 | ); 186 | }); 187 | } 188 | }); 189 | } --------------------------------------------------------------------------------