Compare commits

..

164 Commits
0.2.3 ... 0.4.3

Author SHA1 Message Date
Alireza Ahmadi
8f0a701d9e v0.4.3 2023-03-26 13:38:32 +02:00
Alireza Ahmadi
921a72b5cf v0.4.3 2023-03-24 12:52:00 +01:00
Alireza Ahmadi
9e902fc82f Add version and log 2023-03-24 12:24:12 +01:00
Alireza Ahmadi
6ed8ff8e05 [infoModal] better display 2023-03-24 12:23:23 +01:00
Alireza Ahmadi
f333ad23d9 Merge pull request #92 from alireza0/dependabot/go_modules/google.golang.org/grpc-1.54.0
Bump google.golang.org/grpc from 1.53.0 to 1.54.0
2023-03-24 12:06:58 +01:00
Alireza Ahmadi
3f49aa5541 Merge pull request #89 from alireza0/TLS-enhancements
TLS-enhancements
2023-03-24 12:06:33 +01:00
Alireza Ahmadi
0f487dc10f [tgbot] fix admins input 2023-03-24 08:43:09 +01:00
dependabot[bot]
f3c0edac07 Bump google.golang.org/grpc from 1.53.0 to 1.54.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.53.0 to 1.54.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.53.0...v1.54.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-23 22:07:30 +00:00
Alireza Ahmadi
394c857bb6 [tgbot] fix exhausted report 2023-03-23 22:55:23 +01:00
Alireza Ahmadi
6080709fc7 Enable fallback in xtls as well 2023-03-23 20:13:21 +01:00
Alireza Ahmadi
17c47f0711 Remove ugly-bugy qrCode footer 2023-03-23 18:29:12 +01:00
Alireza Ahmadi
dac234357a Fix input-group dark theme 2023-03-23 12:53:36 +01:00
Alireza Ahmadi
2d15073a90 Fix vmess link 2023-03-23 11:16:48 +01:00
Alireza Ahmadi
1b2e165a65 Fix TLS and ALPN 2023-03-23 10:03:09 +01:00
Alireza Ahmadi
e1f8e8efd2 Add allowInsecure option 2023-03-23 10:02:18 +01:00
Alireza Ahmadi
738c7064a9 Default TLS version change to 1.0-1.2 2023-03-23 10:01:28 +01:00
Alireza Ahmadi
c24483ad44 [tgbot] fix client search #85 2023-03-23 09:59:34 +01:00
Alireza Ahmadi
046f071378 [bug] Fix Transport column in search table 2023-03-21 15:03:26 +01:00
Alireza Ahmadi
d40a16e29d Update README 2023-03-20 13:50:39 +01:00
Alireza Ahmadi
2f1f28cc0b v0.4.2 2023-03-20 12:03:52 +01:00
Alireza Ahmadi
70ff51d629 [Bulk client] add option: without random email #72 2023-03-20 11:50:44 +01:00
Alireza Ahmadi
7472c31c34 [TGBOT] add seach inbound #75 2023-03-20 11:18:47 +01:00
Alireza Ahmadi
04a8d5c177 [DarkMode] better dark theme based on antdv2 2023-03-20 10:50:31 +01:00
Alireza Ahmadi
7b2bd4247d TGBOT: Add xray config to backup #75 2023-03-20 10:22:41 +01:00
Alireza Ahmadi
5aee1c886b Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-19 20:19:04 +01:00
Alireza Ahmadi
7a9a30a038 Dark-Mode: Better contrast 2023-03-19 19:59:35 +01:00
Alireza Ahmadi
eb17cec289 Remove chat answer 2023-03-19 19:07:40 +01:00
Alireza Ahmadi
22e107c746 Update install.sh
Fix centos7+ message
2023-03-19 11:56:42 +01:00
Alireza Ahmadi
36a93e2959 Merge pull request #77 from forkeer/main
Fedora OS Support
2023-03-19 11:56:13 +01:00
Alireza Ahmadi
073247f054 Fix Link bug 2023-03-19 11:51:08 +01:00
Leonardo
96c8932961 Merge branch 'alireza0:main' into main 2023-03-19 00:14:23 +03:30
Alireza Ahmadi
3d4b87d72f v0.4.1 2023-03-18 17:26:39 +01:00
Leonardo
5fac7df174 Merge branch 'alireza0:main' into main 2023-03-18 13:45:41 +03:30
Leonardo
8bba9f3c25 Update install.sh
Fix s390x
2023-03-18 13:43:54 +03:30
Leonardo
268e8c7eb1 Update x-ui.sh
Fix Debian 8 Support
2023-03-18 13:39:54 +03:30
Leonardo
14e8f6cbfa Update x-ui.sh
Fix OS Version
2023-03-18 13:38:37 +03:30
Leonardo
7f0ba495db Update install.sh
Fix OS Version
2023-03-18 13:36:01 +03:30
Alireza Ahmadi
af07a9bf15 Fix error handling in client update 2023-03-17 18:57:20 +01:00
Alireza Ahmadi
6b8ca0c321 v0.4.1 2023-03-17 15:15:06 +01:00
Alireza Ahmadi
48554ab497 Dark-Mode: Media css fix 2023-03-17 12:36:55 +01:00
Alireza Ahmadi
dddebf02ef Add translation xray functions 2023-03-17 11:09:04 +01:00
Alireza Ahmadi
0c469848dc Merge pull request #63 from lk29/main-add-1
add Xray stop & Xray restart into web
2023-03-17 10:58:02 +01:00
Alireza Ahmadi
cb85d4f83f Merge pull request #70 from alireza0/dependabot/github_actions/actions/setup-go-4
Bump actions/setup-go from 3 to 4
2023-03-17 10:57:30 +01:00
Alireza Ahmadi
df29570dc1 Dark-Mode: CSS fixes #50 2023-03-17 10:49:44 +01:00
Alireza Ahmadi
7e9c336a17 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-16 20:48:29 +01:00
Alireza Ahmadi
585f43498c TGBot: Add usage search for user #67 2023-03-16 20:48:21 +01:00
Alireza Ahmadi
2a7a09d12f Merge pull request #71 from alireza0:Dark-Mode
Add Dark-Mode Theme
2023-03-16 16:12:17 +01:00
Alireza Ahmadi
98759e964c Add theme to modal forms #50 2023-03-16 15:31:05 +01:00
Alireza Ahmadi
9ae3d62afb Remove access.log in default config 2023-03-16 11:22:28 +01:00
Alireza Ahmadi
24b718dca3 Init dark mode for main pages #50 2023-03-16 11:06:04 +01:00
dependabot[bot]
d0c4d0a941 Bump actions/setup-go from 3 to 4
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-15 22:07:17 +00:00
Leonardo
f829213dd1 go-version
go-version update to stable
2023-03-14 14:17:52 +03:30
Leonardo
4d5a73512e Update README.md
Fedora 36+
2023-03-14 14:01:52 +03:30
Leonardo
6dccc5b891 Update install.sh
Fedora OS Fix
2023-03-14 14:00:14 +03:30
Leonardo
f642830dc8 Update x-ui.sh
Fix Fedora
2023-03-14 13:56:56 +03:30
root
073b2ee8a8 add Xray stop & Xray restart 2023-03-13 22:32:24 +03:00
Alireza Ahmadi
8bcfa0b101 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-13 13:19:56 +01:00
Alireza Ahmadi
cd9e903ec3 Delete db directory 2023-03-13 13:19:45 +01:00
Alireza Ahmadi
b9fecaa918 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-13 13:19:18 +01:00
Alireza Ahmadi
eb516575ec Remove default docker db 2023-03-13 13:19:12 +01:00
Alireza Ahmadi
aeb5c9acaa Merge pull request #62 from alireza0/revert-59-main
Revert "Added Fedora OS Support"
2023-03-13 11:36:55 +01:00
Alireza Ahmadi
68a7b38117 Revert "Added Fedora OS Support" 2023-03-13 11:36:37 +01:00
Alireza Ahmadi
e084518f4f Merge pull request #59 from forkeer/main
Added Fedora OS Support
2023-03-13 11:23:49 +01:00
Leonardo
e6460d1d00 Update README.md
Added Fedora OS
2023-03-11 15:29:43 +03:30
Leonardo
0218af8b4f Update install.sh
Fix Fedora OS
2023-03-11 15:28:56 +03:30
Leonardo
552bc6c1f6 Update x-ui.sh
Fix Fedora OS
2023-03-11 15:27:26 +03:30
Alireza Ahmadi
3001107282 v0.4.0 2023-03-11 01:09:08 +01:00
Alireza Ahmadi
ed6f45f341 Merge pull request #37 from alireza0/dependabot/go_modules/gorm.io/gorm-1.24.6
Bump gorm.io/gorm from 1.24.5 to 1.24.6
2023-03-10 23:53:21 +01:00
Alireza Ahmadi
83853a963e Merge pull request #38 from alireza0/dependabot/go_modules/github.com/shirou/gopsutil/v3-3.23.2
Bump github.com/shirou/gopsutil/v3 from 3.23.1 to 3.23.2
2023-03-10 23:52:33 +01:00
dependabot[bot]
2774de744c Bump gorm.io/gorm from 1.24.5 to 1.24.6
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.24.5 to 1.24.6.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.24.5...v1.24.6)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-10 22:51:46 +00:00
dependabot[bot]
7306be3cb6 Bump github.com/shirou/gopsutil/v3 from 3.23.1 to 3.23.2
Bumps [github.com/shirou/gopsutil/v3](https://github.com/shirou/gopsutil) from 3.23.1 to 3.23.2.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.23.1...v3.23.2)

---
updated-dependencies:
- dependency-name: github.com/shirou/gopsutil/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-10 22:51:46 +00:00
Alireza Ahmadi
b2d16b87bb Merge pull request #53 from alireza0/dependabot/go_modules/github.com/xtls/xray-core-1.8.0
Bump github.com/xtls/xray-core from 1.7.5 to 1.8.0
2023-03-10 23:50:55 +01:00
Alireza Ahmadi
4ab1135d89 InfoModal for all other protocols #54 2023-03-10 23:45:31 +01:00
Alireza Ahmadi
8a6782273e Add icons to tgbot #34 2023-03-10 21:49:35 +01:00
Alireza Ahmadi
b2075e9548 Copy the code by clicking on QR #41 2023-03-10 20:36:00 +01:00
Alireza Ahmadi
8efa776cd7 Remark only email when exist 2023-03-10 20:34:51 +01:00
Alireza Ahmadi
b641951759 Init + Translate #40 2023-03-10 20:27:11 +01:00
Alireza Ahmadi
f539cbec45 add user title in bulk add #40 2023-03-10 19:33:56 +01:00
Alireza Ahmadi
645b1535ac Revert "add user title in bulk add #40"
This reverts commit 3729ac5e3e.
2023-03-10 19:31:40 +01:00
Alireza Ahmadi
3729ac5e3e add user title in bulk add #40 2023-03-10 19:30:26 +01:00
Alireza Ahmadi
f736f069a0 Fix tgbot: Admin can only search for client 2023-03-10 15:10:17 +01:00
Alireza Ahmadi
cd3d42c7ee Fix labels in ISSUE template 2023-03-10 14:48:57 +01:00
Alireza Ahmadi
88cc4aed19 Merge pull request #55 from alireza0:34-telegram-new-features
34-telegram-new-features
2023-03-10 14:22:21 +01:00
Alireza Ahmadi
2ae1455a1f Conditional stream and sniffing 2023-03-10 14:18:58 +01:00
Alireza Ahmadi
c20d13ea96 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-10 14:16:20 +01:00
Alireza Ahmadi
998d861d0a Add followRedirect to Dokodemo 2023-03-10 14:12:24 +01:00
Alireza Ahmadi
e1c76fdc7a Add CPU load alarm #34 2023-03-10 14:11:31 +01:00
dependabot[bot]
6749d63222 Bump github.com/xtls/xray-core from 1.7.5 to 1.8.0
Bumps [github.com/xtls/xray-core](https://github.com/xtls/xray-core) from 1.7.5 to 1.8.0.
- [Release notes](https://github.com/xtls/xray-core/releases)
- [Commits](https://github.com/xtls/xray-core/compare/v1.7.5...v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/xtls/xray-core
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-09 22:06:07 +00:00
Alireza Ahmadi
546b66a61c Fix initiate DB #34 2023-03-09 22:40:34 +01:00
Alireza Ahmadi
2e2f066c4b Telegram complete reports + Frontend #34 2023-03-09 22:01:08 +01:00
Alireza Ahmadi
5bac00b419 tgbot admin & client separation #34 2023-03-09 15:49:28 +01:00
Alireza Ahmadi
d07a2119ce Fix docker command #47 2023-03-07 14:52:20 +01:00
Alireza Ahmadi
2951b5f94c Support multi telegramId for admins #34 2023-03-06 21:42:02 +01:00
Alireza Ahmadi
a9c25cddb3 Update issue templates 2023-03-05 14:41:13 +01:00
Alireza Ahmadi
edc911c2f3 Move TgBot to service + fix pannel restart bug #34 2023-03-03 17:22:37 +01:00
Alireza Ahmadi
c053991a37 Update README.md 2023-03-03 02:08:38 +01:00
Alireza Ahmadi
73803af14c Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-02 21:49:53 +01:00
Alireza Ahmadi
6dc823e215 Improved Add Client process alireza0/x-ui#35 2023-03-02 21:48:56 +01:00
Alireza Ahmadi
5b1eaf9cc3 Update issue templates 2023-03-02 21:27:53 +01:00
Alireza Ahmadi
2f611684a2 v0.3.2 2023-03-02 17:48:42 +01:00
Alireza Ahmadi
0471bbbdb1 Merge pull request #23 from alireza0/dependabot/go_modules/github.com/gin-gonic/gin-1.9.0
Bump github.com/gin-gonic/gin from 1.8.2 to 1.9.0
2023-03-02 17:44:30 +01:00
Alireza Ahmadi
0ced0fcdc3 Merge pull request #32 from alireza0/dependabot/go_modules/github.com/pelletier/go-toml/v2-2.0.7
Bump github.com/pelletier/go-toml/v2 from 2.0.6 to 2.0.7
2023-03-02 17:44:14 +01:00
Alireza Ahmadi
33f354c71c Update backend Client model 2023-03-02 12:54:38 +01:00
Alireza Ahmadi
f87dad254b New feature: Bulk add #28 (plus adjust methods) 2023-03-02 12:50:59 +01:00
Alireza Ahmadi
340b54d45f Add Export feature #28 2023-03-02 09:32:42 +01:00
Alireza Ahmadi
1288427cce Show clients counter #30 2023-03-01 23:11:51 +01:00
Alireza Ahmadi
52d3956203 Support stats in Windows OS 2023-03-01 21:50:23 +01:00
Alireza Ahmadi
e13540901f Stupid bug detecting traffic email #33 2023-03-01 21:38:07 +01:00
Alireza Ahmadi
4f91375cb2 Small fixes 2023-03-01 19:52:21 +01:00
Alireza Ahmadi
9e398aaa65 Optimize Client-traffic-reset 2023-03-01 17:08:28 +01:00
Alireza Ahmadi
ae45f9fef2 Fallback limit only for TCP+TLS 2023-03-01 14:50:06 +01:00
Alireza Ahmadi
698af18d25 Fix add client #26 2023-03-01 14:01:09 +01:00
Alireza Ahmadi
afd369993a Fix Bug in deleting the client alireza0/x-ui#20 2023-03-01 13:57:48 +01:00
Alireza Ahmadi
40bb9f9864 Simplify method resetClientTraffic 2023-03-01 13:55:09 +01:00
dependabot[bot]
1f669aa61d Bump github.com/pelletier/go-toml/v2 from 2.0.6 to 2.0.7
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.0.6...v2.0.7)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-28 22:38:39 +00:00
Alireza Ahmadi
a776bdbb87 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-02-28 10:25:43 +01:00
Alireza Ahmadi
9a0055d5d5 Rollback useless utls feature 2023-02-28 10:25:27 +01:00
Alireza Ahmadi
19c13625a1 Update README.md 2023-02-26 14:44:53 +01:00
Alireza Ahmadi
e1428ff0dc Fix editing a searched client is buggy alireza0/x-ui#24 2023-02-24 14:46:14 +01:00
Alireza Ahmadi
4843c93608 Fix Vless & Trojan fingerprint problem 2023-02-24 13:07:17 +01:00
Alireza Ahmadi
757c2a76f8 Minimize call API for Client 2023-02-24 00:05:53 +01:00
dependabot[bot]
95c777f0ac Bump github.com/gin-gonic/gin from 1.8.2 to 1.9.0
Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.8.2 to 1.9.0.
- [Release notes](https://github.com/gin-gonic/gin/releases)
- [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gin-gonic/gin/compare/v1.8.2...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/gin-gonic/gin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-23 22:33:47 +00:00
Alireza Ahmadi
fc4f1a4fd3 Fix typos in README 2023-02-23 17:44:16 +01:00
Alireza Ahmadi
c7be137f6d Update screenshot 2023-02-23 17:20:57 +01:00
Alireza Ahmadi
8a07474999 0.3.1 2023-02-23 11:33:42 +01:00
Alireza Ahmadi
7ec68574d0 Fix Bug: Auto restart xray after client update 2023-02-23 11:33:14 +01:00
Alireza Ahmadi
414669fa1b Fix bugs 2023-02-22 23:15:52 +01:00
Alireza Ahmadi
76f774fd6c 0.3.0 2023-02-22 20:38:01 +01:00
Alireza Ahmadi
b82ce31ec1 Change menu to icons for client actions 2023-02-22 20:34:49 +01:00
Alireza Ahmadi
9bbd612327 Fix double release creation 2023-02-22 19:51:01 +01:00
Alireza Ahmadi
a5922a8620 v.0.3.0 2023-02-22 19:44:45 +01:00
Alireza Ahmadi
35e4b96e9e Fix traffic update in db 2023-02-22 18:34:02 +01:00
Alireza Ahmadi
ceb7a7ac14 Fix Trouble resetting traffic alireza0/x-ui#18 2023-02-22 17:33:05 +01:00
Alireza Ahmadi
92dc390e58 Merge pull request #16 from alireza0/dependabot/go_modules/github.com/BurntSushi/toml-1.2.1
Bump github.com/BurntSushi/toml from 1.0.0 to 1.2.1
2023-02-22 15:16:15 +01:00
Alireza Ahmadi
a6edd1c631 Merge pull request #17 from alireza0/dependabot/go_modules/github.com/gin-contrib/sessions-0.0.5
Bump github.com/gin-contrib/sessions from 0.0.3 to 0.0.5
2023-02-22 15:16:01 +01:00
Alireza Ahmadi
146b4d7873 Separate client module 2023-02-22 15:13:45 +01:00
dependabot[bot]
f3dac6395a Bump github.com/gin-contrib/sessions from 0.0.3 to 0.0.5
Bumps [github.com/gin-contrib/sessions](https://github.com/gin-contrib/sessions) from 0.0.3 to 0.0.5.
- [Release notes](https://github.com/gin-contrib/sessions/releases)
- [Changelog](https://github.com/gin-contrib/sessions/blob/master/.goreleaser.yaml)
- [Commits](https://github.com/gin-contrib/sessions/compare/v0.0.3...v0.0.5)

---
updated-dependencies:
- dependency-name: github.com/gin-contrib/sessions
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-20 22:30:00 +00:00
dependabot[bot]
b48707923c Bump github.com/BurntSushi/toml from 1.0.0 to 1.2.1
Bumps [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) from 1.0.0 to 1.2.1.
- [Release notes](https://github.com/BurntSushi/toml/releases)
- [Commits](https://github.com/BurntSushi/toml/compare/v1.0.0...v1.2.1)

---
updated-dependencies:
- dependency-name: github.com/BurntSushi/toml
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-20 22:29:46 +00:00
Alireza Ahmadi
40ac7d4115 Fix problems after updating gorm to 1.24 2023-02-20 12:00:40 +01:00
Alireza Ahmadi
3e550d7e13 v0.2.4 2023-02-19 01:41:24 +01:00
Alireza Ahmadi
2e12cd6f6e Translate none 2023-02-19 01:41:02 +01:00
Alireza Ahmadi
57f538bf6b Some upgrades 2023-02-19 01:40:36 +01:00
Alireza Ahmadi
7d23e8cf9c Merge branch 'main' of https://github.com/alireza0/x-ui 2023-02-19 01:20:54 +01:00
Alireza Ahmadi
4120024e04 Merge pull request #8 from alireza0/dependabot/go_modules/gorm.io/driver/sqlite-1.4.4
Bump gorm.io/driver/sqlite from 1.1.4 to 1.4.4
2023-02-19 01:19:19 +01:00
Alireza Ahmadi
fedb0610e0 Merge pull request #3 from alireza0:dependabot/github_actions/actions/setup-go-3
Bump actions/setup-go from 2 to 3
2023-02-19 01:18:35 +01:00
Alireza Ahmadi
9ed41cf427 Merge pull request #4 from alireza0:dependabot/github_actions/actions/checkout-3
Bump actions/checkout from 2 to 3
2023-02-19 01:18:15 +01:00
dependabot[bot]
8a66a42a4e Bump gorm.io/driver/sqlite from 1.1.4 to 1.4.4
Bumps [gorm.io/driver/sqlite](https://github.com/go-gorm/sqlite) from 1.1.4 to 1.4.4.
- [Release notes](https://github.com/go-gorm/sqlite/releases)
- [Commits](https://github.com/go-gorm/sqlite/compare/v1.1.4...v1.4.4)

---
updated-dependencies:
- dependency-name: gorm.io/driver/sqlite
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-19 00:18:10 +00:00
Alireza Ahmadi
ac98fce996 Merge pull request #6 from alireza0:dependabot/github_actions/docker/setup-qemu-action-2
Bump docker/setup-qemu-action from 1 to 2
2023-02-19 01:17:57 +01:00
Alireza Ahmadi
2aacd1111c Merge pull request #7 from alireza0:dependabot/go_modules/golang.org/x/text-0.7.0
Bump golang.org/x/text from 0.6.0 to 0.7.0
2023-02-19 01:17:38 +01:00
Alireza Ahmadi
bcf3fab524 Merge pull request #9 from alireza0:dependabot/go_modules/github.com/shirou/gopsutil-3.21.11incompatible
Bump github.com/shirou/gopsutil from 3.21.3+incompatible to 3.21.11+incompatible
2023-02-19 01:16:59 +01:00
Alireza Ahmadi
76919a1cc5 Merge pull request #10 from alireza0:dependabot/go_modules/gorm.io/gorm-1.24.5
Bump gorm.io/gorm from 1.21.9 to 1.24.5
2023-02-19 01:16:35 +01:00
dependabot[bot]
cef6e13441 Bump github.com/shirou/gopsutil
Bumps [github.com/shirou/gopsutil](https://github.com/shirou/gopsutil) from 3.21.3+incompatible to 3.21.11+incompatible.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.21.3...v3.21.11)

---
updated-dependencies:
- dependency-name: github.com/shirou/gopsutil
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-19 00:11:30 +00:00
dependabot[bot]
0c8b58841f Bump gorm.io/gorm from 1.21.9 to 1.24.5
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.21.9 to 1.24.5.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.21.9...v1.24.5)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-19 00:11:29 +00:00
dependabot[bot]
c8043bf9e2 Bump golang.org/x/text from 0.6.0 to 0.7.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-19 00:11:28 +00:00
Alireza Ahmadi
e9b7ba8b11 Merge pull request #11 from alireza0:dependabot/go_modules/github.com/gin-gonic/gin-1.8.2
Bump github.com/gin-gonic/gin from 1.7.1 to 1.8.2
2023-02-19 01:10:21 +01:00
Alireza Ahmadi
5bee682d39 tls options 2023-02-19 00:38:54 +01:00
Alireza Ahmadi
ef0dd10fe7 Simplify codes with fix small view problems 2023-02-18 16:50:40 +01:00
Alireza Ahmadi
9ea9d4d4dd Remove case sensitivity in search 2023-02-18 16:47:16 +01:00
Alireza Ahmadi
d2e313a588 Fix bug: Edit inbound 2023-02-18 12:41:26 +01:00
Alireza Ahmadi
35de896e5e Fix formats 2023-02-16 11:12:09 +01:00
dependabot[bot]
1c00da7537 Bump docker/setup-qemu-action from 1 to 2
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 1 to 2.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-16 08:40:51 +00:00
dependabot[bot]
5407e2330b Bump github.com/gin-gonic/gin from 1.7.1 to 1.8.2
Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.7.1 to 1.8.2.
- [Release notes](https://github.com/gin-gonic/gin/releases)
- [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gin-gonic/gin/compare/v1.7.1...v1.8.2)

---
updated-dependencies:
- dependency-name: github.com/gin-gonic/gin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-15 19:54:15 +00:00
dependabot[bot]
cb2c2b1f5c Bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-15 19:53:33 +00:00
dependabot[bot]
f131f2512b Bump actions/setup-go from 2 to 3
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-15 19:53:30 +00:00
71 changed files with 3273 additions and 1634 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,10 @@
---
name: Question template
about: Ask if it is not clear that it is a bug
title: ''
labels: question
assignees: ''
---

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
submodules: true
@@ -27,7 +27,7 @@ jobs:
type=pep440,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

View File

@@ -6,31 +6,15 @@ on:
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-18.04
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
prerelease: true
linuxamd64build:
name: build x-ui amd64 version
needs: release
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: 'stable'
- name: build linux amd64 version
run: |
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
@@ -53,22 +37,20 @@ jobs:
- name: package
run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui
- name: upload
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: svenstaro/upload-release-action@2.5.0
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: x-ui-linux-amd64.tar.gz
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-linux-amd64.tar.gz
asset_name: x-ui-linux-amd64.tar.gz
asset_content_type: application/gzip
prerelease: true
linuxarm64build:
name: build x-ui arm64 version
needs: release
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: build linux arm64 version
@@ -95,22 +77,20 @@ jobs:
- name: package
run: tar -zcvf x-ui-linux-arm64.tar.gz x-ui
- name: upload
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: svenstaro/upload-release-action@2.5.0
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: x-ui-linux-arm64.tar.gz
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-linux-arm64.tar.gz
asset_name: x-ui-linux-arm64.tar.gz
asset_content_type: application/gzip
prerelease: true
linuxs390xbuild:
name: build x-ui s390x version
needs: release
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: build linux s390x version
@@ -137,11 +117,10 @@ jobs:
- name: package
run: tar -zcvf x-ui-linux-s390x.tar.gz x-ui
- name: upload
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: svenstaro/upload-release-action@2.5.0
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: x-ui-linux-s390x.tar.gz
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-linux-s390x.tar.gz
asset_name: x-ui-linux-s390x.tar.gz
asset_content_type: application/gzip
prerelease: true

View File

@@ -6,17 +6,18 @@
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment**
xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
| Features | Enable? |
| ------------- |:-------------:|
| Multi-lang | :heavy_check_mark: |
| Dark/Light Theme | :heavy_check_mark: |
| Search in deep | :heavy_check_mark: |
| Inbound Multi User | :heavy_check_mark: |
| Multi User Traffic & Expiration time | :heavy_check_mark: |
| REST API | :heavy_check_mark: |
| Telegram BOT | :heavy_check_mark: |
| Telegram BOT (admin + clients) | :heavy_check_mark: |
| Backup database using Telegram BOT | :heavy_check_mark: |
**If you think this project is helpful to you, you may wish to give a** :star2:
@@ -24,6 +25,7 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
- System Status Monitoring
- Search within all inbounds and clients
- Support Dark/Light theme UI
- Support multi-user multi-protocol, web page visualization operation
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
- Support for configuring more transport configurations
@@ -33,16 +35,23 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
- Support one-click SSL certificate application and automatic renewal
- For more advanced configuration items, please refer to the panel
# Screenshoot from Inbouds page
# Screenshot from Inbouds page
![inbounds](./media/inbounds.png)
![Dark inbounds](./media/inbounds-dark.png)
# Install & Upgrade
# Install & Upgrade to latest version
```
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh)
```
## Install custom version
To install your desired version you can add the version to the end of install command. Example for ver `0.4.0`:
```
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) 0.4.0
```
## Manual install & upgrade
1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases , generally choose Architecture `amd64`
@@ -65,8 +74,6 @@ systemctl restart x-ui
## Install using docker
> This docker tutorial and docker image are provided by [alireza0](https://github.com/alireza0)
1. install docker
```shell
@@ -77,11 +84,13 @@ curl -fsSL https://get.docker.com | sh
```shell
mkdir x-ui && cd x-ui
docker run -itd --network=host \
docker run -itd \
-p 54321:54321 -p 443:443 -p 80:80 \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--name x-ui --restart=unless-stopped \
alireza0/x-ui:latest
alireza7/x-ui:latest
```
> Build your own image
@@ -106,17 +115,20 @@ ln -s /snap/bin/certbot /usr/bin/certbot
certbot certonly --standalone --register-unsafely-without-email --non-interactive --agree-tos -d <Your Domain Name>
```
## Tg robot use (under development, temporarily unavailable)
## Tg robot use
> This feature and tutorial are provided by [FranzKafkaYu](https://github.com/FranzKafkaYu)
X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
Set the robot-related parameters in the panel background, including:
- Tg Robot Token
- Tg Robot ChatId
- Tg robot Token
- Tg robot ChatId
- Tg robot cycle runtime, in crontab syntax
- Tg robot Expiration threshold
- Tg robot Traffic threshold
- Tg robot Enable send backup in cycle runtime
- Tg robot Enable CPU usage alarm threshold
Reference syntax:
@@ -124,14 +136,22 @@ Reference syntax:
- @hourly // hourly notification
- @daily // Daily notification (00:00 in the morning)
- @every 8h // notify every 8 hours
- TG notification content:
- Node traffic usage
- Panel login reminder
- Node expiration reminder
- Traffic warning reminder
### Telegram Bot Features
- Report periodic
- Login notification
- CPU threshold notification
- Threshold for Expiration time and Traffic to report in advance
- Support client report if client's telegram username is added to the end of `email` like 'test123@telegram_username'
- Support telegram traffic report searched with UID (VMESS/VLESS) or Password (TROJAN) - anonymously
- Menu based bot
- Search client by email ( only admin )
- Check all inbounds
- Check server status
- Check Exhausted users
- Receive backup by request and in periodic reports
More features are planned...
## suggestion system
@@ -139,6 +159,7 @@ More features are planned...
- CentOS 7+
- Ubuntu 16+
- Debian 8+
- Fedora 36+
# common problem
@@ -154,7 +175,7 @@ x-ui v2-ui
# T-Shoots:
**If you ygrade from an old version or other forks, for enable traffic for users you should do :**
**If you upgrade from an old version or other forks, for enable traffic for users you should do :**
find this in config :
``` json
@@ -190,7 +211,11 @@ find this in config :
"routing": {
```
restart panel
# a special thanks to
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
- [FranzKafkaYu](https://github.com/FranzKafkaYu)
- [MHSanaei](https://github.com/MHSanaei)
## Stargazers over time

View File

@@ -1 +1 @@
0.2.3
0.4.3

View File

@@ -68,9 +68,10 @@ type Setting struct {
type Client struct {
ID string `json:"id"`
Password string `json:"password"`
Flow string `json:"flow"`
AlterIds uint16 `json:"alterId"`
Email string `json:"email"`
Security string `json:"security"`
TotalGB int64 `json:"totalGB" form:"totalGB"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
}

Binary file not shown.

71
go.mod
View File

@@ -3,50 +3,59 @@ module x-ui
go 1.20
require (
github.com/BurntSushi/toml v1.0.0
github.com/Workiva/go-datastructures v1.0.53
github.com/gin-contrib/sessions v0.0.3
github.com/gin-gonic/gin v1.7.1
github.com/gin-contrib/sessions v0.0.4
github.com/gin-gonic/gin v1.9.0
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/nicksnyder/go-i18n/v2 v2.2.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.0.7
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil v3.21.3+incompatible
github.com/xtls/xray-core v1.7.5
github.com/shirou/gopsutil/v3 v3.23.2
github.com/xtls/xray-core v1.8.0
go.uber.org/atomic v1.10.0
golang.org/x/text v0.6.0
google.golang.org/grpc v1.53.0
gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.21.9
golang.org/x/text v0.8.0
google.golang.org/grpc v1.54.0
gorm.io/driver/sqlite v1.4.4
gorm.io/gorm v1.24.6
)
require (
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/bytedance/sonic v1.8.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.1.3 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.2 // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/mattn/go-sqlite3 v1.14.5 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pires/go-proxyproto v0.6.2 // indirect
github.com/tklauser/go-sysconf v0.3.5 // indirect
github.com/tklauser/numcpus v0.2.2 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.5.0 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/sys v0.5.0 // indirect
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
google.golang.org/protobuf v1.29.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

494
go.sum
View File

@@ -1,458 +1,254 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 h1:5sXbqlSomvdjlRbWyNqkPsJ3Fg+tQZCbgeX1VGljbQY=
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 h1:BS21ZUJ/B5X2UVUbczfmdWH7GapPWAhxcMsDnjJTU1E=
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI=
github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8=
github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 h1:CqYfpuYIjnlNxM3msdyPRKabhXZWbKjf3Q8BWROFBso=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lucas-clemente/quic-go v0.20.0 h1:FSU3YN5VnLafHR27Ejs1r1CYMS7XMyIVDzRewkDLNBw=
github.com/lucas-clemente/quic-go v0.20.0/go.mod h1:fZq/HUDIM+mW6X6wtzORjC0E/WDBMKe5Hf9bgjISwLk=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
github.com/marten-seemann/qtls-go1-15 v0.1.4 h1:RehYMOyRW8hPVEja1KBVsFVNSm35Jj9Mvs5yNoZZ28A=
github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
github.com/marten-seemann/qtls-go1-16 v0.1.3 h1:XEZ1xGorVy9u+lJq+WXNE+hiqRYLNvJGYmwfwKQN2gU=
github.com/marten-seemann/qtls-go1-16 v0.1.3/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/ginkgo/v2 v2.9.0 h1:Tugw2BKlNHTMfG+CheOITkYvk4LAh6MFOvikhGVnhE8=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pires/go-proxyproto v0.5.0 h1:A4Jv4ZCaV3AFJeGh5mGwkz4iuWUYMlQ7IoO/GTuSuLo=
github.com/pires/go-proxyproto v0.5.0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/refraction-networking/utls v0.0.0-20201210053706-2179f286686b h1:lzo71oHzQEz0fKMSjR0BpVzuh2hOHvJTxnN3Rnikmtg=
github.com/refraction-networking/utls v0.0.0-20201210053706-2179f286686b/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A=
github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk=
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
github.com/refraction-networking/utls v1.2.3-0.20230308205431-4f1df6c200db h1:ULRv/GPW5KYDafE0FACN2no+HTCyQLUtfyOIeyp3GNc=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c h1:pqy40B3MQWYrza7YZXOXgl0Nf0QGFqrOC0BKae1UNAA=
github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil v3.21.3+incompatible h1:uenXGGa8ESCQq+dbgtl916dmg6PSAz2cXov0uORQ9v8=
github.com/shirou/gopsutil v3.21.3+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/sagernet/sing v0.1.7 h1:g4vjr3q8SUlBZSx97Emz5OBfSMBxxW5Q8C2PfdoSo08=
github.com/sagernet/sing-shadowsocks v0.1.1 h1:uFK2rlVeD/b1xhDwSMbUI2goWc6fOKxp+ZeKHZq6C9Q=
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
github.com/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU=
github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4=
github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=
github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA=
github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499 h1:QHESTXtfgc1ABV+ArlbPVqUx9Ht5I0dDkYhxYoXFxNo=
github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499/go.mod h1:5TB2+k58gx4A4g2Nf5miSHNDF6CuAzHKpWBooLAshTs=
github.com/xtls/xray-core v1.4.2 h1:D0Le+Qy9L/eY5LbUQfrk7WJ8wbODpQSW/ZRCg+BRe7c=
github.com/xtls/xray-core v1.4.2/go.mod h1:DmL/9rOCliev/a6HciWEvSJVEhUF6C0EpD3clW8v0pc=
github.com/xtls/xray-core v1.7.5 h1:Ukr3hXnOG2ciViQL7kfYRl9S3GVej2dkV7DzabmoLL4=
github.com/xtls/xray-core v1.7.5/go.mod h1:Mx1QzIDvSk4eZ8hKa3AYsSPfyZJNQXWVXTJxJRJ98wI=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/xtls/reality v0.0.0-20230309125256-0d0713b108c8 h1:LLtLxEe3S0Ko+ckqt4t29RLskpNdOZfgjZCC2/Byr50=
github.com/xtls/xray-core v1.8.0 h1:/OD0sDv6YIBqvE+cVfnqlKrtbMs0Fm9IP5BR5d8Eu4k=
github.com/xtls/xray-core v1.8.0/go.mod h1:i9KWgbLyxg/NT+3+g4nE74Zp3DgTCP3X04YkSfsJeDI=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.starlark.net v0.0.0-20210312235212-74c10e2c17dc h1:pVkptfeOTFfx+zXZo7HEHN3d5LmhatBFvHdm/f2QnpY=
go.starlark.net v0.0.0-20210312235212-74c10e2c17dc/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210330230544-e57232859fb2/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201231184435-2d18734c6014/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57 h1:vArvWooPH749rNHpBGgVl+U9B9dATjiEhJzcWGlovNs=
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.21.9 h1:INieZtn4P2Pw6xPJ8MzT0G4WUOsHq3RhfuDF1M6GW0E=
gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
h12.io/socks v1.0.2/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -10,24 +10,18 @@ cur_dir=$(pwd)
# check root
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error${plain} Please run this script with root privilege \n " && exit 1
# check os
if [[ -f /etc/redhat-release ]]; then
release="centos"
elif cat /etc/issue | grep -Eqi "debian"; then
release="debian"
elif cat /etc/issue | grep -Eqi "ubuntu"; then
release="ubuntu"
elif cat /etc/issue | grep -Eqi "centos|red hat|redhat"; then
release="centos"
elif cat /proc/version | grep -Eqi "debian"; then
release="debian"
elif cat /proc/version | grep -Eqi "ubuntu"; then
release="ubuntu"
elif cat /proc/version | grep -Eqi "centos|red hat|redhat"; then
release="centos"
# Check OS and set release variable
if [[ -f /etc/os-release ]]; then
source /etc/os-release
release=$ID
elif [[ -f /usr/lib/os-release ]]; then
source /usr/lib/os-release
release=$ID
else
echo -e "${red} Check system OS failed, please contact the author! ${plain}\n" && exit 1
echo "Failed to check the system OS, please contact the author!" >&2
exit 1
fi
echo "The OS release is: $release"
arch=$(arch)
@@ -39,42 +33,44 @@ elif [[ $arch == "s390x" ]]; then
arch="s390x"
else
arch="amd64"
echo -e "${red} Fail to check system arch, will use default arch: ${arch}${plain}"
echo -e "${red} Failed to check system arch, will use default arch: ${arch}${plain}"
fi
echo "arch: ${arch}"
if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ]; then
echo "x-ui dosen't support 32-bit(x86) system, please use 64 bit operating system(x86_64) instead,if there is something wrong, plz let me know"
echo "x-ui dosen't support 32-bit(x86) system, please use 64 bit operating system(x86_64) instead, if there is something wrong, please get in touch with me!"
exit -1
fi
os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
# os version
if [[ -f /etc/os-release ]]; then
os_version=$(awk -F'[= ."]' '/VERSION_ID/{print $3}' /etc/os-release)
fi
if [[ -z "$os_version" && -f /etc/lsb-release ]]; then
os_version=$(awk -F'[= ."]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release)
fi
if [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 7 ]]; then
echo -e "${red} Please use CentOS 7 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 18 ]]; then
echo -e "${red}please use Ubuntu 18 or higher version${plain}\n" && exit 1
fi
if [[ x"${release}" == x"centos" ]]; then
if [[ ${os_version} -le 6 ]]; then
echo -e "${red} Please use CentOS 7 or higher version ${plain}\n" && exit 1
elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then
echo -e "${red}please use Fedora 36 or higher version${plain}\n" && exit 1
fi
elif [[ x"${release}" == x"ubuntu" ]]; then
if [[ ${os_version} -lt 16 ]]; then
echo -e "${red} Please use Ubuntu 16 or higher version ${plain}\n" && exit 1
fi
elif [[ x"${release}" == x"debian" ]]; then
elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use Debian 8 or higher version ${plain}\n" && exit 1
echo -e "${red} Please use Debian 8 or higher ${plain}\n" && exit 1
fi
else
echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1
fi
install_base() {
if [[ x"${release}" == x"centos" ]]; then
if [[ "${release}" == "centos" ]]; then
yum install wget curl tar -y
else
apt install wget curl tar -y

10
main.go
View File

@@ -136,7 +136,7 @@ func updateTgbotEnableSts(status bool) {
return
}
func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string) {
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
@@ -165,7 +165,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string)
}
}
if tgBotChatid != 0 {
if tgBotChatid != "" {
err := settingService.SetTgBotChatId(tgBotChatid)
if err != nil {
fmt.Println(err)
@@ -224,7 +224,7 @@ func main() {
var username string
var password string
var tgbottoken string
var tgbotchatid int
var tgbotchatid string
var enabletgbot bool
var tgbotRuntime string
var reset bool
@@ -236,7 +236,7 @@ func main() {
settingCmd.StringVar(&password, "password", "", "set login password")
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
settingCmd.IntVar(&tgbotchatid, "tgbotchatid", 0, "set telegrame bot chat id")
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegrame bot chat id")
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
oldUsage := flag.Usage
@@ -287,7 +287,7 @@ func main() {
if show {
showSetting(show)
}
if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") {
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
}
default:

BIN
media/inbounds-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -4,5 +4,5 @@ import (
_ "unsafe"
)
//go:linkname HostProc github.com/shirou/gopsutil/internal/common.HostProc
//go:linkname HostProc github.com/shirou/gopsutil/v3/internal/common.HostProc
func HostProc(combineWith ...string) string

View File

@@ -1,9 +1,10 @@
//go:build darwin
// +build darwin
package sys
import (
"github.com/shirou/gopsutil/net"
"github.com/shirou/gopsutil/v3/net"
)
func GetTCPCount() (int, error) {

View File

@@ -1,3 +1,4 @@
//go:build linux
// +build linux
package sys

24
util/sys/sys_windows.go Normal file
View File

@@ -0,0 +1,24 @@
//go:build windows
// +build windows
package sys
import (
"github.com/shirou/gopsutil/v3/net"
)
func GetTCPCount() (int, error) {
stats, err := net.Connections("tcp")
if err != nil {
return 0, err
}
return len(stats), nil
}
func GetUDPCount() (int, error) {
stats, err := net.Connections("udp")
if err != nil {
return 0, err
}
return len(stats), nil
}

File diff suppressed because one or more lines are too long

View File

@@ -1,152 +1,339 @@
#app {
height: 100%;
}
.ant-space {
width: 100%;
}
.ant-layout-sider-zero-width-trigger {
display: none;
}
.ant-card {
border-radius: 30px;
}
.ant-card-hoverable {
cursor: auto;
}
.ant-card+.ant-card {
margin-top: 20px;
}
.drawer-handle {
position: absolute;
top: 72px;
width: 41px;
height: 40px;
cursor: pointer;
z-index: 0;
text-align: center;
line-height: 40px;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
background: #fff;
right: -40px;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
border-radius: 0 4px 4px 0;
}
@media (min-width: 769px) {
.drawer-handle {
display: none;
}
}
.fade-in-enter, .fade-in-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active {
opacity: 0
}
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
-webkit-transition: opacity .2s linear;
transition: opacity .2s linear
}
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
-webkit-transition: opacity .2s linear;
transition: opacity .2s linear
}
.fade-in-enter-active, .fade-in-leave-active {
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
transition: all .3s cubic-bezier(.55, 0, .1, 1)
}
.zoom-in-center-enter-active, .zoom-in-center-leave-active {
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
transition: all .3s cubic-bezier(.55, 0, .1, 1)
}
.zoom-in-center-enter, .zoom-in-center-leave-active {
opacity: 0;
-webkit-transform: scaleX(0);
transform: scaleX(0)
}
.zoom-in-top-enter-active, .zoom-in-top-leave-active {
opacity: 1;
-webkit-transform: scaleY(1);
transform: scaleY(1);
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
-webkit-transform-origin: center top;
transform-origin: center top
}
.zoom-in-top-enter, .zoom-in-top-leave-active {
opacity: 0;
-webkit-transform: scaleY(0);
transform: scaleY(0)
}
.zoom-in-bottom-enter-active, .zoom-in-bottom-leave-active {
opacity: 1;
-webkit-transform: scaleY(1);
transform: scaleY(1);
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
-webkit-transform-origin: center bottom;
transform-origin: center bottom
}
.zoom-in-bottom-enter, .zoom-in-bottom-leave-active {
opacity: 0;
-webkit-transform: scaleY(0);
transform: scaleY(0)
}
.zoom-in-left-enter-active, .zoom-in-left-leave-active {
opacity: 1;
-webkit-transform: scale(1, 1);
transform: scale(1, 1);
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
-webkit-transform-origin: top left;
transform-origin: top left
}
.zoom-in-left-enter, .zoom-in-left-leave-active {
opacity: 0;
-webkit-transform: scale(.45, .45);
transform: scale(.45, .45)
}
.list-enter-active, .list-leave-active {
-webkit-transition: all .3s;
transition: all .3s
}
.list-enter, .list-leave-active {
opacity: 0;
-webkit-transform: translateY(-30px);
transform: translateY(-30px)
}
.ant-progress-inner {
background-color: #EBEEF5;
}
.deactive-client .ant-collapse-header{
color:rgb(255, 255, 255) !important;
background-color: rgb(255, 127, 127);
#app {
height: 100%;
}
.ant-space {
width: 100%;
}
.ant-layout-sider-zero-width-trigger {
display: none;
}
.ant-card {
border-radius: 30px;
}
.ant-card-hoverable {
cursor: auto;
}
.ant-card+.ant-card {
margin-top: 20px;
}
.drawer-handle {
position: absolute;
top: 72px;
width: 41px;
height: 40px;
cursor: pointer;
z-index: 0;
text-align: center;
line-height: 40px;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
background: #fff;
right: -40px;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
border-radius: 0 4px 4px 0;
}
@media (min-width: 769px) {
.drawer-handle {
display: none;
}
}
.fade-in-enter, .fade-in-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active {
opacity: 0
}
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
-webkit-transition: opacity .2s linear;
transition: opacity .2s linear
}
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
-webkit-transition: opacity .2s linear;
transition: opacity .2s linear
}
.fade-in-enter-active, .fade-in-leave-active {
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
transition: all .3s cubic-bezier(.55, 0, .1, 1)
}
.zoom-in-center-enter-active, .zoom-in-center-leave-active {
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
transition: all .3s cubic-bezier(.55, 0, .1, 1)
}
.zoom-in-center-enter, .zoom-in-center-leave-active {
opacity: 0;
-webkit-transform: scaleX(0);
transform: scaleX(0)
}
.zoom-in-top-enter-active, .zoom-in-top-leave-active {
opacity: 1;
-webkit-transform: scaleY(1);
transform: scaleY(1);
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
-webkit-transform-origin: center top;
transform-origin: center top
}
.zoom-in-top-enter, .zoom-in-top-leave-active {
opacity: 0;
-webkit-transform: scaleY(0);
transform: scaleY(0)
}
.zoom-in-bottom-enter-active, .zoom-in-bottom-leave-active {
opacity: 1;
-webkit-transform: scaleY(1);
transform: scaleY(1);
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
-webkit-transform-origin: center bottom;
transform-origin: center bottom
}
.zoom-in-bottom-enter, .zoom-in-bottom-leave-active {
opacity: 0;
-webkit-transform: scaleY(0);
transform: scaleY(0)
}
.zoom-in-left-enter-active, .zoom-in-left-leave-active {
opacity: 1;
-webkit-transform: scale(1, 1);
transform: scale(1, 1);
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
-webkit-transform-origin: top left;
transform-origin: top left
}
.zoom-in-left-enter, .zoom-in-left-leave-active {
opacity: 0;
-webkit-transform: scale(.45, .45);
transform: scale(.45, .45)
}
.list-enter-active, .list-leave-active {
-webkit-transition: all .3s;
transition: all .3s
}
.list-enter, .list-leave-active {
opacity: 0;
-webkit-transform: translateY(-30px);
transform: translateY(-30px)
}
.ant-progress-inner {
background-color: #EBEEF5;
}
.deactive-client .ant-collapse-header{
color:rgb(255, 255, 255) !important;
background-color: rgb(255, 127, 127);
}
.ant-table-tbody>tr>td,
.ant-table-thead>tr>th{
padding:16px;
}
.ant-menu-dark,
.ant-menu-dark .ant-menu-sub,
.ant-layout-header,
.ant-layout-sider-dark,
.ant-layout-sider-zero-width-trigger,
.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu,
.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu {
background:#161b22
}
.ant-card-dark {
color: hsla(0,0%,100%,.65);
background-color: #1a212a;
border-color:rgba(0,0,0,.09);
}
.ant-card-dark:hover {
border-color: #e8e8e8;
}
.ant-card-dark .ant-table-thead th {
color: hsla(0,0%,100%,.65);
background-color: #161b22;
}
.ant-card-dark .ant-table-tbody tr td,
.ant-card-dark .ant-modal-title {
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-collapse-content,
.ant-card-dark .ant-calendar,
.ant-card-dark .ant-table-placeholder,
.ant-card-dark .ant-input-group-addon {
color: hsla(0,0%,100%,.65);
background-color: #262f3d;
}
.ant-card-dark .ant-list-item-meta-title,
.ant-card-dark .ant-list-item-meta-description,
.ant-card-dark .ant-form-item-label>label,
.ant-card-dark .ant-form-item,
.ant-card-dark .ant-divider-inner-text,
.ant-card-dark .ant-modal-confirm-content,
.ant-card-dark .ant-modal-confirm-title,
.ant-card-dark .ant-progress-text,
.ant-card-dark .ant-modal-close,
.ant-card-dark i,
.ant-card-dark .ant-select-dropdown-menu-item,
.ant-card-dark .ant-calendar-month-select,
.ant-card-dark .ant-calendar-year-select,
.ant-card-dark .ant-calendar-date,
.ant-card-dark .ant-collapse>.ant-collapse-item>.ant-collapse-header,
.ant-card-dark .ant-empty-normal,
.ant-card-dark .ant-checkbox+span {
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
.ant-card-dark .ant-calendar-date:hover {
background-color: #004488;
}
.ant-card-dark tbody .ant-table-expanded-row {
color: hsla(0,0%,100%,.65);
background-color: #1a212a;
}
.ant-card-dark .ant-input,
.ant-card-dark .ant-input-number,
.ant-card-dark .ant-calendar-input,
.ant-card-dark .ant-select-dropdown-menu-item-selected,
.ant-card-dark .ant-select-selection {
color: hsla(0,0%,100%,.65);
background-color: #2e3b52;
}
.ant-card-dark .ant-collapse-item {
color: hsla(0,0%,100%,.65);
background-color: #161b22;
}
.ant-card-dark .ant-modal-content,
.ant-card-dark .ant-modal-body,
.ant-card-dark .ant-modal-header,
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
color: hsla(0,0%,100%,.65);
background-color: #222a37;
}
.client-table-header {
background-color: #f0f2f5;
}
.client-table-odd-row {
background-color: #fafafa;
}
.ant-card-dark .client-table-header {
background-color: #1a212a;
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .client-table-odd-row {
color: hsla(0,0%,100%,.65);
background-color: #242c3a;
}
.ant-card-dark .ant-calendar-last-month-cell .ant-calendar-date,
.ant-card-dark .ant-calendar-next-month-btn-day .ant-calendar-date {
color: hsla(0,0%,100%,.30);
}
.ant-drawer-dark {
color: hsla(0,0%,100%,.65);
}
.ant-drawer-dark .ant-drawer-wrapper-body,
.ant-drawer-dark .drawer-handle {
background-color: #1a212a;
border: 1px solid hsla(0,0%,100%,.30);
}
.ant-card-dark .ant-tag-blue {
color: #3c9ae8;
background: #111d2c;
border-color: #15395b;
}
.ant-card-dark .ant-tag-green {
color: #6abe39;
background: #162312;
border-color: #274916;
}
.ant-card-dark .ant-tag-cyan {
color: #33bcb7;
background: #112123;
border-color: #144848;
}
.ant-card-dark .ant-tag-red {
color: #e84749;
background: #2a1215;
border-color: #58181c;
}
.ant-card-dark .ant-tag-orange {
color: #e89a3c;
background: #2b1d11;
border-color: #593815;
}
.ant-card-dark .ant-table-row-expand-icon,
.ant-card-dark .ant-checkbox-inner {
background: none;
}
.ant-card-dark .ant-switch-checked {
background-color: #0c61b0;
}
.ant-card-dark .ant-btn,
.ant-card-dark .ant-radio-button-wrapper {
color: hsla(0,0%,100%,.65);
background: none;
border: 1px solid hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-radio-button-wrapper:hover {
color: #177ddc;
}
.ant-card-dark .ant-btn-primary {
color: hsla(0,0%,100%,.65);
background-color: #073763;
border-color: #1890ff;
text-shadow: 0 -1px 0 rgba(0,0,0,.12);
box-shadow: 0 2px 0 rgba(0,0,0,.045);
}

View File

@@ -155,6 +155,11 @@ class DBInbound {
const inbound = this.toInbound();
return inbound.genLink(this.address, this.remark, clientIndex);
}
get genInboundLinks() {
const inbound = this.toInbound();
return inbound.genInboundLinks(this.address, this.remark);
}
}
class AllSetting {
@@ -167,8 +172,12 @@ class AllSetting {
this.webBasePath = "/";
this.tgBotEnable = false;
this.tgBotToken = "";
this.tgBotChatId = 0;
this.tgRunTime = "";
this.tgBotChatId = "";
this.tgRunTime = "@daily";
this.tgBotBackup = false;
this.tgExpireDiff = "";
this.tgTrafficDiff = "";
this.tgCpu = "";
this.xrayTemplateConfig = "";
this.timeLocation = "Asia/Tehran";

View File

@@ -24,6 +24,9 @@ const SSMethods = {
CHACHA20_POLY1305: 'chacha20-poly1305',
AES_256_GCM: 'aes-256-gcm',
AES_128_GCM: 'aes-128-gcm',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
};
const RULE_IP = {
@@ -40,17 +43,70 @@ const RULE_DOMAIN = {
SPEEDTEST: 'geosite:speedtest',
};
const FLOW_CONTROL = {
const XTLS_FLOW_CONTROL = {
ORIGIN: "xtls-rprx-origin",
DIRECT: "xtls-rprx-direct",
};
const TLS_FLOW_CONTROL = {
VISION: "xtls-rprx-vision",
};
const TLS_VERSION_OPTION = {
TLS10: "1.0",
TLS11: "1.1",
TLS12: "1.2",
TLS13: "1.3",
}
const TLS_CIPHER_OPTION = {
RSA_AES_128_CBC: "TLS_RSA_WITH_AES_128_CBC_SHA",
RSA_AES_256_CBC: "TLS_RSA_WITH_AES_256_CBC_SHA",
RSA_AES_128_GCM: "TLS_RSA_WITH_AES_128_GCM_SHA256",
RSA_AES_256_GCM: "TLS_RSA_WITH_AES_256_GCM_SHA384",
AES_128_GCM: "TLS_AES_128_GCM_SHA256",
AES_256_GCM: "TLS_AES_256_GCM_SHA384",
CHACHA20_POLY1305: "TLS_CHACHA20_POLY1305_SHA256",
ECDHE_ECDSA_AES_128_CBC: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
ECDHE_ECDSA_AES_256_CBC: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
ECDHE_RSA_AES_128_CBC: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
ECDHE_RSA_AES_256_CBC: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
ECDHE_ECDSA_AES_128_GCM: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
ECDHE_ECDSA_AES_256_GCM: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
ECDHE_RSA_AES_128_GCM: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
ECDHE_RSA_AES_256_GCM: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
ECDHE_ECDSA_CHACHA20_POLY1305: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
ECDHE_RSA_CHACHA20_POLY1305: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
};
const UTLS_FINGERPRINT = {
UTLS_CHROME: "chrome",
UTLS_FIREFOX: "firefox",
UTLS_SAFARI: "safari",
UTLS_IOS: "ios",
UTLS_android: "android",
UTLS_EDGE: "edge",
UTLS_360: "360",
UTLS_QQ: "qq",
UTLS_RANDOM: "random",
UTLS_RANDOMIZED: "randomized",
};
const ALPN_OPTION = {
H2: "h2",
HTTP1: "http/1.1",
};
Object.freeze(Protocols);
Object.freeze(VmessMethods);
Object.freeze(SSMethods);
Object.freeze(RULE_IP);
Object.freeze(RULE_DOMAIN);
Object.freeze(FLOW_CONTROL);
Object.freeze(XTLS_FLOW_CONTROL);
Object.freeze(TLS_FLOW_CONTROL);
Object.freeze(TLS_VERSION_OPTION);
Object.freeze(TLS_CIPHER_OPTION);
Object.freeze(ALPN_OPTION);
class XrayCommonClass {
@@ -417,11 +473,20 @@ class GrpcStreamSettings extends XrayCommonClass {
class TlsStreamSettings extends XrayCommonClass {
constructor(serverName='',
certificates=[new TlsStreamSettings.Cert()], alpn=[]) {
minVersion = TLS_VERSION_OPTION.TLS10,
maxVersion = TLS_VERSION_OPTION.TLS12,
cipherSuites = '',
certificates=[new TlsStreamSettings.Cert()],
alpn=[''],
settings=[new TlsStreamSettings.Settings()]) {
super();
this.server = serverName;
this.minVersion = minVersion;
this.maxVersion = maxVersion;
this.cipherSuites = cipherSuites;
this.certs = certificates;
this.alpn = alpn;
this.settings = settings;
}
addCert(cert) {
@@ -434,22 +499,35 @@ class TlsStreamSettings extends XrayCommonClass {
static fromJson(json={}) {
let certs;
let settings;
if (!ObjectUtil.isEmpty(json.certificates)) {
certs = json.certificates.map(cert => TlsStreamSettings.Cert.fromJson(cert));
}
if (!ObjectUtil.isEmpty(json.settings)) {
let values = json.settings[0];
settings = [new TlsStreamSettings.Settings(values.allowInsecure , values.fingerprint, values.serverName)];
}
return new TlsStreamSettings(
json.serverName,
json.minVersion,
json.maxVersion,
json.cipherSuites,
certs,
json.alpn
json.alpn,
settings,
);
}
toJson() {
return {
serverName: this.server,
minVersion: this.minVersion,
maxVersion: this.maxVersion,
cipherSuites: this.cipherSuites,
certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn
alpn: this.alpn,
settings: TlsStreamSettings.toJsonArray(this.settings),
};
}
}
@@ -495,6 +573,29 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
}
};
TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor(allowInsecure = false, fingerprint = '', serverName = '') {
super();
this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint;
this.serverName = serverName;
}
static fromJson(json = {}) {
return new TlsStreamSettings.Settings(
json.allowInsecure,
json.fingerprint,
json.servername,
);
}
toJson() {
return {
allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint,
serverName: this.serverName,
};
}
};
class StreamSettings extends XrayCommonClass {
constructor(network='tcp',
security='none',
@@ -710,7 +811,7 @@ class Inbound extends XrayCommonClass {
case Protocols.VLESS:
return this.settings.vlesses[0].flow;
case Protocols.TROJAN:
return this.settings.clients[0].flow;
return this.settings.trojans[0].flow;
default:
return "";
}
@@ -741,7 +842,7 @@ class Inbound extends XrayCommonClass {
get password() {
switch (this.protocol) {
case Protocols.TROJAN:
return this.settings.clients[0].password;
return this.settings.trojans[0].password;
case Protocols.SHADOWSOCKS:
return this.settings.password;
case Protocols.SOCKS:
@@ -857,6 +958,19 @@ class Inbound extends XrayCommonClass {
}
}
//this is used for xtls-rprx-vision
canEnableTlsFlow() {
if ((this.stream.security === 'tls') && (this.network === "tcp")) {
switch (this.protocol) {
case Protocols.VLESS:
return true;
default:
return false;
}
}
return false;
}
canSetTls() {
return this.canEnableTls();
}
@@ -876,6 +990,7 @@ class Inbound extends XrayCommonClass {
switch (this.protocol) {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
return true;
default:
@@ -965,6 +1080,10 @@ class Inbound extends XrayCommonClass {
host: host,
path: path,
tls: this.stream.security,
sni: this.stream.tls.settings[0]['serverName'],
fp: this.stream.tls.settings[0]['fingerprint'],
alpn: this.stream.tls.alpn.join(','),
allowInsecure: this.stream.tls.settings[0].allowInsecure,
};
return 'vmess://' + base64(JSON.stringify(obj, null, 2));
}
@@ -976,12 +1095,6 @@ class Inbound extends XrayCommonClass {
const type = this.stream.network;
const params = new Map();
params.set("type", this.stream.network);
if (this.xtls) {
params.set("security", "xtls");
address = this.stream.tls.server;
} else {
params.set("security", this.stream.security);
}
switch (type) {
case "tcp":
const tcp = this.stream.tcp;
@@ -1027,14 +1140,33 @@ class Inbound extends XrayCommonClass {
break;
}
if (this.stream.security === 'tls') {
if (this.tls) {
params.set("security", "tls");
params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings[0].allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
params.set("sni", address);
}
if (this.stream.tls.settings[0]['serverName'] !== ''){
params.set("sni", this.stream.tls.settings[0]['serverName']);
}
if (type === "tcp" && this.settings.vlesses[clientIndex].flow.length > 0) {
params.set("flow", this.settings.vlesses[clientIndex].flow);
}
}
if (this.xtls) {
params.set("security", "xtls");
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings[0].allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
}
params.set("flow", this.settings.vlesses[clientIndex].flow);
}
@@ -1053,37 +1185,139 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(server)) {
address = server;
}
return 'ss://' + safeBase64(settings.method + ':' + settings.password + '@' + address + ':' + this.port)
+ '#' + encodeURIComponent(remark);
if (settings.method == SSMethods.BLAKE3_AES_128_GCM || settings.method == SSMethods.BLAKE3_AES_256_GCM || settings.method == SSMethods.BLAKE3_CHACHA20_POLY1305) {
return `ss://${settings.method}:${settings.password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
} else {
return 'ss://' + safeBase64(settings.method + ':' + settings.password + '@' + address + ':' + this.port)
+ '#' + encodeURIComponent(remark);
}
}
genTrojanLink(address='', remark='', clientIndex=0) {
genTrojanLink(address = '', remark = '', clientIndex = 0) {
let settings = this.settings;
return `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
const port = this.port;
const type = this.stream.network;
const params = new Map();
params.set("type", this.stream.network);
switch (type) {
case "tcp":
const tcp = this.stream.tcp;
if (tcp.type === 'http') {
const request = tcp.request;
params.set("path", request.path.join(','));
const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = request.headers[index].value;
params.set("host", host);
}
params.set("headerType", 'http');
}
break;
case "kcp":
const kcp = this.stream.kcp;
params.set("headerType", kcp.type);
params.set("seed", kcp.seed);
break;
case "ws":
const ws = this.stream.ws;
params.set("path", ws.path);
const index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = ws.headers[index].value;
params.set("host", host);
}
break;
case "http":
const http = this.stream.http;
params.set("path", http.path);
params.set("host", http.host);
break;
case "quic":
const quic = this.stream.quic;
params.set("quicSecurity", quic.security);
params.set("key", quic.key);
params.set("headerType", quic.type);
break;
case "grpc":
const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName);
break;
}
if (this.tls) {
params.set("security", "tls");
params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings[0].allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
}
if (this.stream.tls.settings[0]['serverName'] !== ''){
params.set("sni", this.stream.tls.settings[0]['serverName']);
}
}
if (this.xtls) {
params.set("security", "xtls");
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings[0].allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
}
params.set("flow", this.settings.trojans[clientIndex].flow);
}
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
const url = new URL(link);
for (const [key, value] of params) {
url.searchParams.set(key, value)
}
url.hash = encodeURIComponent(remark);
return url.toString();
}
genLink(address='', remark='', clientIndex=0) {
switch (this.protocol) {
case Protocols.VMESS:
if (this.settings.vmesses[clientIndex].email != ""){
remark += '-' + this.settings.vmesses[clientIndex].email
remark = this.settings.vmesses[clientIndex].email
}
return this.genVmessLink(address, remark, clientIndex);
case Protocols.VLESS:
if (this.settings.vlesses[clientIndex].email != ""){
remark += '-' + this.settings.vlesses[clientIndex].email
remark = this.settings.vlesses[clientIndex].email
}
return this.genVLESSLink(address, remark, clientIndex);
case Protocols.SHADOWSOCKS: return this.genSSLink(address, remark);
case Protocols.TROJAN:
if (this.settings.trojans[clientIndex].email != ""){
remark += '-' + this.settings.trojans[clientIndex].email
remark = this.settings.trojans[clientIndex].email
}
return this.genTrojanLink(address, remark, clientIndex);
default: return '';
}
}
genInboundLinks(address = '', remark = '') {
let link = '';
switch (this.protocol) {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
JSON.parse(this.settings).clients.forEach((_,index) => {
link += this.genLink(address, remark, index) + '\r\n';
});
return link;
case Protocols.SHADOWSOCKS:
return (this.genSSLink(address, remark) + '\r\n');
default: return '';
}
}
static fromJson(json={}) {
return new Inbound(
json.port,
@@ -1247,7 +1481,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
fallbacks=[],) {
super(protocol);
this.vlesses = vlesses;
this.decryption = decryption;
this.decryption = 'none'; // Using decryption is not implemented here
this.fallbacks = fallbacks;
}
@@ -1259,11 +1493,12 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
this.fallbacks.splice(index, 1);
}
// decryption should be set to static value
static fromJson(json={}) {
return new Inbound.VLESSSettings(
Protocols.VLESS,
json.clients.map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
json.decryption,
'none',
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks),
);
}
@@ -1279,7 +1514,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
};
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), flow=FLOW_CONTROL.DIRECT, email=RandomUtil.randomText(), totalGB=0, expiryTime='') {
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(), totalGB=0, expiryTime='') {
super();
this.id = id;
this.flow = flow;
@@ -1296,9 +1531,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.email,
json.totalGB,
json.expiryTime,
);
}
}
get _expiryTime() {
if (this.expiryTime === 0 || this.expiryTime === "") {
@@ -1393,7 +1627,7 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
}
};
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
constructor(password=RandomUtil.randomSeq(10), flow=FLOW_CONTROL.DIRECT, email=RandomUtil.randomText(), totalGB=0, expiryTime='') {
constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomText(), totalGB=0, expiryTime='') {
super();
this.password = password;
this.flow = flow;
@@ -1412,7 +1646,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
};
}
static fromJson(json={}) {
static fromJson(json = {}) {
return new Inbound.TrojanSettings.Trojan(
json.password,
json.flow,
@@ -1488,8 +1722,8 @@ Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
Inbound.ShadowsocksSettings = class extends Inbound.Settings {
constructor(protocol,
method=SSMethods.AES_256_GCM,
password=RandomUtil.randomSeq(10),
method=SSMethods.BLAKE3_AES_256_GCM,
password=RandomUtil.randomSeq(44),
network='tcp,udp'
) {
super(protocol);
@@ -1517,11 +1751,12 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
};
Inbound.DokodemoSettings = class extends Inbound.Settings {
constructor(protocol, address, port, network='tcp,udp') {
constructor(protocol, address, port, network='tcp,udp', followRedirect=false) {
super(protocol);
this.address = address;
this.port = port;
this.network = network;
this.followRedirect = followRedirect;
}
static fromJson(json={}) {
@@ -1530,6 +1765,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
json.address,
json.port,
json.network,
json.followRedirect,
);
}
@@ -1538,6 +1774,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
address: this.address,
port: this.port,
network: this.network,
followRedirect: this.followRedirect,
};
}
};

View File

@@ -179,7 +179,7 @@ class ObjectUtil {
}
}
} else {
return obj.toString().indexOf(key) >= 0;
return obj.toString().toLowerCase().indexOf(key.toLowerCase()) >= 0;
}
return false;
}

View File

@@ -3,6 +3,7 @@ package controller
import (
"github.com/gin-gonic/gin"
)
type APIController struct {
BaseController
@@ -26,11 +27,9 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound)
a.inboundController = NewInboundController(g)
}
func (a *APIController) inbounds(c *gin.Context) {
a.inboundController.getInbounds(c)
}

View File

@@ -12,7 +12,7 @@ type BaseController struct {
func (a *BaseController) checkLogin(c *gin.Context) {
if !session.IsLogin(c) {
if isAjax(c) {
pureJsonMsg(c, false, I18n(c , "pages.login.loginAgain"))
pureJsonMsg(c, false, I18n(c, "pages.login.loginAgain"))
} else {
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
}
@@ -22,12 +22,11 @@ func (a *BaseController) checkLogin(c *gin.Context) {
}
}
func I18n(c *gin.Context, name string) string {
anyfunc, _ := c.Get("I18n")
i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
func I18n(c *gin.Context , name string) string{
anyfunc, _ := c.Get("I18n")
i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
message, _ := i18n(name)
message, _ := i18n(name)
return message;
return message
}

View File

@@ -31,7 +31,10 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound)
g.POST("/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/addClient/", a.addInboundClient)
g.POST("/delClient/:email", a.delInboundClient)
g.POST("/updateClient/:index", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
}
@@ -123,13 +126,85 @@ func (a *InboundController) updateInbound(c *gin.Context) {
}
}
func (a *InboundController) addInboundClient(c *gin.Context) {
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
err = a.inboundService.AddInboundClient(inbound)
if err != nil {
jsonMsg(c, "something worng!", err)
return
}
jsonMsg(c, "Client added", nil)
if err == nil {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) delInboundClient(c *gin.Context) {
email := c.Param("email")
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
err = a.inboundService.DelInboundClient(inbound, email)
if err != nil {
jsonMsg(c, "something worng!", err)
return
}
jsonMsg(c, "Client deleted", nil)
if err == nil {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) updateInboundClient(c *gin.Context) {
index, err := strconv.Atoi(c.Param("index"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
inbound := &model.Inbound{}
err = c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
err = a.inboundService.UpdateInboundClient(inbound, index)
if err != nil {
jsonMsg(c, "something worng!", err)
return
}
jsonMsg(c, "Client updated", nil)
if err == nil {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
email := c.Param("email")
err := a.inboundService.ResetClientTraffic(email)
err = a.inboundService.ResetClientTraffic(id, email)
if err != nil {
jsonMsg(c, "something worng!", err)
return
}
jsonMsg(c, "traffic reseted", nil)
if err == nil {
a.xrayService.SetToNeedRestart()
}
}

View File

@@ -4,7 +4,6 @@ import (
"net/http"
"time"
"x-ui/logger"
"x-ui/web/job"
"x-ui/web/service"
"x-ui/web/session"
@@ -20,6 +19,7 @@ type IndexController struct {
BaseController
userService service.UserService
tgbot service.Tgbot
}
func NewIndexController(g *gin.RouterGroup) *IndexController {
@@ -46,7 +46,7 @@ func (a *IndexController) login(c *gin.Context) {
var form LoginForm
err := c.ShouldBind(&form)
if err != nil {
pureJsonMsg(c, false, I18n(c , "pages.login.toasts.invalidFormData"))
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.invalidFormData"))
return
}
if form.Username == "" {
@@ -54,24 +54,24 @@ func (a *IndexController) login(c *gin.Context) {
return
}
if form.Password == "" {
pureJsonMsg(c, false, I18n(c , "pages.login.toasts.emptyPassword"))
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword"))
return
}
user := a.userService.CheckUser(form.Username, form.Password)
timeStr := time.Now().Format("2006-01-02 15:04:05")
if user == nil {
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
pureJsonMsg(c, false, I18n(c , "pages.login.toasts.wrongUsernameOrPassword"))
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
return
} else {
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
}
err = session.SetLoginUser(c, user)
logger.Info("user", user.Id, "login success")
jsonMsg(c, I18n(c , "pages.login.toasts.successLogin"), err)
jsonMsg(c, I18n(c, "pages.login.toasts.successLogin"), err)
}
func (a *IndexController) logout(c *gin.Context) {

View File

@@ -1,10 +1,11 @@
package controller
import (
"github.com/gin-gonic/gin"
"time"
"x-ui/web/global"
"x-ui/web/service"
"github.com/gin-gonic/gin"
)
type ServerController struct {
@@ -34,7 +35,10 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.Use(a.checkLogin)
g.POST("/status", a.status)
g.POST("/getXrayVersion", a.getXrayVersion)
g.POST("/stopXrayService", a.stopXrayService)
g.POST("/restartXrayService", a.restartXrayService)
g.POST("/installXray/:version", a.installXray)
g.POST("/logs", a.getLogs)
}
func (a *ServerController) refreshStatus() {
@@ -68,7 +72,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
versions, err := a.serverService.GetXrayVersions()
if err != nil {
jsonMsg(c, I18n(c , "getVersion"), err)
jsonMsg(c, I18n(c, "getVersion"), err)
return
}
@@ -81,5 +85,34 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
func (a *ServerController) installXray(c *gin.Context) {
version := c.Param("version")
err := a.serverService.UpdateXray(version)
jsonMsg(c, I18n(c , "install") + " xray", err)
jsonMsg(c, I18n(c, "install")+" xray", err)
}
func (a *ServerController) stopXrayService(c *gin.Context) {
a.lastGetStatusTime = time.Now()
err := a.serverService.StopXrayService()
if err != nil {
jsonMsg(c, "", err)
return
}
jsonMsg(c, "Xray stoped", err)
}
func (a *ServerController) restartXrayService(c *gin.Context) {
err := a.serverService.RestartXrayService()
if err != nil {
jsonMsg(c, "", err)
return
}
jsonMsg(c, "Xray restarted", err)
}
func (a *ServerController) getLogs(c *gin.Context) {
logs, err := a.serverService.GetLogs()
if err != nil {
jsonMsg(c, I18n(c, "getLogs"), err)
return
}
jsonObj(c, logs, nil)
}

View File

@@ -2,11 +2,12 @@ package controller
import (
"errors"
"github.com/gin-gonic/gin"
"time"
"x-ui/web/entity"
"x-ui/web/service"
"x-ui/web/session"
"github.com/gin-gonic/gin"
)
type updateUserForm struct {
@@ -40,7 +41,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
func (a *SettingController) getAllSetting(c *gin.Context) {
allSetting, err := a.settingService.GetAllSetting()
if err != nil {
jsonMsg(c, I18n(c , "pages.setting.toasts.getSetting"), err)
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
return
}
jsonObj(c, allSetting, nil)
@@ -50,27 +51,27 @@ func (a *SettingController) updateSetting(c *gin.Context) {
allSetting := &entity.AllSetting{}
err := c.ShouldBind(allSetting)
if err != nil {
jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
return
}
err = a.settingService.UpdateAllSetting(allSetting)
jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
}
func (a *SettingController) updateUser(c *gin.Context) {
form := &updateUserForm{}
err := c.ShouldBind(form)
if err != nil {
jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
return
}
user := session.GetLoginUser(c)
if user.Username != form.OldUsername || user.Password != form.OldPassword {
jsonMsg(c, I18n(c , "pages.setting.toasts.modifyUser"), errors.New(I18n(c , "pages.setting.toasts.originalUserPassIncorrect")))
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.originalUserPassIncorrect")))
return
}
if form.NewUsername == "" || form.NewPassword == "" {
jsonMsg(c,I18n(c , "pages.setting.toasts.modifyUser"), errors.New(I18n(c , "pages.setting.toasts.userPassMustBeNotEmpty")))
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.userPassMustBeNotEmpty")))
return
}
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
@@ -79,10 +80,10 @@ func (a *SettingController) updateUser(c *gin.Context) {
user.Password = form.NewPassword
session.SetLoginUser(c, user)
}
jsonMsg(c, I18n(c , "pages.setting.toasts.modifyUser"), err)
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), err)
}
func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3)
jsonMsg(c, I18n(c , "pages.setting.restartPanel"), err)
jsonMsg(c, I18n(c, "pages.setting.restartPanel"), err)
}

View File

@@ -46,12 +46,12 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
if err == nil {
m.Success = true
if msg != "" {
m.Msg = msg + I18n(c , "success")
m.Msg = msg + I18n(c, "success")
}
} else {
m.Success = false
m.Msg = msg + I18n(c , "fail") + ": " + err.Error()
logger.Warning(msg + I18n(c , "fail") + ": ", err)
m.Msg = msg + I18n(c, "fail") + ": " + err.Error()
logger.Warning(msg+I18n(c, "fail")+": ", err)
}
c.JSON(http.StatusOK, m)
}

View File

@@ -34,11 +34,14 @@ type AllSetting struct {
WebBasePath string `json:"webBasePath" form:"webBasePath"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
TgBotChatId int `json:"tgBotChatId" form:"tgBotChatId"`
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgExpireDiff int `json:"tgExpireDiff" form:"tgExpireDiff"`
TgTrafficDiff int `json:"tgTrafficDiff" form:"tgTrafficDiff"`
TgCpu int `json:"tgCpu" form:"tgCpu"`
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
}
func (s *AllSetting) CheckValid() error {

View File

@@ -1,6 +1,7 @@
{{define "promptModal"}}
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
:closable="true" @ok="promptModal.ok" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
<a-input id="prompt-modal-input" :type="promptModal.type"
v-model="promptModal.value"

View File

@@ -1,8 +1,11 @@
{{define "qrcodeModal"}}
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
:closable="true" width="300px" :ok-text="qrModal.okText"
cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}">
<canvas id="qrCode" style="width: 100%; height: 100%;"></canvas>
:closable="true"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:footer="null"
width="300px">
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas>
</a-modal>
<script>
@@ -12,17 +15,15 @@
content: '',
inbound: new Inbound(),
dbInbound: new DBInbound(),
okText: '',
copyText: '',
qrcode: null,
clipboard: null,
visible: false,
show: function (title='', content='', dbInbound=new DBInbound(),okText='{{ i18n "copy" }}', copyText='') {
show: function (title='', content='', dbInbound=new DBInbound(), copyText='') {
this.title = title;
this.content = content;
this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound();
this.okText = okText;
if (ObjectUtil.isEmpty(copyText)) {
this.copyText = content;
} else {
@@ -30,12 +31,6 @@
}
this.visible = true;
qrModalApp.$nextTick(() => {
if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#qr-modal-ok-btn', {
text: () => this.copyText,
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
}
if (this.qrcode === null) {
this.qrcode = new QRious({
element: document.querySelector('#qrCode'),
@@ -57,6 +52,17 @@
data: {
qrModal: qrModal,
},
methods: {
copyToClipboard() {
this.qrModal.clipboard = new ClipboardJS('#qrCode', {
text: () => this.qrModal.copyText,
});
this.qrModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}')
this.qrModal.clipboard.destroy();
});
}
},
});
</script>

View File

@@ -1,9 +1,10 @@
{{define "textModal"}}
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
@click="downloader.download(txtModal.fileName, txtModal.content)">
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName">
{{ i18n "download" }} [[ txtModal.fileName ]]
</a-button>
<a-input type="textarea" v-model="txtModal.content"
@@ -31,15 +32,6 @@
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
}
if (this.qrcode === null) {
this.qrcode = new QRious({
element: document.querySelector('#qrCode'),
size: 260,
value: content,
});
} else {
this.qrcode.value = content;
}
});
},
close: function () {
@@ -48,6 +40,7 @@
};
const textModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#text-modal',
data: {
txtModal: txtModal,

View File

@@ -0,0 +1,162 @@
{{define "clientsBulkModal"}}
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.client.method" }}'>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option :value="0">Random</a-select-option>
<a-select-option :value="1">Random+Prefix</a-select-option>
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
<a-select-option :value="4">Random+Prefix+Num@Telegram Username</a-select-option>
<a-select-option :value="5">Prefix+Num+Postfix [ BE CAREFUL! ]</a-select-option>
</a-select>
</a-form-item><br />
<a-form-item v-if="clientsBulkModal.emailMethod>1">
<span slot="label">{{ i18n "pages.client.first" }}</span>
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod>1">
<span slot="label">{{ i18n "pages.client.last" }}</span>
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod>0">
<span slot="label">{{ i18n "pages.client.prefix" }}</span>
<a-input v-model="clientsBulkModal.emailPrefix" style="width: 120px"></a-input>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod>2">
<span slot="label" v-if="clientsBulkModal.emailMethod == 4">tg_uname</span>
<span slot="label" v-else>{{ i18n "pages.client.postfix" }}</span>
<a-input v-model="clientsBulkModal.emailPostfix" style="width: 120px"></a-input>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod < 2">
<span slot="label">{{ i18n "pages.client.clientCount" }}</span>
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
</a-form>
</a-modal>
<script>
const clientsBulkModal = {
visible: false,
confirmLoading: false,
title: '',
okText: '',
confirm: null,
dbInbound: new DBInbound(),
inbound: new Inbound(),
clients: [],
quantity: 1,
totalGB: 0,
expiryTime: '',
emailMethod: 0,
firstNum: 1,
lastNum: 1,
emailPrefix: "",
emailPostfix: "",
ok() {
method=clientsBulkModal.emailMethod;
if(method>1){
start=clientsBulkModal.firstNum;
end=clientsBulkModal.lastNum + 1;
} else {
start=0;
end=clientsBulkModal.quantity;
}
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? clientsBulkModal.emailPrefix : "";
useNum=(method>1);
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? (method == 4 ? "@" : "") + clientsBulkModal.emailPostfix : "";
for (let i = start; i < end; i++) {
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
if(method==5) newClient.email = "";
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
newClient._totalGB = clientsBulkModal.totalGB;
newClient._expiryTime = clientsBulkModal.expiryTime;
clientsBulkModal.clients.push(newClient);
}
ObjectUtil.execute(clientsBulkModal.confirm, clientsBulkModal.inbound, clientsBulkModal.dbInbound);
},
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
this.visible = true;
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.quantity = 1;
this.totalGB = 0;
this.expiryTime = '';
this.emailMethod= 0;
this.firstNum= 1;
this.lastNum= 1;
this.emailPrefix= "";
this.emailPostfix= "";
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
},
getClients(protocol, clientSettings) {
switch(protocol){
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
default: return null;
}
},
newClient(protocol) {
switch (protocol) {
case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
default: return null;
}
},
close() {
clientsBulkModal.visible = false;
clientsBulkModal.loading(false);
},
loading(loading) {
clientsBulkModal.confirmLoading = loading;
},
};
const clientsBulkModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#client-bulk-modal',
data: {
clientsBulkModal,
get inbound() {
return this.clientsBulkModal.inbound;
},
},
});
</script>
{{end}}

View File

@@ -0,0 +1,111 @@
{{define "clientsModal"}}
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/client"}}
</a-modal>
<script>
const clientModal = {
visible: false,
confirmLoading: false,
title: '',
okText: '',
dbInbound: new DBInbound(),
inbound: new Inbound(),
clients: [],
clientStats: [],
index: null,
isExpired: false,
ok() {
ObjectUtil.execute(clientModal.confirm, clientModal.inbound, clientModal.dbInbound, clientModal.index);
},
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=(index, dbInbound)=>{}, isEdit=false }) {
this.visible = true;
this.title = title;
this.okText = okText;
this.isEdit = isEdit;
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
this.index = index === null ? this.clients.length : index;
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
if (!isEdit){
this.addClient(this.inbound.protocol, this.clients);
}
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm;
},
getClients(protocol, clientSettings) {
switch(protocol){
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
default: return null;
}
},
addClient(protocol, clients) {
switch (protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
default: return null;
}
},
close() {
clientModal.visible = false;
clientModal.loading(false);
},
loading(loading) {
clientModal.confirmLoading = loading;
},
};
const clientModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#client-modal',
data: {
clientModal,
get inbound() {
return this.clientModal.inbound;
},
get client() {
return this.clientModal.clients[this.clientModal.index];
},
get clientStats() {
return this.clientModal.clientStats;
},
get isEdit() {
return this.clientModal.isEdit;
},
get isTrafficExhausted() {
if(!clientStats) return false
if(clientStats.total == 0) return false
if(clientStats.up + clientStats.down < clientStats.total) return false
return true
},
get isExpiry() {
return this.clientModal.isExpired
},
get statsColor() {
if(!clientStats) return 'blue'
if(clientStats.total === 0) return 'blue'
else if(clientStats.total > 0 && (clientStats.down+clientStats.up) < clientStats.total) return 'cyan'
else return 'red'
}
},
methods: {
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string;
},
},
});
</script>
{{end}}

View File

@@ -33,27 +33,50 @@
{{define "commonSider"}}
<a-layout-sider id="sider" collapsible breakpoint="md" collapsed-width="0">
<a-menu theme="dark" mode="inline" :selected-keys="['{{ .request_uri }}']"
<a-layout-sider :theme="siderDrawer.theme" id="sider" collapsible breakpoint="md" collapsed-width="0">
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
<a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item>
</a-menu>
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}}
</a-menu>
</a-layout-sider>
<a-drawer id="sider-drawer" placement="left" :closable="false"
@close="siderDrawer.close()"
:visible="siderDrawer.visible" :wrap-style="{ padding: 0 }">
:visible="siderDrawer.visible"
:wrap-class-name="siderDrawer.isDarkTheme ? 'ant-drawer-dark' : ''"
:wrap-style="{ padding: 0 }">
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
</div>
<a-menu theme="light" mode="inline" :selected-keys="['{{ .request_uri }}']"
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
<a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item>
</a-menu>
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}}
</a-menu>
</a-drawer>
<script>
const darkClass = "ant-card-dark";
const bgDarkStyle = "background-color: #242c3a";
const siderDrawer = {
visible: false,
collapsed: false,
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
show() {
this.visible = true;
},
@@ -62,6 +85,16 @@
},
change() {
this.visible = !this.visible;
},
toggleCollapsed() {
this.collapsed = !this.collapsed;
},
changeTheme() {
this.isDarkTheme = ! this.isDarkTheme;
localStorage.setItem("dark-mode", this.isDarkTheme);
},
get theme() {
return this.isDarkTheme ? 'dark' : 'light';
}
};

View File

@@ -0,0 +1,75 @@
{{define "form/client"}}
<a-form layout="inline" v-if="client">
<template v-if="isEdit">
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
</template>
<a-form-item>
<span slot="label">
Email
<a-tooltip>
<template slot="title">
The email must be completely unique
</template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.email"></a-input>
</a-form-item>
<a-form-item label="password" v-if="inbound.protocol === Protocols.TROJAN">
<a-input v-model.trim="client.password"></a-input>
</a-form-item>
<a-form-item label="id" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
<a-input v-model.trim="client.id"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
<a-input type="number" v-model.number="client.alterId"></a-input>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="flow">
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="flow" layout="inline">
<a-select v-model="client.flow" style="width: 150px">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
<template v-if="isEdit && clientStats">
<span>{{ i18n "usage" }}:</span>
<a-tag :color="statsColor">
[[ sizeFormat(clientStats.up) ]] /
[[ sizeFormat(clientStats.down) ]]
([[ sizeFormat(clientStats.up + clientStats.down) ]])
</a-tag>
</template>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
</a-form-item>
</a-form>
{{end}}

View File

@@ -8,7 +8,7 @@
<a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="inbound.protocol" style="width: 160px;">
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
</a-select>
</a-form-item>
@@ -50,6 +50,7 @@
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
</a-form>

View File

@@ -7,11 +7,14 @@
<a-input type="number" v-model.number="inbound.settings.port"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
<a-select v-model="inbound.settings.network" style="width: 100px;">
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="FollowRedirect">
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
</a-form-item>
</a-form>
{{end}}

View File

@@ -1,7 +1,7 @@
{{define "form/shadowsocks"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="inbound.settings.method" style="width: 165px;">
<a-select v-model="inbound.settings.method" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select>
</a-form-item>
@@ -9,7 +9,7 @@
<a-input v-model.trim="inbound.settings.password"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select v-model="inbound.settings.network" style="width: 100px;">
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option>

View File

@@ -1,11 +1,7 @@
{{define "form/trojan"}}
<a-form layout="inline">
<label>{{ i18n "clients"}} </label>
<a-collapse activeKey="0" v-for="(trojan, index) in inbound.settings.trojans"
:key="`trojan-${index}`">
<a-collapse-panel :class="getHeaderStyle(trojan.email)" :header="getHeaderText(trojan.email)">
<a-tag v-if="isExpiry(index) || ((getUpStats(trojan.email) + getDownStats(trojan.email)) > trojan.totalGB && trojan.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
<a-form layout="inline">
<a-form-item>
<span slot="label">
@@ -14,22 +10,19 @@
<template slot="title">
The email must be completely unique
</template>
<!--Renew Svg Icon-->
<svg
@click="getNewEmail(trojan)"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="trojan.email"></a-input>
<a-input v-model.trim="client.email"></a-input>
</a-form-item>
</a-form>
<a-form-item label="password">
<a-input v-model.trim="trojan.password"></a-input>
<a-input v-model.trim="client.password"></a-input>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="flow">
<a-select v-model="trojan.flow" style="width: 150px">
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
@@ -42,7 +35,7 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="trojan._totalGB" :min="0"></a-input-number>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
@@ -55,89 +48,58 @@
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
v-model="trojan._expiryTime" style="width: 300px;"></a-date-picker>
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
<a-form layout="inline">
<a-tooltip v-if="trojan._totalGB > 0">
<template slot="title">
{{ i18n "pages.inbounds.resetTraffic" }}
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="resetClientTraffic(trojan,$event)"></a-icon>
</span>
</a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(trojan.email)) ]] / [[ sizeFormat(getDownStats(trojan.email)) ]]</a-tag>
<a-tag v-if="trojan._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(trojan.email) + getDownStats(trojan.email)) ]]</a-tag>
<a-tag v-show="inbound.settings.trojans.length > 1">
<svg
v-show="inbound.settings.trojans.length > 1"
@click="removeClient(index, inbound.settings.trojans)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
class="ml-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="#EC4899"
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
/>
</svg>
</a-tag>
</a-form>
</a-collapse-panel>
</a-collapse>
<a-tag>
<svg
@click="addClient(inbound.protocol, inbound.settings.trojans)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
class="ml-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="green"
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
/>
</svg>
</a-tag>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp && (inbound.tls || inbound.xtls)">
<a-form layout="inline">
<a-form-item label="Fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addTrojanFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<a-form layout="inline">
<a-form-item label="fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addTrojanFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<!-- trojan fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider>
fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label="name">
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label="alpn">
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label="path">
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label="dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xver">
<a-input type="number" v-model.number="fallback.xver"></a-input>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form>
<!-- trojan fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider>
fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label="name">
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label="alpn">
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label="path">
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label="dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xver">
<a-input type="number" v-model.number="fallback.xver"></a-input>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form>
</template>
{{end}}

View File

@@ -1,12 +1,7 @@
{{define "form/vless"}}
<a-form layout="inline">
<label>{{ i18n "clients"}}</label>
<a-collapse activeKey="0" v-for="(vless, index) in inbound.settings.vlesses"
:key="`vless-${index}`">
<a-collapse-panel :class="getHeaderStyle(vless.email)" :header="getHeaderText(vless.email)">
<a-tag v-if="isExpiry(index) || ((getUpStats(vless.email) + getDownStats(vless.email)) > vless.totalGB && vless.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
<a-form layout="inline">
<a-form-item>
<span slot="label">
@@ -15,22 +10,25 @@
<template slot="title">
The email must be completely unique
</template>
<!--Renew Svg Icon-->
<svg
@click="getNewEmail(vless)"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="vless.email"></a-input>
<a-input v-model.trim="client.email"></a-input>
</a-form-item>
</a-form>
<a-form-item label="id">
<a-input v-model.trim="vless.id"></a-input>
<a-input v-model.trim="client.id"></a-input>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="flow">
<a-select v-model="vless.flow" style="width: 150px">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
<a-form-item v-if="inbound.xtls" label="flow">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="flow" layout="inline">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
@@ -43,7 +41,7 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="vless._totalGB" :min="0"></a-input-number>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
@@ -56,89 +54,58 @@
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
v-model="vless._expiryTime" style="width: 300px;"></a-date-picker>
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
<a-form layout="inline">
<a-tooltip v-if="vless._totalGB > 0">
<template slot="title">
{{ i18n "pages.inbounds.resetTraffic" }}
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="resetClientTraffic(vless,$event)"></a-icon>
</span>
</a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(vless.email)) ]] / [[ sizeFormat(getDownStats(vless.email)) ]]</a-tag>
<a-tag v-if="vless._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vless.email) + getDownStats(vless.email)) ]]</a-tag>
<a-tag v-show="inbound.settings.vlesses.length > 1">
<svg
@click="removeClient(index, inbound.settings.vlesses)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 22 22"
width="22"
height="22"
class="mt-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="#EC4899"
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
/>
</svg>
</a-tag>
</a-form>
</a-collapse-panel>
</a-collapse>
<a-tag>
<svg
@click="addClient(inbound.protocol, inbound.settings.vlesses)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
class="ml-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="green"
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
/>
</svg>
</a-tag>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp && (inbound.tls || inbound.xtls)">
<a-form layout="inline">
<a-form-item label="Fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<a-form layout="inline">
<a-form-item label="fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider>
fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label="name">
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label="alpn">
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label="path">
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label="dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xver">
<a-input type="number" v-model.number="fallback.xver"></a-input>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form>
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider>
fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label="name">
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label="alpn">
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label="path">
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label="dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xver">
<a-input type="number" v-model.number="fallback.xver"></a-input>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form>
</template>
{{end}}

View File

@@ -1,11 +1,7 @@
{{define "form/vmess"}}
<a-form layout="inline">
<label>{{ i18n "clients"}}</label>
<a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses"
:key="`vmess-${index}`">
<a-collapse-panel :class="getHeaderStyle(vmess.email)" :header="getHeaderText(vmess.email)">
<a-tag v-if="isExpiry(index) || ((getUpStats(vmess.email) + getDownStats(vmess.email)) > vmess.totalGB && vmess.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
<a-form layout="inline">
<a-form-item>
<span slot="label">
@@ -14,20 +10,17 @@
<template slot="title">
The email must be completely unique
</template>
<!--Renew Svg Icon-->
<svg
@click="getNewEmail(vmess)"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="vmess.email"></a-input>
<a-input v-model.trim="client.email"></a-input>
</a-form-item>
</a-form>
<a-form-item label="id">
<a-input v-model.trim="vmess.id"></a-input>
<a-input v-model.trim="client.id"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "additional" }} ID'>
<a-input type="number" v-model.number="vmess.alterId"></a-input>
<a-input type="number" v-model.number="client.alterId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
@@ -39,7 +32,7 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="vmess._totalGB" :min="0"></a-input-number>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
@@ -52,60 +45,23 @@
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
v-model="vmess._expiryTime" style="width: 300px;"></a-date-picker>
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
<a-form layout="inline">
<a-tooltip v-if="vmess._totalGB > 0">
<template slot="title">
{{ i18n "pages.inbounds.resetTraffic" }}
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="resetClientTraffic(vmess,$event)"></a-icon>
</span>
</a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]]</a-tag>
<a-tag v-if="vmess._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vmess.email) + getDownStats(vmess.email)) ]]</a-tag>
<a-tag v-show="inbound.settings.vmesses.length > 1">
<!--Remove Svg Icon-->
<svg
@click="removeClient(index, inbound.settings.vmesses)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 22 22"
width="22"
height="22"
class="mt-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="#EC4899"
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
/>
</svg>
</a-tag>
</a-form>
</a-collapse-panel>
</a-collapse>
<a-tag>
<svg
@click="addClient(inbound.protocol, inbound.settings.vmesses)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
class="ml-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="green"
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
/>
</svg>
</a-tag>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vmesses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>

View File

@@ -1,7 +1,7 @@
{{define "form/streamKCP"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.kcp.type" style="width: 280px;">
<a-select v-model="inbound.stream.kcp.type" style="width: 280px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="none">nonenot camouflage</a-select-option>
<a-select-option value="srtp">srtpcamouflage video call</a-select-option>
<a-select-option value="utp">utpcamouflage BT download</a-select-option>

View File

@@ -1,7 +1,7 @@
{{define "form/streamQUIC"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
<a-select v-model="inbound.stream.quic.security" style="width: 165px;">
<a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
@@ -11,7 +11,7 @@
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.quic.type" style="width: 280px;">
<a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="none">nonenot camouflage</a-select-option>
<a-select-option value="srtp">srtpcamouflage video call</a-select-option>
<a-select-option value="utp">utpcamouflage BT download</a-select-option>

View File

@@ -2,7 +2,7 @@
<!-- select stream network -->
<a-form layout="inline">
<a-form-item label='{{ i18n "transmission" }}'>
<a-select v-model="inbound.stream.network" @change="streamNetworkChange">
<a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="kcp">kcp</a-select-option>
<a-select-option value="ws">ws</a-select-option>

View File

@@ -11,43 +11,64 @@
</a-form>
<!-- tls settings -->
<a-form v-if="inbound.tls || inbound.xtls"
layout="inline">
<a-form v-if="inbound.tls || inbound.xtls" layout="inline">
<a-form-item label="SNI" placeholder="Server Name Indication" v-if="inbound.tls">
<a-input v-model.trim="inbound.stream.tls.settings[0].serverName"></a-input>
</a-form-item>
<a-form-item label="CipherSuites">
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px">
<a-select-option value="">auto</a-select-option>
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="MinVersion">
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="MaxVersion">
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="uTLS" v-if="inbound.tls" >
<a-select v-model="inbound.stream.tls.settings[0].fingerprint" style="width: 135px">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
</a-form-item>
<a-form-item label="alpn" placeholder="http/1.1,h2">
<a-input v-model.trim="inbound.stream.tls.alpn"></a-input>
<a-form-item label="Alpn">
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.tls.settings[0].allowInsecure"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile"
button-style="solid">
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
</a-form-item>
<template v-if="inbound.stream.tls.certs[0].useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile"></a-input>
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile"></a-input>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="2"
v-model="inbound.stream.tls.certs[0].cert"></a-input>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="2"
v-model="inbound.stream.tls.certs[0].key"></a-input>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].key"></a-input>
</a-form-item>
</template>
</a-form>
<a-form layout="inline" v-else-if = "inbound.stream.network === 'tcp' ">
<a-form-item label="tcp-acceptProxyProtocol">
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
</a-form-item>
</a-form>
{{end}}

View File

@@ -1,15 +1,25 @@
{{define "client_table"}}
<template slot="actions" slot-scope="text, client, index">
<a-dropdown>
<a-icon @click="e => e.preventDefault()" type="menu"></a-icon>
<template #overlay>
<a-menu>
<a-menu-item v-if="record.hasLink()" @click="showQrcode(record,index);"><a-icon type="qrcode"></a-icon>{{ i18n "qrCode" }}</a-menu-item>
<a-menu-item @click="showInfo(record,index);"><a-icon type="info-circle"></a-icon>{{ i18n "info" }}</a-menu-item>
<a-menu-item @click="resetClientTraffic(client,record,$event)" v-if="client.email != ''"><a-icon type="retweet"></a-icon>{{ i18n "pages.inbounds.resetTraffic" }}</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-tooltip>
<template slot="title">{{ i18n "qrCode" }}</template>
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "pages.client.edit" }}</template>
<a-icon style="font-size: 24px;" type="edit" @click="openEditClient(record.id,client);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "info" }}</template>
<a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template>
<a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon>
</a-tooltip>
</template>
<template slot="client" slot-scope="text, client">
[[ client.email ]]

View File

@@ -3,6 +3,7 @@
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
:closable="true"
:mask-closable="true"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:footer="null"
width="600px"
>
@@ -43,7 +44,7 @@
</template>
</table>
</td></tr>
<tr colspan="2">
<tr colspan="2" v-if="dbInbound.hasLink()">
<td v-if="inbound.tls">
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
@@ -56,20 +57,18 @@
</td>
</tr>
</table>
<template v-if="infoModal.clientSettings">
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<table style="margin-bottom: 10px; width: 100%;">
<tr><th>[[ Object.keys(infoModal.clientSettings)[0] ]]</th><th>[[ Object.keys(infoModal.clientSettings)[1] ]]</th><th>[[ Object.keys(infoModal.clientSettings)[2] ]]</th></tr>
<tr>
<td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[0] ]]</a-tag></td>
<td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[1] ]]</a-tag></td>
<td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[2] ]]</a-tag></td>
</tr>
<table style="margin-bottom: 10px;">
<tr v-for="col,index in Object.keys(infoModal.clientSettings).slice(0, 3)">
<td>[[ col ]]</td>
<td><a-tag color="green">[[ infoModal.clientSettings[col] ]]</a-tag></td>
</table>
<table style="margin-bottom: 10px; width: 100%;">
<tr><th>{{ i18n "usage" }}</th><th>{{ i18n "pages.inbounds.totalFlow" }}</th><th>{{ i18n "pages.inbounds.expireDate" }}</th><th>{{ i18n "enable" }}</th></tr>
<tr>
<td>
<a-tag :color="statsColor(infoModal.clientStats)">
<a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)">
[[ sizeFormat(infoModal.clientStats['up']) ]] /
[[ sizeFormat(infoModal.clientStats['down']) ]]
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
@@ -88,11 +87,72 @@
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</td>
<td>
<a-tag v-if="infoModal.clientStats.enable" color="blue">{{ i18n "enabled" }}</a-tag>
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
</td>
</tr>
</table>
</template>
<template v-else>
<a-divider></a-divider>
<table v-if="inbound.protocol == Protocols.SHADOWSOCKS" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "encryption" }}</th>
<th>{{ i18n "password" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
</tr>
</table>
<table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th>
<th>FollowRedirect</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.followRedirect ]]</a-tag></td>
</tr>
</table>
</table>
<table v-if="inbound.protocol == Protocols.SOCKS" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
<th>IP</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
</tr><tr v-if="inbound.settings.auth == 'password'">
<td> </td>
<td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td>
</tr><tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</table>
</table>
<table v-if="inbound.protocol == Protocols.HTTP" style="margin-bottom: 10px; width: 100%;">
<tr>
<th> </th>
<th>{{ i18n "username" }}</th>
<th>{{ i18n "password" }}</th>
</tr><tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</table>
</table>
</template>
<div v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider>
<p>[[ infoModal.link ]]</p>
@@ -105,32 +165,24 @@
visible: false,
inbound: new Inbound(),
dbInbound: new DBInbound(),
settings: null,
clientSettings: new Inbound.Settings(),
clientStats: [],
upStats: 0,
downStats: 0,
clipboard: null,
link: null,
index: 0,
index: null,
isExpired: false,
show(dbInbound, index=0) {
show(dbInbound, index) {
this.index = index;
this.inbound = dbInbound.toInbound();
this.dbInbound = new DBInbound(dbInbound);
this.link = dbInbound.genLink(index);
this.clientSettings = Object.values(JSON.parse(this.inbound.settings).clients)[index];
this.clientStats = dbInbound.clientStats;
this.settings = JSON.parse(this.inbound.settings);
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
this.isExpired = this.inbound.isExpiry(index);
if(dbInbound.clientStats.length > 0)
{
for (const key in dbInbound.clientStats) {
if (Object.hasOwnProperty.call(dbInbound.clientStats, key)) {
if(dbInbound.clientStats[key]['email'] == this.clientSettings.email)
this.clientStats = dbInbound.clientStats[key];
}
}
}
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
this.visible = true;
infoModalApp.$nextTick(() => {
if (this.clipboard === null) {
@@ -156,18 +208,15 @@
},
get inbound() {
return this.infoModal.inbound;
},
get isEnable() {
if(infoModal.clientStats){
return infoModal.clientStats.enable;
}
return infoModal.dbInbound.isEnable;
}
},
methods: {
setQrCode(elmentId,index) {
content = infoModal.inbound.genLink(infoModal.dbInbound.address,infoModal.dbInbound.remark,index)
new QRious({
element: document.querySelector('#'+elmentId),
size: 260,
value: content,
});
},
copyTextToClipboard(elmentId,content) {
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
text: () => content,
@@ -178,6 +227,7 @@
});
},
statsColor(stats) {
if(!stats) return 'blue'
if(stats['total'] === 0) return 'blue'
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan'
else return 'red'

View File

@@ -1,6 +1,7 @@
{{define "inboundModal"}}
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/inbound"}}
</a-modal>
@@ -88,87 +89,21 @@
removeClient(index, clients) {
clients.splice(index, 1);
},
async resetClientTraffic(client,event) {
const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email);
if (!msg.success) {
return;
}
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == client.email){
clientStats[key]['up'] = 0
clientStats[key]['down'] = 0
}
}
}
}
},
isExpiry(index) {
return this.inbound.isExpiry(index)
},
getUpStats(email) {
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['up']
}
}
}
},
getDownStats(email) {
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['down']
}
}
}
},
isClientEnable(email) {
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['enable']
}
}
}
clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
return clientStats ? clientStats['enable'] : true
},
getHeaderText(email) {
if(email == "")
return "Add Client"
return email + (this.isClientEnable(email) == true ? ' Active' : ' Deactive')
},
getHeaderStyle(email) {
return (this.isClientEnable(email) == true ? '' : 'deactive-client')
},
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 6 + Math.floor(Math.random() * 5)
var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string + "@gmail.com"
client.email = string;
}
},
});

View File

@@ -15,7 +15,7 @@
<body>
<a-layout id="app" v-cloak>
{{ template "commonSider" . }}
<a-layout id="content-layout">
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading">
<transition name="list" appear>
@@ -24,7 +24,7 @@
</a-tag>
</transition>
<transition name="list" appear>
<a-card hoverable style="margin-bottom: 20px;">
<a-card hoverable style="margin-bottom: 20px;" :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row>
<a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.totalDownUp" }}:
@@ -38,13 +38,20 @@
{{ i18n "pages.inbounds.inboundCount" }}:
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
{{ i18n "clients" }}:
<a-tag color="green">[[ total.clients ]]</a-tag>
<a-tag color="blue">{{ i18n "enabled" }} [[ total.active ]]</a-tag>
<a-tag color="red">{{ i18n "disabled" }} [[ total.deactive ]]</a-tag>
</a-col>
</a-row>
</a-card>
</transition>
<transition name="list" appear>
<a-card hoverable>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
</div>
<a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
@@ -56,7 +63,7 @@
<template slot="action" slot-scope="text, dbInbound">
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="menu"></a-icon>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)">
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme" style="border: 1px solid rgba(255, 255, 255, 0.65);">
<a-menu-item v-if="dbInbound.isSS" key="qrcode">
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
@@ -65,6 +72,26 @@
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
<a-menu-item key="addClient">
<a-icon type="user"></a-icon>
{{ i18n "pages.client.add"}}
</a-menu-item>
<a-menu-item key="addBulkClient">
<a-icon type="team"></a-icon>
{{ i18n "pages.client.bulk"}}
</a-menu-item>
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}}
</a-menu-item>
</template>
<template v-else>
<a-menu-item key="showInfo">
<a-icon type="info-circle"></a-icon>
{{ i18n "info"}}
</a-menu-item>
</template>
<a-menu-item key="resetTraffic">
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
</a-menu-item>
@@ -87,16 +114,16 @@
</template>
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
</template>
<template slot="stream" slot-scope="text, dbInbound, index">
<template slot="stream" slot-scope="text, dbInbound">
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag color="green">[[ inbounds[index].stream.network ]]</a-tag>
<a-tag v-if="inbounds[index].stream.isTls" color="blue">tls</a-tag>
<a-tag v-if="inbounds[index].stream.isXTls" color="blue">xtls</a-tag>
<a-tag color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag v-if="dbInbound.toInbound().stream.isTls" color="blue">tls</a-tag>
<a-tag v-if="dbInbound.toInbound().stream.isXTls" color="blue">xtls</a-tag>
</template>
<template v-else>{{ i18n "none" }}</template>
</template>
<template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch>
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
</template>
<template slot="expiryTime" slot-scope="text, dbInbound">
<template v-if="dbInbound.expiryTime > 0">
@@ -128,15 +155,6 @@
>
{{template "client_table"}}
</a-table>
<a-table
v-else
:row-key="client => client.id"
:columns="innerOneColumns"
:data-source="record"
:pagination="false"
>
{{template "client_table"}}
</a-table>
</template>
</a-table>
</a-card>
@@ -177,11 +195,11 @@
title: '{{ i18n "pages.inbounds.port" }}',
align: 'center',
dataIndex: "port",
width: 30,
width: 50,
}, {
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
align: 'center',
width: 150,
width: 120,
scopedSlots: { customRender: 'traffic' },
}, {
title: '{{ i18n "pages.inbounds.transportConfig" }}',
@@ -196,23 +214,19 @@
}];
const innerColumns = [
{ title: '', width: 20, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 80, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 80, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'UID', width: 150, dataIndex: "id" },
{ title: 'UID', width: 120, dataIndex: "id" },
];
const innerTrojanColumns = [
{ title: '', width: 20, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 80, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 80, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'Password', width: 150, dataIndex: "password" },
];
const innerOneColumns = [
{ title: '', width: 50, scopedSlots: { customRender: 'actions' } },
{ title: 'Password', width: 120, dataIndex: "password" },
];
const app = new Vue({
@@ -238,6 +252,7 @@
return;
}
this.setInbounds(msg.obj);
this.searchKey = '';
},
setInbounds(dbInbounds) {
this.inbounds.splice(0);
@@ -278,14 +293,26 @@
case "qrcode":
this.showQrcode(dbInbound);
break;
case "showInfo":
this.showInfo(dbInbound);
break;
case "edit":
this.openEditInbound(dbInbound);
this.openEditInbound(dbInbound.id);
break;
case "addClient":
this.openAddClient(dbInbound.id)
break;
case "addBulkClient":
this.openAddBulkClient(dbInbound.id)
break;
case "export":
this.inboundLinks(dbInbound.id);
break;
case "resetTraffic":
this.resetTraffic(dbInbound);
this.resetTraffic(dbInbound.id);
break;
case "delete":
this.delInbound(dbInbound);
this.delInbound(dbInbound.id);
break;
}
},
@@ -302,7 +329,8 @@
isEdit: false
});
},
openEditInbound(dbInbound) {
openEditInbound(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
const inbound = dbInbound.toInbound();
inModal.show({
title: '{{ i18n "pages.inbounds.modifyInbound"}}',
@@ -331,9 +359,10 @@
port: inbound.port,
protocol: inbound.protocol,
settings: inbound.settings.toString(),
streamSettings: inbound.stream.toString(),
sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
};
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit('/xui/inbound/add', data, inModal);
},
async updateInbound(inbound, dbInbound) {
@@ -349,15 +378,80 @@
port: inbound.port,
protocol: inbound.protocol,
settings: inbound.settings.toString(),
streamSettings: inbound.stream.toString(),
sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
};
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
},
resetTraffic(dbInbound) {
openAddClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientModal.show({
title: '{{ i18n "pages.client.add"}}',
okText: '{{ i18n "pages.client.submitAdd"}}',
dbInbound: dbInbound,
confirm: async (inbound, dbInbound, index) => {
clientModal.loading();
await this.addClient(inbound, dbInbound);
clientModal.close();
},
isEdit: false
});
},
openAddBulkClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientsBulkModal.show({
title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
okText: '{{ i18n "pages.client.bulk"}}',
dbInbound: dbInbound,
confirm: async (inbound, dbInbound) => {
clientsBulkModal.loading();
await this.addClient(inbound, dbInbound);
clientsBulkModal.close();
},
});
},
openEditClient(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clients = this.getInboundClients(dbInbound);
index = this.findIndexOfClient(clients, client);
clientModal.show({
title: '{{ i18n "pages.client.edit"}}',
okText: '{{ i18n "pages.client.submitEdit"}}',
dbInbound: dbInbound,
index: index,
confirm: async (inbound, dbInbound, index) => {
clientModal.loading();
await this.updateClient(inbound, dbInbound, index);
clientModal.close();
},
isEdit: true
});
},
findIndexOfClient(clients,client) {
firstKey = Object.keys(client)[0];
return clients.findIndex(c => c[firstKey] === client[firstKey]);
},
async addClient(inbound, dbInbound) {
const data = {
id: dbInbound.id,
settings: inbound.settings.toString(),
};
await this.submit('/xui/inbound/addClient', data);
},
async updateClient(inbound, dbInbound, index) {
const data = {
id: dbInbound.id,
settings: inbound.settings.toString(),
};
await this.submit(`/xui/inbound/updateClient/${index}`, data);
},
resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => {
@@ -368,15 +462,44 @@
},
});
},
delInbound(dbInbound) {
delInbound(dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/del/' + dbInbound.id),
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
});
},
delClient(dbInboundId,client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
newDbInbound = new DBInbound(dbInbound);
inbound = newDbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(clients, client);
clients.splice(index, 1);
const data = {
id: dbInboundId,
settings: inbound.settings.toString(),
};
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/delClient/' + client.email, data),
});
},
getClients(protocol, clientSettings) {
switch(protocol){
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
default: return null;
}
},
showQrcode(dbInbound, clientIndex) {
const link = dbInbound.genLink(clientIndex);
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
@@ -384,11 +507,12 @@
showInfo(dbInbound, index) {
infoModal.show(dbInbound, index);
},
switchEnable(dbInbound) {
this.submit(`/xui/inbound/update/${dbInbound.id}`, dbInbound);
switchEnable(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound);
},
async submit(url, data, modal) {
const msg = await HttpUtil.postWithModal(url, data, modal);
async submit(url, data) {
const msg = await HttpUtil.postWithModal(url, data);
if (msg.success) {
await this.getDBInbounds();
}
@@ -402,93 +526,51 @@
return dbInbound.toInbound().settings.trojans
}
},
resetClientTraffic(client,inbound,event) {
resetClientTraffic(client,dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => {
this.resetClTraffic(client,inbound,event);
},
});
},
async resetClTraffic(client,inbound,event) {
const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email);
if (!msg.success) {
return;
}
clientStats = inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == client.email){
clientStats[key]['up'] = 0
clientStats[key]['down'] = 0
}
}
}
}
onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ client.email),
})
},
isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index)
},
getUpStats(dbInbound, email) {
clientStats = dbInbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['up']
}
}
}
if(email.length == 0) return 0
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.up : 0
},
getDownStats(dbInbound, email) {
clientStats = dbInbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['down']
}
}
}
if(email.length == 0) return 0
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.down : 0
},
isTrafficExhausted(dbInbound, email) {
clientStats = dbInbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['down']+clientStats[key]['up'] > clientStats[key]['total']
}
}
}
if(email.length == 0) return false
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
},
isClientEnabled(dbInbound, email) {
clientStats = dbInbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['enable']
}
}
}
else{
return true
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
return clientStats ? clientStats['enable'] : true
},
isRemovable(dbInbound_id){
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
},
inboundLinks(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark);
},
exportAllLinks() {
let copyText = '';
for (const dbInbound of this.dbInbounds) {
copyText += dbInbound.genInboundLinks
}
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
},
},
watch: {
@@ -502,13 +584,32 @@
computed: {
total() {
let down = 0, up = 0;
for (let i = 0; i < this.dbInbounds.length; ++i) {
down += this.dbInbounds[i].down;
up += this.dbInbounds[i].up;
}
let clients = 0, active = 0, deactive = 0;
this.dbInbounds.forEach(dbInbound => {
down += dbInbound.down;
up += dbInbound.up;
inbound = dbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings);
if(clients){
if(dbInbound.enable){
isClientEnable = false;
clients.forEach(client => {
isClientEnable = client.email == "" ? true: this.isClientEnabled(dbInbound,client.email);
isClientEnable ? active++ : deactive++;
});
} else {
deactive += clients.length;
}
} else {
dbInbound.enable ? active++ : deactive++;
}
});
return {
down: down,
up: up,
clients: active + deactive,
active: active,
deactive: deactive,
};
}
},
@@ -521,5 +622,7 @@
{{template "qrcodeModal"}}
{{template "textModal"}}
{{template "inboundInfoModal"}}
{{template "clientsModal"}}
{{template "clientsBulkModal"}}
</body>
</html>

View File

@@ -11,28 +11,34 @@
.ant-col-sm-24 {
margin-top: 10px;
}
.ant-card-dark h2 {
color: hsla(0,0%,100%,.65);
}
</style>
<body>
<a-layout id="app" v-cloak>
{{ template "commonSider" . }}
<a-layout id="content-layout">
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout-content>
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
<transition name="list" appear>
<a-row>
<a-card hoverable>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row>
<a-col :sm="24" :md="12">
<a-row>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.cpu.percent"></a-progress>
<div>CPU</div>
</a-col>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.mem.percent"></a-progress>
<div>
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
@@ -45,6 +51,7 @@
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.swap.percent"></a-progress>
<div>
swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
@@ -53,6 +60,7 @@
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.disk.percent"></a-progress>
<div>
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
@@ -67,7 +75,7 @@
<transition name="list" appear>
<a-row>
<a-col :sm="24" :md="12">
<a-card hoverable>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "pages.index.xrayStatus" }}:
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
<a-tooltip v-if="status.xray.state === State.Error">
@@ -76,14 +84,18 @@
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tag color="green" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
<a-tag color="blue" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch"}}</a-tag>
<a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
x-ui: <a-tag color="green">{{ .cur_ver }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openLogs">Logs</a-tag>
{{ i18n "pages.index.operationHours" }}:
<a-tag color="#87d068">[[ formatSecond(status.uptime) ]]</a-tag>
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.operationHoursDesc" }}
@@ -93,12 +105,12 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
tcp / udp {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
<a-tooltip>
<template slot="title">
@@ -109,7 +121,7 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row>
<a-col :span="12">
<a-icon type="arrow-up"></a-icon>
@@ -135,7 +147,7 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row>
<a-col :span="12">
<a-icon type="cloud-upload"></a-icon>
@@ -166,7 +178,8 @@
</a-layout>
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
:closable="true" @ok="() => versionModal.visible = false"
ok-text='{{ i18n "confirm" }}' cancel-text='{{ i18n "cancel"}}'>
:class="siderDrawer.isDarkTheme ? darkClass : ''"
footer="">
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
<template v-for="version, index in versionModal.versions">
@@ -176,6 +189,17 @@
</a-tag>
</template>
</a-modal>
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
width="800px"
footer="">
<table style="margin: 0px; width: 100%; background-color: black; color: hsla(0,0%,100%,.65);">
<tr v-for="log , index in logModal.logs">
<td style="vertical-align: top;">[[ index ]]</td><td>[[ log ]]</td>
</tr>
</table>
</a-modal>
</a-layout>
{{template "js" .}}
<script>
@@ -269,6 +293,18 @@
},
};
const logModal = {
visible: false,
logs: '',
show(logs) {
this.visible = true;
this.logs = logs;
},
hide() {
this.visible = false;
},
};
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
@@ -276,6 +312,7 @@
siderDrawer,
status: new Status(),
versionModal,
logModal,
spinning: false,
loadingTip: '{{ i18n "loading"}}',
},
@@ -307,6 +344,7 @@
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
okText: '{{ i18n "confirm"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
versionModal.hide();
@@ -316,6 +354,33 @@
},
});
},
//here add stop xray function
async stopXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/stopXrayService');
this.loading(false);
if (!msg.success) {
return;
}
},
//here add restart xray function
async restartXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService');
this.loading(false);
if (!msg.success) {
return;
}
},
async openLogs(){
this.loading(true);
const msg = await HttpUtil.post('server/logs');
this.loading(false);
if (!msg.success) {
return;
}
logModal.show(msg.obj);
}
},
async mounted() {
while (true) {

View File

@@ -20,14 +20,14 @@
display: block;
}
.ant-tabs-top-bar {
:not(.ant-card-dark)>.ant-tabs-top-bar {
background: white;
}
</style>
<body>
<a-layout id="app" v-cloak>
{{ template "commonSider" . }}
<a-layout id="content-layout">
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading">
<a-space direction="vertical">
@@ -35,17 +35,17 @@
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.setting.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.setting.restartPanel" }}</a-button>
</a-space>
<a-tabs default-active-key="1">
<a-tabs default-active-key="1" :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-tab-pane key="1" tab='{{ i18n "pages.setting.panelConfig"}}'>
<a-list item-layout="horizontal" style="background: white">
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="text" title='{{ i18n "pages.setting.panelListeningIP"}}' desc='{{ i18n "pages.setting.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
<a-list-item>
<a-row style="padding: 20px">
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title="Language"/>
</a-col>
@@ -56,9 +56,10 @@
ref="selectLang"
v-model="lang"
@change="setLang(lang)"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
style="width: 100%"
>
<a-select-option :value="l.value" label="China" v-for="l in supportLangs" >
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
@@ -71,7 +72,7 @@
</a-list>
</a-tab-pane>
<a-tab-pane key="2" tab='{{ i18n "pages.setting.userSetting"}}'>
<a-form style="background: white; padding: 20px">
<a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': 'background: white; padding: 20px;'">
<a-form-item label='{{ i18n "pages.setting.oldUsername"}}'>
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
</a-form-item>
@@ -93,7 +94,7 @@
</a-form>
</a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
<a-list item-layout="horizontal" style="background: white">
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigTorrent"}}' desc='{{ i18n "pages.setting.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.setting.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
<a-divider>{{ i18n "pages.setting.advancedTemplate"}}</a-divider>
@@ -113,15 +114,19 @@
</a-list>
</a-tab-pane>
<a-tab-pane key="4" tab='{{ i18n "pages.setting.TGReminder"}}'>
<a-list item-layout="horizontal" style="background: white">
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="switch" title='{{ i18n "pages.setting.telegramBotEnable" }}' desc='{{ i18n "pages.setting.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model.number="allSetting.tgBotChatId"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.tgNotifyBackup" }}' desc='{{ i18n "pages.setting.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyExpireTimeDiff" }}' desc='{{ i18n "pages.setting.tgNotifyExpireTimeDiffDesc" }}' v-model="allSetting.tgExpireDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyTrafficDiff" }}' desc='{{ i18n "pages.setting.tgNotifyTrafficDiffDesc" }}' v-model="allSetting.tgTrafficDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyCpu" }}' desc='{{ i18n "pages.setting.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'>
<a-list item-layout="horizontal" style="background: white">
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="text" title='{{ i18n "pages.setting.timeZonee"}}' desc='{{ i18n "pages.setting.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
</a-list>
</a-tab-pane>

View File

@@ -0,0 +1,30 @@
package job
import (
"fmt"
"time"
"x-ui/web/service"
"github.com/shirou/gopsutil/v3/cpu"
)
type CheckCpuJob struct {
tgbotService service.Tgbot
settingService service.SettingService
}
func NewCheckCpuJob() *CheckCpuJob {
return new(CheckCpuJob)
}
// Here run is a interface method of Job interface
func (j *CheckCpuJob) Run() {
threshold, _ := j.settingService.GetTgCpu()
// get latest status of server
percent, err := cpu.Percent(1*time.Second, false)
if err == nil && percent[0] > float64(threshold) {
msg := fmt.Sprintf("🔴 CPU usage %.2f%% is more than threshold %d%%", percent[0], threshold)
j.tgbotService.SendMsgToTgbotAdmins(msg)
}
}

View File

@@ -1,14 +1,7 @@
package job
import (
"fmt"
"net"
"os"
"time"
"x-ui/logger"
"x-ui/util/common"
"x-ui/web/service"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
type LoginStatus byte
@@ -19,230 +12,18 @@ const (
)
type StatsNotifyJob struct {
enable bool
xrayService service.XrayService
inboundService service.InboundService
settingService service.SettingService
xrayService service.XrayService
tgbotService service.Tgbot
}
func NewStatsNotifyJob() *StatsNotifyJob {
return new(StatsNotifyJob)
}
func (j *StatsNotifyJob) SendMsgToTgbot(msg string) {
//Telegram bot basic info
tgBottoken, err := j.settingService.GetTgBotToken()
if err != nil || tgBottoken == "" {
logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
return
}
tgBotid, err := j.settingService.GetTgBotChatId()
if err != nil {
logger.Warning("sendMsgToTgbot failed,GetTgBotChatId fail:", err)
return
}
bot, err := tgbotapi.NewBotAPI(tgBottoken)
if err != nil {
fmt.Println("get tgbot error:", err)
return
}
bot.Debug = true
fmt.Printf("Authorized on account %s", bot.Self.UserName)
info := tgbotapi.NewMessage(int64(tgBotid), msg)
//msg.ReplyToMessageID = int(tgBotid)
bot.Send(info)
}
//Here run is a interface method of Job interface
// Here run is a interface method of Job interface
func (j *StatsNotifyJob) Run() {
if !j.xrayService.IsXrayRunning() {
return
}
var info string
//get hostname
name, err := os.Hostname()
if err != nil {
fmt.Println("get hostname error:", err)
return
}
info = fmt.Sprintf("Hostname:%s\r\n", name)
//get ip address
var ip string
netInterfaces, err := net.Interfaces()
if err != nil {
fmt.Println("net.Interfaces failed, err:", err.Error())
return
}
for i := 0; i < len(netInterfaces); i++ {
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
addrs, _ := netInterfaces[i].Addrs()
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
ip = ipnet.IP.String()
break
} else {
ip = ipnet.IP.String()
break
}
}
}
}
}
info += fmt.Sprintf("IP:%s\r\n \r\n", ip)
//get traffic
inbouds, err := j.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("StatsNotifyJob run failed:", err)
return
}
//NOTE:If there no any sessions here,need to notify here
//TODO:分节点推送,自动转化格式
for _, inbound := range inbouds {
info += fmt.Sprintf("Node name:%s\r\nPort:%d\r\nUpload↑:%s\r\nDownload↓:%s\r\nTotal:%s\r\n", inbound.Remark, inbound.Port, common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down), common.FormatTraffic((inbound.Up + inbound.Down)))
if inbound.ExpiryTime == 0 {
info += fmt.Sprintf("Expire date:unlimited\r\n \r\n")
} else {
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
}
j.SendMsgToTgbot(info)
}
func (j *StatsNotifyJob) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
if username == "" || ip == "" || time == "" {
logger.Warning("UserLoginNotify failed,invalid info")
return
}
var msg string
//get hostname
name, err := os.Hostname()
if err != nil {
fmt.Println("get hostname error:", err)
return
}
if status == LoginSuccess {
msg = fmt.Sprintf("Successfully logged-in to the panel\r\nHostname:%s\r\n", name)
} else if status == LoginFail {
msg = fmt.Sprintf("Login to the panel was unsuccessful\r\nHostname:%s\r\n", name)
}
msg += fmt.Sprintf("Time:%s\r\n", time)
msg += fmt.Sprintf("Username:%s\r\n", username)
msg += fmt.Sprintf("IP:%s\r\n", ip)
j.SendMsgToTgbot(msg)
}
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "get_usage"),
),
)
func (j *StatsNotifyJob) OnReceive() *StatsNotifyJob {
tgBottoken, err := j.settingService.GetTgBotToken()
if err != nil || tgBottoken == "" {
logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
return j
}
bot, err := tgbotapi.NewBotAPI(tgBottoken)
if err != nil {
fmt.Println("get tgbot error:", err)
return j
}
bot.Debug = false
u := tgbotapi.NewUpdate(0)
u.Timeout = 10
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil {
if update.CallbackQuery != nil {
// Respond to the callback query, telling Telegram to show the user
// a message with the data received.
callback := tgbotapi.NewCallback(update.CallbackQuery.ID, update.CallbackQuery.Data)
if _, err := bot.Request(callback); err != nil {
logger.Warning(err)
}
// And finally, send a message containing the data received.
msg := tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, "")
switch update.CallbackQuery.Data {
case "get_usage":
msg.Text = "for get your usage send command like this : \n <code>/usage uuid | id</code> \n example : <code>/usage fc3239ed-8f3b-4151-ff51-b183d5182142</code>"
msg.ParseMode = "HTML"
}
if _, err := bot.Send(msg); err != nil {
logger.Warning(err)
}
}
continue
}
if !update.Message.IsCommand() { // ignore any non-command Messages
continue
}
// Create a new MessageConfig. We don't have text yet,
// so we leave it empty.
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "")
// Extract the command from the Message.
switch update.Message.Command() {
case "help":
msg.Text = "What you need?"
msg.ReplyMarkup = numericKeyboard
case "start":
msg.Text = "Hi :) \n What you need?"
msg.ReplyMarkup = numericKeyboard
case "status":
msg.Text = "bot is ok."
case "usage":
msg.Text = j.getClientUsage(update.Message.CommandArguments())
default:
msg.Text = "I don't know that command, /help"
msg.ReplyMarkup = numericKeyboard
}
if _, err := bot.Send(msg); err != nil {
logger.Warning(err)
}
}
return j
}
func (j *StatsNotifyJob) getClientUsage(id string) string {
traffic , err := j.inboundService.GetClientTrafficById(id)
if err != nil {
logger.Warning(err)
return "something wrong!"
}
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = fmt.Sprintf("unlimited")
} else {
expiryTime = fmt.Sprintf("%s", time.Unix((traffic.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
total := ""
if traffic.Total == 0 {
total = fmt.Sprintf("unlimited")
} else {
total = fmt.Sprintf("%s", common.FormatTraffic((traffic.Total)))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
return output
j.tgbotService.SendReport()
}

View File

@@ -28,11 +28,10 @@ func (j *XrayTrafficJob) Run() {
if err != nil {
logger.Warning("add traffic failed:", err)
}
err = j.inboundService.AddClientTraffic(clientTraffics)
if err != nil {
logger.Warning("add client traffic failed:", err)
}
}

View File

@@ -1,7 +1,6 @@
{
"log": {
"loglevel": "warning",
"access": "./access.log"
"loglevel": "warning"
},
"api": {

View File

@@ -54,7 +54,7 @@ func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, err
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings)
if settings == nil {
return nil, fmt.Errorf("Setting is null")
return nil, fmt.Errorf("setting is null")
}
clients := settings["clients"]
@@ -125,11 +125,18 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, err
return inbound, common.NewError("Duplicate email:", existEmail)
}
clients, err := s.getClients(inbound)
if err != nil {
return inbound, err
}
db := database.GetDB()
err = db.Save(inbound).Error
if err == nil {
s.UpdateClientStat(inbound.Id, inbound.Settings)
for _, client := range clients {
s.AddClientStat(inbound.Id, &client)
}
}
return inbound, err
}
@@ -168,6 +175,10 @@ func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
func (s *InboundService) DelInbound(id int) error {
db := database.GetDB()
err := db.Where("inbound_id = ?", id).Delete(xray.ClientTraffic{}).Error
if err != nil {
return err
}
return db.Delete(model.Inbound{}, id).Error
}
@@ -216,11 +227,114 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
oldInbound.Sniffing = inbound.Sniffing
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
s.UpdateClientStat(inbound.Id, inbound.Settings)
db := database.GetDB()
return inbound, db.Save(oldInbound).Error
}
func (s *InboundService) AddInboundClient(inbound *model.Inbound) error {
existEmail, err := s.checkEmailExistForInbound(inbound)
if err != nil {
return err
}
if existEmail != "" {
return common.NewError("Duplicate email:", existEmail)
}
clients, err := s.getClients(inbound)
if err != nil {
return err
}
oldInbound, err := s.GetInbound(inbound.Id)
if err != nil {
return err
}
oldClients, err := s.getClients(oldInbound)
if err != nil {
return err
}
oldInbound.Settings = inbound.Settings
if len(clients[len(clients)-1].Email) > 0 {
s.AddClientStat(inbound.Id, &clients[len(clients)-1])
}
for i := len(oldClients); i < len(clients); i++ {
if len(clients[i].Email) > 0 {
s.AddClientStat(inbound.Id, &clients[i])
}
}
db := database.GetDB()
return db.Save(oldInbound).Error
}
func (s *InboundService) DelInboundClient(inbound *model.Inbound, email string) error {
db := database.GetDB()
err := s.DelClientStat(db, email)
if err != nil {
logger.Error("Delete stats Data Error")
return err
}
oldInbound, err := s.GetInbound(inbound.Id)
if err != nil {
logger.Error("Load Old Data Error")
return err
}
oldInbound.Settings = inbound.Settings
return db.Save(oldInbound).Error
}
func (s *InboundService) UpdateInboundClient(inbound *model.Inbound, index int) error {
existEmail, err := s.checkEmailExistForInbound(inbound)
if err != nil {
return err
}
if existEmail != "" {
return common.NewError("Duplicate email:", existEmail)
}
clients, err := s.getClients(inbound)
if err != nil {
return err
}
oldInbound, err := s.GetInbound(inbound.Id)
if err != nil {
return err
}
oldClients, err := s.getClients(oldInbound)
if err != nil {
return err
}
oldInbound.Settings = inbound.Settings
db := database.GetDB()
if len(clients[index].Email) > 0 {
if len(oldClients[index].Email) > 0 {
err = s.UpdateClientStat(oldClients[index].Email, &clients[index])
if err != nil {
return err
}
} else {
s.AddClientStat(inbound.Id, &clients[index])
}
} else {
err = s.DelClientStat(db, oldClients[index].Email)
if err != nil {
return err
}
}
return db.Save(oldInbound).Error
}
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
if len(traffics) == 0 {
return nil
@@ -238,9 +352,9 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
for _, traffic := range traffics {
if traffic.IsInbound {
err = tx.Where("tag = ?", traffic.Tag).
UpdateColumn("up", gorm.Expr("up + ?", traffic.Up)).
UpdateColumn("down", gorm.Expr("down + ?", traffic.Down)).
Error
UpdateColumns(map[string]interface{}{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down)}).Error
if err != nil {
return
}
@@ -275,15 +389,19 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
for _, traffic := range traffics {
inbound := &model.Inbound{}
err := txInbound.Where("settings like ?", "%"+traffic.Email+"%").First(inbound).Error
traffic.InboundId = inbound.Id
client := &xray.ClientTraffic{}
err := tx.Where("email = ?", traffic.Email).First(client).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// delete removed client record
clientErr := s.DelClientStat(tx, traffic.Email)
logger.Warning(err, traffic.Email, clientErr)
logger.Warning(err, traffic.Email)
}
continue
}
err = txInbound.Where("id=?", client.InboundId).First(inbound).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err, traffic.Email)
}
continue
}
@@ -297,12 +415,13 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
traffic.Total = client.TotalGB
}
}
if tx.Where("inbound_id = ?", inbound.Id).Where("email = ?", traffic.Email).
UpdateColumn("enable", true).
UpdateColumn("expiry_time", traffic.ExpiryTime).
UpdateColumn("total", traffic.Total).
UpdateColumn("up", gorm.Expr("up + ?", traffic.Up)).
UpdateColumn("down", gorm.Expr("down + ?", traffic.Down)).RowsAffected == 0 {
if tx.Where("inbound_id = ? and email = ?", inbound.Id, traffic.Email).
UpdateColumns(map[string]interface{}{
"enable": true,
"expiry_time": traffic.ExpiryTime,
"total": traffic.Total,
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down)}).RowsAffected == 0 {
err = tx.Create(traffic).Error
}
@@ -310,7 +429,6 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
logger.Warning("AddClientTraffic update data ", err)
continue
}
}
return
}
@@ -335,33 +453,37 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
count := result.RowsAffected
return count, err
}
func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string) error {
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
db := database.GetDB()
// get settings clients
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inboundSettings), &settings)
clients := settings["clients"]
for _, client := range clients {
result := db.Model(xray.ClientTraffic{}).
Where("inbound_id = ? and email = ?", inboundId, client.Email).
Updates(map[string]interface{}{"enable": true, "total": client.TotalGB, "expiry_time": client.ExpiryTime})
if result.RowsAffected == 0 {
clientTraffic := xray.ClientTraffic{}
clientTraffic.InboundId = inboundId
clientTraffic.Email = client.Email
clientTraffic.Total = client.TotalGB
clientTraffic.ExpiryTime = client.ExpiryTime
clientTraffic.Enable = true
clientTraffic.Up = 0
clientTraffic.Down = 0
db.Create(&clientTraffic)
}
err := result.Error
if err != nil {
return err
}
clientTraffic := xray.ClientTraffic{}
clientTraffic.InboundId = inboundId
clientTraffic.Email = client.Email
clientTraffic.Total = client.TotalGB
clientTraffic.ExpiryTime = client.ExpiryTime
clientTraffic.Enable = true
clientTraffic.Up = 0
clientTraffic.Down = 0
result := db.Create(&clientTraffic)
err := result.Error
if err != nil {
return err
}
return nil
}
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
db := database.GetDB()
result := db.Model(xray.ClientTraffic{}).
Where("email = ?", email).
Updates(map[string]interface{}{
"enable": true,
"email": client.Email,
"total": client.TotalGB,
"expiry_time": client.ExpiryTime})
err := result.Error
if err != nil {
return err
}
return nil
}
@@ -369,13 +491,12 @@ func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {
return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error
}
func (s *InboundService) ResetClientTraffic(clientEmail string) error {
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
db := database.GetDB()
result := db.Model(xray.ClientTraffic{}).
Where("email = ?", clientEmail).
Update("up", 0).
Update("down", 0)
Where("inbound_id = ? and email = ?", id, clientEmail).
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
err := result.Error
@@ -384,12 +505,40 @@ func (s *InboundService) ResetClientTraffic(clientEmail string) error {
}
return nil
}
func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.ClientTraffic, err error) {
func (s *InboundService) GetClientTrafficTgBot(tguname string) (traffic []*xray.ClientTraffic, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("email like ?", "%@"+tguname).Find(&traffics).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err)
return nil, err
}
}
return traffics, err
}
func (s *InboundService) GetClientTrafficByEmail(email string) (traffic []*xray.ClientTraffic, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("email like ?", "%"+email+"%").Find(&traffics).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err)
return nil, err
}
}
return traffics, err
}
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
db := database.GetDB()
inbound := &model.Inbound{}
traffic = &xray.ClientTraffic{}
err = db.Model(model.Inbound{}).Where("settings like ?", "%"+uuid+"%").First(inbound).Error
err = db.Model(model.Inbound{}).Where("settings like ?", "%\""+query+"\"%").First(inbound).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err)
@@ -403,9 +552,17 @@ func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.Client
json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"]
for _, client := range clients {
if uuid == client.ID {
if client.ID == query && client.Email != "" {
traffic.Email = client.Email
break
}
if client.Password == query && client.Email != "" {
traffic.Email = client.Email
break
}
}
if traffic.Email == "" {
return nil, err
}
err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error
if err != nil {
@@ -414,3 +571,13 @@ func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.Client
}
return traffic, err
}
func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("remark like ?", "%"+query+"%").Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
return inbounds, nil
}

View File

@@ -9,18 +9,20 @@ import (
"io/fs"
"net/http"
"os"
"os/exec"
"runtime"
"strings"
"time"
"x-ui/logger"
"x-ui/util/sys"
"x-ui/xray"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/host"
"github.com/shirou/gopsutil/load"
"github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/net"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/load"
"github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/net"
)
type ProcessState string
@@ -198,6 +200,30 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
return versions, nil
}
func (s *ServerService) StopXrayService() (string error) {
err := s.xrayService.StopXray()
if err != nil {
logger.Error("stop xray failed:", err)
return err
}
return nil
}
func (s *ServerService) RestartXrayService() (string error) {
s.xrayService.StopXray()
defer func() {
err := s.xrayService.RestartXray(true)
if err != nil {
logger.Error("start xray failed:", err)
}
}()
return nil
}
func (s *ServerService) downloadXRay(version string) (string, error) {
osName := runtime.GOOS
arch := runtime.GOARCH
@@ -300,3 +326,26 @@ func (s *ServerService) UpdateXray(version string) error {
return nil
}
func (s *ServerService) GetLogs() ([]string, error) {
// Define the journalctl command and its arguments
var cmdArgs []string
if runtime.GOOS == "linux" {
cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", "100"}
} else {
return []string{"Unsupported operating system"}, nil
}
// Run the command
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
return lines, nil
}

View File

@@ -31,8 +31,12 @@ var defaultValueMap = map[string]string{
"timeLocation": "Asia/Tehran",
"tgBotEnable": "false",
"tgBotToken": "",
"tgBotChatId": "0",
"tgRunTime": "",
"tgBotChatId": "",
"tgRunTime": "@daily",
"tgBotBackup": "false",
"tgExpireDiff": "0",
"tgTrafficDiff": "0",
"tgCpu": "0",
}
type SettingService struct {
@@ -202,30 +206,62 @@ func (s *SettingService) SetTgBotToken(token string) error {
return s.setString("tgBotToken", token)
}
func (s *SettingService) GetTgBotChatId() (int, error) {
return s.getInt("tgBotChatId")
func (s *SettingService) GetTgBotChatId() (string, error) {
return s.getString("tgBotChatId")
}
func (s *SettingService) SetTgBotChatId(chatId int) error {
return s.setInt("tgBotChatId", chatId)
}
func (s *SettingService) SetTgbotenabled(value bool) error {
return s.setBool("tgBotEnable", value)
func (s *SettingService) SetTgBotChatId(chatIds string) error {
return s.setString("tgBotChatId", chatIds)
}
func (s *SettingService) GetTgbotenabled() (bool, error) {
return s.getBool("tgBotEnable")
}
func (s *SettingService) SetTgbotRuntime(time string) error {
return s.setString("tgRunTime", time)
func (s *SettingService) SetTgbotenabled(value bool) error {
return s.setBool("tgBotEnable", value)
}
func (s *SettingService) GetTgbotRuntime() (string, error) {
return s.getString("tgRunTime")
}
func (s *SettingService) SetTgbotRuntime(time string) error {
return s.setString("tgRunTime", time)
}
func (s *SettingService) GetTgBotBackup() (bool, error) {
return s.getBool("tgBotBackup")
}
func (s *SettingService) SetTgBotBackup(value bool) error {
return s.setBool("tgBotBackup", value)
}
func (s *SettingService) GetTgExpireDiff() (int, error) {
return s.getInt("tgExpireDiff")
}
func (s *SettingService) SetTgExpireDiff(value int) error {
return s.setInt("tgExpireDiff", value)
}
func (s *SettingService) GetTgTrafficDiff() (int, error) {
return s.getInt("tgTrafficDiff")
}
func (s *SettingService) SetTgTrafficDiff(value int) error {
return s.setInt("tgTrafficDiff", value)
}
func (s *SettingService) GetTgCpu() (int, error) {
return s.getInt("tgCpu")
}
func (s *SettingService) SetTgCpu(value int) error {
return s.setInt("tgCpu", value)
}
func (s *SettingService) GetPort() (int, error) {
return s.getInt("webPort")
}

596
web/service/tgbot.go Normal file
View File

@@ -0,0 +1,596 @@
package service
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
"x-ui/config"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/common"
"x-ui/xray"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
var bot *tgbotapi.BotAPI
var adminIds []int64
var isRunning bool
type LoginStatus byte
const (
LoginSuccess LoginStatus = 1
LoginFail LoginStatus = 0
)
type Tgbot struct {
inboundService InboundService
settingService SettingService
serverService ServerService
lastStatus *Status
}
func (t *Tgbot) NewTgbot() *Tgbot {
return new(Tgbot)
}
func (t *Tgbot) Start() error {
tgBottoken, err := t.settingService.GetTgBotToken()
if err != nil || tgBottoken == "" {
logger.Warning("Get TgBotToken failed:", err)
return err
}
tgBotid, err := t.settingService.GetTgBotChatId()
if err != nil {
logger.Warning("Get GetTgBotChatId failed:", err)
return err
}
for _, adminId := range strings.Split(tgBotid, ",") {
id, err := strconv.Atoi(adminId)
if err != nil {
logger.Warning("Failed to get IDs from GetTgBotChatId:", err)
return err
}
adminIds = append(adminIds, int64(id))
}
bot, err = tgbotapi.NewBotAPI(tgBottoken)
if err != nil {
fmt.Println("Get tgbot's api error:", err)
return err
}
bot.Debug = false
// listen for TG bot income messages
if !isRunning {
logger.Info("Starting Telegram receiver ...")
go t.OnReceive()
isRunning = true
}
return nil
}
func (t *Tgbot) IsRunnging() bool {
return isRunning
}
func (t *Tgbot) Stop() {
bot.StopReceivingUpdates()
logger.Info("Stop Telegram receiver ...")
isRunning = false
adminIds = nil
}
func (t *Tgbot) OnReceive() {
u := tgbotapi.NewUpdate(0)
u.Timeout = 10
updates := bot.GetUpdatesChan(u)
for update := range updates {
tgId := update.FromChat().ID
chatId := update.FromChat().ChatConfig().ChatID
isAdmin := checkAdmin(tgId)
if update.Message == nil {
if update.CallbackQuery != nil {
t.asnwerCallback(update.CallbackQuery, isAdmin)
}
} else {
if update.Message.IsCommand() {
t.answerCommand(update.Message, chatId, isAdmin)
}
}
}
}
func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin bool) {
msg := ""
// Extract the command from the Message.
switch message.Command() {
case "help":
msg = "This bot is providing you some specefic data from the server.\n\n Please choose:"
case "start":
msg = "Hello <i>" + message.From.FirstName + "</i> 👋"
if isAdmin {
hostname, _ := os.Hostname()
msg += "\nWelcome to <b>" + hostname + "</b> management bot"
}
msg += "\n\nI can do some magics for you, please choose:"
case "status":
msg = "bot is ok ✅"
case "usage":
if len(message.CommandArguments()) > 1 {
if isAdmin {
t.searchClient(chatId, message.CommandArguments())
} else {
t.searchForClient(chatId, message.CommandArguments())
}
} else {
msg = "❗Please provide a text for search!"
}
case "inbound":
if isAdmin {
t.searchInbound(chatId, message.CommandArguments())
} else {
msg = "❗ Unknown command"
}
default:
msg = "❗ Unknown command"
}
t.SendAnswer(chatId, msg, isAdmin)
}
func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bool) {
// Respond to the callback query, telling Telegram to show the user
// a message with the data received.
callback := tgbotapi.NewCallback(callbackQuery.ID, callbackQuery.Data)
if _, err := bot.Request(callback); err != nil {
logger.Warning(err)
}
switch callbackQuery.Data {
case "get_usage":
t.SendMsgToTgbot(callbackQuery.From.ID, t.getServerUsage())
case "inbounds":
t.SendMsgToTgbot(callbackQuery.From.ID, t.getInboundUsages())
case "exhausted_soon":
t.SendMsgToTgbot(callbackQuery.From.ID, t.getExhausted())
case "get_backup":
t.sendBackup(callbackQuery.From.ID)
case "client_traffic":
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName)
case "client_commands":
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Passowrd]</code>\r\n \r\nUse UID for vmess and vless and Password for Trojan.")
case "commands":
t.SendMsgToTgbot(callbackQuery.From.ID, "Search for a client email:\r\n<code>/usage email</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [remark]</code>")
}
}
func checkAdmin(tgId int64) bool {
for _, adminId := range adminIds {
if adminId == tgId {
return true
}
}
return false
}
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Server Usage", "get_usage"),
tgbotapi.NewInlineKeyboardButtonData("Get DB Backup", "get_backup"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Inbounds", "inbounds"),
tgbotapi.NewInlineKeyboardButtonData("Exhausted soon", "exhausted_soon"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Commands", "commands"),
),
)
var numericKeyboardClient = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "client_traffic"),
tgbotapi.NewInlineKeyboardButtonData("Commands", "client_commands"),
),
)
msgConfig := tgbotapi.NewMessage(chatId, msg)
msgConfig.ParseMode = "HTML"
if isAdmin {
msgConfig.ReplyMarkup = numericKeyboard
} else {
msgConfig.ReplyMarkup = numericKeyboardClient
}
_, err := bot.Send(msgConfig)
if err != nil {
logger.Warning("Error sending telegram message :", err)
}
}
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) {
var allMessages []string
limit := 2000
// paging message if it is big
if len(msg) > limit {
messages := strings.Split(msg, "\r\n \r\n")
lastIndex := -1
for _, message := range messages {
if (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) {
allMessages = append(allMessages, message)
lastIndex++
} else {
allMessages[lastIndex] += "\r\n \r\n" + message
}
}
} else {
allMessages = append(allMessages, msg)
}
for _, message := range allMessages {
info := tgbotapi.NewMessage(tgid, message)
info.ParseMode = "HTML"
_, err := bot.Send(info)
if err != nil {
logger.Warning("Error sending telegram message :", err)
}
time.Sleep(500 * time.Millisecond)
}
}
func (t *Tgbot) SendMsgToTgbotAdmins(msg string) {
for _, adminId := range adminIds {
t.SendMsgToTgbot(adminId, msg)
}
}
func (t *Tgbot) SendReport() {
runTime, err := t.settingService.GetTgbotRuntime()
if err == nil && len(runTime) > 0 {
t.SendMsgToTgbotAdmins("🕰 Scheduled reports: " + runTime + "\r\nDate-Time: " + time.Now().Format("2006-01-02 15:04:05"))
}
info := t.getServerUsage()
t.SendMsgToTgbotAdmins(info)
exhausted := t.getExhausted()
t.SendMsgToTgbotAdmins(exhausted)
backupEnable, err := t.settingService.GetTgBotBackup()
if err == nil && backupEnable {
for _, adminId := range adminIds {
t.sendBackup(int64(adminId))
}
}
}
func (t *Tgbot) getServerUsage() string {
var info string
//get hostname
name, err := os.Hostname()
if err != nil {
logger.Error("get hostname error:", err)
name = ""
}
info = fmt.Sprintf("💻 Hostname: %s\r\n", name)
info += fmt.Sprintf("🚀X-UI Version: %s\r\n", config.GetVersion())
//get ip address
var ip string
var ipv6 string
netInterfaces, err := net.Interfaces()
if err != nil {
logger.Error("net.Interfaces failed, err:", err.Error())
info += "🌐 IP: Unknown\r\n \r\n"
} else {
for i := 0; i < len(netInterfaces); i++ {
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
addrs, _ := netInterfaces[i].Addrs()
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
ip += ipnet.IP.String() + " "
} else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() {
ipv6 += ipnet.IP.String() + " "
}
}
}
}
}
info += fmt.Sprintf("🌐IP: %s\r\n🌐IPv6: %s\r\n", ip, ipv6)
}
// get latest status of server
t.lastStatus = t.serverService.GetStatus(t.lastStatus)
info += fmt.Sprintf("🔌Server Uptime: %d days\r\n", int(t.lastStatus.Uptime/86400))
info += fmt.Sprintf("📈Server Load: %.1f, %.1f, %.1f\r\n", t.lastStatus.Loads[0], t.lastStatus.Loads[1], t.lastStatus.Loads[2])
info += fmt.Sprintf("📋Server Memory: %s/%s\r\n", common.FormatTraffic(int64(t.lastStatus.Mem.Current)), common.FormatTraffic(int64(t.lastStatus.Mem.Total)))
info += fmt.Sprintf("🔹TcpCount: %d\r\n", t.lastStatus.TcpCount)
info += fmt.Sprintf("🔸UdpCount: %d\r\n", t.lastStatus.UdpCount)
info += fmt.Sprintf("🚦Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
info += fmt.Sprintf("Xray status: %s", t.lastStatus.Xray.State)
return info
}
func (t *Tgbot) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
if username == "" || ip == "" || time == "" {
logger.Warning("UserLoginNotify failed,invalid info")
return
}
var msg string
// Get hostname
name, err := os.Hostname()
if err != nil {
logger.Warning("get hostname error:", err)
return
}
if status == LoginSuccess {
msg = fmt.Sprintf("✅ Successfully logged-in to the panel\r\nHostname:%s\r\n", name)
} else if status == LoginFail {
msg = fmt.Sprintf("❗ Login to the panel was unsuccessful\r\nHostname:%s\r\n", name)
}
msg += fmt.Sprintf("⏰ Time:%s\r\n", time)
msg += fmt.Sprintf("🆔 Username:%s\r\n", username)
msg += fmt.Sprintf("🌐 IP:%s\r\n", ip)
t.SendMsgToTgbotAdmins(msg)
}
func (t *Tgbot) getInboundUsages() string {
info := ""
// get traffic
inbouds, err := t.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("GetAllInbounds run failed:", err)
info += "❌ Failed to get inbounds"
} else {
// NOTE:If there no any sessions here,need to notify here
// TODO:Sub-node push, automatic conversion format
for _, inbound := range inbouds {
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
if inbound.ExpiryTime == 0 {
info += "Expire date: ♾ Unlimited\r\n \r\n"
} else {
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
}
}
return info
}
func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if len(traffics) == 0 {
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username in your configuration(s).\n\nYour username: <b>@" + tgUserName + "</b>"
t.SendMsgToTgbot(chatId, msg)
}
for _, traffic := range traffics {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
}
t.SendAnswer(chatId, "Please choose:", false)
}
func (t *Tgbot) searchClient(chatId int64, email string) {
traffics, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if len(traffics) == 0 {
msg := "No result!"
t.SendMsgToTgbot(chatId, msg)
return
}
for _, traffic := range traffics {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
}
}
func (t *Tgbot) searchInbound(chatId int64, remark string) {
inbouds, err := t.inboundService.SearchInbounds(remark)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
for _, inbound := range inbouds {
info := ""
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
if inbound.ExpiryTime == 0 {
info += "Expire date: ♾ Unlimited\r\n \r\n"
} else {
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
t.SendMsgToTgbot(chatId, info)
for _, traffic := range inbound.ClientStats {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
}
}
}
func (t *Tgbot) searchForClient(chatId int64, query string) {
traffic, err := t.inboundService.SearchClientTraffic(query)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if traffic == nil {
msg := "No result!"
t.SendMsgToTgbot(chatId, msg)
return
}
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
}
func (t *Tgbot) getExhausted() string {
trDiff := int64(0)
exDiff := int64(0)
now := time.Now().Unix() * 1000
var exhaustedInbounds []model.Inbound
var exhaustedClients []xray.ClientTraffic
var disabledInbounds []model.Inbound
var disabledClients []xray.ClientTraffic
output := ""
TrafficThreshold, err := t.settingService.GetTgTrafficDiff()
if err == nil && TrafficThreshold > 0 {
trDiff = int64(TrafficThreshold) * 1073741824
}
ExpireThreshold, err := t.settingService.GetTgExpireDiff()
if err == nil && ExpireThreshold > 0 {
exDiff = int64(ExpireThreshold) * 84600000
}
inbounds, err := t.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("Unable to load Inbounds", err)
}
for _, inbound := range inbounds {
if inbound.Enable {
if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) ||
(inbound.Total > 0 && (inbound.Total-inbound.Up+inbound.Down < trDiff)) {
exhaustedInbounds = append(exhaustedInbounds, *inbound)
}
if len(inbound.ClientStats) > 0 {
for _, client := range inbound.ClientStats {
if client.Enable {
if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) ||
(client.Total > 0 && (client.Total-client.Up+client.Down < trDiff)) {
exhaustedClients = append(exhaustedClients, client)
}
} else {
disabledClients = append(disabledClients, client)
}
}
}
} else {
disabledInbounds = append(disabledInbounds, *inbound)
}
}
output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
if len(exhaustedInbounds) > 0 {
output += "Exhausted Inbounds:\r\n"
for _, inbound := range exhaustedInbounds {
output += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\nTraffic: %s (↑%s,↓%s)\r\n", inbound.Remark, inbound.Port, common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
if inbound.ExpiryTime == 0 {
output += "Expire date: ♾Unlimited\r\n \r\n"
} else {
output += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
}
}
output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
if len(exhaustedClients) > 0 {
output += "Exhausted Clients:\r\n"
for _, traffic := range exhaustedClients {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output += fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire date: %s\r\n \r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
}
}
return output
}
func (t *Tgbot) sendBackup(chatId int64) {
sendingTime := time.Now().Format("2006-01-02 15:04:05")
t.SendMsgToTgbot(chatId, "Backup time: "+sendingTime)
file := tgbotapi.FilePath(config.GetDBPath())
msg := tgbotapi.NewDocument(chatId, file)
_, err := bot.Send(msg)
if err != nil {
logger.Warning("Error in uploading backup: ", err)
}
file = tgbotapi.FilePath(xray.GetConfigPath())
msg = tgbotapi.NewDocument(chatId, file)
_, err = bot.Send(msg)
if err != nil {
logger.Warning("Error in uploading config.json: ", err)
}
}

View File

@@ -46,8 +46,7 @@ func (s *UserService) UpdateUser(id int, username string, password string) error
db := database.GetDB()
return db.Model(model.User{}).
Where("id = ?", id).
Update("username", username).
Update("password", password).
Updates(map[string]interface{}{"username": username, "password": password}).
Error
}

View File

@@ -160,5 +160,5 @@ func (s *XrayService) SetToNeedRestart() {
}
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
return isNeedXrayRestart.CAS(true, false)
return isNeedXrayRestart.CompareAndSwap(true, false)
}

View File

@@ -69,6 +69,8 @@
"memory" = "Memory"
"hard" = "Hard disk"
"xrayStatus" = "xray Status"
"stopXray" = "Stop"
"restartXray" = "Restart"
"xraySwitch" = "Switch Version"
"xraySwitchClick" = "Click on the version you want to switch"
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
@@ -126,6 +128,20 @@
"keyContent" = "Key content"
"clickOnQRcode" = "Click on QR Code to Copy"
"client" = "Client"
"export" = "Export links"
[pages.client]
"add" = "Add client"
"edit" = "Edit client"
"submitAdd" = "Add client"
"submitEdit" = "Save changes"
"clientCount" = "Number of clients"
"bulk" = "Add bulk"
"method" = "Method"
"first" = "First"
"last" = "Last"
"prefix" = "Prefix"
"postfix" = "postfix"
[pages.inbounds.toasts]
"obtain" = "Obtain"
@@ -189,10 +205,18 @@
"telegramBotEnableDesc" = "Restart the panel to take effect"
"telegramToken" = "Telegram Token"
"telegramTokenDesc" = "Restart the panel to take effect"
"telegramChatId" = "Telegram ChatId"
"telegramChatIdDesc" = "Restart the panel to take effect"
"telegramChatId" = "Telegram Admin ChatIds"
"telegramChatIdDesc" = "Multi chatIDs separated by comma. Restart the panel to take effect"
"telegramNotifyTime" = "Telegram bot notification time"
"telegramNotifyTimeDesc" = "Using Crontab timing format, restart the panel to take effect"
"telegramNotifyTimeDesc" = "Using Crontab timing format. Restart the panel to take effect"
"tgNotifyBackup" = "Database backup"
"tgNotifyBackupDesc" = "Sending database backup file with report notification. Restart the panel to take effect"
"tgNotifyExpireTimeDiff" = "Remained time threshold"
"tgNotifyExpireTimeDiffDesc" = "This telegram bot will send you a notification before expiration (unit:day)"
"tgNotifyTrafficDiff" = "Remained traffic threshold"
"tgNotifyTrafficDiffDesc" = "This telegram bot will send you a notification before finishing traffic (unit:GB)"
"tgNotifyCpu" = "CPU percentage alert threshold"
"tgNotifyCpuDesc" = "This telegram bot will send you a notification if CPU usage is more than this percentage (unit:%)"
"timeZonee" = "Time Zone"
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect"

View File

@@ -69,12 +69,14 @@
"memory" = "حافظه رم"
"hard" = "حافظه دیسک"
"xrayStatus" = "وضعیت Xray"
"stopXray" = "توقف"
"restartXray" = "شروع مجدد"
"xraySwitch" = "تغییر ورژن"
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
"operationHours" = "ساعت فعال"
"operationHoursDesc" = "ساعت فعال بعد از شروع سیستم"
"systemLoad" = "سرعت لود سیستم"
"operationHours" = "مدت فعالیت"
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
"systemLoad" = "بار روی سیستم"
"connectionCount" = "تعداد کانکشن ها"
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
@@ -126,6 +128,20 @@
"keyContent" = "محتوای Private.key"
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
"client" = "کاربر"
"export" = "استخراج لینک‌ها"
[pages.client]
"add" = "کاربر جدید"
"edit" = "ویرایش کاربر"
"submitAdd" = "اضافه کردن"
"submitEdit" = "ذخیره تغییرات"
"clientCount" = "تعداد کاربران"
"bulk" = "انبوه سازی"
"method" = "روش"
"first" = "از"
"last" = "تا"
"prefix" = "پیشوند"
"postfix" = "پسوند"
[pages.inbounds.toasts]
"obtain" = "Obtain"
@@ -189,10 +205,18 @@
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramChatId" = "آی دی تلگرام مدیریت . از ربات @getidsbot آی دی خود را دریافت کنید"
"telegramChatIdDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"tgNotifyExpireTimeDiff" = "آستانه زمان باقی مانده"
"tgNotifyExpireTimeDiffDesc" = "این ربات تلگرام قبل از انقضا برای شما پیام ارسال می کند (واحد: روز)"
"tgNotifyTrafficDiff" = "آستانه ترافیک باقی مانده"
"tgNotifyTrafficDiffDesc" = "این ربات تلگرام قبل از اتمام ترافیک برای شما پیام ارسال می کند (واحد: گیگابایت)"
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
"timeZonee" = "منظقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"

View File

@@ -69,6 +69,8 @@
"memory" = "内存"
"hard" = "硬盘"
"xrayStatus" = "xray 状态"
"stopXray" = "停止"
"restartXray" = "重启"
"xraySwitch" = "切换版本"
"xraySwitchClick" = "点击你想切换的版本"
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
@@ -126,6 +128,20 @@
"keyContent" = "密钥内容"
"clickOnQRcode" = "点击二维码复制"
"client" = "客户"
"export" = "导出链接"
[pages.client]
"add" = "添加客户端"
"edit" = "编辑客户"
"submitAdd" = "添加客户端"
"submitEdit" = "保存修改"
"clientCount" = "客户数量"
"bulk" = "批量创建"
"method" = "方法"
"first" = "第一"
"last" = "最后"
"prefix" = "前缀"
"postfix" = "后缀"
[pages.inbounds.toasts]
"obtain" = "获取"
@@ -189,10 +205,18 @@
"telegramBotEnableDesc" = "重启面板生效"
"telegramToken" = "电报机器人TOKEN"
"telegramTokenDesc" = "重启面板生效"
"telegramChatId" = "电报机器人ChatId"
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
"telegramChatIdDesc" = "重启面板生效"
"telegramNotifyTime" = "电报机器人通知时间"
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
"tgNotifyExpireTimeDiff" = "剩余时间阈值"
"tgNotifyExpireTimeDiffDesc" = "这个 talegram bot 会在到期前给你发送通知(单位:天)"
"tgNotifyTrafficDiff" = "剩余流量阈值"
"tgNotifyTrafficDiffDesc" = "这个 talegram bot 会在流量结束前给你发送通知单位GB"
"tgNotifyCpu" = "CPU 百分比警报阈值"
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
"timeZonee" = "时区"
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"

View File

@@ -21,11 +21,11 @@ import (
"x-ui/web/network"
"x-ui/web/service"
"github.com/BurntSushi/toml"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/pelletier/go-toml/v2"
"github.com/robfig/cron/v3"
"golang.org/x/text/language"
)
@@ -88,7 +88,7 @@ type Server struct {
xrayService service.XrayService
settingService service.SettingService
inboundService service.InboundService
tgbotService service.Tgbot
cron *cron.Cron
@@ -298,19 +298,19 @@ func (s *Server) startTask() {
if err != nil {
logger.Warning("start xray failed:", err)
}
// 每 30 秒检查一次 xray 是否在运行
// Check whether xray is running every 30 seconds
s.cron.AddJob("@every 30s", job.NewCheckXrayRunningJob())
go func() {
time.Sleep(time.Second * 5)
// 每 10 秒统计一次流量,首次启动延迟 5 秒,与重启 xray 的时间错开
// Statistics every 10 seconds, start the delay for 5 seconds for the first time, and staggered with the time to restart xray
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
}()
// 每 30 秒检查一次 inbound 流量超出和到期的情况
// Check the inbound traffic every 30 seconds that the traffic exceeds and expires
s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
// 每一天提示一次流量情况,上海时间8点30
// Make a traffic condition every day, 8:30
var entry cron.EntryID
isTgbotenabled, err := s.settingService.GetTgbotenabled()
if (err == nil) && (isTgbotenabled) {
@@ -320,20 +320,25 @@ func (s *Server) startTask() {
runtime = "@daily"
}
logger.Infof("Tg notify enabled,run at %s", runtime)
entry, err = s.cron.AddJob(runtime, job.NewStatsNotifyJob())
_, err = s.cron.AddJob(runtime, job.NewStatsNotifyJob())
if err != nil {
logger.Warning("Add NewStatsNotifyJob error", err)
return
}
// listen for TG bot income messages
go job.NewStatsNotifyJob().OnReceive()
// Check CPU load and alarm to TgBot if threshold passes
cpuThreshold, err := s.settingService.GetTgCpu()
if (err == nil) && (cpuThreshold > 0) {
s.cron.AddJob("@every 10s", job.NewCheckCpuJob())
}
} else {
s.cron.Remove(entry)
}
}
func (s *Server) Start() (err error) {
//这是一个匿名函数,没没有函数名
//This is an anonymous function, no function name
defer func() {
if err != nil {
s.Stop()
@@ -403,6 +408,12 @@ func (s *Server) Start() (err error) {
s.httpServer.Serve(listener)
}()
isTgbotenabled, err := s.settingService.GetTgbotenabled()
if (err == nil) && (isTgbotenabled) {
tgBot := s.tgbotService.NewTgbot()
tgBot.Start()
}
return nil
}
@@ -412,6 +423,9 @@ func (s *Server) Stop() error {
if s.cron != nil {
s.cron.Stop()
}
if s.tgbotService.IsRunnging() {
s.tgbotService.Stop()
}
var err1 error
var err2 error
if s.httpServer != nil {

59
x-ui.sh
View File

@@ -20,46 +20,41 @@ function LOGI() {
# check root
[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1
# check os
if [[ -f /etc/redhat-release ]]; then
release="centos"
elif cat /etc/issue | grep -Eqi "debian"; then
release="debian"
elif cat /etc/issue | grep -Eqi "ubuntu"; then
release="ubuntu"
elif cat /etc/issue | grep -Eqi "centos|red hat|redhat"; then
release="centos"
elif cat /proc/version | grep -Eqi "debian"; then
release="debian"
elif cat /proc/version | grep -Eqi "ubuntu"; then
release="ubuntu"
elif cat /proc/version | grep -Eqi "centos|red hat|redhat"; then
release="centos"
# Check OS and set release variable
if [[ -f /etc/os-release ]]; then
source /etc/os-release
release=$ID
elif [[ -f /usr/lib/os-release ]]; then
source /usr/lib/os-release
release=$ID
else
LOGE "check system OS failed,please contact with author! \n" && exit 1
echo "Failed to check the system OS, please contact the author!" >&2
exit 1
fi
echo "The OS release is: $release"
os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
# os version
if [[ -f /etc/os-release ]]; then
os_version=$(awk -F'[= ."]' '/VERSION_ID/{print $3}' /etc/os-release)
fi
if [[ -z "$os_version" && -f /etc/lsb-release ]]; then
os_version=$(awk -F'[= ."]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release)
fi
if [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 7 ]]; then
echo -e "${red} Please use CentOS 7 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 18 ]]; then
echo -e "${red}please use Ubuntu 18 or higher version${plain}\n" && exit 1
fi
if [[ x"${release}" == x"centos" ]]; then
if [[ ${os_version} -le 6 ]]; then
LOGE "please use CentOS 7 or higher version! \n" && exit 1
elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then
echo -e "${red}please use Fedora 36 or higher version${plain}\n" && exit 1
fi
elif [[ x"${release}" == x"ubuntu" ]]; then
if [[ ${os_version} -lt 16 ]]; then
LOGE "please use Ubuntu 16 or higher version\n" && exit 1
fi
elif [[ x"${release}" == x"debian" ]]; then
elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 8 ]]; then
LOGE "please use Debian 8 or higher version\n" && exit 1
echo -e "${red} Please use Debian 8 or higher ${plain}\n" && exit 1
fi
fi

View File

@@ -1,12 +1,12 @@
package xray
type ClientTraffic struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
InboundId int `json:"inboundId" form:"inboundId"`
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
InboundId int `json:"inboundId" form:"inboundId"`
Enable bool `json:"enable" form:"enable"`
Email string `json:"email" form:"email" gorm:"unique"`
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
Email string `json:"email" form:"email" gorm:"unique"`
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
Total int64 `json:"total" form:"total"`
}