refactor: split http endpoints from main api class
This commit is contained in:
parent
2eebe2378f
commit
c2065afbc4
@ -2,7 +2,6 @@ import 'dart:io';
|
||||
|
||||
import 'package:comfyui_api_sdk/comfyui_api_sdk.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:comfyui_api_sdk/src/prompt_builder.dart';
|
||||
|
||||
bool jobFinished = false;
|
||||
|
||||
|
223
lib/src/_http_endpoints.dart
Normal file
223
lib/src/_http_endpoints.dart
Normal file
@ -0,0 +1,223 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import 'exceptions/comfyui_api_exception.dart';
|
||||
import 'models/history_response.dart';
|
||||
import 'models/checkpoint.dart';
|
||||
import 'models/vae.dart';
|
||||
import 'models/lora.dart';
|
||||
import 'models/submit_prompt_response.dart';
|
||||
import 'websocket_manager.dart'; // Needed for submitPrompt trigger
|
||||
|
||||
/// Internal helper class for handling ComfyUI HTTP API endpoints.
|
||||
class ComfyApiHttpEndpoints {
|
||||
final String host;
|
||||
final http.Client _httpClient;
|
||||
final Logger logger;
|
||||
final WebSocketManager _webSocketManager; // Needed for submitPrompt trigger
|
||||
|
||||
ComfyApiHttpEndpoints({
|
||||
required this.host,
|
||||
required http.Client httpClient,
|
||||
required this.logger,
|
||||
required WebSocketManager webSocketManager,
|
||||
}) : _httpClient = httpClient,
|
||||
_webSocketManager = webSocketManager;
|
||||
|
||||
/// Gets the current queue status
|
||||
Future<Map<String, dynamic>> getQueue() async {
|
||||
final response = await _httpClient.get(Uri.parse('$host/queue'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
/// Gets the history of the queue
|
||||
Future<HistoryResponse> getHistory({int maxItems = 64}) async {
|
||||
final response = await _httpClient
|
||||
.get(Uri.parse('$host/api/history?max_items=$maxItems'));
|
||||
_validateResponse(response);
|
||||
return HistoryResponse.fromJson(jsonDecode(response.body));
|
||||
}
|
||||
|
||||
/// Gets image data by filename
|
||||
Future<List<int>> getImage(String filename) async {
|
||||
final response =
|
||||
await _httpClient.get(Uri.parse('$host/api/view?filename=$filename'));
|
||||
_validateResponse(response);
|
||||
return response.bodyBytes;
|
||||
}
|
||||
|
||||
/// Gets a list of all available models
|
||||
Future<Map<String, dynamic>> getModels() async {
|
||||
final response =
|
||||
await _httpClient.get(Uri.parse('$host/api/experiment/models'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
/// Gets a list of checkpoints
|
||||
Future<List<Checkpoint>> getCheckpoints() async {
|
||||
final response = await _httpClient
|
||||
.get(Uri.parse('$host/api/experiment/models/checkpoints'));
|
||||
_validateResponse(response);
|
||||
final List<dynamic> jsonData = jsonDecode(response.body);
|
||||
return jsonData.map((item) => Checkpoint.fromJson(item)).toList();
|
||||
}
|
||||
|
||||
/// Gets details for a specific checkpoint
|
||||
Future<Map<String, dynamic>> getCheckpointDetails(
|
||||
String pathAndFileName) async {
|
||||
final response = await _httpClient.get(Uri.parse(
|
||||
'$host/api/view_metadata/checkpoints?filename=$pathAndFileName'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
/// Gets a list of LoRAs
|
||||
Future<List<Lora>> getLoras() async {
|
||||
final response =
|
||||
await _httpClient.get(Uri.parse('$host/api/experiment/models/loras'));
|
||||
_validateResponse(response);
|
||||
final List<dynamic> jsonData = jsonDecode(response.body);
|
||||
return jsonData.map((item) => Lora.fromJson(item)).toList();
|
||||
}
|
||||
|
||||
/// Gets details for a specific LoRA
|
||||
Future<Map<String, dynamic>> getLoraDetails(String pathAndFileName) async {
|
||||
final response = await _httpClient.get(
|
||||
Uri.parse('$host/api/view_metadata/loras?filename=$pathAndFileName'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
/// Gets a list of VAEs
|
||||
Future<List<Vae>> getVaes() async {
|
||||
final response =
|
||||
await _httpClient.get(Uri.parse('$host/api/experiment/models/vae'));
|
||||
_validateResponse(response);
|
||||
final List<dynamic> jsonData = jsonDecode(response.body);
|
||||
return jsonData.map((item) => Vae.fromJson(item)).toList();
|
||||
}
|
||||
|
||||
/// Gets details for a specific VAE
|
||||
Future<Map<String, dynamic>> getVaeDetails(String pathAndFileName) async {
|
||||
final response = await _httpClient.get(
|
||||
Uri.parse('$host/api/view_metadata/vae?filename=$pathAndFileName'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
/// Gets a list of upscale models
|
||||
Future<List<dynamic>> getUpscaleModels() async {
|
||||
final response = await _httpClient
|
||||
.get(Uri.parse('$host/api/experiment/models/upscale_models'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
/// Gets details for a specific upscale model
|
||||
Future<Map<String, dynamic>> getUpscaleModelDetails(
|
||||
String pathAndFileName) async {
|
||||
final response = await _httpClient.get(Uri.parse(
|
||||
'$host/api/view_metadata/upscale_models?filename=$pathAndFileName'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
/// Gets a list of embeddings
|
||||
Future<List<dynamic>> getEmbeddings() async {
|
||||
final response = await _httpClient
|
||||
.get(Uri.parse('$host/api/experiment/models/embeddings'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
/// Gets details for a specific embedding
|
||||
Future<Map<String, dynamic>> getEmbeddingDetails(
|
||||
String pathAndFileName) async {
|
||||
final response = await _httpClient.get(Uri.parse(
|
||||
'$host/api/view_metadata/embeddings?filename=$pathAndFileName'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
/// Gets information about all available objects (nodes)
|
||||
Future<Map<String, dynamic>> getObjectInfo() async {
|
||||
final response = await _httpClient.get(Uri.parse('$host/api/object_info'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
/// Gets a list of possible samplers from the object info
|
||||
Future<List<String>> getKSamplers() async {
|
||||
final objectInfo = await getObjectInfo();
|
||||
if (objectInfo.containsKey('KSampler') &&
|
||||
objectInfo['KSampler']['input']['required']
|
||||
.containsKey('sampler_name')) {
|
||||
return List<String>.from(
|
||||
objectInfo['KSampler']['input']['required']['sampler_name'][0]);
|
||||
}
|
||||
throw ComfyUiApiException(
|
||||
statusCode: 500,
|
||||
message: 'KSampler information not found in object info.');
|
||||
}
|
||||
|
||||
/// Gets a list of possible schedulers from the object info
|
||||
Future<List<String>> getSchedulers() async {
|
||||
final objectInfo = await getObjectInfo();
|
||||
if (objectInfo.containsKey('KSampler') &&
|
||||
objectInfo['KSampler']['input']['required'].containsKey('scheduler')) {
|
||||
return List<String>.from(
|
||||
objectInfo['KSampler']['input']['required']['scheduler'][0]);
|
||||
}
|
||||
throw ComfyUiApiException(
|
||||
statusCode: 500,
|
||||
message: 'Scheduler information not found in object info.');
|
||||
}
|
||||
|
||||
/// Submits a prompt (workflow) to generate an image
|
||||
Future<SubmitPromptResponse> submitPrompt(Map<String, dynamic> prompt) async {
|
||||
final response = await _httpClient.post(
|
||||
Uri.parse('$host/api/prompt'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(prompt),
|
||||
);
|
||||
_validateResponse(response);
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
// Trigger onPromptStart event if prompt_id exists
|
||||
if (responseData.containsKey('prompt_id')) {
|
||||
final promptId = responseData['prompt_id'];
|
||||
_webSocketManager.triggerOnPromptStart(promptId);
|
||||
}
|
||||
|
||||
return SubmitPromptResponse.fromJson(responseData);
|
||||
}
|
||||
|
||||
Future<bool> interrupt({bool clearQueue = false}) async {
|
||||
if (clearQueue) {
|
||||
final clearQueueResponse = await _httpClient.post(
|
||||
Uri.parse('$host/api/queue'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'clear': true}),
|
||||
);
|
||||
_validateResponse(clearQueueResponse);
|
||||
}
|
||||
|
||||
final response = await _httpClient.post(Uri.parse('$host/api/interrupt'));
|
||||
_validateResponse(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Validates HTTP response and throws an exception if needed
|
||||
void _validateResponse(http.Response response) {
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw ComfyUiApiException(
|
||||
statusCode: response.statusCode,
|
||||
message: 'API request failed: ${response.body}');
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
@ -7,7 +6,6 @@ import 'package:logger/logger.dart';
|
||||
import 'models/websocket_event.dart';
|
||||
import 'models/progress_event.dart';
|
||||
import 'models/execution_event.dart';
|
||||
import 'exceptions/comfyui_api_exception.dart';
|
||||
import 'types/callback_types.dart';
|
||||
import 'models/history_response.dart';
|
||||
import 'models/checkpoint.dart';
|
||||
@ -15,14 +13,16 @@ import 'models/vae.dart';
|
||||
import 'models/lora.dart';
|
||||
import 'websocket_manager.dart';
|
||||
import 'models/submit_prompt_response.dart';
|
||||
import '_http_endpoints.dart'; // Import the new helper class
|
||||
|
||||
/// A Dart SDK for interacting with the ComfyUI API
|
||||
class ComfyUiApi {
|
||||
final String host;
|
||||
final String clientId;
|
||||
final http.Client _httpClient;
|
||||
final http.Client _httpClient; // Keep for potential direct use or disposal
|
||||
final WebSocketManager _webSocketManager;
|
||||
final Logger logger;
|
||||
late final ComfyApiHttpEndpoints _httpEndpoints; // Add the helper instance
|
||||
|
||||
/// Creates a new ComfyUI API client
|
||||
ComfyUiApi({
|
||||
@ -32,7 +32,14 @@ class ComfyUiApi {
|
||||
http.Client? httpClient,
|
||||
}) : _httpClient = httpClient ?? http.Client(),
|
||||
_webSocketManager = WebSocketManager(host: host, clientId: clientId),
|
||||
logger = logger ?? Logger();
|
||||
logger = logger ?? Logger() {
|
||||
_httpEndpoints = ComfyApiHttpEndpoints(
|
||||
host: host,
|
||||
httpClient: _httpClient,
|
||||
logger: this.logger, // Use the instance logger
|
||||
webSocketManager: _webSocketManager,
|
||||
);
|
||||
}
|
||||
|
||||
/// Expose WebSocketManager streams and methods
|
||||
Stream<WebSocketEvent> get events => _webSocketManager.events;
|
||||
@ -45,7 +52,6 @@ class ComfyUiApi {
|
||||
Stream<void> get executionInterruptedStream =>
|
||||
_webSocketManager.executionInterruptedStream;
|
||||
|
||||
/// Register a callback for when the executing node changes
|
||||
void onExecutingNodeChanged(void Function(int nodeId) callback) {
|
||||
_webSocketManager.executingNodeStream.listen(callback);
|
||||
}
|
||||
@ -78,197 +84,72 @@ class ComfyUiApi {
|
||||
_httpClient.close();
|
||||
}
|
||||
|
||||
// --- HTTP Endpoint Methods (Delegated) ---
|
||||
|
||||
/// Gets the current queue status
|
||||
Future<Map<String, dynamic>> getQueue() async {
|
||||
final response = await _httpClient.get(Uri.parse('$host/queue'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Future<Map<String, dynamic>> getQueue() => _httpEndpoints.getQueue();
|
||||
|
||||
/// Gets the history of the queue
|
||||
Future<HistoryResponse> getHistory({int maxItems = 64}) async {
|
||||
final response = await _httpClient
|
||||
.get(Uri.parse('$host/api/history?max_items=$maxItems'));
|
||||
_validateResponse(response);
|
||||
return HistoryResponse.fromJson(jsonDecode(response.body));
|
||||
}
|
||||
Future<HistoryResponse> getHistory({int maxItems = 64}) =>
|
||||
_httpEndpoints.getHistory(maxItems: maxItems);
|
||||
|
||||
/// Gets image data by filename
|
||||
Future<List<int>> getImage(String filename) async {
|
||||
final response =
|
||||
await _httpClient.get(Uri.parse('$host/api/view?filename=$filename'));
|
||||
_validateResponse(response);
|
||||
return response.bodyBytes;
|
||||
}
|
||||
Future<List<int>> getImage(String filename) =>
|
||||
_httpEndpoints.getImage(filename);
|
||||
|
||||
/// Gets a list of all available models
|
||||
Future<Map<String, dynamic>> getModels() async {
|
||||
final response =
|
||||
await _httpClient.get(Uri.parse('$host/api/experiment/models'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Future<Map<String, dynamic>> getModels() => _httpEndpoints.getModels();
|
||||
|
||||
/// Gets a list of checkpoints
|
||||
Future<List<Checkpoint>> getCheckpoints() async {
|
||||
final response = await _httpClient
|
||||
.get(Uri.parse('$host/api/experiment/models/checkpoints'));
|
||||
_validateResponse(response);
|
||||
final List<dynamic> jsonData = jsonDecode(response.body);
|
||||
return jsonData.map((item) => Checkpoint.fromJson(item)).toList();
|
||||
}
|
||||
Future<List<Checkpoint>> getCheckpoints() => _httpEndpoints.getCheckpoints();
|
||||
|
||||
/// Gets details for a specific checkpoint
|
||||
Future<Map<String, dynamic>> getCheckpointDetails(
|
||||
String pathAndFileName) async {
|
||||
final response = await _httpClient.get(Uri.parse(
|
||||
'$host/api/view_metadata/checkpoints?filename=$pathAndFileName'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Future<Map<String, dynamic>> getCheckpointDetails(String pathAndFileName) =>
|
||||
_httpEndpoints.getCheckpointDetails(pathAndFileName);
|
||||
|
||||
/// Gets a list of LoRAs
|
||||
Future<List<Lora>> getLoras() async {
|
||||
final response =
|
||||
await _httpClient.get(Uri.parse('$host/api/experiment/models/loras'));
|
||||
_validateResponse(response);
|
||||
final List<dynamic> jsonData = jsonDecode(response.body);
|
||||
return jsonData.map((item) => Lora.fromJson(item)).toList();
|
||||
}
|
||||
Future<List<Lora>> getLoras() => _httpEndpoints.getLoras();
|
||||
|
||||
/// Gets details for a specific LoRA
|
||||
Future<Map<String, dynamic>> getLoraDetails(String pathAndFileName) async {
|
||||
final response = await _httpClient.get(
|
||||
Uri.parse('$host/api/view_metadata/loras?filename=$pathAndFileName'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Future<Map<String, dynamic>> getLoraDetails(String pathAndFileName) =>
|
||||
_httpEndpoints.getLoraDetails(pathAndFileName);
|
||||
|
||||
/// Gets a list of VAEs
|
||||
Future<List<Vae>> getVaes() async {
|
||||
final response =
|
||||
await _httpClient.get(Uri.parse('$host/api/experiment/models/vae'));
|
||||
_validateResponse(response);
|
||||
final List<dynamic> jsonData = jsonDecode(response.body);
|
||||
return jsonData.map((item) => Vae.fromJson(item)).toList();
|
||||
}
|
||||
Future<List<Vae>> getVaes() => _httpEndpoints.getVaes();
|
||||
|
||||
/// Gets details for a specific VAE
|
||||
Future<Map<String, dynamic>> getVaeDetails(String pathAndFileName) async {
|
||||
final response = await _httpClient.get(
|
||||
Uri.parse('$host/api/view_metadata/vae?filename=$pathAndFileName'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Future<Map<String, dynamic>> getVaeDetails(String pathAndFileName) =>
|
||||
_httpEndpoints.getVaeDetails(pathAndFileName);
|
||||
|
||||
/// Gets a list of upscale models
|
||||
Future<List<dynamic>> getUpscaleModels() async {
|
||||
final response = await _httpClient
|
||||
.get(Uri.parse('$host/api/experiment/models/upscale_models'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Future<List<dynamic>> getUpscaleModels() => _httpEndpoints.getUpscaleModels();
|
||||
|
||||
/// Gets details for a specific upscale model
|
||||
Future<Map<String, dynamic>> getUpscaleModelDetails(
|
||||
String pathAndFileName) async {
|
||||
final response = await _httpClient.get(Uri.parse(
|
||||
'$host/api/view_metadata/upscale_models?filename=$pathAndFileName'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Future<Map<String, dynamic>> getUpscaleModelDetails(String pathAndFileName) =>
|
||||
_httpEndpoints.getUpscaleModelDetails(pathAndFileName);
|
||||
|
||||
/// Gets a list of embeddings
|
||||
Future<List<dynamic>> getEmbeddings() async {
|
||||
final response = await _httpClient
|
||||
.get(Uri.parse('$host/api/experiment/models/embeddings'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Future<List<dynamic>> getEmbeddings() => _httpEndpoints.getEmbeddings();
|
||||
|
||||
/// Gets details for a specific embedding
|
||||
Future<Map<String, dynamic>> getEmbeddingDetails(
|
||||
String pathAndFileName) async {
|
||||
final response = await _httpClient.get(Uri.parse(
|
||||
'$host/api/view_metadata/embeddings?filename=$pathAndFileName'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Future<Map<String, dynamic>> getEmbeddingDetails(String pathAndFileName) =>
|
||||
_httpEndpoints.getEmbeddingDetails(pathAndFileName);
|
||||
|
||||
/// Gets information about all available objects (nodes)
|
||||
Future<Map<String, dynamic>> getObjectInfo() async {
|
||||
final response = await _httpClient.get(Uri.parse('$host/api/object_info'));
|
||||
_validateResponse(response);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Future<Map<String, dynamic>> getObjectInfo() =>
|
||||
_httpEndpoints.getObjectInfo();
|
||||
|
||||
/// Gets a list of possible samplers from the object info
|
||||
Future<List<String>> getKSamplers() async {
|
||||
final objectInfo = await getObjectInfo();
|
||||
if (objectInfo.containsKey('KSampler') &&
|
||||
objectInfo['KSampler']['input']['required']
|
||||
.containsKey('sampler_name')) {
|
||||
return List<String>.from(
|
||||
objectInfo['KSampler']['input']['required']['sampler_name'][0]);
|
||||
}
|
||||
throw ComfyUiApiException(
|
||||
statusCode: 500,
|
||||
message: 'KSampler information not found in object info.');
|
||||
}
|
||||
Future<List<String>> getKSamplers() => _httpEndpoints.getKSamplers();
|
||||
|
||||
/// Gets a list of possible schedulers from the object info
|
||||
Future<List<String>> getSchedulers() async {
|
||||
final objectInfo = await getObjectInfo();
|
||||
if (objectInfo.containsKey('KSampler') &&
|
||||
objectInfo['KSampler']['input']['required'].containsKey('scheduler')) {
|
||||
return List<String>.from(
|
||||
objectInfo['KSampler']['input']['required']['scheduler'][0]);
|
||||
}
|
||||
throw ComfyUiApiException(
|
||||
statusCode: 500,
|
||||
message: 'Scheduler information not found in object info.');
|
||||
}
|
||||
Future<List<String>> getSchedulers() => _httpEndpoints.getSchedulers();
|
||||
|
||||
/// Submits a prompt (workflow) to generate an image
|
||||
Future<SubmitPromptResponse> submitPrompt(Map<String, dynamic> prompt) async {
|
||||
final response = await _httpClient.post(
|
||||
Uri.parse('$host/api/prompt'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(prompt),
|
||||
);
|
||||
_validateResponse(response);
|
||||
final responseData = jsonDecode(response.body);
|
||||
Future<SubmitPromptResponse> submitPrompt(Map<String, dynamic> prompt) =>
|
||||
_httpEndpoints.submitPrompt(prompt);
|
||||
|
||||
// Trigger onPromptStart event if prompt_id exists
|
||||
if (responseData.containsKey('prompt_id')) {
|
||||
final promptId = responseData['prompt_id'];
|
||||
_webSocketManager.triggerOnPromptStart(promptId);
|
||||
}
|
||||
|
||||
return SubmitPromptResponse.fromJson(responseData);
|
||||
}
|
||||
|
||||
Future<bool> interrupt({bool clearQueue = false}) async {
|
||||
if (clearQueue) {
|
||||
final clearQueueResponse = await _httpClient.post(
|
||||
Uri.parse('$host/api/queue'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'clear': true}),
|
||||
);
|
||||
_validateResponse(clearQueueResponse);
|
||||
}
|
||||
|
||||
final response = await _httpClient.post(Uri.parse('$host/api/interrupt'));
|
||||
_validateResponse(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Validates HTTP response and throws an exception if needed
|
||||
void _validateResponse(http.Response response) {
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw ComfyUiApiException(
|
||||
statusCode: response.statusCode,
|
||||
message: 'API request failed: ${response.body}');
|
||||
}
|
||||
}
|
||||
/// Interrupts the current execution and optionally clears the queue
|
||||
Future<bool> interrupt({bool clearQueue = false}) =>
|
||||
_httpEndpoints.interrupt(clearQueue: clearQueue);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user