diff --git a/config.json.sample b/config.json.sample new file mode 100644 index 0000000..b4329ad --- /dev/null +++ b/config.json.sample @@ -0,0 +1,30 @@ +{ + "api_id": "api_id_here", + "api_hash": "api_hash_here", + "bot_token": "bot_token_here", + "chat_id": 0, + "chat_username": "", + "pinned_message": 0, + "playlist_id": "p0", + "limit": { + "chat_select_limit": 5, + "private_select_limit": 10, + "row_limit": 10, + "queue_limit": 50, + "recent_limit": 50, + "request_song_per_minute": 1 + }, + "vote": { + "enable": true, + "vote_time": 45, + "update_time": 15, + "release_time": 600, + "percent_of_success": 40, + "participants_only": true, + "user_must_join": false + }, + "web": { + "enable": true, + "web_port": 2468 + } +} \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..9c11b6e --- /dev/null +++ b/config/config.go @@ -0,0 +1,97 @@ +package config + +func GetConfig() Config { + return config +} + +func GetApiId() string { + return config.ApiId +} + +func GetApiHash() string { + return config.ApiHash +} + +func GetBotToken() string { + return config.BotToken +} + +func GetChatId() int64 { + return config.ChatId +} + +func SetChatId(value int64) { + config.ChatId = value +} + +func GetChatUsername() string { + return config.ChatUsername +} + +func GetPinnedMessage() int64 { + return config.PinnedMsg << 20 +} + +func GetPlaylistId() string { + return config.PlaylistId +} + +func GetChatSelectLimit() int { + return config.LimitSetting.ChatSelectLimit +} + +func GetPrivateChatSelectLimit() int { + return config.LimitSetting.PriSelectLimit +} + +func GetRowLimit() int { + return config.LimitSetting.RowLimit +} + +func GetQueueLimit() int { + return config.LimitSetting.QueueLimit +} + +func GetRecentLimit() int { + return config.LimitSetting.RecentLimit +} + +func GetReqSongLimit() int { + return config.LimitSetting.ReqSongPerMin +} + +func IsVoteEnabled() bool { + return config.VoteSetting.Enable +} + +func GetSuccessRate() float64 { + return config.VoteSetting.PctOfSuccess +} + +func IsPtcpsOnly() bool { + return config.VoteSetting.PtcpsOnly +} + +func GetVoteTime() int32 { + return config.VoteSetting.VoteTime +} + +func GetReleaseTime() int64 { + return config.VoteSetting.ReleaseTime +} + +func GetUpdateTime() int32 { + return config.VoteSetting.UpdateTime +} + +func IsJoinNeeded() bool { + return config.VoteSetting.UserMustJoin +} + +func IsWebEnabled() bool { + return config.WebSetting.Enable +} + +func GetWebPort() int { + return config.WebSetting.Port +} diff --git a/config/read.go b/config/read.go new file mode 100644 index 0000000..20c4321 --- /dev/null +++ b/config/read.go @@ -0,0 +1,63 @@ +package config + +import ( + "encoding/json" + "io/ioutil" + "log" + "os" + "time" + + "github.com/go-co-op/gocron" +) + +var ( + status Status + config Config + cron = gocron.NewScheduler(time.UTC) +) + +func Read() { + ReadConfig() + ReadStatus() +} + +func ReadConfig() { + if err := LoadConfig(); err != nil { + log.Fatal(err) + } +} + +func LoadConfig() error { + b, err := ioutil.ReadFile("config.json") + if err != nil { + return err + } + e := json.Unmarshal(b, &config) + if e != nil { + return e + } + return nil +} + +func initStatus() { + b := []byte("{}") + os.WriteFile("status.json", b, 0755) + json.Unmarshal(b, &status) +} + +func ReadStatus() { + if b, err := ioutil.ReadFile("status.json"); err == nil { + e := json.Unmarshal(b, &status) + if e != nil { + log.Println("status.json is broken...resetting") + initStatus() + } + } else { + log.Println("status.json not found...initializating") + initStatus() + } + cron.Every(1).Minute().Do(func() { + SaveStatus() + }) + cron.StartAsync() +} diff --git a/config/save.go b/config/save.go new file mode 100644 index 0000000..9e71f87 --- /dev/null +++ b/config/save.go @@ -0,0 +1,30 @@ +package config + +import ( + "encoding/json" + "log" + "os" +) + +func Save() { + SaveStatus() + SaveConfig() +} + +func SaveStatus() { + b, err := json.MarshalIndent(status, "", " ") + if err != nil { + log.Println("Failed to save status...") + return + } + os.WriteFile("status.json", b, 0755) +} + +func SaveConfig() { + b, err := json.MarshalIndent(config, "", " ") + if err != nil { + log.Println("Failed to save config...") + return + } + os.WriteFile("config.json", b, 0755) +} diff --git a/config/status.go b/config/status.go new file mode 100644 index 0000000..7176411 --- /dev/null +++ b/config/status.go @@ -0,0 +1,29 @@ +package config + +func GetStatus() Status { + return status +} + +func (s Status) GetCurrent() string { + return s.Current +} + +func SetCurrentSong(value string) { + status.Current = value +} + +func (s Status) GetQueue() []int { + return s.Queue +} + +func SetQueueSong(value []int) { + status.Queue = value +} + +func (s Status) GetRecent() []int { + return s.Recent +} + +func SetRecentSong(value []int) { + status.Recent = value +} diff --git a/config/struct.go b/config/struct.go new file mode 100644 index 0000000..d5e37e7 --- /dev/null +++ b/config/struct.go @@ -0,0 +1,44 @@ +package config + +type Status struct { + Current string `json:"current"` + Queue []int `json:"queue"` + Recent []int `json:"recent"` +} + +type Config struct { + ApiId string `json:"api_id"` + ApiHash string `json:"api_hash"` + BotToken string `json:"bot_token"` + ChatId int64 `json:"chat_id"` + ChatUsername string `json:"chat_username"` + PinnedMsg int64 `json:"pinned_message"` + PlaylistId string `json:"playlist_id"` + LimitSetting Limit `json:"limit"` + VoteSetting Vote `json:"vote"` + WebSetting Web `json:"web"` +} + +type Limit struct { + ChatSelectLimit int `json:"chat_select_limit"` + PriSelectLimit int `json:"private_select_limit"` + RowLimit int `json:"row_limit"` + QueueLimit int `json:"queue_limit"` + RecentLimit int `json:"recent_limit"` + ReqSongPerMin int `json:"request_song_per_minute"` +} + +type Vote struct { + Enable bool `json:"enable"` + VoteTime int32 `json:"vote_time"` + UpdateTime int32 `json:"update_time"` + ReleaseTime int64 `json:"release_time"` + PctOfSuccess float64 `json:"percent_of_success"` + PtcpsOnly bool `json:"participants_only"` + UserMustJoin bool `json:"user_must_join"` +} + +type Web struct { + Enable bool `json:"enable"` + Port int `json:"port"` +} diff --git a/fb2k/control.go b/fb2k/control.go new file mode 100644 index 0000000..2455dc4 --- /dev/null +++ b/fb2k/control.go @@ -0,0 +1,110 @@ +package fb2k + +import ( + "fmt" + "net/http" + "runtime" + "strconv" + "strings" + + "github.com/c0re100/RadioBot/config" + "github.com/c0re100/go-tdlib" +) + +var ( + bot *tdlib.Client + SongQueue = make(chan int, 100) +) + +func New(client *tdlib.Client) { + bot = client + restoreQueue() + GetEvent() +} + +func restoreQueue() { + for _, q := range config.GetStatus().GetQueue() { + SongQueue <- q + } +} + +func Play() { + http.Post("http://127.0.0.1:8880/api/player/play", "", nil) +} + +func PlayNext() { + http.Post("http://127.0.0.1:8880/api/player/next", "", nil) +} + +func PlayRandom() { + http.Post("http://127.0.0.1:8880/api/player/play/random", "", nil) +} + +func Stop() { + http.Post("http://127.0.0.1:8880/api/player/stop", "", nil) +} + +func Pause() { + http.Post("http://127.0.0.1:8880/api/player/pause", "", nil) +} + +func PlaySelected(selectedIdx int) { + rx.Lock() + defer rx.Unlock() + http.Post("http://127.0.0.1:8880/api/player/play/"+config.GetPlaylistId()+"/"+strconv.Itoa(selectedIdx), "", nil) + + rList := config.GetStatus().GetRecent() + qList := config.GetStatus().GetQueue() + + recent := append(rList, selectedIdx) + config.SetRecentSong(recent) + queue := append(qList[:0], qList[1:]...) + config.SetQueueSong(queue) + + if len(recent) >= config.GetRecentLimit() { + recent = append(recent[:0], recent[1:]...) + config.SetRecentSong(recent) + } + + config.SaveStatus() +} + +func PushQueue(selectedIdx int) { + rx.Lock() + defer rx.Unlock() + SongQueue <- selectedIdx + + queue := config.GetStatus().GetQueue() + queue = append(queue, selectedIdx) + config.SetQueueSong(queue) + + if len(queue) >= config.GetQueueLimit() { + queue = append(queue[:0], queue[1:]...) + config.SetQueueSong(queue) + } + + config.SaveStatus() +} + +func checkNextSong() { + if len(SongQueue) == 0 { + return + } + next := <-SongQueue + PlaySelected(next) +} + +func SetKillSwitch() { + killSwitch <- true +} + +func getGoId() int { + var buf [64]byte + n := runtime.Stack(buf[:], false) + idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0] + id, err := strconv.Atoi(idField) + if err != nil { + panic(fmt.Sprintf("cannot get goroutine id: %v", err)) + } + return id +} diff --git a/fb2k/event.go b/fb2k/event.go new file mode 100644 index 0000000..a50c5a5 --- /dev/null +++ b/fb2k/event.go @@ -0,0 +1,110 @@ +package fb2k + +import ( + "encoding/json" + "fmt" + "log" + "sync" + "time" + + "github.com/c0re100/RadioBot/config" + "github.com/c0re100/RadioBot/utils" + "github.com/c0re100/go-tdlib" + "github.com/r3labs/sse" +) + +var ( + rx sync.Mutex + killSwitch = make(chan bool, 1) +) + +func isSameAsCurrent(songName string) bool { + if config.GetStatus().GetCurrent() == songName { + return true + } + return false +} + +func sendNewMessage(cId int64, msgText *tdlib.InputMessageText) { + m, newErr := bot.SendMessage(cId, 0, 0, nil, nil, msgText) + if newErr != nil { + log.Println("[Send] Failed to broadcast current song...", newErr) + return + } + bot.PinChatMessage(cId, m.Id, true, false) + bot.DeleteMessages(cId, []int64{m.Id + 1048576}, true) + config.SetChatId(m.Id) + config.SaveConfig() +} + +func GetEvent() { + fmt.Println("[Player] Update Event Receiver") + client := sse.NewClient("http://127.0.0.1:8880/api/query/updates?player=true&trcolumns=%25artist%25%20-%20%25title%25,%25artist%25,%25title%25,%25album%25") + + client.Subscribe("messages", func(msg *sse.Event) { + data := string(msg.Data) + if data != "{}" { + var event utils.Event + if err := json.Unmarshal(msg.Data, &event); err == nil { + if len(event.Player.ActiveItem.Columns) >= 1 { + defer func() { + go func(dur int64) { + select { + case <-killSwitch: + fmt.Printf("Next song monitor: Goroutine #%v killed!\n", getGoId()) + return + case <-time.After(time.Duration(dur)*time.Second - 500*time.Millisecond): + checkNextSong() + } + }(int64(event.Player.ActiveItem.Duration - event.Player.ActiveItem.Position)) + }() + + songName := event.Player.ActiveItem.Columns[0] + if isSameAsCurrent(songName) { + return + } + + artist := event.Player.ActiveItem.Columns[1] + track := event.Player.ActiveItem.Columns[2] + album := event.Player.ActiveItem.Columns[3] + idx := event.Player.ActiveItem.Index + text := fmt.Sprintf("Now playing: \n"+ + "Artist: %v\n"+ + "Track: %v\n"+ + "Album: %v\n"+ + "Duration: %v", utils.IsEmpty(artist), utils.IsEmpty(track), utils.IsEmpty(album), utils.SecondsToMinutes(int64(event.Player.ActiveItem.Duration))) + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText(text, nil), true, false) + cId := config.GetChatId() + mId := config.GetPinnedMessage() + + if mId == 0 { + sendNewMessage(cId, msgText) + } else { + _, getErr := bot.GetMessage(cId, mId) + if getErr != nil { + sendNewMessage(cId, msgText) + } else { + _, editErr := bot.EditMessageText(cId, mId, nil, msgText) + if editErr != nil { + log.Println("[Edit] Failed to broadcast current song...", editErr) + return + } + } + } + + rx.Lock() + recent := config.GetStatus().GetRecent() + recent = append(recent, idx) + config.SetRecentSong(recent) + if len(recent) >= config.GetRecentLimit() { + recent = append(recent[:0], recent[1:]...) + config.SetRecentSong(recent) + } + config.SetCurrentSong(songName) + config.SaveStatus() + rx.Unlock() + } + } + } + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8996c24 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/c0re100/RadioBot + +go 1.16 + +require ( + github.com/beefsack/go-rate v0.0.0-20200827232406-6cde80facd47 + github.com/c0re100/go-tdlib v0.0.0-20210225210704-0d7d480c27f8 + github.com/go-co-op/gocron v0.7.0 + github.com/labstack/echo/v4 v4.2.0 + github.com/pion/webrtc/v2 v2.2.26 + github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc + golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3463f85 --- /dev/null +++ b/go.sum @@ -0,0 +1,160 @@ +github.com/beefsack/go-rate v0.0.0-20200827232406-6cde80facd47 h1:M57m0xQqZIhx7CEJgeLSvRFKEK1RjzRuIXiA3HfYU7g= +github.com/beefsack/go-rate v0.0.0-20200827232406-6cde80facd47/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA= +github.com/c0re100/go-tdlib v0.0.0-20210225210704-0d7d480c27f8 h1:kfIiCb/vzHaosRevNilPXjVxvikPY7yKt3oHiHTpowY= +github.com/c0re100/go-tdlib v0.0.0-20210225210704-0d7d480c27f8/go.mod h1:Il5buPm8uQ4Ir3aI2dF0o8x0ymKxaMHihZjfN1/2Zjs= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-co-op/gocron v0.7.0 h1:oHOZagvz/HmQaTLgqcuH0DauG5YDGCZK1UzH1rJCXAE= +github.com/go-co-op/gocron v0.7.0/go.mod h1:Hyge6OdrinfqhNgi1kNLnA/O7GtFsr004+Rbrgx5Ylc= +github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/labstack/echo/v4 v4.2.0 h1:jkCSsjXmBmapVXF6U4BrSz/cgofWM0CU3Q74wQvXkIc= +github.com/labstack/echo/v4 v4.2.0/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= +github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc= +github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= +github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA= +github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0= +github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= +github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U= +github.com/pion/dtls/v2 v2.0.2 h1:FHCHTiM182Y8e15aFTiORroiATUI16ryHiQh8AIOJ1E= +github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I= +github.com/pion/ice v0.7.18 h1:KbAWlzWRUdX9SmehBh3gYpIFsirjhSQsCw6K2MjYMK0= +github.com/pion/ice v0.7.18/go.mod h1:+Bvnm3nYC6Nnp7VV6glUkuOfToB/AtMRZpOU8ihuf4c= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY= +github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= +github.com/pion/quic v0.1.1 h1:D951FV+TOqI9A0rTF7tHx0Loooqz+nyzjEyj8o3PuMA= +github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k= +github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.3 h1:2wrhKnqgSz91Q5nzYTO07mQXztYPtxL8a0XOss4rJqA= +github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I= +github.com/pion/rtp v1.6.0 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk= +github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI= +github.com/pion/sctp v1.7.10 h1:o3p3/hZB5Cx12RMGyWmItevJtZ6o2cpuxaw6GOS4x+8= +github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= +github.com/pion/sdp/v2 v2.4.0 h1:luUtaETR5x2KNNpvEMv/r4Y+/kzImzbz4Lm1z8eQNQI= +github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E= +github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw= +github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA= +github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= +github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= +github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= +github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= +github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= +github.com/pion/transport v0.10.1 h1:2W+yJT+0mOQ160ThZYUx5Zp2skzshiNgxrNE9GUfhJM= +github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= +github.com/pion/turn/v2 v2.0.4 h1:oDguhEv2L/4rxwbL9clGLgtzQPjtuZwCdoM7Te8vQVk= +github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog= +github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= +github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= +github.com/pion/webrtc/v2 v2.2.26 h1:01hWE26pL3LgqfxvQ1fr6O4ZtyRFFJmQEZK39pHWfFc= +github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc= +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.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc h1:zAsgcP8MhzAbhMnB1QQ2O7ZhWYVGYSR2iVcjzQuPV+o= +github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc/go.mod h1:S8xSOnV3CgpNrWd0GQ/OoQfMtlg2uPRSuTzcSGrzwK8= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= +gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..8d97fc4 --- /dev/null +++ b/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/c0re100/RadioBot/config" + "github.com/c0re100/RadioBot/fb2k" + "github.com/c0re100/RadioBot/telegram" + "github.com/c0re100/RadioBot/web" + "github.com/c0re100/RadioBot/wrtc" +) + +func main() { + ch := make(chan os.Signal, 2) + signal.Notify(ch, os.Interrupt, syscall.SIGINT) + signal.Notify(ch, os.Interrupt, syscall.SIGKILL) + signal.Notify(ch, os.Interrupt, syscall.SIGTERM) + signal.Notify(ch, os.Interrupt, syscall.SIGQUIT) + signal.Notify(ch, os.Interrupt, syscall.SIGSEGV) + go func() { + <-ch + config.Save() + wrtc.Disconnnect() + fmt.Println("Shutdown...") + os.Exit(0) + }() + + config.Read() + go web.StartServer() + bot, _ := telegram.New() + fb2k.New(bot) +} diff --git a/telegram/button.go b/telegram/button.go new file mode 100644 index 0000000..0cba936 --- /dev/null +++ b/telegram/button.go @@ -0,0 +1,107 @@ +package telegram + +import ( + "fmt" + "strconv" + + "github.com/c0re100/RadioBot/config" + "github.com/c0re100/RadioBot/fb2k" + "github.com/c0re100/RadioBot/utils" + "github.com/c0re100/go-tdlib" +) + +func createSongListButton(offset int) [][]tdlib.InlineKeyboardButton { + var songKb [][]tdlib.InlineKeyboardButton + + mutex.Lock() + for i := offset; i < offset+config.GetRowLimit(); i++ { + if songList[i] == "" { + continue + } + songKb = append(songKb, []tdlib.InlineKeyboardButton{*tdlib.NewInlineKeyboardButton(songList[i], tdlib.NewInlineKeyboardButtonTypeCallback([]byte("select_song:"+strconv.Itoa(i))))}) + } + mutex.Unlock() + + return songKb +} + +func finalizeButton(songKb [][]tdlib.InlineKeyboardButton, offset int, isSearch bool) *tdlib.ReplyMarkupInlineKeyboard { + cbTag := "page:" + if isSearch { + cbTag = "result:" + } + if len(songKb) < 10 && offset == 0 && isSearch { + + } else if offset == 0 { + songKb = append(songKb, []tdlib.InlineKeyboardButton{ + *tdlib.NewInlineKeyboardButton("Next page", tdlib.NewInlineKeyboardButtonTypeCallback([]byte(cbTag+strconv.Itoa(offset+10)))), + }) + } else if len(songKb) < 10 { + songKb = append(songKb, []tdlib.InlineKeyboardButton{ + *tdlib.NewInlineKeyboardButton("Previous page", tdlib.NewInlineKeyboardButtonTypeCallback([]byte(cbTag+strconv.Itoa(offset-10)))), + }) + } else { + songKb = append(songKb, []tdlib.InlineKeyboardButton{ + *tdlib.NewInlineKeyboardButton("Previous page", tdlib.NewInlineKeyboardButtonTypeCallback([]byte(cbTag+strconv.Itoa(offset-10)))), + *tdlib.NewInlineKeyboardButton("Next page", tdlib.NewInlineKeyboardButtonTypeCallback([]byte(cbTag+strconv.Itoa(offset+10)))), + }) + } + return tdlib.NewReplyMarkupInlineKeyboard(songKb) +} + +func sendButtonMessage(chatId, msgId int64) { + var format *tdlib.FormattedText + if chatId < 0 { + text := "Which song do you want to play?" + + "\n\n" + + "Use Private Chat to request a song WHEN you exceeded rate-limit." + format, _ = bot.ParseTextEntities(text, tdlib.NewTextParseModeHTML()) + } else { + format = tdlib.NewFormattedText("Which song do you want to play?", nil) + } + text := tdlib.NewInputMessageText(format, false, false) + songKb := createSongListButton(0) + kb := finalizeButton(songKb, 0, false) + bot.SendMessage(chatId, 0, msgId, tdlib.NewMessageSendOptions(false, true, nil), kb, text) +} + +func editButtonMessage(chatId, msgId int64, queryId tdlib.JSONInt64, offset int) { + if canSelectPage(chatId, queryId) { + var format *tdlib.FormattedText + if chatId < 0 { + text := "Which song do you want to play?" + + "\n\n" + + "Use Private Chat to request a song WHEN you exceeded rate-limit." + format, _ = bot.ParseTextEntities(text, tdlib.NewTextParseModeHTML()) + } else { + format = tdlib.NewFormattedText("Which song do you want to play?", nil) + } + text := tdlib.NewInputMessageText(format, false, false) + songKb := createSongListButton(offset) + kb := finalizeButton(songKb, offset, false) + bot.EditMessageText(chatId, msgId, kb, text) + } +} + +func selectSongMessage(userId int32, queryId tdlib.JSONInt64, idx int) { + if ok, sec := canReqSong(userId); !ok { + bot.AnswerCallbackQuery(queryId, fmt.Sprintf("You're already requested recently, Please try again in %v seconds...", sec), false, "", 59) + return + } + + if songList[idx] == "" { + bot.AnswerCallbackQuery(queryId, "This song is not available...", false, "", 180) + } else if len(GetQueue()) >= config.GetQueueLimit() { + bot.AnswerCallbackQuery(queryId, "Too many song in request song list now...\nPlease try again later~", false, "", 180) + } else { + if utils.ContainsInt(GetRecent(), idx) { + bot.AnswerCallbackQuery(queryId, "Song was recently played!", false, "", 180) + } else if utils.ContainsInt(GetQueue(), idx) { + bot.AnswerCallbackQuery(queryId, "Song was recently requested!", false, "", 180) + } else { + fb2k.PushQueue(idx) + choice := fmt.Sprintf("Your choice: %v | Song queue: %v", songList[idx], len(GetQueue())) + bot.AnswerCallbackQuery(queryId, choice, false, "", 180) + } + } +} diff --git a/telegram/callback.go b/telegram/callback.go new file mode 100644 index 0000000..452952e --- /dev/null +++ b/telegram/callback.go @@ -0,0 +1,50 @@ +package telegram + +import ( + "fmt" + "strconv" + "strings" + + "github.com/c0re100/go-tdlib" +) + +func callbackQuery() { + fmt.Println("[Music] New Callback Receiver") + eventFilter := func(msg *tdlib.TdMessage) bool { + return true + } + receiver := bot.AddEventReceiver(&tdlib.UpdateNewCallbackQuery{}, eventFilter, 1000) + for newMsg := range receiver.Chan { + go func(newMsg tdlib.TdMessage) { + updateMsg := (newMsg).(*tdlib.UpdateNewCallbackQuery) + queryId := updateMsg.Id + chatId := updateMsg.ChatId + userId := updateMsg.SenderUserId + msgId := updateMsg.MessageId + data := string(updateMsg.Payload.(*tdlib.CallbackQueryPayloadData).Data) + + m, err := bot.GetMessage(chatId, msgId) + if err != nil { + return + } + + page := strings.Split(data, "page:") + selIdx := strings.Split(data, "select_song:") + result := strings.Split(data, "result:") + + switch { + case data == "vote_skip": + setUserVote(chatId, msgId, userId, queryId) + case len(selIdx) == 2: + idx, _ := strconv.Atoi(selIdx[1]) + selectSongMessage(userId, queryId, idx) + case len(page) == 2: + offset, _ := strconv.Atoi(page[1]) + editButtonMessage(chatId, msgId, queryId, offset) + case len(result) == 2: + offset, _ := strconv.Atoi(result[1]) + editCustomButtonMessage(chatId, m, queryId, offset) + } + }(newMsg) + } +} diff --git a/telegram/command.go b/telegram/command.go new file mode 100644 index 0000000..5de403b --- /dev/null +++ b/telegram/command.go @@ -0,0 +1,96 @@ +package telegram + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/c0re100/RadioBot/config" + "github.com/c0re100/RadioBot/fb2k" + "github.com/c0re100/RadioBot/utils" + "github.com/c0re100/go-tdlib" +) + +func getCurrentPlaying(chatId, msgId int64) { + resp, err := http.Get("http://127.0.0.1:8880/api/query?player=true&trcolumns=%25artist%25%20-%20%25title%25") + if err != nil { + return + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + + var event utils.Event + if err := json.Unmarshal(body, &event); err == nil { + if len(event.Player.ActiveItem.Columns) >= 1 { + songName := event.Player.ActiveItem.Columns[0] + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText("Now playing: \n"+songName, nil), true, false) + bot.SendMessage(chatId, 0, msgId, nil, nil, msgText) + } + } +} + +func nominate(chatId, msgId int64, userId int32, arg string) { + if arg == "" { + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText("Track name or Artist name is empty.", nil), true, false) + bot.SendMessage(chatId, 0, msgId, nil, nil, msgText) + return + } + + if ok, sec := canReqSong(userId); !ok { + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText(fmt.Sprintf("Please try again in %v seconds.", sec), nil), true, false) + bot.SendMessage(chatId, 0, msgId, nil, nil, msgText) + } else { + list := searchSong(arg) + if len(list) > 0 { + sendCustomButtonMessage(chatId, msgId, list) + } else { + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText("No result.", nil), true, false) + bot.SendMessage(chatId, 0, msgId, nil, nil, msgText) + } + } +} + +func isAdmin(chatId int64, userId int32) bool { + u, err := bot.GetChatMember(chatId, userId) + if err != nil { + fmt.Println(err.Error()) + return false + } + + if u.Status.GetChatMemberStatusEnum() == "chatMemberStatusAdministrator" || u.Status.GetChatMemberStatusEnum() == "chatMemberStatusCreator" { + return true + } + + return false +} + +func reload(chatId, msgId int64, userId int32) { + if isAdmin(chatId, userId) { + config.LoadConfig() + savePlaylistIndexAndName() + text := tdlib.NewInputMessageText(tdlib.NewFormattedText("Config&Playlist reloaded!", nil), false, false) + bot.SendMessage(chatId, 0, msgId, tdlib.NewMessageSendOptions(false, true, nil), nil, text) + } +} + +func playerControl(chatId int64, userId int32, cs int) { + if isAdmin(chatId, userId) { + switch cs { + case 0: + fb2k.Play() + case 1: + fb2k.Stop() + case 2: + fb2k.Pause() + case 3: + fb2k.PlayRandom() + default: + + } + } +} diff --git a/telegram/fetch.go b/telegram/fetch.go new file mode 100644 index 0000000..f27f07a --- /dev/null +++ b/telegram/fetch.go @@ -0,0 +1,94 @@ +package telegram + +import ( + "encoding/json" + "errors" + "io/ioutil" + "log" + "net/http" + "strconv" + "sync" + + "github.com/c0re100/RadioBot/config" +) + +var ( + songList = make(map[int]string) + mutex sync.Mutex +) + +type playlists struct { + Playlists []struct { + ID string `json:"id"` + Index int `json:"index"` + IsCurrent bool `json:"isCurrent"` + ItemCount int `json:"itemCount"` + Title string `json:"title"` + TotalTime float64 `json:"totalTime"` + } `json:"playlists"` +} + +type playlistColumn struct { + PlaylistItems struct { + Items []struct { + Columns []string `json:"columns"` + } `json:"items"` + Offset int `json:"offset"` + TotalCount int `json:"totalCount"` + } `json:"playlistItems"` +} + +func getPlaylistItemCount() (string, error) { + resp, err := http.Get("http://localhost:8880/api/playlists") + if err != nil { + return "", errors.New("failed to get api") + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", errors.New("failed to read api response") + } + + var pl playlists + if err := json.Unmarshal(body, &pl); err == nil { + for _, item := range pl.Playlists { + if item.ID == config.GetPlaylistId() { + return strconv.Itoa(item.ItemCount), nil + } + } + } + + return "", errors.New("failed to parse api response") +} + +func savePlaylistIndexAndName() { + defer mutex.Unlock() + mutex.Lock() + count, err := getPlaylistItemCount() + if err != nil { + log.Println(err) + return + } + resp, err := http.Get("http://localhost:8880/api/playlists/" + config.GetPlaylistId() + "/items/0%3A" + count + "?columns=%25artist%25%20-%20%25title%25") + if err != nil { + log.Println("playlist not found...") + return + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println("failed to read playlist response...") + return + } + + var plc playlistColumn + if err := json.Unmarshal(body, &plc); err == nil { + for idx, item := range plc.PlaylistItems.Items { + if len(item.Columns[0]) > 0 { + songList[idx] = item.Columns[0] + } + } + } +} diff --git a/telegram/groupCall.go b/telegram/groupCall.go new file mode 100644 index 0000000..eb7d046 --- /dev/null +++ b/telegram/groupCall.go @@ -0,0 +1,114 @@ +package telegram + +import ( + "fmt" + "log" + "time" + + "github.com/c0re100/RadioBot/config" + "github.com/c0re100/RadioBot/utils" + "github.com/c0re100/RadioBot/wrtc" + "github.com/c0re100/go-tdlib" +) + +func JoinGroupCall() { + c, _ := userBot.GetChat(config.GetChatId()) + gc, _ := userBot.GetGroupCall(c.VoiceChatGroupCallId) + grpStatus.vcId = gc.Id + + data := wrtc.CreateOffer(userBot) + payload := tdlib.NewGroupCallPayload(data.UFrag, data.Pwd, nil) + for _, c := range data.Cert { + fp, _ := c.GetFingerprints() + for _, f := range fp { + payload.Fingerprints = append(payload.Fingerprints, *tdlib.NewGroupCallPayloadFingerprint(f.Value, "active", f.Value)) + } + } + gcResp, err := userBot.JoinGroupCall(gc.Id, payload, int32(data.Ssrc), false) + if err != nil { + log.Println(err) + return + } + + addLoadGroupCallPtpcsJob() + if !sch.IsRunning() { + log.Println("Starting scheduler...") + startScheduler() + } + + go wrtc.Connect(gcResp, data) +} + +func loadParticipants(chatId int64, userId int32) { + if isAdmin(chatId, userId) { + gc, _ := userBot.GetGroupCall(grpStatus.vcId) + if gc.LoadedAllParticipants { + return + } + userBot.LoadGroupCallParticipants(gc.Id, 5000) + } +} + +func newGroupCallUpdate() { + fmt.Println("[Music] New GroupCall Receiver") + eventFilter := func(msg *tdlib.TdMessage) bool { + return true + } + + receiver := userBot.AddEventReceiver(&tdlib.UpdateGroupCall{}, eventFilter, 100) + for newMsg := range receiver.Chan { + updateMsg := (newMsg).(*tdlib.UpdateGroupCall) + gcId := updateMsg.GroupCall.Id + // todo + if grpStatus.vcId == gcId && grpStatus.isLoadPtcps && updateMsg.GroupCall.LoadedAllParticipants { + finalizeVote(grpStatus.chatId, grpStatus.msgId, updateMsg.GroupCall.ParticipantCount) + } + } +} + +func newGroupCallPtcpUpdate() { + fmt.Println("[Music] New GroupCallParticipant Receiver") + eventFilter := func(msg *tdlib.TdMessage) bool { + return true + } + + receiver := userBot.AddEventReceiver(&tdlib.UpdateGroupCallParticipant{}, eventFilter, 5000) + for newMsg := range receiver.Chan { + updateMsg := (newMsg).(*tdlib.UpdateGroupCallParticipant) + gcId := updateMsg.GroupCallId + uId := updateMsg.Participant.UserId + if grpStatus.vcId == gcId { + if updateMsg.Participant.Order == 0 { + if uId == userBotId && wrtc.GetConnection().ConnectionState().String() != "closed" { + time.Sleep(1 * time.Second) + log.Println("Userbot left voice chat...re-join now!") + JoinGroupCall() + } + RemovePtcp(uId) + } + AddPtcp(uId) + } + } +} + +func AddPtcp(userId int32) { + if !utils.ContainsInt32(grpStatus.Ptcps, userId) { + //log.Printf("User %v joined voice chat.\n", uId) + grpStatus.Ptcps = append(grpStatus.Ptcps, userId) + } +} + +func RemovePtcp(userId int32) { + //log.Printf("User %v left voice chat.\n", uId) + grpStatus.Ptcps = utils.FilterInt32(grpStatus.Ptcps, func(s int32) bool { + return s != userId + }) +} + +func ResetPtcps() { + grpStatus.Ptcps = []int32{} +} + +func GetPtcps() []int32 { + return grpStatus.Ptcps +} diff --git a/telegram/init.go b/telegram/init.go new file mode 100644 index 0000000..8443a3a --- /dev/null +++ b/telegram/init.go @@ -0,0 +1,170 @@ +package telegram + +import ( + "fmt" + "log" + "os" + "syscall" + + "github.com/c0re100/RadioBot/config" + "github.com/c0re100/go-tdlib" + "golang.org/x/crypto/ssh/terminal" +) + +var ( + bot *tdlib.Client + botId int32 + userBot *tdlib.Client + userBotId int32 +) + +func New() (*tdlib.Client, *tdlib.Client) { + tdlib.SetLogVerbosityLevel(0) + tdlib.SetFilePath("./errors.txt") + + if _, err := os.Stat("instance"); os.IsNotExist(err) { + if err := os.Mkdir("instance", 0755); err != nil { + log.Fatal("Failed to create instance dir...") + } + } + + err := botLogin() + if err != nil { + log.Fatal("bot login failed:", err) + } + checkGroupIsExist(bot) + + if !config.IsWebEnabled() { + err = userLogin() + if err != nil { + log.Fatal("userbot login failed:", err) + } + checkGroupIsExist(userBot) + } + + savePlaylistIndexAndName() + Receiver() + + return bot, userBot +} + +func newClient(name string) *tdlib.Client { + return tdlib.NewClient(tdlib.Config{ + APIID: config.GetApiId(), + APIHash: config.GetApiHash(), + SystemLanguageCode: "en", + DeviceModel: "Radio Controller", + SystemVersion: "1.0", + ApplicationVersion: "1.0", + UseMessageDatabase: true, + UseFileDatabase: true, + UseChatInfoDatabase: true, + UseTestDataCenter: false, + DatabaseDirectory: "./instance/" + name + "-db", + FileDirectory: "./instance/" + name + "-files", + IgnoreFileNames: false, + }) +} + +func botLogin() error { + bot = newClient("bot") + + for { + currentState, _ := bot.Authorize() + if currentState.GetAuthorizationStateEnum() == tdlib.AuthorizationStateWaitPhoneNumberType { + _, err := bot.CheckAuthenticationBotToken(config.GetBotToken()) + if err != nil { + log.Fatal(err) + } + } else if currentState.GetAuthorizationStateEnum() == tdlib.AuthorizationStateReadyType { + me, err := bot.GetMe() + if err != nil { + return err + } + botId = me.Id + fmt.Println(me.Username + " connected.") + break + } + } + return nil +} + +func userLogin() error { + userBot = newClient("user") + + for { + currentState, _ := userBot.Authorize() + if currentState.GetAuthorizationStateEnum() == tdlib.AuthorizationStateWaitPhoneNumberType { + fmt.Print("Enter phone: ") + var number string + fmt.Scanln(&number) + _, err := userBot.SendPhoneNumber(number) + if err != nil { + fmt.Printf("Error sending phone number: %v", err) + } + } else if currentState.GetAuthorizationStateEnum() == tdlib.AuthorizationStateWaitCodeType { + fmt.Print("Enter code: ") + var code string + fmt.Scanln(&code) + _, err := userBot.SendAuthCode(code) + if err != nil { + fmt.Printf("Error sending auth code : %v", err) + } + } else if currentState.GetAuthorizationStateEnum() == tdlib.AuthorizationStateWaitPasswordType { + fmt.Print("Enter Password: ") + bytePassword, err := terminal.ReadPassword(int(syscall.Stdin)) + if err != nil { + fmt.Println(err) + } + _, err = userBot.SendAuthPassword(string(bytePassword)) + if err != nil { + fmt.Printf("Error sending auth password: %v", err) + } + } else if currentState.GetAuthorizationStateEnum() == tdlib.AuthorizationStateReadyType { + me, err := userBot.GetMe() + if err != nil { + return err + } + userBotId = me.Id + fmt.Println("\nHello!", me.FirstName, me.LastName, "("+me.Username+")") + break + } + } + + return nil +} + +func Receiver() { + go newMessages() + go callbackQuery() + if !config.IsWebEnabled() { + go newGroupCallUpdate() + go newGroupCallPtcpUpdate() + JoinGroupCall() + } +} + +func checkGroupIsExist(cl *tdlib.Client) { + chatId := config.GetChatId() + if chatId == 0 { + uName := config.GetChatUsername() + if uName == "" { + log.Fatal("Username should not empty.") + } + s, err := cl.SearchPublicChat(uName) + if err != nil { + log.Fatal("SearchPublicChat error:", err) + } + _, err = cl.GetChat(s.Id) + if err != nil { + log.Fatal("GetChat error:", err) + } + config.SetChatId(s.Id) + config.SaveConfig() + } else { + _, err := cl.GetChat(config.GetChatId()) + if err != nil { + log.Fatal("GetChat error:", err) + } + } +} diff --git a/telegram/limit.go b/telegram/limit.go new file mode 100644 index 0000000..71034a3 --- /dev/null +++ b/telegram/limit.go @@ -0,0 +1,51 @@ +package telegram + +import ( + "fmt" + "time" + + "github.com/beefsack/go-rate" + "github.com/c0re100/RadioBot/config" + "github.com/c0re100/go-tdlib" +) + +var ( + pageLimit = make(map[int32]*rate.RateLimiter) + reqLimit = make(map[int32]*rate.RateLimiter) +) + +func canSelectPage(chatId int64, queryId tdlib.JSONInt64) bool { + var cId int32 + + if chatId == config.GetChatId() { + cId = -1000 + if pageLimit[cId] == nil { + pageLimit[cId] = rate.New(config.GetChatSelectLimit(), 1*time.Minute) + } + } else if chatId > 0 { + cId = int32(chatId) + if pageLimit[cId] == nil { + pageLimit[cId] = rate.New(config.GetPrivateChatSelectLimit(), 1*time.Minute) + } + } else { + return false + } + + if ok, dur := pageLimit[cId].Try(); !ok { + sec := int32(dur.Seconds()) + bot.AnswerCallbackQuery(queryId, fmt.Sprintf("Rate limited! Please try again in %v seconds~", sec), false, "", sec) + return false + } + return true +} + +func canReqSong(userId int32) (bool, int) { + if reqLimit[userId] != nil { + ok, sec := reqLimit[userId].Try() + return ok, int(sec) + } else { + reqLimit[userId] = rate.New(config.GetReqSongLimit(), 1*time.Minute) + reqLimit[userId].Try() + return true, 0 + } +} diff --git a/telegram/message.go b/telegram/message.go new file mode 100644 index 0000000..a70386a --- /dev/null +++ b/telegram/message.go @@ -0,0 +1,60 @@ +package telegram + +import ( + "fmt" + + "github.com/c0re100/go-tdlib" +) + +func newMessages() { + fmt.Println("[Music] New Message Receiver") + eventFilter := func(msg *tdlib.TdMessage) bool { + return true + } + + receiver := bot.AddEventReceiver(&tdlib.UpdateNewMessage{}, eventFilter, 100) + for newMsg := range receiver.Chan { + go func(newMsg tdlib.TdMessage) { + updateMsg := (newMsg).(*tdlib.UpdateNewMessage) + chatId := updateMsg.Message.ChatId + msgId := updateMsg.Message.Id + senderId := GetSenderId(updateMsg.Message.Sender) + var msgText string + var msgEnt []tdlib.TextEntity + + switch updateMsg.Message.Content.GetMessageContentEnum() { + case "messageText": + msgText = updateMsg.Message.Content.(*tdlib.MessageText).Text.Text + msgEnt = updateMsg.Message.Content.(*tdlib.MessageText).Text.Entities + case "messageChatJoinByLink": + bot.DeleteMessages(chatId, []int64{msgId}, true) + case "messageChatAddMembers", "messageChatDeleteMember": + bot.DeleteMessages(chatId, []int64{msgId}, true) + } + + command := CheckCommand(msgText, msgEnt) + switch { + case command == "/request": + sendButtonMessage(chatId, msgId) + case command == "/current": + getCurrentPlaying(chatId, msgId) + case command == "/skip": + startVote(chatId, msgId, int32(senderId)) + case command == "/search" || command == "/nom": + nominate(chatId, msgId, int32(senderId), CommandArgument(msgText)) + case command == "/play": + playerControl(chatId, int32(senderId), 0) + case command == "/stop": + playerControl(chatId, int32(senderId), 1) + case command == "/pause": + playerControl(chatId, int32(senderId), 2) + case command == "/random": + playerControl(chatId, int32(senderId), 3) + case command == "/reload": + reload(chatId, msgId, int32(senderId)) + case command == "/loadptcps": + loadParticipants(chatId, int32(senderId)) + } + }(newMsg) + } +} diff --git a/telegram/parser.go b/telegram/parser.go new file mode 100644 index 0000000..a9865c4 --- /dev/null +++ b/telegram/parser.go @@ -0,0 +1,49 @@ +package telegram + +import ( + "strings" + + "github.com/c0re100/go-tdlib" +) + +func CheckCommand(msgText string, entity []tdlib.TextEntity) string { + if msgText != "" { + if msgText[0] == '/' { + if len(entity) >= 1 { + if entity[0].Type.GetTextEntityTypeEnum() == "textEntityTypeBotCommand" { + if i := strings.Index(msgText[:entity[0].Length], "@"); i != -1 { + return msgText[:i] + } + return msgText[:entity[0].Length] + } + } + if len(msgText) > 1 { + if i := strings.Index(msgText, "@"); i != -1 { + return msgText[:i] + } + if i := strings.Index(msgText, " "); i != -1 { + return msgText[:i] + } + return msgText + } + } + } + return "" +} + +func CommandArgument(msgText string) string { + if msgText[0] == '/' { + if i := strings.Index(msgText, " "); i != -1 { + return msgText[i+1:] + } + } + return "" +} + +func GetSenderId(sender tdlib.MessageSender) int64 { + if sender.GetMessageSenderEnum() == "messageSenderUser" { + return int64(sender.(*tdlib.MessageSenderUser).UserId) + } else { + return sender.(*tdlib.MessageSenderChat).ChatId + } +} diff --git a/telegram/scheduler.go b/telegram/scheduler.go new file mode 100644 index 0000000..c187881 --- /dev/null +++ b/telegram/scheduler.go @@ -0,0 +1,38 @@ +package telegram + +import ( + "log" + "time" + + "github.com/go-co-op/gocron" +) + +var ( + sch = gocron.NewScheduler(time.UTC) +) + +func startScheduler() { + sch.StartAsync() +} + +func stopScheduler() { + sch.Stop() +} + +func addVoteJob(chatId, msgId int64) { + timeLeftJob, err := sch.Every(15).Second().Do(updateVote, chatId, msgId, true) + if err != nil { + log.Println("error creating job:", err) + return + } + timeLeftJob.LimitRunsTo(3) + timeLeftJob.RemoveAfterLastRun() +} + +func addLoadGroupCallPtpcsJob() { + _, err := sch.Every(1).Minute().Do(loadParticipants) + if err != nil { + log.Println("error creating job:", err) + return + } +} diff --git a/telegram/search.go b/telegram/search.go new file mode 100644 index 0000000..d55b741 --- /dev/null +++ b/telegram/search.go @@ -0,0 +1,100 @@ +package telegram + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/c0re100/RadioBot/config" + "github.com/c0re100/go-tdlib" +) + +func searchSong(text string) map[int]string { + var list = make(map[int]string) + for i, s := range songList { + if strings.Contains(strings.ToLower(s), strings.ToLower(text)) { + list[i] = s + } + } + return list +} + +func createSearchSongListButton(list map[int]string, offset int) [][]tdlib.InlineKeyboardButton { + var songKb [][]tdlib.InlineKeyboardButton + + if offset > len(list) { + return songKb + } + + keys := make([]int, 0, len(list)) + for k := range list { + keys = append(keys, k) + } + sort.Ints(keys) + + count := 0 + for _, k := range keys[offset:] { + if count >= config.GetRowLimit() { + break + } + if list[k] == "" { + continue + } + songKb = append(songKb, []tdlib.InlineKeyboardButton{*tdlib.NewInlineKeyboardButton(list[k], tdlib.NewInlineKeyboardButtonTypeCallback([]byte("select_song:"+strconv.Itoa(k))))}) + count++ + } + + return songKb +} + +func sendCustomButtonMessage(chatId, msgId int64, list map[int]string) { + var format *tdlib.FormattedText + if chatId < 0 { + text := fmt.Sprintf("Result: %v matches\n"+ + "Which song do you want to play?\n"+ + "\n"+ + "Use Private Chat to request a song WHEN you exceeded rate-limit.", len(list)) + format, _ = bot.ParseTextEntities(text, tdlib.NewTextParseModeHTML()) + } else { + format = tdlib.NewFormattedText(fmt.Sprintf("Result: %v matches\nWhich song do you want to play?", len(list)), nil) + } + text := tdlib.NewInputMessageText(format, false, false) + songKb := createSearchSongListButton(list, 0) + + var kb *tdlib.ReplyMarkupInlineKeyboard + if len(list) > 0 { + kb = finalizeButton(songKb, 0, true) + } else { + kb = tdlib.NewReplyMarkupInlineKeyboard(songKb) + } + + bot.SendMessage(chatId, 0, msgId, tdlib.NewMessageSendOptions(false, true, nil), kb, text) +} + +func editCustomButtonMessage(chatId int64, m *tdlib.Message, queryId tdlib.JSONInt64, offset int) { + if canSelectPage(chatId, queryId) { + var format *tdlib.FormattedText + if chatId < 0 { + text := "Which song do you want to play?" + + "\n\n" + + "Use Private Chat to request a song WHEN you exceeded rate-limit." + format, _ = bot.ParseTextEntities(text, tdlib.NewTextParseModeHTML()) + } else { + format = tdlib.NewFormattedText("Which song do you want to play?", nil) + } + text := tdlib.NewInputMessageText(format, false, false) + m2, err := bot.GetMessage(chatId, m.ReplyToMessageId) + if err != nil { + return + } + switch m2.Content.GetMessageContentEnum() { + case "messageText": + msgText := m2.Content.(*tdlib.MessageText).Text.Text + list := searchSong(CommandArgument(msgText)) + songKb := createSearchSongListButton(list, offset) + kb := finalizeButton(songKb, offset, true) + bot.EditMessageText(chatId, m.Id, kb, text) + } + } +} diff --git a/telegram/vote.go b/telegram/vote.go new file mode 100644 index 0000000..a2e9ab1 --- /dev/null +++ b/telegram/vote.go @@ -0,0 +1,229 @@ +package telegram + +import ( + "fmt" + "log" + "time" + + "github.com/c0re100/RadioBot/config" + "github.com/c0re100/RadioBot/fb2k" + "github.com/c0re100/RadioBot/utils" + "github.com/c0re100/go-tdlib" +) + +type groupStatus struct { + chatId int64 + msgId int64 + vcId int32 + duartion int32 + Ptcps []int32 + voteSkip []int32 + isVoting bool + isLoadPtcps bool + lastVoteTime int64 +} + +var ( + grpStatus = &groupStatus{chatId: config.GetChatId()} +) + +func GetQueue() []int { + return config.GetStatus().GetQueue() +} + +func GetRecent() []int { + return config.GetStatus().GetRecent() +} + +func startVote(chatId, msgId int64, userId int32) { + if chatId != config.GetChatId() { + return + } + + if !config.IsVoteEnabled() { + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText("This group is not allowed to vote.", nil), true, true) + bot.SendMessage(chatId, 0, msgId, nil, nil, msgText) + return + } + + if !config.IsWebEnabled() { + c, err := userBot.GetChat(chatId) + if err != nil { + log.Println(err) + return + } + + if c.VoiceChatGroupCallId == 0 { + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText("This group do not have a voice chat.", nil), true, true) + bot.SendMessage(chatId, 0, msgId, nil, nil, msgText) + return + } + // Preload all users + _, _ = userBot.LoadGroupCallParticipants(c.VoiceChatGroupCallId, 5000) + } + + if !utils.ContainsInt32(grpStatus.Ptcps, userId) { + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText("Only users which are in a voice chat can vote!", nil), true, true) + bot.SendMessage(chatId, 0, msgId, nil, nil, msgText) + return + } + + if grpStatus.isVoting { + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText("Vote in progress...", nil), true, true) + bot.SendMessage(chatId, 0, msgId, nil, nil, msgText) + return + } + + if time.Now().Unix() < grpStatus.lastVoteTime+config.GetReleaseTime() { + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText("Skip a song was voted too recently...", nil), true, true) + bot.SendMessage(chatId, 0, msgId, nil, nil, msgText) + return + } + + voteKb := tdlib.NewReplyMarkupInlineKeyboard([][]tdlib.InlineKeyboardButton{ + []tdlib.InlineKeyboardButton{ + *tdlib.NewInlineKeyboardButton("Yes - 1", tdlib.NewInlineKeyboardButtonTypeCallback([]byte("vote_skip"))), + }, + }) + + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText("Skip a song?", nil), true, true) + m, err := bot.SendMessage(chatId, 0, msgId, nil, voteKb, msgText) + if err != nil { + log.Println("Can't send message.") + return + } + grpStatus.isVoting = true + grpStatus.duartion = config.GetVoteTime() + grpStatus.msgId = m.Id + grpStatus.lastVoteTime = time.Now().Unix() + + if !utils.ContainsInt32(grpStatus.voteSkip, userId) { + grpStatus.voteSkip = append(grpStatus.voteSkip, userId) + } + updateVote(chatId, m.Id, false) + addVoteJob(chatId, m.Id) + // Wait 15 seconds + time.Sleep(15 * time.Second) + if !sch.IsRunning() { + log.Println("Starting scheduler...") + startScheduler() + } +} + +func updateVote(chatId, msgId int64, isAuto bool) { + if isAuto { + grpStatus.duartion -= config.GetUpdateTime() + } + if grpStatus.duartion <= 0 { + endVote(chatId, msgId) + return + } + voteKb := tdlib.NewReplyMarkupInlineKeyboard([][]tdlib.InlineKeyboardButton{ + []tdlib.InlineKeyboardButton{ + *tdlib.NewInlineKeyboardButton(fmt.Sprintf("Yes - %v", len(grpStatus.voteSkip)), tdlib.NewInlineKeyboardButtonTypeCallback([]byte("vote_skip"))), + }, + }) + + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText(fmt.Sprintf("Skip a song?\n"+ + "Vote count: %v\n"+ + "Vote timeleft: %v second(s)", len(grpStatus.voteSkip), grpStatus.duartion), nil), true, true) + bot.EditMessageText(chatId, msgId, voteKb, msgText) +} + +func resetVote() { + grpStatus.isLoadPtcps = false + grpStatus.isVoting = false + grpStatus.duartion = 0 + grpStatus.voteSkip = []int32{} +} + +func finalizeVote(chatId, msgId int64, ptcpCount int32) { + percentage := float64(len(grpStatus.voteSkip)) / float64(ptcpCount) * 100 + + status := "Failed" + if percentage >= config.GetSuccessRate() { + status = "Succeed" + } + + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText(fmt.Sprintf("Skip a song?\n"+ + "Vote count: %v\n"+ + "Vote Ended!\n\n"+ + "Status: %v", len(grpStatus.voteSkip), status), nil), true, true) + bot.EditMessageText(chatId, msgId, nil, msgText) + + resetVote() + if status == "Succeed" { + fb2k.SetKillSwitch() + if len(GetQueue()) == 0 { + fb2k.PlayNext() + } else { + fb2k.PlaySelected(GetQueue()[0]) + } + } +} + +func endVote(chatId, msgId int64) { + vs := grpStatus + msgText := tdlib.NewInputMessageText(tdlib.NewFormattedText(fmt.Sprintf("Skip a song?\n"+ + "Vote count: %v\n"+ + "Vote Ended!\n\n"+ + "Status: Generating vote results...", len(vs.voteSkip)), nil), true, true) + bot.EditMessageText(chatId, vs.msgId, nil, msgText) + + if !config.IsWebEnabled() { + c, err := userBot.GetChat(chatId) + if err != nil { + resetVote() + log.Println(err) + return + } + if c.VoiceChatGroupCallId == 0 { + resetVote() + log.Println("No group call currently.") + return + } + vc, err := userBot.GetGroupCall(c.VoiceChatGroupCallId) + if err != nil { + resetVote() + log.Println(err) + return + } + finalizeVote(chatId, msgId, vc.ParticipantCount) + } else { + finalizeVote(chatId, msgId, int32(len(grpStatus.Ptcps))) + } +} + +func setUserVote(chatId, msgId int64, userId int32, queryId tdlib.JSONInt64) { + if config.IsJoinNeeded() { + cm, err := bot.GetChatMember(config.GetChatId(), userId) + if err != nil { + bot.AnswerCallbackQuery(queryId, "Failed to fetch chat info! Please try again later~", true, "", 10) + return + } + + if cm.Status.GetChatMemberStatusEnum() == "chatMemberStatusLeft" { + bot.AnswerCallbackQuery(queryId, "Only users which are in the group can vote!", true, "", 10) + return + } + } + + if utils.ContainsInt32(grpStatus.voteSkip, userId) { + bot.AnswerCallbackQuery(queryId, "You're already vote!", false, "", 45) + return + } + + if config.IsPtcpsOnly() { + bot.AnswerCallbackQuery(queryId, "Only users which are in a voice chat can vote!", false, "", 5) + return + } + + AddVote(userId) + updateVote(chatId, msgId, false) +} + +func AddVote(userId int32) { + if !utils.ContainsInt32(grpStatus.voteSkip, userId) { + grpStatus.voteSkip = append(grpStatus.voteSkip, userId) + } +} diff --git a/utils/format.go b/utils/format.go new file mode 100644 index 0000000..25ec5d8 --- /dev/null +++ b/utils/format.go @@ -0,0 +1,49 @@ +package utils + +import "fmt" + +func SecondsToMinutes(sec int64) string { + seconds := sec % 60 + minutes := sec / 60 + str := fmt.Sprintf("%02d:%02d", minutes, seconds) + return str +} + +func IsEmpty(text string) string { + if text == "" { + return "Unknown" + } + return text +} + +func ContainsInt(s []int, v int) bool { + for _, vv := range s { + if vv == v { + return true + } + } + return false +} + +func ContainsInt32(s []int32, v int32) bool { + for _, vv := range s { + if vv == v { + return true + } + } + return false +} + +func FilterInt32(s []int32, cb func(s int32) bool) []int32 { + results := []int32{} + + for _, i := range s { + result := cb(i) + + if result { + results = append(results, i) + } + } + + return results +} diff --git a/utils/struct.go b/utils/struct.go new file mode 100644 index 0000000..b5d905c --- /dev/null +++ b/utils/struct.go @@ -0,0 +1,30 @@ +package utils + +type Event struct { + Player struct { + ActiveItem struct { + Columns []string `json:"columns"` + Duration float64 `json:"duration"` + Index int `json:"index"` + PlaylistID string `json:"playlistId"` + PlaylistIndex int `json:"playlistIndex"` + Position float64 `json:"position"` + } `json:"activeItem"` + Info struct { + Name string `json:"name"` + PluginVersion string `json:"pluginVersion"` + Title string `json:"title"` + Version string `json:"version"` + } `json:"info"` + PlaybackMode int `json:"playbackMode"` + PlaybackModes []string `json:"playbackModes"` + PlaybackState string `json:"playbackState"` + Volume struct { + IsMuted bool `json:"isMuted"` + Max float64 `json:"max"` + Min float64 `json:"min"` + Type string `json:"type"` + Value float64 `json:"value"` + } `json:"volume"` + } `json:"player"` +} diff --git a/web/web.go b/web/web.go new file mode 100644 index 0000000..97c61f9 --- /dev/null +++ b/web/web.go @@ -0,0 +1,80 @@ +package web + +import ( + "fmt" + "log" + "net/http" + "strconv" + + "github.com/c0re100/RadioBot/config" + "github.com/c0re100/RadioBot/telegram" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +var ( + server *echo.Echo +) + +func StartServer() { + if !config.IsWebEnabled() { + fmt.Println("Switching to Userbot mode") + return + } + + // Check Port format + port := config.GetWebPort() + if port < 1024 || port > 65535 { + log.Fatal("Port range: 1024-65535, but current port is ", port) + } + + server = echo.New() + + //server.Use(middleware.Logger()) + server.Use(middleware.Recover()) + + server.GET("/", hello) + server.POST("/ptcp", recvPtcp) + server.POST("/reset", resetPtcps) + + server.Logger.Fatal(server.Start(":" + strconv.Itoa(port))) +} + +func hello(c echo.Context) error { + return c.String(http.StatusOK, "Hello World!") +} + +func recvPtcp(c echo.Context) error { + status := c.FormValue("is_join") + userId := c.FormValue("user_id") + + if status == "" { + return c.HTML(400, "Field `is_join` is empty.") + } else if userId == "" { + return c.HTML(400, "Field `user_id` is empty.") + } + + uId, err := strconv.Atoi(userId) + if err != nil { + return c.HTML(400, "Field `user_id` is empty.") + } + + if uId == 0 { + return c.HTML(400, "Field `user_id` is not accept 0.") + } + + if status == "true" { + telegram.AddPtcp(int32(uId)) + return c.HTML(200, "User added.") + } else if status == "false" { + telegram.RemovePtcp(int32(uId)) + return c.HTML(200, "User removed.") + } else { + return c.HTML(400, "Field `user_id` is empty or wrong type.") + } +} + +func resetPtcps(c echo.Context) error { + telegram.ResetPtcps() + return c.HTML(200, "Resetted.") +} diff --git a/wrtc/connection.go b/wrtc/connection.go new file mode 100644 index 0000000..530fd21 --- /dev/null +++ b/wrtc/connection.go @@ -0,0 +1,58 @@ +package wrtc + +import ( + "fmt" + "strconv" + + "github.com/c0re100/RadioBot/config" + "github.com/c0re100/go-tdlib" + "github.com/pion/webrtc/v2" +) + +func Connect(resp *tdlib.GroupCallJoinResponse, d *data) { + rSdp := createOfferSdp(resp, strconv.FormatInt(d.Ssrc, 10)) + rOffer := webrtc.SessionDescription{ + Type: webrtc.SDPTypeAnswer, + SDP: rSdp, + } + err := peerConnection.SetRemoteDescription(rOffer) + if err != nil { + panic(err) + } + + _, err = peerConnection.CreateAnswer(nil) + if err != nil { + panic(err) + } + + select { + case <-closeRTC: + peerConnection.Close() + fmt.Println("WebRTC connection closed.") + default: + + } +} + +func Disconnnect() { + if !config.IsWebEnabled() { + closeRTC <- true + c, _ := userBot.GetChat(config.GetChatId()) + gc, _ := userBot.GetGroupCall(c.VoiceChatGroupCallId) + userBot.LeaveGroupCall(gc.Id) + } +} + +func GetConnection() *webrtc.PeerConnection { + return peerConnection +} + +func GetCurrentSDP() string { + if peerConnection.LocalDescription() != nil { + return peerConnection.LocalDescription().SDP + } else if peerConnection.PendingLocalDescription() != nil { + return peerConnection.PendingLocalDescription().SDP + } else { + return "" + } +} diff --git a/wrtc/media.go b/wrtc/media.go new file mode 100644 index 0000000..7b019bc --- /dev/null +++ b/wrtc/media.go @@ -0,0 +1,37 @@ +package wrtc + +import ( + "fmt" + "math/rand" + + "github.com/pion/webrtc/v2" +) + +func setupMedia() { + id := rand.Uint32() + for { + if id >= 2147483647 { + id = rand.Uint32() + } else { + break + } + } + audioTrack, addTrackErr := peerConnection.NewTrack(getPayloadType(mediaEngine, webrtc.RTPCodecTypeAudio, "opus"), id, "audio", "radio") + if addTrackErr != nil { + panic(addTrackErr) + } + if _, addTrackErr = peerConnection.AddTransceiverFromTrack(audioTrack, webrtc.RtpTransceiverInit{ + Direction: webrtc.RTPTransceiverDirectionSendrecv, + }); addTrackErr != nil { + panic(addTrackErr) + } +} + +func getPayloadType(m webrtc.MediaEngine, codecType webrtc.RTPCodecType, codecName string) uint8 { + for _, codec := range m.GetCodecsByKind(codecType) { + if codec.Name == codecName { + return codec.PayloadType + } + } + panic(fmt.Sprintf("Remote peer does not support %s", codecName)) +} diff --git a/wrtc/offer.go b/wrtc/offer.go new file mode 100644 index 0000000..f8a2138 --- /dev/null +++ b/wrtc/offer.go @@ -0,0 +1,109 @@ +package wrtc + +import ( + "strconv" + "strings" + + "github.com/c0re100/go-tdlib" + "github.com/pion/webrtc/v2" +) + +type data struct { + UFrag string + Pwd string + Port string + Ssrc int64 + Cert []webrtc.Certificate + Offer string +} + +func extractDesc(pc *webrtc.PeerConnection, sdp string) *data { + var lines []string + if sdp != "" { + lines = strings.Split(sdp, "\n") + } else if peerConnection.LocalDescription() != nil { + lines = strings.Split(peerConnection.LocalDescription().SDP, "\n") + } else if peerConnection.PendingLocalDescription() != nil { + lines = strings.Split(peerConnection.PendingLocalDescription().SDP, "\n") + } + var ufrag, pwd, port string + var ssrc int64 + + for _, s := range lines { + if strings.Contains(s, "a=ice-ufrag:") { + ufrag = strings.Split(s, "a=ice-ufrag:")[1] + } + if strings.Contains(s, "a=ice-pwd:") { + pwd = strings.Split(s, "a=ice-pwd:")[1] + } + if strings.Contains(s, "a=ssrc:") { + ssrc, _ = strconv.ParseInt(strings.Split(strings.Split(s, "a=ssrc:")[1], " ")[0], 10, 64) + } + if strings.Contains(s, "a=candidate:foundation 1 udp 2130706431 192.168.0.100 ") { + port = strings.Split(strings.Split(s, "a=candidate:foundation 1 udp 2130706431 192.168.0.100 ")[1], " typ")[0] + } + } + + return &data{ + UFrag: ufrag, + Pwd: pwd, + Port: port, + Ssrc: ssrc, + Cert: pc.GetConfiguration().Certificates, + Offer: pc.PendingLocalDescription().SDP, + } +} + +func createOfferSdp(resp *tdlib.GroupCallJoinResponse, ssrc string) string { + var offerSdp string + + offerSdp += "v=0\n" + offerSdp += "o=- 6543245 2 IN IP4 0.0.0.0\n" + offerSdp += "s=-\n" + offerSdp += "t=0 0\n" + offerSdp += "a=group:BUNDLE 0\n" + offerSdp += "a=ice-lite\n" + offerSdp += "m=audio 1 UDP/TLS/RTP/SAVPF 111\n" + offerSdp += "c=IN IP4 0.0.0.0\n" + offerSdp += "a=mid:0\n" + if resp != nil { + offerSdp += "a=ice-ufrag:" + resp.Payload.Ufrag + "\n" + offerSdp += "a=ice-pwd:" + resp.Payload.Pwd + "\n" + for _, f := range resp.Payload.Fingerprints { + offerSdp += "a=fingerprint:sha-256 " + f.Fingerprint + "\n" + } + offerSdp += "a=setup:passive\n" + for i, c := range resp.Candidates { + offerSdp += "a=candidate:" + strconv.Itoa(i+1) + " 1 udp " + c.Priority + " " + c.Ip + " " + c.Port + " typ host generation 0\n" + } + } + offerSdp += "a=rtpmap:111 opus/48000/2\n" + offerSdp += "a=rtcp-fb:111 transport-cc\n" + offerSdp += "a=fmtp:111 minptime=10; useinbandfec=1\n" + offerSdp += "a=rtcp:1 IN IP4 0.0.0.0\n" + offerSdp += "a=rtcp-mux\n" + offerSdp += "a=recvonly\n" + + return offerSdp +} + +func createLocalSdp(resp *tdlib.GroupCallJoinResponse, offer string) string { + var offerSdp string + + o := strings.Split(offer, "\n") + for _, s := range o { + if strings.Contains(s, "a=fingerprint:sha-256") { + for _, f := range resp.Payload.Fingerprints { + offerSdp += "a=fingerprint:sha-256 " + f.Fingerprint + "\n" + } + } else if strings.Contains(s, "a=ice-ufrag:") { + offerSdp += "a=ice-ufrag:" + resp.Payload.Ufrag + "\n" + } else if strings.Contains(s, "a=ice-pwd:") { + offerSdp += "a=ice-pwd:" + resp.Payload.Pwd + "\n" + } else { + offerSdp += s + "\n" + } + } + + return offerSdp +} diff --git a/wrtc/webrtc.go b/wrtc/webrtc.go new file mode 100644 index 0000000..b71909f --- /dev/null +++ b/wrtc/webrtc.go @@ -0,0 +1,52 @@ +package wrtc + +import ( + "fmt" + "log" + + "github.com/c0re100/go-tdlib" + "github.com/pion/webrtc/v2" +) + +var ( + peerConnection *webrtc.PeerConnection + mediaEngine webrtc.MediaEngine + closeRTC = make(chan bool, 1) + userBot *tdlib.Client +) + +func setup() { + mediaEngine = webrtc.MediaEngine{} + peerConnection = &webrtc.PeerConnection{} + mediaEngine.RegisterCodec(webrtc.NewRTPOpusCodec(111, 48000)) + api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine)) + peerConnection, _ = api.NewPeerConnection(webrtc.Configuration{}) + setupMedia() +} + +func CreateOffer(bot *tdlib.Client) *data { + setup() + userBot = bot + + peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { + log.Printf("Connection State has changed %s \n", connectionState.String()) + }) + + peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { + if i != nil { + fmt.Println(i.ToJSON()) + } + }) + + offer, err := peerConnection.CreateOffer(nil) + if err != nil { + panic(err) + } + + err = peerConnection.SetLocalDescription(offer) + if err != nil { + panic(err) + } + + return extractDesc(peerConnection, "") +}