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, "")
+}