1 | <img src="https://github.com/antiSocialNet/antiSocial/raw/master/assets/octocloud/logo.jpg" height="200">
|
2 |
|
3 | # antiSocial
|
4 |
|
5 | Building blocks for myAntiSocial.net
|
6 |
|
7 | ## antisocial-friends
|
8 |
|
9 | This module mounts routes for any expressjs application wishing to support building and maintaining antisocial 'friend' relationships whether on the same server/application and/or across distributed servers and applications. The protocol generates key pairs unique to the friend relationship and exchanges public keys for later use in exchanging user to user encrypted messages over socket.io connections.
|
10 |
|
11 | ```
|
12 | var antisocial = require('antisocial-friends');
|
13 | var antisocialApp = antisocial(app, config, dbAdaptor, getAuthenticatedUser, listener);
|
14 | ```
|
15 |
|
16 | ### Parameters:
|
17 |
|
18 | **app** is the express application
|
19 |
|
20 | **config** is a javascript object with the following properties
|
21 |
|
22 | ```
|
23 | var config = {
|
24 | 'APIPrefix': '/antisocial', // where to mount the routes
|
25 | 'publicHost': 'http://127.0.0.1:3000', // public protocol and host
|
26 | 'port': 3000 // port this service is listening on (only used if behind a load balancer or reverse proxy)
|
27 | }
|
28 | ```
|
29 |
|
30 | **dbAdaptor** is an abstract data store that will be called when the app needs to
|
31 | store or retrieve data from the database. For a simple example implementation
|
32 | see app.js for a simple memory store implementation of the methods and a loopback
|
33 | application adaptor below. Yours should implement the required methods to work
|
34 | within your environment (mysql, mongo etc.)
|
35 |
|
36 | **getAuthenticatedUser** is an express middleware function that gets the current
|
37 | logged in user and exposes it on req.antisocialUser. This is application specific but typically would be a token cookie that can be used to look up a user. See simple example in app.js
|
38 |
|
39 | **listener** is an express http(s) listener for setting up socket.io listeners
|
40 |
|
41 | This function returns an **antisocialApp** object which an EventEmitter.
|
42 |
|
43 | ## User Endpoints
|
44 |
|
45 | Endpoints are URLS that function as the base address of a user on an antisocial aware server.
|
46 |
|
47 | 'https://some.antisocial.server/api-prefix/local-username'
|
48 |
|
49 | The URL form of the endpoints conform to the following conventions:
|
50 |
|
51 | `api-prefix` is the location where antisocial-friends is mounted defined in config
|
52 |
|
53 | `local-username` is a unique username for the local user
|
54 |
|
55 | ### On the requestor's server
|
56 | The following API endpoints are mounted
|
57 |
|
58 | #### make a friend request
|
59 | ```
|
60 | GET /api-prefix/local-username/request-friend
|
61 | Query params:
|
62 | endpoint: endpoint url of friend to connect with
|
63 | invite: an invite token if this is in response to an invitation from the user
|
64 | ```
|
65 |
|
66 | #### cancel a pending friend request
|
67 | ```
|
68 | POST /api-prefix/local-username/request-friend-cancel
|
69 | Request Body: (application/x-www-form-urlencoded)
|
70 | endpoint: endpoint of the pending request
|
71 | ```
|
72 |
|
73 | ### On the requestee's server
|
74 | The following API endpoints are mounted
|
75 |
|
76 | #### accept a pending friend request
|
77 | ```
|
78 | /api-prefix/local-username/friend-request-accept
|
79 | Request Body: (application/x-www-form-urlencoded)
|
80 | endpoint: endpoint of the pending request
|
81 | ```
|
82 |
|
83 | #### decline a pending friend request
|
84 | ```
|
85 | /api-prefix/local-username/friend-request-decline
|
86 | Request Body: (application/x-www-form-urlencoded)
|
87 | endpoint: endpoint of the pending request
|
88 | ```
|
89 |
|
90 | ### either side can update or delete the relationship once accepted
|
91 | ```
|
92 | POST /api-prefix/local-username/friend-update
|
93 | Request Body: (application/x-www-form-urlencoded)
|
94 | endpoint: endpoint of the friend
|
95 | status: 'delete'|'block'
|
96 | audiences: array of audiences for the friend eg. ["public","friends","some custom audience"]
|
97 | ```
|
98 |
|
99 | ## socket.io feeds
|
100 |
|
101 | Accepted friends establish socket.io connections to update each other about activity. Posts, photos, IM etc are sent to the friend or groups of friends in audiences. The details of the messages are application specific but the mechanism for sending and responding to messages is driven by `data` and `backfill` events received by the application.
|
102 |
|
103 | ## Events
|
104 | You can handle the following events as needed.
|
105 |
|
106 | ### new-friend-request: a new friend request received
|
107 | Relevant details are in user (a user instance) and friend (a friend instance). Typically would be used to notify the user of the pending friend request.
|
108 | ```
|
109 | antisocialApp.on('new-friend-request', function (user, friend) {
|
110 | console.log('antisocial new-friend-request %s %j', user.username, friend.remoteEndPoint);
|
111 | });
|
112 | ```
|
113 |
|
114 | ### new-friend: a new friend
|
115 | Relevant details are in user (a user instance) and friend (a friend instance). Typically would be used to notify the user that their requests has been approved and the user's friends that they have a new friend.
|
116 | ```
|
117 | antisocialApp.on('new-friend', function (user, friend) {
|
118 | console.log('antisocial new-friend %s %j', user.username, friend.remoteEndPoint);
|
119 | });
|
120 | ```
|
121 |
|
122 | ### friend-updated: the relationship has changed
|
123 | Relevant details are in user (a user instance) and friend (a friend instance). The friend might have changed the audiences for the user. Typically the user would remove any cache of activity originating with the friend and re-load by requesting emitting a highwater event.
|
124 | ```
|
125 | antisocialApp.on('friend-updated', function (user, friend) {
|
126 | console.log('antisocial friend-updated %s %s', user.username, friend.remoteEndPoint);
|
127 | });
|
128 | ```
|
129 |
|
130 | ### friend-deleted: either user or the friend has deleted the other
|
131 | Typically user would clean up the database and remove any activity about or by the friend.
|
132 | ```
|
133 | antisocialApp.on('friend-deleted', function (user, friend) {
|
134 | console.log('antisocial friend-deleted %s %s', user.username, friend.remoteEndPoint);
|
135 | });
|
136 | ```
|
137 |
|
138 | ### open-activity-connection event: a friend activity feed has been connected.
|
139 | Typically would hook up any process that would create activity messages to be transmitted to friends. Could also be used to send a 'highwater' event to the friend to request activity since last logged in.
|
140 | ```
|
141 | antisocialApp.on('open-activity-connection', function (user, friend, emitter) {
|
142 | console.log('antisocial open-activity-connection %s<-%s', user.username, friend.remoteEndPoint);
|
143 | emitter('post', 'highwater', highwater);
|
144 | });
|
145 | ```
|
146 |
|
147 | ### user can send data to a friend using emitter function specifying an appId
|
148 | The appId indicates the class or type of message we are sending (eg. post, reply, photo, IM)
|
149 |
|
150 | Parameters:
|
151 | `appId` is used to direct messages to the appropriate listeners (see activity-data-xxx event)
|
152 | `eventType` 'data' or 'highwater'
|
153 | `message object` JSON object to transmit
|
154 |
|
155 | ```
|
156 | emitter(appId, eventType, {application specific message});
|
157 | ```
|
158 |
|
159 | ### activity-data-xxx event: xxx is the appId defined in the emitter call
|
160 | Friend has sent user a message. Application would typically keep track of highwater mark of last message seen so the user can request a backfill from the friend of activity since the user was last connected.
|
161 | ```
|
162 | antisocialApp.on('activity-data-appId', function (user, friend, data) {
|
163 | console.log('antisocial activity-data-post user: %s friend: %s data: %j', user.name, friend.remoteEndPoint, data);
|
164 | });
|
165 | ```
|
166 |
|
167 | ### activity-backfill-xxx event: xxx is the appId
|
168 | Typically would send any activity that has happened since the last message received by the friend (highwater). This could be a timestamp or a record number, it's up to the application to define this behavior
|
169 |
|
170 | ```
|
171 | antisocialApp.on('activity-backfill-appId', function (user, friend, highwater, emitter) {
|
172 | console.log('antisocial activity-backfill-post user: %s friend: %s highwater: %s', user.name, friend.remoteEndPoint, highwater);
|
173 |
|
174 | // send posts from requested highwater to end of posts
|
175 | emitter('post', 'data', {application specific message});
|
176 | });
|
177 | ```
|
178 |
|
179 | ### close-activity-connection: activity feed closed.
|
180 | Typically used to clean up any event handlers set up in open-activity-connection.
|
181 | ```
|
182 | antisocialApp.on('close-activity-connection', function (user, friend, reason) {
|
183 | console.log('antisocial close-activity-connection %s<-%s %s', user.username, friend.remoteEndpoint, reason);
|
184 | });
|
185 | ```
|
186 |
|
187 | ### open-notification-connection event: The user has opened the notification feed.
|
188 | A user has subscribed to notifications using browser or app.
|
189 | ```
|
190 | antisocialApp.on('open-notification-connection', function (user, emitter) {
|
191 | console.log('antisocial open-notification-connection %s', user.username);
|
192 | });
|
193 | ```
|
194 |
|
195 | ### notification-data event:
|
196 | User has sent server a message.
|
197 | ```
|
198 | antisocialApp.on('notification-data', function (user, friend, data) {
|
199 | console.log('antisocial notification-data user: %s friend: %s data: %j', user.name, friend.remoteEndPoint, data);
|
200 | });
|
201 | ```
|
202 |
|
203 | ### notification-backfill-xxx event: xxx is the appId defined in the emit
|
204 | Typically would send any notifications that has happened since the last notification was received by the user (highwater). This could be a timestamp or a record number, it's up to the application to define this behavior.
|
205 |
|
206 | ```
|
207 | antisocialApp.on('notification-backfill-appId', function (user, highwater, emitter) {
|
208 | console.log('antisocial notification-backfill-post user: %s highwater: %s', user.name, highwater);
|
209 |
|
210 | // send posts from requested highwater to end of posts
|
211 | emitter('post', 'data', {application specific message});
|
212 | });
|
213 | ```
|
214 |
|
215 | ### close-notification-connection event
|
216 | ```
|
217 | antisocialApp.on('close-notification-connection', function (user, reason) {
|
218 | console.log('antisocial close-notification-connection %s %s', user.username, reason);
|
219 | });
|
220 | ```
|
221 |
|
222 | #### establish activity feed connection
|
223 | Once connected this will emit an 'open-notification-connection' event on the antisocalApp so the host application can set up its protocol for exchanging messages.
|
224 | ```
|
225 | antisocialApp.activityFeed.connect(user, friend);
|
226 | ```
|
227 |
|
228 | ## The data structures maintained by these protocols
|
229 | This app uses the following data collections:
|
230 |
|
231 | * users: username property is required to build urls
|
232 | * friends: several properties maintained by the antisocial protocol
|
233 | * invitations: use to simplify "be my friend" invitations
|
234 | * blocks: list of blocked friends
|
235 |
|
236 | friends, invitations and blocks are related to users by a foreign key `userId`
|
237 | which is set to the id of the appropriate user when created.
|
238 |
|
239 | The schema definition is implementation specific and up to the implementor.
|
240 | The following is an example db adaptor for a Loopback.io application. dbHandlers
|
241 | must support all the methods in this example.
|
242 | ```
|
243 | function dbHandler() {
|
244 | var self = this;
|
245 |
|
246 | self.models = {
|
247 | 'users': 'MyUser',
|
248 | 'friends': 'Friend',
|
249 | 'invitations': 'Invite',
|
250 | 'blocks': 'Block'
|
251 | };
|
252 |
|
253 | // store an item
|
254 | this.newInstance = function (collectionName, data, cb) {
|
255 | server.models[self.models[collectionName]].create(data, function (err, instance) {
|
256 | if (cb) {
|
257 | cb(err, instance);
|
258 | }
|
259 | else {
|
260 | return instance;
|
261 | }
|
262 | });
|
263 | };
|
264 |
|
265 | // get an item by matching some properties.
|
266 | // pairs are a list of property/value pairs that are anded
|
267 | // when querying the database example:
|
268 | // [
|
269 | // { 'property':'userId', 'value': 1 },
|
270 | // { 'property':'localAccessToken', 'value': 'jhgasdfjhgsdfjhg' }
|
271 | // ]
|
272 | this.getInstances = function (collectionName, pairs, cb) {
|
273 | var query = {
|
274 | 'where': {
|
275 | 'and': []
|
276 | }
|
277 | };
|
278 |
|
279 | for (var i = 0; i < pairs.length; i++) {
|
280 | var prop = pairs[i].property;
|
281 | var value = pairs[i].value;
|
282 | var pair = {};
|
283 | pair[prop] = value;
|
284 | query.where.and.push(pair);
|
285 | }
|
286 |
|
287 | server.models[self.models[collectionName]].find(query, function (err, found) {
|
288 | if (cb) {
|
289 | cb(err, found);
|
290 | }
|
291 | else {
|
292 | return found;
|
293 | }
|
294 | });
|
295 | };
|
296 |
|
297 | // update item properties by id
|
298 | this.updateInstance = function (collectionName, id, patch, cb) {
|
299 | server.models[self.models[collectionName]].findById(id, function (err, instance) {
|
300 | if (err) {
|
301 | return cb(new Error('error reading ' + collectionName));
|
302 | }
|
303 | if (!instance) {
|
304 | return cb(new Error('error ' + collectionName + ' id ' + id + ' not found'));
|
305 | }
|
306 |
|
307 | instance.updateAttributes(patch, function (err, updated) {
|
308 | if (err) {
|
309 | return cb(new Error('error updating ' + collectionName));
|
310 | }
|
311 | if (cb) {
|
312 | cb(null, updated);
|
313 | }
|
314 | else {
|
315 | return updated;
|
316 | }
|
317 | });
|
318 | });
|
319 | };
|
320 |
|
321 | this.deleteInstance = function (collectionName, id, cb) {
|
322 | server.models[self.models[collectionName]].destroyById(id, function (err) {
|
323 | if (cb) {
|
324 | cb(err);
|
325 | }
|
326 | });
|
327 | };
|
328 | }
|
329 |
|
330 | db = new dbHandler();
|
331 | ```
|
332 |
|
333 | ### User properties
|
334 | The user is application specific but we expect the following properties
|
335 | ```
|
336 | {
|
337 | "name": "user one",
|
338 | "username": "user-one",
|
339 | "id": "c5503436-634c-461f-9d90-ba9e438516c1"
|
340 | }
|
341 | ```
|
342 |
|
343 | ### Friend Invitation properties
|
344 | ```
|
345 | {
|
346 | "token": "testinvite",
|
347 | "userId": "53757323-8555-4ef5-b66c-a58351ce6181",
|
348 | "id": "6165e25e-2c31-47d3-a8c2-7c75737d4003"
|
349 | }
|
350 | ```
|
351 | ### Block list properties
|
352 | ```
|
353 | {
|
354 | "remoteEndPoint": "http://127.0.0.1:3000/antisocial/user-three",
|
355 | "userId": "53757323-8555-4ef5-b66c-a58351ce6181",
|
356 | "id": "7281bcd3-94f8-4906-9714-89d7e5b5c349"
|
357 | }
|
358 | ```
|
359 |
|
360 | ### Friend Properties
|
361 | The result of a friend request and a friend accept is 2 friends records, one owned by the requestor and one by the requestee (who could be on different servers). The requestor's is marked as 'originator'. The structure contains exchanged key pairs that can be used to communicate securely.
|
362 |
|
363 | ```
|
364 | [
|
365 | {
|
366 | "originator": true,
|
367 | "status": "accepted",
|
368 | "remoteEndPoint": "http://127.0.0.1:3000/antisocial/user-two",
|
369 | "remoteHost": "http://127.0.0.1:3000",
|
370 | "localRequestToken": "cbf875ad-5eb7-43e4-8028-415ddf6d95a9",
|
371 | "localAccessToken": "fbb8d3be-b199-45a6-b46e-3bcbbfedd0aa",
|
372 | "keys": {
|
373 | "public": "-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3BotZWYZu/rtFqTOHpdzM9+b2m7c\r\n7I2CtkZrnJ5zPQzmXDg9gLWqImAFrqcR3Ee3LMLniuKsFSYz2I/ERmXiXzv2e8wr14AeuXOV\r\nFzqcsDypKrbtT88lZLor6bt0kQOP7pFcesOedocoU9/DpnRkOYeI9MHsZN1pyZVvzLfkHvdL\r\n08ktiWwjNoFV8EL2h13sVZIFt/GaoPrv/SeWzb9oyAGAcp671smBsExCsafgwXAKYBQHIzrI\r\nScxNBzP2d9/z1ZKQ/dtXbGvWheZ0Ci1G6ngYSdx8APBIRFK+hhRdnhhpbat5juWoMs2dTG8q\r\nWIu45ntjfg7BHLRLExRE6un5lwIDAQAB\r\n-----END PUBLIC KEY-----\r\n",
|
374 | "private": "-----BEGIN RSA PRIVATE KEY-----\r\nMIIEowIBAAKCAQEA3BotZWYZu/rtFqTOHpdzM9+b2m7c7I2CtkZrnJ5zPQzmXDg9gLWqImAF\r\nrqcR3Ee3LMLniuKsFSYz2I/ERmXiXzv2e8wr14AeuXOVFzqcsDypKrbtT88lZLor6bt0kQOP\r\n7pFcesOedocoU9/DpnRkOYeI9MHsZN1pyZVvzLfkHvdL08ktiWwjNoFV8EL2h13sVZIFt/Ga\r\noPrv/SeWzb9oyAGAcp671smBsExCsafgwXAKYBQHIzrIScxNBzP2d9/z1ZKQ/dtXbGvWheZ0\r\nCi1G6ngYSdx8APBIRFK+hhRdnhhpbat5juWoMs2dTG8qWIu45ntjfg7BHLRLExRE6un5lwID\r\nAQABAoIBAEUvFUXiKgSkgxGzC/chs9yCVQL8BgV1FbkluX2pcJ+oBmDGbM6gS7IybJbRfRO4\r\nlyNCwHUvetfLAlD4H8HhFJ7Kwld3ffBnHUE9y4dZrRbYenQqu71yZ1aaDmORwLo0XHGoz2Dn\r\nTFAFe++hTmZr/3T13V7R9fRehHoQtuuqgdIZZAoshX90JpIhJ9Px6F1scgWgmBRH3XcsCbxb\r\nOMjABrVFINv5YUANRwUAwC2DYUBEuptRnEtm4X3++Afg97hK2brR9ofgpw44ej7JhovHeZti\r\n1Xm0AhqP/T6GQa55MS0rPryq5bbIjr/SBqJr4VkAmJJMx+P8KOa8gfBPefj9cgECgYEA+4Bz\r\ntT86OY/Pa+QJTSXZsskCqVlCQbj1V3CAt5dwXXir9NEwlE6yrc2jReqq07bDdqeadQUEwTaZ\r\nyioXxFozRNhs0rQPBoKLB0HuFqOm8GULcB0m3ScWJec5Rz9TCQQrMdUDQaZTLCXQs9izcVmd\r\nhBT1SLLZ3zjJt7hKVwafkqECgYEA4An08IkbEjwfcq7zOHNzh5Dm4G9jRW62vx5WFL+xktl6\r\nDg14IXMH7TVg2Gl96U40JZDBQW2bLH6pcGLp5UAj9ZqtoVW9BN+LZ3uY25hCEhYfAE0zVhhc\r\nE0VLkdyKp7wSQFicqAfPjryMsFTIgCrywo1BxxMibe773ai26YL32TcCgYEA2MOkdrGxGE2X\r\no9DeJ20ZDdvr/FPfJFAqvRtNBW9zvEw2QQJPkXOm0t/q+mbAp0rdexYHrRYPPAw4TqMq6uQn\r\nTg4O9SeVz7GR7EZp039ncchVLGMjzPZUQ4TfvEWa5ql+JSwH63xUMTfCgk+ikW6AsYdyxR7J\r\nY3hJe5xODmW6ASECgYBpPuQs9wublljjpCIn+7xjDAQZnNoSrP72a0be+mpt5PI8lcFAXWx0\r\n16WGJJB8wDspBoZyuQ2zalEotZ7RDj+WSjKU3tUr6+PuGhbl2fH30yJ/HsUmBc2DVAM7I1KT\r\nl3svdTEqknjDwfmJgFqsMwDVukwTO/7pi+IP8Aj1S4wpIwKBgDJwDd0eNPhMRf1wi+rfTefL\r\nb1lo/sSw0/vgyH2GKLnY+svw7p2k2GoPvbT7AEkUFDxh3HYbdKPx2hUO0rbkM94FyA8eSUU4\r\n+osMYDv8bIMwve7dfmRKl7vN2TWhkFhjREwPdFf6xbCV91BstV/qcCrOJJiXYmCrQODZ2qHJ\r\nvvOf\r\n-----END RSA PRIVATE KEY-----\r\n"
|
375 | },
|
376 | "audiences": ["public", "friends"],
|
377 | "hash": "f08bf78",
|
378 | "userId": "7ca18fd6-5d23-4930-8b64-3812f3a8f012",
|
379 | "inviteToken": "testinvite",
|
380 | "id": "97c46a56-c323-4941-bf61-e2c959748617",
|
381 | "remoteAccessToken": "aabaa39b-cb60-4f29-948f-8483bc29f21c",
|
382 | "remotePublicKey": "-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAihLKPC8s1fIi4IR33n8iGA+VInab\r\niN0PzqiI7iPPJlfApwPIrxIVD+SvQXlMOOQNsh8sOADaiIH4w8FvG+3WvFrD/fkOcYl7Uk1t\r\nO5iMxC0McYU0b1zfplivqP9obaSkAWJkv3M2IRIpqJHuCJV/Gx3THFBdgTSqtSqrIJSG3Kjr\r\nNi7xHQPimi5LL9CZJFNbNmGyDly2WIWQM1k6EMgrIn6hR9OaElyAjx88YhJwFIDRS+dGNC2+\r\nu4rcK5YdLuezZGff84rPFWyZueMmEK16xb1P3fhDwFTU2KtmqCs47p7eaz2Mlw1ek1E9nlP+\r\nkmPuhWGF9pIUVbEg9co4IFgnFwIDAQAB\r\n-----END PUBLIC KEY-----\r\n",
|
383 | "remoteName": "user two",
|
384 | "remoteUsername": "user-two",
|
385 | "uniqueRemoteUsername": "user-two"
|
386 | },
|
387 | {
|
388 | "status": "accepted",
|
389 | "remoteRequestToken": "cbf875ad-5eb7-43e4-8028-415ddf6d95a9",
|
390 | "remoteEndPoint": "http://127.0.0.1:3000/antisocial/user-one",
|
391 | "remoteHost": "http://127.0.0.1:3000",
|
392 | "localRequestToken": "97e2a0b7-7a0d-46cb-8be6-4253b842067a",
|
393 | "localAccessToken": "aabaa39b-cb60-4f29-948f-8483bc29f21c",
|
394 | "keys": {
|
395 | "public": "-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAihLKPC8s1fIi4IR33n8iGA+VInab\r\niN0PzqiI7iPPJlfApwPIrxIVD+SvQXlMOOQNsh8sOADaiIH4w8FvG+3WvFrD/fkOcYl7Uk1t\r\nO5iMxC0McYU0b1zfplivqP9obaSkAWJkv3M2IRIpqJHuCJV/Gx3THFBdgTSqtSqrIJSG3Kjr\r\nNi7xHQPimi5LL9CZJFNbNmGyDly2WIWQM1k6EMgrIn6hR9OaElyAjx88YhJwFIDRS+dGNC2+\r\nu4rcK5YdLuezZGff84rPFWyZueMmEK16xb1P3fhDwFTU2KtmqCs47p7eaz2Mlw1ek1E9nlP+\r\nkmPuhWGF9pIUVbEg9co4IFgnFwIDAQAB\r\n-----END PUBLIC KEY-----\r\n",
|
396 | "private": "-----BEGIN RSA PRIVATE KEY-----\r\nMIIEowIBAAKCAQEAihLKPC8s1fIi4IR33n8iGA+VInabiN0PzqiI7iPPJlfApwPIrxIVD+Sv\r\nQXlMOOQNsh8sOADaiIH4w8FvG+3WvFrD/fkOcYl7Uk1tO5iMxC0McYU0b1zfplivqP9obaSk\r\nAWJkv3M2IRIpqJHuCJV/Gx3THFBdgTSqtSqrIJSG3KjrNi7xHQPimi5LL9CZJFNbNmGyDly2\r\nWIWQM1k6EMgrIn6hR9OaElyAjx88YhJwFIDRS+dGNC2+u4rcK5YdLuezZGff84rPFWyZueMm\r\nEK16xb1P3fhDwFTU2KtmqCs47p7eaz2Mlw1ek1E9nlP+kmPuhWGF9pIUVbEg9co4IFgnFwID\r\nAQABAoIBAD9wortEcbVbq+q88taoU2H6xusu1AfuinTJuyCwE13qs/oJIwxNop/K0zuiIAOD\r\nxUcyS37v5XkTPtmy5vpOLXwduC/ZX2mLYb5PFQFs9kCs8iq2qYEBi0FDPnLH55N5MmHwc5oD\r\ntbs8PSfW5SfMiLpM2dMIme3j5QuYr0go9k4sHcravEZWewc89ARRgvC5KnZ3Xo5bOBgq4C2W\r\nSEZF+5LzwG1pcNoNil5JFUXWmV9Kxzrv0N7KaaACxzkiACNL07viW/u5uZo2/f49OZ9gq2qk\r\nd5M5pwGC/uI3J5c7ipDq+Hlf1nzpn6AxwpRraUeLziF5+RG/3dgrNiCeQ6Ll3AECgYEA6yKZ\r\n/Tsj9TZXQeg/mQJ6PlYRqMqdnk6+rRu+37D0B7IfcE0nXKAH+sjWR66qwKm21MEj03pvr9+5\r\nHbE6YuKjq7B0UHBrGf0hyXxd3xMyaLbAmzunHUnmVcMAnavA0pTnubg7DsrL40TNHGiWZcoS\r\ntKd7zb4qnJ0bvHRuUA4u4qcCgYEAllNOKKauIV8C4HYKRgfDqWTJMTUbzva1RNKVRS8CpLkm\r\ntkY4Fskgy5dTq++zDNELfs5O6PGNj7jkLpkFi3XJrvLhIVkLH0wc5y8kfw/Btkf7mTo/PnG4\r\nw1M8P7/+YVj8+wnb/1JgQLt19aEU8dqjM38nmjHvXE/EVfxsvM4RVhECgYEApa+IGqxlthBI\r\nhCSHS+Y3BV3Yq7u6PSb3rTtz0GP8UL/u708ugVIyzUBf3bryjzgHoPtHp2kK8j8PTiDoJ23U\r\nLtLz4wqULYf1GukLrHj2eFrudXQfWcANEjmKYY/5G2nZr0BmPRIhgU+lyHLaJ3ewnqO11VA+\r\n7oS2WqEgakDUQNkCgYBGXO/0rzBKhoJ+NkJQzUmUfIx/7+/4TBpFAJzGKV7/Y3rvTqbqY3Jq\r\nWYbcr/ILSb4ruL3O42HzqAOGnDGwOY4RybX/OgKuv523yKU4pFNz0vW9nzoDLI/jPY6x+FhF\r\nkLW5e7/yHsjXA+gO9Tssib5iWF5dGoqDlwK7jNAJABu1QQKBgAOVsey9v/+GxLprhVuVIMjb\r\n+2fhh0fUfEtx9G1DplhW88AwAFHTSRfffT7VqJBSqcYHajnXDKz9mn3pC1ASyO0QnZxAy0YR\r\nyT7gsd4gOgUuk7IqOCLPNXg4TlTOTV9wsJyNQ6s+EbZqyIAhUzhr4ESEZ/qQLVpMKZ4eLDrp\r\ndx2J\r\n-----END RSA PRIVATE KEY-----\r\n"
|
397 | },
|
398 | "audiences": ["public", "friends"],
|
399 | "hash": "64aeb3ef",
|
400 | "userId": "700acf91-4ee1-4aba-8e18-136e0cc33560",
|
401 | "inviteToken": "testinvite",
|
402 | "id": "771cec97-d8ab-4ae9-b3d4-7c29521a4732",
|
403 | "remoteAccessToken": "fbb8d3be-b199-45a6-b46e-3bcbbfedd0aa",
|
404 | "remotePublicKey": "-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3BotZWYZu/rtFqTOHpdzM9+b2m7c\r\n7I2CtkZrnJ5zPQzmXDg9gLWqImAFrqcR3Ee3LMLniuKsFSYz2I/ERmXiXzv2e8wr14AeuXOV\r\nFzqcsDypKrbtT88lZLor6bt0kQOP7pFcesOedocoU9/DpnRkOYeI9MHsZN1pyZVvzLfkHvdL\r\n08ktiWwjNoFV8EL2h13sVZIFt/GaoPrv/SeWzb9oyAGAcp671smBsExCsafgwXAKYBQHIzrI\r\nScxNBzP2d9/z1ZKQ/dtXbGvWheZ0Ci1G6ngYSdx8APBIRFK+hhRdnhhpbat5juWoMs2dTG8q\r\nWIu45ntjfg7BHLRLExRE6un5lwIDAQAB\r\n-----END PUBLIC KEY-----\r\n",
|
405 | "remoteName": "user one",
|
406 | "remoteUsername": "user-one",
|
407 | "uniqueRemoteUsername": "user-one"
|
408 | }
|
409 | ]
|
410 | ```
|
411 |
|
412 | ## AntiSocial Friend Protocol
|
413 |
|
414 | protocol for making a friend request
|
415 | ------------------------------------
|
416 | ```
|
417 | requester sets up pending Friend data on requester's server (/request-friend)
|
418 | requester calls requestee with a requestToken
|
419 | requestee sets up pending Friend data on requestee's server (/friend-request)
|
420 | requestee calls requester to exchange the requestToken for an accessToken and publicKey (/friend-exchange-token)
|
421 | requestee returns requestToken to requester
|
422 | requestee triggers 'new-friend-request' event for requestee application
|
423 | requester calls requestee to exchange requestToken for accessToken and publicKey (/friend-exchange-token)
|
424 | ```
|
425 |
|
426 | protocol for accepting a friend request
|
427 | ---------------------------------------
|
428 | ```
|
429 | requestee marks requester as accepted and grants access to 'public' and 'friends' (/friend-request-accept)
|
430 | requestee calls requester to update status (/friend-webhook action=friend-request-accepted)
|
431 | requester marks requestee as accepted and grants access to 'public' and 'friends'
|
432 | trigger a 'new-friend' event for requestor application
|
433 | trigger a 'new-friend' event for requestee application
|
434 | ```
|
435 |
|
436 | At the end of this protocol both users will have a Friend record holding the accessToken and the public key of the friend. With these credentials they can exchange signed encrypted messages for notifying each other of activity in their accounts.
|
437 |
|
438 | `accessToken` is a uuid that is used to authenticate connection requests, `requestToken` is a uuid which is used to retrieve an accessToken
|
439 |
|
440 |
|
441 | ### Friend Request
|
442 | ---
|
443 |
|
444 | Use case: Michael wants to friend Alan and already knows the address of Alan's server antisocial endpoint.
|
445 | `http://emtage.com/antisocial/ae`
|
446 |
|
447 | Michael logs on to his account on his server.
|
448 |
|
449 | Michael enters Alan's address on the friend request form.
|
450 |
|
451 | Alan's public profile information is displayed to confirm that the address is correct.
|
452 |
|
453 | Michael clicks the 'add friend' button which starts the friend request protocol.
|
454 |
|
455 | ### FRIEND REQUEST
|
456 | ---
|
457 |
|
458 | 1. Michael's server creates a 'Friend' record in his database marking it as 'pending' and setting the flag 'originator' to indicate that Michael is making the request. This record has a unique 'requestToken', 'accessToken' and an RSA key pair. These credentials will be exchanged with Alan's server.
|
459 | ```
|
460 | Michael's Browser Michael's server Alan's server Alan's Browser
|
461 | ----------------- ---------------- ---------------- ----------------
|
462 |
|
463 | GET --------------------->
|
464 | http://rhodes.com/antisocial/mr/request-friend?endpoint=http://emtage.com/antisocial/ae
|
465 | ```
|
466 |
|
467 | 2. Michael's server sends a POST request to Alan's server to initiate the friend request.
|
468 | ```
|
469 | Michael's Browser Michael's server Alan's server Alan's Browser
|
470 | ----------------- ---------------- ---------------- ----------------
|
471 |
|
472 | POST -------------------->
|
473 | http://emtage.com/antisocial/ae/friend-request
|
474 | BODY {
|
475 | 'remoteEndPoint': 'http://rhodes.com/antisocial/mr',
|
476 | 'requestToken': Michaels Request Token
|
477 | }
|
478 | ```
|
479 | 3. Alans's server connects to Michael's server to validate the origin of the request and to exchange Michael's requestToken for an accessToken.
|
480 | ```
|
481 | Michael's Browser Michael's server Alan's server Alan's Browser
|
482 | ----------------- ---------------- ---------------- ----------------
|
483 |
|
484 | <------------------------ POST
|
485 | http://rhodes.com/antisocial/mr/friend-exchange
|
486 | BODY {
|
487 | 'endpoint': 'http://emtage.com/antisocial/ae',
|
488 | 'requestToken': Michaels Request Token
|
489 | }
|
490 | ```
|
491 | 4. Michael's server looks up the friend record and returns access credentials to Alan's server
|
492 | ```
|
493 | Michael's Browser Michael's server Alan's server Alan's Browser
|
494 | ----------------- ---------------- ---------------- ----------------
|
495 |
|
496 | RESPONSE ---------------->
|
497 | {
|
498 | 'status': 'ok',
|
499 | 'accessToken': Michael's Access Token,
|
500 | 'publicKey': Michael's public key
|
501 | }
|
502 | ```
|
503 | 5. Alan's server creates a 'Friend' record in his database marking it as 'pending' saving Michael's accessToken and the publicKey and notifies Alan of the pending request. Alan's server returns his requestToken to Michael's server so Micael's server can complete the exchange of credentials.
|
504 | ```
|
505 | Michael's Browser Michael's server Alan's server Alan's Browser
|
506 | ----------------- ---------------- ---------------- ----------------
|
507 |
|
508 | <------------------------ RESPONSE
|
509 | {
|
510 | 'status': 'ok',
|
511 | 'requestToken': Alan's RequestToken
|
512 | }
|
513 | ```
|
514 | 6. Michael's server connects to Alan's server to exchange Alan's request token for an accessToken
|
515 | ```
|
516 | Michael's Browser Michael's server Alan's server Alan's Browser
|
517 | ----------------- ---------------- ---------------- ----------------
|
518 |
|
519 | POST ------------------->
|
520 | http://emtage.com/antisocial/ae/friend-exchange
|
521 | BODY {
|
522 | 'endpoint': http://rhodes.com/antisocial/mr,
|
523 | 'requestToken': Alan's Request Token
|
524 | }
|
525 | ```
|
526 | 7. Alan's server looks up the friend record by the requestToken and returns access credentials to Michael's server
|
527 | ```
|
528 | Michael's Browser Michael's server Alan's server Alan's Browser
|
529 | ----------------- ---------------- ---------------- ----------------
|
530 |
|
531 | <------------------------ RESPONSE
|
532 | {
|
533 | 'status': 'ok',
|
534 | 'accessToken': Alan's AccessToken,
|
535 | 'publicKey': Alan's public key
|
536 | }
|
537 | ```
|
538 |
|
539 | 8. Michael's server saves Alan's accessToken and the publicKey in the pending Friend record and returns status to the client.
|
540 | ```
|
541 | Michael's Browser Michael's server Alan's server Alan's Browser
|
542 | ----------------- ---------------- ---------------- ----------------
|
543 |
|
544 | <------------------------ RESPONSE
|
545 | { 'status':'ok' }
|
546 | ```
|
547 |
|
548 | ### FRIEND ACCEPT
|
549 | ---
|
550 |
|
551 | 1. Alan accepts friend Michael's request by clicking the button in the UI calling the accept-friend endpoint
|
552 | ```
|
553 | Michael's Browser Michael's server Alan's server Alan's Browser
|
554 | ----------------- ---------------- ---------------- ----------------
|
555 |
|
556 | <----------------------- POST
|
557 | http://emtage.com/antisocial/ae/friend-request-accept
|
558 |
|
559 | BODY { 'endpoint': http://rhodes.com/antisocial/mr
|
560 | }
|
561 | ```
|
562 |
|
563 | 2. Alan's server marks the Friend record as 'accepted' and sends a POST request to Michael's server to notify him that his friend request was accepted
|
564 | ```
|
565 | Michael's Browser Michael's server Alan's server Alan's Browser
|
566 | ----------------- ---------------- ---------------- ----------------
|
567 |
|
568 | <------------------------ POST
|
569 | http://rhodes.com/antisocial/mr/friend-webhook
|
570 | BODY {
|
571 | 'action': 'friend-request-accepted'
|
572 | 'accessToken': Michael's access token
|
573 | }
|
574 | ```
|
575 |
|
576 | 3.Michael's server marks the Friend record as 'accepted'
|
577 | ```
|
578 | Michael's Browser Michael's server Alan's server Alan's Browser
|
579 | ----------------- ---------------- ---------------- ----------------
|
580 |
|
581 | RESPONSE ---------------->
|
582 | { 'status':'ok' }
|
583 | ```
|
584 |
|
585 | 4. Alan's server returns status to the client.
|
586 | ```
|
587 | Michael's Browser Michael's server Alan's server Alan's Browser
|
588 | ----------------- ---------------- ---------------- ----------------
|
589 |
|
590 |
|
591 | RESPONSE --------------->
|
592 | { 'status':'ok' }
|
593 | ```
|
594 |
|
595 |
|
596 | Copyright Michael Rhodes. 2017,2018. All Rights Reserved.
|
597 | This file is licensed under the MIT License.
|
598 | License text available at https://opensource.org/licenses/MIT
|