add theming and telegram login
This commit is contained in:
parent
0d85507f41
commit
afa622d3c7
34 changed files with 862 additions and 229 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,3 +4,4 @@ install.sh
|
|||
config-*.yml
|
||||
!config-test.yml
|
||||
data
|
||||
.vscode/launch.json
|
12
.vscode/launch-tpl.json
vendored
Normal file
12
.vscode/launch-tpl.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach to Process",
|
||||
"type": "go",
|
||||
"request": "attach",
|
||||
"mode": "local",
|
||||
"processId": 0
|
||||
}
|
||||
]
|
||||
}
|
4
Makefile
4
Makefile
|
@ -22,8 +22,9 @@ build: webroot $(COMMANDSDIST)
|
|||
dev:
|
||||
while true; do \
|
||||
kill $$(cat .server.pid); \
|
||||
make build && \
|
||||
make GOGCFLAGS='all=-N -l' build && \
|
||||
(dist/server -c config-dev.yml & echo $$! > .server.pid); \
|
||||
jq " .configurations[0].processId = $$(cat .server.pid) " .vscode/launch-tpl.json > .vscode/launch.json; \
|
||||
inotifywait -e modify -r webroot internal server config && kill $(cat .server.pid) ; \
|
||||
done
|
||||
|
||||
|
@ -45,6 +46,7 @@ dist/%: ${CMD_DIR}/% FORCE
|
|||
go build -buildvcs\
|
||||
-ldflags "-X ${MODULE_PATH}/internal/version.tagVersion=$(VERSION) \
|
||||
-X ${MODULE_PATH}/internal/version.buildDate=$(BUILDDATE)" \
|
||||
-gcflags "$(GOGCFLAGS)" \
|
||||
-o $@ ${MODULE_PATH}/$<
|
||||
|
||||
.PHONY: build clean
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
htmlTemplate "html/template"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
htmlTpl := htmlTemplate.Must(htmlTemplate.ParseGlob("*.tpl.html"))
|
||||
for _, tpl := range htmlTpl.Templates() {
|
||||
log.Printf("template: %s", tpl.Name())
|
||||
tpl.Execute(os.Stdout, nil)
|
||||
}
|
||||
}
|
|
@ -60,6 +60,7 @@ type C struct {
|
|||
Password string
|
||||
PublicKeyId []string
|
||||
Roles []string
|
||||
Telegram string
|
||||
}
|
||||
Yubikey struct {
|
||||
ClientId string
|
||||
|
|
|
@ -2,4 +2,5 @@ package config
|
|||
|
||||
type FileStore struct {
|
||||
BasePath string
|
||||
Serve bool
|
||||
}
|
||||
|
|
|
@ -3,15 +3,19 @@ package auth
|
|||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
"github.com/eternal-flame-AD/yoake/config"
|
||||
"github.com/eternal-flame-AD/yoake/internal/auth/tglogin"
|
||||
"github.com/eternal-flame-AD/yoake/internal/db"
|
||||
"github.com/eternal-flame-AD/yoake/internal/echoerror"
|
||||
"github.com/eternal-flame-AD/yoake/internal/util"
|
||||
"github.com/gorilla/sessions"
|
||||
|
@ -23,6 +27,16 @@ const AuthSessionName = "auth_session"
|
|||
|
||||
var dummyHash string
|
||||
|
||||
func authSessionOptions() *sessions.Options {
|
||||
return &sessions.Options{
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: config.Config().Listen.Ssl.Use,
|
||||
MaxAge: config.Config().Auth.ValidMinutes * 60 * 5,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
var dummyPassword [16]byte
|
||||
_, err := rand.Read(dummyPassword[:])
|
||||
|
@ -40,6 +54,13 @@ type RequestAuth struct {
|
|||
Valid bool
|
||||
Roles []string
|
||||
Expire time.Time
|
||||
Ident UserIdent
|
||||
}
|
||||
|
||||
type UserIdent struct {
|
||||
Username string `json:"username"`
|
||||
PhotoURL string `json:"photo_url"`
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
|
||||
func (a RequestAuth) HasRole(role Role) bool {
|
||||
|
@ -99,13 +120,7 @@ func Middleware(store sessions.Store) echo.MiddlewareFunc {
|
|||
return func(c echo.Context) error {
|
||||
|
||||
sess, _ := store.Get(c.Request(), AuthSessionName)
|
||||
sess.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: config.Config().Listen.Ssl.Use,
|
||||
MaxAge: config.Config().Auth.ValidMinutes * 60 * 5,
|
||||
}
|
||||
sess.Options = authSessionOptions()
|
||||
|
||||
var auth RequestAuth
|
||||
if expireTs, ok := sess.Values["expire"].(string); ok {
|
||||
|
@ -130,6 +145,15 @@ func Middleware(store sessions.Store) echo.MiddlewareFunc {
|
|||
c.Set("devel", true)
|
||||
}
|
||||
|
||||
if existingIdentJSON, ok := sess.Values["ident"].([]byte); !ok {
|
||||
sess.Values["ident"] = []byte("{}")
|
||||
sess.Save(c.Request(), c.Response())
|
||||
} else if auth.Valid {
|
||||
if err := json.Unmarshal(existingIdentJSON, &auth.Ident); err != nil {
|
||||
log.Printf("invalid ident: %v", existingIdentJSON)
|
||||
}
|
||||
}
|
||||
|
||||
c.Set("auth_"+AuthSessionName, auth)
|
||||
c.Set("auth_store", store)
|
||||
|
||||
|
@ -138,15 +162,20 @@ func Middleware(store sessions.Store) echo.MiddlewareFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func issueIdent(c echo.Context, ident UserIdent) error {
|
||||
sess, _ := c.Get("auth_store").(sessions.Store).Get(c.Request(), AuthSessionName)
|
||||
sess.Options = authSessionOptions()
|
||||
identJSON, err := json.Marshal(ident)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sess.Values["ident"] = identJSON
|
||||
return sess.Save(c.Request(), c.Response())
|
||||
}
|
||||
|
||||
func issueSession(c echo.Context, period time.Duration, roles []string) error {
|
||||
sess, _ := c.Get("auth_store").(sessions.Store).Get(c.Request(), AuthSessionName)
|
||||
sess.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: config.Config().Listen.Ssl.Use,
|
||||
MaxAge: config.Config().Auth.ValidMinutes * 60 * 5,
|
||||
}
|
||||
sess.Options = authSessionOptions()
|
||||
if period == 0 {
|
||||
period = time.Duration(config.Config().Auth.ValidMinutes) * time.Minute
|
||||
}
|
||||
|
@ -169,7 +198,7 @@ type LoginForm struct {
|
|||
|
||||
var errInvalidUserPass = echoerror.NewHttp(http.StatusUnauthorized, errors.New("invalid username or password"))
|
||||
|
||||
func Register(g *echo.Group) (err error) {
|
||||
func Register(g *echo.Group, database db.DB) (err error) {
|
||||
g.GET("/auth.json", func(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, GetRequestAuth(c))
|
||||
})
|
||||
|
@ -182,6 +211,35 @@ func Register(g *echo.Group) (err error) {
|
|||
loginRateLimiter := middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
|
||||
Store: loginRateLimiterStore,
|
||||
})
|
||||
g.POST("/login_tg", func(c echo.Context) error {
|
||||
tgForm, err := tglogin.VerifyLoginCallback(c, database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tgForm.ID == 0 {
|
||||
return echoerror.NewHttp(http.StatusUnauthorized, errors.New("invalid telegram login"))
|
||||
}
|
||||
roles := []string{string(RoleTelgram)}
|
||||
for _, user := range config.Config().Auth.Users {
|
||||
if user.Telegram == "@"+tgForm.UserName ||
|
||||
user.Telegram == strconv.FormatUint(tgForm.ID, 10) {
|
||||
{
|
||||
roles = append(roles, user.Roles...)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := issueSession(c, 0, roles); err != nil {
|
||||
return fmt.Errorf("failed to issue session: %v", err)
|
||||
}
|
||||
if err := issueIdent(c, UserIdent{
|
||||
Username: "@" + tgForm.UserName,
|
||||
PhotoURL: tgForm.PhotoURL,
|
||||
DisplayName: strings.TrimSpace(tgForm.FirstName + " " + tgForm.LastName),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to issue ident: %v", err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, GetRequestAuth(c))
|
||||
})
|
||||
g.POST("/login", func(c echo.Context) error {
|
||||
var form LoginForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
|
@ -228,7 +286,15 @@ func Register(g *echo.Group) (err error) {
|
|||
return echo.NewHTTPError(http.StatusBadRequest, "otp not required but you provided one, this may be an configuration error")
|
||||
}
|
||||
|
||||
issueSession(c, 0, user.Roles)
|
||||
if err := issueSession(c, 0, user.Roles); err != nil {
|
||||
return fmt.Errorf("failed to issue session: %w", err)
|
||||
}
|
||||
if err := issueIdent(c, UserIdent{
|
||||
Username: form.Username,
|
||||
DisplayName: form.Username,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to issue identification: %w", err)
|
||||
}
|
||||
c.JSON(http.StatusOK, map[string]interface{}{"message": "ok", "ok": true})
|
||||
return nil
|
||||
} else {
|
||||
|
|
7
internal/auth/gob.go
Normal file
7
internal/auth/gob.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package auth
|
||||
|
||||
import "encoding/gob"
|
||||
|
||||
func init() {
|
||||
gob.Register(UserIdent{})
|
||||
}
|
|
@ -3,6 +3,7 @@ package auth
|
|||
type Role string
|
||||
|
||||
const (
|
||||
RoleAdmin Role = "admin"
|
||||
RoleUser Role = "user"
|
||||
RoleAdmin Role = "admin"
|
||||
RoleUser Role = "user"
|
||||
RoleTelgram Role = "tg"
|
||||
)
|
||||
|
|
93
internal/auth/tglogin/verify.go
Normal file
93
internal/auth/tglogin/verify.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package tglogin
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/config"
|
||||
"github.com/eternal-flame-AD/yoake/internal/db"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type LoginCallbackForm struct {
|
||||
ID uint64 `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
UserName string `json:"username"`
|
||||
PhotoURL string `json:"photo_url"`
|
||||
AuthDate uint64 `json:"auth_date"`
|
||||
}
|
||||
|
||||
type verifyForm map[string]interface{}
|
||||
|
||||
func sha256Str(input string) []byte {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(input))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func VerifyLoginCallback(c echo.Context, database db.DB) (*LoginCallbackForm, error) {
|
||||
txn := database.NewTransaction(true)
|
||||
defer txn.Discard()
|
||||
tgToken := config.Config().Comm.Telegram.Token
|
||||
if tgToken == "" {
|
||||
return nil, fmt.Errorf("telegram token not set")
|
||||
}
|
||||
verifyForm := make(verifyForm)
|
||||
if err := c.Bind(&verifyForm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verifyKeys := make([]string, 0, len(verifyForm))
|
||||
for k := range verifyForm {
|
||||
if k != "hash" {
|
||||
verifyKeys = append(verifyKeys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(verifyKeys)
|
||||
hmac := hmac.New(sha256.New, sha256Str(tgToken))
|
||||
for i, k := range verifyKeys {
|
||||
if i != 0 {
|
||||
hmac.Write([]byte("\n"))
|
||||
}
|
||||
switch val := verifyForm[k].(type) {
|
||||
case string:
|
||||
fmt.Fprintf(hmac, "%s=%s", k, val)
|
||||
case float64:
|
||||
fmt.Fprintf(hmac, "%s=%v", k, uint64(val))
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid verify form, unexpected type %T", val)
|
||||
}
|
||||
}
|
||||
if fmt.Sprintf("%x", hmac.Sum(nil)) != verifyForm["hash"] {
|
||||
return nil, echo.NewHTTPError(403, "invalid hash")
|
||||
}
|
||||
|
||||
form := new(LoginCallbackForm)
|
||||
remarshaled, err := json.Marshal(verifyForm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(remarshaled, form); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lastAuthDateDBKey := fmt.Sprintf("auth_telegram_last_auth_date_%d", form.ID)
|
||||
var dbLastAuthDate uint64
|
||||
if err := db.GetJSON(txn, []byte(lastAuthDateDBKey), &dbLastAuthDate); db.IsNotFound(err) {
|
||||
dbLastAuthDate = 0
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if form.AuthDate <= dbLastAuthDate || time.Since(time.Unix(int64(form.AuthDate), 0)) > 10*time.Minute {
|
||||
return nil, echo.NewHTTPError(403, "authentication payload expired")
|
||||
}
|
||||
if err := db.SetJSON(txn, []byte(lastAuthDateDBKey), form.AuthDate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return form, nil
|
||||
}
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/eternal-flame-AD/yoake/config"
|
||||
"github.com/eternal-flame-AD/yoake/internal/comm/model"
|
||||
"github.com/eternal-flame-AD/yoake/internal/db"
|
||||
"github.com/eternal-flame-AD/yoake/internal/servetpl/funcmap"
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
|
@ -21,7 +22,7 @@ const (
|
|||
RoleOwner Role = 2
|
||||
)
|
||||
|
||||
type CommandHandler func(bot *Bot, role Role, msg *tgbotapi.Message) error
|
||||
type CommandHandler func(bot *Bot, role Role, update tgbotapi.Update) error
|
||||
|
||||
type Bot struct {
|
||||
client *tgbotapi.BotAPI
|
||||
|
@ -75,6 +76,10 @@ func (b *Bot) saveConf() error {
|
|||
return txn.Commit()
|
||||
}
|
||||
|
||||
func (b *Bot) Client() *tgbotapi.BotAPI {
|
||||
return b.client
|
||||
}
|
||||
|
||||
func (b *Bot) SendHTML(chatID int64, fmtStr string, args ...interface{}) error {
|
||||
for i := range args {
|
||||
switch v := args[i].(type) {
|
||||
|
@ -90,32 +95,50 @@ func (b *Bot) SendHTML(chatID int64, fmtStr string, args ...interface{}) error {
|
|||
return err
|
||||
}
|
||||
func (b *Bot) handleUpdate(update tgbotapi.Update) error {
|
||||
if update.Message == nil {
|
||||
return nil
|
||||
}
|
||||
log.Printf("received message from %s: %v", update.Message.From.UserName, update.Message)
|
||||
msg := *update.Message
|
||||
conf := config.Config().Comm.Telegram
|
||||
if strings.HasPrefix(conf.Owner, "@") && msg.From.UserName == conf.Owner[1:] {
|
||||
log.Printf("telegram owner chat id set: %d", msg.Chat.ID)
|
||||
b.OwnerChatID = msg.Chat.ID
|
||||
} else if id, err := strconv.ParseInt(conf.Owner, 10, 64); err == nil && msg.From.ID == id || msg.Chat.ID == id {
|
||||
if msg.Chat.ID != b.OwnerChatID {
|
||||
log.Printf("telegram owner chat id set: %d", msg.Chat.ID)
|
||||
b.OwnerChatID = msg.Chat.ID
|
||||
|
||||
role := RoleAnonymous
|
||||
if msg := update.Message; msg != nil {
|
||||
log.Printf("received message from %s: %v", update.Message.From.UserName, update.Message)
|
||||
msg := *update.Message
|
||||
conf := config.Config().Comm.Telegram
|
||||
if strings.HasPrefix(conf.Owner, "@") && msg.From.UserName == conf.Owner[1:] {
|
||||
if msg.Chat.ID != b.OwnerChatID {
|
||||
log.Printf("telegram owner chat id set: %d", msg.Chat.ID)
|
||||
b.OwnerChatID = msg.Chat.ID
|
||||
}
|
||||
} else if id, err := strconv.ParseInt(conf.Owner, 10, 64); err == nil && msg.From.ID == id || msg.Chat.ID == id {
|
||||
if msg.Chat.ID != b.OwnerChatID {
|
||||
log.Printf("telegram owner chat id set: %d", msg.Chat.ID)
|
||||
b.OwnerChatID = msg.Chat.ID
|
||||
}
|
||||
}
|
||||
if msg.Chat.ID == b.OwnerChatID {
|
||||
role = RoleOwner
|
||||
}
|
||||
if msg.IsCommand() {
|
||||
if handler, ok := b.cmdHandlers[msg.Command()]; ok {
|
||||
return handler(b, role, update)
|
||||
} else {
|
||||
if err := b.SendHTML(msg.Chat.ID, "unknown command: %s\n", msg.Command()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
role := RoleAnonymous
|
||||
if msg.Chat.ID == b.OwnerChatID {
|
||||
role = RoleOwner
|
||||
}
|
||||
|
||||
if msg.IsCommand() {
|
||||
if handler, ok := b.cmdHandlers[msg.Command()]; ok {
|
||||
return handler(b, role, &msg)
|
||||
} else {
|
||||
if err := b.SendHTML(msg.Chat.ID, "unknown command: %s\n", msg.Command()); err != nil {
|
||||
return err
|
||||
if callback := update.CallbackQuery; callback != nil {
|
||||
cb := tgbotapi.NewCallback(callback.ID, "")
|
||||
defer b.client.Send(cb)
|
||||
if msg := callback.Message; msg != nil {
|
||||
if callbackMsg := msg.ReplyToMessage; callbackMsg != nil && callbackMsg.IsCommand() {
|
||||
if handler, ok := b.cmdHandlers[callbackMsg.Command()]; ok {
|
||||
log.Printf("callback for command %s", callbackMsg.Command())
|
||||
return handler(b, role, update)
|
||||
} else {
|
||||
if err := b.SendHTML(callback.Message.Chat.ID, "unknown command: %s\n", callback.Message.Command()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -127,8 +150,12 @@ func (bot *Bot) start() error {
|
|||
u := tgbotapi.NewUpdate(bot.LastUpdateID + 1)
|
||||
u.Timeout = 60
|
||||
|
||||
bot.RegisterCommand("start", "onboarding command", func(bot *Bot, role Role, msg *tgbotapi.Message) error {
|
||||
bot.client.Send(tgbotapi.NewMessage(msg.Chat.ID, strings.ReplaceAll(banner, "{name}", msg.From.FirstName+" "+msg.From.LastName)))
|
||||
bot.RegisterCommand("start", "onboarding command", func(bot *Bot, role Role, upd tgbotapi.Update) error {
|
||||
if msg := upd.Message; msg != nil {
|
||||
if _, err := bot.client.Send(tgbotapi.NewMessage(msg.Chat.ID, strings.ReplaceAll(banner, "{name}", msg.From.FirstName+" "+msg.From.LastName))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
|
@ -136,10 +163,19 @@ func (bot *Bot) start() error {
|
|||
go func() {
|
||||
for update := range updates {
|
||||
if err := bot.handleUpdate(update); err != nil {
|
||||
if fid := update.FromChat().ID; fid != bot.OwnerChatID {
|
||||
bot.SendHTML(fid, "<b>RUNTIME ERROR</b>\n<pre>%s</pre>\nBot owner has been notified.", err)
|
||||
fromChat := update.FromChat()
|
||||
if stopImgURL, err := funcmap.TrimaImg("ja/btn_stop.gif", "url"); err != nil {
|
||||
log.Printf("failed to get stop image: %v", err)
|
||||
} else {
|
||||
stopPhoto := tgbotapi.NewPhoto(fromChat.ID, tgbotapi.FileURL(stopImgURL))
|
||||
stopPhoto.DisableNotification = true
|
||||
bot.client.Send(stopPhoto)
|
||||
}
|
||||
bot.SendHTML(bot.OwnerChatID, "<b>RUNTIME ERROR</b>\noriginating chat ID: %d (@%s)\n\n<pre>%s</pre>", update.FromChat().ID, update.FromChat().UserName, err)
|
||||
if fromChat.ID != bot.OwnerChatID {
|
||||
bot.SendHTML(fromChat.ID, "<b>Runtime Error</b>\n<pre>%s</pre>\nBot owner has been notified.", err)
|
||||
|
||||
}
|
||||
bot.SendHTML(bot.OwnerChatID, "<b>Runtime Error</b>\noriginating chat ID: %d (@%s)\n\n<pre>%s</pre>", update.FromChat().ID, update.FromChat().UserName, err)
|
||||
log.Printf("telegram runtime error: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"log"
|
||||
|
@ -20,118 +21,173 @@ import (
|
|||
)
|
||||
|
||||
func telegramHandler(database db.DB) telegram.CommandHandler {
|
||||
return func(bot *telegram.Bot, role telegram.Role, msg *tgbotapi.Message) error {
|
||||
if role != telegram.RoleOwner {
|
||||
bot.SendHTML(msg.Chat.ID, "EPERM: You are not authorized to use this command.")
|
||||
return nil
|
||||
}
|
||||
switch msg.Command() {
|
||||
case "medtake":
|
||||
argStr := strings.TrimSpace(msg.CommandArguments())
|
||||
if len(argStr) == 0 || argStr == "help" {
|
||||
bot.SendHTML(msg.Chat.ID, "Usage: /medtake <keyname> [dose] [YYYY-MM-DDZHH:mm:ss]")
|
||||
return nil
|
||||
}
|
||||
args := strings.Split(argStr, " ")
|
||||
meds, err := DBMedListGet(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, med := range meds {
|
||||
if med.KeyName() == strings.ToLower(args[0]) {
|
||||
dose := med.Dosage
|
||||
if len(args) > 1 {
|
||||
dose, err = strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return func(bot *telegram.Bot, role telegram.Role, update tgbotapi.Update) error {
|
||||
msg := update.Message
|
||||
if callback := update.CallbackQuery; callback != nil {
|
||||
msg = callback.Message.ReplyToMessage
|
||||
switch msg.Command() {
|
||||
case "medtake":
|
||||
fields := strings.Fields(callback.Data)
|
||||
if fields[0] == "" {
|
||||
return nil
|
||||
}
|
||||
switch fields[0] {
|
||||
case "medtake_confirm":
|
||||
|
||||
if msg := callback.Message; msg != nil {
|
||||
if _, err := bot.Client().Send(tgbotapi.NewEditMessageReplyMarkup(msg.Chat.ID, msg.MessageID, tgbotapi.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]tgbotapi.InlineKeyboardButton{},
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
timestamp := time.Now()
|
||||
if len(args) > 2 {
|
||||
timestamp, err = time.Parse("2006-01-02Z15:04:05", args[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case "medtake_undo":
|
||||
uuid := fields[1]
|
||||
delDose := ComplianceLog{
|
||||
UUID: uuid,
|
||||
Actual: ComplianceDoseInfo{
|
||||
Time: time.Now(),
|
||||
Dose: -1,
|
||||
},
|
||||
}
|
||||
var log ComplianceLog
|
||||
log.MedKeyname = med.KeyName()
|
||||
log.Actual.Time = timestamp
|
||||
log.Actual.Dose = dose
|
||||
if err := DBMedComplianceLogSetOne(database, med, &log); err != nil {
|
||||
if err := DBMedComplianceLogSetOne(database, Direction{}, &delDose); err != nil {
|
||||
return err
|
||||
}
|
||||
return bot.SendHTML(msg.Chat.ID, "Success.\n\nDose: %d/%d\nTime Actual: %s\nTime Expected: %s\n\nOffset: %.2f", log.Actual.Dose, log.Expected.Dose,
|
||||
log.Actual.Time.Format("2006-01-02 15:04:05"), log.Expected.Time.Format("2006-01-02 15:04:05"), log.DoseOffset)
|
||||
if err := bot.SendHTML(msg.Chat.ID, "Last action has been undone."); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := bot.Client().Request(tgbotapi.NewDeleteMessage(msg.Chat.ID, callback.Message.MessageID)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return bot.SendHTML(msg.Chat.ID, "keyname %s not found", args[0])
|
||||
case "medinfo":
|
||||
meds, err := DBMedListGet(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logs, err := DBMedComplianceLogGet(database, util.DateRangeAround(time.Now(), 1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
argStr := strings.TrimSpace(msg.CommandArguments())
|
||||
if argStr == "help" {
|
||||
bot.SendHTML(msg.Chat.ID, "Usage: /medinfo [[keyname...]|all]")
|
||||
return nil
|
||||
} else if msg != nil {
|
||||
if role != telegram.RoleOwner {
|
||||
bot.SendHTML(msg.Chat.ID, "EPERM: You are not authorized to use this command.")
|
||||
return nil
|
||||
}
|
||||
all := true
|
||||
keynames := strings.Split(msg.CommandArguments(), " ")
|
||||
var replies []strings.Builder
|
||||
if argStr == "" || argStr == "all" {
|
||||
all = argStr == "all"
|
||||
replies = make([]strings.Builder, len(meds))
|
||||
} else {
|
||||
replies = make([]strings.Builder, len(keynames))
|
||||
}
|
||||
|
||||
for i, med := range meds {
|
||||
index := -1
|
||||
if argStr != "" && argStr != "all" {
|
||||
for j, keyname := range keynames {
|
||||
if med.KeyName() == keyname {
|
||||
index = j
|
||||
break
|
||||
switch msg.Command() {
|
||||
case "medtake":
|
||||
argStr := strings.TrimSpace(msg.CommandArguments())
|
||||
if len(argStr) == 0 || argStr == "help" {
|
||||
return bot.SendHTML(msg.Chat.ID, html.EscapeString("Usage: /medtake <keyname> [dose] [YYYY-MM-DDZHH:mm:ss]"))
|
||||
}
|
||||
args := strings.Split(argStr, " ")
|
||||
meds, err := DBMedListGet(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, med := range meds {
|
||||
if med.KeyName() == strings.ToLower(args[0]) {
|
||||
dose := med.Dosage
|
||||
if len(args) > 1 {
|
||||
dose, err = strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
timestamp := time.Now()
|
||||
if len(args) > 2 {
|
||||
timestamp, err = time.Parse("2006-01-02Z15:04:05", args[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var log ComplianceLog
|
||||
log.MedKeyname = med.KeyName()
|
||||
log.Actual.Time = timestamp
|
||||
log.Actual.Dose = dose
|
||||
if err := DBMedComplianceLogSetOne(database, med, &log); err != nil {
|
||||
return err
|
||||
}
|
||||
logJSON, err := json.MarshalIndent(log, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
message := tgbotapi.NewMessage(msg.Chat.ID, fmt.Sprintf("Medication %s taken. <pre>%s</pre>", med.Name, logJSON))
|
||||
message.ChatID = msg.Chat.ID
|
||||
message.ParseMode = tgbotapi.ModeHTML
|
||||
message.ReplyToMessageID = msg.MessageID
|
||||
message.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup([]tgbotapi.InlineKeyboardButton{
|
||||
tgbotapi.NewInlineKeyboardButtonData("Undo", fmt.Sprintf("medtake_undo %s", log.UUID)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Confirm", "medtake_confirm"),
|
||||
})
|
||||
_, err = bot.Client().Send(message)
|
||||
return err
|
||||
}
|
||||
if index == -1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return bot.SendHTML(msg.Chat.ID, "keyname %s not found", args[0])
|
||||
case "medinfo":
|
||||
meds, err := DBMedListGet(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logs, err := DBMedComplianceLogGet(database, util.DateRangeAround(time.Now(), 1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
argStr := strings.TrimSpace(msg.CommandArguments())
|
||||
if argStr == "help" {
|
||||
bot.SendHTML(msg.Chat.ID, "Usage: /medinfo [[keyname...]|all]")
|
||||
return nil
|
||||
}
|
||||
all := true
|
||||
keynames := strings.Split(msg.CommandArguments(), " ")
|
||||
var replies []strings.Builder
|
||||
if argStr == "" || argStr == "all" {
|
||||
all = argStr == "all"
|
||||
replies = make([]strings.Builder, len(meds))
|
||||
} else {
|
||||
index = i
|
||||
replies = make([]strings.Builder, len(keynames))
|
||||
}
|
||||
name, dir := med.ShortHand()
|
||||
fmt.Fprintf(&replies[index], "<b>%s</b> <i>%s</i>\n", html.EscapeString(name), html.EscapeString(dir))
|
||||
fmt.Fprintf(&replies[index], "Dosage: %d\n", med.Dosage)
|
||||
nextDose := logs.ProjectNextDose(med)
|
||||
fmt.Fprintf(&replies[index], "Next Dose: %d%s at %s\n", nextDose.Expected.Dose, html.EscapeString(med.DosageUnit), nextDose.Expected.Time.Format("2006-01-02 15:04:05"))
|
||||
stateStr := "unknown"
|
||||
if util.Contain(med.Flags, DirectionFlagPRN) && nextDose.DoseOffset >= 0 {
|
||||
stateStr = "available"
|
||||
} else if nextDose.DoseOffset > 0 {
|
||||
stateStr = "DUE"
|
||||
} else if util.Contain(med.Flags, DirectionFlagAdLib) {
|
||||
stateStr = "available"
|
||||
} else if nextDose.DoseOffset < 0 {
|
||||
stateStr = "scheduled"
|
||||
|
||||
for i, med := range meds {
|
||||
index := -1
|
||||
if argStr != "" && argStr != "all" {
|
||||
for j, keyname := range keynames {
|
||||
if med.KeyName() == keyname {
|
||||
index = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
index = i
|
||||
}
|
||||
name, dir := med.ShortHand()
|
||||
fmt.Fprintf(&replies[index], "<b>%s</b> <i>%s</i>\n", html.EscapeString(name), html.EscapeString(dir))
|
||||
nextDose := logs.ProjectNextDose(med)
|
||||
stateStr := "unknown"
|
||||
if util.Contain(med.Flags, DirectionFlagPRN) && nextDose.DoseOffset >= 0 {
|
||||
stateStr = "available"
|
||||
} else if nextDose.DoseOffset > 0 {
|
||||
stateStr = "DUE"
|
||||
} else if util.Contain(med.Flags, DirectionFlagAdLib) {
|
||||
stateStr = "available"
|
||||
} else if nextDose.DoseOffset < 0 {
|
||||
stateStr = "scheduled"
|
||||
}
|
||||
fmt.Fprintf(&replies[index], "Status: %s\n", stateStr)
|
||||
fmt.Fprintf(&replies[index], "Offset: %+.2f period, expected %.2f hrs (%s)\n", nextDose.DoseOffset, float64(time.Until(nextDose.Expected.Time))/float64(time.Hour), nextDose.Expected.Time.Format("2006-01-02 15:04:05"))
|
||||
if !all && nextDose.DoseOffset < 0 {
|
||||
replies[index].Reset()
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&replies[index], "Offset: %.2f (%s)\n", nextDose.DoseOffset, stateStr)
|
||||
if !all && nextDose.DoseOffset < 0 {
|
||||
replies[index].Reset()
|
||||
var out strings.Builder
|
||||
for _, reply := range replies {
|
||||
if reply.Len() > 0 {
|
||||
out.WriteString(reply.String())
|
||||
out.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return bot.SendHTML(msg.Chat.ID, out.String())
|
||||
}
|
||||
var out strings.Builder
|
||||
for _, reply := range replies {
|
||||
if reply.Len() > 0 {
|
||||
out.WriteString(reply.String())
|
||||
out.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return bot.SendHTML(msg.Chat.ID, out.String())
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package health
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
|
@ -148,7 +147,6 @@ func (c ComplianceLogList) UpdateDoseOffset(dir Direction) {
|
|||
}
|
||||
|
||||
c[i].DoseOffset = f64OrNan(doseOffset(dir, thisDose, lastDose))
|
||||
log.Printf("thisDose: %+v, \nlastDose: %+v\n-->offset: %f\n", thisDose, lastDose, c[i].DoseOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,13 +189,19 @@ func DBMedComplianceLogSetOne(database db.DB, dir Direction, log *ComplianceLog)
|
|||
}
|
||||
|
||||
logs.UpdateDoseOffset(dir)
|
||||
|
||||
uuid := log.UUID
|
||||
sort.Sort(ComplianceLogList(logs))
|
||||
|
||||
if err := db.SetJSON(txn, []byte(dbMedComplianceLogPrefix+index), logs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*log = logs[len(logs)-1]
|
||||
for i, l := range logs {
|
||||
if l.UUID == uuid {
|
||||
*log = logs[i]
|
||||
}
|
||||
}
|
||||
|
||||
return txn.Commit()
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package funcmap
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func EscapeXML(s string) (string, error) {
|
||||
|
@ -12,3 +13,7 @@ func EscapeXML(s string) (string, error) {
|
|||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func EscapeQuery(s string) string {
|
||||
return url.QueryEscape(s)
|
||||
}
|
7
internal/servetpl/funcmap/files.go
Normal file
7
internal/servetpl/funcmap/files.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package funcmap
|
||||
|
||||
import "net/url"
|
||||
|
||||
func FileAccess(path string) string {
|
||||
return "https://yoake.yumechi.jp/file_access.html?path=" + url.QueryEscape(path)
|
||||
}
|
|
@ -2,7 +2,6 @@ package funcmap
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
|
@ -19,7 +18,6 @@ func Lookup(name string, args ...reflect.Value) (interface{}, error) {
|
|||
zero := reflect.ValueOf("")
|
||||
|
||||
target := args[0]
|
||||
log.Printf("lookup %s %v", name, target)
|
||||
if len(args) > 1 {
|
||||
zero = args[1]
|
||||
}
|
||||
|
@ -60,9 +58,6 @@ func Invoke(name string, target reflect.Value, args ...reflect.Value) (any, erro
|
|||
}
|
||||
target = reflect.ValueOf(t)
|
||||
}
|
||||
for i, arg := range args {
|
||||
log.Printf("invoke %s arg[%d]=%v", name, i, arg)
|
||||
}
|
||||
ret := target.Call(args)
|
||||
if len(ret) == 0 {
|
||||
return nil, nil
|
||||
|
@ -71,9 +66,6 @@ func Invoke(name string, target reflect.Value, args ...reflect.Value) (any, erro
|
|||
if err, ok := ret[len(ret)-1].Interface().(error); ok && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, r := range ret {
|
||||
log.Printf("invoke %s ret[%d]=%v", name, i, r)
|
||||
}
|
||||
|
||||
switch len(ret) {
|
||||
case 0:
|
||||
|
@ -96,25 +88,26 @@ func Void(args ...reflect.Value) string {
|
|||
|
||||
func GetFuncMap() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"lookup": Lookup,
|
||||
"invoke": Invoke,
|
||||
"void": Void,
|
||||
"get": FuncGet,
|
||||
"set": FuncSet,
|
||||
"math": Math,
|
||||
"xml": EscapeXML,
|
||||
"twilio_validate": TwilioValidate,
|
||||
"stop": Stop,
|
||||
"trima_img": TrimaImg,
|
||||
"parse_json": ParseJSON,
|
||||
"json": MarshalJSON,
|
||||
"get_auth": AuthGet,
|
||||
"lookup": Lookup,
|
||||
"invoke": Invoke,
|
||||
"void": Void,
|
||||
"get": FuncGet,
|
||||
"set": FuncSet,
|
||||
"math": Math,
|
||||
"xml": EscapeXML,
|
||||
"stop": Stop,
|
||||
"trima_img": TrimaImg,
|
||||
"parse_json": ParseJSON,
|
||||
"json": MarshalJSON,
|
||||
"get_auth": AuthGet,
|
||||
"sprintf": func(format string, input ...interface{}) interface{} {
|
||||
return fmt.Sprintf(format, input...)
|
||||
},
|
||||
"http": HttpRequest,
|
||||
"version": Version,
|
||||
"find_word": FindWord,
|
||||
"contain": Contain,
|
||||
"http": HttpRequest,
|
||||
"version": Version,
|
||||
"find_word": FindWord,
|
||||
"contain": Contain,
|
||||
"query": EscapeQuery,
|
||||
"theme_color": ThemeColor,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package funcmap
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
|
@ -44,7 +44,7 @@ func HttpRequest(method string, URL string, selector string, responseType string
|
|||
}
|
||||
return result.Html()
|
||||
}
|
||||
response, err := ioutil.ReadAll(resp.Body)
|
||||
response, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
254
internal/servetpl/funcmap/nipponcolor.go
Normal file
254
internal/servetpl/funcmap/nipponcolor.go
Normal file
|
@ -0,0 +1,254 @@
|
|||
package funcmap
|
||||
|
||||
var nipponColors = map[string]string{
|
||||
"nadeshiko": "#DC9F84", "撫子": "#DC9F84",
|
||||
"kohbai": "#E16B8C", "紅梅": "#E16B8C",
|
||||
"suoh": "#8E354A", "蘇芳": "#8E354A",
|
||||
"taikoh": "#F8C3CD", "退紅": "#F8C3CD",
|
||||
"ikkonzome": "#F4A7B9", "一斥染": "#F4A7B9",
|
||||
"kuwazome": "#64363c", "桑染": "#64363c",
|
||||
"momo": "#F596AA", "桃": "#F596AA",
|
||||
"ichigo": "#B5495B", "莓": "#B5495B",
|
||||
"usubeni": "#E87A90", "薄紅": "#E87A90",
|
||||
"imayoh": "#D05A6E", "今様": "#D05A6E",
|
||||
"nakabeni": "#DB4D6D", "中紅": "#DB4D6D",
|
||||
"sakura": "#FEDFE1", "桜": "#FEDFE1",
|
||||
"umenezumi": "#9E7A7A", "梅鼠": "#9E7A7A",
|
||||
"karakurenai": "#D0104C", "韓紅花": "#D0104C",
|
||||
"enji": "#9F353A", "燕脂": "#9F353A",
|
||||
"kurenai": "#CB1B45", "紅": "#CB1B45",
|
||||
"toki": "#EEA9A9", "鴇": "#EEA9A9",
|
||||
"cyohsyun": "#BF6766", "長春": "#BF6766",
|
||||
"kokiake": "#86473F", "深緋": "#86473F",
|
||||
"sakuranezumi": "#B19693", "桜鼠": "#B19693",
|
||||
"jinzamomi": "#EB7A77", "甚三紅": "#EB7A77",
|
||||
"azuki": "#954A45", "小豆": "#954A45",
|
||||
"suohkoh": "#A96360", "蘇芳香": "#A96360",
|
||||
"akabeni": "#CB4042", "赤紅": "#CB4042",
|
||||
"shinsyu": "#AB3B3A", "真朱": "#AB3B3A",
|
||||
"haizakura": "#D7C4BB", "灰桜": "#D7C4BB",
|
||||
"kuriume": "#904840", "栗梅": "#904840",
|
||||
"ebicha": "#734338", "海老茶": "#734338",
|
||||
"ginsyu": "#C73E3A", "銀朱": "#C73E3A",
|
||||
"kurotobi": "#554236", "黒鳶": "#554236",
|
||||
"benitobi": "#994639", "紅鳶": "#994639",
|
||||
"akebono": "#F19483", "曙": "#F19483",
|
||||
"benikaba": "#B54434", "紅樺": "#B54434",
|
||||
"mizugaki": "#B9887D", "水がき": "#B9887D",
|
||||
"sangosyu": "#F17C67", "珊瑚朱": "#F17C67",
|
||||
"benihiwada": "#884C3A", "紅檜皮": "#884C3A",
|
||||
"syojyohi": "#E83015", "猩猩緋": "#E83015",
|
||||
"entan": "#D75455", "鉛丹": "#D75455",
|
||||
"shikancha": "#B55D4C", "芝翫茶": "#B55D4C",
|
||||
"hiwada": "#854836", "檜皮": "#854836",
|
||||
"kakishibu": "#A35E47", "柿渋": "#A35E47",
|
||||
"ake": "#CC543A", "緋": "#CC543A",
|
||||
"tobi": "#724832", "鳶": "#724832",
|
||||
"benihi": "#F75C2F", "紅緋": "#F75C2F",
|
||||
"kurikawacha": "#6A4028", "栗皮茶": "#6A4028",
|
||||
"bengara": "#9A5034", "弁柄": "#9A5034",
|
||||
"terigaki": "#C46243", "照柿": "#C46243",
|
||||
"edocha": "#AF5F3C", "江戸茶": "#AF5F3C",
|
||||
"araisyu": "#FB966E", "洗朱": "#FB966E",
|
||||
"momoshiocha": "#724938", "百塩茶": "#724938",
|
||||
"karacha": "#B47157", "唐茶": "#B47157",
|
||||
"tokigaracha": "#DB8E71", "ときがら茶": "#DB8E71",
|
||||
"ohni": "#F05E1C", "黄丹": "#F05E1C",
|
||||
"sohi": "#ED784A", "纁": "#ED784A",
|
||||
"ensyucha": "#CA7853", "遠州茶": "#CA7853",
|
||||
"kabacha": "#B35C37", "樺茶": "#B35C37",
|
||||
"kogecha": "#563F2E", "焦茶": "#563F2E",
|
||||
"akakoh": "#E3916E", "赤香": "#E3916E",
|
||||
"suzumecha": "#8F5A3C", "雀茶": "#8F5A3C",
|
||||
"shishi": "#F0A986", "宍": "#F0A986",
|
||||
"sodenkaracha": "#A0674B", "宗伝唐茶": "#A0674B",
|
||||
"kaba": "#c1693c", "樺": "#c1693c",
|
||||
"kokikuchinashi": "#FB9966", "深支子": "#FB9966",
|
||||
"kurumi": "#947A6D", "胡桃": "#947A6D",
|
||||
"taisya": "#A36336", "代赭": "#A36336",
|
||||
"araigaki": "#E79460", "洗柿": "#E79460",
|
||||
"kohrozen": "#7D532C", "黄櫨染": "#7D532C",
|
||||
"akakuchiba": "#C78550", "赤朽葉": "#C78550",
|
||||
"tonocha": "#985F2A", "礪茶": "#985F2A",
|
||||
"akashiritsurubami": "#E1A679", "赤白橡": "#E1A679",
|
||||
"sencha": "#855B32", "煎茶": "#855B32",
|
||||
"kanzo": "#FC9F4D", "萱草": "#FC9F4D",
|
||||
"sharegaki": "#FFBA84", "洒落柿": "#FFBA84",
|
||||
"beniukon": "#E98B2A", "紅鬱金": "#E98B2A",
|
||||
"umezome": "#E9A368", "梅染": "#E9A368",
|
||||
"biwacha": "#B17844", "枇杷茶": "#B17844",
|
||||
"chojicha": "#96632E", "丁子茶": "#96632E",
|
||||
"kenpohzome": "#43341B", "憲法染": "#43341B",
|
||||
"kohaku": "#CA7A2C", "琥珀": "#CA7A2C",
|
||||
"usugaki": "#ECB88A", "薄柿": "#ECB88A",
|
||||
"kyara": "#78552B", "伽羅": "#78552B",
|
||||
"chojizome": "#B07736", "丁子染": "#B07736",
|
||||
"fushizome": "#967249", "柴染": "#967249",
|
||||
"kuchiba": "#E2943B", "朽葉": "#E2943B",
|
||||
"kincha": "#C7802D", "金茶": "#C7802D",
|
||||
"kitsune": "#9B6E23", "狐": "#9B6E23",
|
||||
"susutake": "#6E552F", "煤竹": "#6E552F",
|
||||
"usukoh": "#EBB471", "薄香": "#EBB471",
|
||||
"tonoko": "#D7B98E", "砥粉": "#D7B98E",
|
||||
"ginsusutake": "#82663A", "銀煤竹": "#82663A",
|
||||
"ohdo": "#B68E55", "黄土": "#B68E55",
|
||||
"shiracha": "#BC9F77", "白茶": "#BC9F77",
|
||||
"kobicha": "#876633", "媚茶": "#876633",
|
||||
"kigaracha": "#C18A26", "黄唐茶": "#C18A26",
|
||||
"yamabuki": "#FFB11B", "山吹": "#FFB11B",
|
||||
"yamabukicha": "#D19826", "山吹茶": "#D19826",
|
||||
"hajizome": "#DDA52D", "櫨染": "#DDA52D",
|
||||
"kuwacha": "#C99833", "桑茶": "#C99833",
|
||||
"tamago": "#F9BF45", "玉子": "#F9BF45",
|
||||
"shirotsurubami": "#DCB879", "白橡": "#DCB879",
|
||||
"kitsurubami": "#BA9132", "黄橡": "#BA9132",
|
||||
"tamamorokoshi": "#E8B647", "玉蜀黍": "#E8B647",
|
||||
"hanaba": "#F7C242", "花葉": "#F7C242",
|
||||
"namakabe": "#7D6C46", "生壁": "#7D6C46",
|
||||
"torinoko": "#DAC9A6", "鳥の子": "#DAC9A6",
|
||||
"usuki": "#FAD689", "浅黄": "#FAD689",
|
||||
"kikuchiba": "#D9AB42", "黄朽葉": "#D9AB42",
|
||||
"kuchinashi": "#F6C555", "梔子": "#F6C555",
|
||||
"tohoh": "#FFC408", "籐黄": "#FFC408",
|
||||
"ukon": "#EFBB24", "鬱金": "#EFBB24",
|
||||
"karashi": "#CAAD5F", "芥子": "#CAAD5F",
|
||||
"higosusutake": "#8D742A", "肥後煤竹": "#8D742A",
|
||||
"rikyushiracha": "#B4A582", "利休白茶": "#B4A582",
|
||||
"aku": "#877F6C", "灰汁": "#877F6C",
|
||||
"rikyucha": "#897D55", "利休茶": "#897D55",
|
||||
"rokohcha": "#74673E", "路考茶": "#74673E",
|
||||
"nataneyu": "#A28C37", "菜種油": "#A28C37",
|
||||
"uguisucha": "#6C6024", "鶯茶": "#6C6024",
|
||||
"kimirucha": "#867835", "黄海松茶": "#867835",
|
||||
"mirucha": "#62592C", "海松茶": "#62592C",
|
||||
"kariyasu": "#E9CD4C", "刈安": "#E9CD4C",
|
||||
"nanohana": "#F7D94C", "菜の花": "#F7D94C",
|
||||
"kihada": "#FBE251", "黄蘗": "#FBE251",
|
||||
"mushikuri": "#D9CD90", "蒸栗": "#D9CD90",
|
||||
"aokuchiba": "#ADA142", "青朽葉": "#ADA142",
|
||||
"ominaeshi": "#DDD23B", "女郎花": "#DDD23B",
|
||||
"hiwacha": "#A5A051", "鶸茶": "#A5A051",
|
||||
"hiwa": "#BEC23F", "鶸": "#BEC23F",
|
||||
"uguisu": "#6C6A2D", "鶯": "#6C6A2D",
|
||||
"yanagicha": "#939650", "柳茶": "#939650",
|
||||
"koke": "#838A2D", "苔": "#838A2D",
|
||||
"kikujin": "#B1B479", "麹塵": "#B1B479",
|
||||
"rikancha": "#616138", "璃寛茶": "#616138",
|
||||
"aikobicha": "#4B4E2A", "藍媚茶": "#4B4E2A",
|
||||
"miru": "#5B622E", "海松": "#5B622E",
|
||||
"sensaicha": "#4D5139", "千歳茶": "#4D5139",
|
||||
"baikocha": "#89916B", "梅幸茶": "#89916B",
|
||||
"hiwamoegi": "#90B44B", "鶸萌黄": "#90B44B",
|
||||
"yanagizome": "#91AD70", "柳染": "#91AD70",
|
||||
"urayanagi": "#B5CAA0", "裏柳": "#B5CAA0",
|
||||
"iwaicha": "#646A58", "岩井茶": "#646A58",
|
||||
"moegi": "#7BA23F", "萌黄": "#7BA23F",
|
||||
"nae": "#86C166", "苗": "#86C166",
|
||||
"yanagisusutake": "#4A593D", "柳煤竹": "#4A593D",
|
||||
"matsuba": "#42602D", "松葉": "#42602D",
|
||||
"aoni": "#516E41", "青丹": "#516E41",
|
||||
"usuao": "#91B493", "薄青": "#91B493",
|
||||
"yanaginezumi": "#808F7C", "柳鼠": "#808F7C",
|
||||
"tokiwa": "#1B813E", "常磐": "#1B813E",
|
||||
"wakatake": "#5DAC81", "若竹": "#5DAC81",
|
||||
"chitosemidori": "#36563C", "千歳緑": "#36563C",
|
||||
"midori": "#227D51", "緑": "#227D51",
|
||||
"byakuroku": "#A8D8B9", "白緑": "#A8D8B9",
|
||||
"oitake": "#6A8372", "老竹": "#6A8372",
|
||||
"tokusa": "#2D6D4B", "木賊": "#2D6D4B",
|
||||
"onandocha": "#465D4C", "御納戸茶": "#465D4C",
|
||||
"rokusyoh": "#24936E", "緑青": "#24936E",
|
||||
"sabiseiji": "#86A697", "錆青磁": "#86A697",
|
||||
"aotake": "#00896C", "青竹": "#00896C",
|
||||
"veludo": "#096148", "ビロード": "#096148",
|
||||
"mushiao": "#20604F", "虫襖": "#20604F",
|
||||
"aimirucha": "#0F4C3A", "藍海松茶": "#0F4C3A",
|
||||
"tonocha2": "#4F726C", "沈香茶": "#4F726C",
|
||||
"aomidori": "#00AA90", "青緑": "#00AA90",
|
||||
"seiji": "#69B0AC", "青磁": "#69B0AC",
|
||||
"tetsu": "#26453D", "鉄": "#26453D",
|
||||
"mizuasagi": "#66BAB7", "水浅葱": "#66BAB7",
|
||||
"seiheki": "#268785", "青碧": "#268785",
|
||||
"sabitetsuonando": "#405B55", "錆鉄御納戸": "#405B55",
|
||||
"korainando": "#305A56", "高麗納戸": "#305A56",
|
||||
"byakugun": "#78C2C4", "白群": "#78C2C4",
|
||||
"omeshicha": "#376B6D", "御召茶": "#376B6D",
|
||||
"kamenozoki": "#A5DEE4", "瓶覗": "#A5DEE4",
|
||||
"fukagawanezumi": "#77969A", "深川鼠": "#77969A",
|
||||
"sabiasagi": "#6699A1", "錆浅葱": "#6699A1",
|
||||
"mizu": "#81C7D4", "水": "#81C7D4",
|
||||
"asagi": "#33A6B8", "浅葱": "#33A6B8",
|
||||
"onando": "#0C4842", "御納戸": "#0C4842",
|
||||
"ai": "#0D5661", "藍": "#0D5661",
|
||||
"shinbashi": "#0089A7", "新橋": "#0089A7",
|
||||
"sabionando": "#336774", "錆御納戸": "#336774",
|
||||
"tetsuonando": "#255359", "鉄御納戸": "#255359",
|
||||
"hanaasagi": "#1E88A8", "花浅葱": "#1E88A8",
|
||||
"ainezumi": "#566C73", "藍鼠": "#566C73",
|
||||
"masuhana": "#577C8A", "舛花": "#577C8A",
|
||||
"sora": "#58B2DC", "空": "#58B2DC",
|
||||
"noshimehana": "#2B5F75", "熨斗目花": "#2B5F75",
|
||||
"chigusa": "#3A8FB7", "千草": "#3A8FB7",
|
||||
"omeshionando": "#2E5C6E", "御召御納戸": "#2E5C6E",
|
||||
"hanada": "#006284", "縹": "#006284",
|
||||
"wasurenagusa": "#7DB9DE", "勿忘草": "#7DB9DE",
|
||||
"gunjyo": "#51A8DD", "群青": "#51A8DD",
|
||||
"tsuyukusa": "#2EA9DF", "露草": "#2EA9DF",
|
||||
"kurotsurubami": "#0B1013", "黒橡": "#0B1013",
|
||||
"kon": "#0F2540", "紺": "#0F2540",
|
||||
"kachi": "#08192D", "褐": "#08192D",
|
||||
"ruri": "#005CAF", "瑠璃": "#005CAF",
|
||||
"rurikon": "#0B346E", "瑠璃紺": "#0B346E",
|
||||
"benimidori": "#7B90D2", "紅碧": "#7B90D2",
|
||||
"fujinezumi": "#6E75A4", "藤鼠": "#6E75A4",
|
||||
"tetsukon": "#261E47", "鉄紺": "#261E47",
|
||||
"konjyo": "#113285", "紺青": "#113285",
|
||||
"benikakehana": "#4E4F97", "紅掛花": "#4E4F97",
|
||||
"konkikyo": "#211E55", "紺桔梗": "#211E55",
|
||||
"fuji": "#8B81C3", "藤": "#8B81C3",
|
||||
"futaai": "#70649A", "二藍": "#70649A",
|
||||
"ouchi": "#9B90C2", "楝": "#9B90C2",
|
||||
"fujimurasaki": "#8A6BBE", "藤紫": "#8A6BBE",
|
||||
"kikyo": "#6A4C9C", "桔梗": "#6A4C9C",
|
||||
"shion": "#8F77B5", "紫苑": "#8F77B5",
|
||||
"messhi": "#533D5B", "滅紫": "#533D5B",
|
||||
"usu": "#B28FCE", "薄": "#B28FCE",
|
||||
"hashita": "#986DB2", "半": "#986DB2",
|
||||
"edomurasaki": "#77428D", "江戸紫": "#77428D",
|
||||
"shikon": "#3C2F41", "紫紺": "#3C2F41",
|
||||
"kokimurasaki": "#4A225D", "深紫": "#4A225D",
|
||||
"sumire": "#66327C", "菫": "#66327C",
|
||||
"murasaki": "#592C63", "紫": "#592C63",
|
||||
"ayame": "#6F3381", "菖蒲": "#6F3381",
|
||||
"fujisusutake": "#574C57", "藤煤竹": "#574C57",
|
||||
"benifuji": "#B481BB", "紅藤": "#B481BB",
|
||||
"kurobeni": "#3F2B36", "黒紅": "#3F2B36",
|
||||
"nasukon": "#572A3F", "茄子紺": "#572A3F",
|
||||
"budohnezumi": "#5E3D50", "葡萄鼠": "#5E3D50",
|
||||
"hatobanezumi": "#72636E", "鳩羽鼠": "#72636E",
|
||||
"kakitsubata": "#622954", "杜若": "#622954",
|
||||
"ebizome": "#6D2E5B", "蒲葡": "#6D2E5B",
|
||||
"botan": "#C1328E", "牡丹": "#C1328E",
|
||||
"umemurasaki": "#A8497A", "梅紫": "#A8497A",
|
||||
"nisemurasaki": "#562E37", "似紫": "#562E37",
|
||||
"tsutsuji": "#E03C8A", "躑躅": "#E03C8A",
|
||||
"murasakitobi": "#60373E", "紫鳶": "#60373E",
|
||||
"shironeri": "#FCFAF2", "白練": "#FCFAF2",
|
||||
"gofun": "#FFFFFB", "胡粉": "#FFFFFB",
|
||||
"shironezumi": "#BDC0BA", "白鼠": "#BDC0BA",
|
||||
"ginnezumi": "#91989F", "銀鼠": "#91989F",
|
||||
"namari": "#787878", "鉛": "#787878",
|
||||
"hai": "#828282", "灰": "#828282",
|
||||
"sunezumi": "#787D7B", "素鼠": "#787D7B",
|
||||
"rikyunezumi": "#707C74", "利休鼠": "#707C74",
|
||||
"nibi": "#656765", "鈍": "#656765",
|
||||
"aonibi": "#535953", "青鈍": "#535953",
|
||||
"dobunezumi": "#4F4F48", "溝鼠": "#4F4F48",
|
||||
"benikeshinezumi": "#52433D", "紅消鼠": "#52433D",
|
||||
"aisumicha": "#373C38", "藍墨茶": "#373C38",
|
||||
"binrojizome": "#3A3226", "檳榔子染": "#3A3226",
|
||||
"keshizumi": "#434343", "消炭": "#434343",
|
||||
"sumi": "#1C1C1C", "墨": "#1C1C1C",
|
||||
"kuro": "#080808", "黒": "#080808",
|
||||
"ro": "#0C0C0C", "呂": "#0C0C0C",
|
||||
}
|
10
internal/servetpl/funcmap/theme.go
Normal file
10
internal/servetpl/funcmap/theme.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package funcmap
|
||||
|
||||
import "fmt"
|
||||
|
||||
func ThemeColor(key string) (string, error) {
|
||||
if c, ok := nipponColors[key]; ok {
|
||||
return c, nil
|
||||
}
|
||||
return "", fmt.Errorf("no such color %s", key)
|
||||
}
|
|
@ -3,7 +3,7 @@ package funcmap
|
|||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
@ -23,7 +23,7 @@ func TrimaImg(path string, retType string) (string, error) {
|
|||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("bad status code: %d", resp.StatusCode)
|
||||
}
|
||||
return ioutil.ReadAll(resp.Body)
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
switch retType {
|
||||
case "url":
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
package funcmap
|
||||
|
||||
import (
|
||||
"github.com/eternal-flame-AD/yoake/internal/twilio"
|
||||
)
|
||||
|
||||
var TwilioValidate = twilio.TwilioValidate
|
|
@ -122,8 +122,6 @@ func ServeTemplateDir(dir string) echo.MiddlewareFunc {
|
|||
ext := path.Ext(file)
|
||||
tplName := file[:len(file)-len(ext)] + ".tpl" + ext
|
||||
if path.Ext(file[:len(file)-len(ext)]) == ".tpl" {
|
||||
/* reject requests for the template source file */
|
||||
log.Printf("rejecting request for template source file: %s", file)
|
||||
if _, err := os.Stat(filepath.Join(dir, file)); err == nil {
|
||||
return func(wr io.Writer, data any) error {
|
||||
return errTplExtNotStripped
|
||||
|
@ -133,8 +131,6 @@ func ServeTemplateDir(dir string) echo.MiddlewareFunc {
|
|||
|
||||
tplPath := filepath.Join(dir, tplName)
|
||||
if _, err := os.Stat(tplPath); err == nil {
|
||||
//log.Printf("dispatch template: %s(%s) ext=%s", tplName, tplPath, ext)
|
||||
// template file is still there, execute
|
||||
if ext == ".html" {
|
||||
return func(wr io.Writer, data any) error { return templates.ExecuteTemplate(wr, tplName, data) }
|
||||
} else {
|
||||
|
|
|
@ -15,10 +15,13 @@ import (
|
|||
"github.com/eternal-flame-AD/yoake/internal/comm/model"
|
||||
"github.com/eternal-flame-AD/yoake/internal/comm/telegram"
|
||||
"github.com/eternal-flame-AD/yoake/internal/filestore"
|
||||
"github.com/eternal-flame-AD/yoake/internal/servetpl/funcmap"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/twilio/twilio-go"
|
||||
openapi "github.com/twilio/twilio-go/rest/api/v2010"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
const rfc2822 = "Mon, 02 Jan 2006 15:04:05 -0700"
|
||||
|
@ -64,27 +67,27 @@ type CallStatusCallbackProgressForm struct {
|
|||
SequenceNumber int `form:"SequenceNumber" json:"SequenceNumber"`
|
||||
}
|
||||
|
||||
func findCallDir(callsDir filestore.FS, callSid string, from string, to string) (filestore.FS, error) {
|
||||
func findCallDir(callsDir filestore.FS, callSid string, from string, to string) (filestore.FS, string, error) {
|
||||
if callSid == "" {
|
||||
return nil, fmt.Errorf("callSid is empty")
|
||||
return nil, "", fmt.Errorf("callSid is empty")
|
||||
}
|
||||
dirs, err := afero.ReadDir(callsDir, ".")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
if dir.IsDir() {
|
||||
if strings.HasSuffix(dir.Name(), callSid) {
|
||||
return filestore.ChrootFS(callsDir, dir.Name()), nil
|
||||
return filestore.ChrootFS(callsDir, dir.Name()), dir.Name(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
now := time.Now()
|
||||
newName := fmt.Sprintf("%s_%s_%s_%s", now.Format("2006-01-02T15:04:05"), from, to, callSid)
|
||||
newName := fmt.Sprintf("%s_%s_%s_%s", now.Format("2006-01-02T15.04.05"), from, to, callSid)
|
||||
if err := callsDir.Mkdir(newName, 0770); err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
return filestore.ChrootFS(callsDir, newName), nil
|
||||
return filestore.ChrootFS(callsDir, newName), newName, nil
|
||||
}
|
||||
|
||||
func fetchRecording(apiClient *twilio.RestClient, callDir filestore.FS, sid string, recType string) error {
|
||||
|
@ -215,7 +218,7 @@ func Register(g *echo.Group, fs filestore.FS, comm model.Communicator) {
|
|||
if sid == "" {
|
||||
return c.String(http.StatusBadRequest, "missing call sid")
|
||||
}
|
||||
callDir, err := findCallDir(calls, form.CallSid, "", "")
|
||||
callDir, _, err := findCallDir(calls, form.CallSid, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -227,12 +230,19 @@ func Register(g *echo.Group, fs filestore.FS, comm model.Communicator) {
|
|||
if err := c.Bind(form); err != nil {
|
||||
return err
|
||||
}
|
||||
callDir, err := findCallDir(calls, form.CallSid, form.From, form.To)
|
||||
callDir, callDirName, err := findCallDir(calls, form.CallSid, form.From, form.To)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
callDirAbs := fmt.Sprintf("/calls/%s", callDirName)
|
||||
log.Printf("call %s: %s -> %s dirAbs=%s", form.CallSid, form.From, form.To, callDirAbs)
|
||||
if hasTelegram {
|
||||
tg.SendHTML(tg.OwnerChatID, fmt.Sprintf("Call From %s (%s):\n\nTo: %s\nSid: %s", form.From, form.CallStatus, form.To, form.CallSid))
|
||||
msg := tgbotapi.NewMessage(tg.OwnerChatID, fmt.Sprintf("Call From %s (%s):\n\nTo: %s\nSid: %s\nCallDir: <a href=\"%s\">%s</a>",
|
||||
form.From, form.CallStatus, form.To, form.CallSid, funcmap.FileAccess(callDirAbs+"/"), callDirAbs))
|
||||
msg.ParseMode = tgbotapi.ModeHTML
|
||||
if _, err := tg.Client().Send(msg); err != nil {
|
||||
log.Printf("failed to send telegram message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("status.%d.json", time.Now().UnixNano())
|
||||
|
|
|
@ -37,7 +37,6 @@ func VerifyMiddleware(prefix string, baseurlS string) echo.MiddlewareFunc {
|
|||
if err != nil {
|
||||
log.Fatalf("invalid twilio baseurl: %v", baseurlS)
|
||||
}
|
||||
log.Printf("twilio baseurl is %v", baseURL)
|
||||
var basicAuth echo.MiddlewareFunc
|
||||
if userpass := baseURL.User.String(); userpass != "" {
|
||||
basicAuth = middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
|
||||
|
@ -59,14 +58,12 @@ func VerifyMiddleware(prefix string, baseurlS string) echo.MiddlewareFunc {
|
|||
}
|
||||
|
||||
cleanPath := path.Clean(c.Request().URL.Path)
|
||||
//log.Printf("cleanPath: %s", cleanPath)
|
||||
if cleanPath == prefix || strings.HasPrefix(cleanPath, prefix+"/") {
|
||||
fullReq := c.Request().Clone(c.Request().Context())
|
||||
log.Printf("original request URL: %v, scheme=%s, host=%s, user=%s", c.Request().URL, c.Request().URL.Scheme, c.Request().URL.Host, c.Request().URL.User)
|
||||
fullReq.URL = baseURL.ResolveReference(c.Request().URL)
|
||||
fullReq.URL.User = nil
|
||||
if err := TwilioValidate(c, fullReq); err != nil {
|
||||
log.Printf("twilio verify failed: %v", err)
|
||||
log.Printf("twilio verify failed: %v, url=%s", err, fullReq.URL.String())
|
||||
if !bypassOk {
|
||||
c.String(http.StatusOK, "We are sorry. Request Validation Failed. This is not your fault.")
|
||||
return nil
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/eternal-flame-AD/yoake/config"
|
||||
"github.com/eternal-flame-AD/yoake/internal/auth"
|
||||
|
@ -70,12 +71,30 @@ func Init(hostname string, comm *comm.Communicator, database db.DB, fs filestore
|
|||
canvaslms.Register(api.Group("/canvas", logMiddleware("api_canvas", nil)), comm)
|
||||
utilapi.Register(api.Group("/util", logMiddleware("api_util", nil)))
|
||||
comm.RegisterAPIRoute(api.Group("/comm", logMiddleware("api_comm", nil)))
|
||||
auth.Register(api.Group("/auth", logMiddleware("api_auth", nil)))
|
||||
auth.Register(api.Group("/auth", logMiddleware("api_auth", nil)), database)
|
||||
entertainment.Register(api.Group("/entertainment", logMiddleware("api_entertainment", nil)), database)
|
||||
health.Register(api.Group("/health", logMiddleware("api_health", nil)), database, comm)
|
||||
twilio.Register(api.Group("/twilio", logMiddleware("api_twilio", nil)), fs, comm)
|
||||
}
|
||||
|
||||
if fsConf := config.Config().FS; fsConf.Serve {
|
||||
e.Group("/files").Use(auth.RequireMiddleware(auth.RoleAdmin), middleware.RewriteWithConfig(middleware.RewriteConfig{RegexRules: map[*regexp.Regexp]string{regexp.MustCompile("^/files/(.*)$"): "/$1"}}),
|
||||
middleware.StaticWithConfig(middleware.StaticConfig{
|
||||
Skipper: func(c echo.Context) bool {
|
||||
if c.Request().Method != echo.GET {
|
||||
return true
|
||||
}
|
||||
if fetchMode := c.Request().Header.Get("Sec-Fetch-Mode"); fetchMode != "" && fetchMode != "navigate" {
|
||||
// some protection against XSS
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
Root: fsConf.BasePath,
|
||||
Browse: true,
|
||||
}), logMiddleware("files", nil))
|
||||
}
|
||||
|
||||
e.Use(
|
||||
echoerror.Middleware(echoerror.HTMLWriter),
|
||||
func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
|
@ -89,7 +108,7 @@ func Init(hostname string, comm *comm.Communicator, database db.DB, fs filestore
|
|||
middleware.Gzip(),
|
||||
auth.Middleware(sessionCookie),
|
||||
logMiddleware("twilio", twilio.VerifyMiddleware("/twilio", config.Config().Twilio.BaseURL)),
|
||||
middleware.Rewrite(map[string]string{"*/": "$1/index.html"}),
|
||||
middleware.RewriteWithConfig(middleware.RewriteConfig{RegexRules: map[*regexp.Regexp]string{regexp.MustCompile("^/$"): "/index.html"}}),
|
||||
logMiddleware("template", servetpl.ServeTemplateDir(webroot.Root)),
|
||||
logMiddleware("static", middleware.Static(webroot.Root)))
|
||||
|
||||
|
|
Binary file not shown.
21
webroot/file_access.tpl.html
Normal file
21
webroot/file_access.tpl.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
{{- $path := "" }}
|
||||
{{- if $formPath := (invoke "FormValue" .C "path") }}
|
||||
{{- $path = $formPath }}
|
||||
{{- else if $pathParam := (invoke "Param" .C "path") }}
|
||||
{{- $path = $pathParam }}
|
||||
{{- end }}
|
||||
{{- if or (not $path)
|
||||
(ne (slice $path 0 1) "/") }}
|
||||
{{- $path = (sprintf "/%s" $path) }}
|
||||
{{- end}}
|
||||
|
||||
|
||||
<head>
|
||||
{{ if (call .Config).FS.Serve }}
|
||||
<meta http-equiv="refresh" content="0; url=/files{{$path}}">
|
||||
{{ else }}
|
||||
<meta http-equiv="refresh"
|
||||
content="0; url=https://meakan.yumechi.jp/index.cgi?launchApp=SYNO.SDS.App.FileStation3.Instance&launchParam={{ $path | sprintf "/yoake/yoake.yumechi.jp%s" | query | sprintf "openfile=%s" | query }}">
|
||||
{{ end }}
|
||||
</head>
|
|
@ -4,6 +4,7 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="loginModalLabel">Login</h5>
|
||||
{{ template "/partials/tg_login.tpl.html" . }}
|
||||
</div>
|
||||
<form action="/api/auth/login" onsubmit="submitLoginForm(this,event)">
|
||||
<div class="modal-body">
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
{{ $version := version }}
|
||||
<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
|
||||
{{ $auth := (get_auth .C) }}
|
||||
<nav class="navbar sticky-top flex-md-nowrap p-0">
|
||||
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3 fs-6" href="#">
|
||||
|
||||
{{- (call .Config).WebRoot.SiteName -}}
|
||||
<small class="fw-lighter text-muted px-2">{{ $version.Version }} - {{ $version.Date }}</small>
|
||||
</a>
|
||||
|
@ -13,4 +15,11 @@
|
|||
<input class="form-control form-control-dark w-100 rounded-0 border-0" type="text" placeholder="Search"
|
||||
aria-label="Search">
|
||||
-->
|
||||
</header>
|
||||
<div class="navbar-nav">
|
||||
<div class="nav-item text-nowrap px-3">
|
||||
{{ if $auth.Valid }}
|
||||
{{ if $auth.Ident.DisplayName }} {{ $auth.Ident.DisplayName }} {{ else }} {{ $auth.Ident.Username }} {{ end
|
||||
}}
|
||||
{{ end }}
|
||||
</div>
|
||||
</nav>
|
|
@ -109,10 +109,15 @@
|
|||
<div class="card-body text-center">
|
||||
|
||||
{{ if $auth.Valid }}
|
||||
{{ if contain $auth.Roles "admin" }}
|
||||
{{ if $auth.Ident.PhotoURL }}
|
||||
<img src="{{ $auth.Ident.PhotoURL }}" class="rounded-circle" width="100" height="100"
|
||||
alt="Photo">
|
||||
{{ else }}
|
||||
<img class="d-block mx-auto trima-btn-card" src={{ trima_img "ja/btn_vista_operator.gif" "url"
|
||||
}}>
|
||||
<p>
|
||||
{{ end }}
|
||||
{{ if contain $auth.Roles "admin" }}
|
||||
You have super powers!
|
||||
</p>
|
||||
{{ end }}
|
||||
|
|
|
@ -4,6 +4,27 @@ async function getAuth() {
|
|||
return bodyJSON
|
||||
}
|
||||
|
||||
function onLoginError(data) {
|
||||
try {
|
||||
let msg = data.responseJSON.message || data.responseJSON;
|
||||
$('#login-form-error').removeClass('d-none').find('span').text(msg);
|
||||
} catch (e) {
|
||||
$('#login-form-error').removeClass('d-none').find('span').text(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function onTelegramAuth(user) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/api/auth/login_tg',
|
||||
data: JSON.stringify(user),
|
||||
contentType: 'application/json',
|
||||
success: function (data) {
|
||||
window.location.reload();
|
||||
},
|
||||
error: onLoginError,
|
||||
})
|
||||
}
|
||||
|
||||
function submitLoginForm(target, e) {
|
||||
e.preventDefault()
|
||||
|
@ -18,14 +39,7 @@ function submitLoginForm(target, e) {
|
|||
success: function (data) {
|
||||
window.location.reload();
|
||||
},
|
||||
error: function (data) {
|
||||
try {
|
||||
let msg = data.responseJSON.message || data.responseJSON;
|
||||
$('#login-form-error').removeClass('d-none').find('span').text(msg);
|
||||
} catch (e) {
|
||||
$('#login-form-error').removeClass('d-none').find('span').text(e.message);
|
||||
}
|
||||
}
|
||||
error: onLoginError,
|
||||
});
|
||||
|
||||
}
|
||||
|
|
2
webroot/partials/tg_login.tpl.html
Normal file
2
webroot/partials/tg_login.tpl.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
<script async src="https://telegram.org/js/telegram-widget.js?21" data-telegram-login="yoake_yumechi_bot"
|
||||
data-size="large" data-radius="0" data-onauth="onTelegramAuth(user)" data-request-access="write"></script>
|
|
@ -31,6 +31,37 @@
|
|||
fill: currentColor;
|
||||
}
|
||||
|
||||
.card {
|
||||
--bs-card-cap-bg: {{ theme_color "sakura" }};
|
||||
}
|
||||
|
||||
.modal {
|
||||
/* --bs-modal-bg: {{ theme_color "sakura" }}; */
|
||||
}
|
||||
|
||||
.card .card-body {
|
||||
background-color: {{ theme_color "sakura" }}55;
|
||||
}
|
||||
|
||||
/* navigation elements */
|
||||
nav.navbar {
|
||||
background-color: {{ theme_color "sakura" }};
|
||||
}
|
||||
|
||||
.bg-light {
|
||||
--bs-light-rgb: {{ theme_color "sakura" }};
|
||||
}
|
||||
|
||||
nav.navbar * {
|
||||
background-color: {{ theme_color "sakura" }};
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: {{ theme_color "sakura" }}33;
|
||||
}
|
||||
|
||||
|
||||
.nav-scroller {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
@ -49,6 +80,8 @@
|
|||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* trima logos */
|
||||
|
||||
.trima-procedure-hidden img.trima-procedure {
|
||||
display: none;
|
||||
}
|
Loading…
Reference in a new issue