diff --git a/migrations/20161217202823_user_messages.js b/migrations/20161217202823_user_messages.js new file mode 100644 index 00000000..8dca4787 --- /dev/null +++ b/migrations/20161217202823_user_messages.js @@ -0,0 +1,16 @@ +export async function up(knex) { + await knex.schema.createTable('user_messages', table => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.timestamp('created_at', true).defaultTo(knex.raw("(now() at time zone 'utc')")); + table.timestamp('updated_at', true).defaultTo(knex.raw("(now() at time zone 'utc')")); + table.uuid('sender_id').references('id').inTable('users').onDelete('cascade').onUpdate('cascade'); + table.uuid('reciever_id').references('id').inTable('users').onDelete('cascade').onUpdate('cascade'); + table.text('text'); + + table.index(['sender_id', 'reciever_id']); + }); +} + +export async function down(knex) { + await knex.schema.dropTable('user_messages'); +} diff --git a/src/api/client.js b/src/api/client.js index ff0250ad..6550b9eb 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -451,6 +451,16 @@ export default class ApiClient return await response.json(); } + async sendMessage(userId, text) { + const response = await this.postJSON(`/api/v1/user/${userId}/messages`, { text }); + return await response.json(); + } + + async userMessages(userId) { + const response = await this.get(`/api/v1/user/${userId}/messages`); + return await response.json(); + } + async registerUser(userData) { const response = await this.post(`/api/v1/users`, userData); return await response.json(); diff --git a/src/api/controller.js b/src/api/controller.js index 3b98b463..93fe30eb 100644 --- a/src/api/controller.js +++ b/src/api/controller.js @@ -35,7 +35,8 @@ import { User as UserValidators, School as SchoolValidators, Hashtag as HashtagValidators, - Geotag as GeotagValidators + Geotag as GeotagValidators, + UserMessage as UserMessageValidators } from './db/validators'; @@ -3261,8 +3262,77 @@ export default class ApiController { await this.getPostComments(ctx); }; + sendMessage = async (ctx) => { + if (!ctx.session || !ctx.session.user) { + ctx.status = 403; + ctx.body = { error: 'You are not authorized' }; + return; + } + + if (!this.areMutuallyFollowed(ctx.session.user, ctx.params.id)) { + ctx.status = 403; + ctx.body = { error: 'You must be mutually followed with this user to be able to message them' }; + return; + } + + try { + await new Checkit(UserMessageValidators).run(ctx.request.body); + } catch (e) { + ctx.status = 400; + ctx.body = { error: e.toJSON() }; + return; + } + + const User = this.bookshelf.model('User'); + + const currentUser = await new User({ id: ctx.session.user }).fetch({ require: true }); + const message = await currentUser.outbox().create({ + reciever_id: ctx.params.id, + text: ctx.request.body.text + }); + + ctx.body = await message.fetch(); + } + + /** + * Gets a chain of messages between the current user and the specified in params. + */ + getUserMessages = async (ctx) => { + if (!ctx.session || !ctx.session.user) { + ctx.status = 403; + ctx.body = { error: 'You are not authorized' }; + return; + } + + const UserMessage = this.bookshelf.model('UserMessage'); + + const messages = await UserMessage.collection() + .query(qb => { + qb + .where({ sender_id: ctx.session.user, reciever_id: ctx.params.id }) + .orWhere({ sender_id: ctx.params.id, reciever_id: ctx.session.user }) + .orderBy('created_at', 'ASC'); + }) + .fetch(); + + ctx.body = messages; + } + // ========== Helpers ========== + async areMutuallyFollowed(user1Id, user2Id) { + const knex = this.bookshelf.knex; + + const userFollows = await knex('followers') + .where({ user_id: user1Id, following_user_id: user2Id }) + .count(); + const userBeingFollowed = await knex('followers') + .where({ user_id: user2Id, following_user_id: user1Id }) + .count(); + + return userFollows.count != '0' && userBeingFollowed.count != '0'; + } + countComments = async (posts) => { const ids = posts.map(post => { return post.get('id'); diff --git a/src/api/db/index.js b/src/api/db/index.js index ec56ea9e..31e07251 100644 --- a/src/api/db/index.js +++ b/src/api/db/index.js @@ -85,6 +85,12 @@ export function initBookshelfFromKnex(knex) { post_subscriptions() { return this.belongsToMany(Post, 'post_subscriptions'); }, + inbox() { + return this.hasMany(UserMessage, 'reciever_id'); + }, + outbox() { + return this.hasMany(UserMessage, 'sender_id'); + }, virtuals: { gravatarHash() { const email = this.get('email'); @@ -599,6 +605,16 @@ export function initBookshelfFromKnex(knex) { tableName: 'quotes' }); + const UserMessage = bookshelf.Model.extend({ + tableName: 'user_messages', + sender() { + return this.belongsTo(User, 'sender_id'); + }, + reciever() { + return this.belongsTo(User, 'reciever_id'); + } + }); + const Posts = bookshelf.Collection.extend({ model: Post }); @@ -615,6 +631,7 @@ export function initBookshelfFromKnex(knex) { bookshelf.model('Geotag', Geotag); bookshelf.model('Comment', Comment); bookshelf.model('Quote', Quote); + bookshelf.model('UserMessage', UserMessage); bookshelf.collection('Posts', Posts); return bookshelf; diff --git a/src/api/db/validators.js b/src/api/db/validators.js index 8e0dd0de..47d7e4f4 100644 --- a/src/api/db/validators.js +++ b/src/api/db/validators.js @@ -90,4 +90,8 @@ const Hashtag = { } }; -export { User, School, Hashtag, Geotag }; +const UserMessage = { + text: ['string', 'minLength:1', 'required'] +}; + +export { User, School, Hashtag, Geotag, UserMessage }; diff --git a/src/api/routing.js b/src/api/routing.js index f9c875d7..e649fe45 100644 --- a/src/api/routing.js +++ b/src/api/routing.js @@ -90,6 +90,8 @@ export function initApi(bookshelf, sphinx) { api.head('/user/:username', controller.checkUserExists); api.get('/user/:id/following', controller.getFollowedUsers); api.get('/user/:id/mutual-follows', controller.getMutualFollows); + api.get('/user/:id/messages', controller.getUserMessages); + api.post('/user/:id/messages', controller.sendMessage); api.head('/user/email/:email', controller.checkEmailTaken); api.get('/user/available-username/:username', controller.getAvailableUsername); api.get('/user/:username', controller.getUser);