This commit is contained in:
poiuty 2022-07-27 11:28:07 +03:00
commit fec669ea34
10 changed files with 967 additions and 0 deletions

117
app/cmd.go Normal file
View file

@ -0,0 +1,117 @@
package main
import (
"fmt"
"net/http"
"net/url"
"runtime"
"strings"
"sync"
// "time"
)
type Info struct {
room string
Server string `json:"server"`
Proxy string `json:"proxy"`
Start int64 `json:"start"`
Last int64 `json:"last"`
Income int64 `json:"income"`
}
type Debug struct {
Goroutines int
Alloc uint64
HeapSys uint64
Uptime int64
}
type Worker struct {
chQuit chan struct{}
}
type Workers struct {
sync.RWMutex
Map map[string]*Worker
}
var (
memInfo runtime.MemStats
chWorker = &Workers{Map: make(map[string]*Worker)}
)
func removeRoom(room string) {
if checkWorker(room) {
chWorker.Lock()
//fmt.Printf("%v remove %v from chWorker.Map \n", time.Now().UnixMilli(), room )
delete(chWorker.Map, room)
chWorker.Unlock()
}
}
func checkWorker(room string) bool {
chWorker.RLock()
defer chWorker.RUnlock()
if _, ok := chWorker.Map[room]; ok {
return true
}
return false
}
func listRooms() string {
rooms.Json <- ""
s := <-rooms.Json
return s
}
func listHandler(w http.ResponseWriter, _ *http.Request) {
fmt.Fprint(w, listRooms())
}
func debugHandler(w http.ResponseWriter, _ *http.Request) {
runtime.ReadMemStats(&memInfo)
j, err := json.Marshal(Debug{runtime.NumGoroutine(), memInfo.Alloc, memInfo.HeapSys, uptime})
if err == nil {
fmt.Fprint(w, string(j))
}
}
func cmdHandler(w http.ResponseWriter, r *http.Request) {
if !conf.List[r.Header.Get("X-REAL-IP")] {
fmt.Fprint(w, "403")
return
}
params := r.URL.Query()
if len(params["room"]) > 0 && len(params["server"]) > 0 && len(params["proxy"]) > 0 {
room := params["room"][0]
server := params["server"][0]
proxy := params["proxy"][0]
if checkWorker(room) {
fmt.Println("Already track:", room)
return
}
info, ok := getRoomInfo(room)
if !ok {
fmt.Println("No room in MySQL:", room)
return
}
chQuit := make(chan struct{})
chWorker.Lock()
chWorker.Map[room] = &Worker{chQuit: chQuit}
chWorker.Unlock()
go statRoom(chQuit, room, server, proxy, info, url.URL{Scheme: "wss", Host: server + ".bcccdn.com", Path: "/websocket"})
}
if len(params["exit"]) > 0 {
room := strings.Join(params["exit"], "")
if checkWorker(room) {
close(chWorker.Map[room].chQuit) // exit gorutine
removeRoom(room)
}
}
}

33
app/conf.go Normal file
View file

@ -0,0 +1,33 @@
package main
type Conf struct {
Conn map[string]string
Proxy map[string]string
List map[string]bool
}
var conf = &Conf{
Conn: make(map[string]string),
Proxy: make(map[string]string),
List: make(map[string]bool),
}
func startConfig() {
// DB
conf.Conn = map[string]string{
"mysql": "user:passwd@unix(/var/run/mysqld/mysqld.sock)/base?interpolateParams=true",
"click": "tcp://127.0.0.1:9000/base?compress=true&debug=false",
}
// 3proxy
conf.Proxy = map[string]string{
"us": "ip:port",
"fi": "ip:port",
}
// allow ips
conf.List = map[string]bool{
"::1": true,
"127.0.0.1": true,
}
}

89
app/echo.go Normal file
View file

@ -0,0 +1,89 @@
package main
import (
"github.com/gorilla/websocket"
"net/http"
//"fmt"
)
func newHub() *Hub {
return &Hub{
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
}
type Hub struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
}
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case message := <-h.broadcast:
//fmt.Println("map channel:", len(h.broadcast), cap(h.broadcast))
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}
func (c *Client) writePump() {
for {
message, ok := <-c.send
if !ok {
// The hub closed the channel.
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
c.conn.WriteMessage(1, message)
}
c.conn.Close()
}
func (c *Client) readPump() {
for {
// Client close connection
_, _, err := c.conn.ReadMessage()
if err != nil {
break
}
}
c.hub.unregister <- c
c.conn.Close()
}
func (hub *Hub) wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Upgrade(w, r, w.Header(), 1024, 1024)
if err != nil {
return
}
client := &Client{hub: hub, conn: conn, send: make(chan []byte)}
client.hub.register <- client
go client.readPump()
go client.writePump()
}

86
app/main.go Normal file
View file

@ -0,0 +1,86 @@
package main
import (
"log"
"math/rand"
"net"
"net/http"
"os"
"time"
_ "github.com/ClickHouse/clickhouse-go"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
jsoniter "github.com/json-iterator/go"
)
type Rooms struct {
Count chan int
Json chan string
Add chan Info
Del chan string
}
var hub = newHub()
var Mysql, Clickhouse *sqlx.DB
var json = jsoniter.ConfigCompatibleWithStandardLibrary
var save = make(chan saveData, 100)
var slog = make(chan saveLog, 100)
var rooms = &Rooms{
Count: make(chan int),
Json: make(chan string),
Add: make(chan Info),
Del: make(chan string),
}
func main() {
rand.Seed(time.Now().UnixNano())
startConfig()
initMysql()
initClickhouse()
go hub.run()
go mapRooms()
go announceCount()
go saveDB()
go saveLogs()
http.HandleFunc("/bongacams/ws/", hub.wsHandler)
http.HandleFunc("/bongacams/cmd/", cmdHandler)
http.HandleFunc("/bongacams/list/", listHandler)
http.HandleFunc("/bongacams/debug/", debugHandler)
const SOCK = "/tmp/bongacams.sock"
os.Remove(SOCK)
unixListener, err := net.Listen("unix", SOCK)
if err != nil {
log.Fatal("Listen (UNIX socket): ", err)
}
defer unixListener.Close()
os.Chmod(SOCK, 0777)
log.Fatal(http.Serve(unixListener, nil))
}
func initMysql() {
db, err := sqlx.Connect("mysql", conf.Conn["mysql"])
if err != nil {
panic(err)
}
Mysql = db
}
func initClickhouse() {
db, err := sqlx.Connect("clickhouse", conf.Conn["click"])
if err != nil {
panic(err)
}
Clickhouse = db
}
func randInt(min int, max int) int {
return min + rand.Intn(max-min)
}

143
app/save.go Normal file
View file

@ -0,0 +1,143 @@
package main
import (
"fmt"
"time"
)
type tID struct {
Id int64 `db:"id"`
}
type saveData struct {
Room string
From string
Rid int64
Amount int64
Now int64
}
type saveLog struct {
Rid int64
Now int64
Mes string
}
type DonatorCache struct {
Id int64
Last int64
}
func getDonId(name string) int64 {
donator := new(tID)
err := Mysql.Get(donator, "SELECT id FROM donator WHERE name=?", name)
if err != nil {
res, _ := Mysql.Exec("INSERT INTO donator (`name`) VALUES (?)", name)
donator.Id, _ = res.LastInsertId()
}
return donator.Id
}
func getRoomInfo(name string) (*tID, bool) {
result := true
room := new(tID)
err := Mysql.Get(room, "SELECT id FROM room WHERE name=?", name)
if err != nil {
result = false
}
return room, result
}
func saveDB() {
last := time.Now().Unix()
bulk := make(map[int]saveData)
data := make(map[string]*DonatorCache)
for {
select {
case m := <-save:
//fmt.Println("Save channel:", len(save), cap(save))
now := time.Now().Unix()
if _, ok := data[m.From]; ok {
data[m.From].Last = now
} else {
data[m.From] = &DonatorCache{Id: getDonId(m.From), Last: now}
}
if randInt(0, 10000) == 777 { // 0.001%
l := len(data)
for k, v := range data {
if now > v.Last+60*60*48 {
delete(data, k)
}
}
fmt.Println("Clean map:", l, "=>", len(data))
}
Mysql.Exec("UPDATE `room` SET `last` = ? WHERE `id` = ?", m.Now, m.Rid)
num := len(bulk)
bulk[num] = m
if num >= 999 || now >= last+10 {
tx, err := Mysql.Begin()
if err == nil {
for _, v := range bulk {
tx.Exec("INSERT INTO `stat` (`did`, `rid`, `token`, `time`) VALUES (?, ?, ?, ?)", data[v.From].Id, v.Rid, v.Amount, v.Now)
}
}
tx.Commit()
tx, err = Clickhouse.Begin()
if err == nil {
st, _ := tx.Prepare("INSERT INTO stat VALUES (?, ?, ?, ?)")
//fmt.Println("G:", err)
for _, v := range bulk {
st.Exec(uint32(data[v.From].Id), uint32(v.Rid), uint32(v.Amount), time.Unix(v.Now, 0))
//fmt.Println("B:", aaa, sss)
}
tx.Commit()
st.Close()
}
last = now
bulk = make(map[int]saveData)
}
if m.Amount > 99 {
msg, err := json.Marshal(AnnounceDonate{Room: m.Room, Donator: m.From, Amount: m.Amount})
if err == nil {
hub.broadcast <- msg
}
}
}
}
}
func saveLogs() {
last := time.Now().Unix()
bulk := make(map[int]saveLog)
for {
select {
case m := <-slog:
if len(m.Mes) > 0 {
num := len(bulk)
bulk[num] = m
now := time.Now().Unix()
if num >= 2047 || now >= last+10 {
tx, err := Mysql.Begin()
if err == nil {
for _, v := range bulk {
tx.Exec("INSERT INTO `logs` (`rid`, `time`, `mes`) VALUES (?, ?, ?)", v.Rid, v.Now, v.Mes)
}
tx.Commit()
}
last = now
bulk = make(map[int]saveLog)
}
}
}
}
}

284
app/worker.go Normal file
View file

@ -0,0 +1,284 @@
package main
import (
"fmt"
"github.com/gorilla/websocket"
jsoniter "github.com/json-iterator/go"
"net/http"
"net/url"
"strings"
"time"
//"bytes"
)
var uptime = time.Now().Unix()
type AuthResponse struct {
Status string `json:"status"`
LocalData struct {
DataKey string `json:"dataKey"`
} `json:"localData"`
UserData struct {
Username string `json:"username"`
DisplayName string `json:"displayName"`
Location string `json:"location"`
Chathost string `json:"chathost"`
IsRu bool `json:"isRu"`
} `json:"userData"`
}
type ServerResponse struct {
TS int64 `json:"ts"`
Type string `json:"type"`
Body jsoniter.RawMessage `json:"body"`
}
type DonateResponse struct {
F struct {
Username string `json:"username"`
} `json:"f"`
A int64 `json:"a"`
}
type AnnounceCount struct {
Count int `json:"count"`
}
type AnnounceDonate struct {
Room string `json:"room"`
Donator string `json:"donator"`
Amount int64 `json:"amount"`
}
func mapRooms() {
data := make(map[string]*Info)
for {
select {
case m := <-rooms.Add:
data[m.room] = &Info{Server: m.Server, Proxy: m.Proxy, Start: m.Start, Last: m.Last, Income: m.Income}
case s := <-rooms.Json:
j, err := json.Marshal(data)
if err == nil {
s = string(j)
}
rooms.Json <- s
case <-rooms.Count:
rooms.Count <- len(data)
case key := <-rooms.Del:
delete(data, key)
removeRoom(key)
}
}
}
func announceCount() {
for {
time.Sleep(30 * time.Second)
rooms.Count <- 0
l := <-rooms.Count
msg, err := json.Marshal(AnnounceCount{Count: l})
if err == nil {
hub.broadcast <- msg
}
}
}
func getAMF(room string) (bool, *AuthResponse) {
v := &AuthResponse{}
req, err := http.NewRequest(http.MethodPost, "https://rt.bongocams.com/tools/amf.php?res=771840&t=1654437233142", strings.NewReader(`method=getRoomData&args[]=`+room))
if err != nil {
fmt.Println(err.Error())
return false, v
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
req.Header.Add("X-Requested-With", "XMLHttpRequest")
req.Header.Add("Accept", "application/json")
req.Header.Add("Referrer", "https://bongacams.com")
req.Header.Add("User-agent", "curl/7.79.1")
rsp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println(err.Error())
return false, v
}
defer rsp.Body.Close()
if err = json.NewDecoder(rsp.Body).Decode(v); err != nil {
fmt.Println(err.Error())
return false, v
}
return true, v
}
func statRoom(chQuit chan struct{}, room, server, proxy string, info *tID, u url.URL) {
//fmt.Println("Start", room, "server", server, "proxy", proxy)
ok, v := getAMF(room)
if !ok {
fmt.Println("exit: no amf parms")
return
}
Dialer := *websocket.DefaultDialer
if _, ok := conf.Proxy[proxy]; ok {
Dialer = websocket.Dialer{
Proxy: http.ProxyURL(&url.URL{
Scheme: "http", // or "https" depending on your proxy
Host: conf.Proxy[proxy],
Path: "/",
}),
HandshakeTimeout: 45 * time.Second, // https://pkg.go.dev/github.com/gorilla/websocket
}
}
now := time.Now().Unix()
workerData := Info{room, server, proxy, now, now, 0}
rooms.Add <- workerData
defer func() {
fmt.Println("defer remove map", room)
rooms.Del <- room
}()
c, _, err := Dialer.Dial(u.String(), nil)
if err != nil {
fmt.Println(err.Error(), room)
return
}
defer func() {
fmt.Println("defer close", room)
c.Close()
}()
c.SetReadDeadline(time.Now().Add(60 * time.Second))
fmt.Println("send first", room)
if err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf(`{"id":%d,"name":"joinRoom","args":["%s",{"username":"%s","displayName":"%s","location":"%s","chathost":"%s","isRu":%t,"isPerformer":false,"hasStream":false,"isLogged":false,"isPayable":false,"showType":"public"},"%s"]}`, 1, v.UserData.Chathost, v.UserData.Username, v.UserData.DisplayName, v.UserData.Location, v.UserData.Chathost, v.UserData.IsRu, v.LocalData.DataKey))); err != nil {
fmt.Println(err.Error())
return
}
fmt.Println("read first", room)
_, message, err := c.ReadMessage()
if err != nil {
fmt.Println(err.Error(), room)
return
}
fmt.Println(room, len(string(message)), string(message))
slog <- saveLog{info.Id, now, string(message)}
if string(message) == `{"id":1,"result":{"audioAvailable":false,"freeShow":false},"error":null}` {
fmt.Println("room offline, exit", room)
return
}
fmt.Println("send second", room)
if err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf(`{"id":%d,"name":"ChatModule.connect","args":["public-chat"]}`, 2))); err != nil {
fmt.Println(err.Error(), room)
return
}
fmt.Println("read second", room)
_, message, err = c.ReadMessage()
if err != nil {
fmt.Println(err.Error(), room)
return
}
fmt.Println(room, len(string(message)), string(message))
slog <- saveLog{info.Id, now, string(message)}
quit := make(chan bool)
pid := 3
defer func() {
fmt.Println("defer quit", room)
quit <- true
}()
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-quit:
return
case <-ticker.C:
if err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf(`{"id":%d,"name":"ping"}`, pid))); err != nil {
fmt.Println(err.Error(), room)
close(chWorker.Map[room].chQuit)
return
}
pid++
break
}
}
}()
for {
select {
case <-chQuit:
fmt.Println("Exit room:", room)
return
default:
c.SetReadDeadline(time.Now().Add(30 * time.Minute))
_, message, err := c.ReadMessage()
if err != nil {
fmt.Println(err.Error())
return
}
now = time.Now().Unix()
slog <- saveLog{info.Id, now, string(message)}
m := &ServerResponse{}
if err = json.Unmarshal(message, m); err != nil {
fmt.Println(err.Error(), room)
continue
}
workerData.Last = now
rooms.Add <- workerData
if m.Type == "ServerMessageEvent:PERFORMER_STATUS_CHANGE" && string(m.Body) == `"offline"` {
fmt.Println(m.Type, room)
return
}
if m.Type == "ServerMessageEvent:ROOM_CLOSE" {
fmt.Println(m.Type, room)
return
}
if m.Type == "ServerMessageEvent:INCOMING_TIP" {
d := &DonateResponse{}
if err = json.Unmarshal(m.Body, d); err == nil {
//fmt.Println(d.F.Username, " send ", d.A, "tokens")
save <- saveData{room, d.F.Username, info.Id, d.A, now}
workerData.Income += d.A
rooms.Add <- workerData
}
}
}
}
}