2025-07-23 17:42:20 +08:00
package handler
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"sync"
"time"
"github.com/chaitin/MonkeyCode/backend/config"
"github.com/chaitin/MonkeyCode/backend/db"
"github.com/chaitin/MonkeyCode/backend/domain"
socketio "github.com/doquangtan/socket.io/v4"
)
type FileUpdateData struct {
2025-07-25 19:02:52 +08:00
ID string ` json:"id" `
FilePath string ` json:"filePath" `
Hash string ` json:"hash" `
Event string ` json:"event" `
Content string ` json:"content,omitempty" `
PreviousHash string ` json:"previousHash,omitempty" `
Timestamp int64 ` json:"timestamp" `
ApiKey string ` json:"apiKey,omitempty" `
WorkspacePath string ` json:"workspacePath,omitempty" `
2025-07-23 17:42:20 +08:00
}
type AckResponse struct {
ID string ` json:"id" `
Status string ` json:"status" `
Message string ` json:"message,omitempty" `
}
type TestPingData struct {
Timestamp int64 ` json:"timestamp" `
Message string ` json:"message" `
SocketID string ` json:"socketId" `
}
type HeartbeatData struct {
Type string ` json:"type" `
Timestamp int64 ` json:"timestamp" `
ClientID string ` json:"clientId" `
}
type SocketHandler struct {
config * config . Config
logger * slog . Logger
workspaceService domain . WorkspaceFileUsecase
2025-07-25 19:02:52 +08:00
workspaceUsecase domain . WorkspaceUsecase
2025-07-23 17:42:20 +08:00
userService domain . UserUsecase
io * socketio . Io
mu sync . Mutex
2025-07-25 19:02:52 +08:00
workspaceCache map [ string ] * domain . Workspace
cacheMutex sync . RWMutex
2025-07-23 17:42:20 +08:00
}
2025-07-25 19:02:52 +08:00
func NewSocketHandler ( config * config . Config , logger * slog . Logger , workspaceService domain . WorkspaceFileUsecase , workspaceUsecase domain . WorkspaceUsecase , userService domain . UserUsecase ) ( * SocketHandler , error ) {
2025-07-23 17:42:20 +08:00
// 创建Socket.IO服务器
io := socketio . New ( )
handler := & SocketHandler {
config : config ,
logger : logger ,
workspaceService : workspaceService ,
2025-07-25 19:02:52 +08:00
workspaceUsecase : workspaceUsecase ,
2025-07-23 17:42:20 +08:00
userService : userService ,
io : io ,
mu : sync . Mutex { } , // 初始化互斥锁
2025-07-25 19:02:52 +08:00
workspaceCache : make ( map [ string ] * domain . Workspace ) ,
cacheMutex : sync . RWMutex { } ,
2025-07-23 17:42:20 +08:00
}
// 设置事件处理器
handler . setupEventHandlers ( )
return handler , nil
}
func ( h * SocketHandler ) setupEventHandlers ( ) {
h . io . OnConnection ( h . handleConnection )
}
func ( h * SocketHandler ) handleConnection ( socket * socketio . Socket ) {
h . logger . Info ( "Client connected" , "socketId" , socket . Id )
h . sendServerStatus ( socket , "ready" , "Server is ready to receive updates" )
// 注册事件处理器
h . registerDisconnectHandler ( socket )
h . registerFileUpdateHandler ( socket )
h . registerTestPingHandler ( socket )
h . registerHeartbeatHandler ( socket )
h . registerWorkspaceStatsHandler ( socket )
}
func ( h * SocketHandler ) registerDisconnectHandler ( socket * socketio . Socket ) {
socket . On ( "disconnect" , func ( data * socketio . EventPayload ) {
reason := "unknown"
if len ( data . Data ) > 0 {
if r , ok := data . Data [ 0 ] . ( string ) ; ok {
reason = r
}
}
h . logger . Info ( "Client disconnected" , "socketId" , socket . Id , "reason" , reason )
} )
}
func ( h * SocketHandler ) registerFileUpdateHandler ( socket * socketio . Socket ) {
socket . On ( "file:update" , func ( data * socketio . EventPayload ) {
h . logger . Debug ( "Received file:update event" ,
"socketId" , socket . Id ,
"eventName" , data . Name ,
"dataCount" , len ( data . Data ) ,
"hasAck" , data . Ack != nil )
if len ( data . Data ) == 0 {
h . sendErrorACK ( data , "No data provided" )
return
}
h . processFileUpdateData ( socket , data )
} )
}
func ( h * SocketHandler ) processFileUpdateData ( socket * socketio . Socket , data * socketio . EventPayload ) {
switch v := data . Data [ 0 ] . ( type ) {
case map [ string ] interface { } :
response := h . handleFileUpdateFromObject ( socket , * data )
h . sendACKWithLock ( data , response )
case string :
response := h . handleFileUpdate ( socket , v )
h . sendACKWithLock ( data , response )
default :
h . logger . Error ( "Data is neither string nor object" ,
"socketId" , socket . Id ,
"dataType" , fmt . Sprintf ( "%T" , v ) )
h . sendErrorACK ( data , "Invalid data format - expected string or object" )
}
}
func ( h * SocketHandler ) registerTestPingHandler ( socket * socketio . Socket ) {
socket . On ( "test:ping" , func ( data * socketio . EventPayload ) {
h . logger . Info ( "Received test:ping event" ,
"socketId" , socket . Id ,
"hasAck" , data . Ack != nil ,
"dataCount" , len ( data . Data ) )
if len ( data . Data ) > 0 {
if dataStr , ok := data . Data [ 0 ] . ( string ) ; ok {
h . handleTestPing ( socket , dataStr )
}
}
} )
}
func ( h * SocketHandler ) registerHeartbeatHandler ( socket * socketio . Socket ) {
socket . On ( "heartbeat" , func ( data * socketio . EventPayload ) {
if len ( data . Data ) == 0 {
h . sendErrorACK ( data , "No heartbeat data" )
return
}
if dataStr , ok := data . Data [ 0 ] . ( string ) ; ok {
response := h . handleHeartbeat ( socket , dataStr )
h . logger . Debug ( "Sending heartbeat ACK" ,
"socketId" , socket . Id ,
"response" , response )
if data . Ack != nil {
data . Ack ( response )
}
}
} )
}
func ( h * SocketHandler ) registerWorkspaceStatsHandler ( socket * socketio . Socket ) {
socket . On ( "workspace:stats" , func ( data * socketio . EventPayload ) {
h . logger . Info ( "Received workspace:stats event" ,
"socketId" , socket . Id ,
"hasAck" , data . Ack != nil )
// Note: GetWorkspaceStats is not in the new interface.
// This will need to be implemented or removed.
// For now, returning a placeholder.
response := map [ string ] interface { } {
"status" : "not_implemented" ,
"message" : "Workspace stats functionality is not available." ,
}
h . logger . Info ( "Sending workspace stats ACK" ,
"socketId" , socket . Id ,
"response" , response )
if data . Ack != nil {
data . Ack ( response )
}
} )
}
func ( h * SocketHandler ) handleFileUpdate ( socket * socketio . Socket , data string ) interface { } {
var updateData FileUpdateData
if err := json . Unmarshal ( [ ] byte ( data ) , & updateData ) ; err != nil {
h . logger . Error ( "Failed to parse file update data" , "error" , err , "data" , data )
return map [ string ] interface { } {
"status" : "error" ,
"message" : "Invalid data format" ,
}
}
h . logger . Info ( "Processing file update" ,
"socketId" , socket . Id ,
"event" , updateData . Event ,
"file" , updateData . FilePath )
// 立即返回确认收到
immediateAck := AckResponse {
ID : updateData . ID ,
Status : "received" ,
Message : "File update received, processing..." ,
}
// 异步处理文件操作
go h . processFileUpdateAsync ( socket , updateData )
return immediateAck
}
func ( h * SocketHandler ) handleFileUpdateFromObject ( socket * socketio . Socket , data socketio . EventPayload ) interface { } {
// 从数据中获取第一个元素( 应该是map)
if len ( data . Data ) == 0 {
h . logger . Error ( "No data provided for file update" )
return AckResponse {
Status : "error" ,
Message : "No data provided" ,
}
}
dataMap , ok := data . Data [ 0 ] . ( map [ string ] interface { } )
if ! ok {
h . logger . Error ( "Invalid data format for file update" , "type" , fmt . Sprintf ( "%T" , data . Data [ 0 ] ) )
return AckResponse {
Status : "error" ,
Message : "Invalid data format" ,
}
}
// 解析数据字段
var updateData FileUpdateData
// 使用类型断言提取字段
if id , ok := dataMap [ "id" ] . ( string ) ; ok {
updateData . ID = id
}
if filePath , ok := dataMap [ "filePath" ] . ( string ) ; ok {
updateData . FilePath = filePath
}
if event , ok := dataMap [ "event" ] . ( string ) ; ok {
updateData . Event = event
}
if hash , ok := dataMap [ "hash" ] . ( string ) ; ok {
updateData . Hash = hash
}
if content , ok := dataMap [ "content" ] . ( string ) ; ok {
updateData . Content = content
}
if timestamp , ok := dataMap [ "timestamp" ] . ( float64 ) ; ok {
updateData . Timestamp = int64 ( timestamp )
}
if apiKey , ok := dataMap [ "apiKey" ] . ( string ) ; ok {
updateData . ApiKey = apiKey
}
2025-07-25 19:02:52 +08:00
if workspacePath , ok := dataMap [ "workspacePath" ] . ( string ) ; ok {
updateData . WorkspacePath = workspacePath
h . logger . Debug ( "Extracted workspacePath from dataMap" , "workspacePath" , workspacePath )
} else {
h . logger . Debug ( "Failed to extract workspacePath from dataMap" , "workspacePathType" , fmt . Sprintf ( "%T" , dataMap [ "workspacePath" ] ) , "workspacePathValue" , dataMap [ "workspacePath" ] )
2025-07-23 17:42:20 +08:00
}
h . logger . Info ( "Processing file update" ,
"socketId" , socket . Id ,
"event" , updateData . Event ,
"file" , updateData . FilePath ,
"apiKey" , updateData . ApiKey ,
2025-07-25 19:02:52 +08:00
"workspacePath" , updateData . WorkspacePath )
2025-07-23 17:42:20 +08:00
// 立即返回确认收到
immediateAck := AckResponse {
ID : updateData . ID ,
Status : "received" ,
Message : "File update received, processing..." ,
}
// 异步处理文件操作
go h . processFileUpdateAsync ( socket , updateData )
return immediateAck
}
func ( h * SocketHandler ) processFileUpdateAsync ( socket * socketio . Socket , updateData FileUpdateData ) {
// 处理文件操作
var finalStatus , message string
ctx := context . Background ( )
// 通过ApiKey获取用户信息
user , err := h . userService . GetUserByApiKey ( ctx , updateData . ApiKey )
if err != nil {
finalStatus = "error"
message = fmt . Sprintf ( "Invalid API key: %v" , err )
h . logger . Error ( "Failed to get user by API key" , "apiKey" , updateData . ApiKey , "error" , err )
h . sendFinalResult ( socket , updateData , finalStatus , message )
return
}
userID := user . ID . String ( )
2025-07-25 19:02:52 +08:00
// 确保workspace存在
workspaceID , err := h . ensureWorkspace ( ctx , userID , updateData . WorkspacePath , updateData . FilePath )
if err != nil {
finalStatus = "error"
message = fmt . Sprintf ( "Failed to ensure workspace: %v" , err )
h . logger . Error ( "Failed to ensure workspace" , "error" , err )
h . sendFinalResult ( socket , updateData , finalStatus , message )
return
}
h . logger . Debug ( "Workspace ID obtained" , "workspaceID" , workspaceID , "filePath" , updateData . FilePath )
2025-07-23 17:42:20 +08:00
switch updateData . Event {
case "initial_scan" , "added" :
2025-07-25 19:02:52 +08:00
existingFile , err := h . workspaceService . GetByPath ( ctx , userID , workspaceID , updateData . FilePath )
2025-07-23 17:42:20 +08:00
if err != nil {
// "Not Found",文件不存在,执行创建逻辑
if db . IsNotFound ( err ) {
createReq := & domain . CreateWorkspaceFileReq {
Path : updateData . FilePath ,
Content : updateData . Content ,
Hash : updateData . Hash ,
UserID : userID ,
2025-07-25 19:02:52 +08:00
WorkspaceID : workspaceID ,
2025-07-23 17:42:20 +08:00
}
_ , createErr := h . workspaceService . Create ( ctx , createReq )
if createErr != nil {
finalStatus = "error"
message = fmt . Sprintf ( "Failed to create file: %v" , createErr )
h . logger . Error ( "Failed to create file" , "path" , updateData . FilePath , "error" , createErr )
} else {
2025-07-28 14:39:27 +08:00
// 调用GetAndSave处理新创建的文件
fileExtension := h . getFileExtension ( updateData . FilePath )
codeFiles := domain . CodeFiles {
Files : [ ] domain . FileMeta {
{
2025-07-28 17:59:54 +08:00
FilePath : updateData . FilePath ,
// FileExtension: fileExtension,
Language : h . getFileLanguage ( fileExtension ) ,
Content : updateData . Content ,
2025-07-28 14:39:27 +08:00
} ,
} ,
}
getAndSaveReq := & domain . GetAndSaveReq {
2025-07-28 15:10:30 +08:00
UserID : userID ,
WorkspaceID : workspaceID ,
FileMetas : codeFiles . Files ,
2025-07-28 14:39:27 +08:00
}
err = h . workspaceService . GetAndSave ( ctx , getAndSaveReq )
if err != nil {
h . logger . Error ( "Failed to process file with GetAndSave" , "path" , updateData . FilePath , "error" , err )
}
2025-07-23 17:42:20 +08:00
finalStatus = "success"
message = "File created successfully"
h . logger . Info ( "File created successfully" , "path" , updateData . FilePath )
}
} else {
// 其他错误
finalStatus = "error"
message = fmt . Sprintf ( "Error checking for existing file: %v" , err )
h . logger . Error ( "Error checking for existing file" , "path" , updateData . FilePath , "error" , err )
}
} else {
// 文件已存在,检查是否需要更新
if existingFile . Hash == updateData . Hash {
finalStatus = "success"
message = "File is already up-to-date"
h . logger . Info ( "Skipping update for unchanged file" , "path" , updateData . FilePath )
} else {
updateReq := & domain . UpdateWorkspaceFileReq {
ID : existingFile . ID ,
Content : & updateData . Content ,
Hash : & updateData . Hash ,
}
_ , updateErr := h . workspaceService . Update ( ctx , updateReq )
if updateErr != nil {
finalStatus = "error"
message = fmt . Sprintf ( "Failed to update existing file: %v" , updateErr )
h . logger . Error ( "Failed to update existing file" , "path" , updateData . FilePath , "error" , updateErr )
} else {
finalStatus = "success"
message = "File updated successfully"
h . logger . Info ( "File updated successfully" , "path" , updateData . FilePath )
}
}
}
case "modified" :
// First, get the file by path to find its ID
2025-07-25 19:02:52 +08:00
file , err := h . workspaceService . GetByPath ( ctx , userID , workspaceID , updateData . FilePath )
2025-07-23 17:42:20 +08:00
if err != nil {
finalStatus = "error"
message = fmt . Sprintf ( "Failed to find file for update: %v" , err )
h . logger . Error ( "Failed to find file for update" , "path" , updateData . FilePath , "error" , err )
break
}
req := & domain . UpdateWorkspaceFileReq {
ID : file . ID ,
Content : & updateData . Content ,
Hash : & updateData . Hash ,
}
_ , err = h . workspaceService . Update ( ctx , req )
if err != nil {
finalStatus = "error"
message = fmt . Sprintf ( "Failed to update file: %v" , err )
h . logger . Error ( "Failed to update file" , "path" , updateData . FilePath , "error" , err )
} else {
finalStatus = "success"
message = "File updated successfully"
h . logger . Info ( "File updated successfully" , "path" , updateData . FilePath )
2025-07-28 14:39:27 +08:00
// 调用GetAndSave处理更新的文件
fileExtension := h . getFileExtension ( updateData . FilePath )
codeFiles := domain . CodeFiles {
Files : [ ] domain . FileMeta {
{
2025-07-28 17:59:54 +08:00
FilePath : updateData . FilePath ,
// FileExtension: fileExtension,
Language : h . getFileLanguage ( fileExtension ) ,
Content : updateData . Content ,
2025-07-28 14:39:27 +08:00
} ,
} ,
}
getAndSaveReq := & domain . GetAndSaveReq {
2025-07-28 15:10:30 +08:00
UserID : userID ,
WorkspaceID : workspaceID ,
FileMetas : codeFiles . Files ,
2025-07-28 14:39:27 +08:00
}
err = h . workspaceService . GetAndSave ( ctx , getAndSaveReq )
if err != nil {
h . logger . Error ( "Failed to process file with GetAndSave" , "path" , updateData . FilePath , "error" , err )
}
2025-07-23 17:42:20 +08:00
}
case "deleted" :
// First, get the file by path to find its ID
2025-07-25 19:02:52 +08:00
file , err := h . workspaceService . GetByPath ( ctx , userID , workspaceID , updateData . FilePath )
2025-07-23 17:42:20 +08:00
if err != nil {
finalStatus = "error"
message = fmt . Sprintf ( "Failed to find file for deletion: %v" , err )
h . logger . Error ( "Failed to find file for deletion" , "path" , updateData . FilePath , "error" , err )
break
}
err = h . workspaceService . Delete ( ctx , file . ID )
if err != nil {
finalStatus = "error"
message = fmt . Sprintf ( "Failed to delete file: %v" , err )
h . logger . Error ( "Failed to delete file" , "path" , updateData . FilePath , "error" , err )
} else {
finalStatus = "success"
message = "File deleted successfully"
h . logger . Info ( "File deleted successfully" , "path" , updateData . FilePath )
}
default :
finalStatus = "error"
message = fmt . Sprintf ( "Unknown event type: %s" , updateData . Event )
}
// 发送最终处理结果
h . sendFinalResult ( socket , updateData , finalStatus , message )
}
2025-07-25 19:02:52 +08:00
// ensureWorkspace ensures that a workspace exists for the given workspacePath
func ( h * SocketHandler ) ensureWorkspace ( ctx context . Context , userID , workspacePath , filePath string ) ( string , error ) {
h . logger . Debug ( "ensureWorkspace called" , "userID" , userID , "workspacePath" , workspacePath , "filePath" , filePath , "workspacePathLength" , len ( workspacePath ) )
if workspacePath != "" {
h . logger . Debug ( "Ensuring workspace for path" , "path" , workspacePath )
// Use EnsureWorkspace to create or update workspace based on path
workspace , err := h . workspaceUsecase . EnsureWorkspace ( ctx , userID , workspacePath , "" )
if err != nil {
h . logger . Error ( "Error ensuring workspace" , "path" , workspacePath , "error" , err )
return "" , fmt . Errorf ( "failed to ensure workspace: %w" , err )
}
h . logger . Debug ( "Using existing or created workspace" , "workspaceID" , workspace . ID , "path" , workspacePath )
return workspace . ID , nil
}
// If no workspacePath provided, return an error
h . logger . Debug ( "No workspace path provided, returning error" )
return "" , fmt . Errorf ( "no workspace path provided" )
}
2025-07-23 17:42:20 +08:00
func ( h * SocketHandler ) handleTestPing ( socket * socketio . Socket , data string ) {
var pingData TestPingData
if err := json . Unmarshal ( [ ] byte ( data ) , & pingData ) ; err != nil {
h . logger . Error ( "Failed to parse test ping data" , "error" , err )
return
}
h . logger . Debug ( "Received test ping" ,
"socketId" , socket . Id ,
"message" , pingData . Message )
// 发送pong响应
pongData := map [ string ] interface { } {
"timestamp" : time . Now ( ) . UnixMilli ( ) ,
"serverTime" : time . Now ( ) . Format ( time . RFC3339 ) ,
"message" : "Pong from MonkeyCode server" ,
"receivedPing" : pingData ,
"socketId" : socket . Id ,
"serverStatus" : "ok" ,
}
h . mu . Lock ( )
socket . Emit ( "test:pong" , pongData )
h . mu . Unlock ( )
}
func ( h * SocketHandler ) handleHeartbeat ( socket * socketio . Socket , data string ) interface { } {
var heartbeatData HeartbeatData
if err := json . Unmarshal ( [ ] byte ( data ) , & heartbeatData ) ; err != nil {
h . logger . Error ( "Failed to parse heartbeat data" , "error" , err )
return map [ string ] interface { } {
"status" : "error" ,
"message" : "Invalid heartbeat data" ,
}
}
// 记录心跳
if h . config . Debug {
h . logger . Debug ( "Heartbeat received" , "socketId" , socket . Id , "clientId" , heartbeatData . ClientID )
}
// 返回心跳响应
response := map [ string ] interface { } {
"status" : "ok" ,
"serverTime" : time . Now ( ) . UnixMilli ( ) ,
"socketId" : socket . Id ,
}
return response
}
func ( h * SocketHandler ) sendServerStatus ( socket * socketio . Socket , status , message string ) {
statusData := map [ string ] string {
"status" : status ,
"message" : message ,
}
socket . Emit ( "server:status" , statusData )
}
// GetServer 返回Socket.IO服务器实例
func ( h * SocketHandler ) GetServer ( ) * socketio . Io {
return h . io
}
// BroadcastServerStatus 向所有连接的客户端广播服务器状态
func ( h * SocketHandler ) BroadcastServerStatus ( status , message string ) {
statusData := map [ string ] interface { } {
"status" : status ,
"message" : message ,
}
h . io . Emit ( "server:status" , statusData )
h . logger . Debug ( "Broadcasted server status" , "status" , status , "message" , message )
}
// GetConnectedClients 获取连接的客户端数量
func ( h * SocketHandler ) GetConnectedClients ( ) int {
sockets := h . io . Sockets ( )
return len ( sockets )
}
// 辅助方法: 发送错误ACK
func ( h * SocketHandler ) sendErrorACK ( data * socketio . EventPayload , message string ) {
if data . Ack != nil {
errorResp := map [ string ] interface { } {
"status" : "error" ,
"message" : message ,
}
data . Ack ( errorResp )
}
}
// 辅助方法: 带锁发送ACK
func ( h * SocketHandler ) sendACKWithLock ( data * socketio . EventPayload , response interface { } ) {
if data . Ack != nil {
h . mu . Lock ( )
data . Ack ( response )
h . mu . Unlock ( )
}
}
// 发送最终处理结果
func ( h * SocketHandler ) sendFinalResult ( socket * socketio . Socket , updateData FileUpdateData , status , message string ) {
finalResponse := map [ string ] interface { } {
"id" : updateData . ID ,
"status" : status ,
"message" : message ,
"file" : updateData . FilePath ,
}
h . logger . Debug ( "Sending final processing result" ,
"socketId" , socket . Id ,
"response" , finalResponse )
// 使用互斥锁保护Socket写入
h . mu . Lock ( )
socket . Emit ( "file:update:ack" , finalResponse )
h . mu . Unlock ( )
}
2025-07-25 19:02:52 +08:00
2025-07-28 14:39:27 +08:00
// getFileExtension 获取文件扩展名
func ( h * SocketHandler ) getFileExtension ( filePath string ) string {
2025-07-25 19:02:52 +08:00
ext := ""
if len ( filePath ) > 0 {
for i := len ( filePath ) - 1 ; i >= 0 ; i -- {
if filePath [ i ] == '.' {
ext = filePath [ i + 1 : ]
break
}
}
}
2025-07-28 14:39:27 +08:00
return ext
}
2025-07-25 19:02:52 +08:00
2025-07-28 14:39:27 +08:00
// getFileLanguage 根据文件扩展名获取编程语言类型
func ( h * SocketHandler ) getFileLanguage ( fileExtension string ) domain . CodeLanguageType {
switch fileExtension {
case "go" :
return domain . CodeLanguageTypeGo
case "py" :
return domain . CodeLanguageTypePython
case "java" :
return domain . CodeLanguageTypeJava
case "js" :
return domain . CodeLanguageTypeJavaScript
case "ts" :
return domain . CodeLanguageTypeTypeScript
case "jsx" :
return domain . CodeLanguageTypeJSX
case "tsx" :
return domain . CodeLanguageTypeTSX
case "html" :
return domain . CodeLanguageTypeHTML
case "css" :
return domain . CodeLanguageTypeCSS
case "php" :
return domain . CodeLanguageTypePHP
case "rs" :
return domain . CodeLanguageTypeRust
case "swift" :
return domain . CodeLanguageTypeSwift
case "kt" :
return domain . CodeLanguageTypeKotlin
case "c" :
return domain . CodeLanguageTypeC
case "cpp" , "cc" , "cxx" :
return domain . CodeLanguageTypeCpp
default :
2025-07-25 19:02:52 +08:00
return ""
}
}