relax-mj
Version:
Node.js client for the unofficial MidJourney API.
403 lines (398 loc) • 11.1 kB
text/typescript
import { DiscordImage, MJConfig, UploadParam, UploadSlot } from "./interfaces";
import { CreateQueue } from "./queue";
import { nextNonce, sleep } from "./utls";
import fetch from "node-fetch";
import { HttpsProxyAgent } from "https-proxy-agent";
import * as fs from "fs";
import path from "path";
import * as mime from "mime";
export class MidjourneyApi {
private apiQueue = CreateQueue(1);
Agent?: HttpsProxyAgent<string>;
UpId = Date.now() % 10; // upload id
constructor(public config: MJConfig) {
if (this.config.ProxyUrl && this.config.ProxyUrl !== "") {
this.Agent = new HttpsProxyAgent(this.config.ProxyUrl);
}
}
// limit the number of concurrent interactions
protected async safeIteractions(payload: any) {
return this.apiQueue.addTask(
() =>
new Promise<number>((resolve) => {
this.interactions(payload, (res) => {
resolve(res);
});
})
);
}
protected async interactions(
payload: any,
callback?: (result: number) => void
) {
try {
const headers = {
"Content-Type": "application/json",
Authorization: this.config.SalaiToken,
};
const agent = this.Agent;
const response = await fetch(
`${this.config.DiscordBaseUrl}/api/v9/interactions`,
{
method: "POST",
body: JSON.stringify(payload),
headers: headers,
agent,
}
);
callback && callback(response.status);
//discord api rate limit
await sleep(950);
if (response.status >= 400) {
console.error("api.error.config", { config: this.config });
}
return response.status;
} catch (error) {
console.error(error);
callback && callback(500);
}
}
async ImagineApi(prompt: string, nonce: string = nextNonce()) {
const guild_id = this.config.ServerId;
const payload = {
type: 2,
application_id: "936929561302675456",
guild_id,
channel_id: this.config.ChannelId,
session_id: this.config.SessionId,
data: {
version: "1166847114203123795",
id: "938956540159881230",
name: "imagine",
type: 1,
options: [
{
type: 3,
name: "prompt",
value: prompt,
},
],
application_command: {
id: "938956540159881230",
application_id: "936929561302675456",
version: "1166847114203123795",
default_permission: true,
default_member_permissions: null,
type: 1,
nsfw: false,
name: "imagine",
description: "Create images with Midjourney",
dm_permission: true,
options: [
{
type: 3,
name: "prompt",
description: "The prompt to imagine",
required: true,
},
],
},
attachments: [],
},
nonce,
};
return this.safeIteractions(payload);
}
async VariationApi(
index: number,
messageId: string,
messageHash: string,
nonce?: string
) {
const payload = {
type: 3,
guild_id: this.config.ServerId,
channel_id: this.config.ChannelId,
message_flags: 0,
message_id: messageId,
application_id: "936929561302675456",
session_id: this.config.SessionId,
data: {
component_type: 2,
custom_id: `MJ::JOB::variation::${index}::${messageHash}`,
},
nonce,
};
return this.safeIteractions(payload);
}
async UpscaleApi(
index: number,
messageId: string,
messageHash: string,
nonce?: string
) {
const guild_id = this.config.ServerId;
const payload = {
type: 3,
guild_id,
channel_id: this.config.ChannelId,
message_flags: 0,
message_id: messageId,
application_id: "936929561302675456",
session_id: this.config.SessionId,
data: {
component_type: 2,
custom_id: `MJ::JOB::upsample::${index}::${messageHash}`,
},
nonce,
};
return this.safeIteractions(payload);
}
async ClickBtnApi(messageId: string, customId: string, nonce?: string) {
const guild_id = this.config.ServerId;
const payload = {
type: 3,
nonce,
guild_id,
channel_id: this.config.ChannelId,
message_flags: 0,
message_id: messageId,
application_id: "936929561302675456",
session_id: this.config.SessionId,
data: {
component_type: 2,
custom_id: customId,
},
};
return this.safeIteractions(payload);
}
async InfoApi(nonce?: string) {
const guild_id = this.config.ServerId;
const payload = {
type: 2,
application_id: "936929561302675456",
guild_id,
channel_id: this.config.ChannelId,
session_id: this.config.SessionId,
data: {
version: "987795925764280356",
id: "972289487818334209",
name: "info",
type: 1,
options: [],
application_command: {
id: "972289487818334209",
application_id: "936929561302675456",
version: "987795925764280356",
default_member_permissions: null,
type: 1,
nsfw: false,
name: "info",
description: "View information about your profile.",
dm_permission: true,
contexts: null,
},
attachments: [],
},
nonce,
};
return this.safeIteractions(payload);
}
async FastApi(nonce?: string) {
const guild_id = this.config.ServerId;
const payload = {
type: 2,
application_id: "936929561302675456",
guild_id,
channel_id: this.config.ChannelId,
session_id: this.config.SessionId,
data: {
version: "987795926183731231",
id: "972289487818334212",
name: "fast",
type: 1,
options: [],
application_command: {
id: "972289487818334212",
application_id: "936929561302675456",
version: "987795926183731231",
default_member_permissions: null,
type: 1,
nsfw: false,
name: "fast",
description: "Switch to fast mode",
dm_permission: true,
contexts: null,
},
attachments: [],
},
nonce,
};
return this.safeIteractions(payload);
}
async RelaxApi(nonce?: string) {
const guild_id = this.config.ServerId;
const channel_id = this.config.ChannelId;
const payload = {
type: 2,
application_id: "936929561302675456",
guild_id,
channel_id,
session_id: this.config.SessionId,
data: {
version: "987795926183731232",
id: "972289487818334213",
name: "relax",
type: 1,
options: [],
application_command: {
id: "972289487818334213",
application_id: "936929561302675456",
version: "987795926183731232",
default_member_permissions: null,
type: 1,
nsfw: false,
name: "relax",
description: "Switch to relax mode",
dm_permission: true,
contexts: null,
},
attachments: [],
},
nonce,
};
return this.safeIteractions(payload);
}
/**
*
* @param fileUrl http or local file path
* @returns
*/
async UploadImage(fileUrl: string) {
let fileData;
let mimeType;
let filename;
let file_size;
if (fileUrl.startsWith("http")) {
const response = await fetch(fileUrl);
fileData = await response.arrayBuffer();
mimeType = response.headers.get("content-type");
filename = path.basename(fileUrl) || "image.png";
file_size = fileData.byteLength;
} else {
fileData = await fs.promises.readFile(fileUrl);
mimeType = mime.getType(fileUrl);
filename = path.basename(fileUrl);
file_size = (await fs.promises.stat(fileUrl)).size;
}
if (!mimeType) {
throw new Error("Unknown mime type");
}
const { attachments } = await this.attachments({
filename,
file_size,
id: this.UpId++,
});
const UploadSlot = attachments[0];
await this.uploadImage(UploadSlot, fileData, mimeType);
const response: DiscordImage = {
id: UploadSlot.id,
filename: path.basename(UploadSlot.upload_filename),
upload_filename: UploadSlot.upload_filename,
};
return response;
}
/**
* prepare an attachement to upload an image.
*/
private async attachments(
...files: UploadParam[]
): Promise<{ attachments: UploadSlot[] }> {
const headers = {
Authorization: this.config.SalaiToken,
"content-type": "application/json",
};
const url = new URL(
`${this.config.DiscordBaseUrl}/api/v9/channels/${this.config.ChannelId}/attachments`
);
const body = { files };
const response = await fetch(url.toString(), {
headers,
method: "POST",
body: JSON.stringify(body),
});
if (response.status === 200) {
return (await response.json()) as { attachments: UploadSlot[] };
}
throw new Error(
`Attachments return ${response.status} ${
response.statusText
} ${await response.text()}`
);
}
private async uploadImage(
slot: UploadSlot,
data: ArrayBuffer,
contentType: string
): Promise<void> {
const body = new Uint8Array(data);
const headers = { "content-type": contentType };
const response = await fetch(slot.upload_url, {
method: "PUT",
headers,
body,
});
if (!response.ok) {
throw new Error(
`uploadImage return ${response.status} ${
response.statusText
} ${await response.text()}`
);
}
}
async DescribeApi(data: DiscordImage, nonce?: string) {
const payload = {
type: 2,
application_id: "936929561302675456",
guild_id: this.config.ServerId,
channel_id: this.config.ChannelId,
session_id: this.config.SessionId,
data: {
version: "1092492867185950853",
id: "1092492867185950852",
name: "describe",
type: 1,
options: [{ type: 11, name: "image", value: data.id }],
application_command: {
id: "1092492867185950852",
application_id: "936929561302675456",
version: "1092492867185950853",
default_member_permissions: null,
type: 1,
nsfw: false,
name: "describe",
description: "Writes a prompt based on your image.",
dm_permission: true,
contexts: null,
options: [
{
type: 11,
name: "image",
description: "The image to describe",
required: true,
},
],
},
attachments: [
{
id: <string>data.id,
filename: data.filename,
uploaded_filename: data.upload_filename,
},
],
},
nonce,
};
return this.safeIteractions(payload);
}
}