├── .gitattributes ├── .github ├── feat_ddos2 │ └── Снимок экрана 2025-01-16 в 21.31.58.png ├── img │ ├── feat_cloud.png │ ├── feat_ddos1.png │ ├── feat_ddos2.png │ └── feat_json.png └── workflows │ └── go.yml ├── .gitignore ├── .idea ├── .gitignore ├── HalogenGhostCore.iml ├── discord.xml └── modules.xml ├── README.md ├── _recover ├── Dockerfile └── recover.sh ├── docker-compose.yaml └── src ├── DOCS.md ├── Dockerfile ├── LICENSE ├── PLUGIN_DOCS.md ├── api ├── account.go ├── comment.go ├── communication.go ├── content.go ├── essential.go ├── level_levels.go ├── level_lists.go ├── level_packs.go ├── level_rates.go ├── profile.go ├── rewards.go ├── score.go ├── service.go ├── web_ballistics.go ├── web_cache.go ├── web_server.go └── web_templates.go ├── build.sh ├── core ├── CAccount.go ├── CAccount_structs.go ├── CComment.go ├── CFriendship.go ├── CHalogen.go ├── CLevel.go ├── CLevelFilter.go ├── CLevelList.go ├── CLevelListFilter.go ├── CMessage.go ├── CMusic.go ├── CProtect.go ├── CQuests.go ├── CScores.go ├── ConfigBlob.go ├── DBManagement.go ├── Routines.go ├── actions.go ├── connectors │ ├── GDConnector.go │ ├── JSONConnector.go │ ├── connector.go │ └── output.go ├── cryptography.go ├── modCommandProcessor.go ├── modules │ ├── PluginCore.go │ ├── PluginCore_test.go │ └── modDummy.go ├── sfxgen.go └── utils.go ├── go.mod ├── go.sum ├── main.go └── rpc └── server.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/feat_ddos2/Снимок экрана 2025-01-16 в 21.31.58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m41denx/GDPSGhostCore/b585da9c5a29331749c580d28eabe17c80ee97cf/.github/feat_ddos2/Снимок экрана 2025-01-16 в 21.31.58.png -------------------------------------------------------------------------------- /.github/img/feat_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m41denx/GDPSGhostCore/b585da9c5a29331749c580d28eabe17c80ee97cf/.github/img/feat_cloud.png -------------------------------------------------------------------------------- /.github/img/feat_ddos1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m41denx/GDPSGhostCore/b585da9c5a29331749c580d28eabe17c80ee97cf/.github/img/feat_ddos1.png -------------------------------------------------------------------------------- /.github/img/feat_ddos2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m41denx/GDPSGhostCore/b585da9c5a29331749c580d28eabe17c80ee97cf/.github/img/feat_ddos2.png -------------------------------------------------------------------------------- /.github/img/feat_json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m41denx/GDPSGhostCore/b585da9c5a29331749c580d28eabe17c80ee97cf/.github/img/feat_json.png -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version: 1.23 19 | 20 | - name: Log into registry 21 | run: docker login -u ${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }} cr.yandex 22 | 23 | - name: Build 24 | run: docker build -t cr.yandex/crpr24jcqm2dno6qlm3b/ghostcore src 25 | 26 | - name: Push 27 | run: docker push cr.yandex/crpr24jcqm2dno6qlm3b/ghostcore 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /_minio/ 3 | /_mysql/ 4 | /_redis/ 5 | .DS_Store 6 | 7 | HalogenGhostCore 8 | HalogenGhostCore.exe -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/HalogenGhostCore.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # M41den's GDPS GhostCore 2 | 3 | Yeah, that legendary GDPS core written in go 4 | 5 | ## 🔥 Features 6 | > ### 🧑‍💻 Full JSON support for all endpoints 7 | > Just add `?json` URL parameter to endpoint and work with pure json without needing to parse RobTop's horror 8 | > ![](.github/img/feat_json.png) 9 | 10 | > ### ⚡ Fast As F*ck 11 | > 🤓 Just being written in Go makes it 8-15x faster than Cvolton's implementation. 12 | > 13 | > 📦 It can be easily packaged into a tiny alpine container and deployed anywhere 14 | > 15 | > 🚀 ...and then scaled to infinity thanks to optimized queries and caching. 16 | > 17 | > 💪 DDoS? I don't care — here is how many rps 3 GhostCore instances were able to handle thanks to optimization 18 | > and IP reputation system "Ballistics": 19 | > ![](.github/img/feat_ddos2.png) 20 | > (We had even 100k+ rps, but it wasn't on GhostCore itself so who cares) 21 | 22 | > ### 🤗 Code quality is just GOOD 23 | > Sane OOP, clean code, separate api and logic packages, connectors and other goodies make code easy to read and maintain 24 | 25 | > ### ☁️ Cloud-native 26 | > Distributed locks and leader election thanks to Consul, S3 savedata storage and Redis cache makes it easy to scale 27 | > ![](.github/img/feat_cloud.png) 28 | 29 | > ### 🙃 Just steal the code already! 30 | > ✨ This core is optmized, easily maintainable and battle-tested on real GDPS hosting [FruitSpace](https://fruitspace.ru) 31 | > — 20K servers is not a joke. 32 | > 33 | > 🚀 It's built specifically to scale to 100K+ servers with minimal memory footprint of 40MB and near-zero CPU usage in idle. 34 | > 35 | > 😭 "Oh no! It has license and now I'm obligated to mention that my project uses this code!" — bruh how would I know? 36 | 37 | 38 | 39 | --- 40 | ## 🔥 Фичи - RU 🇷🇺 41 | > ### 🧑‍💻 Полная поддержка JSON для всех эндпоинтов 42 | > Просто добавь `?json` к URL эндпоинта и работай с красивым JSON без необходимости работать с кошмаром РобТопа 43 | > 44 | > ![](.github/img/feat_json.png) 45 | 46 | > ### ⚡ Охренически быстрически 47 | > 🤓 Сам факт того, что ядро написано на Go уже делает его в 8-15 раз быстрее, чем решение Cvolton'а. 48 | > 49 | > 📦 Ядро можно легко запаковать в легковесный alpine-контейнер без лишних зависимостей и размещать где угодно 50 | > 51 | > 🚀 ...а затем еще и масштабировать до бесконечности благодаря оптимизированным запросам и кэшированию 52 | > 53 | > 💪 DDoS'еров спросить забыли — вот столько RPS могут выдержать 3 инстанса GhostCore благодаря бесчисленным оптимизациям 54 | > и системе репутации IP "Ballistics": 55 | > 56 | > ![](.github/img/feat_ddos2.png) 57 | > 58 | > (Еще у нас был DDoS 100k+ rps, но не на само ядро, поэтому пропустим этот момент) 59 | 60 | > ### 🤗 Наконец-то НОРМАЛЬНОЕ качество кода 61 | > Адекватное ООП, читаемый код, разделение api и логики, коннекторы и прочие вкусности делают код легко читаемым и 62 | > обслуживаемым 63 | 64 | > ### ☁️ Cloud-native 65 | > Распределенные мьютексы и поддержка лидера/кворума благодаря Consul, S3 для хранения пользовательских сохранений и 66 | > кэш Redis позволяют легко масштабировать ядро. Добро пожаловать в будущее, PHP-шеры 67 | > 68 | > ![](.github/img/feat_cloud.png) 69 | 70 | > ### 🙃 Да просто своруйте уже код! 71 | > ✨ Это ядро оптимизировано, легко в обслуживании и было отполировано за 2 года работы GDPS хостинга [FruitSpace](https://fruitspace.ru) 72 | > — 20K серверов и Дольфи на борту это вам не шутки. 73 | > 74 | > 🚀 Ядро специально было сделано для масштабирования под 100К+ серверов с минимальным использованием RAM под 40MB и 75 | > почти нулевым использованием CPU во время бездействия. 76 | > 77 | > 😭 "О нет! У проекта есть лицензия и я теперь должен упомянать этот репозиторий!" — а будто раньше код у меня не копипастили 78 | 79 | ### Почемууу забросил ядрооо 80 | Потому что роб забросил гд, но я оказался быстрее и забросил его в январе 2024. Не, серьезно - пора уже вам самим писать 81 | нормальный код. Вам исходики, мне звездочку на репозиторий и плюсик на собесе. 82 | 83 | Форкнуть или делать PR — я был бы рад пулл реквестам и мерджил их, если качество кода лучше ChatGPT. 84 | 85 | Если вам что-нибудь придет в голову или вы захотите интеграций, пните меня в телеге [👉@M41den](https://t.me/m41den) 86 | и скажите что вы по поводу ядра (мысли ваши читать, увы, не умею). 87 | 88 | 89 | -------------------------------------------------------------------------------- /_recover/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 ubuntu:latest 2 | LABEL authors="m41den" 3 | 4 | RUN apt update && apt install -y mariadb-client redis-tools wget curl 5 | RUN wget https://dl.min.io/client/mc/release/linux-amd64/mc -O /usr/local/bin/mc && chmod +x /usr/local/bin/mc 6 | 7 | WORKDIR /recover 8 | COPY recover.sh /entrypoint.sh 9 | RUN chmod +x /entrypoint.sh 10 | 11 | ENTRYPOINT ["/entrypoint.sh"] 12 | -------------------------------------------------------------------------------- /_recover/recover.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -z "$NEED_RECOVERY" ]; then 4 | echo "No recovery needed" 5 | exit 0 6 | fi 7 | 8 | cat << EOF > /tmp/minio.json 9 | { 10 | "version": "10", 11 | "aliases": { 12 | "minio": { 13 | "url": "http://minio:9000", 14 | "accessKey": "$MINIO_USER", 15 | "secretKey": "$MINIO_PASSWORD", 16 | "auth": "s3v4", 17 | "path": "auto" 18 | } 19 | } 20 | } 21 | EOF 22 | 23 | REDIS_ARGS=${REDIS_PASSWORD:+-a "$REDIS_PASSWORD"} 24 | 25 | echo "Importing config to Redis..." 26 | redis-cli -h redis $REDIS_ARGS -n 7 -x SET $ID < /recovery/config.json 27 | 28 | echo "Importing database to MariaDB..." 29 | mysql -h mariadb -u "$DB_USER" --password="$DB_PASS" gdps_$ID < /recovery/gdps_$ID.sql 30 | 31 | echo "Importing savedata to Minio..." 32 | mc cp -r /recovery/gdps_savedata minio/gdps_savedata 33 | 34 | echo "Done!" -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | # unified database for host + gdps 3 | mariadb: 4 | image: mariadb:latest 5 | restart: always 6 | healthcheck: 7 | test: ["CMD", "bash", "-c", "mariadb -h 127.0.0.1 -u $${MARIADB_USER} -p$${MARIADB_PASSWORD} -e 'SELECT 1'"] 8 | interval: 10s 9 | timeout: 5s 10 | retries: 5 11 | ports: 12 | - "3306:3306" 13 | volumes: 14 | - ./_mysql:/var/lib/mysql 15 | environment: 16 | MARIADB_ROOT_PASSWORD: "GlooseDumbass" 17 | MARIADB_USER: "gen_user" 18 | MARIADB_PASSWORD: "pineappol" 19 | MARIADB_DATABASE: "gdps_000S" 20 | # redis for gdps config storage (db 7) 21 | redis: 22 | image: redis:latest 23 | restart: always 24 | healthcheck: 25 | test: ["CMD", "redis-cli", "ping"] 26 | interval: 5s 27 | timeout: 2s 28 | retries: 3 29 | ports: 30 | - "6379:6379" 31 | volumes: 32 | - ./_redis:/data 33 | minio: 34 | image: quay.io/minio/minio:latest 35 | restart: always 36 | ports: 37 | - "9000:9000" 38 | - "9001:9001" 39 | volumes: 40 | - ./_minio:/data 41 | command: server /data --console-address ":9001" 42 | environment: 43 | MINIO_ROOT_USER: "apache" 44 | MINIO_ROOT_PASSWORD: "apache2077" 45 | adminer: 46 | image: adminer 47 | restart: always 48 | ports: 49 | - "8080:8080" 50 | depends_on: 51 | mariadb: 52 | condition: service_healthy 53 | 54 | # used for importing backups 55 | recovery: 56 | build: _recover 57 | depends_on: 58 | mariadb: 59 | condition: service_healthy 60 | redis: 61 | condition: service_healthy 62 | minio: 63 | condition: service_started 64 | volumes: 65 | - ./_recover:/recovery 66 | environment: 67 | NEED_RECOVERY: "true" 68 | ID: "000S" # GDPS ID 69 | MINIO_USER: "apache" 70 | MINIO_PASSWORD: "apache2077" 71 | DB_USER: "gen_user" 72 | DB_PASS: "pineappol" 73 | REDIS_PASSWORD: "" 74 | # ghostcore itself 75 | ghostcore: 76 | build: 77 | context: src 78 | dockerfile: Dockerfile 79 | depends_on: 80 | mariadb: 81 | condition: service_healthy 82 | redis: 83 | condition: service_healthy 84 | minio: 85 | condition: service_started 86 | recovery: 87 | condition: service_completed_successfully 88 | ports: 89 | - "80:1997" 90 | environment: 91 | MASTER_KEY: "WhateverKey" # Used for authenticating service endpoints 92 | DB_USER: "gen_user" # database user for everything 93 | DB_PASS: "pineappol" # database password 94 | DB_HOST: "mariadb" # Don't change 95 | REDIS_PORT: "6379" # Don't change 96 | REDIS_HOST: "redis" # Don't change 97 | REDIS_PASSWORD: "" # Don't change unless you've set Redis password 98 | SAVE_PATH: "/core" # Used for security model 99 | S3_CONFIG: "access_key=apache,secret=apache2077,region=us-east-1,bucket=default,endpoint=minio,cdn=minio" # S3 creds for minio 100 | CONSUL_ADDR: "consul:8500" # Ignore this, when core can't connect to consul it will assume self-leadership 101 | CONSUL_DC: "dc1" # Ignore this -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 AS builder 2 | RUN mkdir /app 3 | WORKDIR /app 4 | COPY go.* . 5 | RUN go mod download && \ 6 | go install github.com/gordonklaus/ineffassign@latest && \ 7 | go install github.com/fzipp/gocyclo/cmd/gocyclo@latest 8 | COPY . . 9 | RUN ineffassign ./... 10 | RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o HalogenGhostCore . 11 | 12 | FROM alpine 13 | EXPOSE 1997 14 | RUN apk add --no-cache tzdata 15 | RUN mkdir /app /core 16 | COPY --from=builder /app/HalogenGhostCore /app 17 | CMD ["/app/HalogenGhostCore"] -------------------------------------------------------------------------------- /src/PLUGIN_DOCS.md: -------------------------------------------------------------------------------- 1 | # PLUGINS 2 | 3 | ### LevelPacks 4 | - [ ] `onGauntletNew` - invoked when new gauntlet is created 5 | - [ ] `onMapPackNew` - invoked when new map pack is created 6 | ### Communication 7 | - To Be Done 8 | 9 | ### Essential 10 | 11 | ```go 12 | package modules 13 | 14 | // Plugin describes Plugin type; PluginCore is also Plugin 15 | type Plugin interface { 16 | PreInit(*PluginCore, ...interface{}) 17 | Unload(...interface{}) 18 | } 19 | 20 | // PreInit Invoked to load anything 21 | func PreInit(pch *PluginCore, args ...interface{}) 22 | 23 | // Unload Unloads everything 24 | func Unload(args ...interface{}) 25 | ``` 26 | ### Player 27 | 28 | ```go 29 | package modules 30 | 31 | // OnPlayerNew Invoked when player is registered, but not yet activated account 32 | func OnPlayerNew(uid int, uname string, email string) 33 | 34 | // OnPlayerActivate Invoked when player first activated account 35 | func OnPlayerActivate(uid int, uname string) 36 | 37 | // OnPlayerLogin invoked when player commits login (regular, not gjp) 38 | func OnPlayerLogin(uid int, uname string) 39 | 40 | // OnPlayerBackup invoked when player uploads their backup 41 | func OnPlayerBackup(uid int, decryptedBackup string) 42 | 43 | //onPlayerSync is forbidden 44 | 45 | // OnPlayerScoreUpdate invoked when player updates their score 46 | func OnPlayerScoreUpdate(uid int, uname string, stats map[string]int) 47 | 48 | var stats = map[string]int{ 49 | "stars": 0, 50 | "diamonds": 0, 51 | "coins": 0, 52 | "ucoins": 0, 53 | "demons": 0, 54 | "cpoints": 0, 55 | "orbs": 0, 56 | "moons": 0, 57 | "special": 0, 58 | "lvlsCompleted": 0, 59 | } 60 | ``` 61 | ### Level 62 | 63 | ```go 64 | package modules 65 | 66 | import "strconv" 67 | 68 | // OnLevelUpload invoked when level was uploaded 69 | func OnLevelUpload(id int, name string, builder string, desc string) 70 | 71 | // OnLevelUpdate invoked when level was updated 72 | func OnLevelUpdate(id int, name string, builder string, desc string) 73 | 74 | // OnLevelDelete invoked when level was deleted 75 | func OnLevelDelete(id int, name string, builder string) 76 | 77 | // OnLevelRate invoked when level was rated/rerated 78 | func OnLevelRate(id int, name string, builder string, stars int, likes int, downloads int, length int, demonDiff int, isEpic bool, isFeatured bool, ratedBy map[string]string) 79 | 80 | // OnLevelReport invoked when level was reported 81 | func OnLevelReport(id int, name string, builder string, player string) 82 | 83 | // OnLevelScore invoked when player published their score in level scoreboard 84 | func OnLevelScore(id int, name string, player string, percent int, coins int) 85 | 86 | var ratedBy = map[string]string{ 87 | "uid": strconv.Itoa(1), 88 | "uname": "Username", 89 | } 90 | ``` 91 | 92 | ### Communtication 93 | 94 | ```go 95 | package modules 96 | 97 | // Not implemented yet 98 | ``` -------------------------------------------------------------------------------- /src/api/account.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "HalogenGhostCore/core/connectors" 6 | "bytes" 7 | "compress/gzip" 8 | "encoding/base64" 9 | "fmt" 10 | gorilla "github.com/gorilla/mux" 11 | "io" 12 | "net/http" 13 | "os" 14 | "strconv" 15 | "strings" 16 | ) 17 | 18 | func AccountBackup(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 19 | IPAddr := ipOf(req) 20 | vars := gorilla.Vars(req) 21 | logger := core.Logger{Output: os.Stderr} 22 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 23 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 24 | config, err := conf.LoadById(vars["gdps"]) 25 | if logger.Should(err) != nil { 26 | connector.Error("-1", "Not Found") 27 | return 28 | } 29 | if core.CheckIPBan(IPAddr, config) { 30 | connector.Error("-1", "Banned") 31 | return 32 | } 33 | 34 | Post := ReadPost(req) 35 | if Post.Get("saveData") != "" { 36 | uname := core.ClearGDRequest(Post.Get("userName")) 37 | pass := core.ClearGDRequest(Post.Get("password")) 38 | saveData := core.ClearGDRequest(Post.Get("saveData")) 39 | db := &core.MySQLConn{} 40 | 41 | if logger.Should(db.ConnectBlob(config)) != nil { 42 | serverError(connector) 43 | return 44 | } 45 | acc := core.CAccount{DB: db} 46 | var res int 47 | if Post.Get("gameVersion") == "22" { 48 | res = core.ToInt(acc.PerformGJPAuth(Post, IPAddr)) 49 | } else { 50 | res = acc.LogIn(uname, pass, IPAddr, 0) 51 | } 52 | if res > 0 { 53 | savepath := "/gdps_savedata/" + vars["gdps"] + "/" 54 | taes := core.ThunderAES{} 55 | if logger.Should(taes.GenKey(config.ServerConfig.SrvKey)) != nil { 56 | serverError(connector) 57 | return 58 | } 59 | if logger.Should(taes.Init()) != nil { 60 | serverError(connector) 61 | return 62 | } 63 | datax, err := taes.EncryptRaw(saveData) 64 | if logger.Should(err) != nil { 65 | serverError(connector) 66 | return 67 | } 68 | 69 | s3 := core.NewS3FS() 70 | if logger.Should(s3.PutFile(savepath+strconv.Itoa(acc.Uid)+".hsv", datax)) != nil { 71 | serverError(connector) 72 | return 73 | } 74 | 75 | saveData = strings.ReplaceAll(strings.ReplaceAll(strings.Split(saveData, ";")[0], "_", "/"), "-", "+") 76 | b, err := base64.StdEncoding.DecodeString(saveData) 77 | if logger.Should(err) != nil { 78 | serverError(connector) 79 | return 80 | } 81 | r, err := gzip.NewReader(bytes.NewBuffer(b)) 82 | if logger.Should(err) != nil { 83 | serverError(connector) 84 | return 85 | } 86 | d, err := io.ReadAll(r) 87 | if logger.Should(err) != nil { 88 | serverError(connector) 89 | return 90 | } 91 | saveData = string(d) 92 | acc.LoadStats() 93 | acc.Orbs, _ = strconv.Atoi(strings.Split(strings.Split(saveData, "14")[1], "")[0]) 94 | acc.LvlsCompleted, _ = strconv.Atoi(strings.Split(strings.Split(strings.Split(saveData, "GS_value")[1], "4")[1], "")[0]) 95 | acc.PushStatsAndExtra() 96 | //! Temp 97 | s3.DeleteFile("/savedata_old/" + vars["gdps"] + "/files/savedata/" + strconv.Itoa(acc.Uid) + ".hal") 98 | connector.Success("Backup successful") 99 | } else { 100 | connector.Error("-2", "Invalid credentials") 101 | } 102 | } else { 103 | connector.Error("-1", "Bad request") 104 | } 105 | } 106 | 107 | func AccountSync(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 108 | IPAddr := ipOf(req) 109 | vars := gorilla.Vars(req) 110 | logger := core.Logger{Output: os.Stderr} 111 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 112 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 113 | config, err := conf.LoadById(vars["gdps"]) 114 | if logger.Should(err) != nil { 115 | serverError(connector) 116 | return 117 | } 118 | if core.CheckIPBan(IPAddr, config) { 119 | connector.Error("-1", "Banned") 120 | return 121 | } 122 | Post := ReadPost(req) 123 | if (Post.Get("userName") != "" && Post.Get("password") != "") || Post.Get("gjp2") != "" { 124 | uname := core.ClearGDRequest(Post.Get("userName")) 125 | pass := core.ClearGDRequest(Post.Get("password")) 126 | db := &core.MySQLConn{} 127 | 128 | if logger.Should(db.ConnectBlob(config)) != nil { 129 | return 130 | } 131 | acc := core.CAccount{DB: db} 132 | var res int 133 | if Post.Get("gameVersion") == "22" { 134 | res = core.ToInt(acc.PerformGJPAuth(Post, IPAddr)) 135 | } else { 136 | res = acc.LogIn(uname, pass, IPAddr, 0) 137 | } 138 | if res > 0 { 139 | savepath := "/gdps_savedata/" + vars["gdps"] + "/" + strconv.Itoa(acc.Uid) + ".hsv" 140 | s3 := core.NewS3FS() 141 | if d, err := s3.GetFile(savepath); err == nil { 142 | taes := core.ThunderAES{} 143 | if logger.Should(taes.GenKey(config.ServerConfig.SrvKey)) != nil { 144 | serverError(connector) 145 | return 146 | } 147 | if logger.Should(taes.Init()) != nil { 148 | serverError(connector) 149 | return 150 | } 151 | data, err := taes.DecryptRaw(d) 152 | if err != nil { 153 | serverError(connector) 154 | core.ReportFail(fmt.Sprintf("[%s] NG savedata decrypt error for `%s`", vars["gdps"], uname)) 155 | return 156 | } 157 | connector.Account_Sync(data) 158 | //! Temp transitional 159 | } else if d, err := s3.GetFile("/savedata_old/" + vars["gdps"] + "/files/savedata/" + strconv.Itoa(acc.Uid) + ".hal"); err == nil { 160 | taes := core.ThunderAES{} 161 | if logger.Should(taes.GenKey(pass)) != nil { 162 | serverError(connector) 163 | return 164 | } 165 | if logger.Should(taes.Init()) != nil { 166 | serverError(connector) 167 | return 168 | } 169 | data, err := taes.DecryptLegacy(string(d)) 170 | if err != nil { 171 | serverError(connector) 172 | core.ReportFail(fmt.Sprintf("[%s] HAL savedata decrypt error for `%s`", vars["gdps"], uname)) 173 | return 174 | } 175 | connector.Account_Sync(data) 176 | } else { 177 | connector.Error("-1", "No savedata found") 178 | } 179 | } else { 180 | connector.Error("-2", "Invalid credentials") 181 | } 182 | } else { 183 | connector.Error("-1", "Bad request") 184 | } 185 | } 186 | 187 | func AccountManagement(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 188 | vars := gorilla.Vars(req) 189 | http.Redirect(resp, req, "https://gofruit.space/gdps/"+vars["gdps"], http.StatusMovedPermanently) 190 | } 191 | 192 | func AccountLogin(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 193 | IPAddr := ipOf(req) 194 | vars := gorilla.Vars(req) 195 | logger := core.Logger{Output: os.Stderr} 196 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 197 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 198 | config, err := conf.LoadById(vars["gdps"]) 199 | if logger.Should(err) != nil { 200 | connector.Error("-1", "Not Found") 201 | return 202 | } 203 | if core.CheckIPBan(IPAddr, config) { 204 | connector.Error("-1", "Banned") 205 | return 206 | } 207 | Post := ReadPost(req) 208 | if Post.Get("userName") != "" && Post.Get("password")+Post.Get("gjp2") != "" { 209 | gjp2 := core.ClearGDRequest(Post.Get("gjp2")) 210 | uname := core.ClearGDRequest(Post.Get("userName")) 211 | pass := core.ClearGDRequest(Post.Get("password")) 212 | db := &core.MySQLConn{} 213 | 214 | if logger.Should(db.ConnectBlob(config)) != nil { 215 | serverError(connector) 216 | return 217 | } 218 | acc := core.CAccount{DB: db} 219 | var uid int 220 | if len(gjp2) != 0 { 221 | uid = acc.LogIn22(uname, gjp2, IPAddr, 0) 222 | } else { 223 | uid = acc.LogIn(uname, pass, IPAddr, 0) 224 | } 225 | 226 | if uid < 0 { 227 | connector.Error("-1", "Invalid credentials") 228 | } else { 229 | connector.Account_Login(uid) 230 | core.RegisterAction(core.ACTION_USER_LOGIN, 0, uid, map[string]string{"uname": uname}, db) 231 | } 232 | } else { 233 | connector.Error("-1", "Bad request") 234 | } 235 | } 236 | 237 | func AccountRegister(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 238 | IPAddr := ipOf(req) 239 | vars := gorilla.Vars(req) 240 | if conf.MaintenanceMode { 241 | resp.WriteHeader(403) 242 | core.SendMessageDiscord(fmt.Sprintf("[%s] %s reached registration killswitch", vars["gdps"], IPAddr)) 243 | return 244 | } 245 | 246 | //Ballistics 247 | if PrepareBallistics(req) { 248 | return 249 | } 250 | 251 | logger := core.Logger{Output: os.Stderr} 252 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 253 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 254 | config, err := conf.LoadById(vars["gdps"]) 255 | if logger.Should(err) != nil { 256 | connector.Error("-1", "Not Found") 257 | return 258 | } 259 | if core.CheckIPBan(IPAddr, config) { 260 | connector.Error("-1", "Banned") 261 | return 262 | } 263 | 264 | Post := ReadPost(req) 265 | if Post.Get("userName") != "" && Post.Get("password") != "" && Post.Get("email") != "" { 266 | uname := core.ClearGDRequest(Post.Get("userName")) 267 | pass := core.ClearGDRequest(Post.Get("password")) 268 | email := core.ClearGDRequest(Post.Get("email")) 269 | db := &core.MySQLConn{} 270 | 271 | if logger.Should(db.ConnectBlob(config)) != nil { 272 | serverError(connector) 273 | return 274 | } 275 | acc := core.CAccount{DB: db} 276 | if core.OnRegister(db, conf, config) { 277 | uid := acc.Register(uname, pass, email, IPAddr, config.SecurityConfig.AutoActivate) 278 | if uid > 0 { 279 | core.RegisterAction(core.ACTION_USER_REGISTER, 0, uid, map[string]string{"uname": uname, "email": email}, db) 280 | connector.Success("Registered") 281 | } else { 282 | connector.Error(strconv.Itoa(uid), "Refer to https://github.com/gd-programming/gd.docs/blob/main/docs/topics/status_codes.md#registergjaccount") 283 | } 284 | } else { 285 | connector.Error("-1", "Player limits exceeded") 286 | } 287 | } else { 288 | connector.Error("-1", "Bad request") 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/api/comment.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "HalogenGhostCore/core/connectors" 6 | "encoding/base64" 7 | gorilla "github.com/gorilla/mux" 8 | "io" 9 | "net/http" 10 | "os" 11 | ) 12 | 13 | func AccountCommentDelete(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 14 | IPAddr := ipOf(req) 15 | vars := gorilla.Vars(req) 16 | logger := core.Logger{Output: os.Stderr} 17 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 18 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 19 | config, err := conf.LoadById(vars["gdps"]) 20 | if logger.Should(err) != nil { 21 | connector.Error("-1", "Not Found") 22 | return 23 | } 24 | if core.CheckIPBan(IPAddr, config) { 25 | connector.Error("-1", "Banned") 26 | return 27 | } 28 | Post := ReadPost(req) 29 | if core.CheckGDAuth(Post) && Post.Get("commentID") != "" { 30 | db := &core.MySQLConn{} 31 | if logger.Should(db.ConnectBlob(config)) != nil { 32 | serverError(connector) 33 | return 34 | } 35 | xacc := core.CAccount{DB: db} 36 | if !xacc.PerformGJPAuth(Post, IPAddr) { 37 | connector.Error("-1", "Invalid Credentials") 38 | return 39 | } 40 | cc := core.CComment{DB: db} 41 | var id int 42 | core.TryInt(&id, Post.Get("commentID")) 43 | cc.DeleteAccComment(id, xacc.Uid) 44 | core.OnPost(db, conf, config) 45 | connector.Success("Comment deleted") 46 | } else { 47 | connector.Error("-1", "Bad Request") 48 | } 49 | } 50 | 51 | func AccountCommentGet(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 52 | IPAddr := ipOf(req) 53 | vars := gorilla.Vars(req) 54 | logger := core.Logger{Output: os.Stderr} 55 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 56 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 57 | config, err := conf.LoadById(vars["gdps"]) 58 | if logger.Should(err) != nil { 59 | connector.Error("-1", "Not Found") 60 | return 61 | } 62 | if core.CheckIPBan(IPAddr, config) { 63 | connector.Error("-1", "Banned") 64 | return 65 | } 66 | 67 | Post := ReadPost(req) 68 | if Post.Get("accountID") != "" { 69 | page := 0 70 | core.TryInt(&page, Post.Get("page")) 71 | db := &core.MySQLConn{} 72 | 73 | if logger.Should(db.ConnectBlob(config)) != nil { 74 | serverError(connector) 75 | return 76 | } 77 | var uid int 78 | xuid := Post.Get("accountID") 79 | if len(Post["accountID"]) > 1 { 80 | xuid = Post["accountID"][1] 81 | } 82 | core.TryInt(&uid, xuid) 83 | cc := core.CComment{DB: db} 84 | comments := cc.GetAllAccComments(uid, page) 85 | connector.Comment_AccountGet(comments, cc.CountAccComments(uid), page) 86 | } else { 87 | connector.Error("-1", "Bad Request") 88 | } 89 | } 90 | 91 | func AccountCommentUpload(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 92 | IPAddr := ipOf(req) 93 | vars := gorilla.Vars(req) 94 | logger := core.Logger{Output: os.Stderr} 95 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 96 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 97 | config, err := conf.LoadById(vars["gdps"]) 98 | if logger.Should(err) != nil { 99 | connector.Error("-1", "Not Found") 100 | return 101 | } 102 | 103 | if conf.MaintenanceMode { 104 | config.SecurityConfig.DisableProtection = false 105 | } 106 | 107 | if core.CheckIPBan(IPAddr, config) { 108 | connector.Error("-1", "Banned") 109 | return 110 | } 111 | 112 | Post := ReadPost(req) 113 | if core.CheckGDAuth(Post) && Post.Get("comment") != "" { 114 | db := &core.MySQLConn{} 115 | 116 | if logger.Should(db.ConnectBlob(config)) != nil { 117 | serverError(connector) 118 | return 119 | } 120 | xacc := core.CAccount{DB: db} 121 | if !xacc.PerformGJPAuth(Post, IPAddr) { 122 | connector.Error("-1", "Invalid Credentials") 123 | return 124 | } 125 | comment := core.ClearGDRequest(Post.Get("comment")) 126 | cc := core.CComment{DB: db, Uid: xacc.Uid, Comment: comment} 127 | protect := core.CProtect{DB: db, DisableProtection: config.SecurityConfig.DisableProtection} 128 | if !core.OnPost(db, conf, config) { 129 | connector.Error("-1", "Post limits exceeded") 130 | return 131 | } 132 | if protect.DetectPosts(xacc.Uid) && cc.PostAccComment() { 133 | connector.Success("Comment posted") 134 | return 135 | } 136 | serverError(connector) 137 | } else { 138 | connector.Error("-1", "Bad Request") 139 | } 140 | } 141 | 142 | func CommentDelete(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 143 | IPAddr := ipOf(req) 144 | vars := gorilla.Vars(req) 145 | logger := core.Logger{Output: os.Stderr} 146 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 147 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 148 | config, err := conf.LoadById(vars["gdps"]) 149 | if logger.Should(err) != nil { 150 | connector.Error("-1", "Not Found") 151 | return 152 | } 153 | if core.CheckIPBan(IPAddr, config) { 154 | connector.Error("-1", "Banned") 155 | return 156 | } 157 | 158 | Post := ReadPost(req) 159 | if core.CheckGDAuth(Post) && Post.Get("commentID") != "" && Post.Get("levelID") != "" { 160 | db := &core.MySQLConn{} 161 | 162 | if logger.Should(db.ConnectBlob(config)) != nil { 163 | serverError(connector) 164 | return 165 | } 166 | xacc := core.CAccount{DB: db} 167 | if !xacc.PerformGJPAuth(Post, IPAddr) { 168 | connector.Error("-1", "Invalid Credentials") 169 | return 170 | } 171 | cc := core.CComment{DB: db} 172 | var id, lvl_id int 173 | core.TryInt(&id, Post.Get("commentID")) 174 | core.TryInt(&lvl_id, Post.Get("levelID")) 175 | if lvl_id > 0 { 176 | // levels 177 | cl := core.CLevel{DB: db, Id: lvl_id} 178 | if cl.IsOwnedBy(xacc.Uid) { 179 | cc.DeleteOwnerLevelComment(id, lvl_id) 180 | } else { 181 | cc.DeleteLevelComment(id, xacc.Uid) 182 | } 183 | } else { 184 | // RobTop is retarded 185 | cl := core.CLevelList{DB: db, ID: lvl_id} 186 | if cl.IsOwnedBy(xacc.Uid) { 187 | cc.DeleteOwnerLevelComment(id, lvl_id) 188 | } else { 189 | cc.DeleteLevelComment(id, xacc.Uid) 190 | } 191 | } 192 | core.OnComment(db, conf, config) 193 | connector.Success("Comment deleted") 194 | } else { 195 | connector.Error("-1", "Bad Request") 196 | } 197 | } 198 | 199 | func CommentGet(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 200 | IPAddr := ipOf(req) 201 | vars := gorilla.Vars(req) 202 | logger := core.Logger{Output: os.Stderr} 203 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 204 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 205 | config, err := conf.LoadById(vars["gdps"]) 206 | if logger.Should(err) != nil { 207 | connector.Error("-1", "Not Found") 208 | return 209 | } 210 | if core.CheckIPBan(IPAddr, config) { 211 | connector.Error("-1", "Banned") 212 | return 213 | } 214 | 215 | Post := ReadPost(req) 216 | if Post.Get("levelID") != "" { 217 | page := 0 218 | core.TryInt(&page, Post.Get("page")) 219 | mode := false 220 | if Post.Get("mode") != "0" { 221 | mode = true 222 | } 223 | db := &core.MySQLConn{} 224 | 225 | if logger.Should(db.ConnectBlob(config)) != nil { 226 | serverError(connector) 227 | return 228 | } 229 | var lvlId int 230 | core.TryInt(&lvlId, Post.Get("levelID")) 231 | cc := core.CComment{DB: db} 232 | comments := cc.GetAllLevelComments(lvlId, page, mode) 233 | connector.Comment_LevelGet(comments, cc.CountLevelComments(lvlId), page) 234 | } else { 235 | connector.Error("-1", "Bad Request") 236 | } 237 | } 238 | 239 | func CommentGetHistory(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 240 | IPAddr := ipOf(req) 241 | vars := gorilla.Vars(req) 242 | logger := core.Logger{Output: os.Stderr} 243 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 244 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 245 | config, err := conf.LoadById(vars["gdps"]) 246 | if logger.Should(err) != nil { 247 | connector.Error("-1", "Not Found") 248 | return 249 | } 250 | if core.CheckIPBan(IPAddr, config) { 251 | connector.Error("-1", "Banned") 252 | return 253 | } 254 | 255 | Post := ReadPost(req) 256 | if Post.Get("userID") != "" { 257 | page := 0 258 | core.TryInt(&page, Post.Get("page")) 259 | mode := false 260 | if Post.Get("mode") != "0" { 261 | mode = true 262 | } 263 | db := &core.MySQLConn{} 264 | 265 | if logger.Should(db.ConnectBlob(config)) != nil { 266 | serverError(connector) 267 | return 268 | } 269 | acc := core.CAccount{DB: db} 270 | core.TryInt(&acc.Uid, Post.Get("userID")) 271 | if !acc.Exists(acc.Uid) { 272 | connector.Error("-1", "Invalid Credentials") 273 | return 274 | } 275 | acc.LoadAuth(core.CAUTH_UID) 276 | acc.LoadStats() 277 | acc.LoadVessels() 278 | role := acc.GetRoleObj(false) 279 | cc := core.CComment{DB: db} 280 | comments := cc.GetAllCommentsHistory(acc.Uid, page, mode) 281 | connector.Comment_HistoryGet(comments, acc, role, cc.CountCommentHistory(acc.Uid), page) 282 | } else { 283 | connector.Error("-1", "Bad Request") 284 | } 285 | } 286 | 287 | func CommentUpload(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 288 | IPAddr := ipOf(req) 289 | vars := gorilla.Vars(req) 290 | logger := core.Logger{Output: os.Stderr} 291 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 292 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 293 | config, err := conf.LoadById(vars["gdps"]) 294 | if logger.Should(err) != nil { 295 | connector.Error("-1", "Not Found") 296 | return 297 | } 298 | if core.CheckIPBan(IPAddr, config) { 299 | connector.Error("-1", "Banned") 300 | return 301 | } 302 | 303 | Post := ReadPost(req) 304 | if core.CheckGDAuth(Post) && Post.Get("comment") != "" && Post.Get("levelID") != "" { 305 | db := &core.MySQLConn{} 306 | 307 | if logger.Should(db.ConnectBlob(config)) != nil { 308 | serverError(connector) 309 | return 310 | } 311 | xacc := core.CAccount{DB: db} 312 | if !xacc.PerformGJPAuth(Post, IPAddr) { 313 | connector.Error("-1", "Invalid Credentials") 314 | return 315 | } 316 | comment := core.ClearGDRequest(Post.Get("comment")) 317 | percent := 0 318 | core.TryInt(&percent, Post.Get("percent")) 319 | percent %= 101 320 | cl := core.CLevel{DB: db} 321 | core.TryInt(&cl.Id, Post.Get("levelID")) 322 | if cl.Id < 0 { 323 | // CLevelList 324 | list := core.CLevelList{DB: db, ID: cl.Id * -1} 325 | if !list.Exists(list.ID) { 326 | connector.Error("-1", "Invalid LevelList ID") 327 | return 328 | } 329 | cc := core.CComment{DB: db, Uid: xacc.Uid, LvlId: cl.Id, Comment: comment, Percent: percent} 330 | protect := core.CProtect{DB: db, DisableProtection: config.SecurityConfig.DisableProtection} 331 | if !core.OnComment(db, conf, config) { 332 | connector.Error("-1", "Comment limit exceeded") 333 | return 334 | } 335 | if protect.DetectComments(xacc.Uid) && cc.PostLevelComment() { 336 | connector.Success("Comment posted") 337 | } else { 338 | serverError(connector) 339 | } 340 | return 341 | } 342 | 343 | // Next 344 | if !cl.Exists(cl.Id) { 345 | connector.Error("-1", "Invalid Level ID") 346 | return 347 | } 348 | acc := core.CAccount{DB: db, Uid: xacc.Uid} 349 | acc.LoadAuth(core.CAUTH_UID) 350 | role := acc.GetRoleObj(true) 351 | isOwned := cl.IsOwnedBy(xacc.Uid) 352 | if len(role.Privs) > 0 || isOwned { 353 | modCommentByte, err := base64.StdEncoding.DecodeString(comment) 354 | modComment := string(modCommentByte) 355 | if err == nil && modComment[0] == '!' { 356 | cl.LoadMain() 357 | cl.LoadParams() 358 | cmdRes := core.InvokeCommands(db, cl, acc, modComment, isOwned, role) 359 | switch cmdRes { 360 | case "err": 361 | connector.Error("-1", cmdRes) 362 | case "ok": 363 | connector.Success("Command executed") 364 | default: 365 | connector.Error("-1", cmdRes) 366 | } 367 | } else { 368 | cc := core.CComment{DB: db, Uid: xacc.Uid, LvlId: cl.Id, Comment: comment, Percent: percent} 369 | protect := core.CProtect{DB: db, DisableProtection: config.SecurityConfig.DisableProtection} 370 | if !core.OnComment(db, conf, config) { 371 | connector.Error("-1", "Comment limit exceeded") 372 | return 373 | } 374 | if protect.DetectComments(xacc.Uid) && cc.PostLevelComment() { 375 | connector.Success("Comment posted") 376 | } else { 377 | serverError(connector) 378 | } 379 | } 380 | } else { 381 | cc := core.CComment{DB: db, Uid: xacc.Uid, LvlId: cl.Id, Comment: comment, Percent: percent} 382 | protect := core.CProtect{DB: db, DisableProtection: config.SecurityConfig.DisableProtection} 383 | if !core.OnComment(db, conf, config) { 384 | connector.Error("-1", "Comment limit exceeded") 385 | return 386 | } 387 | if protect.DetectComments(xacc.Uid) && cc.PostLevelComment() { 388 | connector.Success("Comment posted") 389 | } else { 390 | serverError(connector) 391 | } 392 | } 393 | } else { 394 | connector.Error("-1", "Bad Request") 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/api/content.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "fmt" 6 | gorilla "github.com/gorilla/mux" 7 | "io" 8 | "net/http" 9 | "os" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | func GetContentURL(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 15 | vars := gorilla.Vars(req) 16 | io.WriteString(resp, "https://rugd.gofruit.space/"+vars["gdps"]+"/db/content") 17 | } 18 | 19 | func GetSFXLibraryVersion(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 20 | resp.Header().Set("Location", "https://cdn.fruitspace.one/gdps_sfx/sfxlibrary_version.txt") 21 | resp.WriteHeader(301) 22 | } 23 | 24 | func GetSFXLibrary(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 25 | resp.Header().Set("Location", "https://cdn.fruitspace.one/gdps_sfx/sfxlibrary.dat") 26 | } 27 | 28 | func GetSFXTrack(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 29 | vars := gorilla.Vars(req) 30 | resp.Header().Set("Location", "https://geometrydashfiles.b-cdn.net/sfx/s"+vars["sfxid"]+".ogg") 31 | resp.WriteHeader(301) 32 | resp.WriteHeader(301) 33 | } 34 | 35 | func RelaySFX(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 36 | vars := gorilla.Vars(req) 37 | resp.Header().Set("Location", "https://geometrydashfiles.b-cdn.net/sfx/"+vars["sfxid"]) 38 | resp.WriteHeader(301) 39 | } 40 | 41 | func GetMusicLibraryVersion(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 42 | t := time.Now() 43 | v := fmt.Sprintf("%s%d", strconv.Itoa(t.Year())[2:], t.YearDay()) 44 | io.WriteString(resp, v) 45 | } 46 | 47 | func GetMusicLibrary(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 48 | vars := gorilla.Vars(req) 49 | logger := core.Logger{Output: os.Stderr} 50 | config, err := conf.LoadById(vars["gdps"]) 51 | if logger.Should(err) != nil { 52 | return 53 | } 54 | db := &core.MySQLConn{} 55 | 56 | if logger.Should(db.ConnectBlob(config)) != nil { 57 | return 58 | } 59 | 60 | url := fmt.Sprintf("https://cdn.fruitspace.one/gdps_sfx/%s_library.dat", vars["gdps"]) 61 | mdata, err := http.Head(url) 62 | if err != nil { 63 | fmt.Println(err) 64 | resp.WriteHeader(500) 65 | return 66 | } 67 | lmod := mdata.Header.Get("last-modified") 68 | if mdata.StatusCode != 200 { 69 | lmod = "Mon, 02 Jan 2006 15:04:05 GMT" 70 | } 71 | date, err := time.Parse("Mon, 02 Jan 2006 15:04:05 MST", lmod) 72 | if err != nil { 73 | fmt.Println(err) 74 | resp.WriteHeader(500) 75 | return 76 | } 77 | if date.YearDay() != time.Now().YearDay() { 78 | core.GenerateMusicLibraryFile(db, core.NewS3FS(), vars["gdps"]) 79 | } 80 | resp.Header().Set("Location", url) 81 | resp.WriteHeader(301) 82 | } 83 | -------------------------------------------------------------------------------- /src/api/essential.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "HalogenGhostCore/core/connectors" 6 | gorilla "github.com/gorilla/mux" 7 | "io" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | func GetAccountUrl(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 13 | vars := gorilla.Vars(req) 14 | _, _ = io.WriteString(resp, "https://rugd.gofruit.space/"+vars["gdps"]+"/db") 15 | } 16 | 17 | func GetSongInfo(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 18 | IPAddr := ipOf(req) 19 | vars := gorilla.Vars(req) 20 | logger := core.Logger{Output: os.Stderr} 21 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 22 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 23 | config, err := conf.LoadById(vars["gdps"]) 24 | if logger.Should(err) != nil { 25 | connector.Error("-1", "Not Found") 26 | return 27 | } 28 | if core.CheckIPBan(IPAddr, config) { 29 | connector.Error("-1", "Banned") 30 | return 31 | } 32 | 33 | Post := ReadPost(req) 34 | songid := Post.Get("songID") 35 | linkmode := false 36 | if songid == "" { 37 | songid = req.URL.Query().Get("id") 38 | linkmode = true 39 | } 40 | if songid != "" { 41 | db := &core.MySQLConn{} 42 | 43 | if logger.Should(db.ConnectBlob(config)) != nil { 44 | serverError(connector) 45 | return 46 | } 47 | mus := core.CMusic{DB: db, ConfBlob: config, Config: conf} 48 | var id int 49 | core.TryInt(&id, songid) 50 | if mus.GetSong(id) { 51 | if linkmode { 52 | resp.Header().Set("Location", mus.Url) 53 | resp.WriteHeader(301) 54 | } else { 55 | connector.Essential_GetMusic(mus) 56 | } 57 | } else { 58 | connector.Error("-1", "Music Not Found") 59 | } 60 | } else { 61 | connector.Error("-1", "Bad Request") 62 | } 63 | } 64 | 65 | func GetTopArtists(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 66 | IPAddr := ipOf(req) 67 | vars := gorilla.Vars(req) 68 | logger := core.Logger{Output: os.Stderr} 69 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 70 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 71 | config, err := conf.LoadById(vars["gdps"]) 72 | if logger.Should(err) != nil { 73 | connector.Error("-1", "Not Found") 74 | return 75 | } 76 | if core.CheckIPBan(IPAddr, config) { 77 | connector.Error("-1", "Banned") 78 | return 79 | } 80 | 81 | Post := ReadPost(req) 82 | db := &core.MySQLConn{} 83 | 84 | if logger.Should(db.ConnectBlob(config)) != nil { 85 | serverError(connector) 86 | return 87 | } 88 | page := 0 89 | core.TryInt(&page, Post.Get("page")) 90 | if page < 0 { 91 | page = 0 92 | } 93 | if logger.Should(db.ConnectBlob(config)) != nil { 94 | return 95 | } 96 | mus := core.CMusic{DB: db, ConfBlob: config, Config: conf} 97 | artists := mus.GetTopArtists() 98 | connector.Essential_GetTopArtists(artists) 99 | } 100 | 101 | func LikeItem(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 102 | IPAddr := ipOf(req) 103 | vars := gorilla.Vars(req) 104 | logger := core.Logger{Output: os.Stderr} 105 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 106 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 107 | config, err := conf.LoadById(vars["gdps"]) 108 | if logger.Should(err) != nil { 109 | connector.Error("-1", "Not Found") 110 | return 111 | } 112 | if core.CheckIPBan(IPAddr, config) { 113 | connector.Error("-1", "Banned") 114 | return 115 | } 116 | 117 | Post := ReadPost(req) 118 | if core.CheckGDAuth(Post) && Post.Get("itemID") != "" && Post.Get("type") != "" { 119 | db := &core.MySQLConn{} 120 | 121 | if logger.Should(db.ConnectBlob(config)) != nil { 122 | serverError(connector) 123 | return 124 | } 125 | xacc := core.CAccount{DB: db} 126 | if !xacc.PerformGJPAuth(Post, IPAddr) { 127 | connector.Error("-1", "Invalid Credentials") 128 | return 129 | } 130 | var itemId, cType int 131 | like := Post.Get("like") == "1" 132 | core.TryInt(&itemId, Post.Get("itemID")) 133 | core.TryInt(&cType, Post.Get("type")) 134 | switch cType { 135 | case 1: 136 | cl := core.CLevel{DB: db} 137 | if cl.Exists(itemId) { 138 | likeAction := core.CLEVEL_ACTION_DISLIKE 139 | if like { 140 | likeAction = core.CLEVEL_ACTION_LIKE 141 | } 142 | cl.LikeLevel(itemId, xacc.Uid, likeAction) 143 | connector.Success("Liked level") 144 | } else { 145 | connector.Error("-1", "Level Not Found") 146 | } 147 | case 2: 148 | comm := core.CComment{DB: db} 149 | if comm.ExistsLevelComment(itemId) { 150 | comm.LikeLevelComment(itemId, xacc.Uid, like) 151 | connector.Success("Liked comment") 152 | } else { 153 | connector.Error("-1", "Comment Not Found") 154 | } 155 | case 3: 156 | comm := core.CComment{DB: db} 157 | if comm.ExistsAccComment(itemId) { 158 | comm.LikeAccComment(itemId, xacc.Uid, like) 159 | connector.Success("Liked account comment") 160 | } else { 161 | connector.Error("-1", "Account Comment Not Found") 162 | } 163 | case 4: 164 | clist := core.CLevelList{DB: db, ID: itemId} 165 | if clist.Exists(itemId) { 166 | likeAction := core.CLEVEL_ACTION_DISLIKE 167 | if like { 168 | likeAction = core.CLEVEL_ACTION_LIKE 169 | } 170 | clist.LikeList(itemId, xacc.Uid, likeAction) 171 | connector.Success("Liked list") 172 | } else { 173 | connector.Error("-1", "List Not Found") 174 | } 175 | default: 176 | connector.Error("-1", "Invalid Type") 177 | } 178 | } else { 179 | connector.Error("-1", "Bad Request") 180 | } 181 | } 182 | 183 | func RequestMod(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 184 | IPAddr := ipOf(req) 185 | vars := gorilla.Vars(req) 186 | logger := core.Logger{Output: os.Stderr} 187 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 188 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 189 | config, err := conf.LoadById(vars["gdps"]) 190 | if logger.Should(err) != nil { 191 | connector.Error("-1", "Not Found") 192 | return 193 | } 194 | if core.CheckIPBan(IPAddr, config) { 195 | connector.Error("-1", "Banned") 196 | return 197 | } 198 | 199 | Post := ReadPost(req) 200 | if core.CheckGDAuth(Post) { 201 | db := &core.MySQLConn{} 202 | 203 | if logger.Should(db.ConnectBlob(config)) != nil { 204 | serverError(connector) 205 | return 206 | } 207 | xacc := core.CAccount{DB: db} 208 | if !xacc.PerformGJPAuth(Post, IPAddr) { 209 | connector.Error("-1", "Invalid Credentials") 210 | return 211 | } 212 | role := xacc.GetRoleObj(true) 213 | if len(role.Privs) > 0 && role.Privs["aReqMod"] > 0 { 214 | connector.NumberedSuccess(role.ModLevel) 215 | } else { 216 | connector.Error("-1", "Insufficient Privileges") 217 | } 218 | } else { 219 | connector.Error("-1", "Bad Request") 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/api/level_lists.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "HalogenGhostCore/core/connectors" 6 | gorilla "github.com/gorilla/mux" 7 | "io" 8 | "log" 9 | "net/http" 10 | "os" 11 | "regexp" 12 | "strings" 13 | ) 14 | 15 | func LevelListDelete(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 16 | IPAddr := ipOf(req) 17 | vars := gorilla.Vars(req) 18 | logger := core.Logger{Output: os.Stderr} 19 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 20 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 21 | config, err := conf.LoadById(vars["gdps"]) 22 | if logger.Should(err) != nil { 23 | connector.Error("-1", "Not Found") 24 | return 25 | } 26 | if core.CheckIPBan(IPAddr, config) { 27 | connector.Error("-1", "Banned") 28 | return 29 | } 30 | 31 | Post := ReadPost(req) 32 | if core.CheckGDAuth(Post) && Post.Get("listID") != "" { 33 | db := &core.MySQLConn{} 34 | 35 | if logger.Should(db.ConnectBlob(config)) != nil { 36 | serverError(connector) 37 | return 38 | } 39 | xacc := core.CAccount{DB: db} 40 | if !xacc.PerformGJPAuth(Post, IPAddr) { 41 | connector.Error("-1", "Invalid credentials") 42 | return 43 | } 44 | var list_id int 45 | core.TryInt(&list_id, Post.Get("listID")) 46 | cl := core.CLevelList{DB: db, ID: list_id} 47 | if !cl.IsOwnedBy(xacc.Uid) { 48 | connector.Error("-1", "List not found or not owned by user") 49 | return 50 | } 51 | cl.DeleteList() 52 | connector.Success("List deleted") 53 | } else { 54 | connector.Error("-1", "Bad Request") 55 | } 56 | } 57 | 58 | func LevelListUpload(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 59 | IPAddr := ipOf(req) 60 | vars := gorilla.Vars(req) 61 | logger := core.Logger{Output: os.Stderr} 62 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 63 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 64 | config, err := conf.LoadById(vars["gdps"]) 65 | if logger.Should(err) != nil { 66 | connector.Error("-1", "Not Found") 67 | return 68 | } 69 | 70 | if conf.MaintenanceMode { 71 | config.SecurityConfig.DisableProtection = false 72 | } 73 | 74 | if core.CheckIPBan(IPAddr, config) { 75 | connector.Error("-1", "Banned") 76 | return 77 | } 78 | 79 | Post := ReadPost(req) 80 | if core.CheckGDAuth(Post) && Post.Get("listLevels") != "" { 81 | db := &core.MySQLConn{} 82 | 83 | if logger.Should(db.ConnectBlob(config)) != nil { 84 | serverError(connector) 85 | return 86 | } 87 | xacc := core.CAccount{DB: db} 88 | if !xacc.PerformGJPAuth(Post, IPAddr) { 89 | connector.Error("-1", "Invalid credentials") 90 | return 91 | } 92 | cl := core.CLevelList{DB: db} 93 | 94 | cl.UID = xacc.Uid 95 | core.TryInt(&cl.ID, Post.Get("listID")) 96 | cl.Name = core.ClearGDRequest(Post.Get("listName")) 97 | cl.Description = core.ClearGDRequest(Post.Get("listDesc")) 98 | cl.Levels = core.ClearGDRequest(Post.Get("listLevels")) 99 | core.TryInt(&cl.Difficulty, Post.Get("difficulty")) 100 | core.TryInt(&cl.Version, Post.Get("listVersion")) 101 | //core.TryInt(&cl., Post.Get("original")) //??? 102 | core.TryInt(&cl.Unlisted, Post.Get("unlisted")) 103 | 104 | if cl.Name == "" { 105 | cl.Name = "Unnamed" 106 | } 107 | if cl.Version == 0 { 108 | cl.Version = 1 109 | } 110 | 111 | cl.Preload() // Because we have no tables 112 | 113 | if cl.IsOwnedBy(xacc.Uid) { 114 | res := cl.UpdateList() 115 | if res > 0 { 116 | connector.NumberedSuccess(res) 117 | } else { 118 | connector.Error("-1", "Failed to update list") 119 | } 120 | } else { 121 | if !cl.CheckParams() { 122 | connector.Error("-1", "Invalid parameters") 123 | return 124 | } 125 | res := cl.UploadList() 126 | if res > 0 { 127 | connector.NumberedSuccess(res) 128 | } else { 129 | connector.Error("-1", "Failed to upload list") 130 | } 131 | } 132 | } else { 133 | connector.Error("-1", "Bad Request") 134 | } 135 | } 136 | 137 | func LevelListSearch(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 138 | IPAddr := ipOf(req) 139 | vars := gorilla.Vars(req) 140 | logger := core.Logger{Output: os.Stderr} 141 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 142 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 143 | config, err := conf.LoadById(vars["gdps"]) 144 | if logger.Should(err) != nil { 145 | connector.Error("-1", "Not Found") 146 | return 147 | } 148 | if core.CheckIPBan(IPAddr, config) { 149 | connector.Error("-1", "Banned") 150 | return 151 | } 152 | 153 | Post := ReadPost(req) 154 | log.Printf("%s: %s\n", vars["gdps"], Post) 155 | 156 | var mode, page int 157 | core.TryInt(&mode, Post.Get("type")) 158 | core.TryInt(&page, Post.Get("page")) 159 | 160 | Params := make(map[string]string) 161 | if sterm := Post.Get("str"); sterm != "" { 162 | Params["sterm"] = core.ClearGDRequest(Post.Get("str")) 163 | } 164 | 165 | //Difficulty selector 166 | if diff := Post.Get("diff"); diff != "" { 167 | preg, err := regexp.Compile("[^0-9,-]") 168 | if logger.Should(err) != nil { 169 | connector.Error("-1", "Invalid difficulty filter") 170 | return 171 | } 172 | diff = core.CleanDoubles(preg.ReplaceAllString(diff, ""), ",") 173 | if diff != "-" && diff != "," { 174 | // The real diff filter begins 175 | difflist := strings.Split(diff, ",") 176 | var diffl []string 177 | for _, sdiff := range difflist { 178 | if sdiff == "" || sdiff == "-" { 179 | continue 180 | } 181 | diffl = append(diffl, sdiff) 182 | } 183 | Params["diff"] = strings.Join(diffl, ",") 184 | } 185 | } 186 | 187 | //Other params 188 | 189 | var star int 190 | 191 | core.TryInt(&star, Post.Get("star")) 192 | if star != 0 { 193 | Params["star"] = "1" 194 | } 195 | 196 | db := &core.MySQLConn{} 197 | 198 | if logger.Should(db.ConnectBlob(config)) != nil { 199 | serverError(connector) 200 | return 201 | } 202 | filter := core.CLevelListFilter{DB: db} 203 | var lists []int 204 | 205 | switch Post.Get("type") { 206 | case "1": 207 | lists = filter.SearchLists(page, Params, core.CLEVELLISTFILTER_MOSTDOWNLOADED) 208 | case "3": 209 | lists = filter.SearchLists(page, Params, core.CLEVELLISTFILTER_TRENDING) 210 | case "4": 211 | lists = filter.SearchLists(page, Params, core.CLEVELLISTFILTER_LATEST) 212 | case "5": 213 | lists = filter.SearchUserLists(page, Params, false) //User lists (uid in sterm) 214 | case "7": 215 | lists = filter.SearchLists(page, Params, core.CLEVELLISTFILTER_MAGIC) // Robtop lobotomy 216 | case "11": 217 | lists = filter.SearchLists(page, Params, core.CLEVELLISTFILTER_AWARDED) //Awarded tab 218 | case "12": 219 | //Follow levels 220 | preg, err := regexp.Compile("[^0-9,-]") 221 | if logger.Should(err) != nil { 222 | return 223 | } 224 | Params["followList"] = preg.ReplaceAllString(core.ClearGDRequest(Post.Get("followed")), "") 225 | if Params["followList"] == "" { 226 | break 227 | } 228 | lists = filter.SearchUserLists(page, Params, true) 229 | case "13": 230 | //Friend levels 231 | xacc := core.CAccount{DB: db} 232 | if !(core.CheckGDAuth(Post) && xacc.PerformGJPAuth(Post, IPAddr)) { 233 | break 234 | } 235 | xacc.LoadSocial() 236 | if xacc.FriendsCount == 0 { 237 | break 238 | } 239 | fr := core.CFriendship{DB: db} 240 | friendships := core.Decompose(core.CleanDoubles(xacc.FriendshipIds, ","), ",") 241 | friends := []int{xacc.Uid} 242 | for _, frid := range friendships { 243 | id1, id2 := fr.GetFriendByFID(frid) 244 | fid := id1 245 | if id1 == xacc.Uid { 246 | fid = id2 247 | } 248 | friends = append(friends, fid) 249 | } 250 | Params["followList"] = strings.Join(core.ArrTranslate(friends), ",") 251 | lists = filter.SearchUserLists(page, Params, false) 252 | case "27": 253 | lists = filter.SearchLists(page, Params, core.CLEVELLISTFILTER_SENT) 254 | 255 | default: 256 | lists = filter.SearchLists(page, Params, core.CLEVELLISTFILTER_MOSTLIKED) 257 | } 258 | 259 | //Output, begins! 260 | if len(lists) == 0 { 261 | connector.Error("-2", "No results found") 262 | return 263 | } 264 | 265 | listCore := core.CLevelList{DB: db} 266 | llistsX := listCore.LoadBulkSearch(lists) 267 | connector.Level_SearchList(lists, llistsX, filter.Count, page) 268 | } 269 | -------------------------------------------------------------------------------- /src/api/level_packs.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "HalogenGhostCore/core/connectors" 6 | gorilla "github.com/gorilla/mux" 7 | "io" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | func GetGauntlets(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 13 | IPAddr := ipOf(req) 14 | vars := gorilla.Vars(req) 15 | logger := core.Logger{Output: os.Stderr} 16 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 17 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 18 | config, err := conf.LoadById(vars["gdps"]) 19 | if logger.Should(err) != nil { 20 | connector.Error("-1", "Not Found") 21 | return 22 | } 23 | if core.CheckIPBan(IPAddr, config) { 24 | connector.Error("-1", "Banned") 25 | return 26 | } 27 | 28 | //Post:=ReadPost(req) 29 | db := &core.MySQLConn{} 30 | 31 | if logger.Should(db.ConnectBlob(config)) != nil { 32 | serverError(connector) 33 | return 34 | } 35 | filter := core.CLevelFilter{DB: db} 36 | gaus, hash := filter.GetGauntlets() 37 | connector.Level_GetGauntlets(gaus, hash) 38 | } 39 | 40 | func GetMapPacks(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 41 | IPAddr := ipOf(req) 42 | vars := gorilla.Vars(req) 43 | logger := core.Logger{Output: os.Stderr} 44 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 45 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 46 | config, err := conf.LoadById(vars["gdps"]) 47 | if logger.Should(err) != nil { 48 | connector.Error("-1", "Not Found") 49 | return 50 | } 51 | if core.CheckIPBan(IPAddr, config) { 52 | connector.Error("-1", "Banned") 53 | return 54 | } 55 | 56 | Post := ReadPost(req) 57 | db := &core.MySQLConn{} 58 | 59 | if logger.Should(db.ConnectBlob(config)) != nil { 60 | serverError(connector) 61 | return 62 | } 63 | filter := core.CLevelFilter{DB: db} 64 | var page int 65 | core.TryInt(&page, Post.Get("page")) 66 | packs, count := filter.GetMapPacks(page) 67 | connector.Level_GetMapPacks(packs, count, page) 68 | } 69 | -------------------------------------------------------------------------------- /src/api/level_rates.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "HalogenGhostCore/core/connectors" 6 | gorilla "github.com/gorilla/mux" 7 | "io" 8 | "net/http" 9 | "os" 10 | "strconv" 11 | ) 12 | 13 | func RateDemon(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 14 | IPAddr := ipOf(req) 15 | vars := gorilla.Vars(req) 16 | logger := core.Logger{Output: os.Stderr} 17 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 18 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 19 | config, err := conf.LoadById(vars["gdps"]) 20 | if logger.Should(err) != nil { 21 | connector.Error("-1", "Not Found") 22 | return 23 | } 24 | if core.CheckIPBan(IPAddr, config) { 25 | connector.Error("-1", "Banned") 26 | return 27 | } 28 | 29 | Post := ReadPost(req) 30 | if core.CheckGDAuth(Post) && Post.Get("levelID") != "" { 31 | db := &core.MySQLConn{} 32 | 33 | if logger.Should(db.ConnectBlob(config)) != nil { 34 | serverError(connector) 35 | return 36 | } 37 | xacc := core.CAccount{DB: db} 38 | if !xacc.PerformGJPAuth(Post, IPAddr) { 39 | connector.Error("-1", "Invalid Credentials") 40 | return 41 | } 42 | var lvl_id int 43 | core.TryInt(&lvl_id, Post.Get("levelID")) 44 | cl := core.CLevel{DB: db, Id: lvl_id} 45 | if !cl.Exists(cl.Id) { 46 | connector.Error("-1", "Level not found") 47 | return 48 | } 49 | role := xacc.GetRoleObj(true) 50 | var diff, mode int 51 | core.TryInt(&mode, Post.Get("mode")) 52 | core.TryInt(&diff, Post.Get("rating")) 53 | if len(role.Privs) > 0 && role.Privs["aRateDemon"] > 0 && mode != 0 { 54 | cl.RateDemon(diff % 6) 55 | if config.ServerConfig.EnableModules["discord"] { 56 | cl.LoadMain() 57 | cl.LoadParams() 58 | cl.LoadStats() 59 | builder := "[deleted]" 60 | acc := core.CAccount{DB: db, Uid: cl.Uid} 61 | if acc.Exists(acc.Uid) { 62 | acc.LoadAuth(core.CAUTH_UID) 63 | } 64 | if len(acc.Uname) > 0 { 65 | builder = acc.Uname 66 | } 67 | data := map[string]string{ 68 | "id": strconv.Itoa(cl.Id), 69 | "name": cl.Name, 70 | "builder": builder, 71 | "diff": core.DiffToText(cl.StarsGot, cl.DemonDifficulty, cl.IsFeatured, cl.IsEpic), 72 | "stars": strconv.Itoa(cl.StarsGot), 73 | "likes": strconv.Itoa(cl.Likes), 74 | "downloads": strconv.Itoa(cl.Downloads), 75 | "len": strconv.Itoa(cl.Length), 76 | "rateuser": xacc.Uname, 77 | } 78 | core.SendAPIWebhook(vars["gdps"], "rate", data) 79 | } 80 | connector.NumberedSuccess(cl.Id) 81 | } else { 82 | connector.Error("-1", "Insufficient privileges") 83 | } 84 | } else { 85 | connector.Error("-1", "Bad Request") 86 | } 87 | } 88 | 89 | func RateStar(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 90 | IPAddr := ipOf(req) 91 | vars := gorilla.Vars(req) 92 | logger := core.Logger{Output: os.Stderr} 93 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 94 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 95 | config, err := conf.LoadById(vars["gdps"]) 96 | if logger.Should(err) != nil { 97 | connector.Error("-1", "Not Found") 98 | return 99 | } 100 | if core.CheckIPBan(IPAddr, config) { 101 | connector.Error("-1", "Banned") 102 | return 103 | } 104 | 105 | Post := ReadPost(req) 106 | if core.CheckGDAuth(Post) && Post.Get("levelID") != "" { 107 | db := &core.MySQLConn{} 108 | 109 | if logger.Should(db.ConnectBlob(config)) != nil { 110 | serverError(connector) 111 | return 112 | } 113 | xacc := core.CAccount{DB: db} 114 | if !xacc.PerformGJPAuth(Post, IPAddr) { 115 | connector.Error("-1", "Invalid Credentials") 116 | return 117 | } 118 | var lvl_id int 119 | core.TryInt(&lvl_id, Post.Get("levelID")) 120 | cl := core.CLevel{DB: db, Id: lvl_id} 121 | if !cl.Exists(cl.Id) { 122 | connector.Error("-1", "Level not found") 123 | return 124 | } 125 | var diff int 126 | core.TryInt(&diff, Post.Get("stars")) 127 | cl.LoadMain() 128 | cl.DoSuggestDifficulty(diff % 11) 129 | connector.Success("Difficulty suggested") 130 | } else { 131 | connector.Error("-1", "Bad Request") 132 | } 133 | } 134 | 135 | func SuggestStars(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 136 | IPAddr := ipOf(req) 137 | vars := gorilla.Vars(req) 138 | logger := core.Logger{Output: os.Stderr} 139 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 140 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 141 | config, err := conf.LoadById(vars["gdps"]) 142 | if logger.Should(err) != nil { 143 | connector.Error("-1", "Not Found") 144 | return 145 | } 146 | if core.CheckIPBan(IPAddr, config) { 147 | connector.Error("-1", "Banned") 148 | return 149 | } 150 | 151 | Post := ReadPost(req) 152 | if core.CheckGDAuth(Post) && Post.Get("levelID") != "" { 153 | db := &core.MySQLConn{} 154 | 155 | if logger.Should(db.ConnectBlob(config)) != nil { 156 | serverError(connector) 157 | return 158 | } 159 | xacc := core.CAccount{DB: db} 160 | if !xacc.PerformGJPAuth(Post, IPAddr) { 161 | connector.Error("-1", "Invalid Credentials") 162 | return 163 | } 164 | var lvl_id int 165 | core.TryInt(&lvl_id, Post.Get("levelID")) 166 | cl := core.CLevel{DB: db, Id: lvl_id} 167 | if !cl.Exists(cl.Id) { 168 | connector.Error("-1", "Level not found") 169 | return 170 | } 171 | cl.LoadMain() 172 | role := xacc.GetRoleObj(true) 173 | var diff, isFeature int 174 | core.TryInt(&isFeature, Post.Get("feature")) 175 | core.TryInt(&diff, Post.Get("stars")) 176 | if len(role.Privs) > 0 { 177 | if role.Privs["aRateStars"] > 0 { 178 | diff = diff % 11 179 | if v, _ := role.Privs["aRateNoDemon"]; v > 0 && diff == 10 { 180 | connector.Error("-1", "Insufficient privileges") 181 | return 182 | } 183 | cl.RateLevel(diff) 184 | cl.FeatureLevel(isFeature % 5) 185 | switch isFeature { 186 | case 2: 187 | cl.EpicLevel(true) 188 | case 3: 189 | cl.LegendaryLevel(true) 190 | case 4: 191 | cl.MythicLevel(true) 192 | default: 193 | cl.EpicLevel(false) 194 | } 195 | if config.ServerConfig.EnableModules["discord"] { 196 | cl.LoadMain() 197 | cl.LoadParams() 198 | cl.LoadStats() 199 | builder := "[deleted]" 200 | acc := core.CAccount{DB: db, Uid: cl.Uid} 201 | if acc.Exists(acc.Uid) { 202 | acc.LoadAuth(core.CAUTH_UID) 203 | } 204 | if len(acc.Uname) > 0 { 205 | builder = acc.Uname 206 | } 207 | data := map[string]string{ 208 | "id": strconv.Itoa(cl.Id), 209 | "name": cl.Name, 210 | "builder": builder, 211 | "diff": core.DiffToText(cl.StarsGot, 3, isFeature, cl.IsEpic), 212 | "stars": strconv.Itoa(diff), 213 | "likes": strconv.Itoa(cl.Likes), 214 | "downloads": strconv.Itoa(cl.Downloads), 215 | "len": strconv.Itoa(cl.Length), 216 | "rateuser": xacc.Uname, 217 | } 218 | core.SendAPIWebhook(vars["gdps"], "rate", data) 219 | } 220 | core.RegisterAction(core.ACTION_LEVEL_RATE, xacc.Uid, cl.Id, map[string]string{ 221 | "uname": xacc.Uname, "type": "StarRate:" + strconv.Itoa(diff%11), 222 | }, db) 223 | if isFeature != 0 { 224 | core.RegisterAction(core.ACTION_LEVEL_RATE, xacc.Uid, cl.Id, map[string]string{ 225 | "uname": xacc.Uname, "type": "Feature", 226 | }, db) 227 | } 228 | } else if role.Privs["aRateReq"] > 0 { 229 | cl.SendReq(xacc.Uid, diff%11, isFeature) 230 | } else { 231 | connector.Error("-1", "Insufficient privileges") 232 | return 233 | } 234 | connector.Success("Rated successfully") 235 | } else { 236 | connector.Error("-1", "Insufficient privileges") 237 | } 238 | } else { 239 | connector.Error("-1", "Bad Request") 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/api/profile.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "HalogenGhostCore/core/connectors" 6 | gorilla "github.com/gorilla/mux" 7 | "io" 8 | "net/http" 9 | "os" 10 | "slices" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | func GetUserInfo(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 16 | IPAddr := ipOf(req) 17 | vars := gorilla.Vars(req) 18 | logger := core.Logger{Output: os.Stderr} 19 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 20 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 21 | config, err := conf.LoadById(vars["gdps"]) 22 | if logger.Should(err) != nil { 23 | connector.Error("-1", "Not Found") 24 | return 25 | } 26 | if core.CheckIPBan(IPAddr, config) { 27 | connector.Error("-1", "Banned") 28 | return 29 | } 30 | 31 | Post := ReadPost(req) 32 | if Post.Get("targetAccountID") != "" { 33 | db := &core.MySQLConn{} 34 | 35 | if logger.Should(db.ConnectBlob(config)) != nil { 36 | serverError(connector) 37 | return 38 | } 39 | acc := core.CAccount{DB: db} 40 | var uidSelf int 41 | core.TryInt(&acc.Uid, Post.Get("targetAccountID")) 42 | if core.CheckGDAuth(Post) { 43 | xacc := core.CAccount{DB: db} 44 | if !xacc.PerformGJPAuth(Post, IPAddr) { 45 | uidSelf = 0 46 | } else { 47 | uidSelf = xacc.Uid 48 | } 49 | } 50 | if !acc.Exists(acc.Uid) { 51 | connector.Error("-1", "User not Found") 52 | return 53 | } 54 | acc.LoadAll() 55 | blacklist := strings.Split(acc.Blacklist, ",") 56 | if uidSelf > 0 && slices.Contains(blacklist, strconv.Itoa(uidSelf)) { 57 | connector.Error("-1", "User has blacklisted you") 58 | return 59 | } 60 | 61 | connector.Profile_GetUserProfile(acc, uidSelf) 62 | } else { 63 | connector.Error("-1", "Bad Request") 64 | } 65 | } 66 | 67 | func GetUserList(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 68 | IPAddr := ipOf(req) 69 | vars := gorilla.Vars(req) 70 | logger := core.Logger{Output: os.Stderr} 71 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 72 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 73 | config, err := conf.LoadById(vars["gdps"]) 74 | if logger.Should(err) != nil { 75 | connector.Error("-1", "Not Found") 76 | return 77 | } 78 | if core.CheckIPBan(IPAddr, config) { 79 | connector.Error("-1", "Banned") 80 | return 81 | } 82 | 83 | Post := ReadPost(req) 84 | if core.CheckGDAuth(Post) { 85 | db := &core.MySQLConn{} 86 | 87 | if logger.Should(db.ConnectBlob(config)) != nil { 88 | serverError(connector) 89 | return 90 | } 91 | var cType int 92 | core.TryInt(&cType, Post.Get("type")) 93 | xacc := core.CAccount{DB: db} 94 | if !xacc.PerformGJPAuth(Post, IPAddr) { 95 | connector.Error("-1", "Invalid Credentials") 96 | return 97 | } 98 | xacc.LoadSocial() 99 | var usersToDump []core.CAccount 100 | switch cType { 101 | case 0: 102 | if xacc.FriendsCount == 0 { 103 | connector.Error("-2", "No Friends") 104 | return 105 | } else { 106 | flist := strings.Split(xacc.FriendshipIds, ",") 107 | for _, fid := range flist { 108 | var Xfid int 109 | core.TryInt(&Xfid, fid) 110 | cf := core.CFriendship{DB: db} 111 | uid1, uid2 := cf.GetFriendByFID(Xfid) 112 | acc := core.CAccount{DB: db, Uid: uid1} 113 | if uid1 == xacc.Uid { 114 | acc.Uid = uid2 115 | } 116 | acc.LoadAuth(core.CAUTH_UID) 117 | acc.LoadVessels() 118 | acc.LoadStats() 119 | usersToDump = append(usersToDump, acc) 120 | } 121 | } 122 | case 1: 123 | blacklist := strings.Split(xacc.Blacklist, ",") 124 | if len(xacc.Blacklist) == 0 || len(blacklist) == 0 { 125 | connector.Error("-2", "No blacklisted users") 126 | return 127 | } else { 128 | for _, buid := range blacklist { 129 | acc := core.CAccount{DB: db} 130 | core.TryInt(&acc.Uid, buid) 131 | if acc.Uid == 0 { 132 | continue 133 | } 134 | acc.LoadAuth(core.CAUTH_UID) 135 | acc.LoadVessels() 136 | acc.LoadStats() 137 | usersToDump = append(usersToDump, acc) 138 | } 139 | } 140 | default: 141 | connector.Error("-1", "Bad Request") 142 | return 143 | } 144 | 145 | connector.Profile_ListUserProfiles(usersToDump) 146 | } else { 147 | connector.Error("-1", "Bad Request") 148 | } 149 | } 150 | 151 | func GetUsers(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 152 | IPAddr := ipOf(req) 153 | vars := gorilla.Vars(req) 154 | logger := core.Logger{Output: os.Stderr} 155 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 156 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 157 | config, err := conf.LoadById(vars["gdps"]) 158 | if logger.Should(err) != nil { 159 | connector.Error("-1", "Not Found") 160 | return 161 | } 162 | if core.CheckIPBan(IPAddr, config) { 163 | connector.Error("-1", "Banned") 164 | return 165 | } 166 | 167 | Post := ReadPost(req) 168 | if Post.Get("str") != "" { 169 | db := &core.MySQLConn{} 170 | 171 | if logger.Should(db.ConnectBlob(config)) != nil { 172 | serverError(connector) 173 | return 174 | } 175 | xacc := core.CAccount{DB: db} 176 | uids := xacc.SearchUsers(core.ClearGDRequest(Post.Get("str"))) 177 | if len(uids) == 0 { 178 | connector.Error("-1", "No users found") 179 | } else { 180 | var accs []core.CAccount 181 | for _, uid := range uids { 182 | accs = append(accs, core.CAccount{DB: db, Uid: uid}) 183 | } 184 | connector.Profile_GetSearchableUsers(accs, len(uids), 0) 185 | } 186 | } else { 187 | connector.Error("-1", "Bad Request") 188 | } 189 | } 190 | 191 | func UpdateAccountSettings(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 192 | IPAddr := ipOf(req) 193 | vars := gorilla.Vars(req) 194 | logger := core.Logger{Output: os.Stderr} 195 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 196 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 197 | config, err := conf.LoadById(vars["gdps"]) 198 | if logger.Should(err) != nil { 199 | connector.Error("-1", "Not Found") 200 | return 201 | } 202 | if core.CheckIPBan(IPAddr, config) { 203 | connector.Error("-1", "Banned") 204 | return 205 | } 206 | 207 | Post := ReadPost(req) 208 | if core.CheckGDAuth(Post) { 209 | db := &core.MySQLConn{} 210 | 211 | if logger.Should(db.ConnectBlob(config)) != nil { 212 | serverError(connector) 213 | return 214 | } 215 | xacc := core.CAccount{DB: db} 216 | if !xacc.PerformGJPAuth(Post, IPAddr) { 217 | connector.Error("-1", "Invalid Credentials") 218 | return 219 | } 220 | 221 | core.TryInt(&xacc.MS, Post.Get("mS")) 222 | core.TryInt(&xacc.FrS, Post.Get("frS")) 223 | core.TryInt(&xacc.CS, Post.Get("cS")) 224 | xacc.Youtube = core.ClearGDRequest(Post.Get("yt")) 225 | xacc.Twitter = core.ClearGDRequest(Post.Get("twitter")) 226 | xacc.Twitch = core.ClearGDRequest(Post.Get("twitch")) 227 | xacc.PushSettings() 228 | connector.Success("Account updated") 229 | } else { 230 | connector.Error("-1", "Bad Request") 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/api/rewards.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "HalogenGhostCore/core/connectors" 6 | "encoding/base64" 7 | gorilla "github.com/gorilla/mux" 8 | "io" 9 | "net/http" 10 | "os" 11 | "time" 12 | ) 13 | 14 | func GetChallenges(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 15 | IPAddr := ipOf(req) 16 | vars := gorilla.Vars(req) 17 | logger := core.Logger{Output: os.Stderr} 18 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 19 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 20 | config, err := conf.LoadById(vars["gdps"]) 21 | if logger.Should(err) != nil { 22 | connector.Error("-1", "Not Found") 23 | return 24 | } 25 | if core.CheckIPBan(IPAddr, config) { 26 | connector.Error("-1", "Banned") 27 | return 28 | } 29 | 30 | Post := ReadPost(req) 31 | if Post.Get("chk") != "" && Post.Get("udid") != "" { 32 | db := &core.MySQLConn{} 33 | 34 | if logger.Should(db.ConnectBlob(config)) != nil { 35 | serverError(connector) 36 | return 37 | } 38 | cq := core.CQuests{DB: db} 39 | if cq.Exists(core.QUEST_TYPE_CHALLENGE) { 40 | chalk, _ := base64.StdEncoding.DecodeString(Post.Get("chk")[5:]) 41 | chk := core.DoXOR(string(chalk), "19847") 42 | var uid int 43 | core.TryInt(&uid, Post.Get("accountID")) 44 | connector.Rewards_ChallengesOutput(cq, uid, chk, Post.Get("udid")) 45 | } else { 46 | connector.Error("-2", "Challenge not found") 47 | } 48 | } else { 49 | connector.Error("-1", "Bad Request") 50 | } 51 | } 52 | 53 | func GetRewards(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 54 | IPAddr := ipOf(req) 55 | vars := gorilla.Vars(req) 56 | logger := core.Logger{Output: os.Stderr} 57 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 58 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 59 | config, err := conf.LoadById(vars["gdps"]) 60 | if logger.Should(err) != nil { 61 | connector.Error("-1", "Not Found") 62 | return 63 | } 64 | if core.CheckIPBan(IPAddr, config) { 65 | connector.Error("-1", "Banned") 66 | return 67 | } 68 | 69 | Post := ReadPost(req) 70 | if core.CheckGDAuth(Post) && len(Post.Get("chk")) > 5 && Post.Get("udid") != "" { 71 | db := &core.MySQLConn{} 72 | 73 | if logger.Should(db.ConnectBlob(config)) != nil { 74 | serverError(connector) 75 | return 76 | } 77 | xacc := core.CAccount{DB: db} 78 | if !xacc.PerformGJPAuth(Post, IPAddr) { 79 | connector.Error("-1", "Invalid Credentials") 80 | return 81 | } 82 | var chestType int 83 | core.TryInt(&chestType, Post.Get("rewardType")) 84 | chestType %= 3 //Strip to 2 options 85 | xacc.LoadChests() 86 | chalk, _ := base64.StdEncoding.DecodeString(Post.Get("chk")[5:]) 87 | chk := core.DoXOR(string(chalk), "59182") 88 | 89 | chestSmallLeft := core.MaxInt(0, config.ChestConfig.ChestSmallWait-100+xacc.ChestSmallTime-int(time.Now().Unix())) //!+10800 90 | chestBigLeft := core.MaxInt(0, config.ChestConfig.ChestBigWait-100+xacc.ChestBigTime-int(time.Now().Unix())) //!+10800 91 | switch chestType { 92 | case 1: 93 | if chestSmallLeft == 0 { 94 | xacc.ChestSmallCount++ 95 | xacc.ChestSmallTime = int(time.Now().Unix()) 96 | xacc.PushChests() 97 | chestSmallLeft = config.ChestConfig.ChestSmallWait 98 | } else { 99 | connector.Error("-1", "Small chest is not ready yet") 100 | return 101 | } 102 | case 2: 103 | if chestBigLeft == 0 { 104 | xacc.ChestBigCount++ 105 | xacc.ChestBigTime = int(time.Now().Unix()) 106 | xacc.PushChests() 107 | chestBigLeft = config.ChestConfig.ChestBigWait 108 | } else { 109 | connector.Error("-1", "Big chest is not ready yet") 110 | return 111 | } 112 | } 113 | connector.Rewards_ChestOutput(xacc, config, Post.Get("udid"), chk, chestSmallLeft, chestBigLeft, chestType) 114 | } else { 115 | connector.Error("-1", "Bad Request") 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/api/score.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "HalogenGhostCore/core/connectors" 6 | gorilla "github.com/gorilla/mux" 7 | "io" 8 | "math" 9 | "net/http" 10 | "os" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | func GetCreators(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 16 | IPAddr := ipOf(req) 17 | vars := gorilla.Vars(req) 18 | logger := core.Logger{Output: os.Stderr} 19 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 20 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 21 | config, err := conf.LoadById(vars["gdps"]) 22 | if logger.Should(err) != nil { 23 | connector.Error("-1", "Not Found") 24 | return 25 | } 26 | if core.CheckIPBan(IPAddr, config) { 27 | connector.Error("-1", "Banned") 28 | return 29 | } 30 | 31 | //Post:=ReadPost(req) 32 | db := &core.MySQLConn{} 33 | 34 | if logger.Should(db.ConnectBlob(config)) != nil { 35 | serverError(connector) 36 | return 37 | } 38 | acc := core.CAccount{DB: db} 39 | users := acc.GetLeaderboard(core.CLEADERBOARD_BY_CPOINTS, []string{}, 0, config.ServerConfig.TopSize) 40 | if len(users) == 0 { 41 | connector.Error("-2", "No users found") 42 | } else { 43 | xacc := core.CAccount{DB: db} 44 | connector.Score_GetLeaderboard(users, xacc) 45 | } 46 | } 47 | 48 | func GetLevelScores(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 49 | IPAddr := ipOf(req) 50 | vars := gorilla.Vars(req) 51 | logger := core.Logger{Output: os.Stderr} 52 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 53 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 54 | config, err := conf.LoadById(vars["gdps"]) 55 | if logger.Should(err) != nil { 56 | connector.Error("-1", "Not Found") 57 | return 58 | } 59 | if core.CheckIPBan(IPAddr, config) { 60 | connector.Error("-1", "Banned") 61 | return 62 | } 63 | 64 | Post := ReadPost(req) 65 | if core.CheckGDAuth(Post) && Post.Get("levelID") != "" { 66 | db := &core.MySQLConn{} 67 | 68 | if logger.Should(db.ConnectBlob(config)) != nil { 69 | serverError(connector) 70 | return 71 | } 72 | xacc := core.CAccount{DB: db} 73 | if !xacc.PerformGJPAuth(Post, IPAddr) { 74 | connector.Error("-1", "Invalid credentials") 75 | return 76 | } 77 | 78 | cs := core.CScores{DB: db} 79 | 80 | var percent, attempts, coins, lvlId, mode int 81 | core.TryInt(&lvlId, Post.Get("levelID")) 82 | core.TryInt(&mode, Post.Get("mode")) 83 | core.TryInt(&percent, Post.Get("percent")) 84 | core.TryInt(&attempts, Post.Get("s1")) 85 | core.TryInt(&coins, Post.Get("s9")) 86 | percent = int(math.Abs(float64(percent))) % 101 87 | attempts = int(math.Abs(float64(attempts))) 88 | if percent > 0 && attempts > 0 { 89 | // Upload score 90 | if attempts < 8355 { 91 | attempts = 1 92 | } else { 93 | attempts -= 8354 94 | } 95 | if coins < 5820 { 96 | coins = 0 97 | } else { 98 | coins = (coins - 5819) % 4 99 | } 100 | cs.Uid = xacc.Uid 101 | cs.LvlId = lvlId 102 | cs.Percent = percent 103 | cs.Attempts = attempts 104 | cs.Coins = coins 105 | if cs.ScoreExistsByUid(xacc.Uid, lvlId) { 106 | cs.UpdateLevelScore() 107 | } else { 108 | cs.UploadLevelScore() 109 | } 110 | } 111 | //Retrieve all scores 112 | scores := cs.GetScoresForLevelId(lvlId, mode%4+400, xacc) 113 | if len(scores) == 0 { 114 | connector.Error("-2", "No scores found") 115 | return 116 | } 117 | connector.Score_GetScores(scores, "default") 118 | } else { 119 | connector.Error("-1", "Bad Request") 120 | } 121 | } 122 | 123 | func GetLevelPlatScores(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 124 | IPAddr := ipOf(req) 125 | vars := gorilla.Vars(req) 126 | logger := core.Logger{Output: os.Stderr} 127 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 128 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 129 | config, err := conf.LoadById(vars["gdps"]) 130 | if logger.Should(err) != nil { 131 | connector.Error("-1", "Not Found") 132 | return 133 | } 134 | if core.CheckIPBan(IPAddr, config) { 135 | connector.Error("-1", "Banned") 136 | return 137 | } 138 | 139 | Post := ReadPost(req) 140 | if core.CheckGDAuth(Post) && Post.Get("levelID") != "" { 141 | db := &core.MySQLConn{} 142 | 143 | if logger.Should(db.ConnectBlob(config)) != nil { 144 | serverError(connector) 145 | return 146 | } 147 | xacc := core.CAccount{DB: db} 148 | if !xacc.PerformGJPAuth(Post, IPAddr) { 149 | connector.Error("-1", "Invalid credentials") 150 | return 151 | } 152 | 153 | cs := core.CScores{DB: db} 154 | 155 | var percent, time, points, attempts, coins, lvlId, xtype, mode int 156 | core.TryInt(&lvlId, Post.Get("levelID")) 157 | core.TryInt(&xtype, Post.Get("type")) 158 | core.TryInt(&mode, Post.Get("mode")) 159 | core.TryInt(&percent, Post.Get("percent")) 160 | core.TryInt(&points, Post.Get("points")) 161 | core.TryInt(&time, Post.Get("time")) 162 | core.TryInt(&attempts, Post.Get("s1")) 163 | core.TryInt(&coins, Post.Get("s9")) 164 | 165 | // COINS = POINTS 166 | // ATTEMPTS = TIME 167 | //1: Username 168 | //2: playerID 169 | //3: время прохождения в миллисекундах или поинты 170 | //6: ранг 171 | //9: иконка 172 | //10: цвет1 173 | //11: цвет2 174 | //14: тип иконки 175 | //15: special 176 | //16: accountID 177 | //42: как давно 178 | percent = int(math.Abs(float64(percent))) % 101 179 | attempts = int(math.Abs(float64(attempts))) 180 | if percent > 0 && attempts > 0 { 181 | // Upload score 182 | cs.Attempts = time 183 | cs.Coins = points 184 | cs.Uid = xacc.Uid 185 | cs.LvlId = lvlId 186 | cs.Percent = percent 187 | if cs.ScoreExistsByUid(xacc.Uid, lvlId) { 188 | cs.UpdateLevelScore() 189 | } else { 190 | cs.UploadLevelScore() 191 | } 192 | } 193 | //Retrieve all scores 194 | scores := cs.GetScoresForPlatformerLevelId(lvlId, xtype%4+500, mode == 1, xacc) 195 | if len(scores) == 0 { 196 | connector.Error("-2", "No scores found") 197 | return 198 | } 199 | modes := "attempts" 200 | if mode == 1 { 201 | modes = "coins" 202 | } 203 | connector.Score_GetScores(scores, modes) 204 | } else { 205 | connector.Error("-1", "Bad Request") 206 | } 207 | } 208 | 209 | func GetScores(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 210 | IPAddr := ipOf(req) 211 | vars := gorilla.Vars(req) 212 | logger := core.Logger{Output: os.Stderr} 213 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 214 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 215 | config, err := conf.LoadById(vars["gdps"]) 216 | if logger.Should(err) != nil { 217 | connector.Error("-1", "Not Found") 218 | return 219 | } 220 | if core.CheckIPBan(IPAddr, config) { 221 | connector.Error("-1", "Banned") 222 | return 223 | } 224 | 225 | Post := ReadPost(req) 226 | xType := Post.Get("type") 227 | if xType == "" { 228 | xType = "top" 229 | } 230 | db := &core.MySQLConn{} 231 | 232 | if logger.Should(db.ConnectBlob(config)) != nil { 233 | serverError(connector) 234 | return 235 | } 236 | acc := core.CAccount{DB: db} 237 | var users []int 238 | switch xType { 239 | case "relative": 240 | if !acc.PerformGJPAuth(Post, IPAddr) { 241 | connector.Error("-1", "Invalid credentials") 242 | return 243 | } 244 | acc.LoadStats() 245 | users = acc.GetLeaderboard(core.CLEADERBOARD_GLOBAL, []string{}, acc.Stars, config.ServerConfig.TopSize) 246 | case "friends": 247 | if !acc.PerformGJPAuth(Post, IPAddr) { 248 | connector.Error("-1", "Invalid credentials") 249 | return 250 | } 251 | acc.LoadSocial() 252 | if acc.FriendsCount == 0 { 253 | users = []int{} 254 | break 255 | } 256 | cf := core.CFriendship{DB: db} 257 | frs := strings.Split(acc.FriendshipIds, ",") 258 | var friends []string 259 | for _, fr := range frs { 260 | id, err := strconv.Atoi(fr) 261 | if err != nil { 262 | continue 263 | } 264 | uid1, uid2 := cf.GetFriendByFID(id) 265 | if uid1 == 0 { 266 | continue 267 | } 268 | xuid := uid1 269 | if acc.Uid == uid1 { 270 | xuid = uid2 271 | } 272 | friends = append(friends, strconv.Itoa(xuid)) 273 | } 274 | friends = append(friends, strconv.Itoa(acc.Uid)) 275 | users = acc.GetLeaderboard(core.CLEADERBOARD_FRIENDS, friends, 0, config.ServerConfig.TopSize) 276 | case "creators": 277 | users = acc.GetLeaderboard(core.CLEADERBOARD_BY_CPOINTS, []string{}, 0, config.ServerConfig.TopSize) 278 | default: 279 | users = acc.GetLeaderboard(core.CLEADERBOARD_BY_STARS, []string{}, 0, config.ServerConfig.TopSize) 280 | } 281 | if len(users) == 0 { 282 | connector.Error("-2", "No users found") 283 | } else { 284 | connector.Score_GetLeaderboard(users, acc) 285 | } 286 | } 287 | 288 | func UpdateUserScore(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 289 | IPAddr := ipOf(req) 290 | vars := gorilla.Vars(req) 291 | logger := core.Logger{Output: os.Stderr} 292 | connector := connectors.NewConnector(req.URL.Query().Has("json")) 293 | defer func() { _, _ = io.WriteString(resp, connector.Output()) }() 294 | config, err := conf.LoadById(vars["gdps"]) 295 | if logger.Should(err) != nil { 296 | connector.Error("-1", "Not Found") 297 | return 298 | } 299 | 300 | if conf.MaintenanceMode { 301 | config.SecurityConfig.DisableProtection = false 302 | } 303 | 304 | if core.CheckIPBan(IPAddr, config) { 305 | connector.Error("-1", "Banned") 306 | return 307 | } 308 | 309 | Post := ReadPost(req) 310 | if core.CheckGDAuth(Post) { 311 | db := &core.MySQLConn{} 312 | 313 | if logger.Should(db.ConnectBlob(config)) != nil { 314 | serverError(connector) 315 | return 316 | } 317 | xacc := core.CAccount{DB: db} 318 | if !xacc.PerformGJPAuth(Post, IPAddr) { 319 | connector.Success("Invalid Credentials, but as per Geometry Dash API we should return 1 no matter what") 320 | return 321 | } 322 | xacc.LoadStats() 323 | core.TryInt(&xacc.ColorPrimary, Post.Get("color1")) 324 | core.TryInt(&xacc.ColorSecondary, Post.Get("color2")) 325 | core.TryInt(&xacc.ColorGlow, Post.Get("color3")) 326 | core.TryInt(&xacc.Stars, Post.Get("stars")) 327 | core.TryInt(&xacc.Demons, Post.Get("demons")) 328 | core.TryInt(&xacc.Diamonds, Post.Get("diamonds")) 329 | core.TryInt(&xacc.IconType, Post.Get("iconType")) 330 | core.TryInt(&xacc.Coins, Post.Get("coins")) 331 | core.TryInt(&xacc.UCoins, Post.Get("userCoins")) 332 | core.TryInt(&xacc.Moons, Post.Get("moons")) 333 | core.TryInt(&xacc.Special, Post.Get("special")) 334 | core.TryInt(&xacc.Cube, Post.Get("accIcon")) 335 | core.TryInt(&xacc.Ship, Post.Get("accShip")) 336 | core.TryInt(&xacc.Wave, Post.Get("accDart")) 337 | core.TryInt(&xacc.Ball, Post.Get("accBall")) 338 | core.TryInt(&xacc.Ufo, Post.Get("accBird")) 339 | core.TryInt(&xacc.Robot, Post.Get("accRobot")) 340 | core.TryInt(&xacc.Spider, Post.Get("accSpider")) 341 | core.TryInt(&xacc.Swing, Post.Get("accSwing")) 342 | core.TryInt(&xacc.Jetpack, Post.Get("accJetpack")) 343 | core.TryInt(&xacc.Trace, Post.Get("accGlow")) 344 | core.TryInt(&xacc.Death, Post.Get("accExplosion")) 345 | 346 | protect := core.CProtect{DB: db, Savepath: conf.SavePath + "/" + vars["gdps"], DisableProtection: config.SecurityConfig.DisableProtection} 347 | protect.LoadModel(conf, config) 348 | if !protect.DetectStats(xacc.Uid, xacc.Stars, xacc.Diamonds, xacc.Demons, xacc.Coins, xacc.UCoins) { 349 | connector.Error("-1", "Invalid stats breh") // Le trolling 350 | return 351 | } 352 | 353 | // 2.2 demon stats 354 | { 355 | core.TryInt(&xacc.ExtraData.DemonStats.Weeklies, Post.Get("dinfow")) 356 | core.TryInt(&xacc.ExtraData.DemonStats.Gauntlets, Post.Get("dinfog")) 357 | cf := &core.CLevelFilter{DB: db} 358 | data := cf.CountDemonTypes(core.Decompose( 359 | core.CleanDoubles(core.ClearGDRequest(Post.Get("dinfo")), ","), 360 | ",")) 361 | 362 | xacc.ExtraData.DemonStats.Standard.Easy = data.Standard.Easy 363 | xacc.ExtraData.DemonStats.Standard.Medium = data.Standard.Medium 364 | xacc.ExtraData.DemonStats.Standard.Hard = data.Standard.Hard 365 | xacc.ExtraData.DemonStats.Standard.Insane = data.Standard.Insane 366 | xacc.ExtraData.DemonStats.Standard.Extreme = data.Standard.Extreme 367 | 368 | xacc.ExtraData.DemonStats.Platformer.Easy = data.Platformer.Easy 369 | xacc.ExtraData.DemonStats.Platformer.Medium = data.Platformer.Medium 370 | xacc.ExtraData.DemonStats.Platformer.Hard = data.Platformer.Hard 371 | xacc.ExtraData.DemonStats.Platformer.Insane = data.Platformer.Insane 372 | xacc.ExtraData.DemonStats.Platformer.Extreme = data.Platformer.Extreme 373 | } 374 | 375 | // 2.2 standard stats 376 | { 377 | core.TryInt(&xacc.ExtraData.StandardStats.Daily, Post.Get("sinfod")) 378 | core.TryInt(&xacc.ExtraData.StandardStats.Gauntlet, Post.Get("sinfog")) 379 | sinfo := core.Decompose(core.CleanDoubles(core.ClearGDRequest(Post.Get("sinfo")), ","), ",") 380 | if len(sinfo) == 12 { 381 | xacc.ExtraData.StandardStats.Auto = sinfo[0] 382 | xacc.ExtraData.StandardStats.Easy = sinfo[1] 383 | xacc.ExtraData.StandardStats.Normal = sinfo[2] 384 | xacc.ExtraData.StandardStats.Hard = sinfo[3] 385 | xacc.ExtraData.StandardStats.Harder = sinfo[4] 386 | xacc.ExtraData.StandardStats.Insane = sinfo[5] 387 | xacc.ExtraData.PlatformerStats.Auto = sinfo[6] 388 | xacc.ExtraData.PlatformerStats.Easy = sinfo[7] 389 | xacc.ExtraData.PlatformerStats.Normal = sinfo[8] 390 | xacc.ExtraData.PlatformerStats.Hard = sinfo[9] 391 | xacc.ExtraData.PlatformerStats.Harder = sinfo[10] 392 | xacc.ExtraData.PlatformerStats.Insane = sinfo[11] 393 | d := 0 394 | for _, v := range sinfo { 395 | d += v 396 | } 397 | if xacc.Demons > d { 398 | // Still have no idea why 399 | xacc.ExtraData.StandardStats.Hard += min(xacc.Demons-d, 5) 400 | } 401 | } 402 | } 403 | 404 | xacc.PushVessels() 405 | xacc.PushStatsAndExtra() 406 | connector.NumberedSuccess(xacc.Uid) 407 | } else { 408 | connector.Success("Bad Request, but as per Geometry Dash API we should return 1 no matter what") 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /src/api/service.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "encoding/json" 6 | gorilla "github.com/gorilla/mux" 7 | "html" 8 | "io" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | var NotFoundTemplate = ` 14 | 15 | 16 | 404 | Not Found 17 | 21 | 22 | 23 |

You asked for [PATH], but found another 404 page

24 |

The chances are there is something, but it's not exactly here

25 | 26 | ` 27 | 28 | func Shield(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 29 | vars := gorilla.Vars(req) 30 | io.WriteString(resp, "[GhostCore] Serving //"+vars["gdps"]+"//") 31 | } 32 | 33 | func Redirector(resp http.ResponseWriter, req *http.Request) { 34 | http.Redirect(resp, req, "https://fruitspace.ru/", http.StatusMovedPermanently) 35 | } 36 | 37 | type NotFoundHandler int 38 | 39 | func (n NotFoundHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 40 | io.WriteString(resp, strings.ReplaceAll(NotFoundTemplate, "[PATH]", html.EscapeString(req.URL.Path))) 41 | } 42 | 43 | // Private API 44 | 45 | func TriggerMaintenance(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 46 | if req.URL.Query().Get("key") != conf.MasterKey { 47 | return 48 | } 49 | io.WriteString(resp, "OK") 50 | //go core.MaintainTasks() 51 | } 52 | 53 | func EventAction(resp http.ResponseWriter, req *http.Request, conf *core.GlobalConfig) { 54 | q := req.URL.Query() 55 | if q.Get("key") != conf.MasterKey { 56 | io.WriteString(resp, "KEY") 57 | return 58 | } 59 | mk := "off" 60 | if conf.MaintenanceMode { 61 | mk = "on" 62 | } 63 | switch q.Get("a") { 64 | case "on": 65 | conf.MaintenanceMode = true 66 | case "off": 67 | conf.MaintenanceMode = false 68 | default: 69 | io.WriteString(resp, mk) 70 | } 71 | core.SendMessageDiscord("Touched killswitch: status: " + mk) 72 | } 73 | 74 | func SendJson(resp http.ResponseWriter, jsonData map[string]string) { 75 | data, _ := json.Marshal(jsonData) 76 | io.WriteString(resp, string(data)) 77 | } 78 | -------------------------------------------------------------------------------- /src/api/web_ballistics.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | var BallisticsCache map[string]int64 12 | var BadRepIP map[string]int 13 | var clusterFuck map[string][]string // map[timespan][]ip 14 | 15 | const BADREP_THRESHOLD = 20 16 | const CLUSTERFUCK_THRESHOLD = 10 17 | 18 | func PrepareBallistics(req *http.Request) bool { 19 | IPAddr := ipOf(req) 20 | if thr, ok := BadRepIP[IPAddr]; ok && thr > BADREP_THRESHOLD { 21 | // Here we block DDoSers from fucking everywhere 22 | return false 23 | } 24 | //rl := ratelimit.New(10) 25 | // Last timestamp for this path 26 | tm, ok := BallisticsCache[req.URL.Path] 27 | if !ok { 28 | BallisticsCache[req.URL.Path] = time.Now().UnixMilli() 29 | return false 30 | } 31 | BallisticsCache[req.URL.Path] = time.Now().UnixMilli() 32 | 33 | if time.Now().UnixMilli()-tm > 30000 { 34 | // I mean how the fuck are we going to stop that? 35 | return false 36 | } 37 | 38 | if _, ok := BadRepIP[IPAddr]; !ok { 39 | BadRepIP[IPAddr] = 0 40 | } 41 | 42 | // If registered from single IP and multiple requests less in than 30 seconds 43 | if time.Now().UnixMilli()-tm > 2000 && BadRepIP[IPAddr] < 5 { 44 | BadRepIP[IPAddr]++ 45 | return false 46 | } 47 | if time.Now().UnixMilli()-tm > 400 && BadRepIP[IPAddr] < BADREP_THRESHOLD { 48 | t := fmt.Sprintf("[%s, rep=%d] Unusual request speed .4s-2s at `%s`\nResult: `Throttle 5s`", IPAddr, BadRepIP[IPAddr], req.URL.Path) 49 | BadRepIP[IPAddr] += 2 50 | go core.SendMessageDiscord(t) 51 | time.Sleep(time.Second * 5) 52 | return false 53 | } 54 | clusterFuckTime := time.Now().Format("2006-01-02 15:04") 55 | if _, ok := clusterFuck[clusterFuckTime]; !ok { 56 | // Current minute wasn't registered, check previous 57 | if len(clusterFuck) > 0 { 58 | // We had previous keys - most likely from previous minute 59 | for k, v := range clusterFuck { 60 | // Most likely there will be only one key 61 | t := fmt.Sprintf("[Ballistics Clusterfuck] Found unfinished block at %s\n```\n%s\n```", k, strings.Join(v, "\n")) 62 | go core.SendMessageDiscord(t) 63 | break 64 | } 65 | clusterFuck = make(map[string][]string) 66 | } 67 | clusterFuck[clusterFuckTime] = []string{} 68 | } 69 | 70 | clusterFuck[clusterFuckTime] = append(clusterFuck[clusterFuckTime], IPAddr) 71 | if len(clusterFuck[clusterFuckTime]) > CLUSTERFUCK_THRESHOLD { 72 | // 99% it's a fucking DDoS 73 | for _, ip := range clusterFuck[clusterFuckTime] { 74 | BadRepIP[ip] = 100 75 | } 76 | t := fmt.Sprintf("[Ballistics Clusterfuck] Banned IPs at %s\n```\n%s\n```", clusterFuckTime, strings.Join(clusterFuck[clusterFuckTime], "\n")) 77 | go core.SendMessageDiscord(t) 78 | return true 79 | } 80 | 81 | BadRepIP[IPAddr] += 5 82 | time.Sleep(time.Second * 30) 83 | return true 84 | } 85 | -------------------------------------------------------------------------------- /src/api/web_cache.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "context" 6 | "github.com/go-redis/redis/v8" 7 | "time" 8 | ) 9 | 10 | var cacheClient *redis.Client 11 | 12 | func InitCache(conf core.GlobalConfig, cacheDB int) { 13 | cacheClient = redis.NewClient(&redis.Options{ 14 | Addr: conf.RedisHost + ":" + conf.RedisPort, 15 | Password: conf.RedisPassword, 16 | DB: cacheDB, 17 | }) 18 | } 19 | 20 | func cached(key string) (string, error) { 21 | ctx := context.Background() 22 | return cacheClient.Get(ctx, key).Result() 23 | } 24 | 25 | func withCache(key string, value string) string { 26 | return withCacheDuration(key, value, time.Second*2) 27 | } 28 | 29 | func withCacheDuration(key string, value string, duration time.Duration) string { 30 | ctx := context.Background() 31 | cacheClient.Set(ctx, key, value, duration) 32 | return value 33 | } 34 | -------------------------------------------------------------------------------- /src/api/web_server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "HalogenGhostCore/core/connectors" 6 | "fmt" 7 | gorilla "github.com/gorilla/mux" 8 | "io" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "reflect" 13 | "runtime" 14 | "strings" 15 | ) 16 | 17 | type GhostServer struct { 18 | Log core.Logger 19 | Config core.GlobalConfig 20 | } 21 | 22 | var RouteMap = map[string]func(http.ResponseWriter, *http.Request, *core.GlobalConfig){ 23 | "/": Shield, 24 | 25 | // Geometry Dash 26 | "/db/accounts/accountManagement.php": AccountManagement, 27 | "/db/accounts/backupGJAccount.php": AccountBackup, 28 | "/db/accounts/loginGJAccount.php": AccountLogin, 29 | "/db/accounts/registerGJAccount.php": AccountRegister, 30 | "/db/accounts/syncGJAccount.php": AccountSync, 31 | "/db/accounts/syncGJAccount20.php": AccountSync, 32 | 33 | "/db/database/accounts/backupGJAccountNew.php": AccountBackup, 34 | "/db/database/accounts/syncGJAccountNew.php": AccountSync, 35 | 36 | "/db/acceptGJFriendRequest20.php": FriendAcceptRequest, 37 | "/db/blockGJUser20.php": BlockUser, 38 | "/db/deleteGJAccComment20.php": AccountCommentDelete, 39 | "/db/deleteGJComment20.php": CommentDelete, 40 | "/db/deleteGJFriendRequests20.php": FriendRejectRequest, 41 | "/db/deleteGJLevelUser20.php": LevelDelete, 42 | "/db/deleteGJMessages20.php": MessageDelete, 43 | "/db/downloadGJLevel.php": LevelDownload, 44 | "/db/downloadGJLevel19.php": LevelDownload, 45 | "/db/downloadGJLevel20.php": LevelDownload, 46 | "/db/downloadGJLevel21.php": LevelDownload, 47 | "/db/downloadGJLevel22.php": LevelDownload, 48 | "/db/downloadGJMessage20.php": MessageGet, 49 | "/db/getAccountURL.php": GetAccountUrl, 50 | "/db/getGJAccountComments20.php": AccountCommentGet, 51 | "/db/getGJChallenges.php": GetChallenges, 52 | "/db/getGJCommentHistory.php": CommentGetHistory, 53 | "/db/getGJComments.php": CommentGet, 54 | "/db/getGJComments19.php": CommentGet, 55 | "/db/getGJComments20.php": CommentGet, 56 | "/db/getGJComments21.php": CommentGet, 57 | "/db/getGJCreators.php": GetCreators, 58 | "/db/getGJCreators19.php": GetCreators, 59 | "/db/getGJDailyLevel.php": LevelGetDaily, 60 | "/db/getGJFriendRequests20.php": FriendGetRequests, 61 | "/db/getGJGauntlets.php": GetGauntlets, 62 | "/db/getGJGauntlets21.php": GetGauntlets, 63 | "/db/getGJLevels.php": LevelGetLevels, 64 | "/db/getGJLevels19.php": LevelGetLevels, 65 | "/db/getGJLevels20.php": LevelGetLevels, 66 | "/db/getGJLevels21.php": LevelGetLevels, 67 | "/db/getGJLevelScores.php": GetLevelScores, 68 | "/db/getGJLevelScores211.php": GetLevelScores, 69 | "/db/getGJMapPacks.php": GetMapPacks, 70 | "/db/getGJMapPacks20.php": GetMapPacks, 71 | "/db/getGJMapPacks21.php": GetMapPacks, 72 | "/db/getGJMessages20.php": MessageGetAll, 73 | "/db/getGJRewards.php": GetRewards, 74 | "/db/getGJScores.php": GetScores, 75 | "/db/getGJScores19.php": GetScores, 76 | "/db/getGJScores20.php": GetScores, 77 | "/db/getGJSongInfo.php": GetSongInfo, 78 | "/db/getGJTopArtists.php": GetTopArtists, 79 | "/db/getGJUserInfo20.php": GetUserInfo, 80 | "/db/getGJUserList20.php": GetUserList, 81 | "/db/getGJUsers20.php": GetUsers, 82 | "/db/likeGJItem.php": LikeItem, 83 | "/db/likeGJItem19.php": LikeItem, 84 | "/db/likeGJItem20.php": LikeItem, 85 | "/db/likeGJItem21.php": LikeItem, 86 | "/db/likeGJItem211.php": LikeItem, 87 | "/db/rateGJDemon21.php": RateDemon, 88 | "/db/rateGJStars20.php": RateStar, 89 | "/db/rateGJStars211.php": RateStar, 90 | "/db/readGJFriendRequest20.php": FriendReadRequest, 91 | "/db/removeGJFriend20.php": FriendRemove, 92 | "/db/reportGJLevel.php": LevelReport, 93 | "/db/requestUserAccess.php": RequestMod, 94 | "/db/suggestGJStars20.php": SuggestStars, 95 | "/db/unblockGJUser20.php": UnblockUser, 96 | "/db/updateGJAccSettings20.php": UpdateAccountSettings, 97 | "/db/updateGJDesc20.php": LevelUpdateDescription, 98 | "/db/updateGJUserScore.php": UpdateUserScore, 99 | "/db/updateGJUserScore19.php": UpdateUserScore, 100 | "/db/updateGJUserScore20.php": UpdateUserScore, 101 | "/db/updateGJUserScore21.php": UpdateUserScore, 102 | "/db/updateGJUserScore22.php": UpdateUserScore, 103 | "/db/uploadFriendRequest20.php": FriendRequest, 104 | "/db/uploadGJAccComment20.php": AccountCommentUpload, 105 | "/db/uploadGJComment.php": CommentUpload, 106 | "/db/uploadGJComment19.php": CommentUpload, 107 | "/db/uploadGJComment20.php": CommentUpload, 108 | "/db/uploadGJComment21.php": CommentUpload, 109 | "/db/uploadGJLevel.php": LevelUpload, 110 | "/db/uploadGJLevel19.php": LevelUpload, 111 | "/db/uploadGJLevel20.php": LevelUpload, 112 | "/db/uploadGJLevel21.php": LevelUpload, 113 | "/db/uploadGJMessage20.php": MessageUpload, 114 | 115 | "/db/deleteGJLevelList.php": LevelListDelete, 116 | "/db/uploadGJLevelList.php": LevelListUpload, 117 | "/db/getGJLevelLists.php": LevelListSearch, 118 | "/db/getGJLevelScoresPlat.php": GetLevelPlatScores, 119 | 120 | "/db/getCustomContentURL.php": GetContentURL, 121 | "/db/content/sfx/{sfxid}": RelaySFX, 122 | 123 | //"/db/content/sfx/sfxlibrary.dat": GetSFXLibrary, 124 | //"/db/content/sfx/sfxlibrary_version.txt": GetSFXLibraryVersion, 125 | //"/db/content/sfx/s{sfxid}.ogg": GetSFXTrack, 126 | 127 | "/db/content/music/musiclibrary.dat": GetMusicLibrary, 128 | "/db/content/music/musiclibrary_02.dat": GetMusicLibrary, 129 | "/db/content/music/musiclibrary_version.txt": GetMusicLibraryVersion, 130 | "/db/mp3": GetSongInfo, 131 | } 132 | 133 | var RouteIntegraMap = map[string]func(http.ResponseWriter, *http.Request, *core.GlobalConfig){ 134 | // PRIVATE API 135 | "/integra/maintenance": TriggerMaintenance, 136 | //"/integra/killskew": EventAction, 137 | } 138 | 139 | func GetFunctionName(i interface{}) string { 140 | return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() 141 | } 142 | 143 | func (ghost *GhostServer) StartServer(Host string) { 144 | BallisticsCache = make(map[string]int64) 145 | BadRepIP = make(map[string]int) 146 | clusterFuck = make(map[string][]string) 147 | mux := gorilla.NewRouter() 148 | var nfh NotFoundHandler 149 | mux.NotFoundHandler = nfh 150 | mux.HandleFunc("/", Redirector) 151 | for route := range RouteMap { 152 | mux.HandleFunc("/{gdps:[a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9]}"+route, 153 | func(resp http.ResponseWriter, req *http.Request) { 154 | vars := gorilla.Vars(req) 155 | pref := strings.Replace(req.URL.Path, "/"+vars["gdps"], "", 1) 156 | handler := RouteMap[pref] 157 | if strings.HasPrefix(pref, "/db/content/sfx/") { 158 | handler = RelaySFX 159 | } 160 | IPAddr := req.Header.Get("CF-Connecting-IP") 161 | if IPAddr == "" { 162 | IPAddr = req.Header.Get("X-Real-IP") 163 | } 164 | if IPAddr == "" { 165 | IPAddr = strings.Split(req.RemoteAddr, ":")[0] 166 | } 167 | log.Println("["+IPAddr+"] "+req.URL.Path, " Got ", GetFunctionName(handler)) 168 | handler(resp, req, &ghost.Config) 169 | }) 170 | } 171 | for route, handler := range RouteIntegraMap { 172 | mux.HandleFunc(route, 173 | func(resp http.ResponseWriter, req *http.Request) { 174 | defer func() { 175 | if err := recover(); err != nil { 176 | log.Println(err) 177 | core.SendMessageDiscord(fmt.Sprintf("Panic: %s", err)) 178 | } 179 | }() 180 | IPAddr := req.Header.Get("CF-Connecting-IP") 181 | if IPAddr == "" { 182 | IPAddr = req.Header.Get("X-Real-IP") 183 | } 184 | if IPAddr == "" { 185 | IPAddr = strings.Split(req.RemoteAddr, ":")[0] 186 | } 187 | log.Println("["+IPAddr+"] "+req.URL.Path, " Got ", GetFunctionName(handler)) 188 | handler(resp, req, &ghost.Config) 189 | }) 190 | } 191 | log.Println("Server is up and running on http://" + Host) 192 | err := http.ListenAndServe(Host, mux) 193 | if err != nil { 194 | ghost.Log.LogErr(ghost, err.Error()) 195 | } 196 | 197 | } 198 | 199 | func ReadPost(req *http.Request) url.Values { 200 | if req.Body == nil { 201 | return url.Values{} 202 | } 203 | body, err := io.ReadAll(req.Body) 204 | if err != nil { 205 | log.Println(err.Error()) 206 | return url.Values{} 207 | } 208 | if len(body) == 0 || strings.Count(string(body), "=") == 0 { 209 | return url.Values{} 210 | } 211 | vals := make(url.Values) 212 | pairs := strings.Split(string(body), "&") 213 | for _, val := range pairs { 214 | if !strings.Contains(val, "=") { 215 | continue 216 | } 217 | m := strings.SplitN(val, "=", 2) 218 | //fmt.Println(m) 219 | rval, _ := url.QueryUnescape(m[1]) 220 | rkey, _ := url.QueryUnescape(m[0]) 221 | vals[rkey] = append(vals[rkey], rval) 222 | } 223 | return vals 224 | } 225 | 226 | func ipOf(req *http.Request) string { 227 | IPAddr := req.Header.Get("CF-Connecting-IP") 228 | if IPAddr == "" { 229 | IPAddr = req.Header.Get("X-Real-IP") 230 | } 231 | if IPAddr == "" { 232 | IPAddr = strings.Split(req.RemoteAddr, ":")[0] 233 | } 234 | return IPAddr 235 | } 236 | 237 | func serverError(connector connectors.Connector) { 238 | connector.Error("-1", "Server Error") 239 | } 240 | -------------------------------------------------------------------------------- /src/api/web_templates.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/base64" 5 | ) 6 | 7 | func BannedTemplateComment(text string) string { 8 | text=base64.StdEncoding.EncodeToString([]byte(text)) 9 | return "2~"+text+"~3~1~4~0~7~0~10~0~9~1 second~6~1:1~M41dss~9~98~10~35~11~3~14~0~15~2~16~0" 10 | } 11 | 12 | func BannedTemplateUserProfile() string { 13 | uname:="M41dss" 14 | return "1:"+uname+":2:0:3:41:4:0:6:1:7:0:8:41:9:98:10:35:11::13:41:14:0:15:2:16:0:17:41:18:0:19:0:20::21:98:22:SHIP_ID"+ 15 | ":23:BALL_ID:24:UFO_ID:25:WAVE_ID:26:ROBOT_ID:28:1:29:1:30:1:31:0:43:SPIDER_ID:44::45::46:41:48:1:49:2:50:0:38:0:39:0:40:0" 16 | } 17 | 18 | func BannedTemplateUserListItem() string { 19 | uname:="M41dss" 20 | return "1:"+uname+":2:0:9:98:10:35:11:3:14:0:15:2:16:0:18:0:41:1|" 21 | } -------------------------------------------------------------------------------- /src/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RED='\033[0;31m' 4 | BLUE='\033[0;34m' 5 | GREEN='\033[0;32m' 6 | NC='\033[0m' 7 | GRAY='\033[0;37m' 8 | 9 | PATH=$PATH:~/go/bin 10 | echo -e "${GREEN}Checking for ineffectual assignments...${GRAY}" 11 | ineffassign ./... 12 | echo -e "${GREEN}Building...${GRAY}" 13 | CGO_ENABLED=0 go build -ldflags="-s -w" -trimpath -o HalogenGhostCore . 14 | echo -e "${GREEN}Done.${NC}" -------------------------------------------------------------------------------- /src/core/CAccount_structs.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "strings" 4 | 5 | type CAccountJSON struct { 6 | Uid int `json:"uid"` 7 | Uname string `json:"uname"` 8 | Email string `json:"email,omitempty"` 9 | ModBadge int `json:"mod_badge,omitempty"` 10 | Role Role `json:"role,omitempty"` 11 | IsBanned bool `json:"is_banned"` 12 | LeaderboardRank int `json:"leaderboard_rank,omitempty"` 13 | 14 | WeAreFriends bool `json:"we_are_friends,omitempty"` 15 | NewMessagesCount int `json:"new_messages_count,omitempty"` 16 | NewFriendRequests int `json:"new_friend_requests_count,omitempty"` 17 | 18 | Stats CAccountStats `json:"stats,omitempty"` 19 | Technical CAccountTechnical `json:"technical,omitempty"` 20 | Social CAccountSocial `json:"social,omitempty"` 21 | Vessels CAccountVessels `json:"vessels,omitempty"` 22 | Chests CAccountChests `json:"chests,omitempty"` 23 | Settings CAccountSettings `json:"settings,omitempty"` 24 | } 25 | 26 | func NewCAccountJSONFromAccount(acc CAccount, role *Role, personal bool) CAccountJSON { 27 | // Basic stuff 28 | aj := CAccountJSON{ 29 | Uid: acc.Uid, 30 | Uname: acc.Uname, 31 | IsBanned: acc.IsBanned > 0, 32 | LeaderboardRank: acc.GetLeaderboardRank() + 1, 33 | Stats: NewCAccountStatsFromAccount(acc), 34 | Vessels: NewCAccountVesselsFromAccount(acc), 35 | Settings: NewCAccountSettingsFromAccount(acc), 36 | } 37 | 38 | // Roles 39 | if role != nil { 40 | aj.ModBadge = role.ModLevel 41 | if personal { 42 | aj.Role = *role 43 | } 44 | } 45 | 46 | // Personal 47 | if personal { 48 | aj.Email = acc.Email 49 | aj.Social = NewCAccountSocialFromAccount(acc) 50 | aj.Technical = NewCAccountTechnicalFromAccount(acc) 51 | aj.Chests = NewCAccountChestsFromAccount(acc) 52 | cf := CFriendship{DB: acc.DB} 53 | cm := CMessage{DB: acc.DB} 54 | aj.NewFriendRequests = cf.CountFriendRequests(acc.Uid, true) 55 | aj.NewMessagesCount = cm.CountMessages(acc.Uid, true) 56 | } 57 | 58 | return aj 59 | } 60 | 61 | func NewCAccountJSONFromAccountLite(acc CAccount) CAccountJSON { 62 | return CAccountJSON{ 63 | Uid: acc.Uid, 64 | Uname: acc.Uname, 65 | IsBanned: acc.IsBanned > 0, 66 | Stats: NewCAccountStatsFromAccount(acc), 67 | Vessels: NewCAccountVesselsFromAccount(acc), 68 | } 69 | } 70 | 71 | type CAccountStats struct { 72 | Stars int `json:"stars"` 73 | Diamonds int `json:"diamonds"` 74 | Coins int `json:"coins"` 75 | UCoins int `json:"ucoins"` 76 | Demons int `json:"demons"` 77 | CPoints int `json:"cpoints"` 78 | Orbs int `json:"orbs"` 79 | Moons int `json:"moons"` 80 | Special int `json:"special"` 81 | LvlsCompleted int `json:"lvls_completed"` 82 | } 83 | 84 | func NewCAccountStatsFromAccount(acc CAccount) CAccountStats { 85 | return CAccountStats{ 86 | Stars: acc.Stars, 87 | Diamonds: acc.Diamonds, 88 | Coins: acc.Coins, 89 | UCoins: acc.UCoins, 90 | Demons: acc.Demons, 91 | CPoints: acc.CPoints, 92 | Orbs: acc.Orbs, 93 | Moons: acc.Moons, 94 | Special: acc.Special, 95 | LvlsCompleted: acc.LvlsCompleted, 96 | } 97 | } 98 | 99 | type CAccountTechnical struct { 100 | RegDate string `json:"reg_date"` 101 | AccessDate string `json:"access_date"` 102 | LastIP string `json:"last_ip"` 103 | GameVer string `json:"game_ver"` 104 | } 105 | 106 | func NewCAccountTechnicalFromAccount(acc CAccount) CAccountTechnical { 107 | return CAccountTechnical{ 108 | RegDate: acc.RegDate, 109 | AccessDate: acc.AccessDate, 110 | LastIP: acc.LastIP, 111 | GameVer: acc.GameVer, 112 | } 113 | } 114 | 115 | type CAccountSocial struct { 116 | BlacklistIds []int `json:"blacklist_ids"` 117 | FriendsCount int `json:"friends_count"` 118 | FriendshipIds []int `json:"friendship_ids"` 119 | } 120 | 121 | func NewCAccountSocialFromAccount(acc CAccount) CAccountSocial { 122 | blacklist := strings.Split(acc.Blacklist, ",") 123 | friendships := strings.Split(acc.FriendshipIds, ",") 124 | return CAccountSocial{ 125 | BlacklistIds: ArrTranslateToInt(blacklist), 126 | FriendsCount: acc.FriendsCount, 127 | FriendshipIds: ArrTranslateToInt(friendships), 128 | } 129 | } 130 | 131 | type CAccountVessels struct { 132 | ShownIcon int `json:"shown_icon"` 133 | IconType int `json:"icon_type"` 134 | ColorPrimary int `json:"color_primary"` 135 | ColorSecondary int `json:"color_secondary"` 136 | ColorGlow int `json:"color_glow"` 137 | Cube int `json:"cube"` 138 | Ship int `json:"ship"` 139 | Ball int `json:"ball"` 140 | Ufo int `json:"ufo"` 141 | Wave int `json:"wave"` 142 | Robot int `json:"robot"` 143 | Spider int `json:"spider"` 144 | Swing int `json:"swing"` 145 | Jetpack int `json:"jetpack"` 146 | Trace int `json:"trace"` 147 | Death int `json:"death"` 148 | } 149 | 150 | func NewCAccountVesselsFromAccount(acc CAccount) CAccountVessels { 151 | return CAccountVessels{ 152 | ShownIcon: acc.GetShownIcon(), 153 | IconType: acc.IconType, 154 | ColorPrimary: acc.ColorPrimary, 155 | ColorSecondary: acc.ColorSecondary, 156 | ColorGlow: acc.ColorGlow, 157 | Cube: acc.Cube, 158 | Ship: acc.Ship, 159 | Ball: acc.Ball, 160 | Ufo: acc.Ufo, 161 | Wave: acc.Wave, 162 | Robot: acc.Robot, 163 | Spider: acc.Spider, 164 | Swing: acc.Swing, 165 | Jetpack: acc.Jetpack, 166 | Trace: acc.Trace, 167 | Death: acc.Death, 168 | } 169 | } 170 | 171 | type CAccountChests struct { 172 | ChestSmallCount int `json:"chest_small_count"` 173 | ChestSmallTime int `json:"chest_small_time_left"` 174 | ChestBigCount int `json:"chest_big_count"` 175 | ChestBigTime int `json:"chest_big_time_left"` 176 | } 177 | 178 | func NewCAccountChestsFromAccount(acc CAccount) CAccountChests { 179 | return CAccountChests{ 180 | ChestSmallCount: acc.ChestSmallCount, 181 | ChestSmallTime: acc.ChestSmallTime, 182 | ChestBigCount: acc.ChestBigCount, 183 | ChestBigTime: acc.ChestBigTime, 184 | } 185 | } 186 | 187 | type CAccountSettings struct { 188 | AllowFriendReq bool `json:"allow_friend_requests"` 189 | AllowComments string `json:"allow_view_comments"` 190 | AllowMessages string `json:"allow_messages"` 191 | Youtube string `json:"youtube"` 192 | Twitch string `json:"twitch"` 193 | Twitter string `json:"twitter"` 194 | } 195 | 196 | func NewCAccountSettingsFromAccount(acc CAccount) CAccountSettings { 197 | s := CAccountSettings{ 198 | Youtube: acc.Youtube, 199 | Twitch: acc.Twitch, 200 | Twitter: acc.Twitter, 201 | AllowFriendReq: true, 202 | AllowComments: "everybody", 203 | AllowMessages: "everybody", 204 | } 205 | allowances := []string{"everybody", "friends", "nobody"} 206 | 207 | if acc.FrS > 0 { 208 | s.AllowFriendReq = false 209 | } 210 | if acc.CS > 0 { 211 | s.AllowComments = allowances[Clamp(acc.CS, 0, 2)] 212 | } 213 | if acc.MS > 0 { 214 | s.AllowMessages = allowances[Clamp(acc.MS, 0, 2)] 215 | } 216 | return s 217 | } 218 | -------------------------------------------------------------------------------- /src/core/CComment.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | type CComment struct { 9 | Id int `json:"id"` 10 | Uid int `json:"uid"` 11 | Likes int `json:"likes"` 12 | PostedTime string `json:"posted_time"` 13 | Comment string `json:"comment"` 14 | 15 | LvlId int `json:"lvl_id"` 16 | Percent int `json:"percent"` 17 | IsSpam bool `json:"is_spam"` 18 | 19 | DB *MySQLConn `json:"-"` 20 | } 21 | 22 | func (cc *CComment) ExistsLevelComment(id int) bool { 23 | var cnt int 24 | cc.DB.ShouldQueryRow("SELECT count(*) as cnt FROM #DB#.comments WHERE id=?", id).Scan(&cnt) 25 | return cnt > 0 26 | } 27 | 28 | func (cc *CComment) ExistsAccComment(id int) bool { 29 | var cnt int 30 | cc.DB.ShouldQueryRow("SELECT count(*) as cnt FROM #DB#.acccomments WHERE id=?", id).Scan(&cnt) 31 | return cnt > 0 32 | } 33 | 34 | func (cc *CComment) CountAccComments(uid int) int { 35 | var cnt int 36 | pf := "" 37 | if uid > 0 { 38 | pf = " WHERE uid=" + strconv.Itoa(uid) 39 | } 40 | cc.DB.ShouldQueryRow("SELECT count(*) as cnt FROM #DB#.acccomments" + pf).Scan(&cnt) 41 | return cnt 42 | } 43 | 44 | func (cc *CComment) CountLevelComments(lvlId int) int { 45 | var cnt int 46 | pf := "" 47 | if lvlId > 0 { 48 | pf = " WHERE lvl_id=" + strconv.Itoa(lvlId) 49 | } 50 | cc.DB.ShouldQueryRow("SELECT count(*) as cnt FROM #DB#.comments" + pf).Scan(&cnt) 51 | return cnt 52 | } 53 | 54 | func (cc *CComment) CountCommentHistory(uid int) int { 55 | var cnt int 56 | cc.DB.ShouldQueryRow("SELECT count(*) as cnt FROM #DB#.comments WHERE uid=?", uid).Scan(&cnt) 57 | return cnt 58 | } 59 | 60 | func (cc *CComment) LoadAccComment() { 61 | cc.DB.ShouldQueryRow("SELECT uid,comment,postedTime,likes,isSpam FROM #DB#.acccomments WHERE id=?", cc.Id).Scan( 62 | &cc.Uid, &cc.Comment, &cc.PostedTime, &cc.Likes, &cc.IsSpam) 63 | } 64 | 65 | func (cc *CComment) LoadLevelComment() { 66 | cc.DB.ShouldQueryRow("SELECT uid,lvl_id,comment,postedTime,likes,isSpam,percent FROM #DB#.comments WHERE id=?", cc.Id).Scan( 67 | &cc.Uid, &cc.LvlId, &cc.Comment, &cc.PostedTime, &cc.Likes, &cc.IsSpam, &cc.Percent) 68 | } 69 | 70 | func (cc *CComment) GetAllAccComments(uid int, page int) []CComment { 71 | page *= 10 72 | rows := cc.DB.ShouldQuery("SELECT id,comment,postedTime,likes,isSpam FROM #DB#.acccomments WHERE uid=? ORDER BY postedTime DESC LIMIT 10 OFFSET "+strconv.Itoa(page), uid) 73 | defer rows.Close() 74 | var out []CComment 75 | for rows.Next() { 76 | comm := CComment{Uid: uid} 77 | rows.Scan(&comm.Id, &comm.Comment, &comm.PostedTime, &comm.Likes, &comm.IsSpam) 78 | out = append(out, comm) 79 | } 80 | return out 81 | } 82 | 83 | func (cc *CComment) GetAllLevelComments(lvlId int, page int, sortMode bool) []CComment { 84 | filter := "postedTime" 85 | page *= 10 86 | if sortMode { 87 | filter = "likes" 88 | } 89 | rows := cc.DB.ShouldQuery("SELECT id,uid,comment,postedTime,likes,isSpam,percent FROM #DB#.comments WHERE lvl_id=? ORDER BY "+filter+" DESC LIMIT 10 OFFSET "+strconv.Itoa(page), lvlId) 90 | defer rows.Close() 91 | var out []CComment 92 | for rows.Next() { 93 | comm := CComment{LvlId: lvlId, DB: cc.DB} 94 | rows.Scan(&comm.Id, &comm.Uid, &comm.Comment, &comm.PostedTime, &comm.Likes, &comm.IsSpam, &comm.Percent) 95 | out = append(out, comm) 96 | } 97 | return out 98 | } 99 | 100 | func (cc *CComment) GetAllCommentsHistory(uid int, page int, sortMode bool) []CComment { 101 | page *= 10 102 | filter := "postedTime" 103 | if sortMode { 104 | filter = "likes" 105 | } 106 | rows := cc.DB.ShouldQuery("SELECT id,lvl_id,comment,postedTime,likes,isSpam,percent FROM #DB#.comments WHERE uid=? ORDER BY "+filter+" DESC LIMIT 10 OFFSET "+strconv.Itoa(page), uid) 107 | defer rows.Close() 108 | var out []CComment 109 | for rows.Next() { 110 | comm := CComment{Uid: uid} 111 | rows.Scan(&comm.Id, &comm.LvlId, &comm.Comment, &comm.PostedTime, &comm.Likes, &comm.IsSpam, &comm.Percent) 112 | out = append(out, comm) 113 | } 114 | return out 115 | } 116 | 117 | func (cc *CComment) PostAccComment() bool { 118 | if len(cc.Comment) > 128 { 119 | return false 120 | } 121 | cc.DB.ShouldExec("INSERT INTO #DB#.acccomments (uid,comment,postedTime) VALUES (?,?,?)", cc.Uid, cc.Comment, 122 | time.Now().Format("2006-01-02 15:04:05")) 123 | return true 124 | } 125 | 126 | func (cc *CComment) PostLevelComment() bool { 127 | if len(cc.Comment) > 128 { 128 | return false 129 | } 130 | cc.DB.ShouldExec("INSERT INTO #DB#.comments (uid,lvl_id,comment,postedTime,percent) VALUES (?,?,?,?,?)", cc.Uid, 131 | cc.LvlId, cc.Comment, time.Now().Format("2006-01-02 15:04:05"), cc.Percent) 132 | return true 133 | } 134 | 135 | func (cc *CComment) DeleteAccComment(id int, uid int) { 136 | cc.DB.ShouldExec("DELETE FROM #DB#.acccomments WHERE id=? AND uid=?", id, uid) 137 | } 138 | 139 | func (cc *CComment) DeleteLevelComment(id int, uid int) { 140 | cc.DB.ShouldExec("DELETE FROM #DB#.comments WHERE id=? AND uid=?", id, uid) 141 | } 142 | 143 | func (cc *CComment) DeleteOwnerLevelComment(id int, lvlId int) { 144 | cc.DB.ShouldExec("DELETE FROM #DB#.comments WHERE id=? AND lvl_id=?", id, lvlId) 145 | } 146 | 147 | func (cc *CComment) LikeAccComment(id int, uid int, actionLike bool) bool { 148 | if IsLiked(ITEMTYPE_ACCCOMMENT, uid, id, cc.DB) { 149 | return false 150 | } 151 | operator := "-" 152 | actionc := "Dislike" 153 | if actionLike { 154 | operator = "+" 155 | actionc = "Like" 156 | } 157 | cc.DB.ShouldExec("UPDATE #DB#.acccomments SET likes=likes"+operator+"1 WHERE id=?", id) 158 | RegisterAction(ACTION_ACCCOMMENT_LIKE, uid, id, map[string]string{"type": actionc}, cc.DB) 159 | return true 160 | } 161 | 162 | func (cc *CComment) LikeLevelComment(id int, uid int, actionLike bool) bool { 163 | if IsLiked(ITEMTYPE_COMMENT, uid, id, cc.DB) { 164 | return false 165 | } 166 | operator := "-" 167 | actionc := "Dislike" 168 | if actionLike { 169 | operator = "+" 170 | actionc = "Like" 171 | } 172 | cc.DB.ShouldExec("UPDATE #DB#.comments SET likes=likes"+operator+"1 WHERE id=?", id) 173 | RegisterAction(ACTION_COMMENT_LIKE, uid, id, map[string]string{"type": actionc}, cc.DB) 174 | return true 175 | } 176 | -------------------------------------------------------------------------------- /src/core/CFriendship.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "slices" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type CFriendship struct { 11 | DB *MySQLConn 12 | } 13 | 14 | func (cf *CFriendship) IsAlreadyFriend(uid_dest int, uid int) bool { 15 | var cnt int 16 | cf.DB.MustQueryRow("SELECT count(*) as cnt FROM #DB#.friendships WHERE (uid1=? AND uid2=?) OR (uid2=? AND uid1=?)", 17 | uid, uid_dest, uid, uid_dest).Scan(&cnt) 18 | return cnt > 0 19 | } 20 | 21 | func (cf *CFriendship) IsAlreadySentFriend(uid_dest int, uid int) bool { 22 | var cnt int 23 | cf.DB.MustQueryRow("SELECT count(*) as cnt FROM #DB#.friendreqs WHERE uid_src=? AND uid_dest=?", uid, uid_dest).Scan(&cnt) 24 | return cnt > 0 25 | } 26 | 27 | func (cf *CFriendship) CountFriendRequests(uid int, new bool) int { 28 | var cnt int 29 | q := "SELECT count(*) as cnt FROM #DB#.friendreqs WHERE uid_dest=?" 30 | if new { 31 | q += " AND isNew=1" 32 | } 33 | cf.DB.MustQueryRow(q, uid).Scan(&cnt) 34 | return cnt 35 | } 36 | 37 | func (cf *CFriendship) GetFriendRequests(uid int, page int, sent bool) (int, []map[string]string) { 38 | q := "SELECT id,uid_src,uid_dest,uploadDate,comment,isNew FROM #DB#.friendreqs WHERE " 39 | if sent { 40 | q += "uid_src=?" 41 | } else { 42 | q += "uid_dest=?" 43 | } 44 | q += " LIMIT 10 OFFSET " + strconv.Itoa(page*10) 45 | rows := cf.DB.MustQuery(q, uid) 46 | defer rows.Close() 47 | var users []map[string]string 48 | var cnt int 49 | for rows.Next() { 50 | var ( 51 | id int 52 | src int 53 | dest int 54 | date string 55 | comment string 56 | isNew int 57 | ) 58 | rows.Scan(&id, &src, &dest, &date, &comment, &isNew) 59 | cnt++ 60 | user := make(map[string]string) 61 | user["id"] = strconv.Itoa(id) 62 | user["comment"] = comment 63 | acc := CAccount{DB: cf.DB} 64 | if sent { 65 | acc.Uid = dest 66 | } else { 67 | acc.Uid = src 68 | } 69 | user["uid"] = strconv.Itoa(acc.Uid) 70 | acc.LoadAuth(CAUTH_UID) 71 | acc.LoadStats() 72 | acc.LoadVessels() 73 | user["uname"] = acc.Uname 74 | user["isNew"] = strconv.Itoa(isNew) 75 | user["special"] = strconv.Itoa(acc.Special) 76 | user["iconType"] = strconv.Itoa(acc.IconType) 77 | user["clr_primary"] = strconv.Itoa(acc.ColorPrimary) 78 | user["clr_secondary"] = strconv.Itoa(acc.ColorSecondary) 79 | user["iconId"] = strconv.Itoa(acc.GetShownIcon()) 80 | user["date"] = date 81 | users = append(users, user) 82 | } 83 | return cnt, users 84 | } 85 | 86 | func (cf *CFriendship) GetFriendRequestsCount(uid int, sent bool) int { 87 | var cnt int 88 | q := "SELECT count(*) as cnt FROM #DB#.friendreqs WHERE " 89 | if sent { 90 | q += "uid_src=?" 91 | } else { 92 | q += "uid_dest=?" 93 | } 94 | cf.DB.MustQueryRow(q, uid).Scan(&cnt) 95 | return cnt 96 | } 97 | 98 | func (cf *CFriendship) DeleteFriendship(uid int, uid_dest int) { 99 | id := cf.GetFriendshipId(uid, uid_dest) 100 | if id == 0 { 101 | return 102 | } 103 | cf.DB.ShouldExec("DELETE FROM #DB#.friendships WHERE (uid1=? AND uid2=?) OR (uid2=? AND uid1=?)", uid, uid_dest, uid, uid_dest) 104 | u1 := CAccount{DB: cf.DB} 105 | u2 := CAccount{DB: cf.DB} 106 | u1.Uid = uid 107 | u2.Uid = uid_dest 108 | u1.UpdateFriendships(CFRIENDSHIP_REMOVE, id) 109 | u2.UpdateFriendships(CFRIENDSHIP_REMOVE, id) 110 | } 111 | 112 | func (cf *CFriendship) GetFriendshipId(uid int, uid_dest int) int { 113 | var id int 114 | cf.DB.ShouldQueryRow("SELECT id FROM #DB#.friendships WHERE (uid1=? AND uid2=?) OR (uid2=? AND uid1=?)", uid, uid_dest, uid, uid_dest).Scan(&id) 115 | return id 116 | } 117 | 118 | func (cf *CFriendship) GetFriendByFID(id int) (int, int) { 119 | var ( 120 | uid1 int 121 | uid2 int 122 | ) 123 | cf.DB.ShouldQueryRow("SELECT uid1,uid2 FROM #DB#.friendships WHERE id=?", id).Scan(&uid1, &uid2) 124 | return uid1, uid2 125 | } 126 | 127 | func (cf *CFriendship) GetAccFriends(acc CAccount) []int { 128 | fr := strings.Split(acc.FriendshipIds, ",") 129 | var frlist []int 130 | for _, sfr := range fr { 131 | id, err := strconv.Atoi(sfr) 132 | if err != nil { 133 | continue 134 | } 135 | uid1, uid2 := cf.GetFriendByFID(id) 136 | targetUid := uid1 137 | if uid1 == acc.Uid { 138 | targetUid = uid2 139 | } 140 | frlist = append(frlist, targetUid) 141 | } 142 | return frlist 143 | } 144 | 145 | func (cf *CFriendship) ReadFriendRequest(id int) { 146 | cf.DB.ShouldExec("UPDATE #DB#.friendreqs SET isNew=0 WHERE id=?", id) 147 | } 148 | 149 | func (cf *CFriendship) RequestFriend(uid int, uidDest int, comment string) int { 150 | if uid == uidDest || cf.IsAlreadyFriend(uid, uidDest) || cf.IsAlreadySentFriend(uid, uidDest) || len(comment) > 512 { 151 | return -1 152 | } 153 | acc := CAccount{DB: cf.DB} 154 | acc.Uid = uidDest 155 | acc.LoadSettings() 156 | if acc.FrS > 0 { 157 | return -1 158 | } 159 | acc.LoadSocial() 160 | blacklist := strings.Split(acc.Blacklist, ",") 161 | if slices.Contains(blacklist, strconv.Itoa(uid)) { 162 | return -1 163 | } 164 | acc.DB.ShouldExec("INSERT INTO #DB#.friendreqs (uid_src, uid_dest, uploadDate, comment) VALUES (?,?,?,?)", 165 | uid, uidDest, time.Now().Format("2006-01-02 15:04:05"), comment) 166 | return 1 167 | } 168 | 169 | func (cf *CFriendship) AcceptFriendRequest(id int, uid int) int { 170 | var ( 171 | src int 172 | dest int 173 | ) 174 | cf.DB.ShouldQueryRow("SELECT uid_src,uid_dest FROM #DB#.friendreqs WHERE id=?", id).Scan(&src, &dest) 175 | if src == dest || uid != dest { 176 | return -1 177 | } 178 | req, _ := cf.DB.PrepareExec("INSERT INTO #DB#.friendships (uid1, uid2) VALUES (?,?)", src, dest) 179 | iid, _ := req.LastInsertId() 180 | cf.DB.ShouldExec("DELETE FROM #DB#.friendreqs WHERE id=?", id) 181 | u1 := CAccount{DB: cf.DB} 182 | u2 := CAccount{DB: cf.DB} 183 | u1.Uid = src 184 | u2.Uid = dest 185 | res := u1.UpdateFriendships(CFRIENDSHIP_ADD, int(iid)) 186 | res += u2.UpdateFriendships(CFRIENDSHIP_ADD, int(iid)) 187 | if res != 2 { 188 | return -1 189 | } 190 | return 1 191 | } 192 | 193 | func (cf *CFriendship) RejectFriendRequestById(id int, uid int) int { 194 | var ( 195 | uid1 int 196 | uid2 int 197 | ) 198 | cf.DB.ShouldQueryRow("SELECT uid_src, uid_dest FROM #DB#.friendreqs WHERE id=?", id).Scan(&uid1, &uid2) 199 | if uid1 == uid2 || uid != uid2 { 200 | return -1 201 | } 202 | cf.DB.ShouldExec("DELETE FROM #DB#.friendreqs WHERE id=?", id) 203 | return 1 204 | } 205 | 206 | func (cf *CFriendship) RejectFriendRequestByUid(uid int, uid_dest int, isSender bool) { 207 | var ( 208 | uid1 int 209 | uid2 int 210 | ) 211 | if isSender { 212 | uid1 = uid 213 | uid2 = uid_dest 214 | } else { 215 | uid1 = uid_dest 216 | uid2 = uid 217 | } 218 | cf.DB.ShouldExec("DELETE FROM #DB#.friendreqs WHERE uid_src=? AND uid_dest=?", uid1, uid2) 219 | } 220 | -------------------------------------------------------------------------------- /src/core/CHalogen.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | _ "embed" 5 | "net/http" 6 | "strconv" 7 | ) 8 | 9 | //func PushMusicNotify(db MySQLConn, conf *GlobalConfig, blob ConfigBlob, songID int) { 10 | // plug:= modules.PluginCore{} 11 | // mus:= CMusic{DB: db, Logger: db.logger, Config: conf, ConfBlob: blob} 12 | // 13 | //} 14 | 15 | //Count stuff 16 | 17 | func CountUsers(db *MySQLConn) int { 18 | var cnt int 19 | db.ShouldQueryRow("SELECT COUNT(*) as cnt FROM #DB#.users").Scan(&cnt) 20 | return cnt 21 | } 22 | 23 | func CountLevels(db *MySQLConn) int { 24 | var cnt int 25 | db.ShouldQueryRow("SELECT COUNT(*) as cnt FROM #DB#.levels").Scan(&cnt) 26 | return cnt 27 | } 28 | 29 | func CountPosts(db *MySQLConn) int { 30 | var cnt int 31 | db.ShouldQueryRow("SELECT COUNT(*) as cnt FROM #DB#.acccomments").Scan(&cnt) 32 | return cnt 33 | } 34 | 35 | func CountComments(db *MySQLConn) int { 36 | var cnt int 37 | db.ShouldQueryRow("SELECT COUNT(*) as cnt FROM #DB#.comments").Scan(&cnt) 38 | return cnt 39 | } 40 | 41 | //Trigger stuff 42 | 43 | func OnRegister(db *MySQLConn, config *GlobalConfig, blob ConfigBlob) bool { 44 | cnt := CountUsers(db) 45 | if blob.ServerConfig.MaxUsers == -1 { 46 | http.Get(config.ApiEndpoint + "?srvid=" + blob.ServerConfig.SrvID + "&key=" + blob.ServerConfig.SrvKey + "&action=stats.users&value=" + strconv.Itoa(cnt+1)) 47 | return true 48 | } 49 | if cnt > blob.ServerConfig.MaxUsers { 50 | return false 51 | } 52 | http.Get(config.ApiEndpoint + "?srvid=" + blob.ServerConfig.SrvID + "&key=" + blob.ServerConfig.SrvKey + "&action=stats.users&value=" + strconv.Itoa(cnt+1)) 53 | return true 54 | } 55 | 56 | func OnLevel(db *MySQLConn, config *GlobalConfig, blob ConfigBlob) bool { 57 | cnt := CountLevels(db) 58 | if blob.ServerConfig.MaxLevels == -1 { 59 | http.Get(config.ApiEndpoint + "?srvid=" + blob.ServerConfig.SrvID + "&key=" + blob.ServerConfig.SrvKey + "&action=stats.levels&value=" + strconv.Itoa(cnt+1)) 60 | return true 61 | } 62 | if cnt > blob.ServerConfig.MaxLevels { 63 | return false 64 | } 65 | http.Get(config.ApiEndpoint + "?srvid=" + blob.ServerConfig.SrvID + "&key=" + blob.ServerConfig.SrvKey + "&action=stats.levels&value=" + strconv.Itoa(cnt+1)) 66 | return true 67 | } 68 | 69 | func OnPost(db *MySQLConn, config *GlobalConfig, blob ConfigBlob) bool { 70 | cnt := CountPosts(db) 71 | if blob.ServerConfig.MaxPosts == -1 { 72 | http.Get(config.ApiEndpoint + "?srvid=" + blob.ServerConfig.SrvID + "&key=" + blob.ServerConfig.SrvKey + "&action=stats.posts&value=" + strconv.Itoa(cnt+1)) 73 | return true 74 | } 75 | if cnt > blob.ServerConfig.MaxPosts { 76 | return false 77 | } 78 | http.Get(config.ApiEndpoint + "?srvid=" + blob.ServerConfig.SrvID + "&key=" + blob.ServerConfig.SrvKey + "&action=stats.posts&value=" + strconv.Itoa(cnt+1)) 79 | return true 80 | } 81 | 82 | func OnComment(db *MySQLConn, config *GlobalConfig, blob ConfigBlob) bool { 83 | cnt := CountComments(db) 84 | if blob.ServerConfig.MaxComments == -1 { 85 | http.Get(config.ApiEndpoint + "?srvid=" + blob.ServerConfig.SrvID + "&key=" + blob.ServerConfig.SrvKey + "&action=stats.comments&value=" + strconv.Itoa(cnt+1)) 86 | return true 87 | } 88 | if cnt > blob.ServerConfig.MaxComments { 89 | return false 90 | } 91 | http.Get(config.ApiEndpoint + "?srvid=" + blob.ServerConfig.SrvID + "&key=" + blob.ServerConfig.SrvKey + "&action=stats.comments&value=" + strconv.Itoa(cnt+1)) 92 | return true 93 | } 94 | -------------------------------------------------------------------------------- /src/core/CLevelList.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "github.com/jmoiron/sqlx" 4 | 5 | // Gauntlets 6 | //1 Fire 7 | //2 Ice 8 | //3 Poison 9 | //4 Shadow 10 | //5 Lava 11 | //6 Bonus 12 | //7 Chaos 13 | //8 Demon 14 | //9 Time 15 | //10 Crystal 16 | //11 Magic 17 | //12 spike 18 | //13 Monster 19 | //14 Doom 20 | //15 Death 21 | //16 Forest 22 | //17 Rune 23 | //18 Force 24 | //19 Spooky 25 | //20 Dragon 26 | //21 Water 27 | //22 Haunted 28 | //23 Acid 29 | //24 Witch 30 | //25 Power 31 | //26 Potion 32 | //27 Snake 33 | //28 Toxic 34 | //29 Halloween 35 | //30 Treasure 36 | //31 Ghost 37 | //32 Gem 38 | //33 Inferno 39 | //34 Portal 40 | //35 Strange 41 | //36 Fantasy 42 | //37 Christmas 43 | //38 Surprise 44 | //39 Mystery 45 | //40 Cursed 46 | //41 Cyborg 47 | //42 Castle 48 | //43 Grave 49 | //44 Temple 50 | 51 | type CLevelList struct { 52 | ID int `json:"id"` 53 | Name string `json:"name"` 54 | Description string `json:"description"` 55 | Version int `json:"version"` 56 | Difficulty int `json:"difficulty"` 57 | Downloads int `json:"downloads"` 58 | Likes int `json:"likes"` 59 | IsFeatured bool `json:"is_featured"` 60 | UID int `json:"uid"` 61 | Levels string `json:"-"` 62 | Diamonds int `json:"diamonds"` 63 | LevelDiamonds int `json:"level_diamonds"` 64 | UploadDate string `json:"upload_date"` 65 | UpdateDate string `json:"update_date"` 66 | Unlisted int `json:"unlisted"` 67 | 68 | SideloadUname *string `json:"sideload_uname,omitempty"` 69 | DecoupledLevels []string `json:"levels"` 70 | 71 | DB *MySQLConn `json:"-"` 72 | } 73 | 74 | func (cll *CLevelList) Load(id int) { 75 | cll.DB.MustQueryRow("SELECT id,name,description,version,difficulty,downloads,likes,isFeatured,isUnlisted,uid,levels,diamonds,lvlDiamonds,uploadDate,updateDate FROM #DB#.lists WHERE id=?", id). 76 | Scan(&cll.ID, &cll.Name, &cll.Description, &cll.Version, &cll.Difficulty, &cll.Downloads, &cll.Likes, 77 | &cll.IsFeatured, &cll.Unlisted, &cll.UID, &cll.Levels, &cll.Diamonds, &cll.LevelDiamonds, &cll.UploadDate, &cll.UpdateDate) 78 | } 79 | 80 | func (cll *CLevelList) Exists(lid int) bool { 81 | var count int 82 | cll.DB.MustQueryRow("SELECT COUNT(*) FROM #DB#.lists WHERE id=?", lid).Scan(&count) 83 | return count > 0 84 | } 85 | 86 | func (cll *CLevelList) UpdateList() int { 87 | if !cll.CheckParams() { 88 | return -1 89 | } 90 | cll.DB.ShouldExec("UPDATE #DB#.lists SET name=?,description=?,version=?,difficulty=?,downloads=?,likes=?,isFeatured=?,isUnlisted=?,uid=?,levels=?,diamonds=?,lvlDiamonds=?,uploadDate=?,updateDate=NOW() WHERE id=?", 91 | cll.Name, cll.Description, cll.Version, cll.Difficulty, cll.Downloads, cll.Likes, cll.IsFeatured, cll.Unlisted, cll.UID, cll.Levels, cll.Diamonds, cll.LevelDiamonds, cll.UploadDate, cll.ID) 92 | 93 | return cll.ID 94 | } 95 | 96 | func (cll *CLevelList) UploadList() int { 97 | if !cll.CheckParams() { 98 | return -1 99 | } 100 | tx := cll.DB.ShouldPrepareExec("INSERT INTO #DB#.lists (name,description,version,difficulty,isUnlisted,uid,levels) VALUES (?,?,?,?,?,?,?)", 101 | cll.Name, cll.Description, cll.Version, cll.Difficulty, cll.Unlisted, cll.UID, cll.Levels) 102 | id, _ := tx.LastInsertId() 103 | cll.ID = int(id) 104 | return cll.ID 105 | } 106 | 107 | func (cll *CLevelList) CheckParams() bool { 108 | if len(cll.Name) > 32 || len(cll.Description) > 256 || len(cll.Levels) == 0 { 109 | return false 110 | } 111 | return true 112 | } 113 | 114 | func (cll *CLevelList) OnDownloadList() { 115 | cll.DB.ShouldExec("UPDATE #DB#.lists SET downloads=downloads+1 WHERE id=?", cll.ID) 116 | } 117 | 118 | func (cll *CLevelList) LikeList(lid int, uid int, action int) bool { 119 | if IsLiked(ITEMTYPE_LEVEL, uid, lid, cll.DB) { 120 | return false 121 | } 122 | actionv := "+" 123 | actions := "Like" 124 | if action == CLEVEL_ACTION_DISLIKE { 125 | actionv = "-" 126 | actions = "Dislike" 127 | } 128 | cll.DB.ShouldExec("UPDATE #DB#.lists SET likes=likes"+actionv+"1 WHERE id=?", lid) 129 | RegisterAction(ACTION_LIST_LIKE, uid, lid, map[string]string{"type": actions}, cll.DB) 130 | return true 131 | } 132 | 133 | func (cll *CLevelList) DeleteList() { 134 | cll.DB.ShouldExec("DELETE FROM #DB#.lists WHERE id=?", cll.ID) 135 | } 136 | 137 | func (cll *CLevelList) IsOwnedBy(uid int) bool { 138 | cll.Load(cll.ID) 139 | if cll.ID == 0 { 140 | return false 141 | } 142 | return uid == cll.UID 143 | } 144 | 145 | func (cll *CLevelList) LoadBulkSearch(ids []int) []CLevelList { 146 | var res []CLevelList 147 | query := "SELECT id,name,description,version,difficulty,downloads,likes,isFeatured,isUnlisted,#DB#.lists.uid,levels,#DB#.lists.diamonds," + 148 | "lvlDiamonds,uploadDate,updateDate, #DB#.users.uname FROM #DB#.lists LEFT JOIN #DB#.users on #DB#.lists.uid=#DB#.users.uid WHERE id IN(?)" 149 | q, args, _ := sqlx.In(query, ids) 150 | rows := cll.DB.MustQuery(q, args...) 151 | defer rows.Close() 152 | for rows.Next() { 153 | levl := CLevelList{DB: cll.DB} 154 | e := rows.Scan(&levl.ID, &levl.Name, &levl.Description, &levl.Version, &levl.Difficulty, &levl.Downloads, &levl.Likes, 155 | &levl.IsFeatured, &levl.Unlisted, &levl.UID, &levl.Levels, &levl.Diamonds, &levl.LevelDiamonds, &levl.UploadDate, &levl.UpdateDate, 156 | &levl.SideloadUname) 157 | if levl.SideloadUname == nil { 158 | s := "[DELETED]" 159 | levl.SideloadUname = &s 160 | } 161 | if e != nil { 162 | SendMessageDiscord(e.Error()) 163 | } 164 | res = append(res, levl) 165 | } 166 | 167 | return res 168 | } 169 | 170 | // !TEMP 171 | func (cll *CLevelList) Preload() { 172 | cll.DB.ShouldExec(` 173 | CREATE TABLE IF NOT EXISTS #DB#.lists 174 | ( 175 | id int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT, 176 | name varchar(32) NOT NULL DEFAULT 'Unnamed', 177 | description varchar(256) NOT NULL DEFAULT '', 178 | uid int(11) NOT NULL DEFAULT 0, 179 | version tinyint NOT NULL DEFAULT 1, 180 | difficulty tinyint NOT NULL DEFAULT -1, 181 | downloads int NOT NULL DEFAULT 0, 182 | likes int NOT NULL DEFAULT 0, 183 | isFeatured tinyint(1) NOT NULL DEFAULT 0, 184 | isUnlisted tinyint(1) NOT NULL DEFAULT 0, 185 | levels mediumtext NOT NULL DEFAULT '', 186 | diamonds int NOT NULL DEFAULT 0, 187 | lvlDiamonds int NOT NULL DEFAULT 0, 188 | uploadDate DATETIME NOT NULL DEFAULT NOW(), 189 | updateDate DATETIME NOT NULL DEFAULT NOW() 190 | ) 191 | `) 192 | } 193 | -------------------------------------------------------------------------------- /src/core/CLevelListFilter.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | const ( 10 | CLEVELLISTFILTER_MOSTLIKED int = 800 11 | CLEVELLISTFILTER_MOSTDOWNLOADED int = 801 12 | CLEVELLISTFILTER_TRENDING int = 802 13 | CLEVELLISTFILTER_LATEST int = 803 14 | CLEVELLISTFILTER_MAGIC int = 804 15 | CLEVELLISTFILTER_AWARDED int = 805 16 | CLEVELLISTFILTER_SENT int = 806 17 | ) 18 | 19 | type CLevelListFilter struct { 20 | DB *MySQLConn 21 | Count int 22 | } 23 | 24 | /* 25 | * --- [PARAMS] Object --- 26 | * + (s) sterm - search term (used with other params to specify exact usage) 27 | * + (s) diff - difficulties array. If it doesn't exist then all diffs | Ex: array(10,20,30) 28 | * + (b) star - if not set then all, else star/nostar 29 | * + (s) followList - who to follow 30 | */ 31 | 32 | // GenerateQueryString generates SQL string out of params 33 | func (filter *CLevelListFilter) GenerateQueryString(params map[string]string) string { 34 | whereq := "" 35 | // Difficulty 36 | if diff, ok := params["diff"]; ok { 37 | diff = QuickComma(diff) 38 | whereq += " AND difficulty IN (" + diff + ")" 39 | } 40 | 41 | //Is starred 42 | if star, ok := params["star"]; ok { 43 | whereq += " AND diamonds" 44 | if star == "0" { 45 | whereq += "=" 46 | } else { 47 | whereq += ">" 48 | } 49 | whereq += "0" 50 | } 51 | 52 | return whereq 53 | } 54 | 55 | // SearchLevels searches Levels with filters given 56 | func (filter *CLevelListFilter) SearchLists(page int, params map[string]string, xtype int) []int { 57 | page = int(math.Abs(float64(page))) * 10 58 | suffix := filter.GenerateQueryString(params) 59 | query := " FROM #DB#.lists WHERE 1=1" //placeholder 60 | orderBy := "" 61 | 62 | switch xtype { 63 | case CLEVELLISTFILTER_MOSTLIKED: 64 | orderBy = "likes DESC, downloads DESC" 65 | case CLEVELLISTFILTER_MOSTDOWNLOADED: 66 | orderBy = "downloads DESC, likes DESC" 67 | case CLEVELLISTFILTER_TRENDING: 68 | date := time.Now().AddDate(0, 0, -7).Format("2006-01-02 15:04:05") 69 | query += " AND uploadDate>'" + date + "'" 70 | orderBy = "likes DESC, downloads DESC" 71 | case CLEVELLISTFILTER_LATEST: 72 | orderBy = "uploadDate DESC, downloads DESC" 73 | case CLEVELLISTFILTER_MAGIC: 74 | orderBy = "uploadDate DESC, downloads DESC" 75 | query += "" // robtop sniffed some coke 76 | case CLEVELLISTFILTER_AWARDED: 77 | orderBy = "uploadDate DESC, downloads DESC" 78 | query += " AND isFeatured>0 AND diamonds>0" 79 | case CLEVELLISTFILTER_SENT: 80 | orderBy = "uploadDate DESC, downloads DESC" 81 | query += " AND isFeatured=0 AND diamonds=0" 82 | default: 83 | query += " AND 1=0" //Because I can 84 | } 85 | sortstr := " ORDER BY " + orderBy + " LIMIT 10 OFFSET " + strconv.Itoa(page) 86 | 87 | var lists []int 88 | 89 | //If we actually search for something 90 | if sterm, ok := params["sterm"]; ok { 91 | // If it's an ID 92 | if _, err := strconv.Atoi(sterm); err == nil { 93 | compq := query + " AND id=?" + suffix 94 | rows := filter.DB.ShouldQuery("SELECT id"+compq+sortstr, sterm) 95 | defer rows.Close() 96 | filter.DB.ShouldQueryRow("SELECT count(*) as cnt"+compq, sterm).Scan(&filter.Count) 97 | for rows.Next() { 98 | var lid int 99 | rows.Scan(&lid) 100 | lists = append(lists, lid) 101 | } 102 | } else { 103 | // But if it's just text we search title 104 | //! To support unlisted2 aka friendList maybe we should use isUnlisted<>1 or "isUnlisted=ANY(0"+",2"+") 105 | compq := query + " AND name LIKE ? AND isUnlisted=0" + suffix 106 | rows := filter.DB.ShouldQuery("SELECT id"+compq+sortstr, "%"+sterm+"%") 107 | defer rows.Close() 108 | filter.DB.ShouldQueryRow("SELECT count(*) as cnt"+compq, "%"+sterm+"%").Scan(&filter.Count) 109 | for rows.Next() { 110 | var lid int 111 | rows.Scan(&lid) 112 | lists = append(lists, lid) 113 | } 114 | } 115 | } else { 116 | // Or if we're just wandering and clicking buttons 117 | compq := query + " AND isUnlisted=0" + suffix 118 | rows := filter.DB.ShouldQuery("SELECT id" + compq + sortstr) 119 | defer rows.Close() 120 | filter.DB.ShouldQueryRow("SELECT count(*) as cnt" + compq).Scan(&filter.Count) 121 | for rows.Next() { 122 | var lid int 123 | rows.Scan(&lid) 124 | lists = append(lists, lid) 125 | } 126 | } 127 | 128 | return lists 129 | } 130 | 131 | // SearchUserLevels searches levels of Followed users or by UID 132 | func (filter *CLevelListFilter) SearchUserLists(page int, params map[string]string, followMode bool) []int { 133 | page = int(math.Abs(float64(page))) * 10 134 | suffix := filter.GenerateQueryString(params) 135 | query := " FROM #DB#.lists WHERE 1=1" //placehodler 136 | sortstr := " ORDER BY downloads DESC LIMIT 10 OFFSET " + strconv.Itoa(page) 137 | 138 | var lists []int 139 | 140 | if sterm, ok := params["sterm"]; ok { 141 | if followMode { 142 | if _, err := strconv.Atoi(sterm); err != nil { 143 | query += " AND isUnlisted=0 AND name LIKE ?" 144 | sterm = "%" + sterm + "%" 145 | } else { 146 | query += " AND id=?" 147 | } 148 | compq := query + " AND uid IN (" + QuickComma(params["followList"]) + ")" + suffix 149 | rows := filter.DB.ShouldQuery("SELECT id"+compq+sortstr, sterm) 150 | defer rows.Close() 151 | filter.DB.ShouldQueryRow("SELECT count(*) as cnt"+compq, sterm).Scan(&filter.Count) 152 | for rows.Next() { 153 | var lid int 154 | rows.Scan(&lid) 155 | lists = append(lists, lid) 156 | } 157 | } else { 158 | if stermi, err := strconv.Atoi(sterm); err == nil { 159 | compq := query + " AND uid=?" + suffix 160 | rows := filter.DB.ShouldQuery("SELECT id"+compq+sortstr, stermi) 161 | defer rows.Close() 162 | filter.DB.ShouldQueryRow("SELECT count(*) as cnt"+compq, stermi).Scan(&filter.Count) 163 | for rows.Next() { 164 | var lid int 165 | rows.Scan(&lid) 166 | lists = append(lists, lid) 167 | } 168 | } 169 | } 170 | } else { 171 | if followMode { 172 | compq := query + " AND isUnlisted=0 AND uid IN (" + QuickComma(params["followList"]) + ")" + suffix 173 | rows := filter.DB.ShouldQuery("SELECT id" + compq + sortstr) 174 | defer rows.Close() 175 | filter.DB.ShouldQueryRow("SELECT count(*) as cnt" + compq).Scan(&filter.Count) 176 | for rows.Next() { 177 | var lid int 178 | rows.Scan(&lid) 179 | lists = append(lists, lid) 180 | } 181 | } else { 182 | compq := query + suffix 183 | rows := filter.DB.ShouldQuery("SELECT id" + compq + sortstr) 184 | defer rows.Close() 185 | filter.DB.ShouldQueryRow("SELECT count(*) as cnt" + compq).Scan(&filter.Count) 186 | for rows.Next() { 187 | var lid int 188 | rows.Scan(&lid) 189 | lists = append(lists, lid) 190 | } 191 | } 192 | } 193 | 194 | return lists 195 | } 196 | -------------------------------------------------------------------------------- /src/core/CMessage.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "slices" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type CMessage struct { 11 | Id int `json:"id"` 12 | UidSrc int `json:"uid_src"` 13 | UidDest int `json:"uid_dest"` 14 | Subject string `json:"subject"` 15 | Message string `json:"message"` 16 | PostedTime string `json:"posted_time"` 17 | IsNew bool `json:"is_new"` 18 | 19 | DB *MySQLConn `json:"-"` 20 | } 21 | 22 | func (cm *CMessage) Exists(id int) bool { 23 | var cnt int 24 | cm.DB.MustQueryRow("SELECT count(*) as cnt FROM #DB#.messages WHERE id=?", id).Scan(&cnt) 25 | return cnt > 0 26 | } 27 | 28 | func (cm *CMessage) CountMessages(uid int, isNew bool) int { 29 | var cnt int 30 | var postfix string 31 | if isNew { 32 | postfix = " AND isNew=1" 33 | } 34 | cm.DB.MustQueryRow("SELECT count(*) as cnt FROM #DB#.messages WHERE uid_dest=?"+postfix, uid).Scan(&cnt) 35 | return cnt 36 | } 37 | 38 | func (cm *CMessage) LoadMessageById(id int) { 39 | if id > 0 { 40 | cm.Id = id 41 | } 42 | if cm.DB.ShouldQueryRow("SELECT uid_src,uid_dest,subject,body,postedTime,isNew FROM #DB#.messages WHERE id=?", cm.Id).Scan( 43 | &cm.UidSrc, &cm.UidDest, &cm.Subject, &cm.Message, &cm.PostedTime, &cm.IsNew) == nil { 44 | cm.DB.ShouldExec("UPDATE #DB#.messages SET isNew=0 WHERE id=?", cm.Id) 45 | } 46 | } 47 | 48 | func (cm *CMessage) DeleteMessage(uid int) { 49 | cm.DB.ShouldExec("DELETE FROM #DB#.messages WHERE id=? AND (uid_src=? OR uid_dest=?)", cm.Id, uid, uid) 50 | } 51 | 52 | func (cm *CMessage) SendMessageObj() bool { 53 | if len(cm.Subject) > 256 || len(cm.Message) > 1024 { 54 | return false 55 | } 56 | acc := CAccount{DB: cm.DB} 57 | acc.Uid = cm.UidDest 58 | acc.LoadSettings() 59 | if acc.MS == 2 { 60 | return false 61 | } 62 | acc.LoadSocial() 63 | blacklist := strings.Split(acc.Blacklist, ",") 64 | if slices.Contains(blacklist, strconv.Itoa(cm.UidSrc)) { 65 | return false 66 | } 67 | if acc.MS == 1 { 68 | cf := CFriendship{DB: cm.DB} 69 | if !cf.IsAlreadyFriend(cm.UidSrc, cm.UidDest) { 70 | return false 71 | } 72 | } 73 | cm.DB.ShouldExec("INSERT INTO #DB#.messages (uid_src,uid_dest,subject,body,postedTime) VALUES(?,?,?,?,?)", 74 | cm.UidSrc, cm.UidDest, cm.Subject, cm.Message, time.Now().Format("2006-01-02 15:04:05")) 75 | return true 76 | } 77 | 78 | func (cm *CMessage) GetMessageForUid(uid int, page int, sent bool) (int, []map[string]string) { 79 | page *= 10 80 | var cnt int 81 | pf := "uid_dest" 82 | if sent { 83 | pf = "uid_src" 84 | } 85 | cm.DB.MustQueryRow("SELECT count(*) as cnt FROM #DB#.messages WHERE "+pf+"=?", uid).Scan(&cnt) 86 | if cnt == 0 { 87 | return 0, []map[string]string{} 88 | } 89 | rows := cm.DB.ShouldQuery("SELECT id,uid_src,uid_dest,subject,body,postedTime,isNew FROM #DB#.messages WHERE "+pf+"=? ORDER BY id limit 10 OFFSET "+strconv.Itoa(page), uid) 90 | defer rows.Close() 91 | var out []map[string]string 92 | for rows.Next() { 93 | msg := CMessage{} 94 | rows.Scan(&msg.Id, &msg.UidSrc, &msg.UidDest, &msg.Subject, &msg.Message, &msg.PostedTime, &msg.IsNew) 95 | blk := map[string]string{ 96 | "id": strconv.Itoa(msg.Id), 97 | "subject": msg.Subject, 98 | "message": msg.Message, 99 | "isOld": strconv.Itoa(ToInt(!msg.IsNew)), 100 | "date": msg.PostedTime, 101 | } 102 | 103 | uid := msg.UidSrc 104 | if sent { 105 | uid = msg.UidDest 106 | } 107 | acc := CAccount{DB: cm.DB, Uid: uid} 108 | if acc.Exists(uid) { 109 | acc.LoadAuth(CAUTH_UID) 110 | blk["uname"] = acc.Uname 111 | } else { 112 | blk["uname"] = "[DELETED]" 113 | } 114 | blk["uid"] = strconv.Itoa(uid) 115 | if msg.IsNew { 116 | cm.DB.ShouldExec("UPDATE #DB#.messages SET isNew=0 WHERE id=?", msg.Id) 117 | } 118 | out = append(out, blk) 119 | } 120 | return cnt, out 121 | } 122 | -------------------------------------------------------------------------------- /src/core/CMusic.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/jmoiron/sqlx" 8 | "io" 9 | "net/http" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | type CMusic struct { 16 | Status string `json:"status"` 17 | Id int `json:"id"` 18 | Name string `json:"name"` 19 | Artist string `json:"artist"` 20 | Size float64 `json:"size"` 21 | Url string `json:"url"` 22 | IsBanned bool `json:"is_banned"` 23 | Downloads int `json:"downloads"` 24 | 25 | DB *MySQLConn `json:"-"` 26 | Logger Logger `json:"-"` 27 | Config *GlobalConfig `json:"-"` 28 | ConfBlob ConfigBlob `json:"-"` 29 | } 30 | 31 | func (mus *CMusic) Exists(id int) bool { 32 | var cnt int 33 | mus.DB.MustQueryRow("SELECT count(*) as cnt FROM #DB#.songs WHERE id=?", id).Scan(&cnt) 34 | return cnt > 0 35 | } 36 | 37 | func (mus *CMusic) RequestNGOuter(id int) bool { 38 | mus.Id = id 39 | resp, err := http.Get(mus.Config.ApiEndpoint + "?srvid=" + mus.ConfBlob.ServerConfig.SrvID + "&key=" + 40 | mus.ConfBlob.ServerConfig.SrvKey + "&action=requestSong&id=" + strconv.Itoa(id)) 41 | if err != nil { 42 | return false 43 | } 44 | rsp, _ := io.ReadAll(resp.Body) 45 | rsp = bytes.ReplaceAll(rsp, []byte("#"), []byte("")) 46 | if err = json.Unmarshal(rsp, mus); err != nil { 47 | fmt.Println(err) 48 | } 49 | 50 | return mus.Status == "ok" 51 | } 52 | 53 | func (mus *CMusic) TransformHalResource() bool { 54 | arn := strings.Split(mus.Url, ":") 55 | if !mus.isArnValid(arn) { 56 | return false 57 | } 58 | resp, err := http.Get(mus.Config.ApiEndpoint + "?srvid=" + mus.ConfBlob.ServerConfig.SrvID + "&key=" + 59 | mus.ConfBlob.ServerConfig.SrvKey + "&action=requestSongARN&type=" + arn[1] + "&id=" + arn[2]) 60 | if err != nil { 61 | return false 62 | } 63 | rsp, _ := io.ReadAll(resp.Body) 64 | rsp = bytes.ReplaceAll(rsp, []byte("#"), []byte("")) 65 | bufArtist := mus.Artist 66 | bufTitle := mus.Name 67 | if err = json.Unmarshal(rsp, mus); err != nil { 68 | fmt.Println(err) 69 | } 70 | mus.Artist = bufArtist 71 | mus.Name = bufTitle 72 | return mus.Status == "ok" 73 | } 74 | 75 | func (mus *CMusic) isArnValid(arn []string) bool { 76 | if len(arn) != 3 { 77 | return false 78 | } 79 | switch arn[1] { 80 | case "ng": 81 | if f, _ := regexp.MatchString(`[0-9]`, arn[2]); !f { 82 | return false 83 | } 84 | case "dz": 85 | if f, _ := regexp.MatchString(`[0-9]`, arn[2]); !f { 86 | return false 87 | } 88 | case "sc": 89 | if f, _ := regexp.MatchString(`(?i)([a-z\d\-\_])+[\\\\\/]([a-z\d\-\_])+$`, arn[2]); !f { 90 | return false 91 | } 92 | case "yt": 93 | if f, _ := regexp.MatchString(`(?i)^([a-z\d\-\_])+$`, arn[2]); !f { 94 | return false 95 | } 96 | case "vk": 97 | if f, _ := regexp.MatchString(`^(\d)+\_(\d)+$`, arn[2]); !f { 98 | return false 99 | } 100 | default: 101 | return false 102 | } 103 | return true 104 | } 105 | 106 | func (mus *CMusic) TransformBulkHalResources(links []string) []CMusic { 107 | var musics []CMusic 108 | var linksValid []string 109 | for _, link := range links { 110 | arn := strings.Split(link, ":") 111 | if !mus.isArnValid(arn) { 112 | continue 113 | } 114 | linksValid = append(linksValid, arn[1]+":"+arn[2]) 115 | } 116 | resp, err := http.Get(mus.Config.ApiEndpoint + "?srvid=" + mus.ConfBlob.ServerConfig.SrvID + "&key=" + 117 | mus.ConfBlob.ServerConfig.SrvKey + "&action=requestSongARNList&ids=" + strings.Join(linksValid, ",")) 118 | if err != nil { 119 | return musics 120 | } 121 | rsp, _ := io.ReadAll(resp.Body) 122 | rsp = bytes.ReplaceAll(rsp, []byte("#"), []byte("")) 123 | if err = json.Unmarshal(rsp, &musics); err != nil { 124 | fmt.Println(err) 125 | } 126 | return musics 127 | } 128 | 129 | func (mus *CMusic) GetSong(id int) bool { 130 | if !mus.ConfBlob.ServerConfig.HalMusic { 131 | return mus.RequestNGOuter(id) 132 | } 133 | if !mus.Exists(id) { 134 | return false 135 | } 136 | mus.DB.MustQueryRow("SELECT id,name,artist,size,url,isBanned,downloads FROM #DB#.songs WHERE id=?", id).Scan( 137 | &mus.Id, &mus.Name, &mus.Artist, &mus.Size, &mus.Url, &mus.IsBanned, &mus.Downloads) 138 | if mus.IsBanned { 139 | return false 140 | } 141 | if len(mus.Url) > 4 && mus.Url[0:4] == "hal:" { 142 | return mus.TransformHalResource() 143 | } 144 | return true 145 | } 146 | 147 | func (mus *CMusic) GetBulkSongs(ids []int) []CMusic { 148 | q, args, _ := sqlx.In("SELECT id,name,artist,size,url,isBanned,downloads FROM #DB#.songs WHERE id IN (?)", ids) 149 | rows := mus.DB.ShouldQuery(q, args...) 150 | defer rows.Close() 151 | var musics []CMusic 152 | var queryMusics []CMusic 153 | var externalQueries []string 154 | for rows.Next() { 155 | var xmus CMusic 156 | rows.Scan(&xmus.Id, &xmus.Name, &xmus.Artist, &xmus.Size, &xmus.Url, &xmus.IsBanned, &xmus.Downloads) 157 | if xmus.IsBanned { 158 | continue 159 | } 160 | if len(xmus.Url) > 4 && xmus.Url[0:4] == "hal:" { 161 | externalQueries = append(externalQueries, xmus.Url) 162 | queryMusics = append(queryMusics, xmus) 163 | } else { 164 | musics = append(musics, xmus) 165 | } 166 | } 167 | if len(externalQueries) > 0 { 168 | deltas := mus.TransformBulkHalResources(externalQueries) 169 | //musics = append(musics, deltas...) 170 | for i := 0; i < len(deltas); i++ { 171 | delta := deltas[i] 172 | delta.Id = queryMusics[i].Id 173 | if delta.Status != "ok" { 174 | musics = append(musics, queryMusics[i]) 175 | } else { 176 | musics = append(musics, delta) 177 | } 178 | } 179 | //for _, delta := range deltas { 180 | // for _, muss := range queryMusics { 181 | // log.Println("Checking", muss.Id, "against", delta.Id) 182 | // if delta.Id == muss.Id { 183 | // delta.Name = muss.Name 184 | // delta.Artist = muss.Artist 185 | // musics = append(musics, delta) 186 | // log.Println("Added", delta.Id) 187 | // } 188 | // } 189 | //} 190 | } 191 | return musics 192 | } 193 | 194 | func (mus *CMusic) UploadSong() int { 195 | c := mus.DB.ShouldPrepareExec("INSERT INTO #DB#.songs (name,artist,size,url) VALUES (?,?,?,?)", mus.Name, mus.Artist, mus.Size, mus.Url) 196 | id, _ := c.LastInsertId() 197 | return int(id) 198 | } 199 | 200 | func (mus *CMusic) BanMusic(id int, ban bool) { 201 | mus.DB.ShouldExec("UPDATE #DB#.songs SET isBanned=? WHERE id=?", ban, id) 202 | } 203 | 204 | func (mus *CMusic) CountDownloads() { 205 | req := mus.DB.MustQuery("SELECT id FROM #DB#.songs") 206 | defer req.Close() 207 | for req.Next() { 208 | var id int 209 | req.Scan(&id) 210 | var cnt int 211 | mus.DB.ShouldQueryRow("SELECT SUM(downloads) FROM #DB#.levels WHERE song_id=?", id).Scan(&cnt) 212 | mus.DB.ShouldExec("UPDATE #DB#.songs SET downloads=? WHERE id=?", cnt, strconv.Itoa(id)) 213 | } 214 | } 215 | 216 | // ! Implement normal API 217 | func (mus *CMusic) GetTopArtists() map[string]string { 218 | return map[string]string{ 219 | "Riot [Monstercat]": "Monstercat", 220 | "Noisestorm": "noisestorm", 221 | "Nitro Fun": "NitroFunOfficial", 222 | "Throttle": "officialThrottle", 223 | "Tokyo Machine": "TokyoMachine", 224 | "BadComputer": "BadComputer", 225 | "Liquid Soul": "liquidsouliboga", 226 | "Xtrullor": "xtrullor", 227 | "Creo": "CreoMusic", 228 | "Dirty Paws": "DirtyPaws", 229 | "Have better candidates? Join FruitSpace Discord": "", 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/core/CProtect.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "math" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type CProtect struct { 13 | DB *MySQLConn 14 | LevelModel ProtectModel 15 | Savepath string 16 | DisableProtection bool 17 | } 18 | 19 | type ProtectModel struct { 20 | MaxStars int 21 | MaxLevelUpload int 22 | PeakLevelUpload int 23 | Stats map[string]int 24 | } 25 | 26 | func (protect *CProtect) LoadModel(config *GlobalConfig, blob ConfigBlob) { 27 | model, err := os.ReadFile(protect.Savepath + "/levelModel.json") 28 | if err != nil { 29 | os.Mkdir(protect.Savepath, 0777) 30 | protect.FillLevelModel() 31 | //req, err := http.Get(config.ApiEndpoint + "?id=" + blob.ServerConfig.SrvID + "&key=" + blob.ServerConfig.SrvKey + "&action=getModel") 32 | //data, err := io.ReadAll(req.Body) 33 | //if err != nil || req.StatusCode != 200 { 34 | // protect.DisableProtection = true 35 | // return 36 | //} 37 | //os.WriteFile(protect.Savepath+"/levelModel.json", data, 0755) 38 | //protect.LoadModel(config, blob) 39 | } 40 | json.Unmarshal(model, &protect.LevelModel) 41 | if protect.LevelModel.MaxLevelUpload == 0 { 42 | protect.LevelModel.MaxLevelUpload = 10 43 | } 44 | } 45 | 46 | func (protect *CProtect) FillLevelModel() { 47 | 48 | //Calculate LevelModel 49 | date := time.Now() 50 | stats := make(map[string]int) 51 | total := 0 52 | for i := 0; i < 7; i++ { 53 | current := strings.Split(date.AddDate(0, 0, -1*i).Format("2006-01-02 15:04:05"), " ")[0] 54 | currentIndex := strings.Split(date.AddDate(0, 0, -1*(i+1)).Format("2006-01-02 15:04:05"), " ")[0] 55 | var count int 56 | protect.DB.ShouldQueryRow("SELECT count(*) as cnt FROM #DB#.actions WHERE type=4 AND date? AND data LIKE '%Upload%'", 57 | current, currentIndex).Scan(&count) 58 | stats[currentIndex] = count 59 | if count > protect.LevelModel.PeakLevelUpload { 60 | protect.LevelModel.PeakLevelUpload = count 61 | } 62 | total += count 63 | } 64 | if total < 10 { 65 | protect.LevelModel.MaxLevelUpload = 10 66 | } else { 67 | protect.LevelModel.MaxLevelUpload = int(math.Round(float64(total/7))) + protect.LevelModel.PeakLevelUpload 68 | } 69 | 70 | //Calculate total stars allowed 71 | var count2, count1 int 72 | protect.DB.ShouldQueryRow("SELECT SUM(starsGot) as stars FROM #DB#.levels").Scan(&count1) 73 | protect.DB.ShouldQueryRow("SELECT SUM(packStars) as stars FROM #DB#.levelpacks").Scan(&count2) 74 | protect.LevelModel.MaxStars = 200 + count1 + count2 75 | 76 | protect.LevelModel.Stats = stats 77 | //Dump 78 | data, err := json.Marshal(protect.LevelModel) 79 | if err != nil { 80 | data = []byte("{}") 81 | } 82 | protect.DB.logger.Must(os.WriteFile(protect.Savepath+"/levelModel.json", data, 0755)) 83 | } 84 | 85 | func (protect *CProtect) ResetUserLimits() { 86 | protect.DB.ShouldExec("UPDATE #DB#.users SET protect_levelsToday=0") 87 | protect.DB.ShouldExec("UPDATE #DB#.users SET protect_todayStars=stars") 88 | } 89 | 90 | func (protect *CProtect) DetectLevelModel(uid int) bool { 91 | //FIXME 92 | if 1 == 1 { 93 | return true 94 | } 95 | if protect.DisableProtection { 96 | return true 97 | } 98 | var lvlCnt int 99 | protect.DB.ShouldQueryRow("SELECT protect_levelsToday as cnt FROM #DB#.users WHERE uid=?", uid).Scan(&lvlCnt) 100 | if lvlCnt >= protect.LevelModel.MaxLevelUpload { 101 | protect.DB.ShouldExec("UPDATE #DB#.users SET isBanned=2 WHERE uid=?", uid) 102 | RegisterAction(ACTION_BAN_BAN, 0, uid, map[string]string{"type": "Ban:LevelAuto"}, protect.DB) 103 | SendMessageDiscord("[" + protect.Savepath + "] User " + strconv.Itoa(uid) + " has been banned for uploading too many levels (" + strconv.Itoa(lvlCnt) + "/" + strconv.Itoa(protect.LevelModel.MaxLevelUpload) + ") in a day.") 104 | return false 105 | } 106 | protect.DB.ShouldExec("UPDATE #DB#.users SET protect_levelsToday=protect_levelsToday+1 WHERE uid=?", uid) 107 | return true 108 | } 109 | 110 | func (protect *CProtect) DetectStats(uid int, stars int, diamonds int, demons int, coins int, ucoins int) bool { 111 | if protect.DisableProtection { 112 | return true 113 | } 114 | if stars < 0 || diamonds < 0 || demons < 0 || coins < 0 || ucoins < 0 { 115 | protect.DB.ShouldExec("UPDATE #DB#.users SET isBanned=2 WHERE uid=?", uid) 116 | protect.DB.ShouldExec("DELETE FROM #DB#.levels WHERE uid=?", uid) 117 | protect.DB.ShouldExec("DELETE FROM #DB#.actions WHERE type=4 AND uid=?", uid) 118 | RegisterAction(ACTION_BAN_BAN, 0, uid, map[string]string{"type": "Ban:StatsNegative"}, protect.DB) 119 | SendMessageDiscord("User " + strconv.Itoa(uid) + " has been banned for having negative stats.") 120 | return false 121 | } 122 | //FIXME 123 | if 1 == 1 { 124 | return true 125 | } 126 | if protect.LevelModel.MaxStars == 0 { 127 | protect.LevelModel.MaxStars = 200 128 | } 129 | var starCnt int 130 | protect.DB.ShouldQueryRow("SELECT protect_todayStars FROM #DB#.users WHERE uid=?", uid).Scan(&starCnt) 131 | if (stars - starCnt) > protect.LevelModel.MaxStars { 132 | protect.DB.ShouldExec("UPDATE #DB#.users SET isBanned=2 WHERE uid=?", uid) 133 | RegisterAction(ACTION_BAN_BAN, 0, uid, map[string]string{"type": "Ban:StarsLimit"}, protect.DB) 134 | SendMessageDiscord("User " + strconv.Itoa(uid) + " has been banned for having too many stars (" + strconv.Itoa(stars) + "+" + strconv.Itoa(starCnt) + "/" + strconv.Itoa(protect.LevelModel.MaxStars) + ").") 135 | return false 136 | } 137 | return true 138 | } 139 | 140 | func (protect *CProtect) GetMeta(uid int) map[string]int { 141 | meta := make(map[string]int) 142 | var sMeta string 143 | protect.DB.ShouldQueryRow("SELECT protect_meta FROM #DB#.users WHERE uid=?", uid).Scan(&sMeta) 144 | json.Unmarshal([]byte(sMeta), &meta) 145 | return meta 146 | } 147 | 148 | func (protect *CProtect) DetectMessages(uid int) bool { 149 | if protect.DisableProtection { 150 | return true 151 | } 152 | meta := protect.GetMeta(uid) 153 | t := int(time.Now().Unix()) 154 | if t-meta["msg_time"] < 60 { 155 | return false 156 | } 157 | meta["msg_time"] = t 158 | data, _ := json.Marshal(meta) 159 | protect.DB.ShouldExec("UPDATE #DB#.users SET protect_meta=? WHERE uid=?", string(data), uid) 160 | return true 161 | } 162 | 163 | func (protect *CProtect) DetectPosts(uid int) bool { 164 | if protect.DisableProtection { 165 | return true 166 | } 167 | meta := protect.GetMeta(uid) 168 | t := int(time.Now().Unix()) 169 | if t-meta["post_time"] < 60 { 170 | return false 171 | } 172 | meta["post_time"] = t 173 | data, _ := json.Marshal(meta) 174 | protect.DB.ShouldExec("UPDATE #DB#.users SET protect_meta=? WHERE uid=?", string(data), uid) 175 | return true 176 | } 177 | 178 | func (protect *CProtect) DetectComments(uid int) bool { 179 | if protect.DisableProtection { 180 | return true 181 | } 182 | meta := protect.GetMeta(uid) 183 | t := int(time.Now().Unix()) 184 | if t-meta["comm_time"] < 30 { 185 | return false 186 | } 187 | meta["comm_time"] = t 188 | data, _ := json.Marshal(meta) 189 | protect.DB.ShouldExec("UPDATE #DB#.users SET protect_meta=? WHERE uid=?", string(data), uid) 190 | return true 191 | } 192 | 193 | func CheckIPBan(IPAddr string, config ConfigBlob) bool { 194 | return InArray(config.SecurityConfig.BannedIPs, IPAddr) 195 | } 196 | -------------------------------------------------------------------------------- /src/core/CQuests.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | const ( 12 | QUEST_TYPE_DAILY = 0 13 | QUEST_TYPE_WEEKLY = 1 14 | QUEST_TYPE_EVENT = -1 15 | QUEST_TYPE_CHALLENGE = 2 16 | ) 17 | 18 | type CQuests struct { 19 | DB *MySQLConn 20 | } 21 | 22 | func (cq *CQuests) Exists(cType int) bool { 23 | switch cType { 24 | case -1: 25 | cType = 0 26 | case -2: 27 | cType = 1 28 | case -3: 29 | cType = -1 30 | } 31 | xType := "=" + strconv.Itoa(cType) 32 | if cType == 2 { 33 | xType = ">1" 34 | } 35 | var cnt int 36 | cq.DB.MustQueryRow("SELECT count(*) as cnt FROM #DB#.quests WHERE type" + xType).Scan(&cnt) 37 | return cnt > 0 38 | } 39 | 40 | func (cq *CQuests) GetDaily() (id int, lvlId int) { 41 | cq.DB.ShouldQueryRow("SELECT id, lvl_id FROM #DB#.quests WHERE type=0 AND timeExpire1)) AS id) AS r2 " + 65 | "WHERE r1.id >= r2.id AND r1.timeExpire1 ORDER BY r1.id ASC LIMIT 3") 66 | defer req.Close() 67 | out := "" 68 | var cnt int 69 | for req.Next() { 70 | var id, xType, needed, reward int 71 | var name, timeExpire string 72 | req.Scan(&id, &xType, &needed, &reward, &name, &timeExpire) 73 | out += strconv.Itoa(id) + "," + strconv.Itoa(xType-1) + "," + strconv.Itoa(needed) + "," + strconv.Itoa(reward) + "," + name + ":" 74 | cnt++ 75 | } 76 | for cnt < 3 { 77 | out += "1337,2,5,50,Random issues:" 78 | cnt++ 79 | } 80 | return out[:len(out)-1] 81 | } 82 | 83 | func (cq *CQuests) GetSpecialLevel(xType int) (id int, timeleft int) { 84 | timeLeft := 0 85 | var evtId, xEvtId int 86 | tme, _ := time.ParseInLocation("2006-01-02 15:04:05", strings.Split(time.Now().Format("2006-01-02 15:04:05"), " ")[0]+" 00:00:00", loc) 87 | switch xType { 88 | case -1: 89 | fallthrough 90 | case 0: 91 | //!Additional 10800 Review is needed 92 | timeLeft = int(tme.AddDate(0, 0, 1).Unix() - (time.Now().Unix())) 93 | case 1: 94 | timeLeft = int(tme.AddDate(0, 0, 7).Unix() - (time.Now().Unix())) 95 | xEvtId = 100001 96 | } 97 | cq.DB.ShouldQueryRow("SELECT id FROM #DB#.quests WHERE type=" + strconv.Itoa(xType) + " AND timeExpire 0 35 | } 36 | 37 | func (cs *CScores) LoadScoreById() { 38 | cs.DB.ShouldQueryRow("SELECT uid,lvl_id,postedTime,percent,attempts,coins FROM #DB#.scores WHERE id=?", cs.Id).Scan( 39 | &cs.Uid, &cs.LvlId, &cs.PostedTime, &cs.Percent, &cs.Attempts, &cs.Coins) 40 | } 41 | 42 | func (cs *CScores) GetScoresForLevelId(lvlId int, types int, acc CAccount) []CScores { 43 | var suffix string 44 | switch types { 45 | case CSCORE_TYPE_WEEK: 46 | date := strings.Split(time.Now().AddDate(0, 0, 8-int(time.Now().Weekday())).Format("2006-01-02 15:04:05"), " ")[0] + " 00:00:00" 47 | suffix = "AND postedTime>='" + date + "'" 48 | case CSCORE_TYPE_FRIENDS: 49 | acc.LoadSocial() 50 | cf := CFriendship{DB: cs.DB} 51 | frs := cf.GetAccFriends(acc) 52 | frs = append(frs, acc.Uid) 53 | xfrs := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(frs)), ","), "[]") 54 | suffix = "AND uid IN(" + strings.ReplaceAll(xfrs, ",,", ",") + ")" 55 | } 56 | req := cs.DB.ShouldQuery("SELECT uid,lvl_id,postedTime,percent,attempts,coins FROM #DB#.scores WHERE lvl_id=? "+suffix+" ORDER BY percent DESC", lvlId) 57 | var scores []CScores 58 | defer req.Close() 59 | for req.Next() { 60 | xcs := CScores{DB: cs.DB} 61 | req.Scan(&xcs.Uid, &xcs.LvlId, &xcs.PostedTime, &xcs.Percent, &xcs.Attempts, &xcs.Coins) 62 | if xcs.Percent == 100 { 63 | xcs.Ranking = 1 64 | } else if xcs.Percent >= 75 { 65 | xcs.Ranking = 2 66 | } else { 67 | xcs.Ranking = 3 68 | } 69 | scores = append(scores, xcs) 70 | } 71 | return scores 72 | } 73 | 74 | func (cs *CScores) GetScoresForPlatformerLevelId(lvlId int, types int, modeCoins bool, acc CAccount) []CScores { 75 | var suffix string 76 | switch types { 77 | case CSCORE_PLAT_WEEK: 78 | date := strings.Split(time.Now().AddDate(0, 0, 8-int(time.Now().Weekday())).Format("2006-01-02 15:04:05"), " ")[0] + " 00:00:00" 79 | suffix = "AND postedTime>='" + date + "'" 80 | case CSCORE_PLAT_FRIENDS: 81 | acc.LoadSocial() 82 | cf := CFriendship{DB: cs.DB} 83 | frs := cf.GetAccFriends(acc) 84 | frs = append(frs, acc.Uid) 85 | xfrs := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(frs)), ","), "[]") 86 | suffix = "AND uid IN(" + strings.ReplaceAll(xfrs, ",,", ",") + ")" 87 | } 88 | req := cs.DB.ShouldQuery("SELECT uid,lvl_id,postedTime,percent,attempts,coins FROM #DB#.scores WHERE lvl_id=? "+suffix+" ORDER BY percent DESC", lvlId) 89 | var scores []CScores 90 | defer req.Close() 91 | rankx := 1 92 | for req.Next() { 93 | xcs := CScores{DB: cs.DB} 94 | req.Scan(&xcs.Uid, &xcs.LvlId, &xcs.PostedTime, &xcs.Percent, &xcs.Attempts, &xcs.Coins) 95 | xcs.Ranking = rankx 96 | rankx++ 97 | scores = append(scores, xcs) 98 | } 99 | return scores 100 | } 101 | 102 | func (cs *CScores) UpdateLevelScore() { 103 | cs.DB.ShouldExec("UPDATE #DB#.scores SET postedTime=?,percent=?,attempts=?,coins=? WHERE lvl_id=? AND uid=?", 104 | time.Now().Format("2006-01-02 15:04:05"), cs.Percent, cs.Attempts, cs.Coins, cs.LvlId, cs.Uid) 105 | } 106 | 107 | func (cs *CScores) UploadLevelScore() { 108 | cs.DB.ShouldExec("INSERT INTO #DB#.scores (uid,lvl_id,postedTime,percent,attempts,coins) VALUES(?,?,?,?,?,?)", 109 | cs.Uid, cs.LvlId, time.Now().Format("2006-01-02 15:04:05"), cs.Percent, cs.Attempts, cs.Coins) 110 | } 111 | -------------------------------------------------------------------------------- /src/core/ConfigBlob.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | type MysqlConfig struct { 9 | Host string 10 | Port int 11 | User string 12 | Password string 13 | DBName string 14 | } 15 | 16 | type LogConfig struct { 17 | LogEnable bool 18 | LogDB bool 19 | LogEndpoints bool 20 | LogRequests bool 21 | } 22 | 23 | type ChestConfig struct { 24 | ChestSmallOrbsMin int 25 | ChestSmallOrbsMax int 26 | ChestSmallDiamondsMin int 27 | ChestSmallDiamondsMax int 28 | ChestSmallShards []int 29 | ChestSmallKeysMin int 30 | ChestSmallKeysMax int 31 | ChestSmallWait int 32 | 33 | ChestBigOrbsMin int 34 | ChestBigOrbsMax int 35 | ChestBigDiamondsMin int 36 | ChestBigDiamondsMax int 37 | ChestBigShards []int 38 | ChestBigKeysMin int 39 | ChestBigKeysMax int 40 | ChestBigWait int 41 | } 42 | 43 | type ServerConfig struct { 44 | SrvID string 45 | SrvKey string 46 | MaxUsers int 47 | MaxLevels int 48 | MaxComments int 49 | MaxPosts int 50 | HalMusic bool 51 | Locked bool 52 | TopSize int 53 | EnableModules map[string]bool 54 | } 55 | 56 | type SecurityConfig struct { 57 | DisableProtection bool 58 | NoLevelLimits bool 59 | AutoActivate bool 60 | BannedIPs []string 61 | } 62 | 63 | type GlobalConfig struct { 64 | MasterKey string 65 | ApiEndpoint string 66 | LogConnector string 67 | LogEndpoint string 68 | MaintenanceMode bool 69 | RedisHost string 70 | RedisPort string 71 | RedisPassword string 72 | RedisDB int 73 | SavePath string 74 | ModuleSettings map[string]string 75 | } 76 | 77 | type ConfigBlob struct { 78 | DBConfig MysqlConfig 79 | LogConfig LogConfig 80 | ChestConfig ChestConfig 81 | ServerConfig ServerConfig 82 | SecurityConfig SecurityConfig 83 | } 84 | 85 | func (glob *GlobalConfig) LoadById(Srvid string) (ConfigBlob, error) { 86 | rdb := RedisConn{} 87 | log := Logger{} 88 | if err := rdb.ConnectBlob(*glob); err != nil { 89 | log.LogWarn(rdb, "Conn: "+err.Error()) 90 | return ConfigBlob{}, err 91 | } 92 | conf := ConfigBlob{} 93 | t, err := rdb.DB.Get(rdb.context, Srvid).Result() 94 | if err != nil { 95 | return ConfigBlob{}, err 96 | } 97 | err = json.Unmarshal([]byte(t), &conf) 98 | if err != nil { 99 | log.LogWarn(rdb, "Marshal: "+err.Error()) 100 | return ConfigBlob{}, err 101 | } 102 | rdb.DB.Close() 103 | if conf.ServerConfig.Locked { 104 | return ConfigBlob{}, errors.New(".ignore") 105 | } 106 | return conf, nil 107 | } 108 | 109 | func (glob *GlobalConfig) PushById(Srvid string, conf ConfigBlob) error { 110 | rdb := RedisConn{} 111 | log := Logger{} 112 | if err := rdb.ConnectBlob(*glob); err != nil { 113 | log.LogWarn(rdb, err.Error()) 114 | return err 115 | } 116 | if len(conf.DBConfig.Password) < 3 { 117 | err := errors.New("Empty blob") 118 | log.LogWarn(conf, err.Error()) 119 | return err 120 | } 121 | data, err := json.Marshal(conf) 122 | if log.Should(err) != nil { 123 | return err 124 | } 125 | if err := log.Should(rdb.DB.Set(rdb.context, Srvid, string(data), 0).Err()); err != nil { 126 | return err 127 | } 128 | return nil 129 | } 130 | -------------------------------------------------------------------------------- /src/core/DBManagement.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "github.com/getsentry/sentry-go" 8 | "github.com/go-redis/redis/v8" 9 | _ "github.com/go-sql-driver/mysql" 10 | "github.com/jmoiron/sqlx" 11 | "strings" 12 | ) 13 | 14 | var DBTunnel *sqlx.DB 15 | 16 | type MySQLConn struct { 17 | logger Logger 18 | DBName string 19 | } 20 | 21 | func (db *MySQLConn) ConnectBlob(config ConfigBlob) error { 22 | db.DBName = config.DBConfig.DBName 23 | 24 | return nil 25 | } 26 | 27 | func (db *MySQLConn) CloseDB() error { 28 | return nil 29 | } 30 | 31 | // PatchQuery replaces #DB# with the database name 32 | func (db *MySQLConn) PatchQuery(query string) string { 33 | return strings.ReplaceAll(query, "#DB#", db.DBName) 34 | } 35 | 36 | func (db *MySQLConn) PrepareExec(query string, args ...interface{}) (sql.Result, error) { 37 | stmt, err := DBTunnel.Prepare(db.PatchQuery(query)) 38 | if err != nil { 39 | return nil, err 40 | } 41 | res, err1 := stmt.Exec(args...) 42 | return res, err1 43 | } 44 | 45 | func (db *MySQLConn) MustPrepareExec(query string, args ...interface{}) sql.Result { 46 | defer sentry.Recover() 47 | stmt, err := DBTunnel.Prepare(db.PatchQuery(query)) 48 | if err != nil { 49 | db.logger.LogErr(db, err.Error()) 50 | } 51 | fmt.Println(args...) 52 | res, err1 := stmt.Exec(args...) 53 | if err1 != nil { 54 | db.logger.LogErr(db, err1.Error()) 55 | } 56 | return res 57 | } 58 | 59 | func (db *MySQLConn) MustQuery(query string, args ...interface{}) *sql.Rows { 60 | rows, err := DBTunnel.Query(db.PatchQuery(query), args...) 61 | if err != nil { 62 | db.logger.LogErr(db, err.Error()) 63 | } 64 | return rows 65 | } 66 | 67 | func (db *MySQLConn) MustQueryRow(query string, args ...interface{}) *sql.Row { 68 | row := DBTunnel.QueryRow(db.PatchQuery(query), args...) 69 | if row.Err() != nil { 70 | db.logger.LogErr(db, row.Err().Error()) 71 | } 72 | return row 73 | } 74 | 75 | func (db *MySQLConn) ShouldPrepareExec(query string, args ...interface{}) sql.Result { 76 | stmt, err := DBTunnel.Prepare(db.PatchQuery(query)) 77 | if err != nil { 78 | db.logger.LogWarn(db, err.Error()) 79 | } 80 | res, err1 := stmt.Exec(args...) 81 | if err1 != nil { 82 | db.logger.LogWarn(db, err1.Error()) 83 | } 84 | return res 85 | } 86 | 87 | func (db *MySQLConn) ShouldQuery(query string, args ...interface{}) *sql.Rows { 88 | rows, err := DBTunnel.Query(db.PatchQuery(query), args...) 89 | if err != nil { 90 | db.logger.LogWarn(db, err.Error()) 91 | } 92 | return rows 93 | } 94 | 95 | func (db *MySQLConn) ShouldQueryRow(query string, args ...interface{}) *sql.Row { 96 | row := DBTunnel.QueryRow(db.PatchQuery(query), args...) 97 | if row.Err() != nil { 98 | db.logger.LogWarn(db, row.Err().Error()) 99 | } 100 | return row 101 | } 102 | 103 | func (db *MySQLConn) ShouldExec(query string, args ...interface{}) { 104 | _, err := DBTunnel.Exec(db.PatchQuery(query), args...) 105 | if err != nil { 106 | db.logger.LogErr(db, err.Error()) 107 | } 108 | } 109 | 110 | type RedisConn struct { 111 | context context.Context 112 | DB *redis.Client 113 | } 114 | 115 | func (rdb *RedisConn) ConnectBlob(config GlobalConfig) error { 116 | rdb.context = context.Background() 117 | rdb.DB = redis.NewClient(&redis.Options{ 118 | Addr: config.RedisHost + ":" + config.RedisPort, 119 | Password: config.RedisPassword, 120 | DB: config.RedisDB, 121 | }) 122 | return rdb.DB.Ping(rdb.context).Err() 123 | } 124 | -------------------------------------------------------------------------------- /src/core/Routines.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/go-co-op/gocron" 7 | consul "github.com/hashicorp/consul/api" 8 | "io" 9 | "log" 10 | "net/http" 11 | "os" 12 | "path" 13 | "strconv" 14 | ) 15 | 16 | var ucron = gocron.NewScheduler(loc) 17 | 18 | var LEADER = false 19 | var LEAD_CONFIG GlobalConfig 20 | var SessionID string 21 | var KvEngine *consul.KV 22 | 23 | func RunSingleTask(Srvid string, rdb RedisConn, log Logger, config GlobalConfig) { 24 | t, err := rdb.DB.Get(rdb.context, Srvid).Result() 25 | if err != nil { 26 | log.LogWarn(rdb, err.Error()) 27 | return 28 | } 29 | conf := ConfigBlob{} 30 | err = json.Unmarshal([]byte(t), &conf) 31 | if err != nil { 32 | log.LogWarn(rdb, err.Error()) 33 | return 34 | } 35 | db := MySQLConn{} 36 | defer func() { 37 | if r := recover(); r != nil { 38 | fmt.Println("Failed. Dequeuing...") 39 | SendMessageDiscord("[" + Srvid + "] Failed. Dequeuing...") 40 | } 41 | 42 | }() 43 | if log.Should(db.ConnectBlob(conf)) != nil { 44 | return 45 | } 46 | //Start real stuff 47 | os.MkdirAll(config.SavePath+"/"+Srvid+"/savedata", 0777) 48 | mus := CMusic{DB: &db} 49 | mus.CountDownloads() 50 | protect := CProtect{DB: &db, Savepath: config.SavePath + "/" + Srvid} 51 | protect.ResetUserLimits() 52 | protect.FillLevelModel() 53 | 54 | } 55 | 56 | func CleanModels() { 57 | dir, err := os.ReadDir(LEAD_CONFIG.SavePath) 58 | if err != nil { 59 | return 60 | } 61 | for _, p := range dir { 62 | os.RemoveAll(path.Join(LEAD_CONFIG.SavePath, p.Name())) 63 | } 64 | } 65 | 66 | func MaintainTasks() { 67 | if !LEADER { 68 | CleanModels() 69 | return 70 | } 71 | config := LEAD_CONFIG 72 | rdb := RedisConn{} 73 | log := Logger{} 74 | if err := rdb.ConnectBlob(config); err != nil { 75 | log.LogWarn(rdb, err.Error()) 76 | return 77 | } 78 | strsl, err := rdb.DB.Keys(rdb.context, "*").Result() 79 | if err != nil { 80 | log.LogWarn(rdb, err.Error()) 81 | return 82 | } 83 | SendMessageDiscord("Starting Maintenance Routine by `" + EnvOrDefault("NOMAD_SHORT_ALLOC_ID", "default") + "`") 84 | for i, SrvId := range strsl { 85 | fmt.Println("["+strconv.Itoa(i+1)+"/"+strconv.Itoa(len(strsl))+"]", SrvId) 86 | RunSingleTask(SrvId, rdb, log, config) 87 | } 88 | 89 | // Update sfx library 90 | updateSFXLibrary() 91 | 92 | } 93 | 94 | func updateSFXLibrary() { 95 | s3 := NewS3FS() 96 | d, err := http.Get("https://geometrydashfiles.b-cdn.net/sfx/sfxlibrary.dat") 97 | if err != nil { 98 | SendMessageDiscord("⚠️ Failed to fetch SFX Library: " + err.Error()) 99 | return 100 | } 101 | sfx, _ := io.ReadAll(d.Body) 102 | err = s3.PutFile("/gdps_sfx/library.dat", sfx) 103 | if err != nil { 104 | SendMessageDiscord("⚠️ Failed to save SFX Library: " + err.Error()) 105 | return 106 | } 107 | 108 | v, err := http.Get("https://geometrydashfiles.b-cdn.net/sfx/sfxlibrary_version.txt") 109 | if err != nil { 110 | SendMessageDiscord("⚠️ Failed to fetch SFX Library Version: " + err.Error()) 111 | return 112 | } 113 | ver, _ := io.ReadAll(v.Body) 114 | err = s3.PutFile("/gdps_sfx/library_version.txt", ver) 115 | if err != nil { 116 | SendMessageDiscord("⚠️ Failed to save SFX Library Version: " + err.Error()) 117 | return 118 | } 119 | } 120 | 121 | func GetConsulKV() (consulKV *consul.KV, err error) { 122 | consulConf := consul.DefaultConfig() 123 | consulConf.Address = GetEnv("CONSUL_ADDR", "127.0.0.1") 124 | consulConf.Token = GetEnv("CONSUL_TOKEN", "") 125 | consulConf.Datacenter = GetEnv("CONSUL_DC", "m41") 126 | consulCli, err := consul.NewClient(consulConf) 127 | if err != nil { 128 | log.Println("Unable to connect to Consul cluster. Assuming self-leadership: " + err.Error()) 129 | return nil, err 130 | } 131 | KvEngine = consulCli.KV() 132 | SessID, _, err := consulCli.Session().Create(&consul.SessionEntry{Name: "FiberAPI", TTL: "5m"}, nil) 133 | SessionID = SessID 134 | if err != nil { 135 | log.Println("Unable to connect to create Consul Session. Assuming self-leadership: " + err.Error()) 136 | return nil, err 137 | } 138 | ucron.Every(30).Seconds().Do(func() { 139 | consulCli.Session().Renew(SessID, nil) 140 | }) 141 | return KvEngine, nil 142 | } 143 | 144 | func PrepareElection(config GlobalConfig) { 145 | LEAD_CONFIG = config 146 | 147 | KvEngine, _ = GetConsulKV() 148 | 149 | if KvEngine == nil { 150 | log.Println("Unable to connect to create Session. Assuming self-leadership") 151 | LEADER = true 152 | } else { 153 | AquireLeadership() 154 | if !LEADER { 155 | log.Println("Couldn't acquire leadership. Dispatching 10sec watchdog") 156 | if _, err := ucron.Every(10).Seconds().Do(AquireLeadership); err != nil { 157 | log.Println(err) 158 | } 159 | } 160 | } 161 | _, err := ucron.Every(1).Day().At("00:00").Do(MaintainTasks) 162 | if err != nil { 163 | log.Println("CANNOT LAUNCH TASKS") 164 | } 165 | ucron.StartAsync() 166 | } 167 | 168 | func AquireLeadership() { 169 | kvData := &consul.KVPair{ 170 | Key: "sessions/ghostcore_lead", 171 | Value: []byte(EnvOrDefault("NOMAD_SHORT_ALLOC_ID", "default")), 172 | Session: SessionID, 173 | } 174 | isAcq, _, err := KvEngine.Acquire(kvData, nil) 175 | if err == nil && isAcq { 176 | if LEADER { 177 | log.Println("Still leader (ensuring tasks)") 178 | } else { 179 | log.Println("Lock was successfully acquired. NOW LEADER") 180 | LEADER = true 181 | } 182 | } else { 183 | if LEADER { 184 | log.Println("Couldn't acquire leadership. Stepped down by force.") 185 | LEADER = false 186 | } else { 187 | log.Println("Couldn't aquire leadership. Still follower") 188 | } 189 | } 190 | 191 | } 192 | 193 | func StepDown() { 194 | kvData := &consul.KVPair{ 195 | Key: "sessions/ghostcore_lead", 196 | Value: []byte(EnvOrDefault("NOMAD_SHORT_ALLOC_ID", "default")), 197 | Session: SessionID, 198 | } 199 | isRel, _, err := KvEngine.Release(kvData, nil) 200 | if err == nil && isRel { 201 | log.Println("Lock was successfully released. NOW FOLLOWER") 202 | LEADER = false 203 | } else { 204 | log.Println("[!!!] COULD NOT RELEASE LOCK [!!!]") 205 | } 206 | } 207 | 208 | func EnvOrDefault(key string, def string) string { 209 | if val, ok := os.LookupEnv(key); ok { 210 | return val 211 | } 212 | return def 213 | } 214 | -------------------------------------------------------------------------------- /src/core/actions.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | const ( 9 | ACTION_USER_REGISTER int = 110 10 | ACTION_USER_LOGIN int = 111 11 | ACTION_USER_DELETE int = 112 12 | ACTION_BAN_BAN int = 113 13 | ACTION_BAN_UNBAN int = 114 14 | ACTION_LEVEL_UPLOAD int = 115 15 | ACTION_LEVEL_DELETE int = 116 16 | ACTION_LEVEL_UPDATE int = 117 17 | ACTION_LEVEL_RATE int = 109 18 | ACTION_PANEL_GAUNTLET_ADD int = 118 19 | ACTION_PANEL_GAUNTLET_DELETE int = 119 20 | ACTION_PANEL_GAUNTLET_EDIT int = 120 21 | ACTION_PANEL_MAPPACK_ADD int = 121 22 | ACTION_PANEL_MAPPACK_DELETE int = 122 23 | ACTION_PANEL_MAPPACK_EDIT int = 123 24 | ACTION_PANEL_QUEST_ADD int = 124 25 | ACTION_PANEL_QUEST_DELETE int = 125 26 | ACTION_PANEL_QUEST_EDIT int = 126 27 | ACTION_LEVEL_LIKE int = 127 28 | ACTION_ACCCOMMENT_LIKE int = 128 29 | ACTION_COMMENT_LIKE int = 129 30 | ACTION_LIST_LIKE int = 130 31 | 32 | ITEMTYPE_LEVEL int = 131 33 | ITEMTYPE_ACCCOMMENT int = 132 34 | ITEMTYPE_COMMENT int = 133 35 | ITEMTYPE_LIST int = 134 36 | ) 37 | 38 | func RegisterAction(action int, uid int, target_id int, data map[string]string, db *MySQLConn) { 39 | var types int 40 | switch action { 41 | case ACTION_USER_REGISTER: 42 | types = 0 43 | data["action"] = "Register" 44 | case ACTION_USER_LOGIN: 45 | types = 1 46 | data["action"] = "Login" 47 | case ACTION_USER_DELETE: 48 | types = 2 49 | data["action"] = "Delete" 50 | case ACTION_BAN_BAN: 51 | types = 3 52 | data["action"] = "Ban" 53 | data["uname"] = (&CAccount{}).GetUnameByUID(target_id) 54 | case ACTION_BAN_UNBAN: 55 | types = 3 56 | data["action"] = "Unban" 57 | data["uname"] = (&CAccount{}).GetUnameByUID(target_id) 58 | case ACTION_LEVEL_UPLOAD: 59 | types = 4 60 | data["action"] = "Upload" 61 | case ACTION_LEVEL_DELETE: 62 | types = 4 63 | data["action"] = "Delete" 64 | case ACTION_LEVEL_UPDATE: 65 | types = 4 66 | data["action"] = "Update" 67 | case ACTION_LEVEL_RATE: 68 | types = 4 69 | data["action"] = "Rate" 70 | case ACTION_PANEL_GAUNTLET_ADD: 71 | types = 5 72 | data["action"] = "GauntletAdd" 73 | case ACTION_PANEL_GAUNTLET_DELETE: 74 | types = 5 75 | data["action"] = "GauntletDelete" 76 | case ACTION_PANEL_GAUNTLET_EDIT: 77 | types = 5 78 | data["action"] = "GauntletEdit" 79 | case ACTION_PANEL_MAPPACK_ADD: 80 | types = 5 81 | data["action"] = "MapPackAdd" 82 | case ACTION_PANEL_MAPPACK_DELETE: 83 | types = 5 84 | data["action"] = "MapPackDelete" 85 | case ACTION_PANEL_MAPPACK_EDIT: 86 | types = 5 87 | data["action"] = "MapPackEdit" 88 | case ACTION_PANEL_QUEST_ADD: 89 | types = 5 90 | data["action"] = "QuestAdd" 91 | case ACTION_PANEL_QUEST_DELETE: 92 | types = 5 93 | data["action"] = "QuestDelete" 94 | case ACTION_PANEL_QUEST_EDIT: 95 | types = 5 96 | data["action"] = "QuestEdit" 97 | case ACTION_LEVEL_LIKE: 98 | types = 6 99 | data["action"] = "LikeLevel" 100 | case ACTION_ACCCOMMENT_LIKE: 101 | types = 7 102 | data["action"] = "LikeAcccomment" 103 | case ACTION_COMMENT_LIKE: 104 | types = 8 105 | data["action"] = "LikeComment" 106 | case ACTION_LIST_LIKE: 107 | types = 9 108 | data["action"] = "LikeList" 109 | default: 110 | return 111 | } 112 | isMod := 0 113 | if uid > 0 { 114 | ret := 0 115 | db.ShouldQueryRow("SELECT role_id FROM #DB#.users WHERE uid=?", uid).Scan(&ret) 116 | if ret > 0 { 117 | isMod = 1 118 | } 119 | } 120 | datac, _ := json.Marshal(data) 121 | date := time.Now().Format("2006-01-02 15:04:05") 122 | db.ShouldExec("INSERT INTO #DB#.actions (date, uid, type, target_id, isMod, data) VALUES (?,?,?,?,?,?)", 123 | date, 124 | uid, 125 | types, 126 | target_id, 127 | isMod, 128 | string(datac), 129 | ) 130 | } 131 | 132 | func IsLiked(itemType int, uid int, dest_id int, db *MySQLConn) bool { 133 | event_id := 0 134 | switch itemType { 135 | case ITEMTYPE_LEVEL: 136 | event_id = 6 137 | case ITEMTYPE_ACCCOMMENT: 138 | event_id = 7 139 | case ITEMTYPE_COMMENT: 140 | event_id = 8 141 | case ITEMTYPE_LIST: 142 | event_id = 9 143 | default: 144 | return true 145 | } 146 | var q int 147 | db.ShouldQueryRow("SELECT count(*) as cnt FROM #DB#.actions WHERE type=? AND uid=? AND target_id=?", event_id, uid, dest_id).Scan(&q) 148 | return q > 0 149 | } 150 | -------------------------------------------------------------------------------- /src/core/connectors/JSONConnector.go: -------------------------------------------------------------------------------- 1 | package connectors 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "encoding/base64" 6 | "encoding/json" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type JSONConnector struct { 12 | output map[string]interface{} 13 | } 14 | 15 | func (c *JSONConnector) Output() string { 16 | d, _ := json.Marshal(c.output) 17 | return string(d) 18 | } 19 | 20 | func (c *JSONConnector) Error(code string, reason string) { 21 | c.output["status"] = "error" 22 | c.output["message"] = reason 23 | c.output["code"] = code 24 | } 25 | 26 | func (c *JSONConnector) Success(message string) { 27 | c.output["status"] = "success" 28 | c.output["message"] = message 29 | } 30 | 31 | func (c *JSONConnector) NumberedSuccess(id int) { 32 | c.output["code"] = id 33 | c.Success("Success indeed") 34 | } 35 | 36 | func (c *JSONConnector) Account_Sync(savedata string) { 37 | c.output["savedata"] = savedata 38 | c.Success("Savedata present") 39 | } 40 | 41 | func (c *JSONConnector) Account_Login(uid int) { 42 | c.output["uid"] = strconv.Itoa(uid) 43 | c.Success("Logged in") 44 | } 45 | 46 | func (c *JSONConnector) Comment_AccountGet(comments []core.CComment, count int, page int) { 47 | if len(comments) == 0 { 48 | c.output["comments"] = []string{} 49 | c.output["count"] = 0 50 | c.output["page"] = page 51 | } else { 52 | cms := make([]core.CComment, 0) 53 | for _, comm := range comments { 54 | if r, err := base64.StdEncoding.DecodeString(comm.Comment); err == nil { 55 | comm.Comment = string(r) 56 | } 57 | cms = append(cms, comm) 58 | } 59 | 60 | c.output["comments"] = cms 61 | c.output["count"] = count 62 | c.output["page"] = page 63 | } 64 | c.Success("Comments retrieved") 65 | } 66 | 67 | func (c *JSONConnector) Comment_LevelGet(comments []core.CComment, count int, page int) { 68 | c.Comment_AccountGet(comments, count, page) 69 | } 70 | 71 | func (c *JSONConnector) Comment_HistoryGet(comments []core.CComment, acc core.CAccount, role core.Role, count int, page int) { 72 | c.Comment_AccountGet(comments, count, page) 73 | c.output["user"] = struct { 74 | ModBadge int `json:"mod_badge"` 75 | CommentColor string `json:"comment_color"` 76 | Uname string `json:"uname"` 77 | IconId int `json:"icon_id"` 78 | IconType int `json:"icon_type"` 79 | ColorPrimary int `json:"color_primary"` 80 | ColorSecondary int `json:"color_secondary"` 81 | Special int `json:"special"` 82 | }{ 83 | role.ModLevel, 84 | role.CommentColor, 85 | acc.Uname, 86 | acc.GetShownIcon(), 87 | acc.IconType, 88 | acc.ColorPrimary, 89 | acc.ColorSecondary, 90 | acc.Special, 91 | } 92 | } 93 | 94 | func (c *JSONConnector) Communication_FriendGetRequests(reqs []map[string]string, count int, page int) { 95 | c.output["requests"] = reqs 96 | c.output["count"] = count 97 | c.output["page"] = page 98 | c.Success("Friend requests retrieved") 99 | } 100 | 101 | func (c *JSONConnector) Communication_MessageGet(msg core.CMessage, uid int) { 102 | if content, err := base64.StdEncoding.DecodeString(msg.Message); err == nil { 103 | msg.Message = string(content) 104 | } 105 | uidx := msg.UidDest 106 | if uid == msg.UidDest { 107 | uidx = msg.UidSrc 108 | } 109 | xacc := core.CAccount{DB: msg.DB, Uid: uidx} 110 | xacc.LoadAuth(core.CAUTH_UID) 111 | c.output["content"] = struct { 112 | core.CMessage 113 | Uname string `json:"uname"` 114 | }{ 115 | msg, 116 | xacc.Uname, 117 | } 118 | c.Success("Message retrieved") 119 | } 120 | 121 | func (c *JSONConnector) Communication_MessageGetAll(messages []map[string]string, getSent bool, count int, page int) { 122 | c.output["messages"] = messages 123 | c.output["count"] = count 124 | c.output["page"] = page 125 | c.output["sent"] = getSent 126 | c.Success("Messages retrieved") 127 | } 128 | 129 | func (c *JSONConnector) Essential_GetMusic(mus core.CMusic) { 130 | c.output["music"] = mus 131 | c.Success("Music retrieved") 132 | } 133 | 134 | func (c *JSONConnector) Essential_GetTopArtists(artists map[string]string) { 135 | c.output["artists"] = artists 136 | c.Success("Top artists retrieved") 137 | } 138 | 139 | func (c *JSONConnector) Level_GetGauntlets(gaus []map[string]string, hash string) { 140 | type r struct { 141 | PackName string `json:"pack_name"` 142 | Levels []string `json:"levels"` 143 | } 144 | var gaunts []r 145 | for _, gau := range gaus { 146 | gaunts = append(gaunts, r{ 147 | PackName: gau["pack_name"], 148 | Levels: strings.Split(gau["levels"], ","), 149 | }) 150 | } 151 | c.output["gauntlets"] = gaunts 152 | c.output["hash"] = hash 153 | c.Success("Gauntlets retrieved") 154 | } 155 | 156 | func (c *JSONConnector) Level_SearchList(intlists []int, lists []core.CLevelList, count int, page int) { 157 | var llists []*core.CLevelList 158 | for _, lid := range intlists { 159 | for i, list := range lists { 160 | if list.ID == lid { 161 | list.DecoupledLevels = strings.Split(list.Levels, ",") 162 | llists = append(llists, &list) 163 | lists = append(lists[:i], lists[i+1:]...) 164 | break 165 | } 166 | } 167 | } 168 | c.output["lists"] = llists 169 | c.output["count"] = count 170 | c.output["page"] = page 171 | c.Success("Level list retrieved") 172 | } 173 | 174 | func (c *JSONConnector) Level_GetMapPacks(packs []core.LevelPack, count int, page int) { 175 | c.output["packs"] = packs 176 | c.output["count"] = count 177 | c.output["page"] = page 178 | c.Success("Map packs retrieved") 179 | } 180 | 181 | func (c *JSONConnector) Level_GetLevelFull(lvl core.CLevel, passwd string, phash string, quest_id int) { 182 | if txt, err := base64.StdEncoding.DecodeString(lvl.Description); err == nil { 183 | lvl.Description = string(txt) 184 | } 185 | c.output["level"] = lvl 186 | c.output["quest_id"] = quest_id 187 | c.Success("Level retrieved") 188 | } 189 | 190 | func (c *JSONConnector) Level_GetSpecials(id int, left int) { 191 | c.output["id"] = id 192 | c.output["seconds_left"] = left 193 | c.Success("Specials retrieved") 194 | } 195 | 196 | func (c *JSONConnector) Level_SearchLevels( 197 | intlevels []int, levels []core.CLevel, mus *core.CMusic, 198 | count int, page int, gdVersion int, gauntlet int, 199 | ) { 200 | var musQueue []int 201 | musMap := make(map[int]core.CMusic) 202 | 203 | // To keep in order 204 | var lvls []*core.CLevel 205 | for _, lvlid := range intlevels { 206 | for i, lvl := range levels { 207 | if lvl.Id == lvlid { 208 | if lvl.SongId != 0 { 209 | musQueue = append(musQueue, lvl.SongId) 210 | } 211 | if ns, err := base64.StdEncoding.DecodeString(lvl.Description); err == nil { 212 | lvl.Description = string(ns) 213 | } 214 | lvls = append(lvls, &lvl) 215 | levels = append(levels[:i], levels[i+1:]...) 216 | break 217 | } 218 | } 219 | } 220 | 221 | if len(musQueue) > 0 { 222 | songs := mus.GetBulkSongs(musQueue) 223 | for _, sng := range songs { 224 | musMap[sng.Id] = sng 225 | } 226 | } 227 | 228 | c.output["levels"] = lvls 229 | c.output["music"] = musMap 230 | c.Success("Levels search completed") 231 | } 232 | 233 | func (c *JSONConnector) Rewards_ChallengesOutput(cq core.CQuests, uid int, chk string, udid string) { 234 | q := cq.GetQuests(uid) 235 | c.output["challenges"] = q 236 | c.Success("Challenges retrieved") 237 | } 238 | 239 | func (c *JSONConnector) Rewards_ChestOutput(acc core.CAccount, config core.ConfigBlob, udid string, chk string, smallLeft int, bigLeft int, chestType int) { 240 | c.output["readme"] = "You DO realise chests are imaginary and all the loot is managed by the game client itself? " + 241 | "I mean the server is just posing some time restrictions and spitting random numbers to make loot seem random. " + 242 | "Also querying this endpoint usually means that chests timer will be reset, so you won't be able to automate looting " + 243 | "or just \"peek\" inside chests." 244 | c.Success("Did absolutely nothing") 245 | } 246 | 247 | func (c *JSONConnector) Profile_GetUserProfile(acc core.CAccount, selfUid int) { 248 | role := acc.GetRoleObj(acc.Uid == selfUid) 249 | obj := core.NewCAccountJSONFromAccount(acc, &role, acc.Uid == selfUid) 250 | cf := core.CFriendship{DB: acc.DB} 251 | obj.WeAreFriends = cf.IsAlreadyFriend(acc.Uid, selfUid) 252 | c.output["user"] = obj 253 | c.Success("User retrieved successfully") 254 | } 255 | 256 | func (c *JSONConnector) Profile_ListUserProfiles(accs []core.CAccount) { 257 | var users []core.CAccountJSON 258 | 259 | for _, acc := range accs { 260 | users = append(users, core.NewCAccountJSONFromAccountLite(acc)) 261 | } 262 | 263 | c.output["users"] = users 264 | c.Success("User list retrieved successfully") 265 | } 266 | 267 | func (c *JSONConnector) Profile_GetSearchableUsers(accs []core.CAccount, count int, page int) { 268 | var users []core.CAccountJSON 269 | for _, acc := range accs { 270 | acc.LoadAuth(core.CAUTH_UID) 271 | acc.LoadVessels() 272 | acc.LoadStats() 273 | users = append(users, core.NewCAccountJSONFromAccountLite(acc)) 274 | } 275 | 276 | c.output["users"] = users 277 | c.output["count"] = count 278 | c.output["page"] = page 279 | c.Success("Users retrieved successfully") 280 | } 281 | 282 | func (c *JSONConnector) Score_GetLeaderboard(intaccs []int, xacc core.CAccount) { 283 | var accs []core.CAccountJSON 284 | 285 | lpos := 0 286 | for _, uid := range intaccs { 287 | lpos++ 288 | acc := core.CAccount{DB: xacc.DB, Uid: uid} 289 | acc.LoadAll() 290 | ja := core.NewCAccountJSONFromAccountLite(acc) 291 | ja.LeaderboardRank = lpos 292 | accs = append(accs, ja) 293 | } 294 | c.output["leaderboard"] = accs 295 | c.Success("Leaderboard retrieved") 296 | } 297 | 298 | func (c *JSONConnector) Score_GetScores(scores []core.CScores, mode string) { 299 | type CoupledScore struct { 300 | core.CScores 301 | User core.CAccountJSON `json:"user"` 302 | } 303 | 304 | var coupledScores []CoupledScore 305 | for _, score := range scores { 306 | switch mode { 307 | case "coins": 308 | score.Percent = score.Coins 309 | case "attempts": 310 | score.Percent = score.Attempts 311 | } 312 | acc := core.CAccount{DB: score.DB, Uid: score.Uid} 313 | acc.LoadAll() 314 | coupledScores = append(coupledScores, CoupledScore{ 315 | CScores: score, 316 | User: core.NewCAccountJSONFromAccountLite(acc), 317 | }) 318 | } 319 | c.output["scores"] = coupledScores 320 | c.Success("Scores retrieved") 321 | } 322 | -------------------------------------------------------------------------------- /src/core/connectors/connector.go: -------------------------------------------------------------------------------- 1 | package connectors 2 | 3 | import ( 4 | "HalogenGhostCore/core" 5 | "time" 6 | ) 7 | 8 | var loc, _ = time.LoadLocation("Europe/Moscow") 9 | 10 | type Connector interface { 11 | Output() string 12 | 13 | Error(code string, reason string) 14 | Success(message string) 15 | NumberedSuccess(id int) 16 | Account_Sync(savedata string) 17 | Account_Login(uid int) 18 | Comment_AccountGet(comments []core.CComment, count int, page int) 19 | Comment_LevelGet(comments []core.CComment, count int, page int) 20 | Comment_HistoryGet(comments []core.CComment, acc core.CAccount, role core.Role, count int, page int) 21 | Communication_FriendGetRequests(reqs []map[string]string, count int, page int) 22 | Communication_MessageGet(message core.CMessage, uid int) 23 | Communication_MessageGetAll(messages []map[string]string, getSent bool, count int, page int) 24 | Essential_GetMusic(core.CMusic) 25 | Essential_GetTopArtists(artists map[string]string) 26 | Level_GetGauntlets(gaus []map[string]string, hash string) 27 | Level_SearchList(intlists []int, lists []core.CLevelList, count int, page int) 28 | Level_GetMapPacks(packs []core.LevelPack, count int, page int) 29 | Level_GetLevelFull(lvl core.CLevel, passwd string, phash string, quest_id int) 30 | Level_GetSpecials(id int, timeLeft int) 31 | Level_SearchLevels(intlevels []int, levels []core.CLevel, mus *core.CMusic, count int, page int, gdVersion int, gauntlet int) 32 | Rewards_ChallengesOutput(cq core.CQuests, uid int, chk string, udid string) 33 | Rewards_ChestOutput(acc core.CAccount, config core.ConfigBlob, udid string, chk string, smallLeft int, bigLeft int, chestType int) 34 | Profile_GetUserProfile(acc core.CAccount, selfUid int) 35 | Profile_ListUserProfiles(accs []core.CAccount) 36 | Profile_GetSearchableUsers(accs []core.CAccount, count int, page int) 37 | Score_GetLeaderboard(intaccs []int, xacc core.CAccount) 38 | Score_GetScores(scores []core.CScores, mode string) 39 | } 40 | 41 | func NewConnector(isJson bool) Connector { 42 | if isJson { 43 | return &JSONConnector{output: make(map[string]interface{})} 44 | } else { 45 | return &GDConnector{} 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/core/connectors/output.go: -------------------------------------------------------------------------------- 1 | package connectors 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | // OutputConnector used to write anything to anywhere (stdio/http/files) 11 | type OutputConnector interface { 12 | Write() 13 | } 14 | 15 | type HttpWriter struct { 16 | Endpoint string 17 | } 18 | 19 | func (hw HttpWriter) Write(dat []byte ) (int, error) { 20 | resp, err:= http.Post(hw.Endpoint,"text/plain",bytes.NewBuffer(dat)) 21 | return resp.StatusCode, err 22 | } 23 | 24 | type FileWriter struct { 25 | Endpoint string 26 | } 27 | func (fw FileWriter) Write(dat []byte) (int, error) { 28 | f,_:=os.OpenFile(fw.Endpoint,os.O_APPEND,644) 29 | defer f.Close() 30 | return f.Write(dat) 31 | } 32 | 33 | func GetWriter(t string, endpoint string) io.Writer { 34 | switch t { 35 | case "http": 36 | return HttpWriter{endpoint} 37 | case "file": 38 | return FileWriter{endpoint} 39 | default: 40 | return os.Stderr 41 | } 42 | } -------------------------------------------------------------------------------- /src/core/cryptography.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "crypto/sha256" 8 | "encoding/base64" 9 | "io" 10 | ) 11 | 12 | type ThunderAES struct { 13 | CryptKey []byte 14 | Blk cipher.Block 15 | GCM cipher.AEAD 16 | } 17 | 18 | func (taes *ThunderAES) GenKey(s string) error { 19 | sha := sha256.New() 20 | _, err := sha.Write([]byte(s)) 21 | taes.CryptKey = sha.Sum(nil)[:32] 22 | return err 23 | } 24 | 25 | func (taes *ThunderAES) Init() error { 26 | var err error 27 | taes.Blk, err = aes.NewCipher(taes.CryptKey) 28 | if err != nil { 29 | return err 30 | } 31 | taes.GCM, err = cipher.NewGCM(taes.Blk) 32 | return err 33 | } 34 | 35 | func (taes *ThunderAES) Encrypt(text string) (string, error) { 36 | nonce := make([]byte, taes.GCM.NonceSize()) 37 | _, err := io.ReadFull(rand.Reader, nonce) 38 | raw := taes.GCM.Seal(nonce, nonce, []byte(text), nil) 39 | return base64.StdEncoding.EncodeToString(raw), err 40 | } 41 | 42 | func (taes *ThunderAES) Decrypt(blk string) (string, error) { 43 | block, err := base64.StdEncoding.DecodeString(blk) 44 | if err != nil { 45 | return "", err 46 | } 47 | nonce, raw := block[:taes.GCM.NonceSize()], block[taes.GCM.NonceSize():] 48 | plain, err := taes.GCM.Open(nil, nonce, raw, nil) 49 | return string(plain), err 50 | } 51 | 52 | func (taes *ThunderAES) DecryptLegacy(blk string) (string, error) { 53 | block, err := base64.StdEncoding.DecodeString(blk) 54 | if err != nil { 55 | return "", err 56 | } 57 | nSize := taes.GCM.NonceSize() 58 | nonce := make([]byte, nSize) 59 | tag := make([]byte, 16) 60 | copy(nonce, block[:nSize]) 61 | copy(tag, block[nSize:nSize+16]) 62 | raw := append(block[nSize+16:], tag...) 63 | plain, err := taes.GCM.Open(nil, nonce, raw, nil) 64 | return string(plain), err 65 | } 66 | 67 | func (taes *ThunderAES) EncryptRaw(text string) ([]byte, error) { 68 | nonce := make([]byte, taes.GCM.NonceSize()) 69 | _, err := io.ReadFull(rand.Reader, nonce) 70 | raw := taes.GCM.Seal(nonce, nonce, []byte(text), nil) 71 | return raw, err 72 | } 73 | 74 | func (taes *ThunderAES) DecryptRaw(block []byte) (string, error) { 75 | nonce, raw := block[:taes.GCM.NonceSize()], block[taes.GCM.NonceSize():] 76 | plain, err := taes.GCM.Open(nil, nonce, raw, nil) 77 | return string(plain), err 78 | } 79 | -------------------------------------------------------------------------------- /src/core/modules/PluginCore.go: -------------------------------------------------------------------------------- 1 | // Package modules is an ultimate plugin core with plugin autoload 2 | package modules 3 | 4 | import ( 5 | "HalogenGhostCore/core" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | type Plugin interface { 11 | PreInit(*PluginCore, ...interface{}) 12 | Unload(...interface{}) 13 | } 14 | 15 | type PluginCore struct { 16 | HalPlugins map[string]Plugin 17 | } 18 | 19 | func (pch *PluginCore) LoadPrepared(conf core.ConfigBlob) { 20 | //for mod, en := range conf.ServerConfig.EnableModules { 21 | // pch.Load(mod, en) 22 | //} 23 | } 24 | 25 | func (pch *PluginCore) Load(name string, plugin Plugin) { 26 | pch.HalPlugins[name] = plugin 27 | } 28 | 29 | func (pch *PluginCore) CallPlugin(endpoint string, args ...interface{}) []reflect.Value { 30 | _endpoint := strings.Split(endpoint, "::") // PluginName::Method 31 | if plug, ok := pch.HalPlugins[_endpoint[0]]; ok { 32 | if _, ok := reflect.TypeOf(plug).MethodByName(_endpoint[1]); ok { 33 | //if plugin exists and has a method then convert all data to reflect.Value, call method and return its output 34 | inputs := make([]reflect.Value, 0, len(args)+1) 35 | for i := range args { 36 | inputs = append(inputs, reflect.ValueOf(args[i])) 37 | } 38 | return reflect.ValueOf(plug).MethodByName(_endpoint[1]).Call(inputs) 39 | } 40 | } 41 | return []reflect.Value{} 42 | } 43 | 44 | //===ESSENTIAL=== 45 | 46 | // PreInit Invoked to load anything 47 | func (pch *PluginCore) PreInit(args ...interface{}) { 48 | for plug := range pch.HalPlugins { 49 | pch.CallPlugin(plug+"::PreInit", pch, args) 50 | } 51 | } 52 | 53 | // Unload Unloads everything 54 | func (pch *PluginCore) Unload(args ...interface{}) { 55 | for plug := range pch.HalPlugins { 56 | pch.CallPlugin(plug+"::Unload", args) 57 | } 58 | } 59 | 60 | //===PLAYER=== 61 | 62 | // OnPlayerNew Invoked when player is registered, but not yet activated account 63 | func (pch *PluginCore) OnPlayerNew(uid int, uname string, email string) { 64 | for plug := range pch.HalPlugins { 65 | pch.CallPlugin(plug+"::OnPlayerNew", uid, uname, email) 66 | } 67 | } 68 | 69 | // OnPlayerActivate Invoked when player first activated account 70 | func (pch *PluginCore) OnPlayerActivate(uid int, uname string) { 71 | for plug := range pch.HalPlugins { 72 | pch.CallPlugin(plug+"::OnPlayerActivate", uid, uname) 73 | } 74 | } 75 | 76 | // OnPlayerLogin invoked when player commits login (regular, not gjp) 77 | func (pch *PluginCore) OnPlayerLogin(uid int, uname string) { 78 | for plug := range pch.HalPlugins { 79 | pch.CallPlugin(plug+"::OnPlayerLogin", uid, uname) 80 | } 81 | } 82 | 83 | // OnPlayerBackup invoked when player uploads their backup 84 | func (pch *PluginCore) OnPlayerBackup(uid int, decryptedBackup string) { 85 | for plug := range pch.HalPlugins { 86 | pch.CallPlugin(plug+"::OnPlayerBackup", uid, decryptedBackup) 87 | } 88 | } 89 | 90 | // OnPlayerScoreUpdate invoked when player updates their score 91 | func (pch *PluginCore) OnPlayerScoreUpdate(uid int, uname string, stats map[string]int) { 92 | for plug := range pch.HalPlugins { 93 | pch.CallPlugin(plug+"::OnPlayerScoreUpdate", uid, uname, stats) 94 | } 95 | } 96 | 97 | //===LEVEL=== 98 | 99 | // OnLevelUpload invoked when level was uploaded 100 | func (pch *PluginCore) OnLevelUpload(id int, name string, builder string, desc string) { 101 | for plug := range pch.HalPlugins { 102 | pch.CallPlugin(plug+"::OnLevelUpload", id, name, builder, desc) 103 | } 104 | } 105 | 106 | // OnLevelUpdate invoked when level was updated 107 | func (pch *PluginCore) OnLevelUpdate(id int, name string, builder string, desc string) { 108 | for plug := range pch.HalPlugins { 109 | pch.CallPlugin(plug+"::OnLevelUpdate", id, name, builder, desc) 110 | } 111 | } 112 | 113 | // OnLevelDelete invoked when level was deleted 114 | func (pch *PluginCore) OnLevelDelete(id int, name string, builder string) { 115 | for plug := range pch.HalPlugins { 116 | pch.CallPlugin(plug+"::OnLevelDelete", id, name, builder) 117 | } 118 | } 119 | 120 | // OnLevelRate invoked when level was rated/rerated 121 | func (pch *PluginCore) OnLevelRate(id int, name string, builder string, stars int, likes int, downloads int, length int, demonDiff int, isEpic bool, isFeatured bool, ratedBy map[string]string) { 122 | for plug := range pch.HalPlugins { 123 | pch.CallPlugin(plug+"::OnLevelRate", id, name, builder, stars, likes, downloads, length, demonDiff, isEpic, isFeatured, ratedBy) 124 | } 125 | } 126 | 127 | // OnLevelReport invoked when level was reported 128 | func (pch *PluginCore) OnLevelReport(id int, name string, builder string, player string) { 129 | for plug := range pch.HalPlugins { 130 | pch.CallPlugin(plug+"::OnLevelReport", id, name, builder, player) 131 | } 132 | } 133 | 134 | // OnLevelScore invoked when player published their score in level scoreboard 135 | func (pch *PluginCore) OnLevelScore(id int, name string, player string, percent int, coins int) { 136 | for plug := range pch.HalPlugins { 137 | pch.CallPlugin(plug+"::OnLevelScore", id, name, player, percent, coins) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/core/modules/PluginCore_test.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestPluginCore(t *testing.T){ 9 | p:=PluginCore{HalPlugins: make(map[string]Plugin)} 10 | d:=Dummy{} 11 | p.Load("dummy", &d) 12 | p.PreInit() 13 | p.CallPlugin("dummy::Test","Wawa",17,time.Now()) 14 | } 15 | -------------------------------------------------------------------------------- /src/core/modules/modDummy.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type Dummy struct { 9 | pch *PluginCore 10 | } 11 | 12 | func (mod *Dummy) PreInit(pch *PluginCore, args ...interface{}){ 13 | mod.pch=pch 14 | mod.pch.CallPlugin("dummy::Test","mako",190,time.Now()) 15 | } 16 | 17 | func (mod *Dummy) Unload(...interface{}){} 18 | 19 | func (mod *Dummy) Test(s string, i int, t time.Time) { 20 | fmt.Println("Got",s,"and",i,": ",t.Unix()) 21 | } -------------------------------------------------------------------------------- /src/core/sfxgen.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "encoding/base64" 7 | "fmt" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func GenerateMusicLibraryFile(db *MySQLConn, fs *S3FS, srvid string) string { 14 | clean := func(s string) string { 15 | s = strings.ReplaceAll(s, ",", " ") 16 | s = strings.ReplaceAll(s, ";", " ") 17 | s = strings.ReplaceAll(s, "|", " ") 18 | s = strings.ReplaceAll(s, "#", " ") 19 | return s 20 | } 21 | 22 | t := time.Now() 23 | ver := fmt.Sprintf("%s%d", strconv.Itoa(t.Year())[2:], t.YearDay()) 24 | 25 | tags := []string{ 26 | "1,NewGrounds", 27 | "2,YouTube", 28 | "3,Deezer", 29 | "4,VK", 30 | "5,Dropbox", 31 | } 32 | 33 | var artists []string 34 | artistsLookup := make(map[string]int) 35 | rows := db.MustQuery("SELECT DISTINCT artist FROM #DB#.songs") 36 | defer rows.Close() 37 | for rows.Next() { 38 | var artist string 39 | rows.Scan(&artist) 40 | artists = append(artists, fmt.Sprintf("%d,%s, , ", len(artists)+1, clean(artist))) 41 | artistsLookup[artist] = len(artists) 42 | } 43 | 44 | var tracks []string 45 | 46 | songs := db.MustQuery("SELECT id,name,artist,size,url,isBanned,downloads FROM #DB#.songs") 47 | defer songs.Close() 48 | for songs.Next() { 49 | var song CMusic 50 | songs.Scan(&song.Id, &song.Name, &song.Artist, &song.Size, &song.Url, &song.IsBanned, &song.Downloads) 51 | if song.IsBanned { 52 | continue 53 | } 54 | songstr := fmt.Sprintf("%d,%s,%d,%.0f,69,.%s", song.Id, clean(song.Name), artistsLookup[song.Artist], song.Size*1024*1024, getTagByARN(song.Url)) 55 | tracks = append(tracks, songstr) 56 | } 57 | 58 | preparedblock := strings.Join([]string{ 59 | ver, 60 | strings.Join(artists, ";"), 61 | strings.Join(tracks, ";"), 62 | strings.Join(tags, ";"), 63 | }, "|") 64 | 65 | // zlib compress preparedblock 66 | var compressedBlock bytes.Buffer 67 | w := zlib.NewWriter(&compressedBlock) 68 | w.Write([]byte(preparedblock)) 69 | w.Close() 70 | preparedblock = base64.StdEncoding.EncodeToString(compressedBlock.Bytes()) 71 | preparedblock = strings.ReplaceAll(preparedblock, "/", "_") 72 | preparedblock = strings.ReplaceAll(preparedblock, "+", "-") 73 | 74 | fs.PutFile(fmt.Sprintf("/gdps_sfx/%s_library.dat", srvid), []byte(preparedblock)) 75 | 76 | return fmt.Sprintf("/gdps_sfx/%s_library.dat", srvid) 77 | } 78 | 79 | func getTagByARN(arn string) string { 80 | if strings.HasPrefix(arn, "hal:") { 81 | arnType := strings.Split(arn, ":")[1] 82 | switch arnType { 83 | case "ng": 84 | return "1" 85 | case "yt": 86 | return "2" 87 | case "dz": 88 | return "3" 89 | case "vk": 90 | return "4" 91 | default: 92 | return "5" 93 | } 94 | } 95 | return "5" 96 | } 97 | -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module HalogenGhostCore 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.1 6 | 7 | require ( 8 | github.com/aws/aws-sdk-go v1.55.5 9 | github.com/getsentry/sentry-go v0.29.1 10 | github.com/go-co-op/gocron v1.37.0 11 | github.com/go-redis/redis/v8 v8.11.5 12 | github.com/go-sql-driver/mysql v1.8.1 13 | github.com/gorilla/mux v1.8.1 14 | github.com/hashicorp/consul/api v1.30.0 15 | github.com/jmoiron/sqlx v1.4.0 16 | github.com/lesismal/arpc v1.2.16 17 | ) 18 | 19 | require ( 20 | filippo.io/edwards25519 v1.1.0 // indirect 21 | github.com/armon/go-metrics v0.4.1 // indirect 22 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 23 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 24 | github.com/fatih/color v1.18.0 // indirect 25 | github.com/google/uuid v1.6.0 // indirect 26 | github.com/hashicorp/errwrap v1.1.0 // indirect 27 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 28 | github.com/hashicorp/go-hclog v1.6.3 // indirect 29 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 30 | github.com/hashicorp/go-multierror v1.1.1 // indirect 31 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 32 | github.com/hashicorp/golang-lru v1.0.2 // indirect 33 | github.com/hashicorp/serf v0.10.1 // indirect 34 | github.com/jmespath/go-jmespath v0.4.0 // indirect 35 | github.com/mattn/go-colorable v0.1.13 // indirect 36 | github.com/mattn/go-isatty v0.0.20 // indirect 37 | github.com/mitchellh/go-homedir v1.1.0 // indirect 38 | github.com/mitchellh/mapstructure v1.5.0 // indirect 39 | github.com/robfig/cron/v3 v3.0.1 // indirect 40 | go.uber.org/atomic v1.11.0 // indirect 41 | golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect 42 | golang.org/x/sys v0.27.0 // indirect 43 | golang.org/x/text v0.20.0 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "HalogenGhostCore/api" 5 | "HalogenGhostCore/core" 6 | "HalogenGhostCore/core/connectors" 7 | "github.com/getsentry/sentry-go" 8 | "github.com/jmoiron/sqlx" 9 | "log" 10 | "os" 11 | "time" 12 | ) 13 | 14 | var restartAttempts = 3 15 | 16 | func main() { 17 | // Start Sentry so I can sleep well 18 | err := sentry.Init(sentry.ClientOptions{ 19 | Dsn: "https://ef8c6a708a684aa78fdfc0be5a85115b@o1404863.ingest.sentry.io/4504374313222144", 20 | TracesSampleRate: 1.0, 21 | }) 22 | if err != nil { 23 | log.Fatalf("sentry.Init: %s", err) 24 | } 25 | defer sentry.Flush(2 * time.Second) 26 | 27 | config := GenGConfig() 28 | 29 | time.Local, err = time.LoadLocation("Europe/Moscow") 30 | if err != nil { 31 | log.Println(err) 32 | } 33 | 34 | DB_USER := EnvOrDefault("DB_USER", "") 35 | DB_PASS := EnvOrDefault("DB_PASS", "") 36 | DB_HOST := EnvOrDefault("DB_HOST", "") 37 | 38 | core.DBTunnel, err = sqlx.Connect("mysql", DB_USER+":"+DB_PASS+"@tcp("+DB_HOST+":3306)/") 39 | if err != nil { 40 | if restartAttempts == 0 { 41 | os.Exit(1) 42 | } 43 | restartAttempts-- 44 | log.Println("Error while connecting to " + DB_USER + "@localhost: " + err.Error()) 45 | time.Sleep(10 * time.Second) 46 | main() 47 | } 48 | core.DBTunnel.SetMaxOpenConns(100) 49 | ghostServer := api.GhostServer{ 50 | Log: core.Logger{ 51 | Output: connectors.GetWriter("", ""), 52 | }, 53 | Config: config, 54 | } 55 | api.InitCache(config, 16) 56 | core.PrepareElection(config) 57 | defer core.StepDown() 58 | 59 | ghostServer.StartServer("0.0.0.0:1997") 60 | } 61 | 62 | func GenGConfig() core.GlobalConfig { 63 | return core.GlobalConfig{ 64 | MasterKey: EnvOrDefault("MASTER_KEY", "3XTR4OrD1nArY_K3Y_1907"), 65 | ApiEndpoint: EnvOrDefault("API_ENDPOINT", "http://127.0.0.1:6000/sched/gd/api"), 66 | LogConnector: "stdout", 67 | LogEndpoint: "null", 68 | RedisHost: EnvOrDefault("REDIS_HOST", "localhost"), 69 | RedisPort: EnvOrDefault("REDIS_PORT", "6379"), 70 | RedisPassword: EnvOrDefault("REDIS_PASSWORD", ""), 71 | RedisDB: 7, 72 | SavePath: EnvOrDefault("SAVE_PATH", "./"), 73 | ModuleSettings: map[string]string{}, 74 | } 75 | } 76 | 77 | func EnvOrDefault(key string, def string) string { 78 | if val, ok := os.LookupEnv(key); ok { 79 | return val 80 | } 81 | return def 82 | } 83 | -------------------------------------------------------------------------------- /src/rpc/server.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import "github.com/lesismal/arpc" 4 | 5 | func StartRPC(addr string) { 6 | srv := arpc.NewServer() 7 | srv.Handler.Handle("/", func(ctx *arpc.Context) {}) 8 | 9 | srv.Run(addr) 10 | } 11 | --------------------------------------------------------------------------------