Compare commits
No commits in common. "440d36b29178a9aba87865e9c9b9cd40656289b9" and "18cb9715e463249dac793d616379b27efc4b8ef7" have entirely different histories.
440d36b291
...
18cb9715e4
117
bot.go
117
bot.go
|
@ -2,25 +2,17 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
tele "gopkg.in/telebot.v3"
|
tele "gopkg.in/telebot.v3"
|
||||||
|
|
||||||
"git.gensokyo.cafe/kkyy/tgbot_misaka_5882f7/cmds"
|
|
||||||
"git.gensokyo.cafe/kkyy/tgbot_misaka_5882f7/stats"
|
"git.gensokyo.cafe/kkyy/tgbot_misaka_5882f7/stats"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
stickerPanic = "CAACAgUAAxkBAAMjY3zoraxZGB8Xejyw86bHLSWLjVcAArMIAAL7-nhXNK7dStmRUGsrBA"
|
|
||||||
stickerLoading = "CAACAgUAAxkBAAMmY3zp5UCMVRvy1isFCPHrx-UBWX8AApYHAALP8GhXEm9ZIBjn1v8rBA"
|
|
||||||
)
|
|
||||||
|
|
||||||
func isFromAdmin(sender *tele.User) bool {
|
func isFromAdmin(sender *tele.User) bool {
|
||||||
if sender == nil {
|
if sender == nil {
|
||||||
return false
|
return false
|
||||||
|
@ -41,49 +33,17 @@ func initBot() (*tele.Bot, error) {
|
||||||
return nil, errors.Wrap(err, 0)
|
return nil, errors.Wrap(err, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Use(logMiddleware)
|
|
||||||
|
|
||||||
// command routing
|
// command routing
|
||||||
b.Handle("/start", handleStartCmd)
|
b.Handle("/start", handleStartCmd)
|
||||||
|
b.Handle("/traffic", handleTrafficCmd)
|
||||||
|
|
||||||
b.Handle("/me", handleUserInfoCmd)
|
b.Handle("/me", handleUserInfoCmd)
|
||||||
b.Handle("/chat", handleChatInfoCmd)
|
b.Handle("/chat", handleChatInfoCmd)
|
||||||
b.Handle("/year_progress", handleYearProgressCmd)
|
b.Handle("/year_progress", handleYearProgressCmd)
|
||||||
|
|
||||||
b.Handle(tele.OnText, handleGeneralMessage)
|
|
||||||
b.Handle(tele.OnSticker, handleGeneralMessage)
|
|
||||||
|
|
||||||
// admin required
|
|
||||||
adminGrp := b.Group()
|
|
||||||
adminGrp.Use(adminMiddleware)
|
|
||||||
adminGrp.Handle("/traffic", handleTrafficCmd)
|
|
||||||
adminGrp.Handle("/dig", handleDigCmd)
|
|
||||||
|
|
||||||
// adminGrp.Handle("/test", handleTestCmd)
|
|
||||||
|
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminMiddleware(next tele.HandlerFunc) tele.HandlerFunc {
|
|
||||||
return func(c tele.Context) error {
|
|
||||||
if !isFromAdmin(c.Sender()) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return next(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func logMiddleware(next tele.HandlerFunc) tele.HandlerFunc {
|
|
||||||
return func(c tele.Context) error {
|
|
||||||
upd := c.Update()
|
|
||||||
defer func() {
|
|
||||||
logger.Infow("Log middleware", "update", upd)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return next(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleStartCmd(c tele.Context) error {
|
func handleStartCmd(c tele.Context) error {
|
||||||
if !isFromAdmin(c.Sender()) {
|
if !isFromAdmin(c.Sender()) {
|
||||||
return c.Send("Hello, stranger :)")
|
return c.Send("Hello, stranger :)")
|
||||||
|
@ -93,14 +53,18 @@ func handleStartCmd(c tele.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleTrafficCmd(c tele.Context) error {
|
func handleTrafficCmd(c tele.Context) error {
|
||||||
|
if !isFromAdmin(c.Sender()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
dailyTraffic, err := stats.VnstatDailyTraffic(config.WatchedInterface)
|
dailyTraffic, err := stats.VnstatDailyTraffic(config.WatchedInterface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Reply(stickerFromID(stickerPanic))
|
_ = c.Reply("im die, thank you forever")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
monthlyTraffic, err := stats.VnstatMonthlyTraffic(config.WatchedInterface)
|
monthlyTraffic, err := stats.VnstatMonthlyTraffic(config.WatchedInterface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Reply(stickerFromID(stickerPanic))
|
_ = c.Reply("im die, thank you forever")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,19 +163,18 @@ func handleChatInfoCmd(c tele.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawBar(progress float64, length int) string {
|
func drawBar(progress float64, length int) string {
|
||||||
barChars := []rune("·-=")
|
barChars := []rune("░▒▓█")
|
||||||
|
|
||||||
if length <= 0 {
|
if length <= 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
step := 1 / float64(length)
|
step := 1 / float64(length)
|
||||||
buf := make([]rune, length+2)
|
buf := make([]rune, length)
|
||||||
buf[0], buf[length+1] = '[', ']'
|
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < length; i++ {
|
||||||
fill := (progress - float64(i)*step) / step
|
fill := (progress - float64(i)*step) / step
|
||||||
fill = math.Min(math.Max(fill, 0), 1)
|
fill = math.Min(math.Max(fill, 0), 1)
|
||||||
idx := int(math.Round(fill * float64(len(barChars)-1)))
|
idx := int(math.Round(fill * float64(len(barChars)-1)))
|
||||||
buf[i+1] = barChars[idx]
|
buf[i] = barChars[idx]
|
||||||
}
|
}
|
||||||
return string(buf)
|
return string(buf)
|
||||||
}
|
}
|
||||||
|
@ -220,7 +183,7 @@ func drawBarForTrafficRecord(r stats.VnstatTrafficRecord) string {
|
||||||
effective := lo.Max([]uint64{r.Rx, r.Tx})
|
effective := lo.Max([]uint64{r.Rx, r.Tx})
|
||||||
max := config.MonthlyTrafficLimitGiB
|
max := config.MonthlyTrafficLimitGiB
|
||||||
ratio := float64(effective) / 1024 / 1024 / 1024 / float64(max)
|
ratio := float64(effective) / 1024 / 1024 / 1024 / float64(max)
|
||||||
return fmt.Sprintf("`%s %2.0f%%`", drawBar(ratio, 25), ratio*100)
|
return fmt.Sprintf("%s `%2.0f%%`", drawBar(ratio, 16), ratio*100)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleYearProgressCmd(c tele.Context) error {
|
func handleYearProgressCmd(c tele.Context) error {
|
||||||
|
@ -230,61 +193,9 @@ func handleYearProgressCmd(c tele.Context) error {
|
||||||
elapsed := time.Since(yearStart)
|
elapsed := time.Since(yearStart)
|
||||||
ratio := float64(elapsed) / float64(yearDur)
|
ratio := float64(elapsed) / float64(yearDur)
|
||||||
|
|
||||||
replyText := fmt.Sprintf(
|
|
||||||
"\n%d is <b>%2.0f%%</b> complete.\n<pre>%s</pre>",
|
|
||||||
time.Now().Year(), ratio*100, drawBar(ratio, 25),
|
|
||||||
)
|
|
||||||
return c.Reply(replyText, &tele.SendOptions{ParseMode: tele.ModeHTML})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGeneralMessage(_ tele.Context) error {
|
|
||||||
// Do nothing for now
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func stickerFromID(id string) *tele.Sticker {
|
|
||||||
return &tele.Sticker{
|
|
||||||
File: tele.File{
|
|
||||||
FileID: id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleDigCmd(c tele.Context) error {
|
|
||||||
msg := c.Message()
|
|
||||||
if msg == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := cmds.NewDigRequest(msg.Payload)
|
|
||||||
if err != nil {
|
|
||||||
return c.Reply("Invalid arguments.\nUsage: `/dig <name> [type]`", &tele.SendOptions{ParseMode: tele.ModeMarkdown})
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := cmds.Dig(req)
|
|
||||||
if err != nil {
|
|
||||||
_ = c.Reply(stickerFromID(stickerPanic))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
replyBuf := &strings.Builder{}
|
|
||||||
tw := tabwriter.NewWriter(replyBuf, 0, 0, 2, ' ', 0)
|
|
||||||
// Write header
|
|
||||||
if len(resp.Records) > 0 {
|
|
||||||
_, _ = tw.Write([]byte("Name\tTTL\tType\tData\n"))
|
|
||||||
}
|
|
||||||
// Write data
|
|
||||||
for _, r := range resp.Records {
|
|
||||||
_, _ = fmt.Fprintf(tw, "%s\t%d\t%s\t%s\n", r.Name, r.TTL, r.Type, r.Data)
|
|
||||||
}
|
|
||||||
_ = tw.Flush()
|
|
||||||
|
|
||||||
replyText := []string{
|
replyText := []string{
|
||||||
fmt.Sprintf("<i>Status: <b>%s</b></i>\n", resp.Status),
|
fmt.Sprintf("`%d is %2.0f%% complete.`", time.Now().Year(), ratio*100),
|
||||||
fmt.Sprintf("<i>Query Time: <b>%s</b></i>\n\n", resp.QueryTime),
|
drawBar(ratio, 20),
|
||||||
"<pre>",
|
|
||||||
html.EscapeString(replyBuf.String()),
|
|
||||||
"</pre>",
|
|
||||||
}
|
}
|
||||||
return c.Reply(strings.Join(replyText, ""), &tele.SendOptions{ParseMode: tele.ModeHTML})
|
return c.Reply(strings.Join(replyText, "\n"), &tele.SendOptions{ParseMode: tele.ModeMarkdown})
|
||||||
}
|
}
|
||||||
|
|
10
cfg.go
10
cfg.go
|
@ -10,9 +10,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AdminUIDs map[int64]struct{}
|
AdminUIDs map[int64]struct{}
|
||||||
TGBotToken string
|
TGBotToken string
|
||||||
TGAnnounceCommands bool
|
|
||||||
|
|
||||||
WatchedInterface string
|
WatchedInterface string
|
||||||
MonthlyTrafficLimitGiB int
|
MonthlyTrafficLimitGiB int
|
||||||
|
@ -43,11 +42,6 @@ func LoadCfg() error {
|
||||||
cfg.AdminUIDs[uid] = struct{}{}
|
cfg.AdminUIDs[uid] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
announceCmdsEnv := os.Getenv("TG_ANNOUNCE_CMDS")
|
|
||||||
if !lo.Contains([]string{"", "no", "false", "0"}, strings.ToLower(announceCmdsEnv)) {
|
|
||||||
cfg.TGAnnounceCommands = true
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.WatchedInterface = "eth0"
|
cfg.WatchedInterface = "eth0"
|
||||||
if iface := os.Getenv("TG_WATCHED_INTERFACE"); iface != "" {
|
if iface := os.Getenv("TG_WATCHED_INTERFACE"); iface != "" {
|
||||||
cfg.WatchedInterface = iface
|
cfg.WatchedInterface = iface
|
||||||
|
|
172
cmds/dig.go
172
cmds/dig.go
|
@ -1,172 +0,0 @@
|
||||||
package cmds
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os/exec"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-errors/errors"
|
|
||||||
|
|
||||||
"git.gensokyo.cafe/kkyy/tgbot_misaka_5882f7/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// According to wikipedia
|
|
||||||
digValidDnsTypes = utils.ToLookupMap([]string{
|
|
||||||
"A", "AAAA", "AFSDB", "APL", "CAA", "CDNSKEY", "CDS", "CERT", "CNAME", "CSYNC", "DHCID", "DLV", "DNAME",
|
|
||||||
"DNSKEY", "DS", "EUI48", "EUI64", "HINFO", "HIP", "HTTPS", "IPSECKEY", "KEY", "KX", "LOC", "MX", "NAPTR", "NS",
|
|
||||||
"NSEC", "NSEC3", "NSEC3PARAM", "OPENPGPKEY", "PTR", "RRSIG", "RP", "SIG", "SMIMEA", "SOA", "SRV", "SSHFP",
|
|
||||||
"SVCB", "TA", "TKEY", "TLSA", "TSIG", "TXT", "URI", "ZONEMD",
|
|
||||||
})
|
|
||||||
|
|
||||||
digErrInvalidArgs = fmt.Errorf("invalid request")
|
|
||||||
|
|
||||||
digDnsNameRe = regexp.MustCompile(`^([a-z0-9_-]+\.?)+|\.$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
type DigRequest struct {
|
|
||||||
Name string
|
|
||||||
Type string
|
|
||||||
Reverse bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDigRequest(req string) (*DigRequest, error) {
|
|
||||||
ret := &DigRequest{}
|
|
||||||
|
|
||||||
args := strings.Fields(req)
|
|
||||||
nArgs := len(args)
|
|
||||||
if nArgs == 0 || nArgs > 2 {
|
|
||||||
return nil, digErrInvalidArgs
|
|
||||||
}
|
|
||||||
|
|
||||||
if nArgs > 1 {
|
|
||||||
typ := strings.ToUpper(args[1])
|
|
||||||
if _, ok := digValidDnsTypes[typ]; !ok {
|
|
||||||
return nil, digErrInvalidArgs
|
|
||||||
}
|
|
||||||
ret.Type = typ
|
|
||||||
}
|
|
||||||
|
|
||||||
ip := net.ParseIP(args[0])
|
|
||||||
if ip != nil {
|
|
||||||
ret.Name = ip.String()
|
|
||||||
ret.Reverse = true
|
|
||||||
ret.Type = ""
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
name := strings.ToLower(args[0])
|
|
||||||
if !digDnsNameRe.Match([]byte(name)) {
|
|
||||||
return nil, digErrInvalidArgs
|
|
||||||
}
|
|
||||||
ret.Name = name
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *DigRequest) ToCmdArgs() []string {
|
|
||||||
args := make([]string, 0, 4)
|
|
||||||
args = append(args, "-u")
|
|
||||||
|
|
||||||
if r.Reverse {
|
|
||||||
args = append(args, "-x")
|
|
||||||
}
|
|
||||||
args = append(args, r.Name)
|
|
||||||
|
|
||||||
if r.Type != "" {
|
|
||||||
args = append(args, r.Type)
|
|
||||||
}
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
type DigDnsRecord struct {
|
|
||||||
Name string
|
|
||||||
TTL int
|
|
||||||
Class string
|
|
||||||
Type string
|
|
||||||
Data string
|
|
||||||
}
|
|
||||||
|
|
||||||
type DigResponse struct {
|
|
||||||
Status string
|
|
||||||
QueryTime time.Duration
|
|
||||||
Records []DigDnsRecord
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
digRespDnsRecordLineRe = regexp.MustCompile(`^([^;\s]\S*)\s+(\d+)\s+([A-Z]+)\s+([A-Z]+)\s+(.*)$`)
|
|
||||||
digRespHeaderLineRe = regexp.MustCompile(`^;;.*HEADER.*status: ([A-Z]+),.*$`)
|
|
||||||
digResqQueryTimeLineRe = regexp.MustCompile(`^;; Query time: (\d+) usec$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
func buildDigResponse(buf []byte) (*DigResponse, error) {
|
|
||||||
sc := bufio.NewScanner(bytes.NewReader(buf))
|
|
||||||
ret := &DigResponse{}
|
|
||||||
|
|
||||||
for sc.Scan() {
|
|
||||||
line := sc.Text()
|
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if line[0] == ';' {
|
|
||||||
if ret.Status == "" {
|
|
||||||
m := digRespHeaderLineRe.FindStringSubmatch(line)
|
|
||||||
if len(m) == 2 {
|
|
||||||
ret.Status = m[1]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m := digResqQueryTimeLineRe.FindStringSubmatch(line)
|
|
||||||
if len(m) == 2 {
|
|
||||||
usec, err := strconv.ParseInt(m[1], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WrapPrefix(err, "failed to parse query time", 0)
|
|
||||||
}
|
|
||||||
ret.QueryTime = time.Microsecond * time.Duration(usec)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m := digRespDnsRecordLineRe.FindStringSubmatch(line)
|
|
||||||
if len(m) == 6 {
|
|
||||||
ttl, err := strconv.Atoi(m[2])
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WrapPrefix(err, "failed to parse ttl", 0)
|
|
||||||
}
|
|
||||||
ret.Records = append(ret.Records, DigDnsRecord{
|
|
||||||
Name: m[1],
|
|
||||||
TTL: ttl,
|
|
||||||
Class: m[3],
|
|
||||||
Type: m[4],
|
|
||||||
Data: m[5],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ret.Status == "" {
|
|
||||||
return nil, errors.New("failed to parse response: \"status\" is unknown")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Dig(req *DigRequest) (*DigResponse, error) {
|
|
||||||
cmd := exec.Command("/usr/bin/dig", req.ToCmdArgs()...)
|
|
||||||
cmd.Stdin = nil
|
|
||||||
|
|
||||||
buf, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WrapPrefix(err, "failed to run dig command", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
ret, err := buildDigResponse(buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WrapPrefix(err, "failed to parse dig response", 0)
|
|
||||||
}
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
14
main.go
14
main.go
|
@ -6,7 +6,6 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
tele "gopkg.in/telebot.v3"
|
|
||||||
|
|
||||||
"git.gensokyo.cafe/kkyy/tgbot_misaka_5882f7/utils"
|
"git.gensokyo.cafe/kkyy/tgbot_misaka_5882f7/utils"
|
||||||
)
|
)
|
||||||
|
@ -29,19 +28,6 @@ func runBot() {
|
||||||
logger.Fatalw("Failed to initialize bot", "err", err)
|
logger.Fatalw("Failed to initialize bot", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Announce commands
|
|
||||||
if config.TGAnnounceCommands {
|
|
||||||
logger.Info("Announcing commands...")
|
|
||||||
|
|
||||||
if err = bot.SetCommands([]tele.Command{
|
|
||||||
{Text: "traffic", Description: "Show traffic usage."},
|
|
||||||
{Text: "dig", Description: "Diggy diggy dig."},
|
|
||||||
{Text: "year_progress", Description: "Time doesn't wait."},
|
|
||||||
}); err != nil {
|
|
||||||
logger.Fatalw("Failed to announce commands", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
botFinCh := utils.WaitFor(bot.Start)
|
botFinCh := utils.WaitFor(bot.Start)
|
||||||
logger.Infow("Bot started", "username", bot.Me.Username)
|
logger.Infow("Bot started", "username", bot.Me.Username)
|
||||||
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
package utils
|
|
||||||
|
|
||||||
func WaitFor(fn func()) <-chan struct{} {
|
|
||||||
ch := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer close(ch)
|
|
||||||
fn()
|
|
||||||
}()
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToLookupMap[T comparable](s []T) map[T]struct{} {
|
|
||||||
m := make(map[T]struct{}, len(s))
|
|
||||||
for _, item := range s {
|
|
||||||
m[item] = struct{}{}
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
Loading…
Reference in New Issue