Skip to content

Commit 79054e8

Browse files
committed
Add /v convert slash command
- Add `/v convert` slash command -> Converts a video to a different mimetype, like webm to mp4 - Update media url finding in our utils
1 parent 200ef61 commit 79054e8

File tree

13 files changed

+512
-20
lines changed

13 files changed

+512
-20
lines changed

src/api/endpoints.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ export const Api = Object.freeze({
240240
'/utilities/qr/scan',
241241
UTILITIES_SCREENSHOT:
242242
'/utilities/screenshot',
243+
244+
VIDEO_TOOLS_CONVERT:
245+
'/video/tools/convert',
243246
});
244247

245248

src/api/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,3 +1020,11 @@ export async function utilitiesScreenshot(
10201020
) {
10211021
return raw.utilitiesScreenshot(context, options);
10221022
}
1023+
1024+
1025+
export async function videoToolsConvert(
1026+
context: RequestContext,
1027+
options: RestOptions.VideoToolsConvertOptions,
1028+
) {
1029+
return raw.videoToolsConvert(context, options);
1030+
}

src/api/raw.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2127,3 +2127,22 @@ export async function utilitiesScreenshot(
21272127
},
21282128
});
21292129
}
2130+
2131+
2132+
export async function videoToolsConvert(
2133+
context: RequestContext,
2134+
options: RestOptions.VideoToolsConvertOptions,
2135+
): Promise<Response> {
2136+
const query = {
2137+
to: options.to,
2138+
url: options.url,
2139+
};
2140+
return request(context, {
2141+
dataOnly: false,
2142+
query,
2143+
route: {
2144+
method: HTTPMethods.POST,
2145+
path: Api.VIDEO_TOOLS_CONVERT,
2146+
},
2147+
});
2148+
}

src/api/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,15 @@ export namespace RestOptions {
415415
export interface UtilitiesScreenshot {
416416
url: string,
417417
}
418+
419+
420+
export interface VideoBaseOptions {
421+
url: string,
422+
}
423+
424+
export interface VideoToolsConvertOptions extends VideoBaseOptions {
425+
to: string,
426+
}
418427
}
419428

420429

src/commands/interactions/basecommand.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,35 @@ export class BaseInteractionImageCommandOption<ParsedArgsFinished = Interaction.
214214
}
215215

216216

217+
export class BaseInteractionVideoCommandOption<ParsedArgsFinished = Interaction.ParsedArgs> extends BaseInteractionCommandOption<ParsedArgsFinished> {
218+
constructor(data: Interaction.InteractionCommandOptionOptions = {}) {
219+
super({
220+
...data,
221+
options: [
222+
...(data.options || []),
223+
{name: 'video', description: 'Emoji/Media URL/User', label: 'url', default: DefaultParameters.lastVideoUrl, value: Parameters.lastVideoUrl},
224+
],
225+
});
226+
}
227+
228+
onBeforeRun(context: Interaction.InteractionContext, args: {url?: null | string}) {
229+
if (args.url) {
230+
context.metadata = Object.assign({}, context.metadata, {contentUrl: args.url});
231+
}
232+
return !!args.url;
233+
}
234+
235+
onCancelRun(context: Interaction.InteractionContext, args: {url?: null | string}) {
236+
if (args.url === undefined) {
237+
return editOrReply(context, '⚠ Unable to find any media in the last 50 messages.');
238+
} else if (args.url === null) {
239+
return editOrReply(context, '⚠ Unable to find that user or it was an invalid url.');
240+
}
241+
return super.onCancelRun(context, args);
242+
}
243+
}
244+
245+
217246
export class BaseInteractionCommandOptionGroup<ParsedArgsFinished = Interaction.ParsedArgs> extends Interaction.InteractionCommandOption<ParsedArgsFinished> {
218247
error = 'Slash Command';
219248
type = ApplicationCommandOptionTypes.SUB_COMMAND_GROUP;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Interaction } from 'detritus-client';
2+
3+
import { Formatter } from '../../../../utils';
4+
5+
import { BaseInteractionVideoCommandOption } from '../../basecommand';
6+
7+
8+
export const COMMAND_NAME = 'convert';
9+
10+
export class VideoConvertCommand extends BaseInteractionVideoCommandOption {
11+
description = 'Convert a Video';
12+
name = COMMAND_NAME;
13+
14+
constructor() {
15+
super({
16+
options: [
17+
{
18+
name: 'to',
19+
description: 'Conversion Mimetype',
20+
choices: Formatter.Commands.VideoConvert.SLASH_CHOICES,
21+
default: Formatter.Commands.VideoConvert.DEFAULT_MIMETYPE,
22+
},
23+
],
24+
});
25+
}
26+
27+
async run(context: Interaction.InteractionContext, args: Formatter.Commands.VideoConvert.CommandArgs) {
28+
return Formatter.Commands.VideoConvert.createMessage(context, args);
29+
}
30+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Permissions } from 'detritus-client/lib/constants';
2+
3+
import { BaseSlashCommand } from '../../basecommand';
4+
5+
import { VideoConvertCommand } from './convert';
6+
7+
8+
export default class VideoGroupCommand extends BaseSlashCommand {
9+
description = '.';
10+
name = 'v';
11+
12+
constructor() {
13+
super({
14+
permissions: [Permissions.ATTACH_FILES],
15+
options: [
16+
new VideoConvertCommand(),
17+
],
18+
});
19+
}
20+
}

src/constants.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,15 +541,43 @@ export enum Mimetypes {
541541
IMAGE_JPEG = 'image/jpeg',
542542
IMAGE_PNG = 'image/png',
543543
IMAGE_WEBP = 'image/webp',
544+
VIDEO_M4V = 'video/x-m4v',
545+
VIDEO_MP4 = 'video/mp4',
546+
VIDEO_MPEG = 'video/mpeg',
547+
VIDEO_QUICKTIME = 'video/quicktime',
548+
VIDEO_WEBM = 'video/webm',
549+
VIDEO_X_MSVIDEO = 'video/x-msvideo',
544550
};
545551

546-
export const MIMETYPES_SAFE_EMBED = Object.freeze([
552+
export const MimetypesToExtension = Object.freeze({
553+
[Mimetypes.IMAGE_GIF]: 'gif',
554+
[Mimetypes.IMAGE_JPEG]: 'jpg',
555+
[Mimetypes.IMAGE_PNG]: 'png',
556+
[Mimetypes.IMAGE_WEBP]: 'webp',
557+
[Mimetypes.VIDEO_M4V]: 'm4v',
558+
[Mimetypes.VIDEO_MP4]: 'mp4',
559+
[Mimetypes.VIDEO_MPEG]: 'mpeg',
560+
[Mimetypes.VIDEO_QUICKTIME]: 'mov',
561+
[Mimetypes.VIDEO_WEBM]: 'webm',
562+
[Mimetypes.VIDEO_X_MSVIDEO]: 'avi',
563+
});
564+
565+
566+
export const MIMETYPES_IMAGE_EMBEDDABLE = Object.freeze([
547567
Mimetypes.IMAGE_GIF,
548568
Mimetypes.IMAGE_JPEG,
549569
Mimetypes.IMAGE_PNG,
550570
Mimetypes.IMAGE_WEBP,
551571
]);
552572

573+
export const MIMETYPES_VIDEO_EMBEDDABLE = Object.freeze([
574+
Mimetypes.VIDEO_QUICKTIME,
575+
Mimetypes.VIDEO_MP4,
576+
Mimetypes.VIDEO_WEBM,
577+
]);
578+
579+
580+
export const MIMETYPES_SAFE_EMBED = MIMETYPES_IMAGE_EMBEDDABLE;
553581

554582
export const RatelimitKeys = Object.freeze({
555583
IMAGE: Math.random().toString(36).substring(7),

src/utils/defaultparameters.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { GuildExplicitContentFilterTypes } from 'detritus-client/lib/constants';
44
import { GoogleLocales, GoogleLocaleFromDiscord } from '../constants';
55
import UserStore from '../stores/users';
66

7-
import { findImageUrlInMessages } from './tools';
7+
import {
8+
findImageUrlInMessages,
9+
findMediaUrlInMessages,
10+
} from './tools';
811

912

1013
export function applications(context: Command.Context | Interaction.InteractionContext) {
@@ -97,6 +100,73 @@ export async function lastImageUrl(context: Command.Context | Interaction.Intera
97100
}
98101

99102

103+
export async function lastVideoUrl(context: Command.Context | Interaction.InteractionContext): Promise<string | null> {
104+
const mediaSearchOptions = {image: false};
105+
106+
if (context instanceof Command.Context) {
107+
{
108+
const url = findMediaUrlInMessages([context.message], mediaSearchOptions);
109+
if (url) {
110+
return url;
111+
}
112+
}
113+
114+
{
115+
// check reply
116+
const { messageReference } = context.message;
117+
if (messageReference && messageReference.messageId) {
118+
let message = messageReference.message;
119+
if (!message && (context.inDm || (context.channel && context.channel.canReadHistory))) {
120+
try {
121+
message = await context.rest.fetchMessage(messageReference.channelId, messageReference.messageId);
122+
} catch(error) {
123+
// /shrug
124+
}
125+
}
126+
if (message) {
127+
const url = findMediaUrlInMessages([message], mediaSearchOptions);
128+
if (url) {
129+
return url;
130+
}
131+
}
132+
}
133+
}
134+
}
135+
136+
const before = (context instanceof Command.Context) ? context.messageId : undefined;
137+
{
138+
const beforeId = (before) ? BigInt(before) : null;
139+
// we dont get DM channels anymore so we must manually find messages now
140+
const messages = context.messages.filter((message) => {
141+
if (message.channelId !== context.channelId) {
142+
return false;
143+
}
144+
if (message.interaction && message.hasFlagEphemeral) {
145+
return message.interaction.user.id === context.userId;
146+
}
147+
if (beforeId) {
148+
return BigInt(message.id) <= beforeId;
149+
}
150+
return true;
151+
}).reverse();
152+
const url = findMediaUrlInMessages(messages, mediaSearchOptions);
153+
if (url) {
154+
return url;
155+
}
156+
}
157+
158+
if (context.inDm || (context.channel && context.channel.canReadHistory)) {
159+
const messages = await context.rest.fetchMessages(context.channelId!, {before, limit: 50});
160+
const url = findMediaUrlInMessages(messages, mediaSearchOptions);
161+
if (url) {
162+
return url;
163+
}
164+
}
165+
166+
return null;
167+
}
168+
169+
100170
export async function locale(context: Command.Context | Interaction.InteractionContext): Promise<GoogleLocales> {
101171
const user = await UserStore.getOrFetch(context, context.userId);
102172
if (user && user.locale) {

src/utils/formatter/commands/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ import * as ToolsQrScan from './tools.qr.scan';
4040
import * as ToolsScreenshot from './tools.screenshot';
4141
import * as ToolsTranslate from './tools.translate';
4242

43+
import * as VideoConvert from './video.convert';
44+
45+
4346
export {
4447
ImageBlur,
4548
ImageBlurple,
@@ -77,4 +80,5 @@ export {
7780
ToolsQrScan,
7881
ToolsScreenshot,
7982
ToolsTranslate,
83+
VideoConvert,
8084
};

0 commit comments

Comments
 (0)