UNPKG

relax-mj

Version:

Node.js client for the unofficial MidJourney API.

503 lines (479 loc) 14.6 kB
import { MJConfig, WaitMjEvent, MJMessage, LoadingHandler, WsEventMsg, MJInfo, } from "./interfaces"; import { MidjourneyApi } from "./midjourne.api"; import { VerifyHuman } from "./verify.human"; import WebSocket from "isomorphic-ws"; import { HttpsProxyAgent } from "https-proxy-agent"; export class WsMessage { ws: WebSocket; MJBotId = "936929561302675456"; private closed = false; private event: Array<{ event: string; callback: (message: any) => void }> = []; private waitMjEvents: Map<string, WaitMjEvent> = new Map(); private reconnectTime: boolean[] = []; private heartbeatInterval = 0; agent?: HttpsProxyAgent<string>; constructor(public config: MJConfig, public MJApi: MidjourneyApi) { if (this.config.ProxyUrl && this.config.ProxyUrl !== "") { this.agent = new HttpsProxyAgent(this.config.ProxyUrl); } const agent = this.agent; this.ws = new WebSocket(this.config.WsBaseUrl, { agent }); this.ws.addEventListener("open", this.open.bind(this)); } private async heartbeat(num: number) { if (this.reconnectTime[num]) return; this.heartbeatInterval++; this.ws.send( JSON.stringify({ op: 1, d: this.heartbeatInterval, }) ); await this.timeout(1000 * 40); this.heartbeat(num); } close() { this.closed = true; this.ws.close(); } //try reconnect private reconnect() { if (this.closed) return; const agent = this.agent; this.ws = new WebSocket(this.config.WsBaseUrl, { agent }); this.ws.addEventListener("open", this.open.bind(this)); } // After opening ws private async open() { const num = this.reconnectTime.length; this.log("open.time", num); this.reconnectTime.push(false); this.auth(); this.ws.addEventListener("message", (event) => { this.parseMessage(event.data as string); }); this.ws.addEventListener("error", (event) => { this.reconnectTime[num] = true; this.reconnect(); }); setTimeout(() => { this.heartbeat(num); }, 1000 * 10); } // auth private auth() { this.ws.send( JSON.stringify({ op: 2, d: { token: this.config.SalaiToken, capabilities: 8189, properties: { os: "Mac OS X", browser: "Chrome", device: "", }, compress: false, }, }) ); } async timeout(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } private async messageCreate(message: any) { // this.log("messageCreate", message); const { embeds, id, nonce, components } = message; if (nonce) { this.log("waiting start image or info or error"); this.updateMjEventIdByNonce(id, nonce); if (embeds && embeds.length > 0) { if (embeds[0].color === 16711680) { //error const error = new Error(embeds[0].description); this.EventError(id, error); return; } else if (embeds[0].color === 16776960) { //warning console.warn(embeds[0].description); } if (embeds[0].title.includes("continue")) { if (embeds[0].description.includes("verify you're human")) { //verify human await this.verifyHuman(message); return; } } if (embeds[0].title.includes("Invalid")) { //error const error = new Error(embeds[0].description); this.EventError(id, error); return; } } } //finished image if (!nonce && components.length > 0) { this.log("finished image"); this.done(message); return; } this.messageUpdate(message); } private messageUpdate(message: any) { this.log("messageUpdate", message); const { content, embeds, interaction, nonce, id } = message; this.log("coneent===", content, nonce); if(content.includes("(Stop)")) { const error = new Error("(Stop)"); this.emitImage(nonce, { error }); return; } if (embeds && embeds.length > 0) { this.log("embeds===", embeds); const description = embeds?.[0]?.description; this.log("description===", description); if (description && description.includes("blocked by our image filters")) { const error = new Error(description); this.emitImage(nonce, { error }); return; } if (description && description.includes("Sorry! Our AI moderators")) { const error = new Error(description); this.emitImage(nonce, { error }); return; } if (description && description.includes("Please check that your URL")) { const error = new Error(description); this.emitImage(nonce, { error }); return; } } if (content === "") { //describe if (interaction.name === "describe" && !nonce) { this.emitDescribe(id, embeds[0].description); } if (embeds && embeds.length > 0 && embeds[0].color === 0) { this.log(embeds[0].title, embeds[0].description); //maybe info if (embeds[0].title.includes("info")) { this.emit("info", embeds[0].description); return; } } return; } this.log("coneent2222==="); this.processingImage(message); } private processingImage(message: any) { const { content, id, attachments } = message; const event = this.getEventById(id); if (!event) { return; } event.prompt = content; //not image if (!attachments || attachments.length === 0) { return; } const MJmsg: MJMessage = { uri: attachments[0].url, content: content, progress: this.content2progress(content), }; const eventMsg: WsEventMsg = { message: MJmsg, }; this.emitImage(event.nonce, eventMsg); } // parse message from ws private parseMessage(data: string) { const msg = JSON.parse(data); if (msg.t === null || msg.t === "READY_SUPPLEMENTAL") return; if (msg.t === "READY") { this.emit("ready", null); return; } if (!(msg.t === "MESSAGE_CREATE" || msg.t === "MESSAGE_UPDATE")) return; const message = msg.d; const { channel_id, content, id, nonce, author } = message; if (!(author && author.id === this.MJBotId)) return; if (channel_id !== this.config.ChannelId) return; this.log("has message", msg.t, content, nonce, id); if (msg.t === "MESSAGE_CREATE") { this.messageCreate(message); return; } if (msg.t === "MESSAGE_UPDATE") { this.messageUpdate(message); return; } } private async verifyHuman(message: any) { const { HuggingFaceToken } = this.config; if (HuggingFaceToken === "" || !HuggingFaceToken) { this.log("HuggingFaceToken is empty"); return; } const { embeds, components } = message; const uri = embeds[0].image.url; const categories = components[0].components; const classify = categories.map((c: any) => c.label); const verifyClient = new VerifyHuman(HuggingFaceToken); const category = await verifyClient.verify(uri, classify); if (category) { const custom_id = categories.find( (c: any) => c.label === category ).custom_id; const httpStatus = await this.MJApi.ClickBtnApi(custom_id, message.id); this.log("verifyHumanApi", httpStatus, custom_id, message.id); // this.log("verify success", category); } } private EventError(id: string, error: Error) { const event = this.getEventById(id); if (!event) { return; } const eventMsg: WsEventMsg = { error, }; this.emit(event.nonce, eventMsg); } private done(message: any) { const { content, id, attachments } = message; const MJmsg: MJMessage = { id, hash: this.uriToHash(attachments[0].url), progress: "done", uri: attachments[0].url, content: content, }; this.filterMessages(MJmsg); return; } protected content2progress(content: string) { const regex = /\(([^)]+)\)/; // matches the value inside the first parenthesis const match = content.match(regex); let progress = ""; if (match) { progress = match[1]; } return progress; } content2prompt(content: string | undefined) { if (!content) return ""; const pattern = /\*\*(.*?)\*\*/; // Match **middle content const matches = content.match(pattern); if (matches && matches.length > 1) { return matches[1]; // Get the matched content } else { this.log("No match found.", content); return content; } } private filterMessages(MJmsg: MJMessage) { const event = this.getEventByContent(MJmsg.content); if (!event) { this.log("FilterMessages not found", MJmsg, this.waitMjEvents); return; } const eventMsg: WsEventMsg = { message: MJmsg, }; this.emitImage(event.nonce, eventMsg); } private getEventByContent(content: string) { const prompt = this.content2prompt(content); for (const [key, value] of this.waitMjEvents.entries()) { if (prompt === this.content2prompt(value.prompt)) { return value; } } } private getEventById(id: string) { for (const [key, value] of this.waitMjEvents.entries()) { if (value.id === id) { return value; } } } private updateMjEventIdByNonce(id: string, nonce: string) { if (nonce === "" || id === "") return; let event = this.waitMjEvents.get(nonce); if (!event) return; event.id = id; this.log("updateMjEventIdByNonce success", this.waitMjEvents.get(nonce)); } uriToHash(uri: string) { return uri.split("_").pop()?.split(".")[0] ?? ""; } protected async log(...args: any[]) { this.config.Debug && console.info(...args, new Date().toISOString()); } emit(event: string, message: any) { this.event .filter((e) => e.event === event) .forEach((e) => e.callback(message)); } private emitImage(type: string, message: WsEventMsg) { this.emit(type, message); } private emitDescribe(id: string, data: any) { const event = this.getEventById(id); if (!event) return; this.emit(event.nonce, data); } on(event: string, callback: (message: any) => void) { this.event.push({ event, callback }); } once(event: string, callback: (message: any) => void) { const once = (message: any) => { this.remove(event, once); callback(message); }; this.event.push({ event, callback: once }); } remove(event: string, callback: (message: any) => void) { this.event = this.event.filter( (e) => e.event !== event && e.callback !== callback ); } removeEvent(event: string) { this.event = this.event.filter((e) => e.event !== event); } onceInfo(callback: (message: any) => void) { const once = (message: any) => { this.remove("info", once); callback(message); }; this.event.push({ event: "info", callback: once }); } onceDescribe(nonce: string, callback: (data: any) => void) { const once = (message: any) => { this.remove(nonce, once); this.removeWaitMjEvent(nonce); callback(message); }; this.waitMjEvents.set(nonce, { nonce }); this.event.push({ event: nonce, callback: once }); } removeInfo(callback: (message: any) => void) { this.remove("info", callback); } private removeWaitMjEvent(nonce: string) { this.waitMjEvents.delete(nonce); } onceImage(nonce: string, callback: (data: WsEventMsg) => void) { const once = (data: WsEventMsg) => { const { message, error } = data; if (error || (message && message.progress === "done")) { this.remove(nonce, once); this.removeWaitMjEvent(nonce); } callback(data); }; this.waitMjEvents.set(nonce, { nonce }); this.event.push({ event: nonce, callback: once }); } async waitImageMessage(nonce: string, loading?: LoadingHandler) { return new Promise<MJMessage | null>((resolve, reject) => { this.onceImage(nonce, ({ message, error }) => { if (error) { reject(error); return; } if (message && message.progress === "done") { resolve(message); return; } message && loading && loading(message.uri, message.progress || ""); }); }); } async waitDescribe(nonce: string) { return new Promise<string[] | null>((resolve) => { this.onceDescribe(nonce, (message) => { const data = message.split("\n\n"); resolve(data); }); }); } async waitInfo() { return new Promise<MJInfo | null>((resolve, reject) => { this.onceInfo((message) => { resolve(this.msg2Info(message)); }); }); } msg2Info(msg: string) { let jsonResult: MJInfo = { subscription: "", jobMode: "", visibilityMode: "", fastTimeRemaining: "", lifetimeUsage: "", relaxedUsage: "", queuedJobsFast: "", queuedJobsRelax: "", runningJobs: "", }; // Initialize jsonResult with empty object msg.split("\n").forEach(function (line) { const colonIndex = line.indexOf(":"); if (colonIndex > -1) { const key = line.substring(0, colonIndex).trim().replaceAll("**", ""); const value = line.substring(colonIndex + 1).trim(); switch (key) { case "Subscription": jsonResult.subscription = value; break; case "Job Mode": jsonResult.jobMode = value; break; case "Visibility Mode": jsonResult.visibilityMode = value; break; case "Fast Time Remaining": jsonResult.fastTimeRemaining = value; break; case "Lifetime Usage": jsonResult.lifetimeUsage = value; break; case "Relaxed Usage": jsonResult.relaxedUsage = value; break; case "Queued Jobs (fast)": jsonResult.queuedJobsFast = value; break; case "Queued Jobs (relax)": jsonResult.queuedJobsRelax = value; break; case "Running Jobs": jsonResult.runningJobs = value; break; default: // Do nothing } } }); return jsonResult; } }