UNPKG

31.9 kBMarkdownView Raw
1<img src="https://github.com/antiSocialNet/antiSocial/raw/master/assets/octocloud/logo.jpg" height="200">
2
3# antiSocial
4
5Building blocks for myAntiSocial.net
6
7## antisocial-friends protocol overview
8
9This 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
11Once a friend request is accepted, each side of the friend relationship can transmit activity messages using the 'emitter' function that is handed to the application in the `open-activity-connection` event.
12
13```
14antisocialApp.on('open-activity-connection', function (user, friend, emitter, info) {}
15```
16
17The emitter is a private channel for transmitting message from the user to the friend. The emitter takes an `appId` parameter which is an identifier for the class of message (eg. IM, Post, Photo etc.) Your app should subscribe to data events for each appId you need to handle. The message is user to user encrypted for transmission over the internet using the keys that were exchanged in the friend protocol.
18```
19emitter('myappid','data',{'hello':'world'});
20```
21
22Each side sets up data handlers to listen for messages from the friend.
23```
24antisocialApp.on('activity-data-myappid', function (user, friend, message) {
25 // handle the message here
26}
27```
28* `user` is the recipient of the message
29* `friend` is the originator of the message
30* `message` contains the JSON message `{'hello':'world'}`
31
32The appId is expected to be globally unique so the convention should be something like `mydomain-myapid`.
33
34The antisocial app uses
35* `myantisocialnet-post` for posts/reactions/comments
36* `myantisocialnet-photo` for photos
37* `myantisocialnet-im` for instant message
38
39## Initialization
40```
41var app = express();
42
43var antisocial = require('antisocial-friends');
44var antisocialApp = antisocial(app, config, dbAdaptor, getAuthenticatedUser);
45
46// set up event handlers here (see below)
47
48// start http and socket.io listeners
49var http = require('http');
50server = http.createServer(app);
51var listener = server.listen(port);
52antisocialApp.listen( httpListener );
53```
54
55### Parameters:
56
57**app** is the express application
58
59**config** is a javascript object with the following properties
60
61```
62var config = {
63 'APIPrefix': '/antisocial', // where to mount the routes
64 'publicHost': 'http://127.0.0.1:3000', // public protocol and host
65 'port': 3000 // port this service is listening on (only used if behind a load balancer or reverse proxy)
66}
67```
68
69**dbAdaptor** is an abstract data store that will be called when the app needs to
70store or retrieve data from the database. For a simple example implementation
71see app.js for a simple memory store implementation of the methods and a loopback
72application adaptor below. Yours should implement the required methods to work
73within your environment (mysql, mongo etc.)
74
75**getAuthenticatedUser** is an express middleware function that gets the current
76logged 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
77
78**listener** is an express http(s) listener for setting up socket.io listeners
79
80This function returns an **antisocialApp** object which an EventEmitter.
81
82## User Endpoints Mounted by this module
83
84Endpoints are URLS that function as the base address of a user on an antisocial aware server.
85
86'https://some.antisocial.server/api-prefix/local-username'
87
88The URL form of the endpoints conform to the following conventions:
89
90`api-prefix` is the location where antisocial-friends is mounted defined in config
91
92`local-username` is a unique username for the local user
93
94### Creating friend requests
95
96#### Make a friend request
97```
98GET /api-prefix/local-username/request-friend
99 Query params:
100 endpoint: endpoint url of friend to connect with
101 invite: an invite token if this is in response to an invitation from the user
102```
103
104#### Cancel a pending friend request
105```
106POST /api-prefix/local-username/request-friend-cancel
107 Request Body: (application/x-www-form-urlencoded)
108 endpoint: endpoint of the pending request
109```
110
111### Responding to friend requests
112
113#### accept a pending friend request
114```
115/api-prefix/local-username/friend-request-accept
116 Request Body: (application/x-www-form-urlencoded)
117 endpoint: endpoint of the pending request
118```
119
120#### decline a pending friend request
121```
122/api-prefix/local-username/friend-request-decline
123 Request Body: (application/x-www-form-urlencoded)
124 endpoint: endpoint of the pending request
125```
126
127### Updating accepted friend status or info
128Either side can update or delete the relationship once accepted
129```
130POST /api-prefix/local-username/friend-update
131 Request Body: (application/x-www-form-urlencoded)
132 endpoint: endpoint of the friend
133 status: 'delete'|'block'
134 audiences: array of audiences for the friend eg. ["public","friends","some custom audience"]
135```
136
137## socket.io feeds
138
139Accepted friends establish full duplex 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.
140
141## Events
142The application should handle the following events as needed.
143
144### new-friend-request: a new friend request received
145Relevant details are in user (a user instance) and friend (a friend instance). This event would typically be used to notify the user of the pending friend request.
146```
147antisocialApp.on('new-friend-request', function (user, friend) {
148 console.log('antisocial new-friend-request %s %j', user.username, friend.remoteEndPoint);
149});
150```
151
152### new-friend: a new friend relationship has been accepted
153Both the requestor and the requestee recieve this event. Relevant details are in user (a user instance) and friend (a friend instance). This event would typically be used to notify the user that their requests has been approved and perhaps notify the user's friends that they have a new friend.
154```
155antisocialApp.on('new-friend', function (user, friend) {
156 console.log('antisocial new-friend %s %j', user.username, friend.remoteEndPoint);
157});
158```
159
160### friend-updated: the relationship has changed
161Relevant 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 an highwater event to refresh the cache.
162```
163antisocialApp.on('friend-updated', function (user, friend) {
164 console.log('antisocial friend-updated %s %s', user.username, friend.remoteEndPoint);
165});
166```
167
168### friend-deleted: either user or the friend has deleted the other
169Typically user would clean up the database and remove any activity about or by the friend.
170```
171antisocialApp.on('friend-deleted', function (user, friend) {
172 console.log('antisocial friend-deleted %s %s', user.username, friend.remoteEndPoint);
173});
174```
175
176## activity events
177Used to notify friends about user activity. Eg. created a post, has a new friend, posted a photo.
178
179### open-activity-connection: a friend activity feed has been connected.
180Typically 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.
181```
182antisocialApp.on('open-activity-connection', function (user, friend, emitter, info) {
183 console.log('antisocial open-activity-connection %s<-%s', user.username, friend.remoteEndPoint);
184 emitter('post', 'highwater', highwater);
185});
186```
187
188### user can send data to a friend using emitter function specifying an appId
189The appId indicates the class or type of message we are sending (eg. post, reply, photo, IM)
190
191Parameters:
192 `appId` is used to direct messages to the appropriate listeners (see activity-data-xxx event)
193 `eventType` 'data' or 'highwater'
194 `message object` JSON object to transmit
195
196```
197emitter(appId, eventType, {application specific message});
198```
199
200### activity-data-xxx event: xxx is the appId defined in the emitter call
201Friend 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.
202```
203antisocialApp.on('activity-data-appId', function (user, friend, data) {
204 console.log('antisocial activity-data-post user: %s friend: %s data: %j', user.name, friend.remoteEndPoint, data);
205});
206```
207
208### activity-backfill-xxx event: xxx is the appId
209Typically 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
210
211```
212antisocialApp.on('activity-backfill-appId', function (user, friend, highwater, emitter) {
213 console.log('antisocial activity-backfill-post user: %s friend: %s highwater: %s', user.name, friend.remoteEndPoint, highwater);
214
215 // send posts from requested highwater to end of posts
216 // the emitter arguments:
217 // 'appId': the id of the data event that will handle the message
218 // 'eventType': 'data'
219 // 'message': json object to transmit
220 emitter('appId', 'data', {application specific message});
221});
222```
223
224### close-activity-connection: activity feed closed.
225Typically used to clean up any event handlers set up in open-activity-connection.
226```
227antisocialApp.on('close-activity-connection', function (user, friend, reason, info) {
228 console.log('antisocial close-activity-connection %s<-%s %s', user.username, friend.remoteEndpoint, reason);
229});
230```
231
232## Notification events
233Used by the client applications (webapp, native app) to receive notifications in real time about friend activity that has been seen by friend activity event handlers.
234
235### open-notification-connection: The user has opened the notification feed.
236A user has subscribed to notifications using browser or app.
237```
238antisocialApp.on('open-notification-connection', function (user, emitter, info) {
239 console.log('antisocial open-notification-connection %s', user.username);
240});
241```
242
243### notification-data:
244User has sent server a message.
245```
246antisocialApp.on('notification-data', function (user, friend, data) {
247 console.log('antisocial notification-data user: %s friend: %s data: %j', user.name, friend.remoteEndPoint, data);
248});
249```
250
251### notification-backfill-xxx: xxx is the appId defined in the emit
252Typically 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.
253
254```
255antisocialApp.on('notification-backfill-appId', function (user, highwater, emitter) {
256 console.log('antisocial notification-backfill-post user: %s highwater: %s', user.name, highwater);
257
258 // send data events that have occurred since the requested highwater
259 emitter('appId', 'data', {application specific message});
260});
261```
262
263### close-notification-connection:
264```
265antisocialApp.on('close-notification-connection', function (user, reason, info) {
266 console.log('antisocial close-notification-connection %s %s', user.username, reason);
267});
268```
269
270## The data structures maintained by these protocols
271This app uses the following data collections:
272
273* users: username property is required to build urls
274* friends: several properties maintained by the antisocial protocol
275* invitations: use to simplify "be my friend" invitations
276* blocks: list of blocked friends
277
278friends, invitations and blocks are related to users by a foreign key `userId`
279which is set to the id of the appropriate user when created.
280
281The schema definition is implementation specific and up to the implementor.
282The following is an example db adaptor for a Loopback.io application. dbHandlers
283must support all the methods in this example.
284```
285function dbHandler() {
286 var self = this;
287
288 self.models = {
289 'users': 'MyUser',
290 'friends': 'Friend',
291 'invitations': 'Invite',
292 'blocks': 'Block'
293 };
294
295 // store an item
296 this.newInstance = function (collectionName, data, cb) {
297 server.models[self.models[collectionName]].create(data, function (err, instance) {
298 if (cb) {
299 cb(err, instance);
300 }
301 else {
302 return instance;
303 }
304 });
305 };
306
307 // get an item by matching some properties.
308 // pairs are a list of property/value pairs that are anded
309 // when querying the database example:
310 // [
311 // { 'property':'userId', 'value': 1 },
312 // { 'property':'localAccessToken', 'value': 'jhgasdfjhgsdfjhg' }
313 // ]
314 this.getInstances = function (collectionName, pairs, cb) {
315 var query = {
316 'where': {
317 'and': []
318 }
319 };
320
321 for (var i = 0; i < pairs.length; i++) {
322 var prop = pairs[i].property;
323 var value = pairs[i].value;
324 var pair = {};
325 pair[prop] = value;
326 query.where.and.push(pair);
327 }
328
329 server.models[self.models[collectionName]].find(query, function (err, found) {
330 if (cb) {
331 cb(err, found);
332 }
333 else {
334 return found;
335 }
336 });
337 };
338
339 // update item properties by id
340 this.updateInstance = function (collectionName, id, patch, cb) {
341 server.models[self.models[collectionName]].findById(id, function (err, instance) {
342 if (err) {
343 return cb(new Error('error reading ' + collectionName));
344 }
345 if (!instance) {
346 return cb(new Error('error ' + collectionName + ' id ' + id + ' not found'));
347 }
348
349 instance.updateAttributes(patch, function (err, updated) {
350 if (err) {
351 return cb(new Error('error updating ' + collectionName));
352 }
353 if (cb) {
354 cb(null, updated);
355 }
356 else {
357 return updated;
358 }
359 });
360 });
361 };
362
363 this.deleteInstance = function (collectionName, id, cb) {
364 server.models[self.models[collectionName]].destroyById(id, function (err) {
365 if (cb) {
366 cb(err);
367 }
368 });
369 };
370}
371
372db = new dbHandler();
373```
374
375### User properties
376The user is application specific but we expect the following properties
377```
378{
379 "name": "user one",
380 "username": "user-one",
381 "id": "c5503436-634c-461f-9d90-ba9e438516c1"
382}
383```
384
385### Friend Invitation properties
386```
387{
388 "token": "testinvite",
389 "userId": "53757323-8555-4ef5-b66c-a58351ce6181",
390 "id": "6165e25e-2c31-47d3-a8c2-7c75737d4003"
391}
392```
393### Block list properties
394```
395{
396 "remoteEndPoint": "http://127.0.0.1:3000/antisocial/user-three",
397 "userId": "53757323-8555-4ef5-b66c-a58351ce6181",
398 "id": "7281bcd3-94f8-4906-9714-89d7e5b5c349"
399}
400```
401
402### Friend Properties
403The 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.
404
405```
406[
407 {
408 "originator": true,
409 "status": "accepted",
410 "remoteEndPoint": "http://127.0.0.1:3000/antisocial/user-two",
411 "remoteHost": "http://127.0.0.1:3000",
412 "localRequestToken": "cbf875ad-5eb7-43e4-8028-415ddf6d95a9",
413 "localAccessToken": "fbb8d3be-b199-45a6-b46e-3bcbbfedd0aa",
414 "keys": {
415 "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",
416 "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"
417 },
418 "audiences": ["public", "friends"],
419 "hash": "f08bf78",
420 "userId": "7ca18fd6-5d23-4930-8b64-3812f3a8f012",
421 "inviteToken": "testinvite",
422 "id": "97c46a56-c323-4941-bf61-e2c959748617",
423 "remoteAccessToken": "aabaa39b-cb60-4f29-948f-8483bc29f21c",
424 "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",
425 "remoteName": "user two",
426 "remoteUsername": "user-two",
427 "uniqueRemoteUsername": "user-two"
428 },
429 {
430 "status": "accepted",
431 "remoteRequestToken": "cbf875ad-5eb7-43e4-8028-415ddf6d95a9",
432 "remoteEndPoint": "http://127.0.0.1:3000/antisocial/user-one",
433 "remoteHost": "http://127.0.0.1:3000",
434 "localRequestToken": "97e2a0b7-7a0d-46cb-8be6-4253b842067a",
435 "localAccessToken": "aabaa39b-cb60-4f29-948f-8483bc29f21c",
436 "keys": {
437 "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",
438 "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"
439 },
440 "audiences": ["public", "friends"],
441 "hash": "64aeb3ef",
442 "userId": "700acf91-4ee1-4aba-8e18-136e0cc33560",
443 "inviteToken": "testinvite",
444 "id": "771cec97-d8ab-4ae9-b3d4-7c29521a4732",
445 "remoteAccessToken": "fbb8d3be-b199-45a6-b46e-3bcbbfedd0aa",
446 "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",
447 "remoteName": "user one",
448 "remoteUsername": "user-one",
449 "uniqueRemoteUsername": "user-one"
450 }
451]
452```
453
454## AntiSocial Friend Protocol
455
456protocol for making a friend request
457------------------------------------
458```
459requester sets up pending Friend data on requester's server (/request-friend)
460 requester calls requestee with a requestToken
461 requestee sets up pending Friend data on requestee's server (/friend-request)
462 requestee calls requester to exchange the requestToken for an accessToken and publicKey (/friend-exchange-token)
463 requestee returns requestToken to requester
464 requestee triggers 'new-friend-request' event for requestee application
465 requester calls requestee to exchange requestToken for accessToken and publicKey (/friend-exchange-token)
466```
467
468protocol for accepting a friend request
469---------------------------------------
470```
471requestee marks requester as accepted and grants access to 'public' and 'friends' (/friend-request-accept)
472 requestee calls requester to update status (/friend-webhook action=friend-request-accepted)
473 requester marks requestee as accepted and grants access to 'public' and 'friends'
474 trigger a 'new-friend' event for requestor application
475 trigger a 'new-friend' event for requestee application
476```
477
478At 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.
479
480`accessToken` is a uuid that is used to authenticate connection requests, `requestToken` is a uuid which is used to retrieve an accessToken
481
482
483### Friend Request
484---
485
486Use case: Michael wants to friend Alan and already knows the address of Alan's server antisocial endpoint.
487`http://emtage.com/antisocial/ae`
488
489Michael logs on to his account on his server.
490
491Michael enters Alan's address on the friend request form.
492
493Alan's public profile information is displayed to confirm that the address is correct.
494
495Michael clicks the 'add friend' button which starts the friend request protocol.
496
497### FRIEND REQUEST
498---
499
5001. 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.
501```
502Michael's Browser Michael's server Alan's server Alan's Browser
503----------------- ---------------- ---------------- ----------------
504
505GET --------------------->
506http://rhodes.com/antisocial/mr/request-friend?endpoint=http://emtage.com/antisocial/ae
507```
508
5092. Michael's server sends a POST request to Alan's server to initiate the friend request.
510```
511Michael's Browser Michael's server Alan's server Alan's Browser
512----------------- ---------------- ---------------- ----------------
513
514 POST -------------------->
515 http://emtage.com/antisocial/ae/friend-request
516 BODY {
517 'remoteEndPoint': 'http://rhodes.com/antisocial/mr',
518 'requestToken': Michaels Request Token
519 }
520```
5213. Alans's server connects to Michael's server to validate the origin of the request and to exchange Michael's requestToken for an accessToken.
522```
523Michael's Browser Michael's server Alan's server Alan's Browser
524----------------- ---------------- ---------------- ----------------
525
526 <------------------------ POST
527 http://rhodes.com/antisocial/mr/friend-exchange
528 BODY {
529 'endpoint': 'http://emtage.com/antisocial/ae',
530 'requestToken': Michaels Request Token
531 }
532```
5334. Michael's server looks up the friend record and returns access credentials to Alan's server
534```
535Michael's Browser Michael's server Alan's server Alan's Browser
536----------------- ---------------- ---------------- ----------------
537
538 RESPONSE ---------------->
539 {
540 'status': 'ok',
541 'accessToken': Michael's Access Token,
542 'publicKey': Michael's public key
543 }
544```
5455. 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.
546```
547Michael's Browser Michael's server Alan's server Alan's Browser
548----------------- ---------------- ---------------- ----------------
549
550 <------------------------ RESPONSE
551 {
552 'status': 'ok',
553 'requestToken': Alan's RequestToken
554 }
555```
5566. Michael's server connects to Alan's server to exchange Alan's request token for an accessToken
557```
558Michael's Browser Michael's server Alan's server Alan's Browser
559----------------- ---------------- ---------------- ----------------
560
561 POST ------------------->
562 http://emtage.com/antisocial/ae/friend-exchange
563 BODY {
564 'endpoint': http://rhodes.com/antisocial/mr,
565 'requestToken': Alan's Request Token
566 }
567```
5687. Alan's server looks up the friend record by the requestToken and returns access credentials to Michael's server
569```
570Michael's Browser Michael's server Alan's server Alan's Browser
571----------------- ---------------- ---------------- ----------------
572
573 <------------------------ RESPONSE
574 {
575 'status': 'ok',
576 'accessToken': Alan's AccessToken,
577 'publicKey': Alan's public key
578 }
579```
580
5818. Michael's server saves Alan's accessToken and the publicKey in the pending Friend record and returns status to the client.
582```
583Michael's Browser Michael's server Alan's server Alan's Browser
584----------------- ---------------- ---------------- ----------------
585
586<------------------------ RESPONSE
587 { 'status':'ok' }
588```
589
590### FRIEND ACCEPT
591---
592
5931. Alan accepts friend Michael's request by clicking the button in the UI calling the accept-friend endpoint
594```
595Michael's Browser Michael's server Alan's server Alan's Browser
596----------------- ---------------- ---------------- ----------------
597
598 <----------------------- POST
599 http://emtage.com/antisocial/ae/friend-request-accept
600
601 BODY { 'endpoint': http://rhodes.com/antisocial/mr
602 }
603```
604
6052. 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
606```
607Michael's Browser Michael's server Alan's server Alan's Browser
608----------------- ---------------- ---------------- ----------------
609
610 <------------------------ POST
611 http://rhodes.com/antisocial/mr/friend-webhook
612 BODY {
613 'action': 'friend-request-accepted'
614 'accessToken': Michael's access token
615 }
616```
617
6183.Michael's server marks the Friend record as 'accepted'
619```
620Michael's Browser Michael's server Alan's server Alan's Browser
621----------------- ---------------- ---------------- ----------------
622
623 RESPONSE ---------------->
624 { 'status':'ok' }
625```
626
6274. Alan's server returns status to the client.
628```
629Michael's Browser Michael's server Alan's server Alan's Browser
630----------------- ---------------- ---------------- ----------------
631
632
633 RESPONSE --------------->
634 { 'status':'ok' }
635```
636
637
638Copyright Michael Rhodes. 2017,2018. All Rights Reserved.
639This file is licensed under the MIT License.
640License text available at https://opensource.org/licenses/MIT