├── 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 |
14 |
15 |
18 | {{/if}}
19 |
20 |
21 | {{#each groups}}
22 | {{> group}}
23 | {{/each}}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{name}}
33 |
42 |
43 |
46 |
47 |
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 | }
--------------------------------------------------------------------------------