Skip to content

Commit

Permalink
Merge pull request #25 from ably-labs/delete-message-sdk
Browse files Browse the repository at this point in the history
[CON-100] feat: delete message sdk
  • Loading branch information
ttypic authored Jan 2, 2024
2 parents 94ef340 + c166a9f commit 4b41890
Show file tree
Hide file tree
Showing 26 changed files with 2,205 additions and 219 deletions.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const reaction = await conversation.messages.addReaction(msgId, {
Delete reaction:

```ts
await conversation.messages.removeReaction(msgId, type)
await conversation.messages.removeReaction(reactionId)
```

### Reaction object
Expand Down Expand Up @@ -169,7 +169,7 @@ conversation.messages.subscribe(({ type, message }) => {

```ts
// Subscribe to all reactions
conversation.reactions.subscribe(({ type, reaction }) => {
conversation.messages.subscribeReactions(({ type, reaction }) => {
switch (type) {
case 'reaction.added':
console.log(reaction);
Expand Down Expand Up @@ -292,8 +292,7 @@ conversation.reactions.add(reactionType)
Remove reaction

```ts
conversation.reactions.delete(reaction)
conversation.reactions.delete(reactionType)
conversation.reactions.delete(reactionId)
```

## Typing indicator
Expand Down
2 changes: 2 additions & 0 deletions __mocks__/ably/promises/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class MockRealtime {
};
public auth: {
clientId: string;
requestToken(): void;
};
public connection: {
id?: string;
Expand All @@ -65,6 +66,7 @@ class MockRealtime {
};
this.auth = {
clientId: MOCK_CLIENT_ID,
requestToken: () => {},
};
this.connection = {
id: '1',
Expand Down
1 change: 0 additions & 1 deletion demo/api/conversations/.env.example

This file was deleted.

12 changes: 12 additions & 0 deletions demo/api/conversations/controllers/conversationsController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Request, Response } from 'express';
import { createConversation, getConversation } from '../inMemoryDb';

export const handleCreateConversation = (req: Request, res: Response) => {
const conversationId = req.params.conversationId;
res.json(createConversation(conversationId));
};

export const handleGetConversation = (req: Request, res: Response) => {
const conversationId = req.params.conversationId;
res.json(getConversation(conversationId));
};
62 changes: 62 additions & 0 deletions demo/api/conversations/controllers/messagesController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as Ably from 'ably/promises';
import { Request, Response } from 'express';
import { createMessage, deleteMessage, editMessage, findMessages } from '../inMemoryDb';

export const handleCreateMessage = (req: Request, res: Response) => {
const conversationId = req.params.conversationId;
const ablyToken = req.headers.authorization.split(' ')[1];

const message = createMessage({
...JSON.parse(req.body),
client_id: req.headers['ably-clientid'] as string,
conversation_id: conversationId,
});

const client = new Ably.Rest(ablyToken);

client.channels.get(`conversations:${conversationId}`).publish('message.created', message);

res.json({ id: message.id });

res.status(201).end();
};

export const handleQueryMessages = (req: Request, res: Response) => {
const conversationId = req.params.conversationId;
res.json(findMessages(conversationId, req.headers['ably-clientid'] as string));
};

export const handleEditMessages = (req: Request, res: Response) => {
const conversationId = req.params.conversationId;
const ablyToken = req.headers.authorization.split(' ')[1];

const message = editMessage({
id: req.params.messageId,
conversation_id: conversationId,
...JSON.parse(req.body),
});

const client = new Ably.Rest(ablyToken);

client.channels.get(`conversations:${conversationId}`).publish('message.updated', message);

res.json({ id: message.id });

res.status(201).end();
};

export const handleDeleteMessages = (req: Request, res: Response) => {
const conversationId = req.params.conversationId;
const ablyToken = req.headers.authorization.split(' ')[1];

const message = deleteMessage({
id: req.params.messageId,
conversation_id: conversationId,
});

const client = new Ably.Rest(ablyToken);

client.channels.get(`conversations:${conversationId}`).publish('message.deleted', message);

res.status(201).end();
};
34 changes: 34 additions & 0 deletions demo/api/conversations/controllers/reactionsController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Request, Response } from 'express';
import * as Ably from 'ably/promises';
import { addReaction, deleteReaction } from '../inMemoryDb';

export const handleAddReaction = (req: Request, res: Response) => {
const conversationId = req.params.conversationId;
const ablyToken = req.headers.authorization.split(' ')[1];

const reaction = addReaction({
message_id: req.params.messageId,
conversation_id: conversationId,
client_id: req.headers['ably-clientid'] as string,
...JSON.parse(req.body),
});

const client = new Ably.Rest(ablyToken);

client.channels.get(`conversations:${conversationId}`).publish('reaction.added', reaction);

res.status(201).end();
};

export const handleDeleteReaction = (req: Request, res: Response) => {
const reactionId = req.params.reactionId;
const ablyToken = req.headers.authorization.split(' ')[1];

const reaction = deleteReaction(reactionId);

const client = new Ably.Rest(ablyToken);

client.channels.get(`conversations:${reaction.conversation_id}`).publish('reaction.deleted', reaction);

res.status(201).end();
};
138 changes: 138 additions & 0 deletions demo/api/conversations/inMemoryDb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { ulid } from 'ulidx';

export interface Conversation {
id: string;
application_id: string;
ttl: number | null;
created_at: number;
}

export interface Message {
id: string;
client_id: string;
conversation_id: string;
content: string;
reactions: {
counts: Record<string, number>;
latest: Reaction[];
mine: Reaction[];
};
created_at: number;
updated_at: number | null;
deleted_at: number | null;
}

export interface Reaction {
id: string;
message_id: string;
conversation_id: string;
type: string;
client_id: string;
updated_at: number | null;
deleted_at: number | null;
}

const conversations: Conversation[] = [];
const conversationIdToMessages: Record<string, Message[]> = {};
const reactions: Reaction[] = [];

export const createConversation = (id: string): Conversation => {
const existing = conversations.find((conv) => conv.id === id);
if (existing) return existing;
const conversation = {
id,
application_id: 'demo',
ttl: null,
created_at: Date.now(),
};
conversationIdToMessages[id] = [];
conversations.push(conversation);
return conversation;
};

createConversation('conversation1');

export const getConversation = (id: string): Conversation => {
return conversations.find((conv) => conv.id === id);
};

export const findMessages = (conversationId: string, clientId: string) =>
enrichMessagesWithReactions(conversationIdToMessages[conversationId], clientId);

export const createMessage = (message: Pick<Message, 'client_id' | 'conversation_id' | 'content'>) => {
const created: Message = {
...message,
id: ulid(),
reactions: {
counts: {},
latest: [],
mine: [],
},
created_at: Date.now(),
updated_at: null,
deleted_at: null,
};
conversationIdToMessages[created.conversation_id].push(created);
return created;
};

export const editMessage = (message: Pick<Message, 'id' | 'conversation_id' | 'content'>) => {
const edited = conversationIdToMessages[message.conversation_id].find(({ id }) => message.id === id);
edited.content = message.content;
return edited;
};

export const deleteMessage = (message: Pick<Message, 'id' | 'conversation_id'>) => {
const deletedIndex = conversationIdToMessages[message.conversation_id].findIndex(({ id }) => message.id === id);
const deleted = conversationIdToMessages[message.conversation_id][deletedIndex];
conversationIdToMessages[message.conversation_id].splice(deletedIndex, 1);
return deleted;
};

export const addReaction = (
reaction: Pick<Reaction, 'id' | 'message_id' | 'type' | 'client_id' | 'conversation_id'>,
) => {
const created: Reaction = {
...reaction,
id: ulid(),
updated_at: null,
deleted_at: null,
};
reactions.push(created);
return created;
};

export const deleteReaction = (reactionId: string) => {
const deletedIndex = reactions.findIndex((reaction) => reaction.id === reactionId);
const deleted = reactions[deletedIndex];
reactions.splice(deletedIndex, 1);
return deleted;
};

const enrichMessageWithReactions = (message: Message, clientId: string): Message => {
const messageReactions = reactions.filter((reaction) => reaction.message_id === message.id);
const mine = messageReactions.filter((reaction) => reaction.client_id === clientId);
const counts = messageReactions.reduce(
(acc, reaction) => {
if (acc[reaction.type]) {
acc[reaction.type]++;
} else {
acc[reaction.type] = 1;
}
return acc;
},
{} as Record<string, number>,
);
return {
...message,
reactions: {
counts,
latest: messageReactions,
mine,
},
};
};

const enrichMessagesWithReactions = (messages: Message[], clientId: string) => {
return messages.map((message) => enrichMessageWithReactions(message, clientId));
};
68 changes: 6 additions & 62 deletions demo/api/conversations/index.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,12 @@
import * as dotenv from 'dotenv';
import * as Ably from 'ably/promises';
import { HandlerEvent } from '@netlify/functions';
import { ulid } from 'ulidx';
import express from 'express';
import serverless from 'serverless-http';
import { router } from './routes';

dotenv.config();

const messages = [];
const api = express();

export async function handler(event: HandlerEvent) {
if (!process.env.ABLY_API_KEY) {
console.error(`
Missing ABLY_API_KEY environment variable.
If you're running locally, please ensure you have a ./.env file with a value for ABLY_API_KEY=your-key.
If you're running in Netlify, make sure you've configured env variable ABLY_API_KEY.
api.use('/api/conversations/v1', router);

Please see README.md for more details on configuring your Ably API Key.`);

return {
statusCode: 500,
headers: { 'content-type': 'application/json' },
body: JSON.stringify('ABLY_API_KEY is not set'),
};
}

if (/\/api\/conversations\/v1\/conversations\/(\w+)\/messages/.test(event.path) && event.httpMethod === 'POST') {
const conversationId = /\/api\/conversations\/v1\/conversations\/(\w+)\/messages/.exec(event.path)[1];
const message = {
id: ulid(),
...JSON.parse(event.body),
client_id: event.headers['ably-clientid'],
conversation_id: conversationId,
reactions: {
counts: {},
latest: [],
mine: [],
},
created_at: Date.now(),
updated_at: null,
deleted_at: null,
};
messages.push(message);

const client = new Ably.Rest(process.env.ABLY_API_KEY);

client.channels.get(`conversations:${conversationId}`).publish('message.created', message);

return {
statusCode: 201,
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ id: message.id }),
};
}

const getMessagesRegEx = /\/api\/conversations\/v1\/conversations\/(\w+)\/messages/;
if (getMessagesRegEx.test(event.path) && event.httpMethod === 'GET') {
return {
statusCode: 200,
headers: { 'content-type': 'application/json' },
body: JSON.stringify(messages),
};
}

return {
statusCode: 404,
body: 'Not Found',
};
}
export const handler = serverless(api);
Loading

0 comments on commit 4b41890

Please sign in to comment.