server code

This commit is contained in:
袁智鸿
2026-01-07 18:04:01 +08:00
parent 35bd1e0898
commit f1fb7f5608
103 changed files with 3504 additions and 2 deletions
+92
View File
@@ -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.")
}
+6
View File
@@ -0,0 +1,6 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package consts
@@ -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
@@ -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{}
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
@@ -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{}
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
+10
View File
@@ -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
@@ -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{}
}
@@ -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)
}
@@ -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
@@ -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{}
}
@@ -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
}
+10
View File
@@ -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
@@ -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{}
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
View File
+27
View File
@@ -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.
+27
View File
@@ -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.
+27
View File
@@ -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.
+27
View File
@@ -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.
@@ -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.
+81
View File
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
+27
View File
@@ -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.
View File
View File
+11
View File
@@ -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"`
}
+16
View File
@@ -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 //
}
+21
View File
@@ -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 //
}
+16
View File
@@ -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 //
}
+20
View File
@@ -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 // 发帖时间
}
@@ -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 // 评论时间
}
@@ -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 //
}
+11
View File
@@ -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:""` //
}
@@ -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:""` //
}
@@ -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:""` //
}
@@ -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:"发帖时间"` // 发帖时间
}
@@ -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:"评论时间"` // 评论时间
}
@@ -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:""` //
}
+42
View File
@@ -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"`
}
+27
View File
@@ -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:""` //
}
+6
View File
@@ -0,0 +1,6 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package packed
View File
+40
View File
@@ -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
}
+13
View File
@@ -0,0 +1,13 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package service
import (
"testing"
)
func TestCreateMac(t *testing.T) {
}
+70
View File
@@ -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
}
+830
View File
@@ -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
}