From f1fb7f5608c6663e542450fc5326ebdd542b4e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E6=99=BA=E9=B8=BF?= <2524422678@qq.com> Date: Wed, 7 Jan 2026 18:04:01 +0800 Subject: [PATCH] server code --- .gitignore | 3 +- server/Makefile | 7 + server/README.md | 45 +- server/api/dance/dance.go | 18 + server/api/dance/v1/dance.go | 45 + server/api/device/device.go | 19 + server/api/device/v1/device.go | 53 ++ server/api/file/file.go | 15 + server/api/file/v1/file.go | 22 + server/api/friend/friend.go | 15 + server/api/friend/v1/friend.go | 16 + server/api/post/post.go | 20 + server/api/post/v1/post.go | 70 ++ server/go.mod | 46 + server/go.sum | 100 +++ server/hack/config.yaml | 12 + server/hack/hack-cli.mk | 21 + server/hack/hack.mk | 78 ++ server/internal/cmd/cmd.go | 92 ++ server/internal/consts/consts.go | 6 + server/internal/controller/dance/dance.go | 9 + server/internal/controller/dance/dance_new.go | 19 + .../controller/dance/dance_v1_create.go | 67 ++ .../controller/dance/dance_v1_delete.go | 18 + .../controller/dance/dance_v1_get_list.go | 44 + .../controller/dance/dance_v1_update.go | 31 + server/internal/controller/device/device.go | 10 + .../internal/controller/device/device_new.go | 20 + .../controller/device/device_v1_create.go | 27 + .../device/device_v1_get_device_info.go | 24 + .../device/device_v1_get_random_device.go | 37 + .../controller/device/device_v1_update.go | 21 + .../device/device_v1_update_device_info.go | 27 + server/internal/controller/file/file.go | 10 + server/internal/controller/file/file_new.go | 20 + .../internal/controller/file/file_v1_file.go | 17 + server/internal/controller/friend/friend.go | 10 + .../internal/controller/friend/friend_new.go | 20 + .../controller/friend/friend_v1_add.go | 54 ++ server/internal/controller/post/post.go | 10 + server/internal/controller/post/post_new.go | 20 + .../controller/post/post_v1_create_post.go | 39 + .../post/post_v1_create_post_comment.go | 29 + .../controller/post/post_v1_delete_post.go | 21 + .../post/post_v1_delete_post_comment.go | 42 + .../controller/post/post_v1_get_post.go | 69 ++ .../post/post_v1_get_post_comment.go | 57 ++ server/internal/dao/.gitkeep | 0 server/internal/dao/device.go | 27 + server/internal/dao/device_dance.go | 27 + server/internal/dao/device_friend.go | 27 + server/internal/dao/device_post.go | 27 + server/internal/dao/device_post_comment.go | 27 + server/internal/dao/internal/device.go | 81 ++ server/internal/dao/internal/device_dance.go | 89 ++ server/internal/dao/internal/device_friend.go | 81 ++ server/internal/dao/internal/device_post.go | 87 ++ .../dao/internal/device_post_comment.go | 87 ++ .../internal/dao/internal/sqlite_sequence.go | 81 ++ server/internal/dao/sqlite_sequence.go | 27 + server/internal/logic/.gitkeep | 0 server/internal/model/.gitkeep | 0 server/internal/model/device.go | 11 + server/internal/model/do/device.go | 16 + server/internal/model/do/device_dance.go | 21 + server/internal/model/do/device_friend.go | 16 + server/internal/model/do/device_post.go | 20 + .../internal/model/do/device_post_comment.go | 20 + server/internal/model/do/sqlite_sequence.go | 16 + server/internal/model/entity/device.go | 11 + server/internal/model/entity/device_dance.go | 19 + server/internal/model/entity/device_friend.go | 11 + server/internal/model/entity/device_post.go | 18 + .../model/entity/device_post_comment.go | 18 + .../internal/model/entity/sqlite_sequence.go | 11 + server/internal/model/expressionData.go | 42 + server/internal/model/post.go | 27 + server/internal/packed/packed.go | 6 + server/internal/service/.gitkeep | 0 server/internal/service/device.go | 40 + server/internal/service/device_test.go | 13 + server/internal/service/file.go | 70 ++ server/internal/web_socket/web_socket.go | 830 ++++++++++++++++++ server/main.go | 18 + server/manifest/config/config.yaml | 15 + .../deploy/kustomize/base/deployment.yaml | 21 + .../deploy/kustomize/base/kustomization.yaml | 8 + .../deploy/kustomize/base/service.yaml | 12 + .../kustomize/overlays/develop/configmap.yaml | 14 + .../overlays/develop/deployment.yaml | 10 + .../overlays/develop/kustomization.yaml | 14 + server/manifest/docker/Dockerfile | 7 + server/manifest/docker/docker.sh | 8 + server/manifest/i18n/.gitkeep | 0 server/manifest/protobuf/.keep-if-necessary | 0 server/resource/public/html/.gitkeep | 0 server/resource/public/plugin/.gitkeep | 0 server/resource/public/resource/css/.gitkeep | 0 .../resource/public/resource/image/.gitkeep | 0 server/resource/public/resource/js/.gitkeep | 0 server/resource/template/.gitkeep | 0 server/stackChan.sqlite | Bin 0 -> 40960 bytes server/utility/.gitkeep | 0 103 files changed, 3504 insertions(+), 2 deletions(-) create mode 100644 server/Makefile create mode 100644 server/api/dance/dance.go create mode 100644 server/api/dance/v1/dance.go create mode 100644 server/api/device/device.go create mode 100644 server/api/device/v1/device.go create mode 100644 server/api/file/file.go create mode 100644 server/api/file/v1/file.go create mode 100644 server/api/friend/friend.go create mode 100644 server/api/friend/v1/friend.go create mode 100644 server/api/post/post.go create mode 100644 server/api/post/v1/post.go create mode 100644 server/go.mod create mode 100644 server/go.sum create mode 100644 server/hack/config.yaml create mode 100644 server/hack/hack-cli.mk create mode 100644 server/hack/hack.mk create mode 100644 server/internal/cmd/cmd.go create mode 100644 server/internal/consts/consts.go create mode 100644 server/internal/controller/dance/dance.go create mode 100644 server/internal/controller/dance/dance_new.go create mode 100644 server/internal/controller/dance/dance_v1_create.go create mode 100644 server/internal/controller/dance/dance_v1_delete.go create mode 100644 server/internal/controller/dance/dance_v1_get_list.go create mode 100644 server/internal/controller/dance/dance_v1_update.go create mode 100644 server/internal/controller/device/device.go create mode 100644 server/internal/controller/device/device_new.go create mode 100644 server/internal/controller/device/device_v1_create.go create mode 100644 server/internal/controller/device/device_v1_get_device_info.go create mode 100644 server/internal/controller/device/device_v1_get_random_device.go create mode 100644 server/internal/controller/device/device_v1_update.go create mode 100644 server/internal/controller/device/device_v1_update_device_info.go create mode 100644 server/internal/controller/file/file.go create mode 100644 server/internal/controller/file/file_new.go create mode 100644 server/internal/controller/file/file_v1_file.go create mode 100644 server/internal/controller/friend/friend.go create mode 100644 server/internal/controller/friend/friend_new.go create mode 100644 server/internal/controller/friend/friend_v1_add.go create mode 100644 server/internal/controller/post/post.go create mode 100644 server/internal/controller/post/post_new.go create mode 100644 server/internal/controller/post/post_v1_create_post.go create mode 100644 server/internal/controller/post/post_v1_create_post_comment.go create mode 100644 server/internal/controller/post/post_v1_delete_post.go create mode 100644 server/internal/controller/post/post_v1_delete_post_comment.go create mode 100644 server/internal/controller/post/post_v1_get_post.go create mode 100644 server/internal/controller/post/post_v1_get_post_comment.go create mode 100644 server/internal/dao/.gitkeep create mode 100644 server/internal/dao/device.go create mode 100644 server/internal/dao/device_dance.go create mode 100644 server/internal/dao/device_friend.go create mode 100644 server/internal/dao/device_post.go create mode 100644 server/internal/dao/device_post_comment.go create mode 100644 server/internal/dao/internal/device.go create mode 100644 server/internal/dao/internal/device_dance.go create mode 100644 server/internal/dao/internal/device_friend.go create mode 100644 server/internal/dao/internal/device_post.go create mode 100644 server/internal/dao/internal/device_post_comment.go create mode 100644 server/internal/dao/internal/sqlite_sequence.go create mode 100644 server/internal/dao/sqlite_sequence.go create mode 100644 server/internal/logic/.gitkeep create mode 100644 server/internal/model/.gitkeep create mode 100644 server/internal/model/device.go create mode 100644 server/internal/model/do/device.go create mode 100644 server/internal/model/do/device_dance.go create mode 100644 server/internal/model/do/device_friend.go create mode 100644 server/internal/model/do/device_post.go create mode 100644 server/internal/model/do/device_post_comment.go create mode 100644 server/internal/model/do/sqlite_sequence.go create mode 100644 server/internal/model/entity/device.go create mode 100644 server/internal/model/entity/device_dance.go create mode 100644 server/internal/model/entity/device_friend.go create mode 100644 server/internal/model/entity/device_post.go create mode 100644 server/internal/model/entity/device_post_comment.go create mode 100644 server/internal/model/entity/sqlite_sequence.go create mode 100644 server/internal/model/expressionData.go create mode 100644 server/internal/model/post.go create mode 100644 server/internal/packed/packed.go create mode 100644 server/internal/service/.gitkeep create mode 100644 server/internal/service/device.go create mode 100644 server/internal/service/device_test.go create mode 100644 server/internal/service/file.go create mode 100644 server/internal/web_socket/web_socket.go create mode 100644 server/main.go create mode 100644 server/manifest/config/config.yaml create mode 100644 server/manifest/deploy/kustomize/base/deployment.yaml create mode 100644 server/manifest/deploy/kustomize/base/kustomization.yaml create mode 100644 server/manifest/deploy/kustomize/base/service.yaml create mode 100644 server/manifest/deploy/kustomize/overlays/develop/configmap.yaml create mode 100644 server/manifest/deploy/kustomize/overlays/develop/deployment.yaml create mode 100644 server/manifest/deploy/kustomize/overlays/develop/kustomization.yaml create mode 100644 server/manifest/docker/Dockerfile create mode 100644 server/manifest/docker/docker.sh create mode 100644 server/manifest/i18n/.gitkeep create mode 100644 server/manifest/protobuf/.keep-if-necessary create mode 100644 server/resource/public/html/.gitkeep create mode 100644 server/resource/public/plugin/.gitkeep create mode 100644 server/resource/public/resource/css/.gitkeep create mode 100644 server/resource/public/resource/image/.gitkeep create mode 100644 server/resource/public/resource/js/.gitkeep create mode 100644 server/resource/template/.gitkeep create mode 100644 server/stackChan.sqlite create mode 100644 server/utility/.gitkeep diff --git a/.gitignore b/.gitignore index 496ee2c..00741cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.DS_Store \ No newline at end of file +.DS_Store +.idea/ \ No newline at end of file diff --git a/server/Makefile b/server/Makefile new file mode 100644 index 0000000..2a6e6e9 --- /dev/null +++ b/server/Makefile @@ -0,0 +1,7 @@ +ROOT_DIR = $(shell pwd) +NAMESPACE = "default" +DEPLOY_NAME = "template-single" +DOCKER_NAME = "template-single" + +include ./hack/hack-cli.mk +include ./hack/hack.mk \ No newline at end of file diff --git a/server/README.md b/server/README.md index b003416..3d43d8c 100644 --- a/server/README.md +++ b/server/README.md @@ -1 +1,44 @@ -# StackChan Server \ No newline at end of file +# StackChan Server + +**StackChan Server** is the Server of the open-source StackChan project. It handles core functionalities such +as device interactions, post management, and comment systems, providing stable and efficient API support. + +--- + +## Features + +- App and StackChan communication and interaction +- Device post creation and management (text and image support, similar to a social feed) +- Comment CRUD (Create, Read, Update, Delete) operations +- Dance control and data management +- Persistent storage using a relational database + +--- + +## Getting Started + +### Prerequisites + +- **Go**: The project is developed in Go. Install **Go 1.24+** from + the [official download page](https://golang.google.cn/dl/). + +Verify installation: + +```bash +go version +# Expected output: "go version go1.24.x ..." (or similar) + +### Clone the Repository +```bash +git clone https://github.com/m5stack/StackChan # Replace with the actual repository URL +cd StackChan/server + +# Download dependencies +go mod download + +# build +go build -o StackChan main.go + +# Start running +StackChan # Linux/macOS +StackChan.exe # Windows diff --git a/server/api/dance/dance.go b/server/api/dance/dance.go new file mode 100644 index 0000000..aa3d948 --- /dev/null +++ b/server/api/dance/dance.go @@ -0,0 +1,18 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package dance + +import ( + "context" + + "stackChan/api/dance/v1" +) + +type IDanceV1 interface { + Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) + Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error) + Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) + GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error) +} diff --git a/server/api/dance/v1/dance.go b/server/api/dance/v1/dance.go new file mode 100644 index 0000000..a592392 --- /dev/null +++ b/server/api/dance/v1/dance.go @@ -0,0 +1,45 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package v1 + +import ( + "stackChan/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type CreateReq struct { + g.Meta `path:"/dance" method:"post" tags:"Dance" summary:"Dance create request"` + Mac string `json:"mac" v:"required"` + Index int `json:"index" v:"required"` + List []model.DanceData `json:"list" v:"required"` +} + +type CreateRes string + +type DeleteReq struct { + g.Meta `path:"/dance" method:"delete" tags:"Dance" summary:"Dance delete request"` + Mac string `json:"mac" v:"required"` + Index int `json:"index" v:"required"` +} + +type DeleteRes string + +type UpdateReq struct { + g.Meta `path:"/dance" method:"put" tags:"Dance" summary:"Dance put request"` + Mac string `json:"mac" v:"required"` + Index int `json:"index" v:"required"` + Data []model.DanceData `json:"list" v:"required"` +} + +type UpdateRes string + +type GetListReq struct { + g.Meta `path:"/dance" method:"get" tags:"Dance" summary:"Dance get request"` + Mac string `json:"mac" v:"required"` +} + +type GetListRes map[string][]model.DanceData diff --git a/server/api/device/device.go b/server/api/device/device.go new file mode 100644 index 0000000..edf8d0f --- /dev/null +++ b/server/api/device/device.go @@ -0,0 +1,19 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package device + +import ( + "context" + + "stackChan/api/device/v1" +) + +type IDeviceV1 interface { + Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) + Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) + GetRandomDevice(ctx context.Context, req *v1.GetRandomDeviceReq) (res *v1.GetRandomDeviceRes, err error) + GetDeviceInfo(ctx context.Context, req *v1.GetDeviceInfoReq) (res *v1.GetDeviceInfoRes, err error) + UpdateDeviceInfo(ctx context.Context, req *v1.UpdateDeviceInfoReq) (res *v1.UpdateDeviceInfoRes, err error) +} diff --git a/server/api/device/v1/device.go b/server/api/device/v1/device.go new file mode 100644 index 0000000..d8f338b --- /dev/null +++ b/server/api/device/v1/device.go @@ -0,0 +1,53 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package v1 + +import ( + "stackChan/internal/model" + "stackChan/internal/model/entity" + + "github.com/gogf/gf/v2/frame/g" +) + +type CreateReq struct { + g.Meta `path:"/device" method:"post" tags:"Device" summary:"Device create request"` + Mac string `json:"mac" v:"required" description:"Mac address"` + Name string `json:"name,omitempty" description:"Device name"` +} + +type CreateRes struct { + Id int64 `json:"id" dc:"Device id"` +} + +type UpdateReq struct { + g.Meta `path:"/device" method:"put" tags:"Device" summary:"Device update request"` + Mac string `json:"mac" v:"required" description:"Mac address"` + Name string `json:"name" description:"Device name"` +} + +type UpdateRes struct{} + +type GetRandomDeviceReq struct { + g.Meta `path:"/device/randomList" method:"get" tags:"Device" summary:"Device get Random"` + Mac string `json:"mac" v:"required" description:"Mac address"` +} + +type GetRandomDeviceRes []entity.Device + +type GetDeviceInfoReq struct { + g.Meta `path:"/device/info" method:"get" tags:"Device" summary:"Device Info Get request"` + Mac string `json:"mac" v:"required" description:"Mac address"` +} + +type GetDeviceInfoRes model.DeviceInfo + +type UpdateDeviceInfoReq struct { + g.Meta `path:"/device/info" method:"put" tags:"Device" summary:"Device Info Put request"` + Mac string `json:"mac" v:"required" description:"Mac address"` + Name string `json:"name" description:"Device name"` +} + +type UpdateDeviceInfoRes string diff --git a/server/api/file/file.go b/server/api/file/file.go new file mode 100644 index 0000000..e812a0c --- /dev/null +++ b/server/api/file/file.go @@ -0,0 +1,15 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package file + +import ( + "context" + + "stackChan/api/file/v1" +) + +type IFileV1 interface { + File(ctx context.Context, req *v1.FileReq) (res *v1.FileRes, err error) +} diff --git a/server/api/file/v1/file.go b/server/api/file/v1/file.go new file mode 100644 index 0000000..4a9e22d --- /dev/null +++ b/server/api/file/v1/file.go @@ -0,0 +1,22 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package v1 + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" +) + +type FileReq struct { + g.Meta `path:"/uploadFile" method:"post" tags:"File" summary:"File upload request"` + File *ghttp.UploadFile `json:"file" v:"required" description:"File upload request"` + Name string `json:"name" v:"required" description:"File Name"` + Directory string `json:"directory" description:"Directory upload request"` +} + +type FileRes struct { + Path string `json:"path" description:"file path"` +} diff --git a/server/api/friend/friend.go b/server/api/friend/friend.go new file mode 100644 index 0000000..7357046 --- /dev/null +++ b/server/api/friend/friend.go @@ -0,0 +1,15 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package friend + +import ( + "context" + + "stackChan/api/friend/v1" +) + +type IFriendV1 interface { + Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error) +} diff --git a/server/api/friend/v1/friend.go b/server/api/friend/v1/friend.go new file mode 100644 index 0000000..d8162e5 --- /dev/null +++ b/server/api/friend/v1/friend.go @@ -0,0 +1,16 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package v1 + +import "github.com/gogf/gf/v2/frame/g" + +type AddReq struct { + g.Meta `path:"/friend" method:"post" tags:"Friend" summary:"Friend add request"` + Mac string `json:"mac" v:"required" description:"Mac address"` + FriendMac string `json:"friendMac" v:"required" description:"Friend Mac address"` +} + +type AddRes string diff --git a/server/api/post/post.go b/server/api/post/post.go new file mode 100644 index 0000000..640734b --- /dev/null +++ b/server/api/post/post.go @@ -0,0 +1,20 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package post + +import ( + "context" + + "stackChan/api/post/v1" +) + +type IPostV1 interface { + CreatePost(ctx context.Context, req *v1.CreatePostReq) (res *v1.CreatePostRes, err error) + GetPost(ctx context.Context, req *v1.GetPostReq) (res *v1.GetPostRes, err error) + DeletePost(ctx context.Context, req *v1.DeletePostReq) (res *v1.DeletePostRes, err error) + CreatePostComment(ctx context.Context, req *v1.CreatePostCommentReq) (res *v1.CreatePostCommentRes, err error) + DeletePostComment(ctx context.Context, req *v1.DeletePostCommentReq) (res *v1.DeletePostCommentRes, err error) + GetPostComment(ctx context.Context, req *v1.GetPostCommentReq) (res *v1.GetPostCommentRes, err error) +} diff --git a/server/api/post/v1/post.go b/server/api/post/v1/post.go new file mode 100644 index 0000000..8494915 --- /dev/null +++ b/server/api/post/v1/post.go @@ -0,0 +1,70 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package v1 + +import ( + "stackChan/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type CreatePostReq struct { + g.Meta `path:"/post/add" method:"post" tags:"Post" summary:"Post create request"` + Mac string `json:"mac" v:"required" description:"Mac address"` + ContentText string `json:"content_text" v:"required" description:"Content text"` + ContentImage string `json:"content_image" v:"required" description:"Content image"` +} + +type CreatePostRes struct { + Id int64 `json:"id"` +} + +type GetPostReq struct { + g.Meta `path:"/post/get" method:"get" tags:"Post" summary:"Post get request"` + Page int `json:"page" v:"required#Page不能为空" description:"页码"` + PageSize int `json:"pageSize" v:"required#每页数量不能为空" description:"每页条数"` +} + +type GetPostRes []model.Post + +type DeletePostReq struct { + g.Meta `path:"/post/delete" method:"delete" tags:"Post" summary:"Post delete request"` + Id int `json:"id" summary:"Post id"` +} + +type DeletePostRes string + +type CreatePostCommentReq struct { + g.Meta `path:"/post/comment/create" method:"post" tags:"Post" summary:"Post create comment"` + Mac string `json:"mac" v:"required" description:"Mac address"` + PostId int64 `json:"postId" v:"required" summary:"Post comment id"` + Content string `json:"content" description:"评论内容"` +} + +type CreatePostCommentRes struct { + Id int64 `json:"id"` +} + +type DeletePostCommentReq struct { + g.Meta `path:"/post/comment/delete" method:"post" tags:"Post" summary:"Post delete comment"` + Mac string `json:"mac" v:"required" description:"Mac address"` + Id int `json:"id" summary:"Post comment id"` +} + +type DeletePostCommentRes struct{} + +type GetPostCommentReq struct { + g.Meta `path:"/post/comment/get" method:"get" tags:"Post" summary:"Post get comment"` + PostId int64 `json:"postId" summary:"Post comment id"` + Mac string `json:"mac" v:"required" description:"Mac address"` + Page int `json:"page" summary:"Post comment page"` + PageSize int `json:"pageSize" summary:"Post comment page"` +} + +type GetPostCommentRes struct { + List []*model.PostComment `json:"list"` + Total int `json:"total"` +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..ab51781 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,46 @@ +module stackChan + +go 1.24.0 + +require ( + github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.7 + github.com/gorilla/websocket v1.5.3 +) + +require ( + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj/v2 v2.7.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grokify/html-strip-tags-go v0.1.0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.0.9 // indirect + github.com/olekukonko/tablewriter v1.1.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..cce0c82 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,100 @@ +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= +github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.7 h1:mzs0MblNT0pOlUB00c/hTcAenQ5N/cB651wh9VCJitc= +github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.7/go.mod h1:Z2MgGyag0fZQ2+9ykafD7tQhau9h5ie3Chg/GUzfy5E= +github.com/gogf/gf/v2 v2.9.7 h1:Vp3VGZ7drPs89tZslT6j6BKBTaw7Xs3DMGWx4MlVtMA= +github.com/gogf/gf/v2 v2.9.7/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= +github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= +github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= +github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= diff --git a/server/hack/config.yaml b/server/hack/config.yaml new file mode 100644 index 0000000..4ba7553 --- /dev/null +++ b/server/hack/config.yaml @@ -0,0 +1,12 @@ +#SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +#SPDX-License-Identifier: MIT +gfcli: + gen: + dao: + - link: "sqlite::@file(/stackChan.sqlite)" + descriptionTag: true + + docker: + build: "-a amd64 -s linux -p temp -ew" + tagPrefixes: + - my.image.pub/my-app \ No newline at end of file diff --git a/server/hack/hack-cli.mk b/server/hack/hack-cli.mk new file mode 100644 index 0000000..8f8eb69 --- /dev/null +++ b/server/hack/hack-cli.mk @@ -0,0 +1,21 @@ +#SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +#SPDX-License-Identifier: MIT +# Install/Update to the latest CLI tool. +.PHONY: cli +cli: + @set -e; \ + wget -O gf \ + https://github.com/gogf/gf/releases/latest/download/gf_$(shell go env GOOS)_$(shell go env GOARCH) && \ + chmod +x gf && \ + ./gf install -y && \ + rm ./gf + + +# Check and install CLI tool. +.PHONY: cli.install +cli.install: + @set -e; \ + gf -v > /dev/null 2>&1 || if [[ "$?" -ne "0" ]]; then \ + echo "GoFame CLI is not installed, start proceeding auto installation..."; \ + make cli; \ + fi; \ No newline at end of file diff --git a/server/hack/hack.mk b/server/hack/hack.mk new file mode 100644 index 0000000..ba9b4c8 --- /dev/null +++ b/server/hack/hack.mk @@ -0,0 +1,78 @@ +#SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +#SPDX-License-Identifier: MIT + +.DEFAULT_GOAL := build + +# Update GoFrame and its CLI to latest stable version. +.PHONY: up +up: cli.install + @gf up -a + +# Build binary using configuration from hack/config.yaml. +.PHONY: build +build: cli.install + @gf build -ew + +# Parse api and generate controller/sdk. +.PHONY: ctrl +ctrl: cli.install + @gf gen ctrl + +# Generate Go files for DAO/DO/Entity. +.PHONY: dao +dao: cli.install + @gf gen dao + +# Parse current project go files and generate enums go file. +.PHONY: enums +enums: cli.install + @gf gen enums + +# Generate Go files for Service. +.PHONY: service +service: cli.install + @gf gen service + + +# Build docker image. +.PHONY: image +image: cli.install + $(eval _TAG = $(shell git rev-parse --short HEAD)) +ifneq (, $(shell git status --porcelain 2>/dev/null)) + $(eval _TAG = $(_TAG).dirty) +endif + $(eval _TAG = $(if ${TAG}, ${TAG}, $(_TAG))) + $(eval _PUSH = $(if ${PUSH}, ${PUSH}, )) + @gf docker ${_PUSH} -tn $(DOCKER_NAME):${_TAG}; + + +# Build docker image and automatically push to docker repo. +.PHONY: image.push +image.push: cli.install + @make image PUSH=-p; + + +# Deploy image and yaml to current kubectl environment. +.PHONY: deploy +deploy: cli.install + $(eval _TAG = $(if ${TAG}, ${TAG}, develop)) + + @set -e; \ + mkdir -p $(ROOT_DIR)/temp/kustomize;\ + cd $(ROOT_DIR)/manifest/deploy/kustomize/overlays/${_ENV};\ + kustomize build > $(ROOT_DIR)/temp/kustomize.yaml;\ + kubectl apply -f $(ROOT_DIR)/temp/kustomize.yaml; \ + if [ $(DEPLOY_NAME) != "" ]; then \ + kubectl patch -n $(NAMESPACE) deployment/$(DEPLOY_NAME) -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"$(shell date +%s)\"}}}}}"; \ + fi; + + +# Parsing protobuf files and generating go files. +.PHONY: pb +pb: cli.install + @gf gen pb + +# Generate protobuf files for database tables. +.PHONY: pbentity +pbentity: cli.install + @gf gen pbentity \ No newline at end of file diff --git a/server/internal/cmd/cmd.go b/server/internal/cmd/cmd.go new file mode 100644 index 0000000..ca75193 --- /dev/null +++ b/server/internal/cmd/cmd.go @@ -0,0 +1,92 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package cmd + +import ( + "context" + "fmt" + "net" + "net/http" + "path/filepath" + "stackChan/internal/controller/dance" + "stackChan/internal/controller/device" + "stackChan/internal/controller/file" + "stackChan/internal/controller/friend" + "stackChan/internal/controller/post" + "stackChan/internal/web_socket" + "time" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gcmd" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gtimer" +) + +var ( + Main = gcmd.Command{ + Name: "main", + Usage: "main", + Brief: "start http server", + Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { + PrintIPAddr() + + //Start a scheduled task to send ping messages + gtimer.SetInterval(ctx, time.Second*5, func(ctx context.Context) { + web_socket.StartPingTime(ctx) + }) + //Start a timer to clean up long-lived connections that have been inactive for a long time on the app. + gtimer.SetInterval(ctx, time.Second*15, func(ctx context.Context) { + web_socket.CheckExpiredLinks(ctx) + }) + + s := g.Server() + s.BindHandler("/stackChan/ws", web_socket.Handler) + + ///Configuration file access + s.Group("/file", func(group *ghttp.RouterGroup) { + group.GET("/*filepath", func(r *ghttp.Request) { + relativePath := r.Get("filepath").String() + if relativePath == "" { + r.Response.WriteHeader(http.StatusNotFound) + r.Response.Write("File not found") + return + } + filePath := filepath.Join("file", relativePath) + if !gfile.Exists(filePath) { + r.Response.WriteHeader(http.StatusNotFound) + r.Response.Write("File not found") + return + } + r.Response.ServeFile(filePath) + }) + }) + + s.Group("/stackChan", func(group *ghttp.RouterGroup) { + group.Middleware(ghttp.MiddlewareHandlerResponse) + group.Bind(device.NewV1(), friend.NewV1(), dance.NewV1(), file.NewV1(), post.NewV1()) + }) + s.Run() + return nil + }, + } +) + +func PrintIPAddr() { + addrs, err := net.InterfaceAddrs() + if err == nil { + fmt.Println("Local IP addresses detected on this machine:") + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil { + fmt.Println(" -", ipnet.IP.String()) + } + } + } else { + fmt.Println("Could not detect local IP addresses:", err) + } + fmt.Println("Please update the StackChan and iOS client access addresses to use one of the above local IPs as needed.") + +} diff --git a/server/internal/consts/consts.go b/server/internal/consts/consts.go new file mode 100644 index 0000000..c449029 --- /dev/null +++ b/server/internal/consts/consts.go @@ -0,0 +1,6 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package consts diff --git a/server/internal/controller/dance/dance.go b/server/internal/controller/dance/dance.go new file mode 100644 index 0000000..4fe2f49 --- /dev/null +++ b/server/internal/controller/dance/dance.go @@ -0,0 +1,9 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package dance diff --git a/server/internal/controller/dance/dance_new.go b/server/internal/controller/dance/dance_new.go new file mode 100644 index 0000000..fbf8156 --- /dev/null +++ b/server/internal/controller/dance/dance_new.go @@ -0,0 +1,19 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package dance + +import ( + "stackChan/api/dance" +) + +type ControllerV1 struct{} + +func NewV1() dance.IDanceV1 { + return &ControllerV1{} +} diff --git a/server/internal/controller/dance/dance_v1_create.go b/server/internal/controller/dance/dance_v1_create.go new file mode 100644 index 0000000..430cca8 --- /dev/null +++ b/server/internal/controller/dance/dance_v1_create.go @@ -0,0 +1,67 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package dance + +import ( + "context" + "encoding/json" + "stackChan/internal/dao" + "stackChan/internal/model/do" + + "stackChan/api/dance/v1" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) { + if req.Index < 0 { + return nil, gerror.NewCode(gcode.CodeInvalidParameter, "Index cannot be negative") + } + + device, err := dao.Device.Ctx(ctx).Where("mac=", req.Mac).One() + + if err != nil { + return nil, err + } + + if device.IsEmpty() { + _, err = dao.Device.Ctx(ctx).Data(dao.Device.Columns().Mac, req.Mac).Insert() + if err != nil { + return nil, err + } + } + + dance, err := dao.DeviceDance.Ctx(ctx).Where("mac=?", req.Mac).Where("dance_index=?", req.Index).One() + if err != nil { + return nil, err + } + + danceListJSON, err := json.Marshal(req.List) + if err != nil { + return nil, err + } + + if dance.IsEmpty() { + _, err = dao.DeviceDance.Ctx(ctx).Data(do.DeviceDance{ + Mac: req.Mac, + DanceIndex: req.Index, + DanceData: danceListJSON, + }).Insert() + if err != nil { + return nil, err + } + } else { + _, err = dao.DeviceDance.Ctx(ctx).Where("mac=?", req.Mac).Where("dance_index=?", req.Index).Data(do.DeviceDance{ + DanceData: danceListJSON, + }).Update() + if err != nil { + return nil, err + } + } + response := v1.CreateRes("Dance data saved successfully") + return &response, nil +} diff --git a/server/internal/controller/dance/dance_v1_delete.go b/server/internal/controller/dance/dance_v1_delete.go new file mode 100644 index 0000000..4d85354 --- /dev/null +++ b/server/internal/controller/dance/dance_v1_delete.go @@ -0,0 +1,18 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package dance + +import ( + "context" + "stackChan/internal/dao" + + "stackChan/api/dance/v1" +) + +func (c *ControllerV1) Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error) { + _, err = dao.DeviceDance.Ctx(ctx).Where("mac=", req.Mac).Where("dance_index=", req.Index).Delete() + return +} diff --git a/server/internal/controller/dance/dance_v1_get_list.go b/server/internal/controller/dance/dance_v1_get_list.go new file mode 100644 index 0000000..b952c6a --- /dev/null +++ b/server/internal/controller/dance/dance_v1_get_list.go @@ -0,0 +1,44 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package dance + +import ( + "context" + "encoding/json" + "stackChan/internal/dao" + "stackChan/internal/model" + "stackChan/internal/model/do" + "stackChan/internal/model/entity" + "strconv" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + + "stackChan/api/dance/v1" +) + +func (c *ControllerV1) GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error) { + danceMap := make(map[string][]model.DanceData) + var list []entity.DeviceDance + err = dao.DeviceDance.Ctx(ctx).Where(do.DeviceDance{ + Mac: req.Mac, + }).Scan(&list) + if err != nil { + return nil, err + } + if len(list) > 0 { + deviceDance := list[0] + var danceList []model.DanceData + err = json.Unmarshal([]byte(deviceDance.DanceData), &danceList) + if err != nil { + return nil, gerror.WrapCode(gcode.CodeInvalidParameter, err) + } + key := strconv.Itoa(deviceDance.DanceIndex) + danceMap[key] = danceList + } + response := v1.GetListRes(danceMap) + return &response, nil +} diff --git a/server/internal/controller/dance/dance_v1_update.go b/server/internal/controller/dance/dance_v1_update.go new file mode 100644 index 0000000..05cc242 --- /dev/null +++ b/server/internal/controller/dance/dance_v1_update.go @@ -0,0 +1,31 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package dance + +import ( + "context" + "encoding/json" + "stackChan/internal/dao" + "stackChan/internal/model/do" + + "stackChan/api/dance/v1" +) + +func (c *ControllerV1) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) { + response := v1.UpdateRes("") + danceJSON, err := json.Marshal(req.Data) + if err != nil { + return nil, err + } + _, err = dao.DeviceDance.Ctx(ctx).Where("mac=?", req.Mac).Where("dance_index=?", req.Index).Data(do.DeviceDance{ + DanceData: danceJSON, + }).Update() + if err != nil { + return nil, err + } + response = "Update successful" + return &response, nil +} diff --git a/server/internal/controller/device/device.go b/server/internal/controller/device/device.go new file mode 100644 index 0000000..7f890a7 --- /dev/null +++ b/server/internal/controller/device/device.go @@ -0,0 +1,10 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package device diff --git a/server/internal/controller/device/device_new.go b/server/internal/controller/device/device_new.go new file mode 100644 index 0000000..d23b6dc --- /dev/null +++ b/server/internal/controller/device/device_new.go @@ -0,0 +1,20 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package device + +import ( + "stackChan/api/device" +) + +type ControllerV1 struct{} + +func NewV1() device.IDeviceV1 { + return &ControllerV1{} +} diff --git a/server/internal/controller/device/device_v1_create.go b/server/internal/controller/device/device_v1_create.go new file mode 100644 index 0000000..316abe9 --- /dev/null +++ b/server/internal/controller/device/device_v1_create.go @@ -0,0 +1,27 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package device + +import ( + "context" + "stackChan/api/device/v1" + "stackChan/internal/dao" + "stackChan/internal/model/do" +) + +func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) { + insertId, err := dao.Device.Ctx(ctx).Data(do.Device{ + Mac: req.Mac, + Name: req.Name, + }).InsertAndGetId() + if err != nil { + return nil, err + } + res = &v1.CreateRes{ + Id: insertId, + } + return +} diff --git a/server/internal/controller/device/device_v1_get_device_info.go b/server/internal/controller/device/device_v1_get_device_info.go new file mode 100644 index 0000000..6a317ea --- /dev/null +++ b/server/internal/controller/device/device_v1_get_device_info.go @@ -0,0 +1,24 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package device + +import ( + "context" + "stackChan/internal/dao" + "stackChan/internal/model" + + "stackChan/api/device/v1" +) + +func (c *ControllerV1) GetDeviceInfo(ctx context.Context, req *v1.GetDeviceInfoReq) (res *v1.GetDeviceInfoRes, err error) { + var info model.DeviceInfo + err = dao.Device.Ctx(ctx).WherePri(req.Mac).Scan(&info) + if err != nil { + return nil, err + } + res = (*v1.GetDeviceInfoRes)(&info) + return res, nil +} diff --git a/server/internal/controller/device/device_v1_get_random_device.go b/server/internal/controller/device/device_v1_get_random_device.go new file mode 100644 index 0000000..8a56f5b --- /dev/null +++ b/server/internal/controller/device/device_v1_get_random_device.go @@ -0,0 +1,37 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package device + +import ( + "context" + "stackChan/api/device/v1" + "stackChan/internal/dao" + "stackChan/internal/model/entity" + "stackChan/internal/web_socket" +) + +func (c *ControllerV1) GetRandomDevice(ctx context.Context, req *v1.GetRandomDeviceReq) (res *v1.GetRandomDeviceRes, err error) { + + // Obtain the list of online StackChan mac addresses (excluding the current user) from the websocket layer. + macList := web_socket.GetRandomStackChanDevice(req.Mac, 6) + + if len(macList) == 0 { + res = (*v1.GetRandomDeviceRes)(&[]entity.Device{}) + return res, nil + } + + // Query device information based on the Mac list + list := make([]entity.Device, 0, len(macList)) + err = dao.Device. + Ctx(ctx). + WhereIn("mac", macList). + Scan(&list) + if err != nil { + return nil, err + } + res = (*v1.GetRandomDeviceRes)(&list) + return res, nil +} diff --git a/server/internal/controller/device/device_v1_update.go b/server/internal/controller/device/device_v1_update.go new file mode 100644 index 0000000..5dcc21b --- /dev/null +++ b/server/internal/controller/device/device_v1_update.go @@ -0,0 +1,21 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package device + +import ( + "context" + "stackChan/internal/dao" + "stackChan/internal/model/do" + + "stackChan/api/device/v1" +) + +func (c *ControllerV1) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) { + _, err = dao.Device.Ctx(ctx).Data(do.Device{ + Name: req.Name, + }).WherePri(req.Mac).Update() + return +} diff --git a/server/internal/controller/device/device_v1_update_device_info.go b/server/internal/controller/device/device_v1_update_device_info.go new file mode 100644 index 0000000..52dc4ed --- /dev/null +++ b/server/internal/controller/device/device_v1_update_device_info.go @@ -0,0 +1,27 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package device + +import ( + "context" + "stackChan/internal/dao" + "stackChan/internal/model/do" + + "stackChan/api/device/v1" +) + +func (c *ControllerV1) UpdateDeviceInfo(ctx context.Context, req *v1.UpdateDeviceInfoReq) (res *v1.UpdateDeviceInfoRes, err error) { + doDevice := do.Device{} + if req.Name != "" { + doDevice.Name = req.Name + } + _, err = dao.Device.Ctx(ctx).Data(doDevice).WherePri(req.Mac).Update() + if err != nil { + return nil, err + } + response := v1.UpdateDeviceInfoRes("Update successful") + return &response, nil +} diff --git a/server/internal/controller/file/file.go b/server/internal/controller/file/file.go new file mode 100644 index 0000000..4d56e5f --- /dev/null +++ b/server/internal/controller/file/file.go @@ -0,0 +1,10 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package file diff --git a/server/internal/controller/file/file_new.go b/server/internal/controller/file/file_new.go new file mode 100644 index 0000000..474cbde --- /dev/null +++ b/server/internal/controller/file/file_new.go @@ -0,0 +1,20 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package file + +import ( + "stackChan/api/file" +) + +type ControllerV1 struct{} + +func NewV1() file.IFileV1 { + return &ControllerV1{} +} diff --git a/server/internal/controller/file/file_v1_file.go b/server/internal/controller/file/file_v1_file.go new file mode 100644 index 0000000..de43cf9 --- /dev/null +++ b/server/internal/controller/file/file_v1_file.go @@ -0,0 +1,17 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package file + +import ( + "context" + "stackChan/internal/service" + + "stackChan/api/file/v1" +) + +func (c *ControllerV1) File(ctx context.Context, req *v1.FileReq) (res *v1.FileRes, err error) { + return service.AddFile(ctx, req) +} diff --git a/server/internal/controller/friend/friend.go b/server/internal/controller/friend/friend.go new file mode 100644 index 0000000..be62380 --- /dev/null +++ b/server/internal/controller/friend/friend.go @@ -0,0 +1,10 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package friend diff --git a/server/internal/controller/friend/friend_new.go b/server/internal/controller/friend/friend_new.go new file mode 100644 index 0000000..a72845a --- /dev/null +++ b/server/internal/controller/friend/friend_new.go @@ -0,0 +1,20 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package friend + +import ( + "stackChan/api/friend" +) + +type ControllerV1 struct{} + +func NewV1() friend.IFriendV1 { + return &ControllerV1{} +} diff --git a/server/internal/controller/friend/friend_v1_add.go b/server/internal/controller/friend/friend_v1_add.go new file mode 100644 index 0000000..0255381 --- /dev/null +++ b/server/internal/controller/friend/friend_v1_add.go @@ -0,0 +1,54 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package friend + +import ( + "context" + "stackChan/internal/dao" + "stackChan/internal/model/entity" + + "github.com/gogf/gf/v2/errors/gerror" + + "stackChan/api/friend/v1" +) + +func (c *ControllerV1) Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error) { + if req.Mac == req.FriendMac { + return nil, gerror.New("You cannot add yourself as a friend") + } + macA := req.Mac + macB := req.FriendMac + var friend entity.DeviceFriend + err = dao.DeviceFriend.Ctx(ctx). + Where("mac_a", macA). + Where("mac_b", macB). + Scan(&friend) + if err != nil { + return nil, err + } + if friend.MacA == "" { + err = dao.DeviceFriend.Ctx(ctx). + Where("mac_a", macB). + Where("mac_b", macA). + Scan(&friend) + if err != nil { + return nil, err + } + } + if friend.MacA != "" { + res1 := v1.AddRes("Successfully added a friend") + return &res1, nil + } + _, err = dao.DeviceFriend.Ctx(ctx).Data(entity.DeviceFriend{ + MacA: macA, + MacB: macB, + }).Insert() + if err != nil { + return nil, err + } + res2 := v1.AddRes("Successfully added a friend") + return &res2, nil +} diff --git a/server/internal/controller/post/post.go b/server/internal/controller/post/post.go new file mode 100644 index 0000000..934db5f --- /dev/null +++ b/server/internal/controller/post/post.go @@ -0,0 +1,10 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package post diff --git a/server/internal/controller/post/post_new.go b/server/internal/controller/post/post_new.go new file mode 100644 index 0000000..9864bd7 --- /dev/null +++ b/server/internal/controller/post/post_new.go @@ -0,0 +1,20 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package post + +import ( + "stackChan/api/post" +) + +type ControllerV1 struct{} + +func NewV1() post.IPostV1 { + return &ControllerV1{} +} diff --git a/server/internal/controller/post/post_v1_create_post.go b/server/internal/controller/post/post_v1_create_post.go new file mode 100644 index 0000000..2365178 --- /dev/null +++ b/server/internal/controller/post/post_v1_create_post.go @@ -0,0 +1,39 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package post + +import ( + "context" + "stackChan/internal/dao" + "stackChan/internal/model/do" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + + "stackChan/api/post/v1" +) + +func (c *ControllerV1) CreatePost(ctx context.Context, req *v1.CreatePostReq) (res *v1.CreatePostRes, err error) { + device, err := dao.Device.Ctx(ctx).Where("mac", req.Mac).One() + if err != nil { + return nil, err + } + if device == nil { + return nil, gerror.NewCode(gcode.CodeInvalidRequest, "The device does not exist or the Mac address is incorrect") + } + insertId, err := dao.DevicePost.Ctx(ctx).Data(do.DevicePost{ + Mac: req.Mac, + ContentText: req.ContentText, + ContentImage: req.ContentImage, + }).InsertAndGetId() + if err != nil { + return nil, err + } + res = &v1.CreatePostRes{ + Id: insertId, + } + return res, nil +} diff --git a/server/internal/controller/post/post_v1_create_post_comment.go b/server/internal/controller/post/post_v1_create_post_comment.go new file mode 100644 index 0000000..42e1c72 --- /dev/null +++ b/server/internal/controller/post/post_v1_create_post_comment.go @@ -0,0 +1,29 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package post + +import ( + "context" + "stackChan/internal/dao" + "stackChan/internal/model/do" + + "stackChan/api/post/v1" +) + +func (c *ControllerV1) CreatePostComment(ctx context.Context, req *v1.CreatePostCommentReq) (res *v1.CreatePostCommentRes, err error) { + id, err := dao.DevicePostComment.Ctx(ctx).Data(do.DevicePostComment{ + PostId: req.PostId, + Mac: req.Mac, + Content: req.Content, + }).InsertAndGetId() + if err != nil { + return nil, err + } + res = &v1.CreatePostCommentRes{ + Id: id, + } + return res, err +} diff --git a/server/internal/controller/post/post_v1_delete_post.go b/server/internal/controller/post/post_v1_delete_post.go new file mode 100644 index 0000000..c71734c --- /dev/null +++ b/server/internal/controller/post/post_v1_delete_post.go @@ -0,0 +1,21 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package post + +import ( + "context" + "stackChan/api/post/v1" + "stackChan/internal/dao" +) + +func (c *ControllerV1) DeletePost(ctx context.Context, req *v1.DeletePostReq) (res *v1.DeletePostRes, err error) { + _, err = dao.DevicePost.Ctx(ctx).WherePri(req.Id).Delete() + if err != nil { + return nil, err + } + response := v1.DeletePostRes("Deletion successful") + return &response, nil +} diff --git a/server/internal/controller/post/post_v1_delete_post_comment.go b/server/internal/controller/post/post_v1_delete_post_comment.go new file mode 100644 index 0000000..395c4f8 --- /dev/null +++ b/server/internal/controller/post/post_v1_delete_post_comment.go @@ -0,0 +1,42 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package post + +import ( + "context" + "errors" + "stackChan/internal/dao" + "stackChan/internal/model" + + "stackChan/api/post/v1" +) + +func (c *ControllerV1) DeletePostComment(ctx context.Context, req *v1.DeletePostCommentReq) (res *v1.DeletePostCommentRes, err error) { + var postComment model.PostComment + err = dao.DevicePostComment.Ctx(ctx).Where("id=?", req.Id).Scan(&postComment) + + if err != nil { + return nil, err + } + + if postComment.Id == 0 { + return nil, errors.New("post not found") + } + + if postComment.Mac != req.Mac { + return nil, errors.New("no authority to delete") + } + + _, err = dao.DevicePostComment. + Ctx(ctx). + Where("id = ? AND mac = ?", req.Id, req.Mac). + Delete() + if err != nil { + return nil, err + } + + return &v1.DeletePostCommentRes{}, nil +} diff --git a/server/internal/controller/post/post_v1_get_post.go b/server/internal/controller/post/post_v1_get_post.go new file mode 100644 index 0000000..1ea3c09 --- /dev/null +++ b/server/internal/controller/post/post_v1_get_post.go @@ -0,0 +1,69 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package post + +import ( + "context" + "stackChan/internal/dao" + "stackChan/internal/model" + "stackChan/internal/model/entity" + + "stackChan/api/post/v1" +) + +func (c *ControllerV1) GetPost(ctx context.Context, req *v1.GetPostReq) (res *v1.GetPostRes, err error) { + page := req.Page + pageSize := req.PageSize + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + + db := dao.DevicePost.Ctx(ctx).As("dp"). + LeftJoin("device d", "dp.mac = d.mac") + + _, err = db.Count("dp.id") + if err != nil { + return nil, err + } + + var list []model.Post + err = db.Fields( + "dp.id", + "dp.mac", + "d.name", + "dp.content_text", + "dp.content_image", + "dp.created_at", + ).Order("dp.created_at DESC").Limit((page-1)*pageSize, pageSize).Scan(&list) + if err != nil { + return nil, err + } + + for i := 0; i < len(list); i++ { + var comments []*model.PostComment + err = dao.DevicePostComment.Ctx(ctx).Where("post_id", list[i].Id).Order("created_at ASC").Scan(&comments) + if err != nil { + return nil, err + } + + for j := 0; j < len(comments); j++ { + mac := comments[j].Mac + var device entity.Device + err = dao.Device.Ctx(ctx).Where("mac", mac).Scan(&device) + if err != nil { + return nil, err + } + comments[j].Name = device.Name + } + + list[i].PostCommentList = comments + } + res = (*v1.GetPostRes)(&list) + return res, nil +} diff --git a/server/internal/controller/post/post_v1_get_post_comment.go b/server/internal/controller/post/post_v1_get_post_comment.go new file mode 100644 index 0000000..69a4eda --- /dev/null +++ b/server/internal/controller/post/post_v1_get_post_comment.go @@ -0,0 +1,57 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package post + +import ( + "context" + "stackChan/api/post/v1" + "stackChan/internal/dao" + "stackChan/internal/model" + "stackChan/internal/model/entity" +) + +func (c *ControllerV1) GetPostComment(ctx context.Context, req *v1.GetPostCommentReq) (res *v1.GetPostCommentRes, err error) { + page := req.Page + pageSize := req.PageSize + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + offset := (page - 1) * pageSize + + var list []*model.PostComment + + db := dao.DevicePostComment.Ctx(ctx).As("dp").Where("mac = ? AND post_id = ?", req.Mac, req.PostId) + + total, err := db.Count() + if err != nil { + return + } + + err = db.Order("created_at ASC").Limit(offset, pageSize).Scan(&list) + if err != nil { + return nil, err + } + + for i := 0; i < len(list); i++ { + mac := list[i].Mac + var device entity.Device + err = dao.Device.Ctx(ctx).Where("mac", mac).Scan(&device) + if err != nil { + return nil, err + } + list[i].Name = device.Name + } + + res = &v1.GetPostCommentRes{ + List: list, + Total: total, + } + + return res, nil +} diff --git a/server/internal/dao/.gitkeep b/server/internal/dao/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/internal/dao/device.go b/server/internal/dao/device.go new file mode 100644 index 0000000..e8d0c4d --- /dev/null +++ b/server/internal/dao/device.go @@ -0,0 +1,27 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "stackChan/internal/dao/internal" +) + +// deviceDao is the data access object for the table device. +// You can define custom methods on it to extend its functionality as needed. +type deviceDao struct { + *internal.DeviceDao +} + +var ( + // Device is a globally accessible object for table device operations. + Device = deviceDao{internal.NewDeviceDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/device_dance.go b/server/internal/dao/device_dance.go new file mode 100644 index 0000000..c941ea3 --- /dev/null +++ b/server/internal/dao/device_dance.go @@ -0,0 +1,27 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "stackChan/internal/dao/internal" +) + +// deviceDanceDao is the data access object for the table device_dance. +// You can define custom methods on it to extend its functionality as needed. +type deviceDanceDao struct { + *internal.DeviceDanceDao +} + +var ( + // DeviceDance is a globally accessible object for table device_dance operations. + DeviceDance = deviceDanceDao{internal.NewDeviceDanceDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/device_friend.go b/server/internal/dao/device_friend.go new file mode 100644 index 0000000..2261390 --- /dev/null +++ b/server/internal/dao/device_friend.go @@ -0,0 +1,27 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "stackChan/internal/dao/internal" +) + +// deviceFriendDao is the data access object for the table device_friend. +// You can define custom methods on it to extend its functionality as needed. +type deviceFriendDao struct { + *internal.DeviceFriendDao +} + +var ( + // DeviceFriend is a globally accessible object for table device_friend operations. + DeviceFriend = deviceFriendDao{internal.NewDeviceFriendDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/device_post.go b/server/internal/dao/device_post.go new file mode 100644 index 0000000..8db6599 --- /dev/null +++ b/server/internal/dao/device_post.go @@ -0,0 +1,27 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "stackChan/internal/dao/internal" +) + +// devicePostDao is the data access object for the table device_post. +// You can define custom methods on it to extend its functionality as needed. +type devicePostDao struct { + *internal.DevicePostDao +} + +var ( + // DevicePost is a globally accessible object for table device_post operations. + DevicePost = devicePostDao{internal.NewDevicePostDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/device_post_comment.go b/server/internal/dao/device_post_comment.go new file mode 100644 index 0000000..d542cdb --- /dev/null +++ b/server/internal/dao/device_post_comment.go @@ -0,0 +1,27 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "stackChan/internal/dao/internal" +) + +// devicePostCommentDao is the data access object for the table device_post_comment. +// You can define custom methods on it to extend its functionality as needed. +type devicePostCommentDao struct { + *internal.DevicePostCommentDao +} + +var ( + // DevicePostComment is a globally accessible object for table device_post_comment operations. + DevicePostComment = devicePostCommentDao{internal.NewDevicePostCommentDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/internal/device.go b/server/internal/dao/internal/device.go new file mode 100644 index 0000000..f0b64c2 --- /dev/null +++ b/server/internal/dao/internal/device.go @@ -0,0 +1,81 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// DeviceDao is the data access object for the table device. +type DeviceDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns DeviceColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// DeviceColumns defines and stores column names for the table device. +type DeviceColumns struct { + Mac string // + Name string // +} + +// deviceColumns holds the columns for the table device. +var deviceColumns = DeviceColumns{ + Mac: "mac", + Name: "name", +} + +// NewDeviceDao creates and returns a new DAO object for table data access. +func NewDeviceDao(handlers ...gdb.ModelHandler) *DeviceDao { + return &DeviceDao{ + group: "default", + table: "device", + columns: deviceColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *DeviceDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *DeviceDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *DeviceDao) Columns() DeviceColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *DeviceDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *DeviceDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *DeviceDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/device_dance.go b/server/internal/dao/internal/device_dance.go new file mode 100644 index 0000000..af52a5d --- /dev/null +++ b/server/internal/dao/internal/device_dance.go @@ -0,0 +1,89 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// DeviceDanceDao is the data access object for the table device_dance. +type DeviceDanceDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns DeviceDanceColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// DeviceDanceColumns defines and stores column names for the table device_dance. +type DeviceDanceColumns struct { + Id string // + Mac string // 设备MAC地址 + DanceIndex string // 舞蹈编号,初始为1~3,可扩展 + DanceData string // MotionData + CreatedAt string // + UpdatedAt string // +} + +// deviceDanceColumns holds the columns for the table device_dance. +var deviceDanceColumns = DeviceDanceColumns{ + Id: "id", + Mac: "mac", + DanceIndex: "dance_index", + DanceData: "dance_data", + CreatedAt: "created_at", + UpdatedAt: "updated_at", +} + +// NewDeviceDanceDao creates and returns a new DAO object for table data access. +func NewDeviceDanceDao(handlers ...gdb.ModelHandler) *DeviceDanceDao { + return &DeviceDanceDao{ + group: "default", + table: "device_dance", + columns: deviceDanceColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *DeviceDanceDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *DeviceDanceDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *DeviceDanceDao) Columns() DeviceDanceColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *DeviceDanceDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *DeviceDanceDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *DeviceDanceDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/device_friend.go b/server/internal/dao/internal/device_friend.go new file mode 100644 index 0000000..292a878 --- /dev/null +++ b/server/internal/dao/internal/device_friend.go @@ -0,0 +1,81 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// DeviceFriendDao is the data access object for the table device_friend. +type DeviceFriendDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns DeviceFriendColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// DeviceFriendColumns defines and stores column names for the table device_friend. +type DeviceFriendColumns struct { + MacA string // + MacB string // +} + +// deviceFriendColumns holds the columns for the table device_friend. +var deviceFriendColumns = DeviceFriendColumns{ + MacA: "mac_a", + MacB: "mac_b", +} + +// NewDeviceFriendDao creates and returns a new DAO object for table data access. +func NewDeviceFriendDao(handlers ...gdb.ModelHandler) *DeviceFriendDao { + return &DeviceFriendDao{ + group: "default", + table: "device_friend", + columns: deviceFriendColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *DeviceFriendDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *DeviceFriendDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *DeviceFriendDao) Columns() DeviceFriendColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *DeviceFriendDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *DeviceFriendDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *DeviceFriendDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/device_post.go b/server/internal/dao/internal/device_post.go new file mode 100644 index 0000000..cc6b587 --- /dev/null +++ b/server/internal/dao/internal/device_post.go @@ -0,0 +1,87 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// DevicePostDao is the data access object for the table device_post. +type DevicePostDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns DevicePostColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// DevicePostColumns defines and stores column names for the table device_post. +type DevicePostColumns struct { + Id string // + Mac string // 发帖设备MAC + ContentText string // 文本内容 + ContentImage string // 图片URL + CreatedAt string // 发帖时间 +} + +// devicePostColumns holds the columns for the table device_post. +var devicePostColumns = DevicePostColumns{ + Id: "id", + Mac: "mac", + ContentText: "content_text", + ContentImage: "content_image", + CreatedAt: "created_at", +} + +// NewDevicePostDao creates and returns a new DAO object for table data access. +func NewDevicePostDao(handlers ...gdb.ModelHandler) *DevicePostDao { + return &DevicePostDao{ + group: "default", + table: "device_post", + columns: devicePostColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *DevicePostDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *DevicePostDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *DevicePostDao) Columns() DevicePostColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *DevicePostDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *DevicePostDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *DevicePostDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/device_post_comment.go b/server/internal/dao/internal/device_post_comment.go new file mode 100644 index 0000000..04f69d7 --- /dev/null +++ b/server/internal/dao/internal/device_post_comment.go @@ -0,0 +1,87 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// DevicePostCommentDao is the data access object for the table device_post_comment. +type DevicePostCommentDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns DevicePostCommentColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// DevicePostCommentColumns defines and stores column names for the table device_post_comment. +type DevicePostCommentColumns struct { + Id string // + PostId string // 帖子ID + Mac string // 评论设备MAC + Content string // 评论内容 + CreatedAt string // 评论时间 +} + +// devicePostCommentColumns holds the columns for the table device_post_comment. +var devicePostCommentColumns = DevicePostCommentColumns{ + Id: "id", + PostId: "post_id", + Mac: "mac", + Content: "content", + CreatedAt: "created_at", +} + +// NewDevicePostCommentDao creates and returns a new DAO object for table data access. +func NewDevicePostCommentDao(handlers ...gdb.ModelHandler) *DevicePostCommentDao { + return &DevicePostCommentDao{ + group: "default", + table: "device_post_comment", + columns: devicePostCommentColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *DevicePostCommentDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *DevicePostCommentDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *DevicePostCommentDao) Columns() DevicePostCommentColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *DevicePostCommentDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *DevicePostCommentDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *DevicePostCommentDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sqlite_sequence.go b/server/internal/dao/internal/sqlite_sequence.go new file mode 100644 index 0000000..6b5a6ad --- /dev/null +++ b/server/internal/dao/internal/sqlite_sequence.go @@ -0,0 +1,81 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SqliteSequenceDao is the data access object for the table sqlite_sequence. +type SqliteSequenceDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SqliteSequenceColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SqliteSequenceColumns defines and stores column names for the table sqlite_sequence. +type SqliteSequenceColumns struct { + Name string // + Seq string // +} + +// sqliteSequenceColumns holds the columns for the table sqlite_sequence. +var sqliteSequenceColumns = SqliteSequenceColumns{ + Name: "name", + Seq: "seq", +} + +// NewSqliteSequenceDao creates and returns a new DAO object for table data access. +func NewSqliteSequenceDao(handlers ...gdb.ModelHandler) *SqliteSequenceDao { + return &SqliteSequenceDao{ + group: "default", + table: "sqlite_sequence", + columns: sqliteSequenceColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SqliteSequenceDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SqliteSequenceDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SqliteSequenceDao) Columns() SqliteSequenceColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SqliteSequenceDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SqliteSequenceDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SqliteSequenceDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/sqlite_sequence.go b/server/internal/dao/sqlite_sequence.go new file mode 100644 index 0000000..241be06 --- /dev/null +++ b/server/internal/dao/sqlite_sequence.go @@ -0,0 +1,27 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "stackChan/internal/dao/internal" +) + +// sqliteSequenceDao is the data access object for the table sqlite_sequence. +// You can define custom methods on it to extend its functionality as needed. +type sqliteSequenceDao struct { + *internal.SqliteSequenceDao +} + +var ( + // SqliteSequence is a globally accessible object for table sqlite_sequence operations. + SqliteSequence = sqliteSequenceDao{internal.NewSqliteSequenceDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/logic/.gitkeep b/server/internal/logic/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/internal/model/.gitkeep b/server/internal/model/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/internal/model/device.go b/server/internal/model/device.go new file mode 100644 index 0000000..b8d7d0a --- /dev/null +++ b/server/internal/model/device.go @@ -0,0 +1,11 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package model + +type DeviceInfo struct { + Mac string `json:"mac" v:"required" description:"Mac address"` + Name string `json:"name" v:"required" description:"Name"` +} diff --git a/server/internal/model/do/device.go b/server/internal/model/do/device.go new file mode 100644 index 0000000..9dedaf7 --- /dev/null +++ b/server/internal/model/do/device.go @@ -0,0 +1,16 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" +) + +// Device is the golang structure of table device for DAO operations like Where/Data. +type Device struct { + g.Meta `orm:"table:device, do:true"` + Mac any // + Name any // +} diff --git a/server/internal/model/do/device_dance.go b/server/internal/model/do/device_dance.go new file mode 100644 index 0000000..0505e36 --- /dev/null +++ b/server/internal/model/do/device_dance.go @@ -0,0 +1,21 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// DeviceDance is the golang structure of table device_dance for DAO operations like Where/Data. +type DeviceDance struct { + g.Meta `orm:"table:device_dance, do:true"` + Id any // + Mac any // 设备MAC地址 + DanceIndex any // 舞蹈编号,初始为1~3,可扩展 + DanceData any // MotionData + CreatedAt *gtime.Time // + UpdatedAt *gtime.Time // +} diff --git a/server/internal/model/do/device_friend.go b/server/internal/model/do/device_friend.go new file mode 100644 index 0000000..e11c509 --- /dev/null +++ b/server/internal/model/do/device_friend.go @@ -0,0 +1,16 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" +) + +// DeviceFriend is the golang structure of table device_friend for DAO operations like Where/Data. +type DeviceFriend struct { + g.Meta `orm:"table:device_friend, do:true"` + MacA any // + MacB any // +} diff --git a/server/internal/model/do/device_post.go b/server/internal/model/do/device_post.go new file mode 100644 index 0000000..7a21a68 --- /dev/null +++ b/server/internal/model/do/device_post.go @@ -0,0 +1,20 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// DevicePost is the golang structure of table device_post for DAO operations like Where/Data. +type DevicePost struct { + g.Meta `orm:"table:device_post, do:true"` + Id any // + Mac any // 发帖设备MAC + ContentText any // 文本内容 + ContentImage any // 图片URL + CreatedAt *gtime.Time // 发帖时间 +} diff --git a/server/internal/model/do/device_post_comment.go b/server/internal/model/do/device_post_comment.go new file mode 100644 index 0000000..1e0d6ca --- /dev/null +++ b/server/internal/model/do/device_post_comment.go @@ -0,0 +1,20 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// DevicePostComment is the golang structure of table device_post_comment for DAO operations like Where/Data. +type DevicePostComment struct { + g.Meta `orm:"table:device_post_comment, do:true"` + Id any // + PostId any // 帖子ID + Mac any // 评论设备MAC + Content any // 评论内容 + CreatedAt *gtime.Time // 评论时间 +} diff --git a/server/internal/model/do/sqlite_sequence.go b/server/internal/model/do/sqlite_sequence.go new file mode 100644 index 0000000..a54348b --- /dev/null +++ b/server/internal/model/do/sqlite_sequence.go @@ -0,0 +1,16 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" +) + +// SqliteSequence is the golang structure of table sqlite_sequence for DAO operations like Where/Data. +type SqliteSequence struct { + g.Meta `orm:"table:sqlite_sequence, do:true"` + Name any // + Seq any // +} diff --git a/server/internal/model/entity/device.go b/server/internal/model/entity/device.go new file mode 100644 index 0000000..bffc16f --- /dev/null +++ b/server/internal/model/entity/device.go @@ -0,0 +1,11 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +// Device is the golang structure for table device. +type Device struct { + Mac string `json:"mac" orm:"mac" description:""` // + Name string `json:"name" orm:"name" description:""` // +} diff --git a/server/internal/model/entity/device_dance.go b/server/internal/model/entity/device_dance.go new file mode 100644 index 0000000..2b01192 --- /dev/null +++ b/server/internal/model/entity/device_dance.go @@ -0,0 +1,19 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// DeviceDance is the golang structure for table device_dance. +type DeviceDance struct { + Id int64 `json:"id" orm:"id" description:""` // + Mac string `json:"mac" orm:"mac" description:"设备MAC地址"` // 设备MAC地址 + DanceIndex int `json:"danceIndex" orm:"dance_index" description:"舞蹈编号,初始为1~3,可扩展"` // 舞蹈编号,初始为1~3,可扩展 + DanceData string `json:"danceData" orm:"dance_data" description:"MotionData"` // MotionData + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` // + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` // +} diff --git a/server/internal/model/entity/device_friend.go b/server/internal/model/entity/device_friend.go new file mode 100644 index 0000000..367e425 --- /dev/null +++ b/server/internal/model/entity/device_friend.go @@ -0,0 +1,11 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +// DeviceFriend is the golang structure for table device_friend. +type DeviceFriend struct { + MacA string `json:"macA" orm:"mac_a" description:""` // + MacB string `json:"macB" orm:"mac_b" description:""` // +} diff --git a/server/internal/model/entity/device_post.go b/server/internal/model/entity/device_post.go new file mode 100644 index 0000000..12b07cd --- /dev/null +++ b/server/internal/model/entity/device_post.go @@ -0,0 +1,18 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// DevicePost is the golang structure for table device_post. +type DevicePost struct { + Id int64 `json:"id" orm:"id" description:""` // + Mac string `json:"mac" orm:"mac" description:"发帖设备MAC"` // 发帖设备MAC + ContentText string `json:"contentText" orm:"content_text" description:"文本内容"` // 文本内容 + ContentImage string `json:"contentImage" orm:"content_image" description:"图片URL"` // 图片URL + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"发帖时间"` // 发帖时间 +} diff --git a/server/internal/model/entity/device_post_comment.go b/server/internal/model/entity/device_post_comment.go new file mode 100644 index 0000000..2b4a07c --- /dev/null +++ b/server/internal/model/entity/device_post_comment.go @@ -0,0 +1,18 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// DevicePostComment is the golang structure for table device_post_comment. +type DevicePostComment struct { + Id int64 `json:"id" orm:"id" description:""` // + PostId int64 `json:"postId" orm:"post_id" description:"帖子ID"` // 帖子ID + Mac string `json:"mac" orm:"mac" description:"评论设备MAC"` // 评论设备MAC + Content string `json:"content" orm:"content" description:"评论内容"` // 评论内容 + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"评论时间"` // 评论时间 +} diff --git a/server/internal/model/entity/sqlite_sequence.go b/server/internal/model/entity/sqlite_sequence.go new file mode 100644 index 0000000..dd1d7a0 --- /dev/null +++ b/server/internal/model/entity/sqlite_sequence.go @@ -0,0 +1,11 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +// SqliteSequence is the golang structure for table sqlite_sequence. +type SqliteSequence struct { + Name string `json:"name" orm:"name" description:""` // + Seq string `json:"seq" orm:"seq" description:""` // +} diff --git a/server/internal/model/expressionData.go b/server/internal/model/expressionData.go new file mode 100644 index 0000000..747aebe --- /dev/null +++ b/server/internal/model/expressionData.go @@ -0,0 +1,42 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package model + +type ExpressionData struct { + Type string `json:"type"` + LeftEye ExpressionItem `json:"leftEye"` + RightEye ExpressionItem `json:"rightEye"` + Mouth ExpressionItem `json:"mouth"` +} + +type ExpressionItem struct { + X int `json:"x"` + Y int `json:"y"` + Rotation int `json:"rotation"` + Weight int `json:"weight"` + Size int `json:"size"` +} + +type MotionData struct { + Type string `json:"type"` + PitchServo MotionDataItem `json:"pitchServo"` + YawServo MotionDataItem `json:"yawServo"` +} + +type MotionDataItem struct { + Angle int `json:"angle"` + Speed int `json:"speed"` + Rotate int `json:"rotate"` +} + +type DanceData struct { + LeftEye ExpressionItem `json:"leftEye"` + RightEye ExpressionItem `json:"rightEye"` + Mouth ExpressionItem `json:"mouth"` + PitchServo MotionDataItem `json:"pitchServo"` + YawServo MotionDataItem `json:"yawServo"` + DurationMs int `json:"durationMs"` +} diff --git a/server/internal/model/post.go b/server/internal/model/post.go new file mode 100644 index 0000000..56e6dd1 --- /dev/null +++ b/server/internal/model/post.go @@ -0,0 +1,27 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package model + +import "github.com/gogf/gf/v2/os/gtime" + +type Post struct { + Id int64 `json:"id" orm:"id" description:"帖子ID"` + Mac string `json:"mac" orm:"mac" description:"发帖设备MAC"` + Name string `json:"name" orm:"name" description:"发帖设备名称"` + ContentText string `json:"contentText" orm:"content_text" description:"文本内容"` + ContentImage string `json:"contentImage" orm:"content_image" description:"图片URL"` + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"发帖时间"` + PostCommentList []*PostComment `json:"postCommentList" orm:"postCommentList" description:"评论"` +} + +type PostComment struct { + Id int `json:"id" orm:"id" description:""` // + PostId int `json:"postId" orm:"post_id" description:""` // + Mac string `json:"mac" orm:"mac" description:""` // + Name string `json:"name" orm:"name" description:""` // + Content string `json:"content" orm:"content" description:""` // + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` // +} diff --git a/server/internal/packed/packed.go b/server/internal/packed/packed.go new file mode 100644 index 0000000..bc3e3f3 --- /dev/null +++ b/server/internal/packed/packed.go @@ -0,0 +1,6 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package packed diff --git a/server/internal/service/.gitkeep b/server/internal/service/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/internal/service/device.go b/server/internal/service/device.go new file mode 100644 index 0000000..cc9b4b0 --- /dev/null +++ b/server/internal/service/device.go @@ -0,0 +1,40 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package service + +import ( + "context" + "stackChan/internal/dao" + "stackChan/internal/model/do" + "stackChan/internal/model/entity" +) + +func CreateMacIfNotExists(ctx context.Context, mac string) (id int64, err error) { + count, err := dao.Device.Ctx(ctx).Where("mac = ?", mac).Count() + if err != nil { + return 0, err + } + if count > 0 { + return 0, nil + } + id, err = dao.Device.Ctx(ctx).Data(do.Device{ + Mac: mac, + }).InsertAndGetId() + + if err != nil { + return 0, err + } + return id, nil +} + +func GetDeviceName(ctx context.Context, mac string) (name string, err error) { + var device entity.Device + err = dao.Device.Ctx(ctx).Where("mac = ?", mac).Fields("name").Scan(&device) + if err != nil { + return "", err + } + return device.Name, nil +} diff --git a/server/internal/service/device_test.go b/server/internal/service/device_test.go new file mode 100644 index 0000000..3331bf1 --- /dev/null +++ b/server/internal/service/device_test.go @@ -0,0 +1,13 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package service + +import ( + "testing" +) + +func TestCreateMac(t *testing.T) { +} diff --git a/server/internal/service/file.go b/server/internal/service/file.go new file mode 100644 index 0000000..71fbe3b --- /dev/null +++ b/server/internal/service/file.go @@ -0,0 +1,70 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package service + +import ( + "context" + "io" + "os" + "path/filepath" + v1 "stackChan/api/file/v1" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +func AddFile(ctx context.Context, req *v1.FileReq) (res *v1.FileRes, err error) { + currentDir, err := os.Getwd() + if err != nil { + return nil, err + } + + baseDir := "file" + fileDir := filepath.Join(currentDir, baseDir) + + if req.Directory != "" { + fileDir = filepath.Join(fileDir, req.Directory) + } + + if _, err := os.Stat(fileDir); os.IsNotExist(err) { + if err := os.MkdirAll(fileDir, os.ModePerm); err != nil { + return nil, err + } + } + + if req.File.Size == 0 || req.Name == "" { + return nil, gerror.NewCode(gcode.CodeInvalidParameter, "file or filename is empty") + } + + filePath := filepath.Join(fileDir, req.Name) + + file, err := req.File.Open() + if err != nil { + return nil, err + } + + fileBytes, err := io.ReadAll(file) + if err != nil { + return nil, err + } + + if err := os.WriteFile(filePath, fileBytes, os.ModePerm); err != nil { + return nil, err + } + + _ = file.Close() + + var returnPath string + if req.Directory != "" { + returnPath = filepath.Join(baseDir, req.Directory, req.Name) + } else { + returnPath = filepath.Join(baseDir, req.Name) + } + + return &v1.FileRes{ + Path: returnPath, + }, nil +} diff --git a/server/internal/web_socket/web_socket.go b/server/internal/web_socket/web_socket.go new file mode 100644 index 0000000..c8f17be --- /dev/null +++ b/server/internal/web_socket/web_socket.go @@ -0,0 +1,830 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package web_socket + +import ( + "context" + "encoding/binary" + "errors" + "math/rand" + "net" + "net/http" + "stackChan/internal/service" + "sync" + "time" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gorilla/websocket" +) + +const ( + Opus byte = 0x01 + Jpeg byte = 0x02 + ControlAvatar byte = 0x03 + ControlMotion byte = 0x04 + OnCamera byte = 0x05 + OffCamera byte = 0x06 + + TextMessage byte = 0x07 + RequestCall byte = 0x09 + RefuseCall byte = 0x0A + AgreeCall byte = 0x0B + HangupCall byte = 0x0C + + UpdateDeviceName byte = 0x0D + GetDeviceName byte = 0x0E + + inCall byte = 0x0F + + ping byte = 0x10 + pong byte = 0x11 + + OnPhoneScreen byte = 0x12 + OffPhoneScreen byte = 0x13 + Dance byte = 0x14 + GetAvatarPosture byte = 0x15 + + DeviceOffline byte = 0x16 + DeviceOnline byte = 0x17 +) + +var ( + wsUpGrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, + Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) { + logger.Errorf(r.Context(), "WebSocket Upgrade failed: %v", reason) + }, + } + logger = g.Log() + stackChanClientPool = sync.Map{} + appClientPool = sync.Map{} +) + +// AppClient indicates a WebSocket client connection on the App side +type AppClient struct { + Mac string + Conn *websocket.Conn + mu *sync.RWMutex + DeviceId string + LastTime time.Time +} + +// StackChanClient indicates a WebSocket client connection for the device end of a StackChan +type StackChanClient struct { + Mac string + Conn *websocket.Conn + mu *sync.RWMutex + CameraSubscriptionList []*AppClient + CallAppClient *AppClient + phoneScreen bool + LastTime time.Time +} + +func Handler(r *ghttp.Request) { + ctx := r.Context() + mac := r.Get("mac").String() + deviceType := r.Get("deviceType").String() + if mac == "" || deviceType == "" { + r.Response.Write("The mac and deviceType parameters are empty.") + return + } + + ws, err := wsUpGrader.Upgrade(r.Response.Writer, r.Request, nil) + if err != nil { + r.Response.Write(err.Error()) + return + } + + if deviceType == "StackChan" { + isHave := false + var client *StackChanClient + + stackChanClientPool.Range(func(key, value any) bool { + macAddr := key.(string) + stackChanClient := value.(*StackChanClient) + + if macAddr == mac { + isHave = true + client = stackChanClient + client.mu.Lock() + client.Conn = ws + if client.CallAppClient != nil { + reconnectMsg := createStringMessage(TextMessage, "The equipment has been reconnected.") + msgType := websocket.BinaryMessage + forwardMessage(ctx, client.CallAppClient.Conn, &msgType, reconnectMsg, client.CallAppClient.mu) + } + if len(client.CameraSubscriptionList) > 0 { + onMsg := createMessage(OnCamera, nil) + onType := websocket.BinaryMessage + forwardMessage(ctx, client.Conn, &onType, onMsg, client.mu) + } + client.LastTime = time.Now() + client.mu.Unlock() + return false + } + return true + }) + + if !isHave { + client = &StackChanClient{ + Mac: mac, + Conn: ws, + mu: &sync.RWMutex{}, + phoneScreen: false, + LastTime: time.Now(), + } + addStackChenClient(ctx, client) + } else { + // notify app + onlineMsg := createStringMessage(DeviceOnline, "Your StackChan has been launched.") + msgType := websocket.BinaryMessage + // Notify App + appClients := getAppClients(client.Mac) + for _, appClient := range appClients { + forwardMessage(ctx, appClient.Conn, &msgType, onlineMsg, appClient.mu) + } + } + logger.Info(ctx, "There is a StackChen connected to the service.", client.Mac) + defer func() { + logger.Info(ctx, "There is a StackChan that has disconnected.", mac, deviceType) + }() + for { + messageType, msg, err := ws.ReadMessage() + if err != nil { + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { + logger.Infof(ctx, "StackChan Normal disconnection: mac=%s, deviceType=%s, Reason=%v", mac, deviceType, err) + break + } + + var ne net.Error + if errors.As(err, &ne) && ne.Temporary() { + logger.Infof(ctx, "StackChan Temporary network error. Continue reading.: mac=%s,deviceType=%s,Error=%v", mac, deviceType, err) + continue + } + + logger.Errorf(ctx, "StackChan Abnormal disconnection: mac=%s, deviceType=%s, Error=%v", mac, deviceType, err) + break + } + //logger.Infof(ctx, "收到StackChan端消息%d", len(msg)) + readStackChanMessage(ctx, client, &messageType, &msg) + } + } else if deviceType == "App" { + deviceId := r.Get("deviceId").String() + if deviceId == "" { + r.Response.Write("The deviceId parameter in the App end is empty.") + return + } + var client *AppClient + found := false + clients := getAppClients(mac) + for _, appClient := range clients { + if appClient.DeviceId == deviceId && appClient.Mac == mac { + // Already available. Update the connection. + client = appClient + client.mu.Lock() + client.Conn = ws + client.mu.Unlock() + client.LastTime = time.Now() + found = true + break + } + } + if !found { + client = &AppClient{ + Mac: mac, + Conn: ws, + DeviceId: deviceId, + mu: &sync.RWMutex{}, + LastTime: time.Now(), + } + addAppClient(client) + } + logger.Info(ctx, "There is an App connected to the service.", client.Mac) + + // check StackChan status + stackChanClient := getStackChanClient(client.Mac) + if stackChanClient == nil { + offlineMsg := createStringMessage(DeviceOffline, "Your StackChan is offline.") + msgType := websocket.BinaryMessage + forwardMessage(ctx, client.Conn, &msgType, offlineMsg, client.mu) + } else { + onlineMsg := createStringMessage(DeviceOnline, "Your StackChan has been launched.") + msgType := websocket.BinaryMessage + forwardMessage(ctx, client.Conn, &msgType, onlineMsg, client.mu) + } + + defer func() { + logger.Info(ctx, "There is an App that has disconnected.", mac, deviceType) + }() + for { + messageType, msg, err := ws.ReadMessage() + if err != nil { + var ne net.Error + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { + logger.Infof(ctx, "App Normal disconnection: mac=%s, deviceType=%s, Error=%v", mac, deviceType, err) + break + } + if errors.As(err, &ne) && ne.Temporary() { + logger.Infof(ctx, "App Temporary network error. Continue reading.: mac=%s,deviceType=%s,Error=%v", mac, deviceType, err) + continue + } + if errors.As(err, &ne) && ne.Timeout() { + logger.Infof(ctx, "App Timeout disconnection: mac=%s, deviceType=%s", mac, deviceType) + break + } + logger.Errorf(ctx, "App Abnormal disconnection: mac=%s, deviceType=%s, Error=%v", mac, deviceType, err) + break + } + client.LastTime = time.Now() + readAppClientMessage(ctx, client, &messageType, &msg) + } + } +} + +// addStackChenClient adds a StackChan client to the connection pool and ensures the MAC is registered +func addStackChenClient(ctx context.Context, c *StackChanClient) { + stackChanClientPool.Store(c.Mac, c) + _, _ = service.CreateMacIfNotExists(ctx, c.Mac) +} + +// addAppClient adds an App client to the App connection pool (multiple Apps per MAC allowed) +func addAppClient(c *AppClient) { + val, _ := appClientPool.Load(c.Mac) + var clients []*AppClient + if val == nil { + clients = []*AppClient{c} + } else { + clients = val.([]*AppClient) + clients = append(clients, c) + } + appClientPool.Store(c.Mac, clients) +} + +// getAppClients gets all App clients for the specified MAC address +func getAppClients(mac string) []*AppClient { + if val, ok := appClientPool.Load(mac); ok { + return val.([]*AppClient) + } + return nil +} + +// getStackChanClient gets the StackChan client corresponding to the specified MAC address +func getStackChanClient(mac string) *StackChanClient { + if val, ok := stackChanClientPool.Load(mac); ok { + return val.(*StackChanClient) + } + return nil +} + +// parseBinaryMessage parses a custom binary protocol message, returns type, length, payload, and success status +func parseBinaryMessage(ctx context.Context, msg *[]byte) (byte, int, []byte, bool) { + if len(*msg) < 1+4 { + logger.Warning(ctx, "Message too short, cannot parse header, message not forwarded") + return 0, 0, nil, false + } + + msgType := (*msg)[0] + dataLen := int(binary.BigEndian.Uint32((*msg)[1:5])) + payload := (*msg)[5 : 5+dataLen] + + if len(*msg)-5 != dataLen { + logger.Warningf(ctx, "Length mismatch: header says %d, actual is %d, message not forwarded", dataLen, len(*msg)-5) + return 0, 0, nil, false + } + + return msgType, dataLen, payload, true +} + +// StartPingTime sends Ping messages to all connected clients for heartbeat detection +func StartPingTime(ctx context.Context) { + message := createMessage(ping, nil) + messageType := websocket.BinaryMessage + + // Iterate over StackChanClientPool + stackChanClientPool.Range(func(_, value any) bool { + client := value.(*StackChanClient) + forwardMessage(ctx, client.Conn, &messageType, message, client.mu) + return true // continue iteration + }) + + // Iterate over AppClientPool + appClientPool.Range(func(_, value any) bool { + clients := value.([]*AppClient) + for _, client := range clients { + forwardMessage(ctx, client.Conn, &messageType, message, client.mu) + } + return true // continue iteration + }) +} + +// CheckExpiredLinks checks and cleans up App client connections that have been inactive for over 60 seconds +func CheckExpiredLinks(ctx context.Context) { + now := time.Now() + var expiredClients []*AppClient + + // First, iterate over AppClientPool + appClientPool.Range(func(mac, value any) bool { + clients := value.([]*AppClient) + newClients := clients[:0] + for _, client := range clients { + if now.Sub(client.LastTime) > time.Second*15 { + // Found expired client + // Iterate over StackChanClientPool to clean up CallAppClient and CameraSubscriptionList + stackChanClientPool.Range(func(_, scValue any) bool { + stackChanClient, ok := scValue.(*StackChanClient) + stackChanClient.mu.Lock() + if !ok { + return true + } + // Clean up CallAppClient + if stackChanClient.CallAppClient == client { + stackChanClient.CallAppClient = nil + } + + // Update camera subscription list + newCamera := stackChanClient.CameraSubscriptionList[:0] + removedCamera := false + for _, sub := range stackChanClient.CameraSubscriptionList { + if sub != client { + newCamera = append(newCamera, sub) + } else { + removedCamera = true + } + } + stackChanClient.CameraSubscriptionList = newCamera + stackChanClient.mu.Unlock() + if removedCamera && len(newCamera) == 0 { + msg := createMessage(OffCamera, nil) + msgType := websocket.BinaryMessage + forwardMessage(ctx, stackChanClient.Conn, &msgType, msg, stackChanClient.mu) + } + return true + }) + expiredClients = append(expiredClients, client) + } else { + newClients = append(newClients, client) + } + } + if len(newClients) == 0 { + appClientPool.Delete(mac) + } else { + appClientPool.Store(mac, newClients) + } + return true + }) + + for _, client := range expiredClients { + logger.Infof(ctx, "Kicked out an expired App client: %s", client.Mac) + err := client.Conn.Close() + if err != nil { + } + } +} + +// readStackChanMessage handles messages from the StackChan device side +func readStackChanMessage(ctx context.Context, client *StackChanClient, messageType *int, msg *[]byte) { + if *messageType == websocket.BinaryMessage { + msgType, _, _, ok := parseBinaryMessage(ctx, msg) + if !ok { + return + } + switch msgType { + case pong: + break + case ControlAvatar, ControlMotion, OnCamera, OffCamera: + break + case RefuseCall: + // Refused call, remove and notify appClient + appClient := client.CallAppClient + if appClient != nil { + forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu) + client.mu.Lock() + client.CallAppClient = nil + client.mu.Unlock() + } + break + case AgreeCall: + // Agreed to call + appClient := client.CallAppClient + if appClient != nil { + forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu) + client.mu.Lock() + client.CameraSubscriptionList = append(client.CameraSubscriptionList, appClient) + client.mu.Unlock() + if len(client.CameraSubscriptionList) == 1 { + onMsg := createMessage(OnCamera, nil) + onType := websocket.BinaryMessage + forwardMessage(ctx, client.Conn, &onType, onMsg, client.mu) + } + } + break + case HangupCall: + // Hang up call + appClient := client.CallAppClient + if appClient != nil { + forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu) + // Remove the client from the subscription list + newList := client.CameraSubscriptionList[:0] + for _, subClient := range client.CameraSubscriptionList { + if subClient != appClient { + newList = append(newList, subClient) + } + } + client.mu.Lock() + client.CameraSubscriptionList = newList + client.mu.Unlock() + // If the subscription list is empty, notify to turn off the camera + if len(client.CameraSubscriptionList) == 0 { + offMsg := createMessage(OffCamera, nil) + offType := websocket.BinaryMessage + forwardMessage(ctx, client.Conn, &offType, offMsg, client.mu) + } + } + break + case GetDeviceName: + // Query device name + name, err := service.GetDeviceName(ctx, client.Mac) + if err != nil { + return + } + if name == "" { + logger.Infof(ctx, "Queried device nickname is empty") + return + } + newMsg := createStringMessage(GetDeviceName, name) + forwardMessage(ctx, client.Conn, messageType, newMsg, client.mu) + break + case Opus: + + break + case Jpeg: + subscribers := client.CameraSubscriptionList + if len(subscribers) > 0 { + var isAll = true + for _, subClient := range subscribers { + if subClient.Conn != nil { + isAll = false + } + forwardMessage(ctx, subClient.Conn, messageType, msg, subClient.mu) + } + if isAll { + msg = createMessage(OffCamera, nil) + forwardMessage(ctx, client.Conn, messageType, msg, client.mu) + } + } else { + msg = createMessage(OffCamera, nil) + forwardMessage(ctx, client.Conn, messageType, msg, client.mu) + } + break + case GetAvatarPosture: + appClients := getAppClients(client.Mac) + for _, appClient := range appClients { + forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu) + } + break + default: + logger.Infof(ctx, "Unknown binary msgType: %d", msgType) + appClients := getAppClients(client.Mac) + if appClients != nil { + for _, appClient := range appClients { + forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu) + } + } + } + } else if *messageType == websocket.TextMessage { + appClients := getAppClients(client.Mac) + if appClients != nil { + for _, appClient := range appClients { + forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu) + } + } + } else if *messageType == websocket.PingMessage { + logger.Info(ctx, "Received ping message from StackChan side") + } +} + +// readAppClientMessage handles messages from the App side +func readAppClientMessage(ctx context.Context, client *AppClient, messageType *int, msg *[]byte) { + if *messageType == websocket.BinaryMessage { + msgType, _, payload, ok := parseBinaryMessage(ctx, msg) + if !ok { + return + } + switch msgType { + case pong: + break + case GetDeviceName: + // Query device name + name, err := service.GetDeviceName(ctx, client.Mac) + if err != nil { + logger.Errorf(ctx, err.Error()) + return + } + if name == "" { + logger.Infof(ctx, "Queried device nickname is empty") + return + } + newMsg := createStringMessage(GetDeviceName, name) + logger.Infof(ctx, "Device name found, returning: "+name) + forwardMessage(ctx, client.Conn, messageType, newMsg, client.mu) + break + case UpdateDeviceName: + stackChanClient := getStackChanClient(client.Mac) + if stackChanClient != nil { + forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu) + } + appClients := getAppClients(client.Mac) + for _, appClient := range appClients { + forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu) + } + break + case Opus: + break + case Jpeg: + if len(payload) < 12 { + logger.Warningf(ctx, "Payload too short, cannot parse MAC address: %v", payload) + return + } + macAddrBytes := payload[:12] + data := payload[12:] + macAddr := string(macAddrBytes) + newMsg := createMessage(msgType, data) + stackChanClient := getStackChanClient(macAddr) + if stackChanClient != nil { + if stackChanClient.phoneScreen { + forwardMessage(ctx, stackChanClient.Conn, messageType, newMsg, stackChanClient.mu) + } + } + break + case ControlAvatar, ControlMotion: + if len(payload) < 12 { + logger.Warningf(ctx, "Payload too short, cannot parse MAC address: %v", payload) + return + } + macAddrBytes := payload[:12] + data := payload[12:] + macAddr := string(macAddrBytes) + newMsg := createMessage(msgType, data) + stackChanClient := getStackChanClient(macAddr) + if stackChanClient != nil { + forwardMessage(ctx, stackChanClient.Conn, messageType, newMsg, stackChanClient.mu) + } else { + logger.Infof(ctx, "StackChan is currently offline") + } + break + case TextMessage: + if len(payload) < 12 { + logger.Warningf(ctx, "Payload too short, cannot parse MAC address: %v", payload) + return + } + macAddr := string(payload[:12]) + data := payload[12:] + newMsg := createMessage(msgType, data) + stackChanClient := getStackChanClient(macAddr) + if stackChanClient != nil { + forwardMessage(ctx, stackChanClient.Conn, messageType, newMsg, stackChanClient.mu) + } + appClients := getAppClients(macAddr) + if appClients != nil { + for _, appClient := range appClients { + forwardMessage(ctx, appClient.Conn, messageType, newMsg, appClient.mu) + } + } + break + case RequestCall: + // Request call + if len(payload) < 12 { + logger.Warningf(ctx, "Payload too short, cannot parse MAC address: %v", payload) + return + } + macAddr := string(payload[:12]) + data := payload[12:] + stackChanClient := getStackChanClient(macAddr) + if stackChanClient != nil { + stackChanClient.mu.Lock() + if stackChanClient.CallAppClient == nil || stackChanClient.CallAppClient == client { + stackChanClient.CallAppClient = client + stackChanClient.mu.Unlock() + newMsg := createMessage(msgType, data) + forwardMessage(ctx, stackChanClient.Conn, messageType, newMsg, stackChanClient.mu) + } else { + stackChanClient.mu.Unlock() + // Notify App that the other side is already in a call + newMsg := createStringMessage(inCall, "The other party is currently in a call") + forwardMessage(ctx, client.Conn, messageType, newMsg, client.mu) + } + } + break + case HangupCall: + stackChanClientPool.Range(func(_, value any) bool { + stackChanClient := value.(*StackChanClient) + if stackChanClient.CallAppClient == client { + // Found corresponding call + stackChanClient.mu.Lock() + stackChanClient.CallAppClient = nil + forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu) + + newList := stackChanClient.CameraSubscriptionList[:0] + for _, sub := range stackChanClient.CameraSubscriptionList { + if sub != client { + newList = append(newList, sub) + } + } + stackChanClient.CameraSubscriptionList = newList + stackChanClient.mu.Unlock() + if len(stackChanClient.CameraSubscriptionList) == 0 { + offMsg := createMessage(OffCamera, nil) + offType := websocket.BinaryMessage + forwardMessage(ctx, stackChanClient.Conn, &offType, offMsg, stackChanClient.mu) + } + return false + } + return true + }) + break + case OnCamera: + macAddr := string(payload) + stackChanClient := getStackChanClient(macAddr) + if stackChanClient != nil { + stackChanClient.mu.Lock() + alreadySubscribed := false + for _, sub := range stackChanClient.CameraSubscriptionList { + if sub == client { + alreadySubscribed = true + break + } + } + if !alreadySubscribed { + stackChanClient.CameraSubscriptionList = append(stackChanClient.CameraSubscriptionList, client) + stackChanClient.mu.Unlock() + forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu) + } else { + stackChanClient.mu.Unlock() + } + } + break + case OffCamera: + macAddr := string(payload) + stackChanClient := getStackChanClient(macAddr) + if stackChanClient != nil { + stackChanClient.mu.Lock() + existed := false + newList := stackChanClient.CameraSubscriptionList[:0] + for _, subClient := range stackChanClient.CameraSubscriptionList { + if subClient == client { + existed = true + } else { + newList = append(newList, subClient) + } + } + shouldNotify := existed && len(newList) == 0 + stackChanClient.CameraSubscriptionList = newList + stackChanClient.mu.Unlock() + if shouldNotify { + forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu) + } + } + break + case OnPhoneScreen: + // Show phone screen + macAddr := string(payload) + stackChanClient := getStackChanClient(macAddr) + if stackChanClient != nil { + stackChanClient.mu.Lock() + if stackChanClient.phoneScreen == false { + stackChanClient.phoneScreen = true + stackChanClient.mu.Unlock() + forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu) + } else { + stackChanClient.mu.Unlock() + } + } + break + case OffPhoneScreen: + // Hide phone screen + macAddr := string(payload) + stackChanClient := getStackChanClient(macAddr) + if stackChanClient != nil { + stackChanClient.mu.Lock() + if stackChanClient.phoneScreen == true { + stackChanClient.phoneScreen = false + stackChanClient.mu.Unlock() + forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu) + } else { + stackChanClient.mu.Unlock() + } + } + break + case Dance: + // Dance message + stackChanClient := getStackChanClient(client.Mac) + if stackChanClient != nil { + forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu) + } + break + case GetAvatarPosture: + stackChanClient := getStackChanClient(client.Mac) + if stackChanClient != nil { + forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu) + } + default: + logger.Infof(ctx, "Unknown binary msgType: %d", msgType) + stackChanClient := getStackChanClient(client.Mac) + if stackChanClient != nil { + forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu) + } + } + } else if *messageType == websocket.TextMessage { + // Directly forward other message types + stackChanClient := getStackChanClient(client.Mac) + if stackChanClient != nil { + forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu) + } + } else if *messageType == websocket.PingMessage { + logger.Info(ctx, "Received ping message from App side") + } +} + +// forwardMessage forwards a message to the specified connection, with mutex for concurrency safety +func forwardMessage(ctx context.Context, conn *websocket.Conn, messageType *int, msg *[]byte, mu *sync.RWMutex) { + if conn == nil { + logger.Infof(ctx, "StackChan is currently offline") + return + } + mu.Lock() + defer mu.Unlock() + err := conn.WriteMessage(*messageType, *msg) + if err != nil { + //logger.Info(ctx, "Message forwarding failed: %v", err) + } else { + //logger.Info(ctx, "Message sent successfully") + } +} + +// createMessage encapsulates a binary message according to custom protocol (type + length + data) +func createMessage(msgType byte, data []byte) *[]byte { + var dataLen int + if data != nil { + dataLen = len(data) + } else { + dataLen = 0 + } + msg := make([]byte, 1+4+dataLen) + msg[0] = msgType + binary.BigEndian.PutUint32(msg[1:5], uint32(dataLen)) + if dataLen > 0 { + copy(msg[5:], data) + } + return &msg +} + +// createStringMessage creates a binary message with a string payload +func createStringMessage(msgType byte, data string) *[]byte { + return createMessage(msgType, []byte(data)) +} + +// GetRandomStackChanDevice get Random StackChan Device list +func GetRandomStackChanDevice(userMac string, maxLength int) (list []string) { + if maxLength <= 0 { + return []string{} + } + var macs []string + + stackChanClientPool.Range(func(key, value interface{}) bool { + mac := key.(string) + client := value.(*StackChanClient) + + if mac == userMac { + return true + } + + client.mu.RLock() + online := client.Conn != nil + client.mu.RUnlock() + + if online { + macs = append(macs, mac) + } + + return true + }) + + if len(macs) == 0 { + return []string{} + } + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + r.Shuffle(len(macs), func(i, j int) { + macs[i], macs[j] = macs[j], macs[i] + }) + + if len(macs) > maxLength { + macs = macs[:maxLength] + } + + return macs +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..eccb966 --- /dev/null +++ b/server/main.go @@ -0,0 +1,18 @@ +/* +SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +SPDX-License-Identifier: MIT +*/ + +package main + +import ( + "stackChan/internal/cmd" + _ "stackChan/internal/packed" + + _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" + "github.com/gogf/gf/v2/os/gctx" +) + +func main() { + cmd.Main.Run(gctx.GetInitCtx()) +} diff --git a/server/manifest/config/config.yaml b/server/manifest/config/config.yaml new file mode 100644 index 0000000..41ab5d8 --- /dev/null +++ b/server/manifest/config/config.yaml @@ -0,0 +1,15 @@ +# https://goframe.org/docs/web/server-config-file-template +server: + address: ":12800" +# openapiPath: "/api.json" + swaggerPath: "/swagger" + +# https://goframe.org/docs/core/glog-config +logger: + level : "all" + stdout: true + +# https://goframe.org/docs/core/gdb-config-file +database: + default: + link: "sqlite::@file(/stackChan.sqlite)" \ No newline at end of file diff --git a/server/manifest/deploy/kustomize/base/deployment.yaml b/server/manifest/deploy/kustomize/base/deployment.yaml new file mode 100644 index 0000000..28f1d69 --- /dev/null +++ b/server/manifest/deploy/kustomize/base/deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: template-single + labels: + app: template-single +spec: + replicas: 1 + selector: + matchLabels: + app: template-single + template: + metadata: + labels: + app: template-single + spec: + containers: + - name : main + image: template-single + imagePullPolicy: Always + diff --git a/server/manifest/deploy/kustomize/base/kustomization.yaml b/server/manifest/deploy/kustomize/base/kustomization.yaml new file mode 100644 index 0000000..302d92d --- /dev/null +++ b/server/manifest/deploy/kustomize/base/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- deployment.yaml +- service.yaml + + + diff --git a/server/manifest/deploy/kustomize/base/service.yaml b/server/manifest/deploy/kustomize/base/service.yaml new file mode 100644 index 0000000..608771c --- /dev/null +++ b/server/manifest/deploy/kustomize/base/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: template-single +spec: + ports: + - port: 80 + protocol: TCP + targetPort: 8000 + selector: + app: template-single + diff --git a/server/manifest/deploy/kustomize/overlays/develop/configmap.yaml b/server/manifest/deploy/kustomize/overlays/develop/configmap.yaml new file mode 100644 index 0000000..3b1d0af --- /dev/null +++ b/server/manifest/deploy/kustomize/overlays/develop/configmap.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: template-single-configmap +data: + config.yaml: | + server: + address: ":8000" + openapiPath: "/api.json" + swaggerPath: "/swagger" + + logger: + level : "all" + stdout: true diff --git a/server/manifest/deploy/kustomize/overlays/develop/deployment.yaml b/server/manifest/deploy/kustomize/overlays/develop/deployment.yaml new file mode 100644 index 0000000..04e4851 --- /dev/null +++ b/server/manifest/deploy/kustomize/overlays/develop/deployment.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: template-single +spec: + template: + spec: + containers: + - name : main + image: template-single:develop \ No newline at end of file diff --git a/server/manifest/deploy/kustomize/overlays/develop/kustomization.yaml b/server/manifest/deploy/kustomize/overlays/develop/kustomization.yaml new file mode 100644 index 0000000..4731c47 --- /dev/null +++ b/server/manifest/deploy/kustomize/overlays/develop/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../../base +- configmap.yaml + +patchesStrategicMerge: +- deployment.yaml + +namespace: default + + + diff --git a/server/manifest/docker/Dockerfile b/server/manifest/docker/Dockerfile new file mode 100644 index 0000000..c4828a9 --- /dev/null +++ b/server/manifest/docker/Dockerfile @@ -0,0 +1,7 @@ +FROM alpine:latest +ENV WORKDIR=/app +WORKDIR $WORKDIR +COPY ./stackChan $WORKDIR/stackChan +COPY ./config.yaml $WORKDIR/config.yaml +RUN chmod +x $WORKDIR/stackChan +CMD ["./stackChan"] \ No newline at end of file diff --git a/server/manifest/docker/docker.sh b/server/manifest/docker/docker.sh new file mode 100644 index 0000000..ff393f9 --- /dev/null +++ b/server/manifest/docker/docker.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# This shell is executed before docker build. + + + + + diff --git a/server/manifest/i18n/.gitkeep b/server/manifest/i18n/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/manifest/protobuf/.keep-if-necessary b/server/manifest/protobuf/.keep-if-necessary new file mode 100644 index 0000000..e69de29 diff --git a/server/resource/public/html/.gitkeep b/server/resource/public/html/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/resource/public/plugin/.gitkeep b/server/resource/public/plugin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/resource/public/resource/css/.gitkeep b/server/resource/public/resource/css/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/resource/public/resource/image/.gitkeep b/server/resource/public/resource/image/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/resource/public/resource/js/.gitkeep b/server/resource/public/resource/js/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/resource/template/.gitkeep b/server/resource/template/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/stackChan.sqlite b/server/stackChan.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..066b3dcd452a772e04c40695d36ac179f7eaa20d GIT binary patch literal 40960 zcmeI5&u`mg7{~1h7?x#T=6Qx;&eQiWef!}Q-3j{#bjt?zJMErlrdEGD z%l*X+MSf?v%iK3-9&k^cdFAxznX^MHGztL(KmY_l00bUCfp;52(U>f=_fFg9%7#_8 zZd6KEakIW<7fbbO)vDQTqK31JMy6o!h0LWngKu*JKQ_$w5-R1rvu5WD#${uXzp^-+ z&n#Z!Up21rndQR5>^#jZZ_F1adrQ(Sx|^<_;`~B^pI@Gv>oI|OQ#g`!!!m8FT%^|>GZb)PHgE7VMlQ2FSKzbDi;EOP(TQ9tWb#*ftWR!X(U`qF z?*bib(i!I$jhsP~%Nk2T|BY43^5VvY8pc?TZ88~iqKC)tzBCw( zjgGSKXAg=lprxTsBI_oufT*Y&d=(s3SNn2sRMC%9n^Gv+)-9?B$Fd7Z-=I=8*Q}ll z_T@s$c5KD8DCJNG?yg0mu?rX2yJv$ATy0dWTDj>$?;zUFiQY%u+{p8^2t~6m4{y0vwEgWeew&278x9v55cS|lxpgI7*PViObW zH5XCSbj!NFZPiNFfx}1(rUM_2)y%3jNiE0Uy&j6j5()NRB8a_g(gd4XyZhmm)PkRM zW!}Vw_x0}{o!VvDXl!JJ-F-74 zR!@urZ3KFVG+i0FehjqK&UJR0i|h8`@ri**?BYn5!Z(W@q8D3(&IQ+5`VMoSGTblR zcid-m2LS{?00ck)1V8`;KmY_l00ck)1VG?FAn#2Er{7bXp*^ zAK}df3~-^uP;er^`Tjo?e#i_>Mpozs0R%t*1WpQpyG(eHNesR>BFn*!@(ucU^=v*-kZLi1Ex8t|sX>l^XMykogLH?7Lry6x^Z>b7ZD>NR?B$9o7jAd9>xrDc*<HScYRQ<>l_mxaiyr{TJmV6~kGEc;`sHKT=|LTCF zTekt!+92A{k%;nm=ydX8Yd<5nr+F8Eeg!*O9?(r6mPY|Y4jwqOf9@;i{{IKMK>z^|009sH0T2KI5C8!X009sH0T6gn z2|ODL$wSShFN3QYa|5AKHV_{@N7n)HBiw}x!_NIbUGyIT1V8`;KmY_l00ck)1V8`; zKmY_l-~00@8p2!H?xfB*=900@8p2zbB$$8t8q*-!ll9v}b$ zAOHd&00JNY0w4eaAOHd&@V_OnI}nO8iNvlT2baD7`J)f^UhB5KnVy~|DQSvm`jje7 zDRP=fx+qSsRyM5ZYQ1XJ?5$~4)KvN#e2SoCbX`ysO%_sm&JZ$6mMA2vr80VQ>aEQ+ zf0aE+bynFERl1%mrT2hIEYLzYt5EL_MQM>nc7?pRCbjpk_&2DMBhIkxVY7WYv^k7)=m~ASQW2(h^;xS)I}~l_a$ZQBRAa z^ZY-{eaX-l0tkQr2!H?xfB*=900@8p2!H?xfWVVU;36Ag*hC^94bCL&&1}#eIvEkX S|Ci`3|CBD$`3lLq|Nk2T*#2$+ literal 0 HcmV?d00001 diff --git a/server/utility/.gitkeep b/server/utility/.gitkeep new file mode 100644 index 0000000..e69de29