Merge pull request #1515 from gitroomhq/feat/has-extension-helper

feat: hasExtension helper for media type detection
This commit is contained in:
Nevo David 2026-05-14 11:13:36 +07:00 committed by GitHub
commit f2ebadab9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 67 additions and 41 deletions

View file

@ -33,6 +33,7 @@ import dayjs from 'dayjs';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { ExistingDataContextProvider } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
export const AgentChat: FC = () => {
const { backendUrl } = useVariables();
@ -162,7 +163,7 @@ const NewInput: FC<InputProps> = (props) => {
? '\n[--Media--]' +
media
.map((m) =>
m.path.indexOf('mp4') > -1
hasExtension(m.path, 'mp4')
? `Video: ${m.path}`
: `Image: ${m.path}`
)

View file

@ -4,6 +4,7 @@ import { EventEmitter } from 'events';
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
import { useVariables } from '@gitroom/react/helpers/variable.context';
const postUrlEmitter = new EventEmitter();
@ -375,7 +376,7 @@ export const MediaComponentInner: FC<{
className="w-full px-3 py-2 bg-fifth border border-tableBorder rounded-lg text-textColor placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-forth focus:border-transparent"
/>
</div>
{media?.path.indexOf('mp4') > -1 && (
{hasExtension(media?.path, 'mp4') && (
<>
{/* Alt Text Input */}
<div>

View file

@ -14,6 +14,7 @@ import React, {
import { Button } from '@gitroom/react/form/button';
import useSWR from 'swr';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
import { Media } from '@prisma/client';
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
@ -351,7 +352,7 @@ export const MediaBox: FC<{
top: 10,
children: (
<div className="w-full h-full p-[50px]">
{media.path.indexOf('mp4') > -1 ? (
{hasExtension(media.path, 'mp4') ? (
<VideoFrame
autoplay={true}
url={mediaDirectory.set(media.path)}
@ -525,9 +526,9 @@ export const MediaBox: FC<{
{data?.results
?.filter((f: any) => {
if (type === 'video') {
return f.path.indexOf('mp4') > -1;
return hasExtension(f.path, 'mp4');
} else if (type === 'image') {
return f.path.indexOf('mp4') === -1;
return !hasExtension(f.path, 'mp4');
}
return true;
})
@ -579,7 +580,7 @@ export const MediaBox: FC<{
</svg>
</div>
</div>
{media.path.indexOf('mp4') > -1 ? (
{hasExtension(media.path, 'mp4') ? (
<VideoFrame url={mediaDirectory.set(media.path)} />
) : (
<img
@ -803,7 +804,7 @@ export const MultiMediaComponent: FC<{
>
<MediaSettingsIcon className="cursor-pointer relative z-[200]" />
</div>
{media?.path?.indexOf('mp4') > -1 ? (
{hasExtension(media?.path, 'mp4') ? (
<VideoFrame url={mediaDirectory.set(media?.path)} />
) : (
<img

View file

@ -4,6 +4,7 @@ import { useFormContext } from 'react-hook-form';
import { useVideo } from '@gitroom/frontend/components/videos/video.context.wrapper';
import { Textarea } from '@gitroom/react/form/textarea';
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
export interface Voice {
id: string;
@ -46,7 +47,7 @@ const VEO3Settings: FC = () => {
setValue(
'images',
val.target.value
.filter((f) => f.path.indexOf('mp4') === -1)
.filter((f) => !hasExtension(f.path, 'mp4'))
.slice(0, 3)
)
}

View file

@ -0,0 +1,10 @@
export const hasExtension = (
path: string | undefined | null,
extension: string
): boolean => {
if (!path) {
return false;
}
const ext = extension.startsWith('.') ? extension : `.${extension}`;
return path.toLowerCase().indexOf(ext.toLowerCase()) > -1;
};

View file

@ -40,6 +40,7 @@ import { timer } from '@gitroom/helpers/utils/timer';
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
import { stripLinks } from '@gitroom/helpers/utils/strip.links';
type PostWithConditionals = Post & {
@ -367,7 +368,7 @@ export class PostsService {
return m;
}
if (m.path.indexOf('.png') > -1) {
if (hasExtension(m.path, 'png')) {
imageUpdateNeeded = true;
const response = await axios.get(m.url, {
responseType: 'arraybuffer',

View file

@ -27,6 +27,7 @@ import { timer } from '@gitroom/helpers/utils/timer';
import axios from 'axios';
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
async function reduceImageBySize(url: string, maxSizeKB = 976) {
try {
@ -266,9 +267,9 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
): Promise<{ embed: any; images: any[] }> {
// Separate images and videos
const imageMedia =
post.media?.filter((p) => p.path.indexOf('mp4') === -1) || [];
post.media?.filter((p) => !hasExtension(p.path, 'mp4')) || [];
const videoMedia =
post.media?.filter((p) => p.path.indexOf('mp4') !== -1) || [];
post.media?.filter((p) => hasExtension(p.path, 'mp4')) || [];
// Upload images
const images = await Promise.all(

View file

@ -11,6 +11,7 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab
import { FacebookDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/facebook.dto';
import { DribbbleDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dribbble.dto';
import { Integration } from '@prisma/client';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
export class FacebookProvider extends SocialAbstract implements SocialProvider {
identifier = 'facebook';
@ -415,7 +416,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
let finalId = '';
let finalUrl = '';
if ((firstPost?.media?.[0]?.path?.indexOf('mp4') || -2) > -1) {
if (hasExtension(firstPost?.media?.[0]?.path, 'mp4')) {
const {
id: videoId,
permalink_url,

View file

@ -12,6 +12,7 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
import { Integration } from '@prisma/client';
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
@Rules(
"Instagram should have at least one attachment, if it's a story, it can have only one picture"
@ -544,22 +545,21 @@ export class InstagramProvider
(firstPost?.media?.length || 0) > 1 && !isStory
? `&is_carousel_item=true`
: ``;
const mediaType =
m.path.indexOf('.mp4') > -1
? firstPost?.media?.length === 1
? isStory
? `video_url=${m.path}&media_type=STORIES`
: `video_url=${m.path}&media_type=REELS&thumb_offset=${
m?.thumbnailTimestamp || 0
}`
: isStory
const mediaType = hasExtension(m.path, 'mp4')
? firstPost?.media?.length === 1
? isStory
? `video_url=${m.path}&media_type=STORIES`
: `video_url=${m.path}&media_type=VIDEO&thumb_offset=${
: `video_url=${m.path}&media_type=REELS&thumb_offset=${
m?.thumbnailTimestamp || 0
}`
: isStory
? `image_url=${m.path}&media_type=STORIES`
: `image_url=${m.path}`;
? `video_url=${m.path}&media_type=STORIES`
: `video_url=${m.path}&media_type=VIDEO&thumb_offset=${
m?.thumbnailTimestamp || 0
}`
: isStory
? `image_url=${m.path}&media_type=STORIES`
: `image_url=${m.path}`;
const trialParams = isTrialReel
? `&trial_params=${encodeURIComponent(

View file

@ -8,6 +8,7 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import sharp from 'sharp';
import { lookup } from 'mime-types';
import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { Integration } from '@prisma/client';
import { PostPlug } from '@gitroom/helpers/decorators/post.plug';
@ -234,8 +235,8 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
type = 'personal' as 'company' | 'personal'
) {
// Determine the appropriate endpoint based on file type
const isVideo = fileName.indexOf('mp4') > -1;
const isPdf = fileName.toLowerCase().indexOf('pdf') > -1;
const isVideo = hasExtension(fileName, 'mp4');
const isPdf = hasExtension(fileName, 'pdf');
let endpoint: string;
if (isVideo) {
@ -477,7 +478,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
}
private async prepareMediaBuffer(mediaUrl: string): Promise<Buffer> {
const isVideo = mediaUrl.indexOf('mp4') > -1;
const isVideo = hasExtension(mediaUrl, 'mp4');
const isGif = lookup(mediaUrl) === 'image/gif';
if (isVideo || isGif) {

View file

@ -10,6 +10,7 @@ import dayjs from 'dayjs';
import { Integration } from '@prisma/client';
import { MeweDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/mewe.dto';
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
export class MeweProvider extends SocialAbstract implements SocialProvider {
identifier = 'mewe';
@ -244,7 +245,7 @@ export class MeweProvider extends SocialAbstract implements SocialProvider {
// Upload photos if present (exclude videos)
const imageMedia =
firstPost.media?.filter((m) => !m.path || m.path.indexOf('mp4') === -1) ||
firstPost.media?.filter((m) => !m.path || !hasExtension(m.path, 'mp4')) ||
[];
const uploadedPhotoIds: string[] = [];

View file

@ -17,6 +17,7 @@ import {
import dayjs from 'dayjs';
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
@Rules(
'Pinterest requires at least one media, if posting a video, you must have two attachment, one for video, one for the cover picture, When posting a video, there can be only one'
@ -185,10 +186,10 @@ export class PinterestProvider
): Promise<PostResponse[]> {
let mediaId = '';
const findMp4 = postDetails?.[0]?.media?.find(
(p) => (p.path?.indexOf('mp4') || -1) > -1
(p) => hasExtension(p.path, 'mp4')
);
const picture = postDetails?.[0]?.media?.find(
(p) => (p.path?.indexOf('mp4') || -1) === -1
(p) => !hasExtension(p.path, 'mp4')
);
if (findMp4) {

View file

@ -14,6 +14,7 @@ import axios from 'axios';
import WebSocket from 'ws';
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
import { Integration } from '@prisma/client';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
// @ts-ignore
global.WebSocket = WebSocket;
@ -186,7 +187,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
for (const firstPostSettings of post.settings.subreddit) {
const kind =
firstPostSettings.value.type === 'media'
? post.media[0].path.indexOf('mp4') > -1
? hasExtension(post.media[0].path, 'mp4')
? 'video'
: 'image'
: firstPostSettings.value.type;
@ -211,7 +212,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
accessToken,
post.media[0].path
),
...(post.media[0].path.indexOf('mp4') > -1
...(hasExtension(post.media[0].path, 'mp4')
? {
video_poster_url: await this.uploadFileToReddit(
accessToken,

View file

@ -13,6 +13,7 @@ import { capitalize, chunk } from 'lodash';
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
import { Integration } from '@prisma/client';
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
export class ThreadsProvider extends SocialAbstract implements SocialProvider {
identifier = 'threads';
@ -189,7 +190,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
replyToId?: string
): Promise<string> {
const mediaType =
media.path.indexOf('.mp4') > -1 ? 'video_url' : 'image_url';
hasExtension(media.path, 'mp4') ? 'video_url' : 'image_url';
const mediaParams = new URLSearchParams({
...(mediaType === 'video_url' ? { video_url: media.path } : {}),
...(mediaType === 'image_url' ? { image_url: media.path } : {}),

View file

@ -12,6 +12,7 @@ import {
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { TikTokDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/tiktok.dto';
import { timer } from '@gitroom/helpers/utils/timer';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
import { Integration } from '@prisma/client';
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
@ -457,7 +458,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
}
private buildTikokPostInfoBody(firstPost: PostDetails<TikTokDto>) {
const isPhoto = (firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) === -1;
const isPhoto = !hasExtension(firstPost?.media?.[0]?.path, 'mp4');
const method = firstPost?.settings?.content_posting_method;
if (method === 'DIRECT_POST') {
@ -507,7 +508,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
}
private buildTikokSourceInfoBody(firstPost: PostDetails<TikTokDto>) {
const isPhoto = (firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) === -1;
const isPhoto = !hasExtension(firstPost?.media?.[0]?.path, 'mp4');
if (isPhoto) {
return {
@ -545,7 +546,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
integration: Integration
): Promise<PostResponse[]> {
const [firstPost] = postDetails;
const isPhoto = (firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) === -1;
const isPhoto = !hasExtension(firstPost?.media?.[0]?.path, 'mp4');
console.log({
...this.buildTikokPostInfoBody(firstPost),
@ -557,7 +558,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
await this.fetch(
`https://open.tiktokapis.com/v2/post/publish${this.postingMethod(
firstPost.settings.content_posting_method,
(firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) === -1
!hasExtension(firstPost?.media?.[0]?.path, 'mp4')
)}`,
{
method: 'POST',

View file

@ -12,6 +12,7 @@ import axios from 'axios';
import FormDataNew from 'form-data';
import mime from 'mime-types';
import { Integration } from '@prisma/client';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
export class VkProvider extends SocialAbstract implements SocialProvider {
override maxConcurrentJob = 2; // VK has moderate API limits
@ -168,7 +169,7 @@ export class VkProvider extends SocialAbstract implements SocialProvider {
(post?.media || []).map(async (media) => {
const all = await (
await this.fetch(
media.path.indexOf('mp4') > -1
hasExtension(media.path, 'mp4')
? `https://api.vk.com/method/video.save?access_token=${accessToken}&v=5.251`
: `https://api.vk.com/method/photos.getWallUploadServer?owner_id=${userId}&access_token=${accessToken}&v=5.251`
)
@ -193,7 +194,7 @@ export class VkProvider extends SocialAbstract implements SocialProvider {
})
).data;
if (media.path.indexOf('mp4') > -1) {
if (hasExtension(media.path, 'mp4')) {
return {
id: all.response.video_id,
type: 'video',

View file

@ -21,6 +21,7 @@ import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validatio
import { stripLinks as removeLinks } from '@gitroom/helpers/utils/strip.links';
import { XDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/x.dto';
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
@Rules(
'X can have maximum 4 pictures, or maximum one video, it can also be without attachments'
@ -390,7 +391,7 @@ export class XProvider extends SocialAbstract implements SocialProvider {
id: await this.runInConcurrent(
async () =>
client.v2.uploadMedia(
m.path.indexOf('mp4') > -1
hasExtension(m.path, 'mp4')
? Buffer.from(await readOrFetch(m.path))
: await sharp(await readOrFetch(m.path), {
animated: lookup(m.path) === 'image/gif',

View file

@ -1,5 +1,6 @@
import { FC } from 'react';
import { clsx } from 'clsx';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
export const VideoOrImage: FC<{
src: string;
autoplay: boolean;
@ -8,7 +9,7 @@ export const VideoOrImage: FC<{
videoClassName?: string;
}> = (props) => {
const { src, autoplay, isContain, imageClassName, videoClassName } = props;
if (src?.indexOf('mp4') > -1) {
if (hasExtension(src, 'mp4')) {
return (
<video
src={src}