feat: introduce dependencies upload
This commit is contained in:
		
							parent
							
								
									df46e3a0e5
								
							
						
					
					
						commit
						4348ab45dc
					
				
							
								
								
									
										134
									
								
								custom_routes.py
									
									
									
									
									
								
							
							
						
						
									
										134
									
								
								custom_routes.py
									
									
									
									
									
								
							@ -22,6 +22,8 @@ from logging.handlers import RotatingFileHandler
 | 
			
		||||
from enum import Enum
 | 
			
		||||
from urllib.parse import quote
 | 
			
		||||
import threading
 | 
			
		||||
import hashlib
 | 
			
		||||
import aiohttp
 | 
			
		||||
 | 
			
		||||
api = None
 | 
			
		||||
api_task = None
 | 
			
		||||
@ -143,6 +145,138 @@ async def comfy_deploy_run(request):
 | 
			
		||||
 | 
			
		||||
sockets = dict()
 | 
			
		||||
 | 
			
		||||
def get_comfyui_path_from_file_path(file_path):
 | 
			
		||||
    file_path_parts = file_path.split("\\")
 | 
			
		||||
 | 
			
		||||
    if file_path_parts[0] == "input":
 | 
			
		||||
        print("matching input")
 | 
			
		||||
        file_path = os.path.join(folder_paths.get_directory_by_type("input"), *file_path_parts[1:])
 | 
			
		||||
    elif file_path_parts[0] == "models":
 | 
			
		||||
        print("matching models")
 | 
			
		||||
        file_path = folder_paths.get_full_path(file_path_parts[1], os.path.join(*file_path_parts[2:]))
 | 
			
		||||
 | 
			
		||||
    print(file_path)
 | 
			
		||||
 | 
			
		||||
    return file_path
 | 
			
		||||
 | 
			
		||||
# Form ComfyUI Manager
 | 
			
		||||
def compute_sha256_checksum(filepath):
 | 
			
		||||
    filepath = get_comfyui_path_from_file_path(filepath)
 | 
			
		||||
    """Compute the SHA256 checksum of a file, in chunks"""
 | 
			
		||||
    sha256 = hashlib.sha256()
 | 
			
		||||
    with open(filepath, 'rb') as f:
 | 
			
		||||
        for chunk in iter(lambda: f.read(4096), b''):
 | 
			
		||||
            sha256.update(chunk)
 | 
			
		||||
    return sha256.hexdigest()
 | 
			
		||||
 | 
			
		||||
# This is start uploading the files to Comfy Deploy
 | 
			
		||||
@server.PromptServer.instance.routes.post('/comfyui-deploy/upload-file')
 | 
			
		||||
async def upload_file(request):
 | 
			
		||||
    data = await request.json()
 | 
			
		||||
 | 
			
		||||
    file_path = data.get("file_path")
 | 
			
		||||
 | 
			
		||||
    print("Original file path", file_path)
 | 
			
		||||
 | 
			
		||||
    file_path = get_comfyui_path_from_file_path(file_path)
 | 
			
		||||
 | 
			
		||||
    # return web.json_response({
 | 
			
		||||
    #     "error": f"File not uploaded"
 | 
			
		||||
    # }, status=500)
 | 
			
		||||
 | 
			
		||||
    token = data.get("token")
 | 
			
		||||
    get_url = data.get("url")
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        base = folder_paths.base_path
 | 
			
		||||
        file_path = os.path.join(base, file_path)
 | 
			
		||||
        
 | 
			
		||||
        if os.path.exists(file_path):
 | 
			
		||||
            file_size = os.path.getsize(file_path)
 | 
			
		||||
            file_extension = os.path.splitext(file_path)[1]
 | 
			
		||||
 | 
			
		||||
            if file_extension in ['.jpg', '.jpeg']:
 | 
			
		||||
                file_type = 'image/jpeg'
 | 
			
		||||
            elif file_extension == '.png':
 | 
			
		||||
                file_type = 'image/png'
 | 
			
		||||
            elif file_extension == '.webp':
 | 
			
		||||
                file_type = 'image/webp'
 | 
			
		||||
            else:
 | 
			
		||||
                file_type = 'application/octet-stream'  # Default to binary file type if unknown
 | 
			
		||||
        else:
 | 
			
		||||
            return web.json_response({
 | 
			
		||||
                "error": f"File not found: {file_path}"
 | 
			
		||||
            }, status=404)
 | 
			
		||||
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        return web.json_response({
 | 
			
		||||
            "error": str(e)
 | 
			
		||||
        }, status=500)
 | 
			
		||||
 | 
			
		||||
    if get_url:
 | 
			
		||||
        try:
 | 
			
		||||
            async with aiohttp.ClientSession() as session:
 | 
			
		||||
                headers = {'Authorization': f'Bearer {token}'}
 | 
			
		||||
                params = {'file_size': file_size, 'type': file_type}
 | 
			
		||||
                async with session.get(get_url, params=params, headers=headers) as response:
 | 
			
		||||
                    if response.status == 200:
 | 
			
		||||
                        content = await response.json()
 | 
			
		||||
                        upload_url = content["upload_url"]
 | 
			
		||||
 | 
			
		||||
                        with open(file_path, 'rb') as f:
 | 
			
		||||
                            headers = {
 | 
			
		||||
                                "Content-Type": file_type,
 | 
			
		||||
                                "x-amz-acl": "public-read",
 | 
			
		||||
                                "Content-Length": str(file_size)
 | 
			
		||||
                            }
 | 
			
		||||
                            async with session.put(upload_url, data=f, headers=headers) as upload_response:
 | 
			
		||||
                                if upload_response.status == 200:
 | 
			
		||||
                                    return web.json_response({
 | 
			
		||||
                                        "message": "File uploaded successfully",
 | 
			
		||||
                                        "download_url": content["download_url"]
 | 
			
		||||
                                    })
 | 
			
		||||
                                else:
 | 
			
		||||
                                    return web.json_response({
 | 
			
		||||
                                        "error": f"Failed to upload file to {upload_url}. Status code: {upload_response.status}"
 | 
			
		||||
                                    }, status=upload_response.status)
 | 
			
		||||
                    else:
 | 
			
		||||
                        return web.json_response({
 | 
			
		||||
                            "error": f"Failed to fetch data from {get_url}. Status code: {response.status}"
 | 
			
		||||
                        }, status=response.status)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            return web.json_response({
 | 
			
		||||
                "error": f"An error occurred while fetching data from {get_url}: {str(e)}"
 | 
			
		||||
            }, status=500)
 | 
			
		||||
        
 | 
			
		||||
    return web.json_response({
 | 
			
		||||
        "error": f"File not uploaded"
 | 
			
		||||
    }, status=500)
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
@server.PromptServer.instance.routes.get('/comfyui-deploy/get-file-hash')
 | 
			
		||||
async def get_file_hash(request):
 | 
			
		||||
    file_path = request.rel_url.query.get('file_path', '')
 | 
			
		||||
 | 
			
		||||
    if file_path is None:
 | 
			
		||||
        return web.json_response({
 | 
			
		||||
            "error": "file_path is required"
 | 
			
		||||
        }, status=400)
 | 
			
		||||
    
 | 
			
		||||
    try:
 | 
			
		||||
        base = folder_paths.base_path
 | 
			
		||||
        file_path = os.path.join(base, file_path)
 | 
			
		||||
        # print("file_path", file_path)
 | 
			
		||||
        file_hash = compute_sha256_checksum(
 | 
			
		||||
            file_path
 | 
			
		||||
        )
 | 
			
		||||
        return web.json_response({
 | 
			
		||||
            "file_hash": file_hash
 | 
			
		||||
        })
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        return web.json_response({
 | 
			
		||||
            "error": str(e)
 | 
			
		||||
        }, status=500)
 | 
			
		||||
 | 
			
		||||
@server.PromptServer.instance.routes.get('/comfyui-deploy/ws')
 | 
			
		||||
async def websocket_handler(request):
 | 
			
		||||
    ws = web.WebSocketResponse()
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { app } from "./app.js";
 | 
			
		||||
import { api } from "./api.js";
 | 
			
		||||
import { ComfyWidgets, LGraphNode } from "./widgets.js";
 | 
			
		||||
import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.7";
 | 
			
		||||
 | 
			
		||||
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
 | 
			
		||||
/** @type {ComfyExtension} */
 | 
			
		||||
@ -178,6 +179,71 @@ function showError(title, message) {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createDynamicUIHtml(data) {
 | 
			
		||||
  let html =
 | 
			
		||||
    '<div style="max-width: 1024px; margin: 14px auto; display: flex; flex-direction: column; gap: 24px;">';
 | 
			
		||||
  const bgcolor = "var(--comfy-input-bg)";
 | 
			
		||||
  const textColor = "var(--input-text)";
 | 
			
		||||
 | 
			
		||||
  // Custom Nodes
 | 
			
		||||
  html += `<div style="background-color: ${bgcolor}; padding: 24px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);">`;
 | 
			
		||||
  html +=
 | 
			
		||||
    '<h2 style="margin-top: 0px; font-size: 24px; font-weight: bold; margin-bottom: 16px;">Custom Nodes</h2>';
 | 
			
		||||
  Object.values(data.custom_nodes).forEach((node) => {
 | 
			
		||||
    html += `
 | 
			
		||||
          <div style="border-bottom: 1px solid #e2e8f0; padding-top: 16px;">
 | 
			
		||||
              <a href="${
 | 
			
		||||
                node.url
 | 
			
		||||
              }" target="_blank" style="font-size: 18px; font-weight: semibold; color: white; text-decoration: none;">${
 | 
			
		||||
                node.name
 | 
			
		||||
              }</a>
 | 
			
		||||
              <p style="font-size: 14px; color: #4b5563;">${node.hash}</p>
 | 
			
		||||
              ${
 | 
			
		||||
                node.warning
 | 
			
		||||
                  ? `<p style="font-size: 14px; color: #d69e2e;">${node.warning}</p>`
 | 
			
		||||
                  : ""
 | 
			
		||||
              }
 | 
			
		||||
          </div>
 | 
			
		||||
      `;
 | 
			
		||||
  });
 | 
			
		||||
  html += "</div>";
 | 
			
		||||
 | 
			
		||||
  // Models
 | 
			
		||||
  html += `<div style="background-color: ${bgcolor}; padding: 24px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);">`;
 | 
			
		||||
  html +=
 | 
			
		||||
    '<h2 style="margin-top: 0px; font-size: 24px; font-weight: bold; margin-bottom: 16px;">Models</h2>';
 | 
			
		||||
  const modelSections = ["checkpoints", "ipadapter", "clip_vision"];
 | 
			
		||||
  modelSections.forEach((section) => {
 | 
			
		||||
    html += `
 | 
			
		||||
          <div style="border-bottom: 1px solid #e2e8f0; padding-top: 8px; padding-bottom: 8px;">
 | 
			
		||||
              <h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">${
 | 
			
		||||
                section.charAt(0).toUpperCase() + section.slice(1)
 | 
			
		||||
              }</h3>
 | 
			
		||||
      `;
 | 
			
		||||
    data.models[section].forEach((item) => {
 | 
			
		||||
      html += `<p style="font-size: 14px; color: ${textColor};">${item.name}</p>`;
 | 
			
		||||
    });
 | 
			
		||||
    html += "</div>";
 | 
			
		||||
  });
 | 
			
		||||
  html += "</div>";
 | 
			
		||||
 | 
			
		||||
  // Files
 | 
			
		||||
  html += `<div style="background-color: ${bgcolor}; padding: 24px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);">`;
 | 
			
		||||
  html +=
 | 
			
		||||
    '<h2 style="margin-top: 0px; font-size: 24px; font-weight: bold; margin-bottom: 16px;">Files</h2>';
 | 
			
		||||
  html += `
 | 
			
		||||
      <div style="border-bottom: 1px solid #e2e8f0; padding-top: 8px; padding-bottom: 8px;">
 | 
			
		||||
          <h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">Images</h3>
 | 
			
		||||
  `;
 | 
			
		||||
  data.files.images.forEach((image) => {
 | 
			
		||||
    html += `<p style="font-size: 14px; color: ${textColor};">${image.name}</p>`;
 | 
			
		||||
  });
 | 
			
		||||
  html += "</div></div>";
 | 
			
		||||
 | 
			
		||||
  html += "</div>";
 | 
			
		||||
  return html;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addButton() {
 | 
			
		||||
  const menu = document.querySelector(".comfy-menu");
 | 
			
		||||
 | 
			
		||||
@ -189,8 +255,29 @@ function addButton() {
 | 
			
		||||
    /** @type {LGraph} */
 | 
			
		||||
    const graph = app.graph;
 | 
			
		||||
 | 
			
		||||
    let { endpoint, apiKey, displayName } = getData();
 | 
			
		||||
 | 
			
		||||
    if (!endpoint || !apiKey || apiKey === "" || endpoint === "") {
 | 
			
		||||
      configDialog.show();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const ok = await confirmDialog.confirm(
 | 
			
		||||
      "Confirm deployment -> " + displayName,
 | 
			
		||||
      `A new version will be deployed, are you conform? <br><br><input id="include-deps" type="checkbox" checked>Include dependence</input>`,
 | 
			
		||||
    );
 | 
			
		||||
    if (!ok) return;
 | 
			
		||||
 | 
			
		||||
    const includeDeps = document.getElementById("include-deps").checked;
 | 
			
		||||
 | 
			
		||||
    if (endpoint.endsWith("/")) {
 | 
			
		||||
      endpoint = endpoint.slice(0, -1);
 | 
			
		||||
    }
 | 
			
		||||
    loadingDialog.showLoading("Generating snapshot", "Please wait...");
 | 
			
		||||
 | 
			
		||||
    const snapshot = await fetch("/snapshot/get_current").then((x) => x.json());
 | 
			
		||||
    // console.log(snapshot);
 | 
			
		||||
    loadingDialog.close();
 | 
			
		||||
 | 
			
		||||
    if (!snapshot) {
 | 
			
		||||
      showError(
 | 
			
		||||
@ -224,32 +311,94 @@ function addButton() {
 | 
			
		||||
 | 
			
		||||
    const deployMetaNode = deployMeta[0];
 | 
			
		||||
 | 
			
		||||
    // console.log(deployMetaNode);
 | 
			
		||||
 | 
			
		||||
    const workflow_name = deployMetaNode.widgets[0].value;
 | 
			
		||||
    const workflow_id = deployMetaNode.widgets[1].value;
 | 
			
		||||
 | 
			
		||||
    console.log(workflow_name, workflow_id);
 | 
			
		||||
 | 
			
		||||
    const prompt = await app.graphToPrompt();
 | 
			
		||||
    console.log(graph);
 | 
			
		||||
    console.log(prompt);
 | 
			
		||||
    let deps = undefined;
 | 
			
		||||
 | 
			
		||||
    // const endpoint = localStorage.getItem("endpoint") ?? "";
 | 
			
		||||
    // const apiKey = localStorage.getItem("apiKey");
 | 
			
		||||
    if (includeDeps) {
 | 
			
		||||
      loadingDialog.showLoading("Fetching existing version", "Please wait...");
 | 
			
		||||
 | 
			
		||||
    const { endpoint, apiKey, displayName } = getData();
 | 
			
		||||
      const existing_workflow = await fetch(
 | 
			
		||||
        endpoint + "/api/workflow/" + workflow_id,
 | 
			
		||||
        {
 | 
			
		||||
          method: "GET",
 | 
			
		||||
          headers: {
 | 
			
		||||
            "Content-Type": "application/json",
 | 
			
		||||
            Authorization: "Bearer " + apiKey,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      )
 | 
			
		||||
        .then((x) => x.json())
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
          return {};
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    if (!endpoint || !apiKey || apiKey === "" || endpoint === "") {
 | 
			
		||||
      configDialog.show();
 | 
			
		||||
      return;
 | 
			
		||||
      loadingDialog.close();
 | 
			
		||||
 | 
			
		||||
      loadingDialog.showLoading(
 | 
			
		||||
        "Generating dependency graph",
 | 
			
		||||
        "Please wait...",
 | 
			
		||||
      );
 | 
			
		||||
      deps = await generateDependencyGraph(
 | 
			
		||||
        prompt.output,
 | 
			
		||||
        snapshot,
 | 
			
		||||
        async (file) => {
 | 
			
		||||
          console.log(file);
 | 
			
		||||
          loadingDialog.showLoading("Generating hash", file);
 | 
			
		||||
          const hash = await fetch(
 | 
			
		||||
            `/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(
 | 
			
		||||
              file,
 | 
			
		||||
            )}`,
 | 
			
		||||
          ).then((x) => x.json());
 | 
			
		||||
          loadingDialog.showLoading("Generating hash", file);
 | 
			
		||||
          console.log(hash);
 | 
			
		||||
          return hash.file_hash;
 | 
			
		||||
        },
 | 
			
		||||
        async (file, hash, prevhash) => {
 | 
			
		||||
          console.log("Uploading ", file);
 | 
			
		||||
          loadingDialog.showLoading("Uploading file", file);
 | 
			
		||||
          try {
 | 
			
		||||
            const { download_url } = await fetch(
 | 
			
		||||
              `/comfyui-deploy/upload-file`,
 | 
			
		||||
              {
 | 
			
		||||
                method: "POST",
 | 
			
		||||
                body: JSON.stringify({
 | 
			
		||||
                  file_path: file,
 | 
			
		||||
                  token: apiKey,
 | 
			
		||||
                  url: endpoint + "/api/upload-url",
 | 
			
		||||
                }),
 | 
			
		||||
              },
 | 
			
		||||
            )
 | 
			
		||||
              .then((x) => x.json())
 | 
			
		||||
              .catch(() => {
 | 
			
		||||
                loadingDialog.close();
 | 
			
		||||
                confirmDialog.confirm("Error", "Unable to upload file " + file);
 | 
			
		||||
              });
 | 
			
		||||
            loadingDialog.showLoading("Uploaded file", file);
 | 
			
		||||
            console.log(download_url);
 | 
			
		||||
            return download_url;
 | 
			
		||||
          } catch (error) {
 | 
			
		||||
            return undefined;
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        existing_workflow.dependencies,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      loadingDialog.close();
 | 
			
		||||
 | 
			
		||||
      const depsOk = await confirmDialog.confirm(
 | 
			
		||||
        "Check dependencies",
 | 
			
		||||
        // JSON.stringify(deps, null, 2),
 | 
			
		||||
        createDynamicUIHtml(deps),
 | 
			
		||||
      );
 | 
			
		||||
      if (!depsOk) return;
 | 
			
		||||
 | 
			
		||||
      console.log(deps);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const ok = await confirmDialog.confirm(
 | 
			
		||||
      "Confirm deployment -> " + displayName,
 | 
			
		||||
      "A new version will be deployed, are you conform?",
 | 
			
		||||
    );
 | 
			
		||||
    if (!ok) return;
 | 
			
		||||
    loadingDialog.showLoading("Deploying...");
 | 
			
		||||
 | 
			
		||||
    title.innerText = "Deploying...";
 | 
			
		||||
    title.style.color = "orange";
 | 
			
		||||
@ -261,6 +410,8 @@ function addButton() {
 | 
			
		||||
      endpoint = endpoint.slice(0, -1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // console.log(prompt.workflow);
 | 
			
		||||
 | 
			
		||||
    const apiRoute = endpoint + "/api/workflow";
 | 
			
		||||
    // const userId = apiKey
 | 
			
		||||
    try {
 | 
			
		||||
@ -270,6 +421,7 @@ function addButton() {
 | 
			
		||||
        workflow: prompt.workflow,
 | 
			
		||||
        workflow_api: prompt.output,
 | 
			
		||||
        snapshot: snapshot,
 | 
			
		||||
        dependencies: deps,
 | 
			
		||||
      };
 | 
			
		||||
      console.log(body);
 | 
			
		||||
      let data = await fetch(apiRoute, {
 | 
			
		||||
@ -289,6 +441,8 @@ function addButton() {
 | 
			
		||||
        data = await data.json();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      loadingDialog.close();
 | 
			
		||||
 | 
			
		||||
      title.textContent = "Done";
 | 
			
		||||
      title.style.color = "green";
 | 
			
		||||
 | 
			
		||||
@ -305,6 +459,7 @@ function addButton() {
 | 
			
		||||
        title.style.color = "white";
 | 
			
		||||
      }, 1000);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      loadingDialog.close();
 | 
			
		||||
      app.ui.dialog.show(e);
 | 
			
		||||
      console.error(e);
 | 
			
		||||
      title.textContent = "Error";
 | 
			
		||||
@ -381,7 +536,7 @@ export class InfoDialog extends ComfyDialog {
 | 
			
		||||
 | 
			
		||||
  showMessage(title, message) {
 | 
			
		||||
    this.show(`
 | 
			
		||||
      <div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
 | 
			
		||||
      <div style="width: 100%; max-width: 600px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
 | 
			
		||||
        <h3 style="margin: 0px;">${title}</h3>
 | 
			
		||||
        <label>
 | 
			
		||||
          ${message}
 | 
			
		||||
@ -437,7 +592,10 @@ export class LoadingDialog extends ComfyDialog {
 | 
			
		||||
  showLoading(title, message) {
 | 
			
		||||
    this.show(`
 | 
			
		||||
      <div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
 | 
			
		||||
        <h3 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 4px;">${title} ${this.loadingIcon}</h3>
 | 
			
		||||
        <h3 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 12px;">${title} ${
 | 
			
		||||
          this.loadingIcon
 | 
			
		||||
        }</h3>
 | 
			
		||||
          ${message ? `<label>${message}</label>` : ""}
 | 
			
		||||
        </div>
 | 
			
		||||
      `);
 | 
			
		||||
  }
 | 
			
		||||
@ -556,7 +714,7 @@ export class ConfirmDialog extends InfoDialog {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      this.callback = resolve;
 | 
			
		||||
      this.show(`
 | 
			
		||||
      <div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
 | 
			
		||||
      <div style="width: 100%; max-width: 600px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
 | 
			
		||||
        <h3 style="margin: 0px;">${title}</h3>
 | 
			
		||||
        <label>
 | 
			
		||||
          ${message}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user