feat: implement memogram

This commit is contained in:
Steven
2024-05-14 22:04:46 +08:00
parent 3ebf96c827
commit 1dca2ffce0
11 changed files with 274 additions and 0 deletions

1
.air/build-errors.log Normal file
View File

@@ -0,0 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1

BIN
.air/memogram Executable file

Binary file not shown.

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
SERVER_ADDR=demo.usememos.com
ACCESS_TOKEN=your_access_token
BOT_TOKEN=your_bot_token

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.env

11
bin/memogram/main.go Normal file
View File

@@ -0,0 +1,11 @@
package main
import "github.com/usememos/memogram"
func main() {
service, err := memogram.NewService()
if err != nil {
panic(err)
}
service.Start()
}

22
client.go Normal file
View File

@@ -0,0 +1,22 @@
package memogram
import (
v1pb "github.com/usememos/memos/proto/gen/api/v1"
"google.golang.org/grpc"
)
type MemosClient struct {
AuthService v1pb.AuthServiceClient
UserService v1pb.UserServiceClient
MemoService v1pb.MemoServiceClient
ResourceService v1pb.ResourceServiceClient
}
func NewMemosClient(conn *grpc.ClientConn) *MemosClient {
return &MemosClient{
AuthService: v1pb.NewAuthServiceClient(conn),
UserService: v1pb.NewUserServiceClient(conn),
MemoService: v1pb.NewMemoServiceClient(conn),
ResourceService: v1pb.NewResourceServiceClient(conn),
}
}

25
config.go Normal file
View File

@@ -0,0 +1,25 @@
package memogram
import (
"github.com/caarlos0/env"
"github.com/joho/godotenv"
"github.com/pkg/errors"
)
type Config struct {
ServerAddr string `env:"SERVER_ADDR,required"`
BotToken string `env:"BOT_TOKEN,required"`
}
func getConfigFromEnv() (*Config, error) {
err := godotenv.Load(".env")
if err != nil {
panic(err.Error())
}
config := Config{}
if err := env.Parse(&config); err != nil {
return nil, errors.Wrap(err, "invalid configuration")
}
return &config, nil
}

25
go.mod Normal file
View File

@@ -0,0 +1,25 @@
module github.com/usememos/memogram
go 1.22.2
require (
github.com/caarlos0/env v3.5.0+incompatible
github.com/usememos/memos v0.21.1-0.20240509141027-584c66906883
google.golang.org/grpc v1.63.2
)
require (
github.com/go-telegram/bot v1.2.2
github.com/joho/godotenv v1.5.1
)
require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
github.com/pkg/errors v0.9.1
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect
google.golang.org/protobuf v1.34.1 // indirect
)

36
go.sum Normal file
View File

@@ -0,0 +1,36 @@
github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs=
github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-telegram/bot v1.2.2 h1:LwGbSzjcSi0w4Ke8JUpgbBhJwwYTl2ITmhubeM2WvN8=
github.com/go-telegram/bot v1.2.2/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/usememos/memos v0.21.1-0.20240509141027-584c66906883 h1:eEEnUzReqdPbYZZtMTTes6p976POfHeW6lxz9LZuyFI=
github.com/usememos/memos v0.21.1-0.20240509141027-584c66906883/go.mod h1:EOB3cAQEaeYvVggKIe7oMyOJYQoZ0w2vTNS5uWUYSaU=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU=
google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

134
memogram.go Normal file
View File

@@ -0,0 +1,134 @@
package memogram
import (
"context"
"fmt"
"log/slog"
"strings"
"sync"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/pkg/errors"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
// userAccessTokenCache is a cache for user access token.
// Key is the user id from telegram.
// Value is the access token from memos.
var userAccessTokenCache sync.Map // map[int64]string
type Service struct {
config *Config
client *MemosClient
}
func NewService() (*Service, error) {
config, err := getConfigFromEnv()
if err != nil {
return nil, errors.Wrap(err, "failed to get config from env")
}
conn, err := grpc.Dial(config.ServerAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
slog.Error("failed to connect to server", slog.Any("err", err))
return nil, errors.Wrap(err, "failed to connect to server")
}
client := NewMemosClient(conn)
return &Service{
config,
client,
}, nil
}
func (s *Service) Start() {
config, err := getConfigFromEnv()
if err != nil {
slog.Error("failed to get config from env", slog.Any("err", err))
return
}
opts := []bot.Option{
bot.WithDefaultHandler(s.handler),
}
b, err := bot.New(config.BotToken, opts...)
if err != nil {
slog.Error("failed to create bot", slog.Any("err", err))
return
}
slog.Info("memogram started")
b.Start(context.Background())
}
func (s *Service) handler(ctx context.Context, b *bot.Bot, m *models.Update) {
if strings.HasPrefix(m.Message.Text, "/start ") {
s.startHandler(ctx, b, m)
return
}
userID := m.Message.From.ID
if _, ok := userAccessTokenCache.Load(userID); !ok {
b.SendMessage(ctx, &bot.SendMessageParams{
ChatID: m.Message.Chat.ID,
Text: "Please start the bot with /start <access_token>",
})
return
}
message := m.Message
text := message.Text
if text == "" {
b.SendMessage(ctx, &bot.SendMessageParams{
ChatID: m.Message.Chat.ID,
Text: "Please input memo content",
})
return
}
accessToken, _ := userAccessTokenCache.Load(userID)
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("Authorization", fmt.Sprintf("Bearer %s", accessToken.(string))))
memo, err := s.client.MemoService.CreateMemo(ctx, &v1pb.CreateMemoRequest{
Content: text,
})
if err != nil {
slog.Error("failed to create memo", slog.Any("err", err))
b.SendMessage(ctx, &bot.SendMessageParams{
ChatID: m.Message.Chat.ID,
Text: "Failed to create memo",
})
return
}
b.SendMessage(ctx, &bot.SendMessageParams{
ChatID: m.Message.Chat.ID,
Text: fmt.Sprintf("Memo created with %s", memo.Name),
})
}
func (s *Service) startHandler(ctx context.Context, b *bot.Bot, m *models.Update) {
userID := m.Message.From.ID
accessToken := strings.TrimPrefix(m.Message.Text, "/start ")
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("Authorization", fmt.Sprintf("Bearer %s", accessToken)))
user, err := s.client.AuthService.GetAuthStatus(ctx, &v1pb.GetAuthStatusRequest{})
if err != nil {
b.SendMessage(ctx, &bot.SendMessageParams{
ChatID: m.Message.Chat.ID,
Text: "Invalid access token",
})
return
}
userAccessTokenCache.Store(userID, accessToken)
b.SendMessage(ctx, &bot.SendMessageParams{
ChatID: m.Message.Chat.ID,
Text: fmt.Sprintf("Hello %s!", user.Nickname),
})
}

16
scripts/.air.toml Normal file
View File

@@ -0,0 +1,16 @@
root = "."
tmp_dir = ".air"
[build]
bin = "./.air/memogram"
cmd = "go build -o ./.air/memogram ./bin/memogram/main.go"
delay = 1000
exclude_dir = [".air", "build"]
include_ext = ["go", "mod", "sum"]
exclude_file = []
exclude_regex = []
exclude_unchanged = true
follow_symlink = false
send_interrupt = true
kill_delay = 2000
stop_on_error = true