Skip to content

Conversation

@devin-ai-integration
Copy link

@devin-ai-integration devin-ai-integration bot commented Nov 20, 2025

Custom Message Routing System with No-Copy Projection Architecture

This PR implements a complete custom message routing system that allows messages to be projected across multiple channels without duplication, using a no-copy projection architecture.

What This PR Adds

New Features

1. RoutingRule Entity and API (src/util/entities/RoutingRule.ts, src/api/routes/routing-rules.ts)

  • New database entity for defining message routing rules
  • REST API endpoints for managing routing rules (GET, POST, PATCH, DELETE)
  • Fields: source_channel_id, storage_channel_id, sink_channel_id, source_users, target_users, valid_since, valid_until
  • Immutable by design (only valid_until can be updated while rule is in effect)
  • Validation logic ensuring rules cannot be deleted while in effect or if they have affected messages
  • Permission-based access control (requires MANAGE_ROUTING right)

2. No-Copy Projection Architecture (src/api/util/helpers/MessageProjection.ts)

  • Messages stored once in storage_channel instead of being copied to sink channels
  • Messages projected into source + sink channels at read time
  • Four core projection functions:
    • computeProjectionsForMessage(): determines all channels where a message should appear
    • isMessageVisibleInChannelForUser(): validates projection visibility with target_users filtering
    • resolveMessageInChannel(): fetches and validates messages in a specific channel context
    • getProjectedMessagesForChannel(): queries all projected messages for a channel with proper filtering

3. Message Entity Updates (src/util/entities/Message.ts)

  • Added source_channel_id field to track where messages were originally posted
  • Added toProjectedJSON(channelId) method to override channel_id in API responses
  • Database migrations for postgres, mysql, mariadb

4. Updated Message Handling (src/api/util/handlers/Message.ts)

  • sendMessage() now evaluates routing rules using message ID timestamps (not wall-clock time)
  • Messages stored in storage_channel with source_channel_id set to original channel
  • MESSAGE_CREATE events emitted to all projection channels (source + sinks)
  • Identity routing by default when no custom route is defined (source = storage = sink)

5. Updated Message Endpoints

  • All message operations updated to use projection validation:
    • GET/PATCH/DELETE /channels/:id/messages/:id use resolveMessageInChannel()
    • GET /channels/:id/messages uses getProjectedMessagesForChannel()
    • Reactions, pins, replies, threads endpoints all use projection validation
  • All message events (UPDATE, DELETE, REACTION_ADD, REACTION_REMOVE, PINS_UPDATE) emit to all projection channels

Updates Since Last Revision: Simulated Intimacy Broadcast DM

New Feature: Intimacy Broadcast Mode (similar to OnlyFans-style DM broadcasts)

When a routing rule has target_role_id set with target_role_guild_id = null, the system treats this as a "simulated intimacy broadcast DM":

  • Storage channel ignored: Messages are stored in the source channel directly instead of a separate storage channel
  • Reaction privacy: Recipients (excluding the original sender) cannot see other recipients' reactions or reaction counts. Each recipient only sees their own reactions, while the message author sees all reactions.

New additions:

  • target_role_id and target_role_guild_id fields on RoutingRule entity
  • isIntimacyBroadcast() method to detect intimacy broadcast mode
  • filterReactionsForIntimacyBroadcast() function in MessageProjection.ts
  • getIntimacyBroadcastRuleForChannel() helper function
  • Updated GET /channels/:id/messages and GET /channels/:id/messages/:id/reactions/:emoji endpoints to filter reactions for intimacy broadcast

Architecture Benefits

No Message Duplication: Messages are stored once in storage_channel, eliminating data duplication and ensuring consistency.

Same ID Across Projections: Messages keep the same ID in all channels where they appear, fixing reactions/replies/references that would break with copied messages.

Target Users Filtering: The target_users field is enforced at read/dispatch time, allowing fine-grained control over who can see routed messages in sink channels.

Route Immutability: Routes are immutable (except valid_until) because we need them to trace back which storage_channel a message belongs to. Without immutability, we'd need to move messages in bulk when routes change.

Timestamp-Based Evaluation: Routing rules are evaluated using message timestamps (from Snowflake IDs), not current time, ensuring consistent routing regardless of when queries are executed.

Database Changes

  • Added source_channel_id column to messages table (nullable, indexed)
  • Added routing_rules table with all routing rule fields (including new target_role_id and target_role_guild_id)
  • Created migrations for postgres, mysql, mariadb (sqlite uses DB_SYNC)
  • Legacy messages (without source_channel_id) are handled via fallback logic

Summary

This PR extends the custom message routing system to support a new "intimacy broadcast" mode where recipients cannot see each other's reactions, simulating private DM-like interactions in a broadcast context.

Review & Testing Checklist for Human

⚠️ HIGH RISK CHANGES - This is a fundamental architectural change affecting core message functionality.

  • Test intimacy broadcast reaction filtering: Create a routing rule with target_role_id set and target_role_guild_id = null, add reactions from multiple users, and verify each recipient only sees their own reactions while the author sees all
  • Verify author sees all reactions: Confirm the message author can see all reactions and counts in intimacy broadcast mode
  • Test message routing end-to-end: Create a routing rule and verify messages appear in both source and sink channels with the same ID
  • Test reactions on routed messages: Add reactions to a message visible in multiple channels and verify they sync correctly (non-intimacy broadcast mode)
  • Test legacy message compatibility: Verify old messages (without source_channel_id) still display and work correctly

Recommended Test Plan:

  1. Create a DM channel and set up a routing rule with target_role_id pointing to a role and target_role_guild_id = null
  2. Send a message as user A
  3. Have users B and C (recipients) add different reactions
  4. Verify user B only sees their own reaction, user C only sees their own reaction, and user A (author) sees all reactions
  5. Test the GET messages endpoint and GET reactions endpoint both filter correctly

Notes

  • Build passes for all changes (pre-existing errors in unrelated files like discord-protos imports are not from this PR)
  • Lint checks passed (pre-commit hooks ran automatically)
  • No functional testing was performed - this PR requires thorough manual testing before merge
  • The reaction filtering logic in filterReactionsForIntimacyBroadcast() returns null for reactions where the viewing user hasn't reacted, which are then filtered out

Link to Devin runs:

Requested by: Erkin Alp Güney (erkinalp9035@gmail.com) / @erkinalp

@devin-ai-integration
Copy link
Author

Original prompt from Erkin
# Implement Custom Message Routing System

## Overview
Implement a custom message routing system for the Spacebar backend that allows messages to be automatically forwarded between channels based on configurable rules with proper permission controls.

## Background Context

The codebase already has some foundation for this feature:

1. **Permission System**: The `MANAGE_ROUTING` right is already defined in `anticensor://src/util/util/Rights.ts` at line 43 as `BitFlag(5)`. The permission checking logic exists in the `has()` method (line 98) and route middleware enforces rights at `anticensor://src/api/util/handlers/route.ts` (line 96).

2. **Message Types**: Routing message types are already defined in `anticensor://src/schemas/api/messages/Message.ts` at line 42:
   - `ROUTE_ADDED = 41` for new route notifications
   - `ROUTE_DISABLED = 42` for disabled route notifications

3. **Message Processing**: The main message handling occurs in `anticensor://src/api/util/handlers/Message.ts`. There's a TODO comment at line 208 that says "code below has to be redone when we add custom message routing" - this is where routing logic needs to be integrated.

4. **Database Structure**: The system uses TypeORM with a BaseClass pattern (`anticensor://src/util/entities/BaseClass.ts` line 71) that provides Snowflake ID generation. Entities are auto-loaded from the entities directory (configured in `anticensor://src/util/util/Database.ts` line 68).

5. **Channel Permissions**: Permission validation for channels is handled by `Permissions.finalPermission()` in `anticensor://src/util/util/Permissions.ts` (line 131) and `Channel.getUserPermissions()` in `anticensor://src/util/entities/Channel.ts` (line 440).

## Task Requirements

### 1. Create Routing Rule Entity

Create a new entity file `anticensor://src/util/entities/RoutingRule.ts` that extends BaseClass with the following structure:

**Eight required fields:**
- `registration_timestamp` (string, stored as snowflake) - when the rule... (4576 chars truncated...)

@devin-ai-integration
Copy link
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@erkinalp
Copy link
Owner

@samuelscheit review appreciated

Repository owner deleted a comment from devin-ai-integration bot Nov 20, 2025
@devin-ai-integration devin-ai-integration bot changed the title feat: implement custom message routing system Rewrite message routing to use no-copy projection architecture Nov 20, 2025
@erkinalp erkinalp changed the title Rewrite message routing to use no-copy projection architecture feat: implement custom message routing system Nov 20, 2025
@erkinalp erkinalp linked an issue Nov 21, 2025 that may be closed by this pull request
devin-ai-integration bot and others added 4 commits January 21, 2026 06:34
- Add RoutingRule entity with 8 required fields and channel relationships
- Add MANAGE_ROUTING permission flag (BitFlag 39) for guild-level checks
- Implement routing rule validation (invariants, permission checks)
- Implement routing rule deletion validation logic
- Integrate routing logic into message processing with:
  - Deduplication per sink channel using Map
  - Idempotency using nonce to prevent duplicates across workers
  - Prevention of re-routing already routed messages
  - DB-side time-window filtering for performance
- Create API endpoints for routing rule management:
  - POST /routing-rules (create with MANAGE_ROUTING right)
  - GET /routing-rules (list filtered by permission)
  - PATCH /routing-rules/:id (update valid_until only)
  - DELETE /routing-rules/:id (delete with validation)

Signed-off-by: Devin AI <devin@cognition.ai>
For: Erkin Alp Güney <erkinalp9035@gmail.com>
Co-Authored-By: Erkin Alp Güney <erkinalp9035@gmail.com>
…alidity check

The validity check (valid_since and valid_until) should apply to when the
message was sent, not when the routing rules are being processed. This fix
uses message.id (which is a Snowflake containing the send timestamp) instead
of generating a new Snowflake with the current time.

This ensures routing rules are evaluated based on when the message was
originally sent, which is the correct behavior for time-based routing.

Signed-off-by: Devin AI <devin@cognition.ai>
For: Erkin Alp Güney <erkinalp9035@gmail.com>
Co-Authored-By: Erkin Alp Güney <erkinalp9035@gmail.com>
- Store messages once in storage_channel instead of copying to sink channels
- Add source_channel_id field to Message entity to track original posted channel
- Implement projection helpers: computeProjectionsForMessage, isMessageVisibleInChannelForUser, resolveMessageInChannel, getProjectedMessagesForChannel
- Update sendMessage to evaluate routing rules and store in storage_channel with source_channel_id set
- Emit MESSAGE_CREATE to all projection channels (source + sinks) with proper channel_id override
- Update all message endpoints to use resolveMessageInChannel for projection validation
- Update GET /channels/:id/messages to use projection query logic
- Update message operations (edit, delete, reactions, pins, replies, threads) to emit events to all projections
- Add database migrations for source_channel_id field (postgres, mysql, mariadb)
- Messages now keep same ID across all projections (fixes reactions/replies/references)
- Implement target_users filtering at read/dispatch time as required by architecture

Co-Authored-By: Erkin Alp Güney <erkinalp9035@gmail.com>
- Add VIEW_CHANNEL permission enforcement in isMessageVisibleInChannelForUser()
- Sanitize routing rule relations for non-admin users in GET /routing-rules
- Fix RoutingRule.canDelete() to check storage_channel instead of sink_channel
- Prevent information leakage of channels users cannot view

Security improvements:
- Users can only see projected messages in channels they have VIEW_CHANNEL permission for
- Non-admin users cannot see storage/sink channel details for channels they lack permissions on
- Rule deletion now correctly checks for affected messages in the projection architecture

Co-Authored-By: Erkin Alp Güney <erkinalp9035@gmail.com>
@erkinalp erkinalp force-pushed the devin/1763583149-custom-message-routing branch from 7a011e1 to 6c02b09 Compare January 21, 2026 03:49
@devin-ai-integration
Copy link
Author

Devin is currently unreachable - the session may have died.

@openhands-ai
Copy link

openhands-ai bot commented Jan 21, 2026

Looks like there are a few issues preventing this PR from being merged!

  • GitHub Actions are failing:
    • Nix build
    • Nix build
    • Nix build

If you'd like me to help, just leave a comment, like

@OpenHands please fix the failing actions on PR #128 at branch `devin/1763583149-custom-message-routing`

Feel free to include any additional details that might help me get this PR into a better state.

You can manage your notification settings

- Add target_role_id and target_role_guild_id fields to RoutingRule
- When target_role_id is set with null guild, treat as intimacy broadcast
- In intimacy broadcast mode, ignore storage_channel (store in source)
- Filter reactions so recipients only see their own reactions (sender sees all)
- Update reactions endpoint to filter reactions for intimacy broadcast
- Update messages endpoint to filter reactions for intimacy broadcast

Co-Authored-By: Erkin Alp Güney <erkinalp9035@gmail.com>
@devin-ai-integration devin-ai-integration bot changed the title feat: implement custom message routing system feat: implement custom message routing system with intimacy broadcast Jan 21, 2026
@erkinalp erkinalp changed the title feat: implement custom message routing system with intimacy broadcast feat: implement custom message routing system Jan 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custom message routing

1 participant