# Support images from messages in timeline - Tech Spec
## Problems - [SC](https://app.shortcut.com/leadflo/story/3136/support-images-from-messages-in-timeline)
Questions/Issues:
1. Inline attachments? Are we going to need to parse content to check for inline? These are unlikely to be high res and might conflict with signature images etc (untested)
After speaking to James we dont need to support direct file uploads via forms. All attachments should come with the https urls which we will need to stream to s3.
## Solution
A simple attachment timeline event will be created to display a single attachment or a group of attachments from Emails only. Image will use a thumbnail. PDF and other none images will get an icon.
Also be built with form submissions in mind even if not specifically asked for.
## API
### MimeTypes
Full common list taken from here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
MimeTypes available to leadflo:
https://docs.google.com/spreadsheets/d/1vtp5s1DKVpRzUKCccKGTJd8d2jljwaZVQvVyGWjQZSg/edit?usp=sharing
Create an ENUM table `attachment_mime` for all the available mimetype.
`mime_type: string;`
Allows for easy rejection of any mime type which we do not support. Note: We will need to be notiofied of what attachment mimetypes are attempting to be uploaded that we don't support.
### Attachment Comms Table
Create new DB table `attachments` which will be the unified table for all attachments.
Although we can grab the `patient_id` from the parent Comm/FormSub. Thinking it might be better having this on the table so in future we can direct check for patients total media count and things like that.
```
id: uuid
client_id: int
patient_id: uuid
content_type: string // foreign from ENUM table `attachment_mime`
name: string // filename
hash: string // for use with grabbing from S3
```
Create new DB table `communication_attachments` to store attachments references. Attachments will be stored in S3 with the DB storing the hash, filename, content_type (mime_type) for the attachments.
```
attachment_id: uuid // foreign
communication_id: uuid // foreign
```
`communication_id` will relate to the message the attachment came from. E.g. group of images with a comment will have the same comm id for all attachments. If no text with Attachment - Comm will still be created on where it came from e.g. Email
A comm only has attachment if the communication_id exists within table
### Attachment Form Submissions Table
Form Submissions can also have attachments. Create table `form_submissions_attachments` to handle storing attachments from forms.
```
attachment_id: uuid // foreign
form_submission_id: uuid // foreign
```
`form_submission_id` is the ID from `form_submissions` multiple attachments with the same form_submission_id can exist.
### Create Attachments Folder
Using the sub folder `services/leadflo-api/src/Attachments`
New Entity - Attachment - will be very similar to the Asset Entity `services/leadflo-api/src/Clients/Entities/Asset.php`
`fromRequest` will be used by both Comm and FormSubmissions. Comm will use base64 encoded strings from emails.
Form Subs will use HTTPS file links which we will need to GET and upload to S3
```
name: string // filename
content_type: string // mime_type
hash: string // reference to file in S3
```
Hashing the file will need to use the patientID and the attachmentID
```
private function hash(string $patientId): string
{
return substr(hash('sha256', "{$patientId}:{$this->name}"), 0, 8);
}
```
### Storing Attachments
Note: All uploading should be handled by the worker due to the data being internally buffered.
Upon the receival of an attachment. The attachment will need processing and uploading to S3 bucket.
We will use `s3-comms` bucket.
Attachments will be store in patient specific folders with a sub folder of attachments. e.g. `s3-comms/{patientId}/attachments/{fileHash}`
The storage of attachments will be handled with new `AttachmentRepository` and `AttachmentStore`. Bind to `DBS3AttachmentRepository` and `StorageAttachmentStore`. See `src/Clients/Adapters/AssetStore.php` & `src/Clients/Repositories/AssetRepository.php` for example.
Note: Memory versions will need creating for testing purposes.
`AttachmentRepository` will need to differentiate between a Comm and a FormSub using the CommId or FormSubId. Then it will populate the correct table and store in the correct folders.
Binding can be done within new `AttachmentServiceProvider`
```
$this->app->bind(AttachmentRepository::class, DBS3AttachmentRepository::class);
$this->app->bind(AttachmentStore::class, StorageAttachmentStore::class);
....
$this->app->when(StorageAttachmentStore::class)
->needs(FilesystemAdapter::class)
->give(function () {
return Storage::disk('s3-comms');
});
```
This will involve mapping any attachments from the Comms/FormSubs to the new Attachment entity and using the AttachmentRepository.
### Streaming attachments from URLs
We cant use the s3 client to stream data directly to s3. This causes the buffer to be on the CLient. Buffer needs to be on workers.
Best use GuzzleHttp within worker.
One thing to check for is the use of 'sink' with guzzle. You might be able to Directly pipe this to the S3 bucket. Untested.
Something similar to this:
```
// Create a GuzzleHttp client
$guzzleClient = new Client();
// Specify the URL from which you want to stream data
$sourceUrl = 'https://www.dental-suite.co.uk/wp-content/uploads/2022/10/banner-2.jpeg';
// Create a GET request with stream option
$request = new Request('GET', $sourceUrl, ['stream' => true]);
// Send the request and get the response
$response = $guzzleClient->send($request);
// Get the response body stream
$stream = $response->getBody();
$type = $response->getHeader('Content-Type')
// Upload the stream to S3
$result = $s3Client->putObject([
'Bucket' => $bucketName,
'Key' => $objectKey,
'Body' => $stream,
'SourceFile' => $attachment->getname(),
]);
```
```
// using the sink option with S3 - untested dont think uploadStream is correct
$guzzleClient = new Client();
// Specify the URL from which you want to stream data
$sourceUrl = 'https://www.dental-suite.co.uk/wp-content/uploads/2022/10/banner-2.jpeg';
// Specify the S3 bucket and object key for the destination
$destination = [
'Bucket' => $bucketName,
'Key' => $objectKey,
];
// Make a GET request and stream the data directly to S3 using the 'sink' option
$response = $guzzleClient->get($sourceUrl, [
'sink' => $s3Client->uploadStream($destination),
]);
// Output the response
echo $response->getStatusCode(); // Output the HTTP status code
```
Note: This doesn't get the filename.
### From Email
The existing GMail and Outlook setups will need editing to store incoming attachments. You can check for attachments using the email mime type.
Note: GMail uses MessageParts for a multi type email. Each Part will need checking for Attachments (or check docs might be easier way)
The attachments are usually base64 encoded. They will need decoding and uploading to S3. You can directly upload the decoded data.
**Gmail** - Modify getContent to search for available mimeType (from `attachment_mime` table).
```
private function getContent(string $type, Google_Service_Gmail_Message $email): string
{
if ($email->payload->mimeType === "text/{$type}" && $email->payload->body->size > 0) {
return $this->normalizeEncoding($email->payload);
}
foreach ($email->payload->parts as $part) {
$result = $this->searchPartForText($type, $part);
if (!empty($result)) {
return $result;
}
}
throw new InvalidGmailEmailException("Could not find {$type} content from email");
}
```
**Outlook** - We do already grab the 'hasAttachments' field from Outlook. Check for this then pipe to the function to get the attachments from Outlook and store. https://learn.microsoft.com/en-us/graph/api/message-list-attachments?view=graph-rest-1.0&tabs=http
### From Form Submissions
Form Entities will need editing to handle attachments.
Form Subs will use HTTPS file links which we will need to stream and upload to S3.
All forms will allow attachments by default.
`src/App/Entities/Form.php`
`src/App/Entities/FormSubmission.php`
FormSub Attachments will need hooking into the repo and mappers to allow the get to receive Attachments if they exist (AttachmentRepo).
Modify `DBFormSubmissionRepository` `DBFormSubmissionMapper` to implement AttachmentsRepo for new Assets.
### Displaying Attachments
Attachments will proxy through the API so we can ensure correct permissions. To handle this we will need to create an endpoint `GET /attachments/{hash}` using middleware `auth.new`
```
Route::group([
'prefix' => '/attachments',
'middleware' => ['auth.new'],
], function (): void {
Route::get('/{hash}', GetAttachment::class);
});
```
#### GetAttachment Controller
New GetAttachment Controller need to be scoped to a client. Note: Ensure OpenAPI spec for this.
```
class GetAttachment implements ClientScopeListener
{
use ListensForClient;
public function __construct(private AttachmentRepository $attachments)
{
}
public function __invoke(string $hash)
{
assert(!is_null($this->client), "Client is bound at this point.");
$attachment = $this->attachments->loadWithHash($hash);
if (is_null($attachment)) {
return response()->json([
'message' => 'Not found'
], 404);
}
return response()->stream(function () use ($attachment) {
echo $attachment->content;
}, 200, [
'Content-Type' => $attachment->contentType
]);
}
}
```
#### Timeline Resolver
Update the timeline resolver to include attachments URLs along with Com types. Just for Emails for now. Needs to return:
```
name: string // filename
type: string // mime type
url: string // proxied URL
```
Update email-event (OpenAPI) and email comm types to support `attachments: array|null`
### Monitoring
**All uploading workers will need to be monitored for memory usage. This will give us the data we need to optimize.**
## UI
### Types
Create new attachment type.
```
interface Attachment {
name: string // filename
type: string // mime type
url: string // proxied URL
}
```
The UI types will need updating to support `attachments: Attachment[]|null`. Do this on the Comm `types/patient.ts:34` This will allow easier support for other comm types down the line.
Also on `src/api/patients.ts` for the `TimelinePayload`
```
export interface Email extends Event {
type: 'Email';
inbound: boolean;
text_content: string;
from_email: string;
to_email: string;
subject: string;
status: string;
read_at?: string;
email_type: CommType.MarketingEmail | CommType.Email;
attachments: Attachment[]|null; // add this
}
```
Note: Might be worth splitting these up in to Comm Events and Events to make it easier to add Attachments to all Comm events. Similar to how its done here: `types/patient.ts:34`
### Attachment Components
Create new Timeline Attachments part. This part will need to match the Figma design: https://www.figma.com/file/ao2VLy9C0EJwlljK6q2v3y/Leadflo?type=design&node-id=5018%3A1285&mode=design&t=OA2sLzOGQ9jSHfAa-1
If the email snippet is empty but has attachments, the content block will be replaced with attachment block with colored background (see the PDF box at bottom of figma design). This is so the user knows the attachment was sent in an Email without content.
Proxied URLs will be passed along with data. It will work with this structure: `/attachments/{hash}`
#### Attachment Modal/Lightbox
Images will use a modal//lightbox to show larger upon click, with a download button top right of image on hover. Using the anchor `<a href="{api}/attachments/{hash}" download>` should be all thats needed to trigger a download of the attachments. See the prototype of above figma.