Questions
- Should entities be allowed to have more than one associated message thread?
- A: Yes. A "thread" is just a linked list of messages, linked by their inReplyTo reference, which is optional. The ID of the thread is just the ID of the first message in the list, which has an empty 'inReplyTo' field. So if more than one message is 'sent to' an entity (as part of a discussion forum) with an empty inReplyTo field, then there is more than one thread for the entity.
- Or message threads to have more than one associated entity?
- Should we allow messages to be sent to multiple non-principals at once?
- Should messages with low numbers of recipients be processed in an immediately consistent manner?
- Sending messages to non-principals (i.e. commenting on an entity) does not need asynchronous processing
- Sending messages to a single recipient will be done transactionally
- What ACCESS_TYPE should be associated with the ability to comment on an entity?
- SEND_MESSAGE
- What ACCESS_TYPE should be associated with the ability to message a user?
- No restriction
- Add a blacklist
- Flag as inappropriate
- Should the worker use SQS or RDS to manage the flow of messages to send?
- Proposed RDS implementation:
- Add a migratable table with a single column of message IDs
- Add a worker that periodically polls the table
- If the table is not empty, process N rows from the top
- Delete rows once finished processing
- Using SQS opens up the possibility of losing messages (especially during weekend stack switching). The state stored in SQS would not be migrated between stacks. And unlike change messages, there is no trivial method to detect if a message has been changed. And reusing the change messages is not possible, since retransmission of messages is incorrect behavior.
- Proposed RDS implementation:
- How should we bounce messages? Silently? Via an auto-generated message?
- Add services to check if message can be sent. This way, the UI can check if a message can be sent before sending it.
- On an error, send an error message to the sender's inbox.
- How should we handle messages sent to groups rather than individuals? Should the message be broken into individuals messages (after checking for SEND_MESSAGE permission on the group)? Currently, it simply stashes the message, leaving no way of fetching the message (other than as a sender).
When we send emails to users, should we download the body of the email and send it in the email body? Or should we tell them to visit the portal?
- At the moment, the body of the email == the body of the message. And files are assumed to be stored as plain text.
Should notifications of this nature be implemented now or later?Jira Legacy server JIRA (sagebionetworks.jira.com) columns key,summary,type,created,updated,due,assignee,reporter,priority,status,resolution serverId ba6fb084-9827-3160-8067-8ac7470f78b2 key PLFM-1039 - Also, if the affected entity has child entities, should those be included in the notification?
- How should we tie Entities to users in terms of notifications?
- Ownership? << It might be this.
- Favorites? << It might be this.
- Permissions? << I don't think it's this.
- Something else? << It could be a completely new set of services, allowing users to enable/disable notification. (Could be initialized from Favorites and Owners.)
Objects
Name | DBO | Migration | DTO | |
---|---|---|---|---|
(Immutable after creation) |
Etag
CreatedOn CreatedBy (FK to JDOUSERGROUP) InReplyTo (FK to ID column) (nullable) backUpID is
| Backup via ID Note: Etag is required because MessageStatus is mutable interface (superclass represented as JSON schema) ID (derived from ID generator). | Interface
| |
(Immutable after creation)MessageContentID (FK to |
| Secondary to MessageContent backUpID is MessageContentID | implements Implements MessageContent
| |
(Immutable after creation) FK to MessageContent FK to JDOUSERGOUP |
| Secondary to MessageContent backUpID is MessageContentID | n/a | |
|
| Secondary to MessageContent backUpID is MessageContentID |
| |
(Immutable after creation)MessageContentID (FK to |
| Secondary to MessageContent backUpID is MessageContentIDimplements | Implements MessageContent | |
MessageInReplyToRoot (Immutable after creation) |
| Secondary to MessageContent backUpID is MessageContentID | n/a | |
| ||||
| Part of UserProfile Use existing services to support this | Secondary to UserGroup |
| |
|
| TBD |
| |
|
|
| ||
| Bundles the Message and Message Status | |||
|
|
Other
- Message ID generator
- Thread ID generator
- Email mechanism
- Use Amazon SES
- Since we've decided to use SES, we should use their REST API rather than their SMTP API.
- The Gmail server may be useful for handling bounced messages.
- This will make the following two mocking methods impossible:
- Mocking javax mail: http://ollivander.franzoni.eu/2011/08/30/mock-javamail-primer/
- Mocking an SMTP server: http://quintanasoft.com/dumbster/
- Use Amazon SES
- Message delivery will be via worker, i.e. asynchronous (in most cases) with respect to message creation.
...
Services
- Sort by recipient
Method | URI | underlying query (if any)
| Bundles the MessageToUser and Message Status | ||
---|---|---|---|---|---|
|
|
'* per Marcel, Comments may have a more complex set of references/relationships than Messages to Users have. Rather than just 'inReplyTo', it might be that one Comment is an 'answer' to another comment which is a question. Further, the multiple answers to a question may have 'previous' references which order them.
Other TODOs
- Throttle message input
- Max 10 creations per minute
- Max 50 recipients
- When adding a new person to a conversation, the new person should get the whole history of the conversation
- Might be implemented by forwarding ALL the previous messages
- OR by concatenating all previous into one new message
Configure Amazon SES
- Automated proposal:
- Reroute bounce and complaints to SNS
- Have a worker subscribe to the topic via SQS
- Have a worker disable emails to hard-bounced recipients
- The worker may email a Google Group to notify us of anything it cannot handle
- Also flip flags in settings for complaints
- Store the bounce/complaint in a blob
- Automated proposal:
- Add methods to send messages according to templates
- Each individual Message should be stored in its final form.
- This design choice would change for very very large notification message volumes.
- Templates:
- Password reset
- Welcome
- Delivery failure
- Entity shared
- Entity changed
- TBD
...
Message sender (worker)
- Extend ObjectType to include a "MESSAGE" type
- Have DBOMessage extend ObservableEntity
- Send a change message when a message is created
- Subscribe worker to the repo-changes topic
- Look for MESSAGE-CREATED changes to process
- The worker should always check to see if a message has already been sent, so that messages do not get resent every time the stack changes.
- Whenever the message sending worker encounters an error:
- Send the sender of the message a notification with the summary of delivery failures
- Add CloudWatch metrics to count delivery failures
- If the message cannot be sent to anyone, mark the sender as the receiver of the message so that it doesn't get reprocessed (i.e. after migration)
- Process change of other ObjectTypes (i.e. ENTITY) for notifications
- Needs additional state: either a link between ObjectID/Type and messageContent or ObjectID/Type and UserID (to prevent messages from being resent everytime the stack blows away the change messages)
...
Services
- Sorting & Pagination
- Sort by creation date
- Sort by subject
- Sort by recipient name
...
Method | URI | Body | Parameters | Return | Description | Permission | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| /message | /inboxselect * from messagecontent c, messagetouser m, messagestatus s where c.ID=m.messageContentID and s.messageContentID=c.ID and s.recipientID=<pid> | Sorting + pagination | Paginated results<MessageBundle> | Gets all messages the authenticated user has received | Authenticated User | ||||||||||||||
| /message/outbox | select * from messagecontent c, messagetouser m where c.ID=m.messageContentID and c.createdBy=<pid> TODO: what is the best way to populate the recipients? | Sorting + pagination | Paginated results<Message> | Gets all messages the authenticated user has sent | Authenticated User | ||||||||||||||
| /message | n/a | Message | inReplyTo=null (i.e. another Message) | Message | MessageToUser | MessageToUser | Sends a message. Note, message delivery permission is on a recipient-by-recipient basis, asynchronous to the message creation. Unauthorized delivery may result in silent failure or a bounce message (TBD). | Authenticated User
Must be admin to send to AUTH_USERS | Must have SEND_MESSAGE permission on team to send to team | ||||||||||
| /message/check | use existing userGroupDAO query to check recipients. | Message | inReplyTo=null (i.e. another Message) | Boolean ErrorResponse | Checks to see if the message is correctly formatted and that the sender has permission to send to the indicated recipients | Authenticated Userinbox | Sorting + pagination | Paginated results<MessageBundle> | Gets all messages the authenticated user has received | Authenticated User | |||||||||
| /message/outbox | Sorting + pagination | Paginated results<MessageToUser> | Gets all messages the authenticated user has sent | Authenticated User | |||||||||||||||
| /message/{id} | MessageToUser | Gets a specific message | Sender or Receiver | ||||||||||||||||
| /message/{id}/forward | n/aMessageRecipientSet | RecipientBundle | MessageToUser | Forwards a message to other recipients. This is equivalent to getting a (visible) message and POST-ing it to /message | Sender or Receiver | ||||||||||||||
| /entity/{id}/comments | n/a | Message | Message | Convenience method for commenting on an entity. The service fills out fields like message ID, thread ID, and recipients, leaving just subject and body for the user. | Authenticated user with SEND_MESSAGE permission on entity | ||||||||||||||
| /message/{id} | select * from messagecontent c, messagetouser m where c.ID=m.messageContentID and c.ID=<id> | Message | Gets a specific message | with a different set of recipients | Sender or Receiver | ||||||||||||||
| /message/{id}/conversation | TODO | Sorting + pagination | Paginated results< | MessageMessageToUser> | Gets messages belonging in the same thread as the message ID. The list is filtered according to the user's ID. | Sender or Receiver | |||||||||||||
| /entitymessage/{id}/conversation/comments | select * from messagecontent mc, comment c, inreploytoroot r where mc.ID=c.messageContentID and c.TargetType='ENTITY" and c.TargetID=<id> | Sorting + pagination | Paginated results<Message> | Gets message belonging to the thread tied to the entity. | Authenticated user with READ permission on entity |
| /message/{id}/status | select * from messagestatus where messagstatus.messageContentID=<id> | Message Status | Gets the status of a message | forward | MessageRecipientSet | Adds the set of recipients to the conversation by marking each recipient as a receiver of each message within the conversation. Should each recipient be sent the messages (i.e. via email)? Or should a notification be sent instead? | Sender or Receiver | |||||
| /message/status | n/a | MessageStatus | Marks a message as:
| Receiver | |||||||||||||||
| /message/{id}/settingsfile | TODO | MessageSettings | Gets the notification settings of the user | Authenticated User | |||||||||||||||
| /message/settings | TODO | MessageSettings | Changes notification settings | Authenticated User |
'* 'replyTo' is a misnomer. In email 'replyTo' is the address of the person to whom you send an email reply. In this case it's the ID of the message referenced by the current message. So it should be called 'inReplyTo' or some other distinctive name.
(Redirect) | Checks to see if the user can read the message and then redirects to the actual URL of the file associated with the message | Sender or Receiver | ||||||||||||||
| /entity/{id}/message | MessageToUser | MessageToUser | Sends a message to the creator or administrator of the given entity. This allows users to ask for permission to see restricted entities. Added per
| Authenticated user | |||||||||||
| /entity/{id}/comment | Comment | Comment | Method for commenting on an entity. | Authenticated user with SEND_MESSAGE permission on entity | |||||||||||
| /entity/{id}/comment | Sorting + pagination | Paginated results<Comment> | Gets messages belonging to the thread tied to the entity. | Authenticated user with READ permission on entity |