소스 검색

in progress

0x4a52466c696e74 5 달 전
부모
커밋
2c8c1f9a4c
55개의 변경된 파일2182개의 추가작업 그리고 100개의 파일을 삭제
  1. 0 13
      case.go
  2. 0 2
      command.go
  3. 3 1
      example/application.go
  4. 340 0
      example/fox-ar/8th.html
  5. BIN
      example/fox-ar/assets/fonts/Montserrat-Medium.ttf
  6. BIN
      example/fox-ar/assets/fonts/Montserrat-Regular.ttf
  7. BIN
      example/fox-ar/assets/fox_v30.glb
  8. BIN
      example/fox-ar/assets/images/loading-logo.jpg
  9. BIN
      example/fox-ar/assets/images/loading.jpg
  10. BIN
      example/fox-ar/assets/images/loading.png
  11. BIN
      example/fox-ar/assets/images/logo.jpg
  12. BIN
      example/fox-ar/assets/images/og.png
  13. BIN
      example/fox-ar/assets/images/preview-background.jpg
  14. 23 0
      example/fox-ar/change-prompt-on-ios.js
  15. 23 0
      example/fox-ar/components/buttons-handler.js
  16. 34 0
      example/fox-ar/components/customize-buttons-in-capture-preview.js
  17. 18 0
      example/fox-ar/components/disable-culling.js
  18. 23 0
      example/fox-ar/components/fox-lifecycle.js
  19. 5 0
      example/fox-ar/components/hide-capture-button.js
  20. 18 0
      example/fox-ar/components/on-placed-handler.js
  21. 34 0
      example/fox-ar/components/tap-to-place.js
  22. 54 0
      example/fox-ar/index.html
  23. 142 0
      example/fox-ar/styles.css
  24. 41 0
      example/fox-ar/tap-place.js
  25. BIN
      example/fox-ar/tree.glb
  26. 2 2
      example/user.go
  27. 30 0
      example_chat/application.go
  28. 48 0
      example_chat/chat/chat.go
  29. 29 0
      example_chat/chat/user.go
  30. 26 0
      example_chat/users/auth.go
  31. 40 0
      example_chat/users/register.go
  32. 56 0
      example_chat/zv_test.go
  33. 3 4
      fielder.go
  34. 5 0
      go.mod
  35. 4 0
      go.sum
  36. 63 0
      reader.go
  37. 31 12
      request.go
  38. 4 6
      response.go
  39. 17 0
      response_empty.go
  40. 9 4
      rest_http/request.go
  41. 4 4
      rest_http/response.go
  42. 12 52
      rest_http/rest.go
  43. 80 0
      rest_websocket/application.go
  44. 70 0
      rest_websocket/client.go
  45. 242 0
      rest_websocket/message.go
  46. 35 0
      rest_websocket/message_answer.go
  47. 22 0
      rest_websocket/message_event.go
  48. 38 0
      rest_websocket/message_incoming.go
  49. 101 0
      rest_websocket/request.go
  50. 66 0
      rest_websocket/response.go
  51. 55 0
      rest_websocket/rest.go
  52. 162 0
      rest_websocket/websocket.go
  53. 45 0
      rest_websocket/z_test.go
  54. 32 0
      rest_websocket/zv_test.go
  55. 93 0
      server.go

+ 0 - 13
case.go

@@ -24,16 +24,3 @@ func camelToSnake(s string) string {
 
 	return result.String()
 }
-
-// SnakeToCamel преобразует строку из snake_case в CamelCase.
-func snakeToCamel(s string) string {
-	var result string
-	words := strings.Split(s, "_")
-
-	for _, word := range words {
-		w := strings.Title(word)
-		result += w
-	}
-
-	return result
-}

+ 0 - 2
command.go

@@ -1,8 +1,6 @@
 package rest
 
 type IApplication interface {
-	Addr() string
-	Secret() []byte
 	Executer(r IRequest) (IExecuter, bool)
 }
 

+ 3 - 1
example/application.go

@@ -69,9 +69,11 @@ func ExampleNew() {
 		},
 	)
 	// создаем сервер
+	server := rest.NewServer("localhost:8080", []byte("top-secret"))
 	restServ := rest_http.New(App, App)
+	restServ.Prepare(server, "/")
 	// пробуем запустить его. Если через секунду запуск не удался, будет возвращена ошибка
-	if err := restServ.Listen(time.Second); err != nil {
+	if err := server.Listen(time.Second); err != nil {
 		// ошибка запуска
 		log.Fatal(err)
 	}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 340 - 0
example/fox-ar/8th.html


BIN
example/fox-ar/assets/fonts/Montserrat-Medium.ttf


BIN
example/fox-ar/assets/fonts/Montserrat-Regular.ttf


BIN
example/fox-ar/assets/fox_v30.glb


BIN
example/fox-ar/assets/images/loading-logo.jpg


BIN
example/fox-ar/assets/images/loading.jpg


BIN
example/fox-ar/assets/images/loading.png


BIN
example/fox-ar/assets/images/logo.jpg


BIN
example/fox-ar/assets/images/og.png


BIN
example/fox-ar/assets/images/preview-background.jpg


+ 23 - 0
example/fox-ar/change-prompt-on-ios.js

@@ -0,0 +1,23 @@
+const changePromptOnIos = (promptBoxInnerHtml, cancelButtonInnetHtml, continueButtonInnerHtml) => {
+    let inDom = false
+    console.log('changePromptOnIos')
+    const observer = new MutationObserver(() => {
+        console.log('MutationObserver')
+        if (document.querySelector('.prompt-box-8w')) {
+            console.log('if prompt-box-8w')
+            if (!inDom) {
+                console.log('inDom')
+                document.querySelector('.prompt-box-8w p').innerHTML = promptBoxInnerHtml
+                document.querySelector('.prompt-button-8w').innerHTML = cancelButtonInnetHtml
+                document.querySelector('.button-primary-8w').innerHTML = continueButtonInnerHtml
+            }
+            inDom = true
+        } else if (inDom) {
+            inDom = false
+            observer.disconnect()
+        }
+    })
+    observer.observe(document.documentElement || document.body, { childList: true, subtree: true })
+}
+
+changePromptOnIos('Для работы AR<br/>необходим доступ к сенсорам<br/>движения', 'Отмена', 'Продолжить')

+ 23 - 0
example/fox-ar/components/buttons-handler.js

@@ -0,0 +1,23 @@
+AFRAME.registerComponent('buttons-handler', {
+    init() {
+        this.fox = document.querySelector('#fox')
+        this.afterExpiriensButtons = document.querySelector('#after-expiriens-buttons')
+        this.repeatButton = document.querySelector('#repeat-button')
+        this.shareButton = document.querySelector('#share-button')
+
+        this.fox.addEventListener('animation-finished', () => {
+            this.afterExpiriensButtons.classList.remove('hidden')
+        })
+
+        this.repeatButton.addEventListener('click', () => {
+            this.afterExpiriensButtons.classList.add('hidden')
+            document.querySelector('#fox-wrapper').setAttribute('tap-to-place', '')
+        })
+
+        this.shareButton.addEventListener('click', () => {
+            navigator.share({
+                url: 'https://fox.mordovia-russia.ru/',
+            })
+        })
+    },
+})

+ 34 - 0
example/fox-ar/components/customize-buttons-in-capture-preview.js

@@ -0,0 +1,34 @@
+const removeChildren = (element) => {
+    if (!element) {
+        return
+    }
+
+    while (element.firstChild) {
+        element.removeChild(element.firstChild)
+    }
+}
+
+AFRAME.registerComponent('customize-buttons-in-capture-preview', {
+    init() {
+        console.log('customizeButtonsInCapturePreviewComponent.init')
+        this.downloadButton = document.querySelector('#downloadButton')
+        this.actionButton = document.querySelector('#actionButton')
+
+        removeChildren(this.downloadButton)
+        removeChildren(this.actionButton)
+
+        this.downloadButton.classList.remove('icon-button')
+        this.downloadButton.classList.add('capture-preview-button')
+        this.actionButton.classList.add('capture-preview-button')
+
+        this.downloadButton.innerHTML = `<svg width="47" height="47" viewBox="0 0 47 47" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M38.4545 0H34.1818V12.8182C34.1818 13.9975 33.2247 14.9545 32.0455 14.9545H12.8182C11.6389 14.9545 10.6818 13.9975 10.6818 12.8182V0H4.27273C1.91205 0 0 1.91205 0 4.27273V42.7273C0 45.088 1.91205 47 4.27273 47H42.7273C45.088 47 47 45.088 47 42.7273V8.54545L38.4545 0ZM38.4545 42.7273H8.54545V29.9091C8.54545 27.5484 10.4575 25.6364 12.8182 25.6364H34.1818C36.5425 25.6364 38.4545 27.5484 38.4545 29.9091V42.7273Z" fill="white"/>
+          <path d="M29.5931 0H25.2412V10.4444H29.5931V0Z" fill="white"/>
+          </svg>
+          <span>Сохранить</span>`
+        this.actionButton.innerHTML = `<svg width="47" height="47" viewBox="0 0 47 47" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path fill-rule="evenodd" clip-rule="evenodd" d="M45.0333 11.462C46.2925 10.2028 46.9999 8.49503 46.9999 6.71429C46.9999 4.93355 46.2925 3.22574 45.0333 1.96657C43.7741 0.707397 42.0663 0 40.2856 0C38.5049 0 36.7971 0.707397 35.5379 1.96657C34.2787 3.22574 33.5713 4.93355 33.5713 6.71429C33.5713 7.19067 33.6219 7.66184 33.7202 8.1206L11.7785 19.0915C11.6773 18.9752 11.5717 18.862 11.462 18.7523C10.2028 17.4932 8.49503 16.7858 6.71429 16.7858C4.93355 16.7858 3.22574 17.4932 1.96657 18.7523C0.707397 20.0115 0 21.7193 0 23.5C0 25.2808 0.707397 26.9886 1.96657 28.2478C3.22574 29.5069 4.93355 30.2143 6.71429 30.2143C8.49503 30.2143 10.2028 29.5069 11.462 28.2478C11.5717 28.138 11.6772 28.0249 11.7785 27.9086L33.7202 38.8795C33.6219 39.3382 33.5713 39.8094 33.5713 40.2857C33.5713 42.0664 34.2787 43.7742 35.5379 45.0334C36.7971 46.2926 38.5049 47 40.2856 47C42.0663 47 43.7741 46.2926 45.0333 45.0334C46.2925 43.7742 46.9999 42.0664 46.9999 40.2857C46.9999 38.505 46.2925 36.7972 45.0333 35.538C43.7741 34.2788 42.0663 33.5714 40.2856 33.5714C38.5049 33.5714 36.7971 34.2788 35.5379 35.538C35.4282 35.6476 35.3228 35.7606 35.2216 35.8768L13.2798 24.9059C13.378 24.4473 13.4286 23.9763 13.4286 23.5C13.4286 23.0238 13.378 22.5528 13.2798 22.0942L35.2217 11.1232C35.3229 11.2394 35.4283 11.3524 35.5379 11.462C36.7971 12.7212 38.5049 13.4286 40.2856 13.4286C42.0663 13.4286 43.7741 12.7212 45.0333 11.462Z" fill="white"/>
+          </svg>
+          <span>Поделиться</span>`
+    },
+})

+ 18 - 0
example/fox-ar/components/disable-culling.js

@@ -0,0 +1,18 @@
+AFRAME.registerComponent('disable-culling', {
+    init() {
+        this.el.addEventListener('model-loaded', () => {
+            this.disableCulling()
+        })
+        this.el.addEventListener('model-changed', () => {
+            this.disableCulling()
+        })
+    },
+    disableCulling() {
+        const mesh = this.el.getObject3D('mesh')
+        mesh.traverse((node) => {
+            if (node.isMesh) {
+                node.frustumCulled = false
+            }
+        })
+    },
+})

+ 23 - 0
example/fox-ar/components/fox-lifecycle.js

@@ -0,0 +1,23 @@
+AFRAME.registerComponent('fox-lifecycle', {
+    init() {
+        this.fox = document.querySelector('#fox')
+
+        this.listeners = {
+            startAnimation: this.startAnimation.bind(this),
+        }
+
+        this.fox.addEventListener('start-animation', this.listeners.startAnimation)
+    },
+    startAnimation() {
+        this.fox.setAttribute('visible', 'true')
+        this.fox.setAttribute('animation-mixer', {
+            clip: 'Animation',
+            loop: 'once',
+            clampWhenFinished: true,
+        })
+        this.fox.addEventListener('animation-finished', () => {
+            this.fox.setAttribute('visible', 'false')
+            this.fox.removeAttribute('animation-mixer')
+        })
+    },
+})

+ 5 - 0
example/fox-ar/components/hide-capture-button.js

@@ -0,0 +1,5 @@
+AFRAME.registerComponent('hide-capture-button', {
+    init() {
+        document.querySelector('#recorder').classList.add('hidden')
+    },
+})

+ 18 - 0
example/fox-ar/components/on-placed-handler.js

@@ -0,0 +1,18 @@
+AFRAME.registerComponent('on-placed-handler', {
+    init() {
+        this.fox = document.querySelector('#fox')
+
+        this.listeners = {
+            onPlaced: this.onPlaced.bind(this),
+        }
+
+        this.el.addEventListener('placed', this.listeners.onPlaced)
+    },
+    remove() {
+        this.el.removeEventListener('placed', this.listeners.onPlaced)
+    },
+    onPlaced() {
+        this.fox.emit('start-animation')
+        document.querySelector('#recorder').classList.remove('hidden')
+    },
+})

+ 34 - 0
example/fox-ar/components/tap-to-place.js

@@ -0,0 +1,34 @@
+AFRAME.registerComponent('tap-to-place', {
+    init() {
+        this.raycaster = new THREE.Raycaster()
+        this.camera = document.getElementById('camera')
+        this.threeCamera = this.camera.getObject3D('camera')
+        this.ground = document.getElementById('ground')
+        document.querySelector('#tap-to-place-cursor').setAttribute('visible', 'true')
+        // 2D coordinates of the raycast origin, in normalized device coordinates (NDC)---X and Y
+        // components should be between -1 and 1.  Here we want the cursor in the center of the screen.
+        this.rayOrigin = new THREE.Vector2(0, 0)
+        this.cursorLocation = new THREE.Vector3(0, 0, 0)
+        this.onTap = this.onTap.bind(this)
+        this.el.sceneEl.addEventListener('touchstart', this.onTap)
+    },
+    remove() {
+        this.el.sceneEl.removeEventListener('touchstart', this.onTap)
+    },
+    tick() {
+        this.raycaster.setFromCamera(this.rayOrigin, this.threeCamera)
+        const intersects = this.raycaster.intersectObject(this.ground.object3D, true)
+        if (intersects.length > 0) {
+            const [intersect] = intersects
+            this.cursorLocation = intersect.point
+        }
+        this.el.object3D.position.y = 0.1
+        this.el.object3D.position.lerp(this.cursorLocation, 0.4)
+        this.el.object3D.lookAt(this.threeCamera.position.x, this.el.object3D.position.y, this.threeCamera.position.z)
+    },
+    onTap() {
+        document.querySelector('#tap-to-place-cursor').setAttribute('visible', 'false')
+        this.el.emit('placed')
+        this.el.removeAttribute('tap-to-place')
+    },
+})

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 54 - 0
example/fox-ar/index.html


+ 142 - 0
example/fox-ar/styles.css

@@ -0,0 +1,142 @@
+@font-face {
+    font-family: 'Montserrat';
+    src: url('assets/fonts/Montserrat-Regular.ttf') format('truetype');
+    font-weight: 400;
+}
+
+@font-face {
+    font-family: 'Montserrat';
+    src: url('assets/fonts/Montserrat-Medium.ttf') format('truetype');
+    font-weight: 500;
+}
+
+body * {
+    box-sizing: border-box;
+}
+
+body {
+    font-family: 'Montserrat';
+    font-weight: 500;
+}
+
+#requestingCameraIcon {
+    display: none;
+}
+
+#requestingCameraPermissions {
+    display: none;
+}
+
+#loadBackground {
+    background: url('assets/images/loading.png') top left/cover no-repeat;
+}
+
+#loadImage {
+    display: none;
+}
+
+#previewContainer {
+    background: url('assets/images/preview-background.jpg') center/cover no-repeat;
+}
+
+.preview-box {
+    background: #1362b2;
+    background: linear-gradient(180deg, #1362b2 0%, #d20101 100%);
+}
+
+#videoPreview,
+#imagePreview {
+    border-radius: 0;
+    border: 1vmin solid transparent;
+    background: none;
+    max-height: 75vh;
+}
+
+.bottom-bar {
+    height: 15vh;
+}
+
+.logo {
+    position: absolute;
+    z-index: 31;
+    top: 0;
+    left: 0;
+    width: 40vw;
+}
+
+.after-expiriens-buttons {
+    position: absolute;
+    z-index: 1;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    display: grid;
+    grid-template-rows: repeat(3, 1fr);
+    grid-row-gap: 3vh;
+}
+
+.after-expiriens-button {
+    font-size: 5vmin;
+    font-weight: 500;
+    color: #ffffff;
+    text-decoration: none;
+    padding: 2.4vh 8vw;
+    width: 100%;
+    background: #d20101;
+    border: none;
+    display: grid;
+    grid-template-columns: 48px 1fr;
+    grid-gap: 20px;
+    align-items: center;
+}
+
+.after-expiriens-button svg {
+    width: 7vmin;
+    height: 7vmin;
+}
+
+#actionButton {
+    font-family: 'Montserrat';
+    font-size: 4vmin;
+    border-radius: 0;
+    color: #ffffff;
+    background: #d20101;
+}
+
+/* 
+  #downloadButton.capture-preview-button,
+  #actionButton.capture-preview-button {
+    font-family: 'Montserrat';
+    font-size: 4vmin;
+    font-weight: 500;
+    color: #ffffff;
+    padding: 1.5vh 3.5vw;
+    width: 41vw;
+    background: #d20101;
+    display: grid;
+    grid-template-columns: 5vmin 1fr;
+    grid-gap: 16px;
+    align-items: center;
+    position: absolute;
+    top: 50%;
+    transform: translate(0, -50%);
+    border-radius: 0;
+  }
+  
+  .capture-preview-button svg {
+    width: 5vmin;
+    height: 5vmin;
+  } */
+
+.prompt-box-8w {
+    background-color: #ffffff !important;
+    color: #000000 !important;
+}
+
+.prompt-button-8w {
+    background-color: #828398 !important;
+}
+
+.button-primary-8w {
+    background-color: #d20101 !important;
+}

+ 41 - 0
example/fox-ar/tap-place.js

@@ -0,0 +1,41 @@
+// Copyright (c) 2021 8th Wall, Inc.
+/* globals AFRAME */
+
+// Component that places trees where the ground is clicked
+AFRAME.registerComponent('tap-place', {
+  init() {
+    const ground = document.getElementById('ground')
+    ground.addEventListener('click', (event) => {
+      // Create new entity for the new object
+      const newElement = document.createElement('a-entity')
+
+      // The raycaster gives a location of the touch in the scene
+      const touchPoint = event.detail.intersection.point
+      newElement.setAttribute('position', touchPoint)
+
+      const randomYRotation = Math.random() * 360
+      newElement.setAttribute('rotation', `0 ${randomYRotation} 0`)
+
+      newElement.setAttribute('visible', 'false')
+      newElement.setAttribute('scale', '0.0001 0.0001 0.0001')
+
+      newElement.setAttribute('shadow', {
+        receive: false,
+      })
+
+      newElement.setAttribute('gltf-model', '#treeModel')
+      this.el.sceneEl.appendChild(newElement)
+
+      newElement.addEventListener('model-loaded', () => {
+        // Once the model is loaded, we are ready to show it popping in using an animation
+        newElement.setAttribute('visible', 'true')
+        newElement.setAttribute('animation', {
+          property: 'scale',
+          to: '7 7 7',
+          easing: 'easeOutElastic',
+          dur: 800,
+        })
+      })
+    })
+  },
+})

BIN
example/fox-ar/tree.glb


+ 2 - 2
example/user.go

@@ -92,7 +92,7 @@ func (s *ExampleRequestRegister) Execute(req rest.IRequest) rest.IResponse {
 		return req.ResponseError(500, rest.ErrorMessage("TokenGenerateError", err.Error()))
 	}
 
-	files := make(map[string]io.ReadCloser)
+	files := make(map[string]rest.IReadCloserLen)
 	// сохраняем пользлвателя в хранилище
 	App.users.Store(userID, user)
 	fields, err := rest.Fields(user, files, req.Data().Slice("fields", nil)...)
@@ -130,7 +130,7 @@ func (s *ExampleRequestUserInfo) Validate(req rest.IRequest) rest.IResponse {
 }
 
 func (s *ExampleRequestUserInfo) Execute(req rest.IRequest) rest.IResponse {
-	files := make(map[string]io.ReadCloser)
+	files := make(map[string]rest.IReadCloserLen)
 	log.Println(s.user.Group)
 	fields := req.Data().Slice("fields", nil)
 	rFields, err := rest.Fields(s.user, files, fields...)

+ 30 - 0
example_chat/application.go

@@ -0,0 +1,30 @@
+package example_chat_test
+
+import (
+	"context"
+	"log"
+
+	"git.ali33.ru/fcg-xvii/rest"
+	"git.ali33.ru/fcg-xvii/rest/example_chat/users"
+	"git.ali33.ru/fcg-xvii/rest/rest_websocket"
+)
+
+func NewApplication(ctx context.Context) *Application {
+	rApp := rest_websocket.NewApplication(ctx)
+	return &Application{
+		Application: rApp,
+	}
+}
+
+type Application struct {
+	*rest_websocket.Application
+}
+
+func (s *Application) Executer(r rest.IRequest) (rest.IExecuter, bool) {
+	log.Println("COMMAND", r.Command())
+	switch r.Command() {
+	case "/users/register":
+		return &users.Register{}, true
+	}
+	return nil, false
+}

+ 48 - 0
example_chat/chat/chat.go

@@ -0,0 +1,48 @@
+package chat
+
+import (
+	"sync"
+	"sync/atomic"
+)
+
+func New() *Chat {
+	res := &Chat{
+		users:     &sync.Map{},
+		idCounter: &atomic.Int64{},
+	}
+	return res
+}
+
+type Chat struct {
+	users     *sync.Map
+	idCounter *atomic.Int64
+}
+
+func (s *Chat) Register(name, password string) *User {
+	user := &User{
+		ID:       s.idCounter.Add(1),
+		Name:     name,
+		Password: password,
+	}
+	s.users.Store(user.ID, user)
+	return user
+}
+
+func (s *Chat) SearchByID(id int64) (*User, bool) {
+	if u, check := s.users.Load(id); check {
+		return u.(*User), true
+	}
+	return nil, false
+}
+
+func (s *Chat) Auth(name, password string) (res *User, check bool) {
+	s.users.Range(func(key, val any) bool {
+		u := val.(*User)
+		if u.Name == name && u.Password == password {
+			res, check = u, true
+			return false
+		}
+		return true
+	})
+	return
+}

+ 29 - 0
example_chat/chat/user.go

@@ -0,0 +1,29 @@
+package chat
+
+import (
+	"git.ali33.ru/fcg-xvii/rest"
+	"git.ali33.ru/fcg-xvii/rest/rest_websocket"
+	"github.com/fcg-xvii/go-tools/json"
+)
+
+type User struct {
+	ID       int64  `rest:"default"`
+	Name     string `rest:"default"`
+	Password string `rest:"ignore"`
+	Socket   *rest_websocket.WebSocket
+}
+
+func (s *User) SendMessage(sender *User, message string, file rest.IReadCloserLen) {
+	var files map[string]rest.IReadCloserLen
+	if file != nil {
+		files = make(map[string]rest.IReadCloserLen)
+	}
+	mes := rest_websocket.NewMessage(
+		"/users/message",
+		json.Map{
+			"sender": sender,
+			"message": message,
+		},
+		map[string]
+	)
+}

+ 26 - 0
example_chat/users/auth.go

@@ -0,0 +1,26 @@
+package users
+
+import (
+	"git.ali33.ru/fcg-xvii/rest"
+	"git.ali33.ru/fcg-xvii/rest/example_chat/chat"
+)
+
+type Auth struct {
+	Name     string `rest:"required"`
+	Password string `rest:"required"`
+}
+
+func (s *Auth) Validate(req rest.IRequest) rest.IResponse {
+	if req.IsAuth() {
+		return req.ResponseError(500, rest.ErrorMessage("ErrAuth", "already auth"))
+	}
+	return nil
+}
+
+func (s *Auth) Execute(req rest.IRequest) rest.IResponse {
+	core := req.Core().(*chat.Chat)
+	user, check := core.Auth(s.Name, s.Password)
+	if !check {
+		return req.ResponseError(500, rest.ErrorMessage("ErrAuthData", "user is not found"))
+	}
+}

+ 40 - 0
example_chat/users/register.go

@@ -0,0 +1,40 @@
+package users
+
+import (
+	"log"
+	"strings"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+	"git.ali33.ru/fcg-xvii/rest"
+	"git.ali33.ru/fcg-xvii/rest/example_chat/chat"
+)
+
+type Register struct {
+	Name     string `rest:"require"`
+	Password string `rest:"require"`
+}
+
+func (s *Register) Validate(req rest.IRequest) rest.IResponse {
+	log.Println(s.Name, s.Password)
+	s.Name = strings.TrimSpace(s.Name)
+	if len(s.Name) < 3 {
+		return req.ResponseError(500, rest.ErrorFiled("name", "minimum 3 symbols"))
+	}
+	if len(s.Password) < 5 {
+		return req.ResponseError(500, rest.ErrorFiled("name", "minimum 5 symbols"))
+	}
+	return nil
+}
+
+func (s *Register) Execute(req rest.IRequest) rest.IResponse {
+	core := req.Core().(*chat.Chat)
+	user := core.Register(s.Name, s.Password)
+	fields, err := rest.Fields(user, nil)
+	if err != nil {
+		return req.ResponseError(500, err)
+	}
+	req.SetAuth(json.Map{
+		"user": user,
+	})
+	return req.ResponseSuccess(fields, nil)
+}

+ 56 - 0
example_chat/zv_test.go

@@ -0,0 +1,56 @@
+package example_chat_test
+
+import (
+	"testing"
+	"time"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+	"git.ali33.ru/fcg-xvii/rest"
+	"git.ali33.ru/fcg-xvii/rest/example_chat/chat"
+	"git.ali33.ru/fcg-xvii/rest/rest_websocket"
+)
+
+func TestChat(t *testing.T) {
+
+	addr := "127.0.0.1:40000"
+	clAddr := "ws://127.0.0.1:40000/ws"
+
+	core := chat.New()
+
+	server := rest.NewServer(addr, []byte("top-secret"))
+	if err := server.Listen(time.Second); err != nil {
+		t.Fatal(err)
+	}
+
+	app := NewApplication(server.Context())
+
+	restEngine := rest_websocket.New(app, core)
+	restEngine.Prepare(server, "/ws")
+
+	//ch := make(chan struct{})
+	//<-ch
+
+	cl, err := rest_websocket.NewClient(clAddr)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	t.Log(cl)
+
+	mes := rest_websocket.NewMessage(
+		"/users/register",
+		json.Map{
+			"name":     "LETO",
+			"password": "my-pass-99",
+		},
+		nil,
+		time.Second*10,
+		rest.RequestTypeMessage,
+	)
+
+	if err := cl.SendMessage(mes); err != nil {
+		t.Fatal(err)
+	}
+
+	time.Sleep(time.Second * 5)
+}

+ 3 - 4
fielder.go

@@ -2,7 +2,6 @@ package rest
 
 import (
 	"fmt"
-	"io"
 	"reflect"
 	"strings"
 
@@ -31,7 +30,7 @@ func (s FieldList) Field(name string) (*Field, bool) {
 // result - массив с полями, сформированными автоматически, в него можно вносить правки
 // files - глобальный массив файловых дескрипторов, который будет передан в ответе клиенту
 type IFielder interface {
-	RestFields(result json.Map, files map[string]io.ReadCloser, names FieldList)
+	RestFields(result json.Map, files map[string]IReadCloserLen, names FieldList)
 }
 
 func fieldsDefault(t reflect.Type) (res []any) {
@@ -77,7 +76,7 @@ func parseName(val reflect.Value) (res *Field, err error) {
 	return
 }
 
-func fieldVal(val reflect.Value, fieldName string, files map[string]io.ReadCloser, names ...any) (res reflect.Value, err IErrorArgs) {
+func fieldVal(val reflect.Value, fieldName string, files map[string]IReadCloserLen, names ...any) (res reflect.Value, err IErrorArgs) {
 	switch val.Kind() {
 	case reflect.Ptr, reflect.Interface:
 		return fieldVal(val.Elem(), fieldName, files, names...)
@@ -171,7 +170,7 @@ func fieldVal(val reflect.Value, fieldName string, files map[string]io.ReadClose
 }
 
 // Fields позволяет получить значения объекта в json
-func Fields(obj any, files map[string]io.ReadCloser, names ...any) (json.Map, IErrorArgs) {
+func Fields(obj any, files map[string]IReadCloserLen, names ...any) (json.Map, IErrorArgs) {
 	sVal := reflect.ValueOf(obj)
 	rVal, err := fieldVal(sVal.Elem(), "", files, names...)
 	if err != nil {

+ 5 - 0
go.mod

@@ -6,3 +6,8 @@ require (
 	git.ali33.ru/fcg-xvii/go-tools v0.0.0-20230529104008-2552c5121c91
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 )
+
+require (
+	github.com/gorilla/websocket v1.5.1 // indirect
+	golang.org/x/net v0.17.0 // indirect
+)

+ 4 - 0
go.sum

@@ -2,3 +2,7 @@ git.ali33.ru/fcg-xvii/go-tools v0.0.0-20230529104008-2552c5121c91 h1:8N3j1V1Yx24
 git.ali33.ru/fcg-xvii/go-tools v0.0.0-20230529104008-2552c5121c91/go.mod h1:YbBhWFFNNQIKcRisQFnpVaN5KA+XHGImSU1Z/MuntqU=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=

+ 63 - 0
reader.go

@@ -0,0 +1,63 @@
+package rest
+
+import (
+	"io"
+	"os"
+)
+
+type IReadCloserLen interface {
+	io.ReadCloser
+	Len() int64
+}
+
+func NewReadCloserLen(rc io.ReadCloser, len int64) IReadCloserLen {
+	return &readCloserLen{rc, len}
+}
+
+type readCloserLen struct {
+	io.ReadCloser
+	size int64
+}
+
+func (s *readCloserLen) Len() int64 {
+	return s.size
+}
+
+func NewTemporaryFile(size int64, r io.Reader) (IReadCloserLen, IErrorArgs) {
+	// Создаем временный файл
+	f, err := os.CreateTemp("", "*")
+	if err != nil {
+		return nil, ErrorMessage("ErrTempFileCrete", err.Error())
+	}
+	if _, err := io.CopyN(f, r, size); err != nil {
+		f.Close()
+		os.Remove(f.Name())
+		return nil, ErrorMessage("ErrTempFileCopy", err.Error())
+	}
+	tmp := &TemporaryFile{
+		File: f,
+		size: size,
+	}
+	return tmp, nil
+}
+
+type TemporaryFile struct {
+	*os.File
+	size int64
+}
+
+func (s *TemporaryFile) Len() int64 {
+	return s.size
+}
+
+func (s *TemporaryFile) Close() error {
+	if err := s.File.Close(); err != nil {
+		return err
+	}
+	return os.Remove(s.File.Name())
+}
+
+type IReadCloser interface {
+	io.ReadCloser
+	Len() int64
+}

+ 31 - 12
request.go

@@ -6,26 +6,37 @@ import (
 	"git.ali33.ru/fcg-xvii/go-tools/json"
 )
 
+type RequestType byte
+
+const (
+	RequestTypeMessage RequestType = iota
+	RequestTypeEvent
+	RequestTypeAnswer
+)
+
 type IRequest interface {
+	Type() RequestType
 	IsAuth() bool
+	SetAuth(auth json.Map)
 	Command() string
 	Auth() json.Map
 	Data() json.Map
 	FileKeys() []string
-	File(name string) (io.Reader, bool)
+	File(name string) (IReadCloserLen, bool)
 	GenerateToken(data json.Map, expire int64) (string, error)
 	Root() any
 	Core() any
-	ResponseSuccess(data json.Map, files map[string]io.ReadCloser) IResponse
+	ResponseSuccess(data json.Map, files map[string]IReadCloserLen) IResponse
 	ResponseError(code int, err IErrorArgs) IResponse
+	Close()
 }
 
 type Request struct {
-	auth          json.Map `json:"auth"`
-	command       string   `json:"command"`
-	data          json.Map `json:"data"`
-	fileKeys      []string `json:"file_keys"`
-	files         map[string]io.Reader
+	rType         RequestType
+	auth          json.Map
+	command       string
+	data          json.Map
+	files         map[string]IReadCloserLen
 	core          any
 	root          any
 	generateToken func(data json.Map, expire int64) (string, error)
@@ -34,6 +45,10 @@ type Request struct {
 
 // сеттеры
 
+func (s *Request) SetType(rType RequestType) {
+	s.rType = rType
+}
+
 func (s *Request) SetAuth(auth json.Map) {
 	s.auth = auth
 }
@@ -46,7 +61,7 @@ func (s *Request) SetData(data json.Map) {
 	s.data = data
 }
 
-func (s *Request) SetFiles(files map[string]io.Reader) {
+func (s *Request) SetFiles(files map[string]IReadCloserLen) {
 	s.files = files
 }
 
@@ -64,6 +79,10 @@ func (s *Request) SetGenerateToken(generateToken func(data json.Map, expire int6
 
 // методы интерфейса
 
+func (s *Request) Type() RequestType {
+	return RequestTypeMessage
+}
+
 func (s *Request) IsAuth() bool {
 	return s.auth != nil
 }
@@ -81,9 +100,9 @@ func (s *Request) Data() json.Map {
 }
 
 func (s *Request) FileKeys() []string {
-	keys := make([]string, len(s.files))
-	for key, _ := range s.files {
-		keys[0] = key
+	keys := make([]string, 0, len(s.files))
+	for key := range s.files {
+		keys = append(keys, key)
 	}
 	return keys
 }
@@ -105,7 +124,7 @@ func (s *Request) Core() any {
 	return s.core
 }
 
-func (s *Request) ResponseSuccess(data json.Map, files map[string]io.ReadCloser) IResponse {
+func (s *Request) ResponseSuccess(data json.Map, files map[string]IReadCloserLen) IResponse {
 	return &Response{
 		code:  200,
 		data:  data,

+ 4 - 6
response.go

@@ -1,15 +1,13 @@
 package rest
 
 import (
-	"io"
-
 	"git.ali33.ru/fcg-xvii/go-tools/json"
 )
 
 type IResponse interface {
 	IsError() bool
 	KeySet(key string, val any)
-	FileSet(name string, file io.ReadCloser)
+	FileSet(name string, file IReadCloserLen)
 	Close()
 	Send(writer any) IErrorArgs
 }
@@ -18,7 +16,7 @@ type Response struct {
 	code  int
 	err   IErrorArgs
 	data  json.Map
-	files map[string]io.ReadCloser
+	files map[string]IReadCloserLen
 }
 
 func (s *Response) IsError() bool {
@@ -29,7 +27,7 @@ func (s *Response) KeySet(key string, val any) {
 	s.data[key] = val
 }
 
-func (s *Response) FileSet(name string, file io.ReadCloser) {
+func (s *Response) FileSet(name string, file IReadCloserLen) {
 	s.files[name] = file
 }
 
@@ -51,7 +49,7 @@ func (s *Response) Data() json.Map {
 	return s.data
 }
 
-func (s *Response) Files() map[string]io.ReadCloser {
+func (s *Response) Files() map[string]IReadCloserLen {
 	return s.files
 }
 

+ 17 - 0
response_empty.go

@@ -0,0 +1,17 @@
+package rest
+
+type ResponseEmpty struct{}
+
+func (s *ResponseEmpty) IsError() bool {
+	return false
+}
+
+func (s *ResponseEmpty) KeySet(key string, val any) {}
+
+func (s *ResponseEmpty) FileSet(name string, file IReadCloserLen) {}
+
+func (s *ResponseEmpty) Close() {}
+
+func (s *ResponseEmpty) Send(writer any) IErrorArgs {
+	return nil
+}

+ 9 - 4
rest_http/request.go

@@ -1,7 +1,6 @@
 package rest_http
 
 import (
-	"io"
 	"net/http"
 
 	"git.ali33.ru/fcg-xvii/go-tools/json"
@@ -14,10 +13,14 @@ type Request struct {
 	core           any
 	auth           json.Map
 	data           json.Map
-	files          map[string]io.ReadCloser
+	files          map[string]rest.IReadCloserLen
 	tokenGenerator func(json.Map, int64) (string, error)
 }
 
+func (s *Request) Type() rest.RequestType {
+	return rest.RequestTypeMessage
+}
+
 func (s *Request) Root() any {
 	return s.Request
 }
@@ -51,7 +54,7 @@ func (s *Request) FileKeys() []string {
 }
 
 // File возвращает файл, принятый в запросе multipart/form-data, если он существует
-func (s *Request) File(name string) (io.Reader, bool) {
+func (s *Request) File(name string) (rest.IReadCloserLen, bool) {
 	r, check := s.files[name]
 	return r, check
 }
@@ -66,6 +69,8 @@ func (s *Request) IsAuth() bool {
 	return s.auth != nil
 }
 
+func (s *Request) SetAuth(json.Map) {}
+
 // IsJSON возарвщвет true, если в заголовке ContentType application/json
 func (s *Request) IsJSON() bool {
 	return s.Header.Get("Content-Type") == "application/json"
@@ -83,7 +88,7 @@ func (s *Request) Close() {
 	}
 }
 
-func (s *Request) ResponseSuccess(data json.Map, files map[string]io.ReadCloser) rest.IResponse {
+func (s *Request) ResponseSuccess(data json.Map, files map[string]rest.IReadCloserLen) rest.IResponse {
 	return ResponseSuccess(data, files)
 }
 

+ 4 - 4
rest_http/response.go

@@ -16,14 +16,14 @@ import (
 func NewResponse() *Response {
 	return &Response{
 		data:  make(mjson.Map),
-		files: make(map[string]io.ReadCloser),
+		files: make(map[string]rest.IReadCloserLen),
 	}
 }
 
 // Response реализует объект ответа
 type Response struct {
 	data  mjson.Map
-	files map[string]io.ReadCloser
+	files map[string]rest.IReadCloserLen
 	err   rest.IErrorArgs
 	code  int
 }
@@ -38,7 +38,7 @@ func (s *Response) KeySet(key string, val any) {
 }
 
 // FileSet устанавливает файл в словаре файлов по ключу
-func (s *Response) FileSet(name string, file io.ReadCloser) {
+func (s *Response) FileSet(name string, file rest.IReadCloserLen) {
 	s.files[name] = file
 }
 
@@ -110,7 +110,7 @@ func (s *Response) Send(writer any) rest.IErrorArgs {
 
 // Успрешный ответ
 
-func ResponseSuccess(data mjson.Map, files map[string]io.ReadCloser) *Response {
+func ResponseSuccess(data mjson.Map, files map[string]rest.IReadCloserLen) *Response {
 	return &Response{
 		code:  200,
 		data:  data,

+ 12 - 52
rest_http/rest.go

@@ -2,16 +2,11 @@ package rest_http
 
 import (
 	"bytes"
-	"context"
 	"encoding/json"
-	"errors"
 	"fmt"
-	"io"
 	"log"
 	"net/http"
 	"strings"
-	"sync/atomic"
-	"time"
 
 	mjson "git.ali33.ru/fcg-xvii/go-tools/json"
 	"git.ali33.ru/fcg-xvii/rest"
@@ -26,8 +21,7 @@ func New(app rest.IApplication, core any) *Rest {
 }
 
 type Rest struct {
-	opened atomic.Bool
-	server *http.Server
+	server rest.IServer
 	app    rest.IApplication
 	core   any
 }
@@ -36,47 +30,9 @@ func (s *Rest) App() rest.IApplication {
 	return s.app
 }
 
-func (s *Rest) TokenGenerate(m mjson.Map, expire int64) (string, error) {
-	token := jwt.New(jwt.SigningMethodHS256)
-
-	claims := token.Claims.(jwt.MapClaims)
-	for key, val := range m {
-		claims[key] = val
-	}
-	if expire > 0 {
-		claims["exp"] = time.Now().Add(time.Minute * 30).Unix()
-	}
-
-	tokenString, err := token.SignedString(s.app.Secret())
-	return tokenString, err
-}
-
-// Listen start server in other goroutine
-func (s *Rest) Listen(timeout time.Duration) (err error) {
-	if s.opened.Swap(true) {
-		return errors.New("ErrAlreadyOpened")
-	}
-	ctx, _ := context.WithTimeout(context.Background(), timeout)
-	go func() {
-		mux := http.NewServeMux()
-		mux.HandleFunc("/", s.handle)
-		s.server = &http.Server{
-			Addr:    s.app.Addr(),
-			Handler: mux,
-		}
-		err = s.server.ListenAndServe()
-		s.opened.Store(false)
-	}()
-	<-ctx.Done()
-	return
-}
-
-// Close
-func (s *Rest) Close() error {
-	if !s.opened.Load() {
-		return errors.New("ErrNotOpened")
-	}
-	return s.server.Close()
+func (s *Rest) Prepare(server rest.IServer, httpPrefix string) {
+	server.HandleFunc(httpPrefix, s.handle)
+	s.server = server
 }
 
 func responseNotFound(w http.ResponseWriter) {
@@ -95,8 +51,8 @@ func (s *Rest) handle(w http.ResponseWriter, r *http.Request) {
 		Request:        r,
 		core:           s.core,
 		data:           mjson.Map{},
-		files:          make(map[string]io.ReadCloser),
-		tokenGenerator: s.TokenGenerate,
+		files:          make(map[string]rest.IReadCloserLen),
+		tokenGenerator: s.server.TokenGenerate,
 	}
 
 	// Парсим Bearer токен и извлекаем claims
@@ -108,7 +64,7 @@ func (s *Rest) handle(w http.ResponseWriter, r *http.Request) {
 				if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
 					return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
 				}
-				return s.app.Secret(), nil
+				return s.server.Secret(), nil
 			})
 
 			if err != nil {
@@ -155,7 +111,11 @@ func (s *Rest) handle(w http.ResponseWriter, r *http.Request) {
 					)
 					responseError(w, err, 500)
 				}
-				rr.files[filename] = file
+				rlFile := rest.NewReadCloserLen(
+					file,
+					header.Size,
+				)
+				rr.files[filename] = rlFile
 			}
 		}
 		defer rr.Close()

+ 80 - 0
rest_websocket/application.go

@@ -0,0 +1,80 @@
+package rest_websocket
+
+import (
+	"context"
+
+	"git.ali33.ru/fcg-xvii/rest"
+)
+
+type IApplication interface {
+	rest.IApplication
+	Incoming() chan<- *WebSocket
+	Disconnect() chan<- *WebSocket
+}
+
+func NewApplication(ctx context.Context) *Application {
+	app := &Application{
+		ctx:          ctx,
+		chIncoming:   make(chan *WebSocket),
+		chDisconnect: make(chan *WebSocket),
+	}
+	go app.work()
+	return app
+}
+
+type Application struct {
+	sockets      []*WebSocket
+	ctx          context.Context
+	chIncoming   chan *WebSocket
+	chDisconnect chan *WebSocket
+}
+
+func (s *Application) Incoming() chan<- *WebSocket {
+	return s.chIncoming
+}
+
+func (s *Application) Disconnect() chan<- *WebSocket {
+	return s.chDisconnect
+}
+
+func (s *Application) work() {
+	for {
+		select {
+		case <-s.ctx.Done():
+			return
+		case socket, ok := <-s.chIncoming:
+			if !ok {
+				return
+			}
+			s.addSocket(socket)
+		case socket := <-s.chDisconnect:
+			for i, sock := range s.sockets {
+				if sock == socket {
+					s.sockets[i] = nil
+					break
+				}
+			}
+		}
+	}
+}
+
+func (s *Application) searchFree() (int, bool) {
+	for i, sock := range s.sockets {
+		if sock == nil {
+			return i, true
+		}
+	}
+	return -1, false
+}
+
+func (s *Application) addSocket(socket *WebSocket) {
+	// ищем свободный слот
+	index, free := s.searchFree()
+	if !free {
+		sockets := make([]*WebSocket, len(s.sockets)+50)
+		copy(sockets, s.sockets)
+		sockets[len(s.sockets)] = socket
+	} else {
+		s.sockets[index] = socket
+	}
+}

+ 70 - 0
rest_websocket/client.go

@@ -0,0 +1,70 @@
+package rest_websocket
+
+import (
+	"io"
+	"log"
+	"time"
+
+	"github.com/gorilla/websocket"
+)
+
+func NewClient(addr string) (*Client, error) {
+	conn, _, err := websocket.DefaultDialer.Dial(addr, nil)
+	if err != nil {
+		return nil, err
+	}
+	cl := &Client{
+		conn: conn,
+	}
+	go cl.work()
+	return cl, nil
+}
+
+type Client struct {
+	conn             *websocket.Conn
+	messagesIncoming map[int64]*Message
+}
+
+func (s *Client) SendMessage(mes *Message) (err error) {
+	var writer io.WriteCloser
+	if writer, err = s.conn.NextWriter(websocket.BinaryMessage); err != nil {
+		return
+	}
+	err = mes.Write(writer)
+	writer.Close()
+	return
+}
+
+func (s *Client) work() {
+	for {
+		// Read message from server
+		mType, r, err := s.conn.NextReader()
+		if err != nil {
+			log.Println(err)
+			return
+		}
+		switch mType {
+		case websocket.TextMessage, websocket.BinaryMessage:
+			// Обработка текстового сообщения
+			mes, err := ReadMessage(r)
+			if err != nil {
+				log.Println("data error: ", err)
+				return
+			}
+			log.Println("RESPONSE", mes)
+		case websocket.PingMessage:
+			// Отправка Pong в ответ на Ping
+			//s.sendLocker.Lock()
+			err := s.conn.WriteControl(websocket.PongMessage, nil, time.Now().Add(time.Second))
+			//s.sendLocker.Unlock()
+			if err != nil {
+				log.Println("pong write:", err)
+				return
+			}
+		case websocket.CloseMessage:
+			// Обработка закрытия соединения
+			log.Println("websocket connection closed")
+			return
+		}
+	}
+}

+ 242 - 0
rest_websocket/message.go

@@ -0,0 +1,242 @@
+package rest_websocket
+
+import (
+	"bytes"
+	"io"
+	"time"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+	"git.ali33.ru/fcg-xvii/rest"
+)
+
+// Int64ToBytes упаковывает int64 в срез байтов заданной длины
+func Int64ToBytes(num int64, byteCount int) []byte {
+	bytes := make([]byte, byteCount)
+	for i := 0; i < byteCount; i++ {
+		shift := uint((byteCount - 1 - i) * 8)
+		bytes[i] = byte(num >> shift)
+	}
+	return bytes
+}
+
+// BytesToInt64 конвертирует срез байтов в int64
+func BytesToInt64(bytes []byte) int64 {
+	var num int64
+	for _, b := range bytes {
+		num = (num << 8) | int64(b)
+	}
+	return num
+}
+
+func ioError(field string, err error) rest.IErrorArgs {
+	return rest.NewError(
+		"ErrIO",
+		json.Map{
+			"field": field,
+			"error": err.Error(),
+		},
+	)
+}
+
+func ReadMessage(r io.Reader) (*Message, rest.IErrorArgs) {
+	// todo
+	// id
+	sType := make([]byte, 1)
+	if _, err := r.Read(sType); err != nil {
+		return nil, ioError("type", err)
+	}
+	mes := Message{
+		mType: rest.RequestType(sType[0]),
+	}
+	if mes.mType == rest.RequestTypeMessage || mes.mType == rest.RequestTypeAnswer {
+		sID := make([]byte, 2)
+		if _, err := r.Read(sID); err != nil {
+			return nil, ioError("id", err)
+		}
+		mes.id = BytesToInt64(sID)
+	}
+	if mes.mType == rest.RequestTypeMessage {
+		sTimeout := make([]byte, 8)
+		if _, err := r.Read(sTimeout); err != nil {
+			return nil, ioError("timeout", err)
+		}
+		mes.timeout = time.Unix(BytesToInt64(sTimeout), 0)
+	}
+	sCommandSize := make([]byte, 2)
+	if _, err := r.Read(sCommandSize); err != nil {
+		return nil, ioError("data_size", err)
+	}
+	if BytesToInt64(sCommandSize) > 0 {
+		sCommand := make([]byte, BytesToInt64(sCommandSize))
+		if _, err := r.Read(sCommand); err != nil {
+			return nil, ioError("data", err)
+		}
+		mes.command = string(sCommand)
+	}
+	sDataSize := make([]byte, 8)
+	if _, err := r.Read(sDataSize); err != nil {
+		return nil, ioError("data_size", err)
+	}
+	sData := make([]byte, BytesToInt64(sDataSize))
+	if _, err := r.Read(sData); err != nil {
+		return nil, ioError("data", err)
+	}
+	if len(sData) > 0 {
+		if err := json.Unmarshal(sData, &mes.data); err != nil {
+			return nil, ioError("data", err)
+		}
+	}
+	sFilesCount := make([]byte, 2)
+	if _, err := r.Read(sFilesCount); err != nil {
+		return nil, ioError("files_count", err)
+	}
+	filesCount := BytesToInt64(sFilesCount)
+	files := make(map[string]rest.IReadCloserLen)
+	for i := 0; i < int(filesCount); i++ {
+		sFileNameLen := make([]byte, 2)
+		if _, err := r.Read(sFileNameLen); err != nil {
+			return nil, ioError("file_name_length", err)
+		}
+		sFileName := make([]byte, BytesToInt64(sFileNameLen))
+		if _, err := r.Read(sFileName); err != nil {
+			return nil, ioError("file_name", err)
+		}
+		sFileSize := make([]byte, 8)
+		if _, err := r.Read(sFileSize); err != nil {
+			return nil, ioError("file_size", err)
+		}
+		fileSize := BytesToInt64(sFileSize)
+		if fileSize < 1024*1024 {
+			// RAM buffer
+			sFileData := make([]byte, fileSize)
+			if _, err := r.Read(sFileData); err != nil {
+				return nil, ioError("file_data", err)
+			}
+			buf := rest.NewReadCloserLen(
+				io.NopCloser(bytes.NewBuffer(sFileData)),
+				int64(len(sFileData)),
+			)
+			files[string(sFileName)] = buf
+		} else {
+			// temporary file
+			tmpF, err := rest.NewTemporaryFile(fileSize, r)
+			if err != nil {
+				return nil, err
+			}
+			files[string(sFileName)] = tmpF
+		}
+	}
+	return &mes, nil
+}
+
+func NewMessage(command string, data json.Map, files map[string]rest.IReadCloserLen, timeout time.Duration, mType rest.RequestType) *Message {
+	return &Message{
+		command: command,
+		data:    data,
+		files:   files,
+		mType:   mType,
+		timeout: time.Now().Add(timeout),
+	}
+}
+
+type Message struct {
+	id      int64
+	command string
+	mType   rest.RequestType
+	timeout time.Time
+	data    json.Map
+	files   map[string]rest.IReadCloserLen
+	owner   *WebSocket
+}
+
+func (s *Message) Data() json.Map {
+	return s.data
+}
+
+func (s *Message) File(name string) (rest.IReadCloserLen, bool) {
+	file, check := s.files[name]
+	return file, check
+}
+
+func (s *Message) FileKeys() []string {
+	keys := make([]string, 0, len(s.files))
+	for k := range s.files {
+		keys = append(keys, k)
+	}
+	return keys
+}
+
+func (s *Message) Command() string {
+	return s.command
+}
+
+func (s *Message) IsBinagy() bool {
+	return len(s.files) > 0
+}
+
+func (s *Message) Write(w io.Writer) rest.IErrorArgs {
+	// id
+	if _, err := w.Write(Int64ToBytes(s.id, 2)); err != nil {
+		return ioError("id", err)
+	}
+	// type
+	if _, err := w.Write(Int64ToBytes(int64(s.mType), 1)); err != nil {
+		return ioError("type", err)
+	}
+	// timeout
+	if _, err := w.Write(Int64ToBytes(s.timeout.Unix(), 8)); err != nil {
+		return ioError("timeout", err)
+	}
+	// command length
+	if _, err := w.Write(Int64ToBytes(int64(len(s.command)), 2)); err != nil {
+		return ioError("command_length", err)
+	}
+	// command
+	if len(s.command) > 0 {
+		if _, err := w.Write([]byte(s.command)); err != nil {
+			return ioError("command", err)
+		}
+	}
+	// data
+	data := s.data.JSON()
+	// data size
+	if _, err := w.Write(Int64ToBytes(int64(len(data)), 8)); err != nil {
+		return ioError("data_size", err)
+	}
+	// data body
+	if _, err := w.Write(data); err != nil {
+		return ioError("data_body", err)
+	}
+	// files count
+	filesCount := int64(len(s.files))
+	if _, err := w.Write(Int64ToBytes(filesCount, 2)); err != nil {
+		return ioError("files_count", err)
+	}
+	// files
+	for name, file := range s.files {
+		// file name size
+		fileNameSize := int64(len(name))
+		if _, err := w.Write(Int64ToBytes(fileNameSize, 2)); err != nil {
+			return ioError("file_name_size", err)
+		}
+		// file name
+		if _, err := w.Write([]byte(name)); err != nil {
+			return ioError("file_name", err)
+		}
+		// file body size
+		if _, err := w.Write(Int64ToBytes(file.Len(), 8)); err != nil {
+			return ioError("file_body_size", err)
+		}
+		// file body
+		if _, err := io.Copy(w, file); err != nil {
+			return ioError("file_body", err)
+		}
+	}
+	return nil
+}
+
+func (s *Message) Close() {
+	for _, file := range s.files {
+		file.Close()
+	}
+}

+ 35 - 0
rest_websocket/message_answer.go

@@ -0,0 +1,35 @@
+package rest_websocket
+
+import (
+	"git.ali33.ru/fcg-xvii/rest"
+)
+
+type Answer struct {
+	*Message
+	err    rest.IErrorArgs
+	socket *WebSocket
+}
+
+func (s *Answer) IsError() bool {
+	return s.err != nil
+}
+
+func (s *Answer) KeySet(key string, val any) {
+	s.data[key] = val
+}
+
+func (s *Answer) FileSet(name string, file rest.IReadCloserLen) {
+	if !s.IsError() {
+		s.files[name] = file
+	}
+}
+
+func (s *Answer) Send(any) rest.IErrorArgs {
+	if s.IsError() {
+		s.data = s.err.Map()
+		s.data["error"] = true
+		s.Close()
+		s.files = nil
+	}
+	return s.socket.sendMessage(s.Message)
+}

+ 22 - 0
rest_websocket/message_event.go

@@ -0,0 +1,22 @@
+package rest_websocket
+
+import (
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+	"git.ali33.ru/fcg-xvii/rest"
+)
+
+type MessageEvent struct {
+	*MessageEvent
+}
+
+func (s *MessageEvent) Type() rest.RequestType {
+	return rest.RequestTypeEvent
+}
+
+func (s *MessageEvent) ResponseSuccess(data json.Map, files map[string]rest.IReadCloserLen) rest.IResponse {
+	return nil
+}
+
+func (s *MessageEvent) ResponseError(code int, err rest.IErrorArgs) rest.IResponse {
+	return nil
+}

+ 38 - 0
rest_websocket/message_incoming.go

@@ -0,0 +1,38 @@
+package rest_websocket
+
+import (
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+	"git.ali33.ru/fcg-xvii/rest"
+)
+
+type MessageIncoming struct {
+	*Message
+	socket *WebSocket
+}
+
+func (s *MessageIncoming) Type() rest.RequestType {
+	return rest.RequestTypeMessage
+}
+
+func (s *MessageIncoming) Answer(data json.Map, files map[string]rest.IReadCloserLen) rest.IResponse {
+	return &Answer{
+		Message: &Message{
+			id:    s.id,
+			mType: rest.RequestTypeAnswer,
+			data:  data,
+			files: files,
+		},
+		socket: s.socket,
+	}
+}
+
+func (s *MessageIncoming) AnswerError(code int, err rest.IErrorArgs) rest.IResponse {
+	return &Answer{
+		Message: &Message{
+			id:    s.id,
+			mType: rest.RequestTypeAnswer,
+		},
+		err:    err,
+		socket: s.socket,
+	}
+}

+ 101 - 0
rest_websocket/request.go

@@ -0,0 +1,101 @@
+package rest_websocket
+
+import (
+	"log"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+	"git.ali33.ru/fcg-xvii/rest"
+)
+
+/*
+type IRequest interface {
+	Type() RequestType
+	IsAuth() bool
+	Command() string
+	Auth() json.Map
+	Data() json.Map
+	FileKeys() []string
+	File(name string) (IReadCloserLen, bool)
+	GenerateToken(data json.Map, expire int64) (string, error)
+	Root() any
+	Core() any
+	ResponseSuccess(data json.Map, files map[string]IReadCloserLen) IResponse
+	ResponseError(code int, err IErrorArgs) IResponse
+	Close()
+}
+)*/
+
+type Request struct {
+	mes *Message
+}
+
+func (s *Request) Type() rest.RequestType {
+	return s.mes.mType
+}
+
+func (s *Request) IsAuth() bool {
+	return s.mes.owner.Auth != nil
+}
+
+func (s *Request) Auth() json.Map {
+	return s.mes.owner.Auth
+}
+
+func (s *Request) SetAuth(m json.Map) {
+	s.mes.owner.Auth = m
+}
+
+func (s *Request) Command() string {
+	return s.mes.command
+}
+
+func (s *Request) Data() json.Map {
+	return s.mes.data
+}
+
+func (s *Request) FileKeys() []string {
+	return s.mes.FileKeys()
+}
+
+func (s *Request) File(name string) (rest.IReadCloserLen, bool) {
+	res, check := s.mes.files[name]
+	return res, check
+}
+
+func (s *Request) GenerateToken(data json.Map, expire int64) (string, error) {
+	return s.mes.owner.appConf.tokenGenerator(data, expire)
+}
+
+func (s *Request) Root() any {
+	return s.mes.owner
+}
+
+func (s *Request) Core() any {
+	return s.mes.owner.appConf.core
+}
+
+func (s *Request) ResponseSuccess(data json.Map, files map[string]rest.IReadCloserLen) rest.IResponse {
+	if s.mes.mType == rest.RequestTypeEvent {
+		return &rest.ResponseEmpty{}
+	}
+	return &Response{
+		mes:   s.mes,
+		data:  data,
+		files: files,
+	}
+}
+
+func (s *Request) ResponseError(code int, err rest.IErrorArgs) rest.IResponse {
+	log.Println("RESP-ERROR", err.Map())
+	if s.mes.mType == rest.RequestTypeEvent {
+		return &rest.ResponseEmpty{}
+	}
+	return &Response{
+		mes: s.mes,
+		err: err,
+	}
+}
+
+func (s *Request) Close() {
+	s.mes.Close()
+}

+ 66 - 0
rest_websocket/response.go

@@ -0,0 +1,66 @@
+package rest_websocket
+
+import (
+	"log"
+	"time"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+	"git.ali33.ru/fcg-xvii/rest"
+)
+
+/*
+type IResponse interface {
+	IsError() bool
+	KeySet(key string, val any)
+	FileSet(name string, file IReadCloserLen)
+	Close()
+	Send(writer any) IErrorArgs
+}
+*/
+
+type Response struct {
+	mes   *Message
+	err   rest.IErrorArgs
+	data  json.Map
+	files map[string]rest.IReadCloserLen
+}
+
+func (s *Response) IsError() bool {
+	return s.err != nil
+}
+
+func (s *Response) KeySet(key string, val any) {
+	s.data[key] = val
+}
+
+func (s *Response) FileSet(name string, file rest.IReadCloserLen) {
+	s.files[name] = file
+}
+
+func (s *Response) Close() {
+	for _, file := range s.files {
+		file.Close()
+	}
+	s.files = nil
+}
+
+func (s *Response) Send(writer any) rest.IErrorArgs {
+	if s.IsError() {
+		log.Println("ERRRRRRRRRRRRRRRRRRR")
+		s.Close()
+		s.data = s.err.Map()
+		s.data["error"] = true
+	}
+	mes := &Message{
+		id:      s.mes.id,
+		command: s.mes.command,
+		mType:   rest.RequestTypeAnswer,
+		timeout: time.Now(),
+		data:    s.data,
+		files:   s.files,
+	}
+	log.Println("AAA", mes)
+	err := s.mes.owner.sendMessage(mes)
+	mes.Close()
+	return err
+}

+ 55 - 0
rest_websocket/rest.go

@@ -0,0 +1,55 @@
+package rest_websocket
+
+import (
+	"context"
+	"log"
+	"net/http"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+	"git.ali33.ru/fcg-xvii/rest"
+	"github.com/gorilla/websocket"
+)
+
+type appConfig struct {
+	app            IApplication
+	core           any
+	ctx            context.Context
+	tokenGenerator func(json.Map, int64) (string, error)
+}
+
+func New(app IApplication, core any) *Rest {
+	return &Rest{
+		upgrader: &websocket.Upgrader{
+			ReadBufferSize:  1024,
+			WriteBufferSize: 1024,
+		},
+		appConf: &appConfig{
+			app:  app,
+			core: core,
+		},
+	}
+}
+
+type Rest struct {
+	upgrader *websocket.Upgrader
+	appConf  *appConfig
+	server   rest.IServer
+}
+
+// Listen start server in other goroutine
+func (s *Rest) Prepare(server rest.IServer, httpPrefix string) {
+	s.appConf.ctx = server.Context()
+	s.appConf.tokenGenerator = server.TokenGenerate
+	server.HandleFunc(httpPrefix, s.handle)
+	s.server = server
+}
+
+func (s *Rest) handle(w http.ResponseWriter, r *http.Request) {
+	// Преобразование HTTP соединения в WebSocket соединение
+	ws, err := s.upgrader.Upgrade(w, r, nil)
+	if err != nil {
+		log.Println(err)
+	}
+	socket := newWebSocket(ws, s.appConf)
+	s.appConf.app.Incoming() <- socket
+}

+ 162 - 0
rest_websocket/websocket.go

@@ -0,0 +1,162 @@
+package rest_websocket
+
+import (
+	"context"
+	"log"
+	"sync"
+	"time"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+	"git.ali33.ru/fcg-xvii/rest"
+	"github.com/gorilla/websocket"
+)
+
+func newWebSocket(conn *websocket.Conn, appConf *appConfig) *WebSocket {
+	ctx, cancel := context.WithCancel(context.Background())
+	ws := &WebSocket{
+		conn:              conn,
+		waitMessages:      make(map[int64]*MessageIncoming),
+		ctx:               ctx,
+		cancel:            cancel,
+		sendLocker:        &sync.Mutex{},
+		appConf:           appConf,
+		chMessageIncoming: make(chan *MessageIncoming, 10),
+	}
+	go ws.read()
+	return ws
+}
+
+type WebSocket struct {
+	conn              *websocket.Conn
+	waitMessages      map[int64]*MessageIncoming
+	ctx               context.Context
+	cancel            context.CancelFunc
+	sendLocker        *sync.Mutex
+	appConf           *appConfig
+	chMessageIncoming chan *MessageIncoming
+	Auth              json.Map
+	Data              json.Map
+}
+
+func (s *WebSocket) MessagesIncoming() <-chan *MessageIncoming {
+	return s.chMessageIncoming
+}
+
+func (s *WebSocket) Close() {
+
+}
+
+func (s *WebSocket) sendMessage(msg *Message) rest.IErrorArgs {
+	s.sendLocker.Lock()
+	writer, err := s.conn.NextWriter(websocket.TextMessage)
+	s.sendLocker.Unlock()
+	if err != nil {
+		return rest.ErrorMessage("ErrWriterInit", err.Error())
+	}
+	err = msg.Write(writer)
+	writer.Close()
+	return nil
+}
+
+func (s *WebSocket) execMessage(mes *Message) {
+	defer mes.Close()
+	log.Println("Message", mes)
+	req := &Request{
+		mes: mes,
+	}
+	var resp rest.IResponse
+	command, check := s.appConf.app.Executer(req)
+	if !check {
+		resp = req.ResponseError(404, rest.ErrorMessage("ErrNotFound", "command is not found"))
+	} else {
+		// serialize
+		if err := rest.Serialize(mes.data, command); err != nil {
+			log.Println("serialize error", err)
+			return
+		}
+		// validate
+		if validator, check := command.(rest.IValidator); check {
+			resp := validator.Validate(req)
+			if resp != nil {
+				if err := resp.Send(nil); err != nil {
+					log.Println("socket send error", err.Map())
+				}
+				return
+			}
+		}
+		resp = command.Execute(req)
+	}
+	log.Println("RESP", resp)
+	resp.Send(nil)
+	resp.Close()
+}
+
+func (s *WebSocket) messageIncoming() chan<- *Message {
+	chIncoming := make(chan *Message, 100)
+	tClean := time.NewTicker(time.Second * 60)
+	defer tClean.Stop()
+	go func() {
+		for {
+			select {
+			case <-s.ctx.Done():
+				return
+			case mes, ok := <-chIncoming:
+				if !ok {
+					return
+				}
+				mes.owner = s
+				log.Println("INCOMING!!!!!")
+				switch mes.mType {
+				case rest.RequestTypeMessage, rest.RequestTypeEvent:
+					s.execMessage(mes)
+				case rest.RequestTypeAnswer:
+					log.Println("ANSWER")
+				}
+			case <-tClean.C:
+				now := time.Now()
+				for id, mes := range s.waitMessages {
+					if mes.timeout.Before(now) {
+						delete(s.waitMessages, id)
+					}
+				}
+			}
+		}
+	}()
+	return chIncoming
+}
+
+func (s *WebSocket) read() {
+	chIncoming := s.messageIncoming()
+	defer s.cancel()
+	for {
+		// Read message from server
+		mType, r, err := s.conn.NextReader()
+		if err != nil {
+			log.Println(err)
+			return
+		}
+		switch mType {
+		case websocket.TextMessage, websocket.BinaryMessage:
+			// Обработка текстового сообщения
+			mes, err := ReadMessage(r)
+			if err != nil {
+				log.Println("data error: ", err)
+				return
+			}
+			chIncoming <- mes
+		case websocket.PingMessage:
+			// Отправка Pong в ответ на Ping
+			s.sendLocker.Lock()
+			err := s.conn.WriteControl(websocket.PongMessage, nil, time.Now().Add(time.Second))
+			s.sendLocker.Unlock()
+			if err != nil {
+				log.Println("pong write:", err)
+				return
+			}
+		case websocket.CloseMessage:
+			// Обработка закрытия соединения
+			log.Println("websocket connection closed")
+			return
+		}
+	}
+}

+ 45 - 0
rest_websocket/z_test.go

@@ -0,0 +1,45 @@
+package rest_websocket
+
+import (
+	"bytes"
+	"io"
+	"testing"
+	"time"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+	"git.ali33.ru/fcg-xvii/rest"
+)
+
+func TestMessage(t *testing.T) {
+	rbuf := make([]byte, 100)
+	fTmp := rest.NewReadCloserLen(
+		io.NopCloser(bytes.NewReader(rbuf)),
+		int64(len(rbuf)),
+	)
+
+	mes := NewMessage(
+		"okko",
+		json.Map{
+			"one": 1,
+			"two": 2,
+		},
+		map[string]rest.IReadCloserLen{
+			"file.txt": fTmp,
+		},
+		time.Second,
+		rest.RequestTypeMessage,
+	)
+	var buf bytes.Buffer
+	if err := mes.Write(&buf); err != nil {
+		t.Fatal(err)
+	}
+
+	mes2, err := ReadMessage(&buf)
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Log(mes2)
+	t.Log(mes2.files["file.txt"].Len())
+
+	mes2.Close()
+}

+ 32 - 0
rest_websocket/zv_test.go

@@ -0,0 +1,32 @@
+package rest_websocket_test
+
+import (
+	"testing"
+
+	"git.ali33.ru/fcg-xvii/rest/rest_websocket"
+)
+
+func TestInt64Pack(t *testing.T) {
+	i := 125540
+	b := rest_websocket.Int64ToBytes(int64(i), 3)
+	t.Log(b)
+	di := rest_websocket.BytesToInt64(b)
+	t.Log(di)
+}
+
+func TestMessage(t *testing.T) {
+	//m := rest.Message{
+	//	Type: rest.MessageTypeText,
+	//	Data: []byte("Hello"),
+	//}
+	//t.Log(m)
+}
+
+func TestMessagePack(t *testing.T) {
+
+	//m := rest.Message{
+	//	Type: rest.MessageTypeText,
+	//	Data: []byte("Hello"),
+	//}
+	//t.Log(m
+}

+ 93 - 0
server.go

@@ -0,0 +1,93 @@
+package rest
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"sync/atomic"
+	"time"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+	"github.com/dgrijalva/jwt-go"
+)
+
+type IServer interface {
+	Addr() string
+	Secret() []byte
+	HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
+	Listen(timeout time.Duration) error
+	TokenGenerate(m json.Map, expire int64) (string, error)
+	Close() error
+	Context() context.Context
+}
+
+func NewServer(addr string, secret []byte) IServer {
+	return &Server{
+		secret: secret,
+		server: &http.Server{
+			Addr:    addr,
+			Handler: http.NewServeMux(),
+		},
+	}
+}
+
+type Server struct {
+	secret []byte
+	server *http.Server
+	opened atomic.Bool
+	ctx    context.Context
+}
+
+func (s *Server) Context() context.Context {
+	return s.ctx
+}
+
+func (s *Server) Close() error {
+	if !s.opened.Load() {
+		return errors.New("ErrNotOpened")
+	}
+	return s.server.Close()
+}
+
+func (s *Server) Listen(timeout time.Duration) (err error) {
+	if s.opened.Swap(true) {
+		return errors.New("ErrAlreadyOpened")
+	}
+	ctx, _ := context.WithTimeout(context.Background(), timeout)
+	go func() {
+		var cancel context.CancelFunc
+		s.ctx, cancel = context.WithCancel(context.Background())
+		err = s.server.ListenAndServe()
+		s.opened.Store(false)
+		cancel()
+	}()
+	<-ctx.Done()
+	return
+}
+
+func (s *Server) TokenGenerate(m json.Map, expire int64) (string, error) {
+	token := jwt.New(jwt.SigningMethodHS256)
+
+	claims := token.Claims.(jwt.MapClaims)
+	for key, val := range m {
+		claims[key] = val
+	}
+	if expire > 0 {
+		claims["exp"] = time.Now().Add(time.Minute * 30).Unix()
+	}
+
+	tokenString, err := token.SignedString(s.Secret())
+	return tokenString, err
+}
+
+func (s *Server) Addr() string {
+	return s.server.Addr
+}
+
+func (s *Server) Secret() []byte {
+	return s.secret
+}
+
+func (s *Server) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
+	s.server.Handler.(*http.ServeMux).HandleFunc(pattern, handler)
+}

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.