midjourney
Version:
Node.js client for the unofficial MidJourney API.
464 lines (438 loc) • 10.7 kB
text/typescript
import {
CustomZoomModalSubmitID,
DescribeModalSubmitID,
DiscordImage,
MJConfig,
ModalSubmitID,
RemixModalSubmitID,
ShortenModalSubmitID,
UploadParam,
UploadSlot,
} from "./interfaces";
import { nextNonce, sleep } from "./utils";
import { Command } from "./command";
import async from "async";
export class MidjourneyApi extends Command {
UpId = Date.now() % 10; // upload id
constructor(public config: MJConfig) {
super(config);
}
private safeIteractions = (request: any) => {
return new Promise<number>((resolve, reject) => {
this.queue.push(
{
request,
callback: (any: any) => {
resolve(any);
},
},
(error: any, result: any) => {
if (error) {
reject(error);
} else {
resolve(result);
}
}
);
});
};
private processRequest = async ({
request,
callback,
}: {
request: any;
callback: (any: any) => void;
}) => {
const httpStatus = await this.interactions(request);
callback(httpStatus);
await sleep(this.config.ApiInterval);
};
private queue = async.queue(this.processRequest, 1);
private interactions = async (payload: any) => {
try {
const headers = {
"Content-Type": "application/json",
Authorization: this.config.SalaiToken,
};
const response = await this.config.fetch(
`${this.config.DiscordBaseUrl}/api/v9/interactions`,
{
method: "POST",
body: JSON.stringify(payload),
headers: headers,
}
);
if (response.status >= 400) {
console.error("api.error.config", {
payload: JSON.stringify(payload),
config: this.config,
});
}
return response.status;
} catch (error) {
console.error(error);
return 500;
}
};
async ImagineApi(prompt: string, nonce: string = nextNonce()) {
const payload = await this.imaginePayload(prompt, nonce);
return this.safeIteractions(payload);
}
async SwitchRemixApi(nonce: string = nextNonce()) {
const payload = await this.PreferPayload(nonce);
return this.safeIteractions(payload);
}
async ShortenApi(prompt: string, nonce: string = nextNonce()) {
const payload = await this.shortenPayload(prompt, nonce);
return this.safeIteractions(payload);
}
async VariationApi({
index,
msgId,
hash,
nonce = nextNonce(),
flags = 0,
}: {
index: 1 | 2 | 3 | 4;
msgId: string;
hash: string;
nonce?: string;
flags?: number;
}) {
return this.CustomApi({
msgId,
customId: `MJ::JOB::variation::${index}::${hash}`,
flags,
nonce,
});
}
async UpscaleApi({
index,
msgId,
hash,
nonce = nextNonce(),
flags,
}: {
index: 1 | 2 | 3 | 4;
msgId: string;
hash: string;
nonce?: string;
flags: number;
}) {
return this.CustomApi({
msgId,
customId: `MJ::JOB::upsample::${index}::${hash}`,
flags,
nonce,
});
}
async RerollApi({
msgId,
hash,
nonce = nextNonce(),
flags,
}: {
msgId: string;
hash: string;
nonce?: string;
flags: number;
}) {
return this.CustomApi({
msgId,
customId: `MJ::JOB::reroll::0::${hash}::SOLO`,
flags,
nonce,
});
}
async CustomApi({
msgId,
customId,
flags,
nonce = nextNonce(),
}: {
msgId: string;
customId: string;
flags: number;
nonce?: string;
}) {
if (!msgId) throw new Error("msgId is empty");
if (flags === undefined) throw new Error("flags is undefined");
const payload = {
type: 3,
nonce,
guild_id: this.config.ServerId,
channel_id: this.config.ChannelId,
message_flags: flags,
message_id: msgId,
application_id: this.config.BotId,
session_id: this.config.SessionId,
data: {
component_type: 2,
custom_id: customId,
},
};
return this.safeIteractions(payload);
}
//FIXME: get SubmitCustomId from discord api
async ModalSubmitApi({
nonce,
msgId,
customId,
prompt,
submitCustomId,
}: {
nonce: string;
msgId: string;
customId: string;
prompt: string;
submitCustomId: ModalSubmitID;
}) {
var payload = {
type: 5,
application_id: this.config.BotId,
channel_id: this.config.ChannelId,
guild_id: this.config.ServerId,
data: {
id: msgId,
custom_id: customId,
components: [
{
type: 1,
components: [
{
type: 4,
custom_id: submitCustomId,
value: prompt,
},
],
},
],
},
session_id: this.config.SessionId,
nonce,
};
console.log("submitCustomId", JSON.stringify(payload));
return this.safeIteractions(payload);
}
async RemixApi({
nonce,
msgId,
customId,
prompt,
}: {
nonce: string;
msgId: string;
customId: string;
prompt: string;
}) {
return this.ModalSubmitApi({
nonce,
msgId,
customId,
prompt,
submitCustomId: RemixModalSubmitID,
});
}
async ShortenImagineApi({
nonce,
msgId,
customId,
prompt,
}: {
nonce: string;
msgId: string;
customId: string;
prompt: string;
}) {
return this.ModalSubmitApi({
nonce,
msgId,
customId,
prompt,
submitCustomId: ShortenModalSubmitID,
});
}
async DescribeImagineApi({
nonce,
msgId,
customId,
prompt,
}: {
nonce: string;
msgId: string;
customId: string;
prompt: string;
}) {
return this.ModalSubmitApi({
nonce,
msgId,
customId,
prompt,
submitCustomId: DescribeModalSubmitID,
});
}
async CustomZoomImagineApi({
nonce,
msgId,
customId,
prompt,
}: {
nonce: string;
msgId: string;
customId: string;
prompt: string;
}) {
customId = customId.replace(
"MJ::CustomZoom",
"MJ::OutpaintCustomZoomModal"
);
return this.ModalSubmitApi({
nonce,
msgId,
customId,
prompt,
submitCustomId: CustomZoomModalSubmitID,
});
}
async InfoApi(nonce?: string) {
const payload = await this.infoPayload(nonce);
return this.safeIteractions(payload);
}
async SettingsApi(nonce?: string) {
const payload = await this.settingsPayload(nonce);
return this.safeIteractions(payload);
}
async FastApi(nonce?: string) {
const payload = await this.fastPayload(nonce);
return this.safeIteractions(payload);
}
async RelaxApi(nonce?: string) {
const payload = await this.relaxPayload(nonce);
return this.safeIteractions(payload);
}
/**
*
* @param fileUrl http file path
* @returns
*/
async UploadImageByUri(fileUrl: string) {
const response = await this.config.fetch(fileUrl);
const fileData = await response.arrayBuffer();
const mimeType = response.headers.get("content-type");
const filename = fileUrl.split("/").pop() || "image.png";
const file_size = fileData.byteLength;
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 resp: DiscordImage = {
id: UploadSlot.id,
filename: UploadSlot.upload_filename.split("/").pop() || "image.png",
upload_filename: UploadSlot.upload_filename,
};
return resp;
}
async UploadImageByBole(blob: Blob, filename = nextNonce() + ".png") {
const fileData = await blob.arrayBuffer();
const mimeType = blob.type;
const file_size = fileData.byteLength;
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 resp: DiscordImage = {
id: UploadSlot.id,
filename: UploadSlot.upload_filename.split("/").pop() || "image.png",
upload_filename: UploadSlot.upload_filename,
};
return resp;
}
/**
* prepare an attachement to upload an image.
*/
private async attachments(
...files: UploadParam[]
): Promise<{ attachments: UploadSlot[] }> {
const { SalaiToken, DiscordBaseUrl, ChannelId, fetch } = this.config;
const headers = {
Authorization: SalaiToken,
"content-type": "application/json",
};
const url = new URL(
`${DiscordBaseUrl}/api/v9/channels/${ChannelId}/attachments`
);
const body = { files };
const response = await this.config.fetch(url, {
headers,
method: "POST",
body: JSON.stringify(body),
});
if (response.status === 200) {
return (await response.json()) as { attachments: UploadSlot[] };
}
const error = `Attachments return ${response.status} ${
response.statusText
} ${await response.text()}`;
throw new Error(error);
}
private async uploadImage(
slot: UploadSlot,
data: ArrayBuffer,
contentType: string
): Promise<void> {
const body = new Uint8Array(data);
const headers = { "content-type": contentType };
const response = await this.config.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(image: DiscordImage, nonce?: string) {
const payload = await this.describePayload(image, nonce);
return this.safeIteractions(payload);
}
async upImageApi(image: DiscordImage, nonce?: string) {
const { SalaiToken, DiscordBaseUrl, ChannelId, fetch } = this.config;
const payload = {
content: "",
nonce,
channel_id: ChannelId,
type: 0,
sticker_ids: [],
attachments: [image],
};
const url = new URL(
`${DiscordBaseUrl}/api/v9/channels/${ChannelId}/messages`
);
const headers = {
Authorization: SalaiToken,
"content-type": "application/json",
};
const response = await fetch(url, {
headers,
method: "POST",
body: JSON.stringify(payload),
});
return response.status;
}
}