server: implement messages and gRPC support for creating messages
This commit is contained in:
@@ -35,6 +35,13 @@ func Start() {
|
|||||||
r.Route("/{channelID}", func(r chi.Router) {
|
r.Route("/{channelID}", func(r chi.Router) {
|
||||||
r.Get("/", GetChannel)
|
r.Get("/", GetChannel)
|
||||||
r.Delete("/", DeleteChannel)
|
r.Delete("/", DeleteChannel)
|
||||||
|
|
||||||
|
r.Route("/messages", func(r chi.Router) {
|
||||||
|
r.Get("/", ListMessages)
|
||||||
|
r.Route("/{messageID}", func(r chi.Router) {
|
||||||
|
r.Get("/", GetMessage)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.dubyatp.xyz/dubyatp/scannerbot/server/db"
|
"git.dubyatp.xyz/dubyatp/scannerbot/server/db"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
@@ -14,6 +15,7 @@ var ErrUserNotFound = errors.New("db: user not found")
|
|||||||
var ErrSessionNotFound = errors.New("db: session not found")
|
var ErrSessionNotFound = errors.New("db: session not found")
|
||||||
var ErrChannelNotFound = errors.New("db: channel not found")
|
var ErrChannelNotFound = errors.New("db: channel not found")
|
||||||
var ErrFileNotFound = errors.New("db: file not found")
|
var ErrFileNotFound = errors.New("db: file not found")
|
||||||
|
var ErrMessageNotFound = errors.New("db: message not found")
|
||||||
|
|
||||||
func dbGetUser(id string) (*User, error) {
|
func dbGetUser(id string) (*User, error) {
|
||||||
query := `SELECT id, name, password FROM users WHERE id = $1`
|
query := `SELECT id, name, password FROM users WHERE id = $1`
|
||||||
@@ -233,3 +235,112 @@ func dbGetFile(id string) (*File, error) {
|
|||||||
slog.Debug("db: file found", "fileid", file.ID, "filename", file.Name)
|
slog.Debug("db: file found", "fileid", file.ID, "filename", file.Name)
|
||||||
return &file, nil
|
return &file, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DBGetChannel(id string) (*Channel, error) { return dbGetChannel(id) }
|
||||||
|
func DBAddFile(file *File) error { return dbAddFile(file) }
|
||||||
|
|
||||||
|
func DBAddMessage(msg *Message) error {
|
||||||
|
query := `INSERT INTO messages (id, channel, created, content, audio) VALUES ($1, $2, $3, $4, $5)`
|
||||||
|
_, err := db.Pool.Exec(context.Background(), query, msg.ID, msg.Channel.ID, msg.Created, msg.Content, msg.Audio.ID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("db: failed to add message", "error", err, "messageid", msg.ID)
|
||||||
|
return fmt.Errorf("failed to add message")
|
||||||
|
}
|
||||||
|
slog.Debug("db: message added", "messageid", msg.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbGetMessage(id string) (*Message, error) {
|
||||||
|
query := `SELECT id, channel, created, content, audio FROM messages WHERE id = $1`
|
||||||
|
var channelID, audioID string
|
||||||
|
var msg Message
|
||||||
|
err := db.Pool.QueryRow(context.Background(), query, id).Scan(&msg.ID, &channelID, &msg.Created, &msg.Content, &audioID)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
slog.Debug("db: message not found", "messageid", id)
|
||||||
|
return nil, ErrMessageNotFound
|
||||||
|
} else if err != nil {
|
||||||
|
slog.Error("db: failed to query message", "error", err)
|
||||||
|
return nil, fmt.Errorf("failed to query message")
|
||||||
|
}
|
||||||
|
|
||||||
|
channel, err := dbGetChannel(channelID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("db: failed to fetch channel for message", "messageid", id, "channelid", channelID, "error", err)
|
||||||
|
return nil, fmt.Errorf("failed to fetch channel for message")
|
||||||
|
}
|
||||||
|
audio, err := dbGetFile(audioID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("db: failed to fetch audio for message", "messageid", id, "audioid", audioID, "error", err)
|
||||||
|
return nil, fmt.Errorf("failed to fetch audio for message")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Channel = *channel
|
||||||
|
msg.Audio = *audio
|
||||||
|
slog.Debug("db: message found", "messageid", msg.ID)
|
||||||
|
return &msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbGetMessagesByChannel(channelID string, from, to *time.Time) ([]*Message, error) {
|
||||||
|
query := `SELECT id, channel, created, content, audio FROM messages WHERE channel = $1`
|
||||||
|
args := []any{channelID}
|
||||||
|
if from != nil {
|
||||||
|
args = append(args, *from)
|
||||||
|
query += fmt.Sprintf(" AND created >= $%d", len(args))
|
||||||
|
}
|
||||||
|
if to != nil {
|
||||||
|
args = append(args, *to)
|
||||||
|
query += fmt.Sprintf(" AND created <= $%d", len(args))
|
||||||
|
}
|
||||||
|
query += " ORDER BY created DESC"
|
||||||
|
rows, err := db.Pool.Query(context.Background(), query, args...)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("db: failed to query messages", "error", err)
|
||||||
|
return nil, fmt.Errorf("failed to query messages")
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
type messageRow struct {
|
||||||
|
msg Message
|
||||||
|
channelID string
|
||||||
|
audioID string
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows_ []messageRow
|
||||||
|
for rows.Next() {
|
||||||
|
var mr messageRow
|
||||||
|
if err := rows.Scan(&mr.msg.ID, &mr.channelID, &mr.msg.Created, &mr.msg.Content, &mr.audioID); err != nil {
|
||||||
|
slog.Error("db: failed to scan message", "error", err)
|
||||||
|
return nil, fmt.Errorf("failed to scan message")
|
||||||
|
}
|
||||||
|
rows_ = append(rows_, mr)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
slog.Error("db: row iteration error", "error", err)
|
||||||
|
return nil, fmt.Errorf("failed to iterate messages")
|
||||||
|
}
|
||||||
|
if len(rows_) == 0 {
|
||||||
|
slog.Debug("db: no messages found", "channelid", channelID)
|
||||||
|
return nil, ErrMessageNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
channel, err := dbGetChannel(channelID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("db: failed to fetch channel for messages", "channelid", channelID, "error", err)
|
||||||
|
return nil, fmt.Errorf("failed to fetch channel for messages")
|
||||||
|
}
|
||||||
|
|
||||||
|
var messages []*Message
|
||||||
|
for _, mr := range rows_ {
|
||||||
|
audio, err := dbGetFile(mr.audioID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("db: failed to fetch audio for message", "messageid", mr.msg.ID, "audioid", mr.audioID, "error", err)
|
||||||
|
return nil, fmt.Errorf("failed to fetch audio for message")
|
||||||
|
}
|
||||||
|
mr.msg.Channel = *channel
|
||||||
|
mr.msg.Audio = *audio
|
||||||
|
messages = append(messages, &mr.msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("db: message list returned", "channelid", channelID, "count", len(messages))
|
||||||
|
return messages, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Channel Channel
|
||||||
|
Created time.Time
|
||||||
|
Content string
|
||||||
|
Audio File
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessagePayload struct {
|
||||||
|
*Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMessage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
slog.Debug("message: entering GetMessage handler")
|
||||||
|
|
||||||
|
messageID := chi.URLParam(r, "messageID")
|
||||||
|
parsed, err := uuid.Parse(messageID)
|
||||||
|
if err != nil {
|
||||||
|
render.Render(w, r, ErrInvalidRequest(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := dbGetMessage(parsed.String())
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrMessageNotFound) {
|
||||||
|
render.Render(w, r, ErrNotFound)
|
||||||
|
} else {
|
||||||
|
slog.Error("message: failed to fetch message", "messageid", parsed.String(), "error", err)
|
||||||
|
render.Render(w, r, ErrInternal(err))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("message: rendering message", "messageid", msg.ID)
|
||||||
|
if err := render.Render(w, r, NewMessagePayloadResponse(msg)); err != nil {
|
||||||
|
slog.Error("message: failed to render message", "messageid", parsed.String(), "error", err)
|
||||||
|
render.Render(w, r, ErrInternal(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListMessages(w http.ResponseWriter, r *http.Request) {
|
||||||
|
slog.Debug("message: entering ListMessages handler")
|
||||||
|
|
||||||
|
channelID := chi.URLParam(r, "channelID")
|
||||||
|
parsed, err := uuid.Parse(channelID)
|
||||||
|
if err != nil {
|
||||||
|
render.Render(w, r, ErrInvalidRequest(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var from, to *time.Time
|
||||||
|
if v := r.URL.Query().Get("from"); v != "" {
|
||||||
|
t, err := time.Parse(time.RFC3339, v)
|
||||||
|
if err != nil {
|
||||||
|
render.Render(w, r, ErrInvalidRequest(fmt.Errorf("invalid 'from' timestamp: %w", err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
from = &t
|
||||||
|
}
|
||||||
|
if v := r.URL.Query().Get("to"); v != "" {
|
||||||
|
t, err := time.Parse(time.RFC3339, v)
|
||||||
|
if err != nil {
|
||||||
|
render.Render(w, r, ErrInvalidRequest(fmt.Errorf("invalid 'to' timestamp: %w", err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
to = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
messages, err := dbGetMessagesByChannel(parsed.String(), from, to)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrMessageNotFound) {
|
||||||
|
render.Render(w, r, ErrNotFound)
|
||||||
|
} else {
|
||||||
|
slog.Error("message: failed to fetch messages", "channelid", parsed.String(), "error", err)
|
||||||
|
render.Render(w, r, ErrInternal(err))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("message: successfully fetched messages", "channelid", parsed.String(), "count", len(messages))
|
||||||
|
if err := render.RenderList(w, r, NewMessageListResponse(messages)); err != nil {
|
||||||
|
slog.Error("message: failed to render message list", "channelid", parsed.String(), "error", err)
|
||||||
|
render.Render(w, r, ErrInternal(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,3 +49,19 @@ func NewFilePayloadResponse(file *File) *FilePayload {
|
|||||||
func (f *FilePayload) Render(w http.ResponseWriter, r *http.Request) error {
|
func (f *FilePayload) Render(w http.ResponseWriter, r *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewMessagePayloadResponse(msg *Message) *MessagePayload {
|
||||||
|
return &MessagePayload{Message: msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMessageListResponse(messages []*Message) []render.Renderer {
|
||||||
|
list := []render.Renderer{}
|
||||||
|
for _, msg := range messages {
|
||||||
|
list = append(list, NewMessagePayloadResponse(msg))
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MessagePayload) Render(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ require (
|
|||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
golang.org/x/net v0.53.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||||
|
google.golang.org/grpc v1.81.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,10 +30,20 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||||
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||||
|
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.dubyatp.xyz/dubyatp/scannerbot/server/api"
|
||||||
|
pb "git.dubyatp.xyz/dubyatp/scannerbot/server/proto"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MessageServer struct {
|
||||||
|
pb.UnimplementedMessageServiceServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MessageServer) SendMessage(ctx context.Context, req *pb.SendMessageRequest) (*pb.SendMessageResponse, error) {
|
||||||
|
slog.Debug("grpc: entering SendMessage handler")
|
||||||
|
|
||||||
|
channelID, err := uuid.Parse(req.ChannelId)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("grpc: invalid channel_id", "error", err)
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "invalid channel_id: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
channel, err := api.DBGetChannel(channelID.String())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("grpc: channel not found", "channelid", channelID, "error", err)
|
||||||
|
return nil, status.Errorf(codes.NotFound, "channel not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := req.AudioFilename
|
||||||
|
if filename == "" {
|
||||||
|
filename = channelID.String() + ".wav"
|
||||||
|
}
|
||||||
|
|
||||||
|
audio, err := api.Store.Save(filename, bytes.NewReader(req.Audio))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("grpc: failed to save audio file", "error", err)
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed to save audio file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := api.DBAddFile(audio); err != nil {
|
||||||
|
slog.Error("grpc: failed to persist audio file record", "error", err)
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed to persist audio file")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &api.Message{
|
||||||
|
ID: uuid.New(),
|
||||||
|
Channel: *channel,
|
||||||
|
Created: time.Now(),
|
||||||
|
Content: req.Content,
|
||||||
|
Audio: *audio,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := api.DBAddMessage(msg); err != nil {
|
||||||
|
slog.Error("grpc: failed to persist message", "error", err)
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed to persist message")
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("grpc: message saved", "messageid", msg.ID, "channelid", channelID)
|
||||||
|
return &pb.SendMessageResponse{
|
||||||
|
Id: msg.ID.String(),
|
||||||
|
ChannelId: channel.ID.String(),
|
||||||
|
Created: msg.Created.Format(time.RFC3339),
|
||||||
|
Content: msg.Content,
|
||||||
|
AudioId: audio.ID.String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
pb "git.dubyatp.xyz/dubyatp/scannerbot/server/proto"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Start() {
|
||||||
|
lis, err := net.Listen("tcp", ":3001")
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("grpc: failed to listen", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s := grpc.NewServer()
|
||||||
|
pb.RegisterMessageServiceServer(s, &MessageServer{})
|
||||||
|
|
||||||
|
slog.Info("Starting the gRPC server...", "addr", lis.Addr())
|
||||||
|
if err := s.Serve(lis); err != nil {
|
||||||
|
slog.Error("grpc: server failed", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.dubyatp.xyz/dubyatp/scannerbot/server/api"
|
"git.dubyatp.xyz/dubyatp/scannerbot/server/api"
|
||||||
|
grpcserver "git.dubyatp.xyz/dubyatp/scannerbot/server/grpc"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,6 +36,8 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go grpcserver.Start()
|
||||||
|
|
||||||
slog.Info("Starting the API server...")
|
slog.Info("Starting the API server...")
|
||||||
api.Start()
|
api.Start()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.36.11
|
||||||
|
// protoc v3.21.12
|
||||||
|
// source: proto/message.proto
|
||||||
|
|
||||||
|
package proto
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
unsafe "unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
type SendMessageRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
ChannelId string `protobuf:"bytes,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"`
|
||||||
|
Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"`
|
||||||
|
AudioFilename string `protobuf:"bytes,3,opt,name=audio_filename,json=audioFilename,proto3" json:"audio_filename,omitempty"`
|
||||||
|
Audio []byte `protobuf:"bytes,4,opt,name=audio,proto3" json:"audio,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SendMessageRequest) Reset() {
|
||||||
|
*x = SendMessageRequest{}
|
||||||
|
mi := &file_proto_message_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SendMessageRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SendMessageRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *SendMessageRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_message_proto_msgTypes[0]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use SendMessageRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*SendMessageRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_message_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SendMessageRequest) GetChannelId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ChannelId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SendMessageRequest) GetContent() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Content
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SendMessageRequest) GetAudioFilename() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AudioFilename
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SendMessageRequest) GetAudio() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Audio
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendMessageResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
ChannelId string `protobuf:"bytes,2,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"`
|
||||||
|
Created string `protobuf:"bytes,3,opt,name=created,proto3" json:"created,omitempty"`
|
||||||
|
Content string `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"`
|
||||||
|
AudioId string `protobuf:"bytes,5,opt,name=audio_id,json=audioId,proto3" json:"audio_id,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SendMessageResponse) Reset() {
|
||||||
|
*x = SendMessageResponse{}
|
||||||
|
mi := &file_proto_message_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SendMessageResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SendMessageResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *SendMessageResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_message_proto_msgTypes[1]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use SendMessageResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*SendMessageResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_message_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SendMessageResponse) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SendMessageResponse) GetChannelId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ChannelId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SendMessageResponse) GetCreated() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Created
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SendMessageResponse) GetContent() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Content
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SendMessageResponse) GetAudioId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AudioId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_proto_message_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
const file_proto_message_proto_rawDesc = "" +
|
||||||
|
"\n" +
|
||||||
|
"\x13proto/message.proto\x12\n" +
|
||||||
|
"scannerbot\"\x8a\x01\n" +
|
||||||
|
"\x12SendMessageRequest\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"channel_id\x18\x01 \x01(\tR\tchannelId\x12\x18\n" +
|
||||||
|
"\acontent\x18\x02 \x01(\tR\acontent\x12%\n" +
|
||||||
|
"\x0eaudio_filename\x18\x03 \x01(\tR\raudioFilename\x12\x14\n" +
|
||||||
|
"\x05audio\x18\x04 \x01(\fR\x05audio\"\x93\x01\n" +
|
||||||
|
"\x13SendMessageResponse\x12\x0e\n" +
|
||||||
|
"\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"channel_id\x18\x02 \x01(\tR\tchannelId\x12\x18\n" +
|
||||||
|
"\acreated\x18\x03 \x01(\tR\acreated\x12\x18\n" +
|
||||||
|
"\acontent\x18\x04 \x01(\tR\acontent\x12\x19\n" +
|
||||||
|
"\baudio_id\x18\x05 \x01(\tR\aaudioId2`\n" +
|
||||||
|
"\x0eMessageService\x12N\n" +
|
||||||
|
"\vSendMessage\x12\x1e.scannerbot.SendMessageRequest\x1a\x1f.scannerbot.SendMessageResponseB1Z/git.dubyatp.xyz/dubyatp/scannerbot/server/protob\x06proto3"
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_proto_message_proto_rawDescOnce sync.Once
|
||||||
|
file_proto_message_proto_rawDescData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_proto_message_proto_rawDescGZIP() []byte {
|
||||||
|
file_proto_message_proto_rawDescOnce.Do(func() {
|
||||||
|
file_proto_message_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_message_proto_rawDesc), len(file_proto_message_proto_rawDesc)))
|
||||||
|
})
|
||||||
|
return file_proto_message_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_proto_message_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||||
|
var file_proto_message_proto_goTypes = []any{
|
||||||
|
(*SendMessageRequest)(nil), // 0: scannerbot.SendMessageRequest
|
||||||
|
(*SendMessageResponse)(nil), // 1: scannerbot.SendMessageResponse
|
||||||
|
}
|
||||||
|
var file_proto_message_proto_depIdxs = []int32{
|
||||||
|
0, // 0: scannerbot.MessageService.SendMessage:input_type -> scannerbot.SendMessageRequest
|
||||||
|
1, // 1: scannerbot.MessageService.SendMessage:output_type -> scannerbot.SendMessageResponse
|
||||||
|
1, // [1:2] is the sub-list for method output_type
|
||||||
|
0, // [0:1] is the sub-list for method input_type
|
||||||
|
0, // [0:0] is the sub-list for extension type_name
|
||||||
|
0, // [0:0] is the sub-list for extension extendee
|
||||||
|
0, // [0:0] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_proto_message_proto_init() }
|
||||||
|
func file_proto_message_proto_init() {
|
||||||
|
if File_proto_message_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_message_proto_rawDesc), len(file_proto_message_proto_rawDesc)),
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 2,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_proto_message_proto_goTypes,
|
||||||
|
DependencyIndexes: file_proto_message_proto_depIdxs,
|
||||||
|
MessageInfos: file_proto_message_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_proto_message_proto = out.File
|
||||||
|
file_proto_message_proto_goTypes = nil
|
||||||
|
file_proto_message_proto_depIdxs = nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package scannerbot;
|
||||||
|
|
||||||
|
option go_package = "git.dubyatp.xyz/dubyatp/scannerbot/server/proto";
|
||||||
|
|
||||||
|
service MessageService {
|
||||||
|
rpc SendMessage(SendMessageRequest) returns (SendMessageResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message SendMessageRequest {
|
||||||
|
string channel_id = 1;
|
||||||
|
string content = 2;
|
||||||
|
string audio_filename = 3;
|
||||||
|
bytes audio = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SendMessageResponse {
|
||||||
|
string id = 1;
|
||||||
|
string channel_id = 2;
|
||||||
|
string created = 3;
|
||||||
|
string content = 4;
|
||||||
|
string audio_id = 5;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.6.2
|
||||||
|
// - protoc v3.21.12
|
||||||
|
// source: proto/message.proto
|
||||||
|
|
||||||
|
package proto
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.64.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
|
const (
|
||||||
|
MessageService_SendMessage_FullMethodName = "/scannerbot.MessageService/SendMessage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MessageServiceClient is the client API for MessageService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type MessageServiceClient interface {
|
||||||
|
SendMessage(ctx context.Context, in *SendMessageRequest, opts ...grpc.CallOption) (*SendMessageResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type messageServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMessageServiceClient(cc grpc.ClientConnInterface) MessageServiceClient {
|
||||||
|
return &messageServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *messageServiceClient) SendMessage(ctx context.Context, in *SendMessageRequest, opts ...grpc.CallOption) (*SendMessageResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(SendMessageResponse)
|
||||||
|
err := c.cc.Invoke(ctx, MessageService_SendMessage_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageServiceServer is the server API for MessageService service.
|
||||||
|
// All implementations must embed UnimplementedMessageServiceServer
|
||||||
|
// for forward compatibility.
|
||||||
|
type MessageServiceServer interface {
|
||||||
|
SendMessage(context.Context, *SendMessageRequest) (*SendMessageResponse, error)
|
||||||
|
mustEmbedUnimplementedMessageServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedMessageServiceServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedMessageServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedMessageServiceServer) SendMessage(context.Context, *SendMessageRequest) (*SendMessageResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method SendMessage not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedMessageServiceServer) mustEmbedUnimplementedMessageServiceServer() {}
|
||||||
|
func (UnimplementedMessageServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeMessageServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to MessageServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeMessageServiceServer interface {
|
||||||
|
mustEmbedUnimplementedMessageServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterMessageServiceServer(s grpc.ServiceRegistrar, srv MessageServiceServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedMessageServiceServer was
|
||||||
|
// embedded by pointer and is nil. This will cause panics if an
|
||||||
|
// unimplemented method is ever invoked, so we test this at initialization
|
||||||
|
// time to prevent it from happening at runtime later due to I/O.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&MessageService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _MessageService_SendMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(SendMessageRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(MessageServiceServer).SendMessage(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: MessageService_SendMessage_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(MessageServiceServer).SendMessage(ctx, req.(*SendMessageRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageService_ServiceDesc is the grpc.ServiceDesc for MessageService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var MessageService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "scannerbot.MessageService",
|
||||||
|
HandlerType: (*MessageServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "SendMessage",
|
||||||
|
Handler: _MessageService_SendMessage_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "proto/message.proto",
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user