Building a Real-time Chat Backend
Learn how to build a scalable real-time chat backend with message brokers, features, presence detection, and notification systems using Ductape SDK.
What You'll Build
- Real-time messaging with message brokers
- User presence and typing indicators
- Message delivery and read receipts
- File upload handling
- Group chat management
- Message search and history
- Automated moderation features
- Push notifications
- Message encryption
Prerequisites
- Node.js and npm installed
- Ductape account and API credentials
- Basic understanding of TypeScript/JavaScript
- WebSocket knowledge (helpful)
Setup
- TypeScript
- Java
- Go
- .NET
npm install @ductape/sdk@0.1.8
<dependency>
<groupId>app.ductape</groupId>
<artifactId>sdk</artifactId>
<version>0.1.8</version>
</dependency>
go get github.com/ductape/ductape/sdk/go@v0.1.8
dotnet add package Ductape.Sdk --version 0.1.8
Create .env:
DUCTAPE_API_KEY=your_api_key
PORT=3000
Initialize Ductape and Express
import { Ductape } from '@ductape/sdk';
import express from 'express';
import { createServer } from 'http';
import { Server as SocketIOServer } from 'socket.io';
const ductape = new Ductape({
apiKey: process.env.DUCTAPE_API_KEY!
});
const app = express();
const httpServer = createServer(app);
const io = new SocketIOServer(httpServer, {
cors: {
origin: '*'
}
});
app.use(express.json());
Database Schema
- TypeScript
- Java
- Go
- .NET
// Users table
await ductape.databases.schema.create('users', {
username: { type: 'String', unique: true, required: true },
email: { type: 'String', unique: true, required: true },
display_name: { type: 'String', required: true },
avatar_url: { type: 'String' },
status: { type: 'String', default: 'offline' }, // online, offline, away
last_seen: { type: 'Date' },
created_at: { type: 'Date', default: 'now' }
});
// Conversations table
await ductape.databases.schema.create('conversations', {
type: { type: 'String', required: true }, // direct, group, channel
name: { type: 'String' },
avatar_url: { type: 'String' },
participant_ids: { type: 'Array', required: true },
admin_ids: { type: 'Array' },
last_message_id: { type: 'String' },
last_message_at: { type: 'Date' },
created_by: { type: 'String', required: true },
settings: { type: 'JSON', default: {} },
created_at: { type: 'Date', default: 'now' },
updated_at: { type: 'Date', default: 'now' }
});
// Messages table
await ductape.databases.schema.create('messages', {
conversation_id: { type: 'String', required: true },
sender_id: { type: 'String', required: true },
content: { type: 'String' },
type: { type: 'String', default: 'text' }, // text, image, file, system, call
encrypted_content: { type: 'String' },
file_url: { type: 'String' },
file_name: { type: 'String' },
file_size: { type: 'Number' },
metadata: { type: 'JSON' },
reactions: { type: 'JSON', default: {} },
reply_to_id: { type: 'String' },
is_edited: { type: 'Boolean', default: false },
is_deleted: { type: 'Boolean', default: false },
is_flagged: { type: 'Boolean', default: false },
edited_at: { type: 'Date' },
deleted_at: { type: 'Date' },
created_at: { type: 'Date', default: 'now' }
});
// Message status table
await ductape.databases.schema.create('message_status', {
message_id: { type: 'String', required: true },
user_id: { type: 'String', required: true },
status: { type: 'String', required: true }, // sent, delivered, read
delivered_at: { type: 'Date' },
read_at: { type: 'Date' },
created_at: { type: 'Date', default: 'now' }
});
// Presence table
await ductape.databases.schema.create('presence', {
user_id: { type: 'String', unique: true, required: true },
status: { type: 'String', required: true }, // online, offline, away
conversation_id: { type: 'String' },
is_typing: { type: 'Boolean', default: false },
last_activity: { type: 'Date', default: 'now' },
device_info: { type: 'JSON' }
});
// Create indexes
await ductape.databases.schema.createIndex('conversations', ['participant_ids']);
await ductape.databases.schema.createIndex('messages', ['conversation_id', 'created_at']);
await ductape.databases.schema.createIndex('messages', ['sender_id']);
await ductape.databases.schema.createIndex('message_status', ['message_id', 'user_id']);
await ductape.databases.schema.createIndex('presence', ['user_id']);
// Users table
ductape.databases.schema.create('users', Map.of(
username: Map.of( "type", "String", "unique", true, "required", true ),
email: Map.of( "type", "String", "unique", true, "required", true ),
display_name: Map.of( "type", "String", "required", true ),
avatar_url: Map.of( "type", "String" ),
status: Map.of( "type", "String", "default", "offline" ), // online, offline, away
last_seen: Map.of( "type", "Date" ),
created_at: Map.of( "type", "Date", "default", "now" )
));
// Conversations table
ductape.databases.schema.create('conversations', Map.of(
type: Map.of( "type", "String", "required", true ), // direct, group, channel
name: Map.of( "type", "String" ),
avatar_url: Map.of( "type", "String" ),
participant_ids: Map.of( "type", "Array", "required", true ),
admin_ids: Map.of( "type", "Array" ),
last_message_id: Map.of( "type", "String" ),
last_message_at: Map.of( "type", "Date" ),
created_by: Map.of( "type", "String", "required", true ),
settings: Map.of( "type", "JSON", default: Map.of() ),
created_at: Map.of( "type", "Date", "default", "now" ),
updated_at: Map.of( "type", "Date", "default", "now" )
));
// Messages table
ductape.databases.schema.create('messages', Map.of(
conversation_id: Map.of( "type", "String", "required", true ),
sender_id: Map.of( "type", "String", "required", true ),
content: Map.of( "type", "String" ),
type: Map.of( "type", "String", "default", "text" ), // text, image, file, system, call
encrypted_content: Map.of( "type", "String" ),
file_url: Map.of( "type", "String" ),
file_name: Map.of( "type", "String" ),
file_size: Map.of( "type", "Number" ),
metadata: Map.of( "type", "JSON" ),
reactions: Map.of( "type", "JSON", default: Map.of() ),
reply_to_id: Map.of( "type", "String" ),
is_edited: Map.of( "type", "Boolean", "default", false ),
is_deleted: Map.of( "type", "Boolean", "default", false ),
is_flagged: Map.of( "type", "Boolean", "default", false ),
edited_at: Map.of( "type", "Date" ),
deleted_at: Map.of( "type", "Date" ),
created_at: Map.of( "type", "Date", "default", "now" )
));
// Message status table
ductape.databases.schema.create('message_status', Map.of(
message_id: Map.of( "type", "String", "required", true ),
user_id: Map.of( "type", "String", "required", true ),
status: Map.of( "type", "String", "required", true ), // sent, delivered, read
delivered_at: Map.of( "type", "Date" ),
read_at: Map.of( "type", "Date" ),
created_at: Map.of( "type", "Date", "default", "now" )
));
// Presence table
ductape.databases.schema.create('presence', Map.of(
user_id: Map.of( "type", "String", "unique", true, "required", true ),
status: Map.of( "type", "String", "required", true ), // online, offline, away
conversation_id: Map.of( "type", "String" ),
is_typing: Map.of( "type", "Boolean", "default", false ),
last_activity: Map.of( "type", "Date", "default", "now" ),
device_info: Map.of( "type", "JSON" )
));
// Create indexes
ductape.databases.schema.createIndex('conversations', ['participant_ids']);
ductape.databases.schema.createIndex('messages', ['conversation_id', 'created_at']);
ductape.databases.schema.createIndex('messages', ['sender_id']);
ductape.databases.schema.createIndex('message_status', ['message_id', 'user_id']);
ductape.databases.schema.createIndex('presence', ['user_id']);
// Users table
client.databases.schema.create('users', {
username: { "type": "String", "unique": true, "required": true },
email: { "type": "String", "unique": true, "required": true },
display_name: { "type": "String", "required": true },
avatar_url: { "type": "String" },
status: { "type": "String", "default": "offline" }, // online, offline, away
last_seen: { "type": "Date" },
created_at: { "type": "Date", "default": "now" }
});
// Conversations table
client.databases.schema.create('conversations', {
type: { "type": "String", "required": true }, // direct, group, channel
name: { "type": "String" },
avatar_url: { "type": "String" },
participant_ids: { "type": "Array", "required": true },
admin_ids: { "type": "Array" },
last_message_id: { "type": "String" },
last_message_at: { "type": "Date" },
created_by: { "type": "String", "required": true },
settings: { "type": "JSON", default: {} },
created_at: { "type": "Date", "default": "now" },
updated_at: { "type": "Date", "default": "now" }
});
// Messages table
client.databases.schema.create('messages', {
conversation_id: { "type": "String", "required": true },
sender_id: { "type": "String", "required": true },
content: { "type": "String" },
type: { "type": "String", "default": "text" }, // text, image, file, system, call
encrypted_content: { "type": "String" },
file_url: { "type": "String" },
file_name: { "type": "String" },
file_size: { "type": "Number" },
metadata: { "type": "JSON" },
reactions: { "type": "JSON", default: {} },
reply_to_id: { "type": "String" },
is_edited: { "type": "Boolean", "default": false },
is_deleted: { "type": "Boolean", "default": false },
is_flagged: { "type": "Boolean", "default": false },
edited_at: { "type": "Date" },
deleted_at: { "type": "Date" },
created_at: { "type": "Date", "default": "now" }
});
// Message status table
client.databases.schema.create('message_status', {
message_id: { "type": "String", "required": true },
user_id: { "type": "String", "required": true },
status: { "type": "String", "required": true }, // sent, delivered, read
delivered_at: { "type": "Date" },
read_at: { "type": "Date" },
created_at: { "type": "Date", "default": "now" }
});
// Presence table
client.databases.schema.create('presence', {
user_id: { "type": "String", "unique": true, "required": true },
status: { "type": "String", "required": true }, // online, offline, away
conversation_id: { "type": "String" },
is_typing: { "type": "Boolean", "default": false },
last_activity: { "type": "Date", "default": "now" },
device_info: { "type": "JSON" }
});
// Create indexes
client.databases.schema.createIndex('conversations', ['participant_ids']);
client.databases.schema.createIndex('messages', ['conversation_id', 'created_at']);
client.databases.schema.createIndex('messages', ['sender_id']);
client.databases.schema.createIndex('message_status', ['message_id', 'user_id']);
client.databases.schema.createIndex('presence', ['user_id']);
// Users table
await ductape.databases.schema.create('users', {
username: { ["type"] = "String", ["unique"] = true, ["required"] = true },
email: { ["type"] = "String", ["unique"] = true, ["required"] = true },
display_name: { ["type"] = "String", ["required"] = true },
avatar_url: { ["type"] = "String" },
status: { ["type"] = "String", ["default"] = "offline" }, // online, offline, away
last_seen: { ["type"] = "Date" },
created_at: { ["type"] = "Date", ["default"] = "now" }
});
// Conversations table
await ductape.databases.schema.create('conversations', {
type: { ["type"] = "String", ["required"] = true }, // direct, group, channel
name: { ["type"] = "String" },
avatar_url: { ["type"] = "String" },
participant_ids: { ["type"] = "Array", ["required"] = true },
admin_ids: { ["type"] = "Array" },
last_message_id: { ["type"] = "String" },
last_message_at: { ["type"] = "Date" },
created_by: { ["type"] = "String", ["required"] = true },
settings: { ["type"] = "JSON", default: {} },
created_at: { ["type"] = "Date", ["default"] = "now" },
updated_at: { ["type"] = "Date", ["default"] = "now" }
});
// Messages table
await ductape.databases.schema.create('messages', {
conversation_id: { ["type"] = "String", ["required"] = true },
sender_id: { ["type"] = "String", ["required"] = true },
content: { ["type"] = "String" },
type: { ["type"] = "String", ["default"] = "text" }, // text, image, file, system, call
encrypted_content: { ["type"] = "String" },
file_url: { ["type"] = "String" },
file_name: { ["type"] = "String" },
file_size: { ["type"] = "Number" },
metadata: { ["type"] = "JSON" },
reactions: { ["type"] = "JSON", default: {} },
reply_to_id: { ["type"] = "String" },
is_edited: { ["type"] = "Boolean", ["default"] = false },
is_deleted: { ["type"] = "Boolean", ["default"] = false },
is_flagged: { ["type"] = "Boolean", ["default"] = false },
edited_at: { ["type"] = "Date" },
deleted_at: { ["type"] = "Date" },
created_at: { ["type"] = "Date", ["default"] = "now" }
});
// Message status table
await ductape.databases.schema.create('message_status', {
message_id: { ["type"] = "String", ["required"] = true },
user_id: { ["type"] = "String", ["required"] = true },
status: { ["type"] = "String", ["required"] = true }, // sent, delivered, read
delivered_at: { ["type"] = "Date" },
read_at: { ["type"] = "Date" },
created_at: { ["type"] = "Date", ["default"] = "now" }
});
// Presence table
await ductape.databases.schema.create('presence', {
user_id: { ["type"] = "String", ["unique"] = true, ["required"] = true },
status: { ["type"] = "String", ["required"] = true }, // online, offline, away
conversation_id: { ["type"] = "String" },
is_typing: { ["type"] = "Boolean", ["default"] = false },
last_activity: { ["type"] = "Date", ["default"] = "now" },
device_info: { ["type"] = "JSON" }
});
// Create indexes
await ductape.databases.schema.createIndex('conversations', ['participant_ids']);
await ductape.databases.schema.createIndex('messages', ['conversation_id', 'created_at']);
await ductape.databases.schema.createIndex('messages', ['sender_id']);
await ductape.databases.schema.createIndex('message_status', ['message_id', 'user_id']);
await ductape.databases.schema.createIndex('presence', ['user_id']);
Message Broker Setup
- TypeScript
- Java
- Go
- .NET
// Configure message broker topics
const MESSAGE_TOPICS = {
NEW_MESSAGE: 'chat.message.new',
MESSAGE_UPDATED: 'chat.message.updated',
MESSAGE_DELETED: 'chat.message.deleted',
TYPING: 'chat.typing',
PRESENCE: 'chat.presence',
READ_RECEIPT: 'chat.receipt.read'
};
// Publish message to broker
async function publishMessage(topic: string, data: any) {
await ductape.messageBrokers.publish({
topic,
message: {
...data,
timestamp: new Date().toISOString()
}
});
}
// Subscribe to message events
async function subscribeToMessages(conversationId: string, callback: (message: any) => void) {
await ductape.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.NEW_MESSAGE,
filter: { conversation_id: conversationId },
handler: async (message) => {
callback(message);
}
});
}
// Configure message broker topics
Map<String, Object> MESSAGE_TOPICS = Map.of(
"NEW_MESSAGE", "chat.message.new",
"MESSAGE_UPDATED", "chat.message.updated",
"MESSAGE_DELETED", "chat.message.deleted",
"TYPING", "chat.typing",
"PRESENCE", "chat.presence",
"READ_RECEIPT", "chat.receipt.read"
);
// Publish message to broker
async function publishMessage(topic: string, data: any) Map.of(
ductape.messageBrokers.publish(Map.of(
topic,
message: Map.of(
...data,
timestamp: Instant.now().toISOString()
)
));
)
// Subscribe to message events
async function subscribeToMessages(conversationId: string, callback: (message: any) => void) Map.of(
ductape.messageBrokers.subscribe(Map.of(
topic: MESSAGE_TOPICS.NEW_MESSAGE,
filter: Map.of( conversation_id: conversationId ),
handler: async (message) => Map.of(
callback(message);
)
));
)
// Configure message broker topics
MESSAGE_TOPICS := map[string]any{
"NEW_MESSAGE": "chat.message.new",
"MESSAGE_UPDATED": "chat.message.updated",
"MESSAGE_DELETED": "chat.message.deleted",
"TYPING": "chat.typing",
"PRESENCE": "chat.presence",
"READ_RECEIPT": "chat.receipt.read"
};
// Publish message to broker
async function publishMessage(topic: string, data: any) {
client.messageBrokers.publish({
topic,
message: {
...data,
timestamp: new Date().toISOString()
}
});
}
// Subscribe to message events
async function subscribeToMessages(conversationId: string, callback: (message: any) => void) {
client.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.NEW_MESSAGE,
filter: { conversation_id: conversationId },
handler: async (message) => {
callback(message);
}
});
}
// Configure message broker topics
var MESSAGE_TOPICS = new Dictionary<string, object?>
{
["NEW_MESSAGE"] = "chat.message.new",
["MESSAGE_UPDATED"] = "chat.message.updated",
["MESSAGE_DELETED"] = "chat.message.deleted",
["TYPING"] = "chat.typing",
["PRESENCE"] = "chat.presence",
["READ_RECEIPT"] = "chat.receipt.read"
};
// Publish message to broker
async function publishMessage(topic: string, data: any) {
await ductape.messageBrokers.publish({
topic,
message: {
...data,
timestamp: DateTime.UtcNow.toISOString()
}
});
}
// Subscribe to message events
async function subscribeToMessages(conversationId: string, callback: (message: any) => void) {
await ductape.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.NEW_MESSAGE,
filter: { conversation_id: conversationId },
handler: async (message) => {
callback(message);
}
});
}
Sending Messages with Features
- TypeScript
- Java
- Go
- .NET
// Send message function
async function sendMessage(data: {
conversation_id: string;
sender_id: string;
content: string;
type?: string;
reply_to_id?: string;
}) {
// Start message feature
const feature = await ductape.features.execute({
feature: 'send-message',
input: {
conversation_id: data.conversation_id,
sender_id: data.sender_id,
content: data.content,
type: data.type || 'text',
reply_to_id: data.reply_to_id
}
});
return feature;
}
// Send message function
async function sendMessage(data: Map.of(
conversation_id: string;
sender_id: string;
content: string;
type?: string;
reply_to_id?: string;
)) Map.of(
// Start message feature
Map<String, Object> feature = ductape.features.execute(Map.of(
"feature", "send-message",
input: Map.of(
conversation_id: data.conversation_id,
sender_id: data.sender_id,
content: data.content,
type: data.type || 'text',
reply_to_id: data.reply_to_id
)
));
return feature;
)
// Send message function
async function sendMessage(data: {
conversation_id: string;
sender_id: string;
content: string;
type?: string;
reply_to_id?: string;
}) {
// Start message feature
feature := client.features.execute({
"feature": "send-message",
input: {
conversation_id: data.conversation_id,
sender_id: data.sender_id,
content: data.content,
type: data.type || 'text',
reply_to_id: data.reply_to_id
}
});
return feature;
}
// Send message function
async function sendMessage(data: {
conversation_id: string;
sender_id: string;
content: string;
type?: string;
reply_to_id?: string;
}) {
// Start message feature
var feature = await ductape.features.execute({
["feature"] = "send-message",
input: {
conversation_id: data.conversation_id,
sender_id: data.sender_id,
content: data.content,
type: data.type || 'text',
reply_to_id: data.reply_to_id
}
});
return feature;
}
Message Sending Feature
Create features/send-message.ts:
- TypeScript
- Java
- Go
- .NET
export const sendMessageFeature = {
name: 'send-message',
version: '1.0.0',
steps: [
// Step 1: Validate conversation access
{
name: 'validate-access',
type: 'function',
handler: async (context: any) => {
const conversation = await ductape.databases.findOne({
table: 'conversations',
where: { id: context.input.conversation_id }
});
if (!conversation.row) {
throw new Error('Conversation not found');
}
// Check if user is participant
if (!conversation.row.participant_ids.includes(context.input.sender_id)) {
throw new Error('User not authorized');
}
return { conversation: conversation.row };
}
},
// Step 2: Check content moderation
{
name: 'content-moderation',
type: 'function',
handler: async (context: any) => {
// Simple profanity check (in production, use AI moderation)
const profanityWords = ['spam', 'abuse']; // Add more
const content = context.input.content.toLowerCase();
const hasProfanity = profanityWords.some(word => content.includes(word));
if (hasProfanity) {
return {
flagged: true,
reason: 'Content contains inappropriate language'
};
}
return { flagged: false };
}
},
// Step 3: Create message
{
name: 'create-message',
type: 'function',
handler: async (context: any) => {
const message = await ductape.databases.insert({
table: 'messages',
data: {
conversation_id: context.input.conversation_id,
sender_id: context.input.sender_id,
content: context.input.content,
type: context.input.type,
reply_to_id: context.input.reply_to_id,
is_flagged: context.steps['content-moderation'].flagged
}
});
return { message: message.rows[0] };
}
},
// Step 4: Update conversation
{
name: 'update-conversation',
type: 'function',
handler: async (context: any) => {
const message = context.steps['create-message'].message;
await ductape.databases.update({
table: 'conversations',
where: { id: context.input.conversation_id },
data: {
last_message_id: message.id,
last_message_at: new Date(),
updated_at: new Date()
}
});
return { updated: true };
}
},
// Step 5: Create message status for all participants
{
name: 'create-status',
type: 'function',
handler: async (context: any) => {
const conversation = context.steps['validate-access'].conversation;
const message = context.steps['create-message'].message;
for (const participantId of conversation.participant_ids) {
await ductape.databases.insert({
table: 'message_status',
data: {
message_id: message.id,
user_id: participantId,
status: participantId === context.input.sender_id ? 'read' : 'sent',
read_at: participantId === context.input.sender_id ? new Date() : null
}
});
}
return { created: true };
}
},
// Step 6: Publish to message broker
{
name: 'publish-message',
type: 'function',
handler: async (context: any) => {
const message = context.steps['create-message'].message;
await ductape.messageBrokers.publish({
topic: MESSAGE_TOPICS.NEW_MESSAGE,
message: {
conversation_id: context.input.conversation_id,
message: message
}
});
return { published: true };
}
},
// Step 7: Send push notifications
{
name: 'send-notifications',
type: 'function',
handler: async (context: any) => {
const conversation = context.steps['validate-access'].conversation;
const message = context.steps['create-message'].message;
// Get sender info
const sender = await ductape.databases.findOne({
table: 'users',
where: { id: context.input.sender_id }
});
// Send to all participants except sender
for (const participantId of conversation.participant_ids) {
if (participantId !== context.input.sender_id) {
// Check if user is online
const presence = await ductape.databases.findOne({
table: 'presence',
where: { user_id: participantId }
});
// Only send push if user is offline or away
if (!presence.row || presence.row.status !== 'online') {
await ductape.notifications.send({
channel: 'push',
to: participantId,
data: {
title: sender.row.display_name,
body: message.content.substring(0, 100),
conversation_id: context.input.conversation_id,
message_id: message.id
}
});
}
}
}
return { sent: true };
}
}
],
errorHandlers: [
{
step: 'validate-access',
handler: async (context: any, error: any) => {
console.error('Access validation failed:', error.message);
throw error;
}
},
{
step: 'content-moderation',
handler: async (context: any, error: any) => {
// Log moderation failure but don't block message
console.error('Moderation failed:', error.message);
return { flagged: false };
}
}
]
};
export Map<String, Object> sendMessageFeature = Map.of(
"name", "send-message",
"version", "1.0.0",
steps: [
// Step 1: Validate conversation access
Map.of(
"name", "validate-access",
"type", "function",
handler: async (context: any) => Map.of(
Map<String, Object> conversation = ductape.databases.findOne(Map.of(
"table", "conversations",
where: Map.of( id: context.input.conversation_id )
));
if (!conversation.row) Map.of(
throw new Error('Conversation not found');
)
// Check if user is participant
if (!conversation.row.participant_ids.includes(context.input.sender_id)) Map.of(
throw new Error('User not authorized');
)
return Map.of( conversation: conversation.row );
)
),
// Step 2: Check content moderation
Map.of(
"name", "content-moderation",
"type", "function",
handler: async (context: any) => Map.of(
// Simple profanity check (in production, use AI moderation)
Map<String, Object> profanityWords = ['spam', 'abuse']; // Add more
Map<String, Object> content = context.input.content.toLowerCase();
Map<String, Object> hasProfanity = profanityWords.some(word => content.includes(word));
if (hasProfanity) Map.of(
return Map.of(
"flagged", true,
"reason", "Content contains inappropriate language"
);
)
return Map.of( "flagged", false );
)
),
// Step 3: Create message
Map.of(
"name", "create-message",
"type", "function",
handler: async (context: any) => Map.of(
Map<String, Object> message = ductape.databases().insert(Map<String, Object>.of(
"table", "messages",
data: Map.of(
conversation_id: context.input.conversation_id,
sender_id: context.input.sender_id,
content: context.input.content,
type: context.input.type,
reply_to_id: context.input.reply_to_id,
is_flagged: context.steps['content-moderation'].flagged
)
));
return Map.of( message: message.rows[0] );
)
),
// Step 4: Update conversation
Map.of(
"name", "update-conversation",
"type", "function",
handler: async (context: any) => Map.of(
Map<String, Object> message = context.steps['create-message'].message;
ductape.databases.update(Map.of(
"table", "conversations",
where: Map.of( id: context.input.conversation_id ),
data: Map.of(
last_message_id: message.id,
last_message_at: Instant.now(),
updated_at: Instant.now()
)
));
return Map.of( "updated", true );
)
),
// Step 5: Create message status for all participants
Map.of(
"name", "create-status",
"type", "function",
handler: async (context: any) => Map.of(
Map<String, Object> conversation = context.steps['validate-access'].conversation;
Map<String, Object> message = context.steps['create-message'].message;
for (Map<String, Object> participantId of conversation.participant_ids) Map.of(
ductape.databases().insert(Map<String, Object>.of(
"table", "message_status",
data: Map.of(
message_id: message.id,
user_id: participantId,
status: participantId === context.input.sender_id ? 'read' : 'sent',
read_at: participantId === context.input.sender_id ? Instant.now() : null
)
));
)
return Map.of( "created", true );
)
),
// Step 6: Publish to message broker
Map.of(
"name", "publish-message",
"type", "function",
handler: async (context: any) => Map.of(
Map<String, Object> message = context.steps['create-message'].message;
ductape.messageBrokers.publish(Map.of(
topic: MESSAGE_TOPICS.NEW_MESSAGE,
message: Map.of(
conversation_id: context.input.conversation_id,
message: message
)
));
return Map.of( "published", true );
)
),
// Step 7: Send push notifications
Map.of(
"name", "send-notifications",
"type", "function",
handler: async (context: any) => Map.of(
Map<String, Object> conversation = context.steps['validate-access'].conversation;
Map<String, Object> message = context.steps['create-message'].message;
// Get sender info
Map<String, Object> sender = ductape.databases.findOne(Map.of(
"table", "users",
where: Map.of( id: context.input.sender_id )
));
// Send to all participants except sender
for (Map<String, Object> participantId of conversation.participant_ids) Map.of(
if (participantId !== context.input.sender_id) Map.of(
// Check if user is online
Map<String, Object> presence = ductape.databases.findOne(Map.of(
"table", "presence",
where: Map.of( user_id: participantId )
));
// Only send push if user is offline or away
if (!presence.row || presence.row.status !== 'online') Map.of(
ductape.notifications().send(Map<String, Object>.of(
"channel", "push",
to: participantId,
data: Map.of(
title: sender.row.display_name,
body: message.content.substring(0, 100),
conversation_id: context.input.conversation_id,
message_id: message.id
)
));
)
)
)
return Map.of( "sent", true );
)
)
],
errorHandlers: [
Map.of(
"step", "validate-access",
handler: async (context: any, error: any) => Map.of(
console.error('Access validation "failed", ", error.message);
throw error;
)
),
Map.of(
step: "content-moderation',
handler: async (context: any, error: any) => Map.of(
// Log moderation failure but don't block message
console.error('Moderation failed:', error.message);
return Map.of( "flagged", false );
)
)
]
);
import "context"
export sendMessageFeature := map[string]any{
"name": "send-message",
"version": "1.0.0",
steps: [
// Step 1: Validate conversation access
{
"name": "validate-access",
"type": "function",
handler: async (context: any) => {
conversation := client.databases.findOne({
"table": "conversations",
where: { id: context.input.conversation_id }
});
if (!conversation.row) {
throw new Error('Conversation not found');
}
// Check if user is participant
if (!conversation.row.participant_ids.includes(context.input.sender_id)) {
throw new Error('User not authorized');
}
return { conversation: conversation.row };
}
},
// Step 2: Check content moderation
{
"name": "content-moderation",
"type": "function",
handler: async (context: any) => {
// Simple profanity check (in production, use AI moderation)
profanityWords := ['spam', 'abuse']; // Add more
content := context.input.content.toLowerCase();
hasProfanity := profanityWords.some(word => content.includes(word));
if (hasProfanity) {
return {
"flagged": true,
"reason": "Content contains inappropriate language"
};
}
return { "flagged": false };
}
},
// Step 3: Create message
{
"name": "create-message",
"type": "function",
handler: async (context: any) => {
message := client.Databases.Insert(ctx, map[string]any{
"table": "messages",
data: {
conversation_id: context.input.conversation_id,
sender_id: context.input.sender_id,
content: context.input.content,
type: context.input.type,
reply_to_id: context.input.reply_to_id,
is_flagged: context.steps['content-moderation'].flagged
}
});
return { message: message.rows[0] };
}
},
// Step 4: Update conversation
{
"name": "update-conversation",
"type": "function",
handler: async (context: any) => {
message := context.steps['create-message'].message;
client.databases.update({
"table": "conversations",
where: { id: context.input.conversation_id },
data: {
last_message_id: message.id,
last_message_at: new Date(),
updated_at: new Date()
}
});
return { "updated": true };
}
},
// Step 5: Create message status for all participants
{
"name": "create-status",
"type": "function",
handler: async (context: any) => {
conversation := context.steps['validate-access'].conversation;
message := context.steps['create-message'].message;
for (const participantId of conversation.participant_ids) {
client.Databases.Insert(ctx, map[string]any{
"table": "message_status",
data: {
message_id: message.id,
user_id: participantId,
status: participantId === context.input.sender_id ? 'read' : 'sent',
read_at: participantId === context.input.sender_id ? new Date() : null
}
});
}
return { "created": true };
}
},
// Step 6: Publish to message broker
{
"name": "publish-message",
"type": "function",
handler: async (context: any) => {
message := context.steps['create-message'].message;
client.messageBrokers.publish({
topic: MESSAGE_TOPICS.NEW_MESSAGE,
message: {
conversation_id: context.input.conversation_id,
message: message
}
});
return { "published": true };
}
},
// Step 7: Send push notifications
{
"name": "send-notifications",
"type": "function",
handler: async (context: any) => {
conversation := context.steps['validate-access'].conversation;
message := context.steps['create-message'].message;
// Get sender info
sender := client.databases.findOne({
"table": "users",
where: { id: context.input.sender_id }
});
// Send to all participants except sender
for (const participantId of conversation.participant_ids) {
if (participantId !== context.input.sender_id) {
// Check if user is online
presence := client.databases.findOne({
"table": "presence",
where: { user_id: participantId }
});
// Only send push if user is offline or away
if (!presence.row || presence.row.status !== 'online') {
client.Notifications.Send(ctx, map[string]any{
"channel": "push",
to: participantId,
data: {
title: sender.row.display_name,
body: message.content.substring(0, 100),
conversation_id: context.input.conversation_id,
message_id: message.id
}
});
}
}
}
return { "sent": true };
}
}
],
errorHandlers: [
{
"step": "validate-access",
handler: async (context: any, error: any) => {
console.error('Access validation "failed": ", error.message);
throw error;
}
},
{
step: "content-moderation',
handler: async (context: any, error: any) => {
// Log moderation failure but don't block message
console.error('Moderation failed:', error.message);
return { "flagged": false };
}
}
]
};
export var sendMessageFeature = new Dictionary<string, object?>
{
["name"] = "send-message",
["version"] = "1.0.0",
steps: [
// Step 1: Validate conversation access
{
["name"] = "validate-access",
["type"] = "function",
handler: async (context: any) => {
var conversation = await ductape.databases.findOne({
["table"] = "conversations",
where: { id: context.input.conversation_id }
});
if (!conversation.row) {
throw new Error('Conversation not found');
}
// Check if user is participant
if (!conversation.row.participant_ids.includes(context.input.sender_id)) {
throw new Error('User not authorized');
}
return { conversation: conversation.row };
}
},
// Step 2: Check content moderation
{
["name"] = "content-moderation",
["type"] = "function",
handler: async (context: any) => {
// Simple profanity check (in production, use AI moderation)
var profanityWords = ['spam', 'abuse']; // Add more
var content = context.input.content.toLowerCase();
var hasProfanity = profanityWords.some(word => content.includes(word));
if (hasProfanity) {
return {
["flagged"] = true,
["reason"] = "Content contains inappropriate language"
};
}
return { ["flagged"] = false };
}
},
// Step 3: Create message
{
["name"] = "create-message",
["type"] = "function",
handler: async (context: any) => {
var message = await ductape.Database.Insert(new Dictionary<string, object?>
{
["table"] = "messages",
data: {
conversation_id: context.input.conversation_id,
sender_id: context.input.sender_id,
content: context.input.content,
type: context.input.type,
reply_to_id: context.input.reply_to_id,
is_flagged: context.steps['content-moderation'].flagged
}
});
return { message: message.rows[0] };
}
},
// Step 4: Update conversation
{
["name"] = "update-conversation",
["type"] = "function",
handler: async (context: any) => {
var message = context.steps['create-message'].message;
await ductape.databases.update({
["table"] = "conversations",
where: { id: context.input.conversation_id },
data: {
last_message_id: message.id,
last_message_at: DateTime.UtcNow,
updated_at: DateTime.UtcNow
}
});
return { ["updated"] = true };
}
},
// Step 5: Create message status for all participants
{
["name"] = "create-status",
["type"] = "function",
handler: async (context: any) => {
var conversation = context.steps['validate-access'].conversation;
var message = context.steps['create-message'].message;
for (var participantId of conversation.participant_ids) {
await ductape.Database.Insert(new Dictionary<string, object?>
{
["table"] = "message_status",
data: {
message_id: message.id,
user_id: participantId,
status: participantId === context.input.sender_id ? 'read' : 'sent',
read_at: participantId === context.input.sender_id ? DateTime.UtcNow : null
}
});
}
return { ["created"] = true };
}
},
// Step 6: Publish to message broker
{
["name"] = "publish-message",
["type"] = "function",
handler: async (context: any) => {
var message = context.steps['create-message'].message;
await ductape.messageBrokers.publish({
topic: MESSAGE_TOPICS.NEW_MESSAGE,
message: {
conversation_id: context.input.conversation_id,
message: message
}
});
return { ["published"] = true };
}
},
// Step 7: Send push notifications
{
["name"] = "send-notifications",
["type"] = "function",
handler: async (context: any) => {
var conversation = context.steps['validate-access'].conversation;
var message = context.steps['create-message'].message;
// Get sender info
var sender = await ductape.databases.findOne({
["table"] = "users",
where: { id: context.input.sender_id }
});
// Send to all participants except sender
for (var participantId of conversation.participant_ids) {
if (participantId !== context.input.sender_id) {
// Check if user is online
var presence = await ductape.databases.findOne({
["table"] = "presence",
where: { user_id: participantId }
});
// Only send push if user is offline or away
if (!presence.row || presence.row.status !== 'online') {
await await ductape.Notifications.SendAsync(new Dictionary<string, object?>
{
["channel"] = "push",
to: participantId,
data: {
title: sender.row.display_name,
body: message.content.substring(0, 100),
conversation_id: context.input.conversation_id,
message_id: message.id
}
});
}
}
}
return { ["sent"] = true };
}
}
],
errorHandlers: [
{
["step"] = "validate-access",
handler: async (context: any, error: any) => {
console.error('Access validation ["failed"] = ", error.message);
throw error;
}
},
{
step: "content-moderation',
handler: async (context: any, error: any) => {
// Log moderation failure but don't block message
console.error('Moderation failed:', error.message);
return { ["flagged"] = false };
}
}
]
};
WebSocket Server with Socket.IO
- TypeScript
- Java
- Go
- .NET
// Socket.IO connection handling
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
let currentUserId: string;
// Authenticate user
socket.on('authenticate', async (data: { user_id: string; token: string }) => {
// Verify token (implement your auth logic)
currentUserId = data.user_id;
// Update presence
await updatePresence(currentUserId, 'online', socket.id);
// Join user's conversations
const conversations = await getUserConversations(currentUserId);
conversations.forEach((conv: any) => {
socket.join(`conversation:${conv.id}`);
});
socket.emit('authenticated', { user_id: currentUserId });
});
// Send message
socket.on('send-message', async (data: {
conversation_id: string;
content: string;
type?: string;
reply_to_id?: string;
}) => {
try {
await sendMessage({
conversation_id: data.conversation_id,
sender_id: currentUserId,
content: data.content,
type: data.type,
reply_to_id: data.reply_to_id
});
} catch (error: any) {
socket.emit('error', { message: error.message });
}
});
// Typing indicator
socket.on('typing', async (data: { conversation_id: string; is_typing: boolean }) => {
await updateTypingStatus(currentUserId, data.conversation_id, data.is_typing);
// Publish to message broker
await publishMessage(MESSAGE_TOPICS.TYPING, {
user_id: currentUserId,
conversation_id: data.conversation_id,
is_typing: data.is_typing
});
// Broadcast to conversation
socket.to(`conversation:${data.conversation_id}`).emit('user-typing', {
user_id: currentUserId,
is_typing: data.is_typing
});
});
// Mark message as read
socket.on('mark-read', async (data: { message_id: string }) => {
await markMessageAsRead(data.message_id, currentUserId);
// Publish read receipt
await publishMessage(MESSAGE_TOPICS.READ_RECEIPT, {
message_id: data.message_id,
user_id: currentUserId,
read_at: new Date()
});
});
// Join conversation
socket.on('join-conversation', (data: { conversation_id: string }) => {
socket.join(`conversation:${data.conversation_id}`);
});
// Leave conversation
socket.on('leave-conversation', (data: { conversation_id: string }) => {
socket.leave(`conversation:${data.conversation_id}`);
});
// Disconnect
socket.on('disconnect', async () => {
if (currentUserId) {
await updatePresence(currentUserId, 'offline', null);
}
console.log('User disconnected:', socket.id);
});
});
// Socket.IO connection handling
io.on('connection', (socket) => Map.of(
System.out.println('User "connected", ", socket.id);
Map<String, Object> currentUserId: string;
// Authenticate user
socket.on("authenticate', async (data: Map.of( user_id: string; token: string )) => Map.of(
// Verify token (implement your auth logic)
currentUserId = data.user_id;
// Update presence
updatePresence(currentUserId, 'online', socket.id);
// Join user's conversations
Map<String, Object> conversations = getUserConversations(currentUserId);
conversations.forEach((conv: any) => Map.of(
socket.join(`conversation:$Map.of(conv.id)`);
));
socket.emit('authenticated', Map.of( user_id: currentUserId ));
));
// Send message
socket.on('send-message', async (data: Map.of(
conversation_id: string;
content: string;
type?: string;
reply_to_id?: string;
)) => Map.of(
try Map.of(
sendMessage(Map.of(
conversation_id: data.conversation_id,
sender_id: currentUserId,
content: data.content,
type: data.type,
reply_to_id: data.reply_to_id
));
) catch (error: any) Map.of(
socket.emit('error', Map.of( message: error.message ));
)
));
// Typing indicator
socket.on('typing', async (data: Map.of( conversation_id: string; is_typing: boolean )) => Map.of(
updateTypingStatus(currentUserId, data.conversation_id, data.is_typing);
// Publish to message broker
publishMessage(MESSAGE_TOPICS.TYPING, Map.of(
user_id: currentUserId,
conversation_id: data.conversation_id,
is_typing: data.is_typing
));
// Broadcast to conversation
socket.to(`conversation:$Map.of(data.conversation_id)`).emit('user-typing', Map.of(
user_id: currentUserId,
is_typing: data.is_typing
));
));
// Mark message as read
socket.on('mark-read', async (data: Map.of( message_id: string )) => Map.of(
markMessageAsRead(data.message_id, currentUserId);
// Publish read receipt
publishMessage(MESSAGE_TOPICS.READ_RECEIPT, Map.of(
message_id: data.message_id,
user_id: currentUserId,
read_at: Instant.now()
));
));
// Join conversation
socket.on('join-conversation', (data: Map.of( conversation_id: string )) => Map.of(
socket.join(`conversation:$Map.of(data.conversation_id)`);
));
// Leave conversation
socket.on('leave-conversation', (data: Map.of( conversation_id: string )) => Map.of(
socket.leave(`conversation:$Map.of(data.conversation_id)`);
));
// Disconnect
socket.on('disconnect', async () => Map.of(
if (currentUserId) Map.of(
updatePresence(currentUserId, 'offline', null);
)
System.out.println('User disconnected:', socket.id);
));
));
// Socket.IO connection handling
io.on('connection', (socket) => {
fmt.Println('User "connected": ", socket.id);
let currentUserId: string;
// Authenticate user
socket.on("authenticate', async (data: { user_id: string; token: string }) => {
// Verify token (implement your auth logic)
currentUserId = data.user_id;
// Update presence
updatePresence(currentUserId, 'online', socket.id);
// Join user's conversations
conversations := getUserConversations(currentUserId);
conversations.forEach((conv: any) => {
socket.join(`conversation:${conv.id}`);
});
socket.emit('authenticated', { user_id: currentUserId });
});
// Send message
socket.on('send-message', async (data: {
conversation_id: string;
content: string;
type?: string;
reply_to_id?: string;
}) => {
try {
sendMessage({
conversation_id: data.conversation_id,
sender_id: currentUserId,
content: data.content,
type: data.type,
reply_to_id: data.reply_to_id
});
} catch (error: any) {
socket.emit('error', { message: error.message });
}
});
// Typing indicator
socket.on('typing', async (data: { conversation_id: string; is_typing: boolean }) => {
updateTypingStatus(currentUserId, data.conversation_id, data.is_typing);
// Publish to message broker
publishMessage(MESSAGE_TOPICS.TYPING, {
user_id: currentUserId,
conversation_id: data.conversation_id,
is_typing: data.is_typing
});
// Broadcast to conversation
socket.to(`conversation:${data.conversation_id}`).emit('user-typing', {
user_id: currentUserId,
is_typing: data.is_typing
});
});
// Mark message as read
socket.on('mark-read', async (data: { message_id: string }) => {
markMessageAsRead(data.message_id, currentUserId);
// Publish read receipt
publishMessage(MESSAGE_TOPICS.READ_RECEIPT, {
message_id: data.message_id,
user_id: currentUserId,
read_at: new Date()
});
});
// Join conversation
socket.on('join-conversation', (data: { conversation_id: string }) => {
socket.join(`conversation:${data.conversation_id}`);
});
// Leave conversation
socket.on('leave-conversation', (data: { conversation_id: string }) => {
socket.leave(`conversation:${data.conversation_id}`);
});
// Disconnect
socket.on('disconnect', async () => {
if (currentUserId) {
updatePresence(currentUserId, 'offline', null);
}
fmt.Println('User disconnected:', socket.id);
});
});
// Socket.IO connection handling
io.on('connection', (socket) => {
Console.WriteLine('User ["connected"] = ", socket.id);
var currentUserId: string;
// Authenticate user
socket.on("authenticate', async (data: { user_id: string; token: string }) => {
// Verify token (implement your auth logic)
currentUserId = data.user_id;
// Update presence
await updatePresence(currentUserId, 'online', socket.id);
// Join user's conversations
var conversations = await getUserConversations(currentUserId);
conversations.forEach((conv: any) => {
socket.join(`conversation:${conv.id}`);
});
socket.emit('authenticated', { user_id: currentUserId });
});
// Send message
socket.on('send-message', async (data: {
conversation_id: string;
content: string;
type?: string;
reply_to_id?: string;
}) => {
try {
await sendMessage({
conversation_id: data.conversation_id,
sender_id: currentUserId,
content: data.content,
type: data.type,
reply_to_id: data.reply_to_id
});
} catch (error: any) {
socket.emit('error', { message: error.message });
}
});
// Typing indicator
socket.on('typing', async (data: { conversation_id: string; is_typing: boolean }) => {
await updateTypingStatus(currentUserId, data.conversation_id, data.is_typing);
// Publish to message broker
await publishMessage(MESSAGE_TOPICS.TYPING, {
user_id: currentUserId,
conversation_id: data.conversation_id,
is_typing: data.is_typing
});
// Broadcast to conversation
socket.to(`conversation:${data.conversation_id}`).emit('user-typing', {
user_id: currentUserId,
is_typing: data.is_typing
});
});
// Mark message as read
socket.on('mark-read', async (data: { message_id: string }) => {
await markMessageAsRead(data.message_id, currentUserId);
// Publish read receipt
await publishMessage(MESSAGE_TOPICS.READ_RECEIPT, {
message_id: data.message_id,
user_id: currentUserId,
read_at: DateTime.UtcNow
});
});
// Join conversation
socket.on('join-conversation', (data: { conversation_id: string }) => {
socket.join(`conversation:${data.conversation_id}`);
});
// Leave conversation
socket.on('leave-conversation', (data: { conversation_id: string }) => {
socket.leave(`conversation:${data.conversation_id}`);
});
// Disconnect
socket.on('disconnect', async () => {
if (currentUserId) {
await updatePresence(currentUserId, 'offline', null);
}
Console.WriteLine('User disconnected:', socket.id);
});
});
Presence Management
- TypeScript
- Java
- Go
- .NET
// Update user presence
async function updatePresence(
userId: string,
status: 'online' | 'offline' | 'away',
socketId: string | null
) {
const existing = await ductape.databases.findOne({
table: 'presence',
where: { user_id: userId }
});
if (existing.row) {
await ductape.databases.update({
table: 'presence',
where: { user_id: userId },
data: {
status,
last_activity: new Date(),
device_info: socketId ? { socket_id: socketId } : existing.row.device_info
}
});
} else {
await ductape.databases.insert({
table: 'presence',
data: {
user_id: userId,
status,
device_info: socketId ? { socket_id: socketId } : {}
}
});
}
// Update user record
await ductape.databases.update({
table: 'users',
where: { id: userId },
data: {
status,
last_seen: new Date()
}
});
// Publish presence update
await publishMessage(MESSAGE_TOPICS.PRESENCE, {
user_id: userId,
status,
timestamp: new Date()
});
}
// Update typing status
async function updateTypingStatus(
userId: string,
conversationId: string,
isTyping: boolean
) {
await ductape.databases.update({
table: 'presence',
where: { user_id: userId },
data: {
conversation_id: isTyping ? conversationId : null,
is_typing: isTyping,
last_activity: new Date()
}
});
}
// Auto-clear inactive typing indicators
await ductape.jobs.schedule({
name: 'clear-stale-typing',
schedule: '*/30 * * * * *', // Every 30 seconds
handler: async () => {
const thirtySecondsAgo = new Date(Date.now() - 30000);
await ductape.databases.update({
table: 'presence',
where: {
is_typing: true,
last_activity: { $lt: thirtySecondsAgo }
},
data: {
is_typing: false,
conversation_id: null
}
});
}
});
// Update user presence
async function updatePresence(
userId: string,
"status", "online" | 'offline' | 'away',
socketId: string | null
) Map.of(
Map<String, Object> existing = ductape.databases.findOne(Map.of(
"table", "presence",
where: Map.of( user_id: userId )
));
if (existing.row) Map.of(
ductape.databases.update(Map.of(
"table", "presence",
where: Map.of( user_id: userId ),
data: Map.of(
status,
last_activity: Instant.now(),
device_info: socketId ? Map.of( socket_id: socketId ) : existing.row.device_info
)
));
) else Map.of(
ductape.databases().insert(Map<String, Object>.of(
"table", "presence",
data: Map.of(
user_id: userId,
status,
device_info: socketId ? Map.of( socket_id: socketId ) : Map.of()
)
));
)
// Update user record
ductape.databases.update(Map.of(
"table", "users",
where: Map.of( id: userId ),
data: Map.of(
status,
last_seen: Instant.now()
)
));
// Publish presence update
publishMessage(MESSAGE_TOPICS.PRESENCE, Map.of(
user_id: userId,
status,
timestamp: Instant.now()
));
)
// Update typing status
async function updateTypingStatus(
userId: string,
conversationId: string,
isTyping: boolean
) Map.of(
ductape.databases.update(Map.of(
"table", "presence",
where: Map.of( user_id: userId ),
data: Map.of(
conversation_id: isTyping ? conversationId : null,
is_typing: isTyping,
last_activity: Instant.now()
)
));
)
// Auto-clear inactive typing indicators
ductape.jobs.schedule(Map.of(
"name", "clear-stale-typing",
"schedule", "*/30 * * * * *", // Every 30 seconds
handler: async () => Map.of(
Map<String, Object> thirtySecondsAgo = new Date(Date.now() - 30000);
ductape.databases.update(Map.of(
"table", "presence",
where: Map.of(
"is_typing", true,
last_activity: Map.of( $lt: thirtySecondsAgo )
),
data: Map.of(
"is_typing", false,
conversation_id: null
)
));
)
));
import "context"
// Update user presence
async function updatePresence(
userId: string,
"status": "online" | 'offline' | 'away',
socketId: string | null
) {
existing := client.databases.findOne({
"table": "presence",
where: { user_id: userId }
});
if (existing.row) {
client.databases.update({
"table": "presence",
where: { user_id: userId },
data: {
status,
last_activity: new Date(),
device_info: socketId ? { socket_id: socketId } : existing.row.device_info
}
});
} else {
client.Databases.Insert(ctx, map[string]any{
"table": "presence",
data: {
user_id: userId,
status,
device_info: socketId ? { socket_id: socketId } : {}
}
});
}
// Update user record
client.databases.update({
"table": "users",
where: { id: userId },
data: {
status,
last_seen: new Date()
}
});
// Publish presence update
publishMessage(MESSAGE_TOPICS.PRESENCE, {
user_id: userId,
status,
timestamp: new Date()
});
}
// Update typing status
async function updateTypingStatus(
userId: string,
conversationId: string,
isTyping: boolean
) {
client.databases.update({
"table": "presence",
where: { user_id: userId },
data: {
conversation_id: isTyping ? conversationId : null,
is_typing: isTyping,
last_activity: new Date()
}
});
}
// Auto-clear inactive typing indicators
client.jobs.schedule({
"name": "clear-stale-typing",
"schedule": "*/30 * * * * *", // Every 30 seconds
handler: async () => {
thirtySecondsAgo := new Date(Date.now() - 30000);
client.databases.update({
"table": "presence",
where: {
"is_typing": true,
last_activity: { $lt: thirtySecondsAgo }
},
data: {
"is_typing": false,
conversation_id: null
}
});
}
});
// Update user presence
async function updatePresence(
userId: string,
["status"] = "online" | 'offline' | 'away',
socketId: string | null
) {
var existing = await ductape.databases.findOne({
["table"] = "presence",
where: { user_id: userId }
});
if (existing.row) {
await ductape.databases.update({
["table"] = "presence",
where: { user_id: userId },
data: {
status,
last_activity: DateTime.UtcNow,
device_info: socketId ? { socket_id: socketId } : existing.row.device_info
}
});
} else {
await ductape.Database.Insert(new Dictionary<string, object?>
{
["table"] = "presence",
data: {
user_id: userId,
status,
device_info: socketId ? { socket_id: socketId } : {}
}
});
}
// Update user record
await ductape.databases.update({
["table"] = "users",
where: { id: userId },
data: {
status,
last_seen: DateTime.UtcNow
}
});
// Publish presence update
await publishMessage(MESSAGE_TOPICS.PRESENCE, {
user_id: userId,
status,
timestamp: DateTime.UtcNow
});
}
// Update typing status
async function updateTypingStatus(
userId: string,
conversationId: string,
isTyping: boolean
) {
await ductape.databases.update({
["table"] = "presence",
where: { user_id: userId },
data: {
conversation_id: isTyping ? conversationId : null,
is_typing: isTyping,
last_activity: DateTime.UtcNow
}
});
}
// Auto-clear inactive typing indicators
await ductape.jobs.schedule({
["name"] = "clear-stale-typing",
["schedule"] = "*/30 * * * * *", // Every 30 seconds
handler: async () => {
var thirtySecondsAgo = new Date(Date.now() - 30000);
await ductape.databases.update({
["table"] = "presence",
where: {
["is_typing"] = true,
last_activity: { $lt: thirtySecondsAgo }
},
data: {
["is_typing"] = false,
conversation_id: null
}
});
}
});
File Upload with Storage
- TypeScript
- Java
- Go
- .NET
// Upload file for message
app.post('/upload', async (req, res) => {
try {
const { conversation_id, user_id } = req.body;
const file = req.files?.file; // Using express-fileupload middleware
if (!file) {
return res.status(400).json({ error: 'No file provided' });
}
// Upload to storage
const upload = await ductape.storage.upload({
file: file as any,
path: `chat/${conversation_id}/${Date.now()}`,
metadata: {
conversation_id,
user_id,
original_name: file.name
}
});
res.json({
url: upload.url,
file_name: file.name,
file_size: file.size,
file_type: file.mimetype
});
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
// Get file with signed URL
app.get('/files/:fileId', async (req, res) => {
try {
const signedUrl = await ductape.storage.getSignedUrl({
path: `chat/files/${req.params.fileId}`,
expiresIn: 3600 // 1 hour
});
res.json({ url: signedUrl });
} catch (error: any) {
res.status(404).json({ error: 'File not found' });
}
});
// Upload file for message
app.post('/upload', async (req, res) => Map.of(
try Map.of(
Map<String, Object> Map.of( conversation_id, user_id ) = req.body;
Map<String, Object> file = req.files?.file; // Using express-fileupload middleware
if (!file) Map.of(
return res.status(400).json(Map.of( "error", "No file provided" ));
)
// Upload to storage
Map<String, Object> upload = ductape.storage.upload(Map.of(
file: file as any,
path: `chat/$Map.of(conversation_id)/$Map.of(Date.now())`,
metadata: Map.of(
conversation_id,
user_id,
original_name: file.name
)
));
res.json(Map.of(
url: upload.url,
file_name: file.name,
file_size: file.size,
file_type: file.mimetype
));
) catch (error: any) Map.of(
res.status(500).json(Map.of( error: error.message ));
)
));
// Get file with signed URL
app.get('/files/:fileId', async (req, res) => Map.of(
try Map.of(
Map<String, Object> signedUrl = ductape.storage.getSignedUrl(Map.of(
path: `chat/files/$Map.of(req.params.fileId)`,
"expiresIn", 3600 // 1 hour
));
res.json(Map.of( url: signedUrl ));
) catch (error: any) Map.of(
res.status(404).json(Map.of( "error", "File not found" ));
)
));
// Upload file for message
app.post('/upload', async (req, res) => {
try {
const { conversation_id, user_id } = req.body;
file := req.files?.file; // Using express-fileupload middleware
if (!file) {
return res.status(400).json({ "error": "No file provided" });
}
// Upload to storage
upload := client.storage.upload({
file: file as any,
path: `chat/${conversation_id}/${Date.now()}`,
metadata: {
conversation_id,
user_id,
original_name: file.name
}
});
res.json({
url: upload.url,
file_name: file.name,
file_size: file.size,
file_type: file.mimetype
});
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
// Get file with signed URL
app.get('/files/:fileId', async (req, res) => {
try {
signedUrl := client.storage.getSignedUrl({
path: `chat/files/${req.params.fileId}`,
"expiresIn": 3600 // 1 hour
});
res.json({ url: signedUrl });
} catch (error: any) {
res.status(404).json({ "error": "File not found" });
}
});
// Upload file for message
app.post('/upload', async (req, res) => {
try {
var { conversation_id, user_id } = req.body;
var file = req.files?.file; // Using express-fileupload middleware
if (!file) {
return res.status(400).json({ ["error"] = "No file provided" });
}
// Upload to storage
var upload = await ductape.storage.upload({
file: file as any,
path: `chat/${conversation_id}/${Date.now()}`,
metadata: {
conversation_id,
user_id,
original_name: file.name
}
});
res.json({
url: upload.url,
file_name: file.name,
file_size: file.size,
file_type: file.mimetype
});
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
// Get file with signed URL
app.get('/files/:fileId', async (req, res) => {
try {
var signedUrl = await ductape.storage.getSignedUrl({
path: `chat/files/${req.params.fileId}`,
["expiresIn"] = 3600 // 1 hour
});
res.json({ url: signedUrl });
} catch (error: any) {
res.status(404).json({ ["error"] = "File not found" });
}
});
Message Search
- TypeScript
- Java
- Go
- .NET
// Search messages in conversation
app.get('/conversations/:conversationId/search', async (req, res) => {
const { q, limit = 20, offset = 0 } = req.query;
const messages = await ductape.databases.find({
table: 'messages',
where: {
conversation_id: req.params.conversationId,
content: { $regex: q as string, $options: 'i' },
is_deleted: false
},
limit: Number(limit),
offset: Number(offset),
orderBy: { created_at: 'desc' }
});
res.json(messages.rows);
});
// Global search across all user's conversations
app.get('/search', async (req, res) => {
const { q, user_id, limit = 20 } = req.query;
// Get user's conversations
const conversations = await getUserConversations(user_id as string);
const conversationIds = conversations.map((c: any) => c.id);
// Search messages
const messages = await ductape.databases.find({
table: 'messages',
where: {
conversation_id: { $in: conversationIds },
content: { $regex: q as string, $options: 'i' },
is_deleted: false
},
limit: Number(limit),
orderBy: { created_at: 'desc' }
});
res.json(messages.rows);
});
// Search messages in conversation
app.get('/conversations/:conversationId/search', async (req, res) => Map.of(
Map<String, Object> Map.of( q, limit = 20, offset = 0 ) = req.query;
Map<String, Object> messages = ductape.databases().query(Map<String, Object>.of(
"table", "messages",
where: Map.of(
conversation_id: req.params.conversationId,
content: Map.of( $regex: q as string, $"options", "i" ),
"is_deleted", false
),
limit: Number(limit),
offset: Number(offset),
orderBy: Map.of( "created_at", "desc" )
));
res.json(messages.rows);
));
// Global search across all user's conversations
app.get('/search', async (req, res) => Map.of(
Map<String, Object> Map.of( q, user_id, limit = 20 ) = req.query;
// Get user's conversations
Map<String, Object> conversations = getUserConversations(user_id as string);
Map<String, Object> conversationIds = conversations.map((c: any) => c.id);
// Search messages
Map<String, Object> messages = ductape.databases().query(Map<String, Object>.of(
"table", "messages",
where: Map.of(
conversation_id: Map.of( $in: conversationIds ),
content: Map.of( $regex: q as string, $"options", "i" ),
"is_deleted", false
),
limit: Number(limit),
orderBy: Map.of( "created_at", "desc" )
));
res.json(messages.rows);
));
import "context"
// Search messages in conversation
app.get('/conversations/:conversationId/search', async (req, res) => {
const { q, limit = 20, offset = 0 } = req.query;
messages := client.Databases.Query(ctx, map[string]any{
"table": "messages",
where: {
conversation_id: req.params.conversationId,
content: { $regex: q as string, $"options": "i" },
"is_deleted": false
},
limit: Number(limit),
offset: Number(offset),
orderBy: { "created_at": "desc" }
});
res.json(messages.rows);
});
// Global search across all user's conversations
app.get('/search', async (req, res) => {
const { q, user_id, limit = 20 } = req.query;
// Get user's conversations
conversations := getUserConversations(user_id as string);
conversationIds := conversations.map((c: any) => c.id);
// Search messages
messages := client.Databases.Query(ctx, map[string]any{
"table": "messages",
where: {
conversation_id: { $in: conversationIds },
content: { $regex: q as string, $"options": "i" },
"is_deleted": false
},
limit: Number(limit),
orderBy: { "created_at": "desc" }
});
res.json(messages.rows);
});
// Search messages in conversation
app.get('/conversations/:conversationId/search', async (req, res) => {
var { q, limit = 20, offset = 0 } = req.query;
var messages = await ductape.Database.Query(new Dictionary<string, object?>
{
["table"] = "messages",
where: {
conversation_id: req.params.conversationId,
content: { $regex: q as string, $["options"] = "i" },
["is_deleted"] = false
},
limit: Number(limit),
offset: Number(offset),
orderBy: { ["created_at"] = "desc" }
});
res.json(messages.rows);
});
// Global search across all user's conversations
app.get('/search', async (req, res) => {
var { q, user_id, limit = 20 } = req.query;
// Get user's conversations
var conversations = await getUserConversations(user_id as string);
var conversationIds = conversations.map((c: any) => c.id);
// Search messages
var messages = await ductape.Database.Query(new Dictionary<string, object?>
{
["table"] = "messages",
where: {
conversation_id: { $in: conversationIds },
content: { $regex: q as string, $["options"] = "i" },
["is_deleted"] = false
},
limit: Number(limit),
orderBy: { ["created_at"] = "desc" }
});
res.json(messages.rows);
});
Rate Limiting with Quotas
- TypeScript
- Java
- Go
- .NET
// Configure quotas
await ductape.quotas.configure({
'chat:send-message': {
limit: 100,
window: '1m',
per: 'user_id'
},
'chat:create-group': {
limit: 5,
window: '1h',
per: 'user_id'
},
'chat:upload-file': {
limit: 20,
window: '1h',
per: 'user_id'
}
});
// Check quota before sending message
async function checkSendMessageQuota(userId: string): Promise<boolean> {
const check = await ductape.quotas.check({
key: 'chat:send-message',
identifier: userId
});
if (!check.allowed) {
throw new Error('Rate limit exceeded. Please slow down.');
}
// Increment quota
await ductape.quotas.increment({
key: 'chat:send-message',
identifier: userId
});
return true;
}
// Configure quotas
ductape.quotas.configure(Map.of(
'chat:send-message': Map.of(
"limit", 100,
"window", "1m",
"per", "user_id"
),
'chat:create-group': Map.of(
"limit", 5,
"window", "1h",
"per", "user_id"
),
'chat:upload-file': Map.of(
"limit", 20,
"window", "1h",
"per", "user_id"
)
));
// Check quota before sending message
async function checkSendMessageQuota(userId: string): Promise<boolean> Map.of(
Map<String, Object> check = ductape.quotas.check(Map.of(
"key", "chat:send-message",
identifier: userId
));
if (!check.allowed) Map.of(
throw new Error('Rate limit exceeded. Please slow down.');
)
// Increment quota
ductape.quotas.increment(Map.of(
"key", "chat:send-message",
identifier: userId
));
return true;
)
// Configure quotas
client.quotas.configure({
'chat:send-message': {
"limit": 100,
"window": "1m",
"per": "user_id"
},
'chat:create-group': {
"limit": 5,
"window": "1h",
"per": "user_id"
},
'chat:upload-file': {
"limit": 20,
"window": "1h",
"per": "user_id"
}
});
// Check quota before sending message
async function checkSendMessageQuota(userId: string): Promise<boolean> {
check := client.quotas.check({
"key": "chat:send-message",
identifier: userId
});
if (!check.allowed) {
throw new Error('Rate limit exceeded. Please slow down.');
}
// Increment quota
client.quotas.increment({
"key": "chat:send-message",
identifier: userId
});
return true;
}
// Configure quotas
await ductape.quotas.configure({
'chat:send-message': {
["limit"] = 100,
["window"] = "1m",
["per"] = "user_id"
},
'chat:create-group': {
["limit"] = 5,
["window"] = "1h",
["per"] = "user_id"
},
'chat:upload-file': {
["limit"] = 20,
["window"] = "1h",
["per"] = "user_id"
}
});
// Check quota before sending message
async function checkSendMessageQuota(userId: string): Promise<boolean> {
var check = await ductape.quotas.check({
["key"] = "chat:send-message",
identifier: userId
});
if (!check.allowed) {
throw new Error('Rate limit exceeded. Please slow down.');
}
// Increment quota
await ductape.quotas.increment({
["key"] = "chat:send-message",
identifier: userId
});
return true;
}
Message Broker Consumers
- TypeScript
- Java
- Go
- .NET
// Subscribe to new messages for real-time delivery
await ductape.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.NEW_MESSAGE,
handler: async (message) => {
const { conversation_id, message: msg } = message;
// Broadcast to all clients in conversation room
io.to(`conversation:${conversation_id}`).emit('new-message', msg);
}
});
// Subscribe to typing events
await ductape.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.TYPING,
handler: async (message) => {
const { user_id, conversation_id, is_typing } = message;
io.to(`conversation:${conversation_id}`).emit('user-typing', {
user_id,
is_typing
});
}
});
// Subscribe to presence updates
await ductape.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.PRESENCE,
handler: async (message) => {
const { user_id, status } = message;
// Broadcast to all users who have conversations with this user
const conversations = await getUserConversations(user_id);
conversations.forEach((conv: any) => {
io.to(`conversation:${conv.id}`).emit('user-presence', {
user_id,
status
});
});
}
});
// Subscribe to read receipts
await ductape.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.READ_RECEIPT,
handler: async (message) => {
const { message_id, user_id, read_at } = message;
// Get message to find conversation
const msg = await ductape.databases.findOne({
table: 'messages',
where: { id: message_id }
});
if (msg.row) {
io.to(`conversation:${msg.row.conversation_id}`).emit('message-read', {
message_id,
user_id,
read_at
});
}
}
});
// Subscribe to new messages for real-time delivery
ductape.messageBrokers.subscribe(Map.of(
topic: MESSAGE_TOPICS.NEW_MESSAGE,
handler: async (message) => Map.of(
Map<String, Object> Map.of( conversation_id, message: msg ) = message;
// Broadcast to all clients in conversation room
io.to(`conversation:$Map.of(conversation_id)`).emit('new-message', msg);
)
));
// Subscribe to typing events
ductape.messageBrokers.subscribe(Map.of(
topic: MESSAGE_TOPICS.TYPING,
handler: async (message) => Map.of(
Map<String, Object> Map.of( user_id, conversation_id, is_typing ) = message;
io.to(`conversation:$Map.of(conversation_id)`).emit('user-typing', Map.of(
user_id,
is_typing
));
)
));
// Subscribe to presence updates
ductape.messageBrokers.subscribe(Map.of(
topic: MESSAGE_TOPICS.PRESENCE,
handler: async (message) => Map.of(
Map<String, Object> Map.of( user_id, status ) = message;
// Broadcast to all users who have conversations with this user
Map<String, Object> conversations = getUserConversations(user_id);
conversations.forEach((conv: any) => Map.of(
io.to(`conversation:$Map.of(conv.id)`).emit('user-presence', Map.of(
user_id,
status
));
));
)
));
// Subscribe to read receipts
ductape.messageBrokers.subscribe(Map.of(
topic: MESSAGE_TOPICS.READ_RECEIPT,
handler: async (message) => Map.of(
Map<String, Object> Map.of( message_id, user_id, read_at ) = message;
// Get message to find conversation
Map<String, Object> msg = ductape.databases.findOne(Map.of(
"table", "messages",
where: Map.of( id: message_id )
));
if (msg.row) Map.of(
io.to(`conversation:$Map.of(msg.row.conversation_id)`).emit('message-read', Map.of(
message_id,
user_id,
read_at
));
)
)
));
// Subscribe to new messages for real-time delivery
client.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.NEW_MESSAGE,
handler: async (message) => {
const { conversation_id, message: msg } = message;
// Broadcast to all clients in conversation room
io.to(`conversation:${conversation_id}`).emit('new-message', msg);
}
});
// Subscribe to typing events
client.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.TYPING,
handler: async (message) => {
const { user_id, conversation_id, is_typing } = message;
io.to(`conversation:${conversation_id}`).emit('user-typing', {
user_id,
is_typing
});
}
});
// Subscribe to presence updates
client.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.PRESENCE,
handler: async (message) => {
const { user_id, status } = message;
// Broadcast to all users who have conversations with this user
conversations := getUserConversations(user_id);
conversations.forEach((conv: any) => {
io.to(`conversation:${conv.id}`).emit('user-presence', {
user_id,
status
});
});
}
});
// Subscribe to read receipts
client.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.READ_RECEIPT,
handler: async (message) => {
const { message_id, user_id, read_at } = message;
// Get message to find conversation
msg := client.databases.findOne({
"table": "messages",
where: { id: message_id }
});
if (msg.row) {
io.to(`conversation:${msg.row.conversation_id}`).emit('message-read', {
message_id,
user_id,
read_at
});
}
}
});
// Subscribe to new messages for real-time delivery
await ductape.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.NEW_MESSAGE,
handler: async (message) => {
var { conversation_id, message: msg } = message;
// Broadcast to all clients in conversation room
io.to(`conversation:${conversation_id}`).emit('new-message', msg);
}
});
// Subscribe to typing events
await ductape.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.TYPING,
handler: async (message) => {
var { user_id, conversation_id, is_typing } = message;
io.to(`conversation:${conversation_id}`).emit('user-typing', {
user_id,
is_typing
});
}
});
// Subscribe to presence updates
await ductape.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.PRESENCE,
handler: async (message) => {
var { user_id, status } = message;
// Broadcast to all users who have conversations with this user
var conversations = await getUserConversations(user_id);
conversations.forEach((conv: any) => {
io.to(`conversation:${conv.id}`).emit('user-presence', {
user_id,
status
});
});
}
});
// Subscribe to read receipts
await ductape.messageBrokers.subscribe({
topic: MESSAGE_TOPICS.READ_RECEIPT,
handler: async (message) => {
var { message_id, user_id, read_at } = message;
// Get message to find conversation
var msg = await ductape.databases.findOne({
["table"] = "messages",
where: { id: message_id }
});
if (msg.row) {
io.to(`conversation:${msg.row.conversation_id}`).emit('message-read', {
message_id,
user_id,
read_at
});
}
}
});
Automated Cleanup Jobs
- TypeScript
- Java
- Go
- .NET
// Delete old messages (data retention)
await ductape.jobs.schedule({
name: 'cleanup-old-messages',
schedule: '0 2 * * *', // Every day at 2 AM
handler: async () => {
const sixMonthsAgo = new Date();
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
// Soft delete old messages
await ductape.databases.update({
table: 'messages',
where: {
created_at: { $lt: sixMonthsAgo },
is_deleted: false
},
data: {
is_deleted: true,
deleted_at: new Date(),
content: '[Message deleted]'
}
});
}
});
// Clean up offline users
await ductape.jobs.schedule({
name: 'cleanup-offline-presence',
schedule: '*/5 * * * *', // Every 5 minutes
handler: async () => {
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
await ductape.databases.update({
table: 'presence',
where: {
status: 'online',
last_activity: { $lt: fiveMinutesAgo }
},
data: {
status: 'away'
}
});
}
});
// Delete old messages (data retention)
ductape.jobs.schedule(Map.of(
"name", "cleanup-old-messages",
"schedule", "0 2 * * *", // Every day at 2 AM
handler: async () => Map.of(
Map<String, Object> sixMonthsAgo = Instant.now();
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
// Soft delete old messages
ductape.databases.update(Map.of(
"table", "messages",
where: Map.of(
created_at: Map.of( $lt: sixMonthsAgo ),
"is_deleted", false
),
data: Map.of(
"is_deleted", true,
deleted_at: Instant.now(),
"content", "[Message deleted]"
)
));
)
));
// Clean up offline users
ductape.jobs.schedule(Map.of(
"name", "cleanup-offline-presence",
"schedule", "*/5 * * * *", // Every 5 minutes
handler: async () => Map.of(
Map<String, Object> fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
ductape.databases.update(Map.of(
"table", "presence",
where: Map.of(
"status", "online",
last_activity: Map.of( $lt: fiveMinutesAgo )
),
data: Map.of(
"status", "away"
)
));
)
));
// Delete old messages (data retention)
client.jobs.schedule({
"name": "cleanup-old-messages",
"schedule": "0 2 * * *", // Every day at 2 AM
handler: async () => {
sixMonthsAgo := new Date();
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
// Soft delete old messages
client.databases.update({
"table": "messages",
where: {
created_at: { $lt: sixMonthsAgo },
"is_deleted": false
},
data: {
"is_deleted": true,
deleted_at: new Date(),
"content": "[Message deleted]"
}
});
}
});
// Clean up offline users
client.jobs.schedule({
"name": "cleanup-offline-presence",
"schedule": "*/5 * * * *", // Every 5 minutes
handler: async () => {
fiveMinutesAgo := new Date(Date.now() - 5 * 60 * 1000);
client.databases.update({
"table": "presence",
where: {
"status": "online",
last_activity: { $lt: fiveMinutesAgo }
},
data: {
"status": "away"
}
});
}
});
// Delete old messages (data retention)
await ductape.jobs.schedule({
["name"] = "cleanup-old-messages",
["schedule"] = "0 2 * * *", // Every day at 2 AM
handler: async () => {
var sixMonthsAgo = DateTime.UtcNow;
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
// Soft delete old messages
await ductape.databases.update({
["table"] = "messages",
where: {
created_at: { $lt: sixMonthsAgo },
["is_deleted"] = false
},
data: {
["is_deleted"] = true,
deleted_at: DateTime.UtcNow,
["content"] = "[Message deleted]"
}
});
}
});
// Clean up offline users
await ductape.jobs.schedule({
["name"] = "cleanup-offline-presence",
["schedule"] = "*/5 * * * *", // Every 5 minutes
handler: async () => {
var fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
await ductape.databases.update({
["table"] = "presence",
where: {
["status"] = "online",
last_activity: { $lt: fiveMinutesAgo }
},
data: {
["status"] = "away"
}
});
}
});
Helper Functions
- TypeScript
- Java
- Go
- .NET
// Get user conversations
async function getUserConversations(userId: string) {
const conversations = await ductape.databases.find({
table: 'conversations',
where: {
participant_ids: { $in: [userId] }
},
orderBy: { last_message_at: 'desc' }
});
return conversations.rows;
}
// Mark message as read
async function markMessageAsRead(messageId: string, userId: string) {
await ductape.databases.update({
table: 'message_status',
where: {
message_id: messageId,
user_id: userId
},
data: {
status: 'read',
read_at: new Date()
}
});
}
// Get conversation messages
async function getConversationMessages(
conversationId: string,
limit: number = 50,
before?: Date
) {
const where: any = {
conversation_id: conversationId,
is_deleted: false
};
if (before) {
where.created_at = { $lt: before };
}
const messages = await ductape.databases.find({
table: 'messages',
where,
limit,
orderBy: { created_at: 'desc' }
});
return messages.rows.reverse();
}
// Get user conversations
async function getUserConversations(userId: string) Map.of(
Map<String, Object> conversations = ductape.databases().query(Map<String, Object>.of(
"table", "conversations",
where: Map.of(
participant_ids: Map.of( $in: [userId] )
),
orderBy: Map.of( "last_message_at", "desc" )
));
return conversations.rows;
)
// Mark message as read
async function markMessageAsRead(messageId: string, userId: string) Map.of(
ductape.databases.update(Map.of(
"table", "message_status",
where: Map.of(
message_id: messageId,
user_id: userId
),
data: Map.of(
"status", "read",
read_at: Instant.now()
)
));
)
// Get conversation messages
async function getConversationMessages(
conversationId: string,
limit: number = 50,
before?: Date
) Map.of(
Map<String, Object> where: any = Map.of(
conversation_id: conversationId,
"is_deleted", false
);
if (before) Map.of(
where.created_at = Map.of( $lt: before );
)
Map<String, Object> messages = ductape.databases().query(Map<String, Object>.of(
"table", "messages",
where,
limit,
orderBy: Map.of( "created_at", "desc" )
));
return messages.rows.reverse();
)
import "context"
// Get user conversations
async function getUserConversations(userId: string) {
conversations := client.Databases.Query(ctx, map[string]any{
"table": "conversations",
where: {
participant_ids: { $in: [userId] }
},
orderBy: { "last_message_at": "desc" }
});
return conversations.rows;
}
// Mark message as read
async function markMessageAsRead(messageId: string, userId: string) {
client.databases.update({
"table": "message_status",
where: {
message_id: messageId,
user_id: userId
},
data: {
"status": "read",
read_at: new Date()
}
});
}
// Get conversation messages
async function getConversationMessages(
conversationId: string,
limit: number = 50,
before?: Date
) {
const where: any = {
conversation_id: conversationId,
"is_deleted": false
};
if (before) {
where.created_at = { $lt: before };
}
messages := client.Databases.Query(ctx, map[string]any{
"table": "messages",
where,
limit,
orderBy: { "created_at": "desc" }
});
return messages.rows.reverse();
}
// Get user conversations
async function getUserConversations(userId: string) {
var conversations = await ductape.Database.Query(new Dictionary<string, object?>
{
["table"] = "conversations",
where: {
participant_ids: { $in: [userId] }
},
orderBy: { ["last_message_at"] = "desc" }
});
return conversations.rows;
}
// Mark message as read
async function markMessageAsRead(messageId: string, userId: string) {
await ductape.databases.update({
["table"] = "message_status",
where: {
message_id: messageId,
user_id: userId
},
data: {
["status"] = "read",
read_at: DateTime.UtcNow
}
});
}
// Get conversation messages
async function getConversationMessages(
conversationId: string,
limit: number = 50,
before?: Date
) {
var where: any = {
conversation_id: conversationId,
["is_deleted"] = false
};
if (before) {
where.created_at = { $lt: before };
}
var messages = await ductape.Database.Query(new Dictionary<string, object?>
{
["table"] = "messages",
where,
limit,
orderBy: { ["created_at"] = "desc" }
});
return messages.rows.reverse();
}
Start Server
- TypeScript
- Java
- Go
- .NET
const PORT = process.env.PORT || 3000;
httpServer.listen(PORT, () => {
console.log(`Chat server running on port ${PORT}`);
console.log(`WebSocket server ready`);
});
Map<String, Object> PORT = System.getenv("PORT") || 3000;
httpServer.listen(PORT, () => Map.of(
System.out.println(`Chat server running on port $Map.of(PORT)`);
System.out.println(`WebSocket server ready`);
));
PORT := os.Getenv("PORT") || 3000;
httpServer.listen(PORT, () => {
fmt.Println(`Chat server running on port ${PORT}`);
fmt.Println(`WebSocket server ready`);
});
var PORT = Environment.GetEnvironmentVariable("PORT") || 3000;
httpServer.listen(PORT, () => {
Console.WriteLine(`Chat server running on port ${PORT}`);
Console.WriteLine(`WebSocket server ready`);
});
Next Steps
- Implement end-to-end encryption
- Add voice and video calling
- Create message threading
- Implement reactions and emoji support
- Add message forwarding
- Create channel broadcasting
- Set up analytics and monitoring
- Implement AI-powered moderation
- Add message translation