fix: attachment issues wip

This commit is contained in:
abdou6666 2025-04-09 16:00:02 +01:00
parent 208ddd411f
commit 33cc3f713d
14 changed files with 229 additions and 42 deletions

BIN
api/assets/hexavatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -133,6 +133,7 @@ export class AttachmentController extends BaseController<Attachment> {
access = AttachmentAccess.Public,
}: AttachmentContextParamDto,
): Promise<Attachment[]> {
debugger;
if (!files || !Array.isArray(files?.file) || files.file.length === 0) {
throw new BadRequestException('No file was selected');
}

View File

@ -6,27 +6,74 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import fs from 'fs';
import path from 'path';
import { Readable, Stream } from 'stream';
import { Injectable, Optional, StreamableFile } from '@nestjs/common';
import {
Injectable,
OnApplicationBootstrap,
Optional,
StreamableFile,
} from '@nestjs/common';
import { HelperService } from '@/helper/helper.service';
import { HelperType } from '@/helper/types';
import { SettingService } from '@/setting/services/setting.service';
import { BaseService } from '@/utils/generics/base-service';
import { AttachmentMetadataDto } from '../dto/attachment.dto';
import { AttachmentRepository } from '../repositories/attachment.repository';
import { Attachment } from '../schemas/attachment.schema';
import {
AttachmentAccess,
AttachmentCreatedByRef,
AttachmentResourceRef,
} from '../types';
@Injectable()
export class AttachmentService extends BaseService<Attachment> {
export class AttachmentService
extends BaseService<Attachment>
implements OnApplicationBootstrap
{
constructor(
readonly repository: AttachmentRepository,
@Optional() private readonly helperService: HelperService,
private settingService: SettingService,
) {
super(repository);
}
async onApplicationBootstrap() {
debugger;
const defaultAttachment = await this.repository.findOne({
createdByRef: AttachmentCreatedByRef.System,
});
if (!defaultAttachment) {
const imagePath = path.join(process.cwd(), 'assets', 'hexavatar.png');
// console.log({ imagePath });
const imageBuffer = fs.readFileSync(imagePath);
const result = await this.store(imageBuffer, {
access: AttachmentAccess.Public,
name: 'hexavatar.png',
createdBy: 'system',
size: imageBuffer.length,
type: 'png',
createdByRef: AttachmentCreatedByRef.System,
resourceRef: AttachmentResourceRef.SettingAttachment,
channel: {
'web-channel': '',
'console-channel': '',
'discord-channel': '',
'whatsapp-channel': '',
},
});
console.log({ result });
console.log({ result });
}
//
}
/**
* Stores a file using the default storage helper and creates an attachment record.
*

View File

@ -15,6 +15,7 @@ import { Readable, Stream } from 'stream';
export enum AttachmentCreatedByRef {
User = 'User',
Subscriber = 'Subscriber',
System = 'System',
}
/**

View File

@ -308,21 +308,20 @@ export default abstract class ChannelHandler<
* @return A signed URL string for downloading the specified attachment.
*/
public async getPublicUrl(attachment: AttachmentRef | Attachment) {
const [name, _suffix] = this.getName().split('-');
if ('id' in attachment) {
if (!attachment.id) {
throw new TypeError(
'Attachment ID is empty, unable to generate public URL.',
);
if (!attachment || !attachment.id) {
return buildURL(config.apiBaseUrl, `/webhook/${name}/not-found`);
}
const resource = await this.attachmentService.findOne(attachment.id);
if (!resource) {
throw new NotFoundException('Unable to find attachment');
this.logger.warn('Unable to find attachment sending fallback image');
return buildURL(config.apiBaseUrl, `/webhook/${name}/not-found`);
}
const token = this.jwtService.sign({ ...resource }, this.jwtSignOptions);
const [name, _suffix] = this.getName().split('-');
return buildURL(
config.apiBaseUrl,
`/webhook/${name}/download/${resource.name}?t=${encodeURIComponent(token)}`,
@ -331,7 +330,9 @@ export default abstract class ChannelHandler<
// In case the url is external
return attachment.url;
} else {
throw new TypeError('Unable to resolve the attachment public URL.');
return buildURL(config.apiBaseUrl, `/webhook/${name}/not-found`);
// throw new TypeError('Unable to resolve the attachment public URL.');
}
}

View File

@ -88,4 +88,10 @@ export class WebhookController {
this.logger.log('Channel notification : ', req.method, channel);
return await this.channelService.handle(channel, req, res);
}
@Roles('public')
@Get(':channel/not-found')
async handleNotFound(@Res() res: Response) {
return res.status(404).send({ error: 'Not found!' });
}
}

View File

@ -47,6 +47,25 @@ export class SettingService extends BaseService<Setting> {
if (count === 0) {
await this.seeder.seed(data);
}
// const imagePath = path.join(process.cwd(), 'assets', 'hexavatar.png');
// const imageBuffer = fs.readFileSync(imagePath);
// await this.attachmentService.store(imageBuffer, {
// access: AttachmentAccess.Public,
// name: 'hexavatar.png',
// createdBy: 'system',
// size: imageBuffer.length,
// type: 'png',
// createdByRef: AttachmentCreatedByRef.System,
// resourceRef: AttachmentResourceRef.SettingAttachment,
// channel: {
// 'web-channel': '',
// 'console-channel': '',
// 'discord-channel': '',
// 'whatsapp-channel': '',
// },
// });
}
/**

View File

@ -116,7 +116,11 @@
"select_category": "Select a flow",
"logout_failed": "Something went wrong during logout",
"duplicate_labels_not_allowed": "Duplicate labels are not allowed",
"duplicate_block_error": "Something went wrong while duplicating block"
"duplicate_block_error": "Something went wrong while duplicating block",
"image_error": "Image not found",
"file_error": "File not found",
"audio_error": "Audio not found",
"video_error": "Video not found"
},
"menu": {
"terms": "Terms of Use",

View File

@ -116,7 +116,11 @@
"select_category": "Sélectionner une catégorie",
"logout_failed": "Une erreur s'est produite lors de la déconnexion",
"duplicate_labels_not_allowed": "Les étiquettes en double ne sont pas autorisées",
"duplicate_block_error": "Une erreur est survenue lors de la duplication du bloc"
"duplicate_block_error": "Une erreur est survenue lors de la duplication du bloc",
"image_error": "Image introuvable",
"file_error": "Fichier introuvable",
"audio_error": "Audio introuvable",
"video_error": "Vidéo introuvable"
},
"menu": {
"terms": "Conditions d'utilisation",

View File

@ -8,7 +8,7 @@
import DownloadIcon from "@mui/icons-material/Download";
import { Box, Button, Typography } from "@mui/material";
import { FC } from "react";
import { FC, useState } from "react";
import { useDialogs } from "@/hooks/useDialogs";
import { useGetAttachmentMetadata } from "@/hooks/useGetAttachmentMetadata";
@ -30,11 +30,17 @@ interface AttachmentInterface {
const componentMap: { [key in FileType]: FC<AttachmentInterface> } = {
[FileType.image]: ({ url }: AttachmentInterface) => {
const dialogs = useDialogs();
const [imageErrored, setImageErrored] = useState(false);
const { t } = useTranslate();
if (imageErrored) {
return <p>{t("message.image_error")}</p>;
}
if (url)
return (
// eslint-disable-next-line @next/next/no-img-element
<img
onError={() => setImageErrored(true)}
width="auto"
height={200}
style={{ objectFit: "contain", cursor: "pointer" }}
@ -59,11 +65,24 @@ const componentMap: { [key in FileType]: FC<AttachmentInterface> } = {
);
},
[FileType.audio]: (props: AttachmentInterface) => {
return <audio controls src={props.url} />;
const [audioErrored, setAudioErrored] = useState(false);
const { t } = useTranslate();
if (audioErrored) {
return <p>{t("message.video_error")}</p>;
}
return (
<audio controls src={props.url} onError={() => setAudioErrored(true)} />
);
},
[FileType.file]: (props: AttachmentInterface) => {
const { t } = useTranslate();
if (!props.url) {
return <p>{t("message.file_error")}</p>;
}
return (
<Box>
<Typography
@ -84,11 +103,20 @@ const componentMap: { [key in FileType]: FC<AttachmentInterface> } = {
</Box>
);
},
[FileType.video]: ({ url }: AttachmentInterface) => (
<video controls width="250">
<source src={url} />
</video>
),
[FileType.video]: ({ url }: AttachmentInterface) => {
const [videoErrored, setVideoErrored] = useState(false);
const { t } = useTranslate();
if (videoErrored) {
return <p>{t("message.video_error")}</p>;
}
return (
<video controls width="250">
<source src={url} onError={() => setVideoErrored(true)} />
</video>
);
},
[FileType.unknown]: ({ url }: AttachmentInterface) => <>Unknown Type:{url}</>,
};

View File

@ -10,6 +10,15 @@
video {
max-width: 100%;
}
.error-message {
margin: 0;
// padding: 10px 0 10px 10px;
padding: 10px;
border-radius: 5px;
color: #666;
width: 100%;
display: block;
}
}
.sc-message--content.sent .sc-message--file {
@ -64,3 +73,13 @@
.sc-message--content.received .sc-message--file a:hover {
color: #0c0c0c;
}
// .error-message {
// margin: 0;
// padding: 10px 0;
// text-align: center;
// color: #666;
// font-style: italic;
// width: 100%;
// display: block;
// }

View File

@ -6,13 +6,12 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import React from "react";
import React, { useState } from "react";
import { useTranslation } from "../../hooks/useTranslation";
import { useColors } from "../../providers/ColorProvider";
import { Direction, TMessage } from "../../types/message.types";
import FileIcon from "../icons/FileIcon";
import "./FileMessage.scss";
interface FileMessageProps {
@ -23,11 +22,17 @@ const FileMessage: React.FC<FileMessageProps> = ({ message }) => {
const { t } = useTranslation();
const { colors: allColors } = useColors();
const colors = allColors[message.direction || Direction.received];
const [videoErrored, setVideoErrored] = useState(false);
const [audioErrored, setAudioErrored] = useState(false);
const [fileErrored, setFileErrored] = useState(false);
const [imageErrored, setImageErrored] = useState(false);
if (!("type" in message.data)) {
throw new Error("Unable to detect type for file message");
}
if (message.data.type === "unknown") {
return <p className="error-message">unknown file type</p>;
}
if (
message.data &&
message.data.type !== "image" &&
@ -35,7 +40,7 @@ const FileMessage: React.FC<FileMessageProps> = ({ message }) => {
message.data.type !== "video" &&
message.data.type !== "file"
) {
throw new Error("Uknown type for file message");
throw new Error("Unknown type for file message");
}
return (
@ -48,23 +53,60 @@ const FileMessage: React.FC<FileMessageProps> = ({ message }) => {
>
{message.data.type === "image" && (
<div className="sc-message--file-icon">
<img src={message.data.url || ""} className="sc-image" alt="File" />
{imageErrored ? (
<p
className="error-message"
style={{
backgroundColor: colors.bg,
}}
>
{t("messages.file_message.image_error")}
</p>
) : (
<img
onError={() => setImageErrored(true)}
src={message?.data?.url}
className="sc-image"
alt="File"
/>
)}
</div>
)}
{message.data.type === "audio" && (
<div className="sc-message--file-audio">
<audio controls>
<source src={message.data.url} />
{t("messages.file_message.browser_audio_unsupport")}
</audio>
{audioErrored ? (
<p
className="error-message"
style={{
backgroundColor: colors.bg,
}}
>
{t("messages.file_message.audio_error")}
</p>
) : (
<audio controls onError={() => setAudioErrored(true)}>
<source src={message.data.url} />
</audio>
)}
</div>
)}
{message.data.type === "video" && (
<div className="sc-message--file-video">
<video controls width="100%">
<source src={message.data.url} />
{t("messages.file_message.browser_video_unsupport")}
</video>
{videoErrored ? (
<p
className="error-message"
style={{
backgroundColor: colors.bg,
}}
>
{t("messages.file_message.video_error")}
</p>
) : (
<video controls width="100%" onError={() => setVideoErrored(true)}>
<source src={message.data.url} />
{t("messages.file_message.browser_video_unsupport")}
</video>
)}
</div>
)}
{message.data.type === "file" && (
@ -75,15 +117,22 @@ const FileMessage: React.FC<FileMessageProps> = ({ message }) => {
backgroundColor: colors.bg,
}}
>
<a
href={message.data.url ? message.data.url : "#"}
target="_blank"
rel="noopener noreferrer"
download
>
<FileIcon />
{t("messages.file_message.download")}
</a>
{!message?.data?.url ||
message?.data?.url?.includes("webhook/download/not-found") ? (
<p className="error-message" style={{ padding: 0 }}>
{t("messages.file_message.file_error")}
</p>
) : (
<a
href={message.data.url ? message.data.url : "#"}
target="_blank"
rel="noopener noreferrer"
download
>
<FileIcon />
{t("messages.file_message.download")}
</a>
)}
</div>
)}
</div>

View File

@ -17,7 +17,11 @@
"browser_video_unsupport": "Browser does not support the video element.",
"download": "Download",
"unsupported_file_type": "This file type is not supported.",
"unsupported_file_size": "This file size is not supported."
"unsupported_file_size": "This file size is not supported.",
"image_error": "Image not found",
"file_error": "File not found",
"audio_error": "Audio not found",
"video_error": "Video not found"
}
}
}

View File

@ -17,7 +17,11 @@
"browser_video_unsupport": "Le navigateur ne prend pas en charge l'élément vidéo.",
"download": "Télécharger",
"unsupported_file_type": "Ce type de fichier n'est pas pris en charge.",
"unsupported_file_size": "Cette taille de fichier n'est pas prise en charge."
"unsupported_file_size": "Cette taille de fichier n'est pas prise en charge.",
"image_error": "Image introuvable",
"file_error": "Fichier introuvable",
"audio_error": "Audio introuvable",
"video_error": "Vidéo introuvable"
}
}
}