253 Commits

Author SHA1 Message Date
JinU Choi
c2bb653ee1 Merge pull request #124 from dalbodeule/develop
if account deleted?
2025-03-31 18:47:13 +09:00
dalbodeule
8ab1dc585e if account deleted? 2025-03-31 18:43:12 +09:00
JinU Choi
22d97c6604 Merge pull request #123 from dalbodeule/debug
debug done
2025-01-09 00:12:01 +09:00
dalbodeule
e653af7853 debug chzzk login 3 2025-01-09 00:04:52 +09:00
JinU Choi
0951169ce1 Merge pull request #122 from dalbodeule/develop
debug chzzk login 2
2025-01-09 00:01:13 +09:00
dalbodeule
9cf85a7bef debug chzzk login 2 2025-01-08 23:58:11 +09:00
JinU Choi
6bf80c309f Merge pull request #121 from dalbodeule/develop
debug chzzk login
2025-01-08 23:45:30 +09:00
dalbodeule
2c686e5777 debug chzzk login 2025-01-08 23:42:21 +09:00
JinU Choi
65fae33467 Merge pull request #120 from dalbodeule/develop
login method changed to chzzk login
2025-01-08 23:17:45 +09:00
dalbodeule
d3ed6c2d86 login method changed to chzzk login 2025-01-08 23:13:04 +09:00
JinU Choi
d576d085f7 Merge pull request #119 from dalbodeule/develop
add botIsDisabled, welcomeMessageDisabled.
2024-12-09 14:51:56 +09:00
dalbodeule
eccf1a29bc add botIsDisabled, welcomeMessageDisabled. 2024-12-09 14:47:41 +09:00
dalbodeule
4fca9df9c2 debugs... 4 2024-11-25 20:04:50 +09:00
dalbodeule
c14fc53dee debugs... 3 2024-11-25 19:58:17 +09:00
dalbodeule
768eadd561 debugs... 2024-11-25 19:56:22 +09:00
dalbodeule
9396e69570 debugs... 2024-11-25 18:50:23 +09:00
dalbodeule
8dd8f33cdd dependencies update 2024-11-16 20:35:30 +09:00
dalbodeule
ba1be00390 add one options.
- disableStartupMessage
2024-11-16 20:22:06 +09:00
JinU Choi
99686496b4 Merge pull request #118 from dalbodeule/debug
debug on WSSongListRoutes.kt
2024-09-24 11:28:58 +09:00
dalbodeule
ca2089631a debug on WSSongListRoutes.kt
- debug re-tx logics
2024-09-24 10:48:50 +09:00
dalbodeule
c9ceaf01fc debug on WSSongListRoutes.kt
- debug re-tx logics
2024-09-24 10:42:39 +09:00
dalbodeule
94e226fcab debug on WSSongListRoutes.kt
- add re-tx logics
- add ACK enum value
- else other improve
2024-09-24 10:35:11 +09:00
dalbodeule
3e0246771e debug on ApiCommandRoutes.kt, SongList.kt
- ApiCommandRoutes.kt is CommandReloadEvent post 4 times.
- SongList.kt is song_list.req_name length 20 to 80(byte)
2024-09-14 18:36:24 +09:00
dalbodeule
c0cec43f39 debug on loading (7x) 2024-08-28 22:16:14 +09:00
dalbodeule
b7985ff72c debug on loading (6x) 2024-08-28 21:42:46 +09:00
dalbodeule
be057c59e1 debug on loading (5x) 2024-08-28 21:37:25 +09:00
dalbodeule
5450d5039a debug on loading (4x) 2024-08-28 21:26:24 +09:00
dalbodeule
ae850b93d0 debug on loading (3x) 2024-08-28 21:20:43 +09:00
dalbodeule
5226c5b60d debug on eagerLoading (2x) 2024-08-28 21:07:49 +09:00
dalbodeule
534aaecb6e debug on eagerLoading (1x) 2024-08-28 21:01:35 +09:00
dalbodeule
a7069046f6 managers relation is fixed (9x) 2024-08-28 20:39:23 +09:00
dalbodeule
593546c89b managers relation is fixed (8x) 2024-08-28 17:58:35 +09:00
dalbodeule
f5133c4551 managers relation is fixed (6x) 2024-08-28 17:39:35 +09:00
dalbodeule
7c2f5f90fe managers relation is fixed (5x) 2024-08-28 17:26:24 +09:00
dalbodeule
3e0f63a3d1 managers relation is fixed (4x) 2024-08-28 17:24:02 +09:00
dalbodeule
5d9b6a7e5e managers relation is fixed (3x) 2024-08-28 17:16:48 +09:00
dalbodeule
1ec899a55a managers relation is fixed (2x) 2024-08-28 17:02:22 +09:00
dalbodeule
447858a48c managers relation is fixed 2024-08-28 16:21:34 +09:00
JinU Choi
7784130be3 Merge pull request #117 from dalbodeule/develop
Manager function to web
2024-08-28 16:01:40 +09:00
dalbodeule
82c4f7b021 managers table is dropped. 2024-08-28 15:58:17 +09:00
dalbodeule
f1dc591b55 some fix on Manager functions. 2024-08-28 15:55:47 +09:00
JinU Choi
bf01af04c6 Merge pull request #116 from dalbodeule/develop
some fix on wsTimerRoutes
2024-08-28 10:46:09 +09:00
dalbodeule
3806b0f824 some fix on wsTimerRoutes 2024-08-28 10:42:41 +09:00
JinU Choi
3cea492563 Merge pull request #115 from dalbodeule/develop
some fix on wsSongListRoutes
2024-08-24 15:13:02 +09:00
dalbodeule
1186f647d2 some fix on songList logics 2024-08-24 15:10:44 +09:00
dalbodeule
b87cf8cbfb some fix on wsSongListRoutes
- song remove from list(database) logic add
2024-08-24 15:08:05 +09:00
JinU Choi
fc94ffc03c Merge pull request #114 from dalbodeule/develop
some fix on WSSongListRoutes
2024-08-24 14:34:52 +09:00
dalbodeule
9f4f8e84aa some fix on WSSongListRoutes
- add delUrl to SongResponse
2024-08-24 14:32:38 +09:00
JinU Choi
36a5b24e4c Merge pull request #113 from dalbodeule/develop
some fix on SongSResponseDTO
2024-08-24 13:59:13 +09:00
dalbodeule
22bbd05aff some fix on SongSResponseDTO
- add @Serializable annotation
2024-08-24 13:57:59 +09:00
JinU Choi
ae5e520dea Merge pull request #112 from dalbodeule/develop
fix SongLists
2024-08-24 13:53:32 +09:00
dalbodeule
8bbd163458 fix some event-emitter 2024-08-24 13:51:08 +09:00
dalbodeule
36d0ed82d6 fix SongLists
- add currentSong object
- change some event, dto.
2024-08-24 13:31:50 +09:00
JinU Choi
8e1e472bda Merge pull request #111 from dalbodeule/develop
fix discord alert.
2024-08-22 16:33:34 +09:00
dalbodeule
ef983cbe22 fix discord alert.
- Number to Long change.
2024-08-22 16:29:53 +09:00
JinU Choi
91e22107c9 Merge pull request #110 from dalbodeule/develop
fix some endpoints.
2024-08-21 19:10:28 +09:00
dalbodeule
a733f56d8c fix some endpoints.
- In apiDiscordRoutes, guildId and channelId return string.
- In wsSongListRoutes, retry 5 times, some logic changed.
2024-08-21 19:08:23 +09:00
JinU Choi
e855566193 Merge pull request #109 from dalbodeule/develop
fix wsSongRoutes
2024-08-20 11:31:48 +09:00
dalbodeule
327862f6a5 fix wsSongRoutes
- re-tx logic add
- if re-tx logic fail 3 times, disconnect websocket.
2024-08-20 11:26:55 +09:00
JinU Choi
3972530c79 Merge pull request #108 from dalbodeule/develop
fix wsSongListRoutes
2024-08-19 17:27:29 +09:00
dalbodeule
e3c0266253 fix wsSongListRoutes
- duplicated session is not permited.
- re-tx logic add
2024-08-19 17:24:42 +09:00
JinU Choi
7553e8be7e Merge pull request #107 from dalbodeule/debug
add "!명령어" commandListCommand.
2024-08-18 18:36:26 +09:00
dalbodeule
764714c33a add "!명령어" commandListCommand. 2024-08-18 18:34:49 +09:00
JinU Choi
0813955b12 Merge pull request #106 from dalbodeule/develop
command list pages.
2024-08-18 18:28:49 +09:00
dalbodeule
5c3c69c585 command list pages.
- set /commands/{uid} page to not require login.
2024-08-18 18:25:46 +09:00
JinU Choi
4a754569cd Merge pull request #105 from dalbodeule/develop
chisu playlist fix
2024-08-16 08:46:26 +09:00
dalbodeule
89dda9742a chisu playlist fix
- if youtube video limited 10-min
- add youtube music regex.
2024-08-16 08:42:53 +09:00
JinU Choi
0d92027acc Merge pull request #104 from dalbodeule/develop
asdf4
2024-08-15 20:34:39 +09:00
dalbodeule
35d8759ee3 asdf4 2024-08-15 20:32:31 +09:00
JinU Choi
97b29ad76b Merge pull request #103 from dalbodeule/develop
asdf3
2024-08-15 19:54:22 +09:00
dalbodeule
8a3c64ab46 asdf3 2024-08-15 19:53:40 +09:00
JinU Choi
325860523f Merge pull request #102 from dalbodeule/develop
asdf2
2024-08-15 19:48:55 +09:00
dalbodeule
1bea333d9d asdf2 2024-08-15 19:48:31 +09:00
JinU Choi
09ed95a3b3 Merge pull request #101 from dalbodeule/develop
asdf
2024-08-15 19:47:37 +09:00
dalbodeule
c4db370c91 asdf 2024-08-15 19:45:36 +09:00
dalbodeule
6cb915a3e1 chzzkHandler fix (2x) 2024-08-15 19:31:03 +09:00
dalbodeule
23c28ce643 chzzkHandler fix 2024-08-15 19:26:26 +09:00
JinU Choi
032a03b3ce Merge pull request #100 from dalbodeule/develop
error handler errorlog add.
2024-08-15 19:19:04 +09:00
dalbodeule
6ba1d1bca9 error handler errorlog add. 2024-08-15 19:18:30 +09:00
JinU Choi
599a532d81 Merge pull request #99 from dalbodeule/develop
chzzkHandler fix
2024-08-15 17:37:40 +09:00
dalbodeule
a3b69acc18 chzzkHandler fix 2024-08-15 17:35:45 +09:00
JinU Choi
b889599558 Merge pull request #98 from dalbodeule/develop
command, ws ping message fix
2024-08-15 16:58:12 +09:00
dalbodeule
20417745d4 command, ws ping message fix
- remove commands (command CRUD, hook, alert)
- ws ping message fix
2024-08-15 16:56:31 +09:00
JinU Choi
ec0d811dbc Merge pull request #97 from dalbodeule/develop
apiDiscordRoutes add.
2024-08-15 13:50:47 +09:00
dalbodeule
9a303ff342 apiDiscordRoutes add.
- add POST /discord/{uid} endpoints.
2024-08-15 13:49:18 +09:00
JinU Choi
3fe36daf14 Merge pull request #96 from dalbodeule/develop
apiDiscordRoutes fix. (18x)
2024-08-15 11:23:36 +09:00
dalbodeule
a56f504418 apiDiscordRoutes fix. (18x)
- delete some debugging codes.
2024-08-15 11:21:55 +09:00
JinU Choi
b2f4549e7f Merge pull request #95 from dalbodeule/develop
apiDiscordRoutes fix. (17x)
2024-08-15 11:18:00 +09:00
dalbodeule
15d6f3d9b1 apiDiscordRoutes fix. (17x)
- wrong type definition(GuildChannel.type) fix
2024-08-15 11:13:44 +09:00
JinU Choi
6e4770a8de Merge pull request #94 from dalbodeule/develop
apiDiscordRoutes fix. (16x)
2024-08-15 11:08:59 +09:00
dalbodeule
f85cbb8111 apiDiscordRoutes fix. (16x)
- wrong type definition(GuildRoles.color) fix
2024-08-15 11:07:34 +09:00
JinU Choi
4945f4cfbe Merge pull request #93 from dalbodeule/develop
apiDiscordRoutes fix. (15x)
2024-08-15 11:03:04 +09:00
dalbodeule
34376ab720 apiDiscordRoutes fix. (15x)
- some logic changed.
2024-08-15 11:01:21 +09:00
JinU Choi
8903b7660d Merge pull request #92 from dalbodeule/develop
apiDiscordRoutes fix. (14x)
2024-08-15 10:53:53 +09:00
dalbodeule
c38af5a511 apiDiscordRoutes fix. (14x)
- some logic changed.
2024-08-15 10:52:06 +09:00
dalbodeule
5fbf47507c apiDiscordRoutes fix. (13x)
- some logic changed.
2024-08-15 10:49:45 +09:00
JinU Choi
68c76da9c7 Merge pull request #91 from dalbodeule/develop
apiDiscordRoutes fix. (11x)
2024-08-15 10:41:59 +09:00
dalbodeule
c0e65ad771 apiDiscordRoutes fix. (11x)
- some logic changed.
2024-08-15 10:40:27 +09:00
JinU Choi
46e084d258 Merge pull request #90 from dalbodeule/develop
apiDiscordRoutes fix. (10x)
2024-08-15 10:36:56 +09:00
dalbodeule
355e07bf21 apiDiscordRoutes fix. (10x)
- some logic changed.
2024-08-15 10:34:27 +09:00
JinU Choi
090cb8ade2 Merge pull request #89 from dalbodeule/develop
apiDiscordRoutes fix. (9x)
2024-08-15 10:31:53 +09:00
dalbodeule
1ac716cc06 apiDiscordRoutes fix. (9x)
- some logic changed.
2024-08-15 10:30:15 +09:00
JinU Choi
380e36188f Merge pull request #88 from dalbodeule/develop
apiDiscordRoutes fix. (7x)
2024-08-15 10:25:37 +09:00
dalbodeule
f16e3658ea apiDiscordRoutes fix. (7x)
- some logic changed.
2024-08-15 10:24:19 +09:00
JinU Choi
bea6905b7b Merge pull request #87 from dalbodeule/develop
apiDiscordRoutes fix. (6x)
2024-08-15 10:18:52 +09:00
dalbodeule
67b4209d13 apiDiscordRoutes fix. (6x)
- add debug code
2024-08-15 10:17:20 +09:00
JinU Choi
63318a9ab6 Merge pull request #86 from dalbodeule/develop
apiDiscordRoutes fix. (5x)
2024-08-15 10:12:22 +09:00
dalbodeule
a9c4398be3 apiDiscordRoutes fix. (5x)
- add debug code
2024-08-15 10:10:45 +09:00
JinU Choi
54905803bb Merge pull request #85 from dalbodeule/develop
apiDiscordRoutes fix. (4x)
2024-08-15 07:39:17 +09:00
dalbodeule
e7bdb360fc apiDiscordRoutes fix. (4x)
- fix server insights.
2024-08-15 07:29:27 +09:00
JinU Choi
c7ddefa38d Merge pull request #84 from dalbodeule/develop
apiDiscordRoutes fix. (3x)
2024-08-15 07:24:24 +09:00
dalbodeule
3285341b9c apiDiscordRoutes fix. (3x)
- fix server insights.
2024-08-15 07:22:36 +09:00
JinU Choi
3d2e9a8cb0 Merge pull request #83 from dalbodeule/develop
apiDiscordRoutes fix.
2024-08-15 07:16:09 +09:00
dalbodeule
456e447a5e apiDiscordRoutes fix. (2x)
- add guidChannels.
2024-08-15 07:14:35 +09:00
dalbodeule
1a3282d46c apiDiscordRoutes fix.
- add guidChannels.
2024-08-15 07:13:37 +09:00
JinU Choi
568c3638c6 Merge pull request #82 from dalbodeule/develop
apiDiscordRoutes fix.
2024-08-15 07:04:09 +09:00
dalbodeule
c87eeaa9e8 apiDiscordRoutes fix.
- add guildRole appends.
2024-08-15 07:02:39 +09:00
JinU Choi
58fd4684b8 Merge pull request #81 from dalbodeule/develop
apiDiscordRoutes fix.
2024-08-15 06:54:06 +09:00
dalbodeule
91422aefce apiDiscordRoutes fix.
- explicitNulls = false
2024-08-15 06:52:36 +09:00
JinU Choi
85e253af1a Merge pull request #80 from dalbodeule/develop
apiDiscordRoutes fix.
2024-08-15 06:49:23 +09:00
dalbodeule
d0267ec50a apiDiscordRoutes fix.
- roles is missing, to emptyList()
2024-08-15 06:47:56 +09:00
JinU Choi
84d7349c98 Merge pull request #79 from dalbodeule/develop
apiDiscordRoutes fix.
2024-08-15 06:42:04 +09:00
dalbodeule
d90790f742 apiDiscordRoutes fix.
- roles is nullable.
2024-08-15 06:40:38 +09:00
JinU Choi
595df4ecaa Merge pull request #78 from dalbodeule/develop
apiDiscordRoutes fix.
2024-08-15 06:37:08 +09:00
dalbodeule
3572a5c5db apiDiscordRoutes fix.
- add respond to /discord endpoints.
- fix /guild/{gid} endpoints.
2024-08-15 06:33:52 +09:00
JinU Choi
62449959a5 Merge pull request #77 from dalbodeule/develop
Discord guild related bug fix, improve
2024-08-15 06:15:25 +09:00
dalbodeule
85d728df8a get bot's guilds. 2024-08-15 06:11:48 +09:00
dalbodeule
ce9be7e47f get guild roles with getGuilds. 2024-08-15 06:08:22 +09:00
JinU Choi
5fb25089d5 Merge pull request #76 from dalbodeule/develop
debug some codes...
2024-08-14 22:17:58 +09:00
dalbodeule
66c1252312 debug some codes... 2024-08-14 22:15:36 +09:00
JinU Choi
5ff78f4532 Merge pull request #75 from dalbodeule/develop
SongType.NEXT to songList.isEmpty() true/false.
2024-08-14 22:10:33 +09:00
dalbodeule
c2e546da88 SongType.NEXT to songList.isEmpty() true/false. 2024-08-14 22:08:58 +09:00
dalbodeule
5498135257 asdf16 2024-08-14 21:23:53 +09:00
dalbodeule
7d12985801 asdf15 2024-08-14 21:18:40 +09:00
dalbodeule
ac2cd3725c asdf14 2024-08-14 21:13:13 +09:00
dalbodeule
f13412d77a asdf13 2024-08-14 20:50:58 +09:00
dalbodeule
ab32fcc7dc asdf12 2024-08-14 20:40:20 +09:00
dalbodeule
0e3264a9f3 asdf11 2024-08-14 20:28:17 +09:00
dalbodeule
cb5bbf73c3 asdf10 2024-08-14 20:20:50 +09:00
dalbodeule
1e1cd50e52 asdf9 2024-08-14 20:18:06 +09:00
dalbodeule
370e4519c2 asdf8 2024-08-14 20:13:47 +09:00
dalbodeule
c6a1c229f7 asdf7 2024-08-14 20:10:50 +09:00
dalbodeule
1a0067cca9 asdf6 2024-08-14 20:05:18 +09:00
dalbodeule
a7628f0a7e asdf5 2024-08-14 20:02:06 +09:00
dalbodeule
b3ba7767f6 asdf4 2024-08-14 19:54:32 +09:00
dalbodeule
1679d8cd18 asdf3 2024-08-14 19:39:47 +09:00
dalbodeule
4d0345be9e asdf2 2024-08-14 19:32:30 +09:00
dalbodeule
40f1df7ba6 asdf 2024-08-14 19:28:14 +09:00
JinU Choi
9ab004d646 Merge pull request #74 from dalbodeule/develop
avoid discord limits.
2024-08-14 19:20:20 +09:00
dalbodeule
dd8e5065df avoid discord limits.
- fix DiscordRatelimits
2024-08-14 19:18:50 +09:00
JinU Choi
ce24c6b0c5 Merge pull request #73 from dalbodeule/develop
avoid discord limits.
2024-08-14 19:14:03 +09:00
dalbodeule
7a9eccd293 avoid discord limits.
- fix DiscordRatelimits
2024-08-14 19:12:05 +09:00
JinU Choi
12d3396630 Merge pull request #72 from dalbodeule/develop
avoid discord limits.
2024-08-14 18:49:14 +09:00
dalbodeule
bdf1420455 avoid discord limits.
- fix DiscordRatelimits
2024-08-14 18:46:57 +09:00
JinU Choi
61b27ade29 Merge pull request #71 from dalbodeule/develop
avoid discord limits.
2024-08-14 18:13:41 +09:00
dalbodeule
f958912d43 avoid discord limits.
- add DiscordRatelimits
2024-08-14 18:12:00 +09:00
JinU Choi
9f15e8f9fb Merge pull request #70 from dalbodeule/develop
avoid discord limits.
2024-08-14 17:57:21 +09:00
dalbodeule
dfc84452af avoid discord limits. 2024-08-14 17:54:49 +09:00
JinU Choi
3ab6a4031a Merge pull request #69 from dalbodeule/develop
debug DiscordGuildCache (x2)
2024-08-14 17:37:46 +09:00
dalbodeule
0e5b99bfb9 debug DiscordGuildCache (x2)
- avoid ratelimit.
- cache logic fix
- on login, add Cache
2024-08-14 17:35:54 +09:00
JinU Choi
b241a0cf73 Merge pull request #68 from dalbodeule/develop
debug DiscordGuildCache (x2)
2024-08-14 17:13:32 +09:00
dalbodeule
4c8c6daae1 debug DiscordGuildCache (x2)
- avoid ratelimit.
2024-08-14 17:10:54 +09:00
JinU Choi
74709a076d Merge pull request #67 from dalbodeule/develop
debug DiscordGuildCache
2024-08-14 17:08:23 +09:00
dalbodeule
8244dbebfa debug DiscordGuildCache
- avoid ratelimit.
2024-08-14 17:07:04 +09:00
JinU Choi
8aab3aa15b Merge pull request #66 from dalbodeule/develop
Develop
2024-08-14 17:00:03 +09:00
dalbodeule
c1553fd47a add DiscordGuildCache
- GET /discord endpoint, return guild information.
- GET /discord/{uid} to return null(404)
2024-08-14 16:56:25 +09:00
dalbodeule
167278e5cf add DiscordGuildCache
- add DiscordGuildCache
- GET /discord endpoint, return guild information.
2024-08-14 10:44:15 +09:00
JinU Choi
88c84c4610 Merge pull request #65 from dalbodeule/develop
debug some errors.
2024-08-14 05:59:00 +09:00
dalbodeule
07af664b0d debug some errors.
- ChzzkHandler.runStreamInfo debugs with startThread functions.
2024-08-14 05:57:30 +09:00
JinU Choi
1d5b726a89 Merge pull request #64 from dalbodeule/develop
debug some errors.
2024-08-14 05:52:38 +09:00
dalbodeule
4f093291f4 debug some errors.
- ChzzkHandler.runStreamInfo debugs with startThread functions.
2024-08-14 05:50:58 +09:00
JinU Choi
b4d1f59df1 Merge pull request #63 from dalbodeule/develop
debug some errors.
2024-08-14 05:46:57 +09:00
dalbodeule
b643ed83e7 debug some errors.
- ChzzkHandler.runStreamInfo debugs with startThread functions.
2024-08-14 05:45:15 +09:00
JinU Choi
35074d0aac Merge pull request #62 from dalbodeule/develop
debug some errors.
2024-08-13 20:28:57 +09:00
dalbodeule
b6a68fe35a debug some errors.
- ChzzkHandler.runStreamInfo debugs with startThread functions.
2024-08-13 20:25:49 +09:00
JinU Choi
d8c27b3238 Merge pull request #61 from dalbodeule/develop
debug some errors.
2024-08-13 19:23:21 +09:00
dalbodeule
910886f8b5 debug some errors.
- ChzzkHandler.runStreamInfo debugs with startThread functions.
2024-08-13 19:20:16 +09:00
JinU Choi
4c15aac295 Merge pull request #60 from dalbodeule/develop
debug some errors.
2024-08-13 07:18:05 +09:00
dalbodeule
f994972fac debug some errors.
- wsSongListRoutes to isNotEmpty(), access list[0]
- ChzzkHandler.runStreamInfo debugs with startThread functions.
2024-08-13 07:07:40 +09:00
dalbodeule
355b56a6cf debug discord login (16x)
- remove unless println
- DONE
2024-08-11 17:09:02 +09:00
dalbodeule
27ee9b811c debug discord login (15x)
- session add owned server.
2024-08-11 17:07:42 +09:00
dalbodeule
c49ba73ec4 debug discord login (14x)
- DiscordGuildLIstAPI fix
2024-08-11 17:03:40 +09:00
dalbodeule
c05521d23f debug discord login (13x)
- DiscordGuildLIstAPI fix
2024-08-11 17:00:24 +09:00
dalbodeule
f407310348 debug discord login (12x)
- DiscordGuildLIstAPI fix
2024-08-11 16:55:38 +09:00
dalbodeule
15b40bbc54 debug discord login (11x)
- getUserGuilds fix (2x)
2024-08-11 16:52:35 +09:00
dalbodeule
18ad1c8b83 debug discord login (10x)
- getUserGuilds fix
2024-08-11 16:48:43 +09:00
dalbodeule
7d74fb87c5 debug discord login (9x)
- add identify scopes
2024-08-11 16:44:00 +09:00
dalbodeule
e68794ce33 debug discord login (8x) 2024-08-11 16:39:35 +09:00
dalbodeule
6ccbb4f3f7 debug discord login (7x) 2024-08-11 16:34:50 +09:00
dalbodeule
a1e44c3bb1 debug discord login (6x) 2024-08-11 16:28:26 +09:00
dalbodeule
25b835456e debug discord login (5x) 2024-08-11 16:02:51 +09:00
dalbodeule
299ecd2943 debug discord login (4x) 2024-08-11 16:00:03 +09:00
dalbodeule
dc7e05781f debug discord login (3x) 2024-08-11 15:56:47 +09:00
dalbodeule
b38080111c debug discord login (2x) 2024-08-11 15:52:36 +09:00
dalbodeule
eab30ff5c3 debug discord login 2024-08-11 15:48:01 +09:00
JinU Choi
977ca3e69f Merge pull request #59 from dalbodeule/develop
add discord login
2024-08-11 15:42:26 +09:00
dalbodeule
9c047d3d87 add discord login
- add discord login logics
- add discord api classes, functions.
- add current user's discord guilds. (if session has discordGuilds, return it else return null lists)
2024-08-11 15:39:38 +09:00
JinU Choi
2293596459 Merge pull request #58 from dalbodeule/develop
fix somes.
2024-08-10 21:54:19 +09:00
dalbodeule
dd628738f7 fix somes.
- command renamed "!노래추가" to "!신청곡", and its debugged.
- command "!노래시작" to user.discordId is null, send announce to chzzk chat.
- songlist function isDisabled value added.
2024-08-10 21:50:58 +09:00
JinU Choi
7e417b5d12 Merge pull request #57 from dalbodeule/develop
fix HookCommand, fix documentation
2024-08-10 18:06:24 +09:00
dalbodeule
ac7f9b3ad0 fix HookCommand, fix documentation
- fix HookCommand (.queue() typo)
- fix hook command.
2024-08-10 18:04:02 +09:00
JinU Choi
953e451597 Merge pull request #56 from dalbodeule/develop
fix ApiDiscordRoutes.kt
2024-08-10 17:36:46 +09:00
dalbodeule
0acccce720 fix ApiDiscordRoutes.kt
- add @Serializable annotation of DTO class
2024-08-10 17:35:18 +09:00
JinU Choi
42ac32b303 Merge pull request #55 from dalbodeule/develop
debug session (2x)
2024-08-10 17:30:59 +09:00
dalbodeule
a26586c6b4 debug session (2x)
- MariadbSessionStorage not found error fix.
2024-08-10 17:29:40 +09:00
JinU Choi
eab0107115 Merge pull request #54 from dalbodeule/develop
debug session
2024-08-10 17:27:18 +09:00
dalbodeule
a01827b055 debug session
- MariadbSessionStorage not found error fix.
2024-08-10 17:25:52 +09:00
JinU Choi
6f9fe9ee21 Merge pull request #53 from dalbodeule/develop
debug discordHook logics, session fix.
2024-08-10 17:19:35 +09:00
dalbodeule
2335a09391 debug discordHook logics, session fix.
- DiscordHook normal respond is not defined
- add MariadbSessionStorage
2024-08-10 17:16:36 +09:00
JinU Choi
ba028a01a5 Merge pull request #52 from dalbodeule/develop
add and debug discord hook logics
2024-08-10 16:57:42 +09:00
dalbodeule
e951ccee56 debug discordHook logics 2024-08-10 16:55:48 +09:00
dalbodeule
2d0e83efc2 add discord hook logics
- add ApiDiscordRoutes.kt
- modify HookCommand from RegisterCommand.kt
2024-08-10 16:54:32 +09:00
dalbodeule
6d7a6beb10 debug Chzzk-Streaminfo.
- add re-run code.
2024-08-10 13:15:16 +09:00
dalbodeule
b12e29674a debug some codes. 2024-08-10 00:39:23 +09:00
dalbodeule
9a91537e59 debug WSSongListRoutes.kt
- session deleted.
2024-08-09 19:17:27 +09:00
JinU Choi
18d28262d1 Merge pull request #51 from dalbodeule/develop
update naverMEAPI
2024-08-09 15:03:33 +09:00
dalbodeule
9110917e50 update naverMEAPI
- remove nickname, profileImage args.
2024-08-09 15:00:00 +09:00
JinU Choi
fd281b8271 Merge pull request #50 from dalbodeule/develop
Develop
2024-08-09 13:43:10 +09:00
dalbodeule
1b49780cae add ApiTimerRoutes endpoint 2024-08-09 13:40:52 +09:00
dalbodeule
94b87dc87e add getUserInfo function 2024-08-09 13:11:09 +09:00
JinU Choi
9e30ca1451 Merge pull request #49 from dalbodeule/develop
add ApiCommandRoutes.kt
2024-08-09 12:39:43 +09:00
dalbodeule
80dbf10e7e add ApiCommandRoutes.kt
- add GET/PUT/POST/DELETE endpoints.
2024-08-09 11:36:47 +09:00
dalbodeule
4c3300203b fix User.kt done 2024-08-08 21:08:29 +09:00
dalbodeule
042afde5c7 fix User.kt(7x)
- fun enable fix
2024-08-08 21:02:30 +09:00
dalbodeule
9dd1d1804a fix User.kt(6x)
- fun enable fix
2024-08-08 20:49:52 +09:00
dalbodeule
60aedc6954 fix User.kt(5x)
- fun enable fix
2024-08-08 20:46:10 +09:00
dalbodeule
e342a3aabb fix User.kt(4x)
- fun enable fix
2024-08-08 20:41:17 +09:00
dalbodeule
81c62d6c55 fix User.kt(3x)
- fun enable fix
2024-08-08 20:33:27 +09:00
dalbodeule
cbe1c64411 fix User.kt(2x)
- fun enable fix
2024-08-08 20:29:07 +09:00
dalbodeule
a6cd5ba8b5 fix User.kt
- fun enable fix
2024-08-08 20:26:02 +09:00
dalbodeule
0f175ab045 fix User.kt
- set "discord", "token" row to nullable.
2024-08-08 20:23:03 +09:00
dalbodeule
f2a871d664 fix User.kt
- set "discord", "token" row to nullable.
2024-08-08 20:18:49 +09:00
dalbodeule
80fa2d5029 add POST /user endpoints.
- POST /user {"chzzkUrl": String} to add chzzk account to bots.
- and it post event UserRegisterEvent
2024-08-08 19:36:58 +09:00
dalbodeule
d0840ad6a7 modify RegisterCommand
- register page is created.
2024-08-08 17:52:20 +09:00
dalbodeule
6b60e2c698 add Session flows (2x)
- fix APIRoutes (debug Naver UserID)
2024-08-08 17:39:43 +09:00
dalbodeule
fb3470ae83 add Session flows (1x)
- fix APIRoutes (debug Naver UserID)
2024-08-08 17:38:15 +09:00
dalbodeule
172885ae38 add Session flows (1x)
- fix APIRoutes (debug Naver UserID)
2024-08-08 17:37:01 +09:00
dalbodeule
561ab188b2 add Session flows (1x)
- fix APIRoutes
2024-08-08 17:30:42 +09:00
dalbodeule
369d5491e8 add Session flows (5x)
- fix CORS
2024-08-08 17:26:33 +09:00
dalbodeule
a2b2608312 add Session flows (4x)
- fix CORS
2024-08-08 17:13:52 +09:00
dalbodeule
8e6a8a106b add Session flows (4x)
- fix CORS
2024-08-08 17:10:52 +09:00
dalbodeule
cd2b4e0fc0 add Session flows (3x)
- fix CORS
2024-08-08 17:08:01 +09:00
dalbodeule
9879fafa25 add Session flows (2x)
- fix CORS
2024-08-08 17:06:20 +09:00
dalbodeule
caf3965334 add Session flows
- fix CORS
2024-08-08 16:43:39 +09:00
dalbodeule
6a7f57ac34 Merge remote-tracking branch 'origin/main' 2024-08-08 16:30:09 +09:00
dalbodeule
e18571778a add Session flows
- fix CORS
2024-08-08 16:29:59 +09:00
JinU Choi
70ce830c94 Merge pull request #48 from dalbodeule/develop
add Session flows
2024-08-08 16:23:45 +09:00
dalbodeule
c179195b18 add Session flows
- fix CORS
2024-08-08 16:22:06 +09:00
JinU Choi
54add09e79 Merge pull request #47 from dalbodeule/develop
add Session flows
2024-08-08 15:51:56 +09:00
dalbodeule
b864fc262b add Session flows
- WSSongListRoutes
- fix CORS
2024-08-08 15:47:40 +09:00
JinU Choi
e9d2ee26d6 Merge pull request #46 from dalbodeule/develop
add Session flows
2024-08-08 14:56:19 +09:00
dalbodeule
8dde66c069 add Session flows
- songlist page to changed.
2024-08-08 14:53:37 +09:00
dalbodeule
a828e23767 add OAuth flows
- add naver OAuth flow
2024-08-08 14:30:26 +09:00
54 changed files with 1919 additions and 940 deletions

2
.idea/kotlinc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.0.0" />
<option name="version" value="2.0.21" />
</component>
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/modules/chzzk_bot.main.iml" filepath="$PROJECT_DIR$/.idea/modules/chzzk_bot.main.iml" />
</modules>
</component>
</project>

9
.idea/nabot_chzzk_bot.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

4
.idea/sqldialects.xml generated
View File

@@ -3,4 +3,8 @@
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="MariaDB" />
</component>
<component name="SqlResolveMappings">
<file url="file://$PROJECT_DIR$/common/src/main/kotlin/space/mori/chzzk_bot/common/models/User.kt" scope="{&quot;node&quot;:{ &quot;@negative&quot;:&quot;1&quot;, &quot;group&quot;:{ &quot;@kind&quot;:&quot;root&quot;, &quot;node&quot;:{ &quot;name&quot;:{ &quot;@qname&quot;:&quot;90f8ee11-600e-4155-a316-e8062c7c828b&quot; }, &quot;group&quot;:{ &quot;@kind&quot;:&quot;schema&quot;, &quot;node&quot;:{ &quot;name&quot;:{ &quot;@qname&quot;:&quot;chzzk&quot; } } } } } }}" />
<file url="PROJECT" scope="{&quot;node&quot;:{ &quot;@negative&quot;:&quot;1&quot;, &quot;group&quot;:{ &quot;@kind&quot;:&quot;root&quot;, &quot;node&quot;:{ &quot;name&quot;:{ &quot;@qname&quot;:&quot;90f8ee11-600e-4155-a316-e8062c7c828b&quot; }, &quot;group&quot;:{ &quot;@kind&quot;:&quot;schema&quot;, &quot;node&quot;:{ &quot;name&quot;:{ &quot;@qname&quot;:&quot;chzzk&quot; } } } } } }}" />
</component>
</project>

View File

@@ -18,7 +18,7 @@
- [x] \<days:yyyy-mm-dd>
### 관리 명령어 (on Discord)
- [x] /register chzzk_id: \[치지직 고유ID]
- [x] /hook token: \[디스코드 연동 페이지에서 받은 Token]
- [x] /alert channel: \[디스코드 Channel ID] content: \[알림 내용]
- [x] /add label: \[명령어] content: \[내용]
- [ ] /list
@@ -79,3 +79,5 @@
- [mariadb](https://mariadb.org/)
- [docker](https://www.docker.com/)
- [Teamcity](https://www.jetbrains.com/teamcity/)
- [Nuxtjs](https://nuxt.com/)
- [Bulma](https://bulma.io/)

View File

@@ -1,5 +1,5 @@
plugins {
val kotlinVersion = "2.0.0"
val kotlinVersion = "2.0.21"
id("java")
id("application")
@@ -28,21 +28,21 @@ repositories {
dependencies {
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
implementation("ch.qos.logback:logback-classic:1.5.6")
implementation("ch.qos.logback:logback-classic:1.5.12")
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect
implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.0")
implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.21")
// https://mvnrepository.com/artifact/com.google.code.gson/gson
implementation("com.google.code.gson:gson:2.11.0")
// https://mvnrepository.com/artifact/io.github.cdimascio/dotenv-kotlin
implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
implementation("io.github.cdimascio:dotenv-kotlin:6.4.2")
// https://mvnrepository.com/artifact/io.insert-koin/koin-core
implementation("io.insert-koin:koin-core:4.0.0-RC1")
implementation("io.insert-koin:koin-core:4.0.0")
kotlin("stdlib")

View File

@@ -11,32 +11,32 @@ repositories {
dependencies {
// https://mvnrepository.com/artifact/net.dv8tion/JDA
api("net.dv8tion:JDA:5.0.1") {
api("net.dv8tion:JDA:5.2.1") {
exclude(module = "opus-java")
}
// https://mvnrepository.com/artifact/io.github.R2turnTrue/chzzk4j
implementation("io.github.R2turnTrue:chzzk4j:0.0.9")
implementation("io.github.R2turnTrue:chzzk4j:0.0.12")
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
implementation("ch.qos.logback:logback-classic:1.5.6")
implementation("ch.qos.logback:logback-classic:1.5.12")
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect
implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.0")
implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.21")
// https://mvnrepository.com/artifact/com.google.code.gson/gson
implementation("com.google.code.gson:gson:2.11.0")
// https://mvnrepository.com/artifact/io.github.cdimascio/dotenv-kotlin
implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
implementation("io.github.cdimascio:dotenv-kotlin:6.4.2")
// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// https://mvnrepository.com/artifact/io.insert-koin/koin-core
implementation("io.insert-koin:koin-core:4.0.0-RC1")
implementation("io.insert-koin:koin-core:4.0.0")
testImplementation(kotlin("test"))

View File

@@ -3,11 +3,13 @@ package space.mori.chzzk_bot.chatbot.chzzk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.inject
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.chatbot.chzzk.Connector.chzzk
import space.mori.chzzk_bot.chatbot.chzzk.Connector.getChannel
import space.mori.chzzk_bot.chatbot.discord.Discord
import space.mori.chzzk_bot.common.events.*
import space.mori.chzzk_bot.common.models.User
@@ -28,6 +30,7 @@ object ChzzkHandler {
private val logger = LoggerFactory.getLogger(this::class.java)
lateinit var botUid: String
@Volatile private var running: Boolean = false
private val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
fun addUser(chzzkChannel: ChzzkChannel, user: User) {
handlers.add(UserHandler(chzzkChannel, logger, user, streamStartTime = null))
@@ -36,13 +39,42 @@ object ChzzkHandler {
fun enable() {
botUid = chzzk.loggedUser.userId
UserService.getAllUsers().map {
chzzk.getChannel(it.token)?.let { token -> addUser(token, it) }
if(!it.isDisabled)
try {
chzzk.getChannel(it.token)?.let { token -> addUser(token, it) }
} catch(e: Exception) {
logger.info("Exception: ${it.token}(${it.username}) not found. ${e.stackTraceToString()}")
}
}
handlers.forEach { handler ->
val streamInfo = getStreamInfo(handler.listener.channelId)
if (streamInfo.content?.status == "OPEN") handler.isActive(true, streamInfo)
}
dispatcher.subscribe(UserRegisterEvent::class) {
val channel = getChannel(it.chzzkId)
val user = UserService.getUser(it.chzzkId)
if(channel != null && user != null) {
addUser(channel, user)
}
}
dispatcher.subscribe(CommandReloadEvent::class) {
handlers.firstOrNull { handlers -> handlers.channel.channelId == it.uid }?.reloadCommand()
}
dispatcher.subscribe(BotEnabledEvent::class) {
if(it.isDisabled) {
handlers.removeIf { handlers -> handlers.channel.channelId == it.chzzkId }
} else {
val channel = getChannel(it.chzzkId)
val user = UserService.getUser(it.chzzkId)
if(channel != null && user != null) {
addUser(channel, user)
}
}
}
}
fun disable() {
@@ -69,27 +101,90 @@ object ChzzkHandler {
fun runStreamInfo() {
running = true
val thread = Thread({
while(running) {
val threadRunner1 = Runnable {
logger.info("Thread 1 started!")
while (running) {
handlers.forEach {
if (!running) return@forEach
try {
val streamInfo = getStreamInfo(it.channel.channelId)
if (streamInfo.content?.status == "OPEN" && !it.isActive) it.isActive(true, streamInfo)
if (streamInfo.content?.status == "OPEN" && !it.isActive) {
try {
it.isActive(true, streamInfo)
} catch(e: Exception) {
logger.info("Exception: ${e.stackTraceToString()}")
}
}
if (streamInfo.content?.status == "CLOSE" && it.isActive) it.isActive(false, streamInfo)
} catch(e: SocketTimeoutException) {
logger.info("Timeout: ${it.channel.channelName} / ${e.stackTraceToString()}")
} catch (e: SocketTimeoutException) {
logger.info("Thread 1 Timeout: ${it.channel.channelName} / ${e.stackTraceToString()}")
} catch (e: Exception) {
logger.info("Exception: ${it.channel.channelName} / ${e.stackTraceToString()}")
logger.info("Thread 1 Exception: ${it.channel.channelName} / ${e.stackTraceToString()}")
} finally {
Thread.sleep(5000)
}
}
Thread.sleep(60000)
}
}, "Chzzk-StreamInfo")
}
thread.start()
val threadRunner2 = Runnable {
logger.info("Thread 2 started!")
logger.info("Thread 2 started!")
while (running) {
handlers.forEach {
if (!running) return@forEach
try {
val streamInfo = getStreamInfo(it.channel.channelId)
if (streamInfo.content?.status == "OPEN" && !it.isActive) {
try {
it.isActive(true, streamInfo)
} catch(e: Exception) {
logger.info("Exception: ${e.stackTraceToString()}")
}
}
if (streamInfo.content?.status == "CLOSE" && it.isActive) it.isActive(false, streamInfo)
} catch (e: SocketTimeoutException) {
logger.info("Thread 2 Timeout: ${it.channel.channelName} / ${e.stackTraceToString()}")
} catch (e: Exception) {
logger.info("Thread 2 Exception: ${it.channel.channelName} / ${e.stackTraceToString()}")
} finally {
Thread.sleep(5000)
}
}
Thread.sleep(60000)
}
}
fun startThread(name: String, runner: Runnable) {
Thread({
while(running) {
try {
val thread = Thread(runner, name)
thread.start()
thread.join()
} catch(e: Exception) {
logger.error("Thread $name Exception: ${e.stackTraceToString()}")
}
if(running) {
logger.info("Thread $name restart in 5 seconds")
Thread.sleep(5000)
}
}
}, "${name}-runner").start()
}
// 첫 번째 스레드 시작
startThread("Chzzk-StreamInfo-1", threadRunner1)
// 85초 대기 후 두 번째 스레드 시작
CoroutineScope(Dispatchers.Default).launch {
delay(95000) // start with 95 secs after.
if (running) {
startThread("Chzzk-StreamInfo-2", threadRunner2)
}
}
}
fun stopStreamInfo() {
@@ -103,7 +198,9 @@ class UserHandler(
private var user: User,
var streamStartTime: LocalDateTime?,
) {
private lateinit var messageHandler: MessageHandler
var messageHandler: MessageHandler
var listener: ChzzkChat
private val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
private var _isActive: Boolean
get() = LiveStatusService.getLiveStatus(user)?.status ?: false
@@ -111,30 +208,33 @@ class UserHandler(
LiveStatusService.updateOrCreate(user, value)
}
var listener: ChzzkChat = chzzk.chat(channel.channelId)
.withAutoReconnect(true)
.withChatListener(object : ChatEventListener {
override fun onConnect(chat: ChzzkChat, isReconnecting: Boolean) {
logger.info("ChzzkChat connected. ${channel.channelName} - ${channel.channelId} / reconnected: $isReconnecting")
messageHandler = MessageHandler(this@UserHandler)
}
init {
listener = chzzk.chat(channel.channelId)
.withAutoReconnect(true)
.withChatListener(object : ChatEventListener {
override fun onConnect(chat: ChzzkChat, isReconnecting: Boolean) {
logger.info("${channel.channelName} - ${channel.channelId} / reconnected: $isReconnecting")
}
override fun onError(ex: Exception) {
logger.info("ChzzkChat error. ${channel.channelName} - ${channel.channelId}")
logger.debug(ex.stackTraceToString())
}
override fun onError(ex: Exception) {
logger.info("ChzzkChat error. ${channel.channelName} - ${channel.channelId}")
logger.info(ex.stackTraceToString())
}
override fun onChat(msg: ChatMessage) {
if(!_isActive) return
messageHandler.handle(msg, user)
}
override fun onChat(msg: ChatMessage) {
if(!_isActive) return
messageHandler.handle(msg, user)
}
override fun onConnectionClosed(code: Int, reason: String?, remote: Boolean, tryingToReconnect: Boolean) {
logger.info("ChzzkChat closed. ${channel.channelName} - ${channel.channelId}")
logger.info("Reason: $reason / $tryingToReconnect")
}
})
.build()
messageHandler = MessageHandler(this@UserHandler)
}
override fun onConnectionClosed(code: Int, reason: String?, remote: Boolean, tryingToReconnect: Boolean) {
logger.info("ChzzkChat closed. ${channel.channelName} - ${channel.channelId}")
logger.info("Reason: $reason / $tryingToReconnect")
}
})
.build()
internal fun disable() {
listener.closeAsync()
@@ -153,15 +253,18 @@ class UserHandler(
internal fun isActive(value: Boolean, status: IData<IStreamInfo?>) {
if(value) {
logger.info("${user.username} is live.")
logger.info("ChzzkChat connecting... ${channel.channelName} - ${channel.channelId}")
listener.connectBlocking()
streamStartTime = status.content?.openDate?.let { convertChzzkDateToLocalDateTime(it) }
CoroutineScope(Dispatchers.Default).launch {
logger.info("${user.username} is live.")
reloadUser(UserService.getUser(user.id.value)!!)
logger.info("ChzzkChat connecting... ${channel.channelName} - ${channel.channelId}")
listener.connectAsync().await()
streamStartTime = status.content?.openDate?.let { convertChzzkDateToLocalDateTime(it) }
if(!_isActive) {
_isActive = true
when(TimerConfigService.getConfig(UserService.getUser(channel.channelId)!!)?.option) {
TimerType.UPTIME.value -> dispatcher.post(
TimerEvent(
@@ -181,7 +284,8 @@ class UserHandler(
}
delay(5000L)
try {
listener.sendChat("${user.username} 님! 오늘도 열심히 방송하세요!")
if(!user.isDisableStartupMsg)
listener.sendChat("${user.username} 님! 오늘도 열심히 방송하세요!")
Discord.sendDiscord(user, status)
} catch(e: Exception) {
logger.info("Stream on logic has some error: ${e.stackTraceToString()}")
@@ -192,6 +296,7 @@ class UserHandler(
logger.info("${user.username} is offline.")
streamStartTime = null
listener.closeAsync()
_isActive = false
CoroutineScope(Dispatchers.Default).launch {
val events = listOf(
@@ -207,13 +312,11 @@ class UserHandler(
null,
null,
null,
null,
null
)
)
events.forEach { dispatcher.post(it) }
}
}
_isActive = value
}
}

View File

@@ -16,7 +16,6 @@ import xyz.r2turntrue.chzzk4j.chat.ChzzkChat
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.UUID
class MessageHandler(
@@ -43,7 +42,7 @@ class MessageHandler(
if(it.type == SongType.STREAM_OFF) {
val user = UserService.getUser(channel.channelId)
if(! user?.let { usr -> SongListService.getSong(usr) }.isNullOrEmpty()) {
SongListService.deleteUser(user!!)
SongListService.deleteUser(user)
}
}
}
@@ -54,13 +53,14 @@ class MessageHandler(
?: throw RuntimeException("User not found. it's bug? ${channel.channelName} - ${channel.channelId}")
val commands = CommandService.getCommands(user)
val manageCommands = mapOf(
"!명령어" to this::commandListCommand,
"!명령어추가" to this::manageAddCommand,
"!명령어삭제" to this::manageRemoveCommand,
"!명령어수정" to this::manageUpdateCommand,
"!시간" to this::timerCommand,
"!노래추가" to this::songAddCommand,
"!신청곡" to this::songAddCommand,
"!노래목록" to this::songListCommand,
"!노래시작" to this::songStartCommand
"!노래시작" to this::songStartCommand,
)
manageCommands.forEach { (commandName, command) ->
@@ -77,6 +77,10 @@ class MessageHandler(
}
}
private fun commandListCommand(msg: ChatMessage, user: User) {
listener.sendChat("리스트는 여기입니다. https://nabot.mori.space/commands/${user.token}")
}
private fun manageAddCommand(msg: ChatMessage, user: User) {
if (msg.profile?.userRoleCode == "common_user") {
listener.sendChat("매니저만 명령어를 추가할 수 있습니다.")
@@ -205,7 +209,11 @@ class MessageHandler(
// songs
private fun songAddCommand(msg: ChatMessage, user: User) {
val parts = msg.content.split(" ", limit = 3)
if(SongConfigService.getConfig(user).disabled) {
return
}
val parts = msg.content.split(" ", limit = 2)
if (parts.size < 2) {
listener.sendChat("유튜브 URL을 입력해주세요!")
return
@@ -242,6 +250,11 @@ class MessageHandler(
return
}
if (video.length > 600) {
listener.sendChat("10분이 넘는 노래는 신청할 수 없습니다.")
return
}
SongListService.saveSong(
user,
msg.userId,
@@ -257,16 +270,13 @@ class MessageHandler(
user.token,
SongType.ADD,
msg.userId,
msg.profile?.nickname ?: "",
video.name,
video.author,
video.length,
video.url
null,
video,
)
)
}
listener.sendChat("노래가 추가되었습니다.")
listener.sendChat("노래가 추가되었습니다. ${video.name} - ${video.author}")
} catch(e: Exception) {
listener.sendChat("유튜브 영상 주소로 다시 신청해주세요!")
logger.info(e.stackTraceToString())
@@ -274,6 +284,10 @@ class MessageHandler(
}
private fun songListCommand(msg: ChatMessage, user: User) {
if(SongConfigService.getConfig(user).disabled) {
return
}
listener.sendChat("리스트는 여기입니다. https://nabot.mori.space/songs/${user.token}")
}
@@ -283,16 +297,16 @@ class MessageHandler(
return
}
val session = "${UUID.randomUUID()}${UUID.randomUUID()}".replace("-", "")
SongConfigService.updateSession(user, session)
bot.retrieveUserById(user.discord).queue { discordUser ->
discordUser?.openPrivateChannel()?.queue { channel ->
channel.sendMessage("여기로 접속해주세요! ||https://nabot.mori.space/songlist/${session}||.\n주소가 노출될 경우 방송을 다시 켜셔야 합니다!")
.queue()
if(user.discord != null) {
bot.retrieveUserById(user.discord!!).queue { discordUser ->
discordUser?.openPrivateChannel()?.queue { channel ->
channel.sendMessage("여기로 접속해주세요! ||https://nabot.mori.space/songlist||.")
.queue()
}
}
} else {
listener.sendChat("나봇 홈페이지의 노래목록 페이지를 이용해주세요! 디스코드 연동을 하시면 DM으로 바로 전송됩니다.")
}
}

View File

@@ -8,7 +8,6 @@ import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.entities.Guild
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel
import net.dv8tion.jda.api.events.guild.GuildJoinEvent
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder
@@ -17,7 +16,6 @@ import space.mori.chzzk_bot.common.utils.IData
import space.mori.chzzk_bot.common.utils.IStreamInfo
import space.mori.chzzk_bot.chatbot.discord.commands.*
import space.mori.chzzk_bot.common.models.User
import space.mori.chzzk_bot.common.services.ManagerService
import java.time.Instant
val dotenv = dotenv {
@@ -31,12 +29,15 @@ class Discord: ListenerAdapter() {
companion object {
lateinit var bot: JDA
internal fun getChannel(guildId: Long, channelId: Long): TextChannel? = bot.getGuildById(guildId)?.getTextChannelById(channelId)
internal fun getChannel(guildId: Long, channelId: Long): TextChannel? {
return bot.getGuildById(guildId)?.getTextChannelById(channelId)
}
fun sendDiscord(user: User, status: IData<IStreamInfo?>) {
if(status.content == null) return
if(user.liveAlertMessage != "" && user.liveAlertGuild != null && user.liveAlertChannel != null) {
val channel = getChannel(user.liveAlertGuild!!, user.liveAlertChannel!!) ?: throw RuntimeException("${user.liveAlertChannel} is not valid.")
if(user.liveAlertMessage != null && user.liveAlertGuild != null && user.liveAlertChannel != null) {
val channel = getChannel(user.liveAlertGuild ?: 0, user.liveAlertChannel ?: 0)
?: throw RuntimeException("${user.liveAlertChannel} is not valid.")
val embed = EmbedBuilder()
embed.setTitle(status.content!!.liveTitle, "https://chzzk.naver.com/live/${user.token}")
@@ -58,15 +59,7 @@ class Discord: ListenerAdapter() {
}
private val commands = listOf(
AddCommand,
AlertCommand,
PingCommand,
RegisterCommand,
RemoveCommand,
UpdateCommand,
AddManagerCommand,
ListManagerCommand,
RemoveManagerCommand,
)
override fun onSlashCommandInteraction(event: SlashCommandInteractionEvent) {
@@ -76,10 +69,6 @@ class Discord: ListenerAdapter() {
handler?.run(event, bot)
}
override fun onGuildMemberRemove(event: GuildMemberRemoveEvent) {
event.member?.let { ManagerService.deleteManager(event.guild.idLong, it.idLong) }
}
override fun onGuildJoin(event: GuildJoinEvent) {
commandUpdate(event.guild)
}

View File

@@ -1,73 +0,0 @@
package space.mori.chzzk_bot.chatbot.discord.commands
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.OptionType
import net.dv8tion.jda.api.interactions.commands.build.Commands
import net.dv8tion.jda.api.interactions.commands.build.OptionData
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.chatbot.chzzk.ChzzkHandler
import space.mori.chzzk_bot.chatbot.chzzk.Connector
import space.mori.chzzk_bot.chatbot.discord.CommandInterface
import space.mori.chzzk_bot.common.services.CommandService
import space.mori.chzzk_bot.common.services.ManagerService
import space.mori.chzzk_bot.common.services.UserService
object AddCommand : CommandInterface {
private val logger = LoggerFactory.getLogger(this::class.java)
override val name: String = "add"
override val command = Commands.slash(name, "명령어를 추가합니다.")
.addOptions(OptionData(OptionType.STRING, "label", "작동할 명령어를 입력하세요.", true))
.addOptions(OptionData(OptionType.STRING, "content", "표시될 텍스트를 입력하세요.", true))
.addOptions(OptionData(OptionType.STRING, "fail_content", "카운터 업데이트 실패시 표시될 텍스트를 입력하세요.", false))
override fun run(event: SlashCommandInteractionEvent, bot: JDA) {
val label = event.getOption("label")?.asString
val content = event.getOption("content")?.asString
val failContent = event.getOption("fail_content")?.asString
if(label == null || content == null) {
event.hook.sendMessage("명령어와 텍스트는 필수 입력입니다.").queue()
return
}
var user = UserService.getUser(event.user.idLong)
val manager = event.guild?.idLong?.let { ManagerService.getUser(it, event.user.idLong) }
if(user == null && manager == null) {
event.hook.sendMessage("당신은 이 명령어를 사용할 수 없습니다.").queue()
return
}
if (manager != null) {
transaction {
user = manager.user
}
user?.let { ManagerService.updateManager(it, event.user.idLong, event.user.effectiveName) }
}
if (user == null) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
return
}
val commands = CommandService.getCommands(user!!)
if (commands.any { it.command == label }) {
event.hook.sendMessage("$label 명령어는 이미 있습니다! 업데이트 명령어를 써주세요.").queue()
return
}
val chzzkChannel = Connector.getChannel(user!!.token)
try {
CommandService.saveCommand(user!!, label, content, failContent ?: "")
try {
ChzzkHandler.reloadCommand(chzzkChannel!!)
} catch (_: Exception) {}
event.hook.sendMessage("등록이 완료되었습니다. $label = $content / $failContent").queue()
} catch (e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
logger.debug(e.stackTraceToString())
}
}
}

View File

@@ -1,47 +0,0 @@
package space.mori.chzzk_bot.chatbot.discord.commands
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.OptionType
import net.dv8tion.jda.api.interactions.commands.build.Commands
import net.dv8tion.jda.api.interactions.commands.build.OptionData
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.chatbot.discord.CommandInterface
import space.mori.chzzk_bot.common.services.ManagerService
import space.mori.chzzk_bot.common.services.UserService
object AddManagerCommand : CommandInterface {
private val logger = LoggerFactory.getLogger(this::class.java)
override val name: String = "addmanager"
override val command = Commands.slash(name, "매니저를 추가합니다.")
.addOptions(OptionData(OptionType.USER, "user", "추가할 유저를 선택하세요.", true))
override fun run(event: SlashCommandInteractionEvent, bot: JDA) {
val manager = event.getOption("user")?.asUser
if(manager == null) {
event.hook.sendMessage("유저는 필수사항입니다.").queue()
return
}
if(manager.idLong == event.user.idLong) {
event.hook.sendMessage("자신은 매니저로 설정할 수 없습니다.").queue()
return
}
val user = UserService.getUser(event.user.idLong)
if(user == null) {
event.hook.sendMessage("치지직 계정을 찾을 수 없습니다.").queue()
return
}
try {
ManagerService.saveManager(user, manager.idLong, manager.effectiveName)
if(user.liveAlertGuild == null)
UserService.updateLiveAlert(user.id.value, event.guild!!.idLong, event.channelIdLong, "")
event.hook.sendMessage("등록이 완료되었습니다. ${manager.effectiveName}").queue()
} catch (e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
logger.debug(e.stackTraceToString())
}
}
}

View File

@@ -1,59 +0,0 @@
package space.mori.chzzk_bot.chatbot.discord.commands
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.OptionType
import net.dv8tion.jda.api.interactions.commands.build.Commands
import net.dv8tion.jda.api.interactions.commands.build.OptionData
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.chatbot.chzzk.ChzzkHandler
import space.mori.chzzk_bot.chatbot.chzzk.Connector
import space.mori.chzzk_bot.chatbot.discord.CommandInterface
import space.mori.chzzk_bot.common.services.ManagerService
import space.mori.chzzk_bot.common.services.UserService
object AlertCommand : CommandInterface {
private val logger = LoggerFactory.getLogger(this::class.java)
override val name: String = "alert"
override val command = Commands.slash(name, "방송알람 채널을 설정합니다. / 알람 취소도 이 명령어를 이용하세요!")
.addOptions(OptionData(OptionType.CHANNEL, "channel", "알림을 보낼 채널을 입력하세요."))
.addOptions(OptionData(OptionType.STRING, "content", "표시될 텍스트를 입력하세요. 비워두면 알람이 취소됩니다."))
override fun run(event: SlashCommandInteractionEvent, bot: JDA) {
val channel = event.getOption("channel")?.asChannel
val content = event.getOption("content")?.asString
var user = UserService.getUser(event.user.idLong)
val manager = event.guild?.idLong?.let { ManagerService.getUser(it, event.user.idLong) }
if(user == null && manager == null) {
event.hook.sendMessage("당신은 이 명령어를 사용할 수 없습니다.").queue()
return
}
if (manager != null) {
transaction {
user = manager.user
}
user?.let { ManagerService.updateManager(it, event.user.idLong, event.user.effectiveName) }
}
if (user == null) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
return
}
val chzzkChannel = Connector.getChannel(user!!.token)
try {
val newUser = UserService.updateLiveAlert(user!!.id.value, channel?.guild?.idLong ?: 0L, channel?.idLong ?: 0L, content ?: "")
try {
ChzzkHandler.reloadUser(chzzkChannel!!, newUser)
} catch (_: Exception) {}
event.hook.sendMessage("업데이트가 완료되었습니다.").queue()
} catch (e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
logger.debug(e.stackTraceToString())
}
}
}

View File

@@ -1,44 +0,0 @@
package space.mori.chzzk_bot.chatbot.discord.commands
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.build.Commands
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.chatbot.discord.CommandInterface
import space.mori.chzzk_bot.common.services.ManagerService
import space.mori.chzzk_bot.common.services.UserService
object ListManagerCommand : CommandInterface {
private val logger = LoggerFactory.getLogger(this::class.java)
override val name: String = "listmanager"
override val command = Commands.slash(name, "매니저 목록을 확인합니다.")
override fun run(event: SlashCommandInteractionEvent, bot: JDA) {
try {
val managers = event.guild?.idLong?.let { ManagerService.getAllUsers(it) }
if(managers == null) {
event.channel.sendMessage("여기에서는 사용할 수 없습니다.")
return
}
val user = UserService.getUserWithGuildId(event.guild!!.idLong)
val embed = EmbedBuilder()
embed.setTitle("${user!!.username} 매니저 목록")
embed.setDescription("매니저 목록입니다.")
var idx = 1
managers.forEach {
embed.addField("${idx++}", it.lastUserName ?: it.managerId.toString(), true)
}
event.channel.sendMessageEmbeds(embed.build()).queue()
} catch (e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
logger.debug(e.stackTraceToString())
}
}
}

View File

@@ -1,60 +0,0 @@
package space.mori.chzzk_bot.chatbot.discord.commands
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.OptionType
import net.dv8tion.jda.api.interactions.commands.build.Commands
import net.dv8tion.jda.api.interactions.commands.build.OptionData
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.chatbot.chzzk.ChzzkHandler
import space.mori.chzzk_bot.chatbot.chzzk.Connector
import space.mori.chzzk_bot.chatbot.discord.CommandInterface
import space.mori.chzzk_bot.common.services.UserService
object RegisterCommand: CommandInterface {
private val logger = LoggerFactory.getLogger(this::class.java)
override val name = "register"
private val regex = """(?:.+chzzk\.naver\.com/)?([a-f0-9]{32})?(?:/live)?${'$'}""".toRegex()
override val command = Commands.slash(name, "치지직 계정을 등록합니다.")
.addOptions(
OptionData(
OptionType.STRING,
"chzzk_id",
"치지직 채널 URL 혹은 ID를 입력해주세요.",
true
)
)
override fun run(event: SlashCommandInteractionEvent, bot: JDA) {
val chzzkID = event.getOption("chzzk_id")?.asString
if(chzzkID == null) {
event.hook.sendMessage("치지직 계정은 필수 입력입니다.").queue()
return
}
val matchResult = regex.find(chzzkID)
val matchedChzzkId = matchResult?.groups?.get(1)?.value
val chzzkChannel = matchedChzzkId?.let { Connector.getChannel(it) }
if (chzzkChannel == null) {
event.hook.sendMessage("치지직 계정을 찾을 수 없습니다.").queue()
return
}
try {
val user = UserService.saveUser(chzzkChannel.channelName, chzzkChannel.channelId, event.user.idLong)
CoroutineScope(Dispatchers.Main).launch {
ChzzkHandler.addUser(chzzkChannel, user)
}
event.hook.sendMessage("등록이 완료되었습니다. `${chzzkChannel.channelId}` - `${chzzkChannel.channelName}`")
} catch(e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
logger.debug(e.stackTraceToString())
}
}
}

View File

@@ -1,63 +0,0 @@
package space.mori.chzzk_bot.chatbot.discord.commands
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.OptionType
import net.dv8tion.jda.api.interactions.commands.build.Commands
import net.dv8tion.jda.api.interactions.commands.build.OptionData
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.chatbot.chzzk.ChzzkHandler
import space.mori.chzzk_bot.chatbot.chzzk.Connector
import space.mori.chzzk_bot.chatbot.discord.CommandInterface
import space.mori.chzzk_bot.common.services.CommandService
import space.mori.chzzk_bot.common.services.ManagerService
import space.mori.chzzk_bot.common.services.UserService
object RemoveCommand : CommandInterface {
private val logger = LoggerFactory.getLogger(this::class.java)
override val name: String = "remove"
override val command = Commands.slash(name, "명령어를 삭제합니다.")
.addOptions(OptionData(OptionType.STRING, "label", "삭제할 명령어를 입력하세요.", true))
override fun run(event: SlashCommandInteractionEvent, bot: JDA) {
val label = event.getOption("label")?.asString
if(label == null) {
event.hook.sendMessage("명령어는 필수 입력입니다.").queue()
return
}
var user = UserService.getUser(event.user.idLong)
val manager = event.guild?.idLong?.let { ManagerService.getUser(it, event.user.idLong) }
if(user == null && manager == null) {
event.hook.sendMessage("당신은 이 명령어를 사용할 수 없습니다.").queue()
return
}
if (manager != null) {
transaction {
user = manager.user
}
user?.let { ManagerService.updateManager(it, event.user.idLong, event.user.effectiveName) }
}
if (user == null) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
return
}
val chzzkChannel = Connector.getChannel(user!!.token)
try {
CommandService.removeCommand(user!!, label)
try {
ChzzkHandler.reloadCommand(chzzkChannel!!)
} catch (_: Exception) {}
event.hook.sendMessage("삭제가 완료되었습니다. $label").queue()
} catch (e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
logger.debug(e.stackTraceToString())
}
}
}

View File

@@ -1,49 +0,0 @@
package space.mori.chzzk_bot.chatbot.discord.commands
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.OptionType
import net.dv8tion.jda.api.interactions.commands.build.Commands
import net.dv8tion.jda.api.interactions.commands.build.OptionData
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.chatbot.discord.CommandInterface
import space.mori.chzzk_bot.common.services.ManagerService
import space.mori.chzzk_bot.common.services.UserService
object RemoveManagerCommand : CommandInterface {
private val logger = LoggerFactory.getLogger(this::class.java)
override val name: String = "removemanager"
override val command = Commands.slash(name, "매니저를 삭제합니다.")
.addOptions(OptionData(OptionType.USER, "user", "삭제할 유저를 선택하세요.", true))
override fun run(event: SlashCommandInteractionEvent, bot: JDA) {
val manager = event.getOption("user")?.asUser
if(manager == null) {
event.hook.sendMessage("유저는 필수사항입니다.").queue()
return
}
if(manager.idLong == event.user.idLong) {
event.hook.sendMessage("자신은 매니저로 설정할 수 없습니다.").queue()
return
}
val user = UserService.getUser(event.user.idLong)
if(user == null) {
event.hook.sendMessage("치지직 계정을 찾을 수 없습니다.").queue()
return
}
if(ManagerService.getUser(user.liveAlertGuild ?: 0L, manager.idLong) == null) {
event.hook.sendMessage("${manager.name}은 매니저가 아닙니다.")
}
try {
ManagerService.deleteManager(user, manager.idLong)
event.hook.sendMessage("삭제가 완료되었습니다. ${manager.effectiveName}").queue()
} catch (e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
logger.debug(e.stackTraceToString())
}
}
}

View File

@@ -1,65 +0,0 @@
package space.mori.chzzk_bot.chatbot.discord.commands
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.OptionType
import net.dv8tion.jda.api.interactions.commands.build.Commands
import net.dv8tion.jda.api.interactions.commands.build.OptionData
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.chatbot.chzzk.ChzzkHandler
import space.mori.chzzk_bot.chatbot.chzzk.Connector
import space.mori.chzzk_bot.chatbot.discord.CommandInterface
import space.mori.chzzk_bot.common.services.CommandService
import space.mori.chzzk_bot.common.services.ManagerService
import space.mori.chzzk_bot.common.services.UserService
object UpdateCommand : CommandInterface {
private val logger = LoggerFactory.getLogger(this::class.java)
override val name: String = "update"
override val command = Commands.slash(name, "명령어를 수정합니다.")
.addOptions(OptionData(OptionType.STRING, "label", "수정할 명령어를 입력하세요.", true))
.addOptions(OptionData(OptionType.STRING, "content", "표시될 텍스트를 입력하세요.", true))
.addOptions(OptionData(OptionType.STRING, "fail_content", "카운터 업데이트 실패시 표시될 텍스트를 입력하세요.", false))
override fun run(event: SlashCommandInteractionEvent, bot: JDA) {
val label = event.getOption("label")?.asString
val content = event.getOption("content")?.asString
val failContent = event.getOption("fail_content")?.asString
if(label == null || content == null) {
event.hook.sendMessage("명령어와 텍스트는 필수 입력입니다.").queue()
return
}
var user = UserService.getUser(event.user.idLong)
val manager = event.guild?.idLong?.let { ManagerService.getUser(it, event.user.idLong) }
if(user == null && manager == null) {
event.hook.sendMessage("당신은 이 명령어를 사용할 수 없습니다.").queue()
return
}
if (manager != null) {
transaction {
user = manager.user
}
user?.let { ManagerService.updateManager(it, event.user.idLong, event.user.effectiveName) }
}
if (user == null) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
return
}
val chzzkChannel = Connector.getChannel(user!!.token)
try {
CommandService.updateCommand(user!!, label, content, failContent ?: "")
chzzkChannel?.let { ChzzkHandler.reloadCommand(it) }
event.hook.sendMessage("등록이 완료되었습니다. $label = $content").queue()
} catch (e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
logger.debug(e.stackTraceToString())
}
}
}

View File

@@ -11,25 +11,25 @@ repositories {
dependencies {
// https://mvnrepository.com/artifact/org.jetbrains.exposed/exposed-core
api("org.jetbrains.exposed:exposed-core:0.52.0")
api("org.jetbrains.exposed:exposed-core:0.56.0")
// https://mvnrepository.com/artifact/org.jetbrains.exposed/exposed-dao
api("org.jetbrains.exposed:exposed-dao:0.52.0")
api("org.jetbrains.exposed:exposed-dao:0.56.0")
// https://mvnrepository.com/artifact/org.jetbrains.exposed/exposed-jdbc
api("org.jetbrains.exposed:exposed-jdbc:0.52.0")
api("org.jetbrains.exposed:exposed-jdbc:0.56.0")
// https://mvnrepository.com/artifact/org.jetbrains.exposed/exposed-kotlin-datetime
api("org.jetbrains.exposed:exposed-java-time:0.52.0")
api("org.jetbrains.exposed:exposed-java-time:0.56.0")
// https://mvnrepository.com/artifact/com.zaxxer/HikariCP
api("com.zaxxer:HikariCP:5.1.0")
api("com.zaxxer:HikariCP:6.1.0")
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
implementation("ch.qos.logback:logback-classic:1.5.6")
implementation("ch.qos.logback:logback-classic:1.5.12")
// https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
implementation("org.mariadb.jdbc:mariadb-java-client:3.4.1")
implementation("org.mariadb.jdbc:mariadb-java-client:3.5.0")
// https://mvnrepository.com/artifact/io.github.cdimascio/dotenv-kotlin
implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
implementation("io.github.cdimascio:dotenv-kotlin:6.4.2")
// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
implementation("com.squareup.okhttp3:okhttp:4.12.0")

View File

@@ -25,16 +25,17 @@ object Connector {
init {
Database.connect(dataSource)
val tables = listOf(
UserManagers,
Users,
Commands,
Counters,
DailyCounters,
PersonalCounters,
Managers,
TimerConfigs,
LiveStatuses,
SongLists,
SongConfigs
SongConfigs,
Sessions
)
transaction {

View File

@@ -0,0 +1,8 @@
package space.mori.chzzk_bot.common.events
data class BotEnabledEvent(
val chzzkId: String,
val isDisabled: Boolean,
): Event {
val TAG = javaClass.simpleName
}

View File

@@ -0,0 +1,7 @@
package space.mori.chzzk_bot.common.events
data class CommandReloadEvent(
val uid: String
): Event {
val TAG = javaClass.simpleName
}

View File

@@ -0,0 +1,8 @@
package space.mori.chzzk_bot.common.events
class DiscordRegisterEvent(
val user: String,
val token: String,
): Event {
val TAG = javaClass.simpleName
}

View File

@@ -1,22 +1,23 @@
package space.mori.chzzk_bot.common.events
import space.mori.chzzk_bot.common.utils.YoutubeVideo
enum class SongType(var value: Int) {
ADD(0),
REMOVE(1),
NEXT(2),
STREAM_OFF(50)
STREAM_OFF(50),
ACK(51)
}
class SongEvent(
val uid: String,
val type: SongType,
val reqUid: String?,
val reqName: String?,
val name: String?,
val author: String?,
val time: Int?,
val url: String?
val current: YoutubeVideo? = null,
val next: YoutubeVideo? = null,
val delUrl: String? = null,
): Event {
var TAG = javaClass.simpleName
}

View File

@@ -0,0 +1,7 @@
package space.mori.chzzk_bot.common.events
data class UserRegisterEvent(
val chzzkId: String
): Event {
val TAG = javaClass.simpleName
}

View File

@@ -1,22 +0,0 @@
package space.mori.chzzk_bot.common.models
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
object Managers: IntIdTable("manager") {
val user = reference("user", Users)
val managerId = long("manager_id")
val discordGuildId = long("discord_guild_id")
var lastUserName = varchar("last_user_name", 255).nullable()
}
class Manager(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Manager>(Managers)
var user by User referencedOn Managers.user
var managerId by Managers.managerId
var discordGuildId by Managers.discordGuildId
var lastUserName by Managers.lastUserName
}

View File

@@ -0,0 +1,18 @@
package space.mori.chzzk_bot.common.models
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
object Sessions: IntIdTable("session") {
val key = text("key")
val value = text("value")
}
class Session(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Session>(Sessions)
var key by Sessions.key
var value by Sessions.value
}

View File

@@ -12,6 +12,7 @@ object SongConfigs: IntIdTable("song_config") {
val streamerOnly = bool("streamer_only").default(false)
val queueLimit = integer("queue_limit").default(50)
val personalLimit = integer("personal_limit").default(5)
val disabled = bool("disabled").default(false)
}
class SongConfig(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<SongConfig>(SongConfigs)
@@ -21,4 +22,5 @@ class SongConfig(id: EntityID<Int>) : IntEntity(id) {
var streamerOnly by SongConfigs.streamerOnly
var queueLimit by SongConfigs.queueLimit
var personalLimit by SongConfigs.personalLimit
var disabled by SongConfigs.disabled
}

View File

@@ -12,7 +12,7 @@ object SongLists: IntIdTable("song_list") {
val uid = varchar("uid", 64)
val url = varchar("url", 128)
val name = text("name")
val reqName = varchar("req_name", 20)
val reqName = varchar("req_name", 80)
val author = text("author")
val time = integer("time")
val created_at = datetime("created_at").default(LocalDateTime.now())

View File

@@ -9,10 +9,12 @@ import org.jetbrains.exposed.dao.id.IntIdTable
object Users: IntIdTable("users") {
val username = varchar("username", 255)
val token = varchar("token", 64)
val discord = long("discord")
val discord = long("discord").nullable()
val liveAlertGuild = long("live_alert_guild").nullable()
val liveAlertChannel = long("live_alert_channel").nullable()
val liveAlertMessage = text("live_alert_message").nullable()
val isDisableStartupMsg = bool("is_disable_startup_msg").default(false)
val isDisabled = bool("is_disabled").default(false)
}
class User(id: EntityID<Int>) : IntEntity(id) {
@@ -24,4 +26,12 @@ class User(id: EntityID<Int>) : IntEntity(id) {
var liveAlertGuild by Users.liveAlertGuild
var liveAlertChannel by Users.liveAlertChannel
var liveAlertMessage by Users.liveAlertMessage
var isDisableStartupMsg by Users.isDisableStartupMsg
var isDisabled by Users.isDisabled
// 유저가 가진 매니저들
var managers by User.via(UserManagers.user, UserManagers.manager)
// 매니저가 관리하는 유저들
var subordinates by User.via(UserManagers.manager, UserManagers.user)
}

View File

@@ -0,0 +1,9 @@
package space.mori.chzzk_bot.common.models
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ReferenceOption
object UserManagers: IntIdTable("user_managers") {
val user = reference("user_id", Users, ReferenceOption.CASCADE)
val manager = reference("manager_id", Users, ReferenceOption.CASCADE)
}

View File

@@ -1,79 +0,0 @@
package space.mori.chzzk_bot.common.services
import org.jetbrains.exposed.dao.load
import org.jetbrains.exposed.dao.with
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.transactions.transaction
import space.mori.chzzk_bot.common.models.Manager
import space.mori.chzzk_bot.common.models.Managers
import space.mori.chzzk_bot.common.models.User
object ManagerService {
fun saveManager(user: User, discordId: Long, name: String): Manager {
if (user.liveAlertGuild == null)
throw RuntimeException("${user.username} has no liveAlertGuild")
return transaction {
Manager.new {
this.user = user
this.discordGuildId = user.liveAlertGuild!!
this.managerId = discordId
this.lastUserName = name
}
}
}
fun updateManager(user: User, discordId: Long, name: String): Manager {
return transaction {
if (user.liveAlertGuild == null)
throw RuntimeException("${user.username} has no liveAlertGuild")
val manager = getUser(user.liveAlertGuild!!, discordId) ?: throw RuntimeException("$name isn't manager.")
manager.lastUserName = name
manager
}
}
fun getUser(guildId: Long, discordId: Long): Manager? {
return transaction {
val manager = Manager.find(
(Managers.discordGuildId eq guildId) and (Managers.managerId eq discordId),
)
.with(Manager::user)
.firstOrNull()
manager
}
}
fun getAllUsers(guildId: Long): List<Manager> {
return transaction {
val result = Manager.find(Managers.discordGuildId eq guildId)
.with(Manager::user)
.toList()
result.forEach { it.load(Manager::user) }
result
}
}
fun deleteManager(user: User, discordId: Long): Manager {
if (user.liveAlertGuild == null)
throw RuntimeException("${user.username} has no liveAlertGuild")
return deleteManager(user.liveAlertGuild!!, discordId)
}
fun deleteManager(guildId: Long, discordId: Long): Manager {
return transaction {
val managerRow = Manager.find((Managers.discordGuildId eq guildId) and (Managers.managerId eq discordId)).firstOrNull()
managerRow ?: throw RuntimeException("Manager not found! $discordId")
managerRow.delete()
managerRow
}
}
}

View File

@@ -26,19 +26,6 @@ object SongConfigService {
}
fun getConfig(token: String): SongConfig? {
return transaction {
SongConfig.find(SongConfigs.token eq token).firstOrNull()
}
}
fun getUserByToken(token: String): User? {
return transaction {
val songConfig = SongConfig.find(SongConfigs.token eq token).firstOrNull()
if(songConfig == null) null
else UserService.getUser(songConfig.user.discord)
}
}
fun updatePersonalLimit(user: User, limit: Int): SongConfig {
return transaction {
var songConfig = SongConfig.find(SongConfigs.user eq user.id).firstOrNull()
@@ -60,18 +47,6 @@ object SongConfigService {
}
}
fun updateSession(user: User, token: String?): SongConfig {
return transaction {
var songConfig = SongConfig.find(SongConfigs.user eq user.id).firstOrNull()
if (songConfig == null) {
songConfig = initConfig(user)
}
songConfig.token = token
songConfig
}
}
fun updateStreamerOnly(user: User, config: Boolean): SongConfig {
return transaction {
var songConfig = SongConfig.find(SongConfigs.user eq user.id).firstOrNull()
@@ -83,4 +58,16 @@ object SongConfigService {
songConfig
}
}
fun updateDisabled(user: User, config: Boolean): SongConfig {
return transaction {
var songConfig = SongConfig.find(SongConfigs.user eq user.id).firstOrNull()
if (songConfig == null) {
songConfig = initConfig(user)
}
songConfig.disabled = config
songConfig
}
}
}

View File

@@ -1,49 +1,66 @@
package space.mori.chzzk_bot.common.services
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.dao.load
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import space.mori.chzzk_bot.common.models.User
import space.mori.chzzk_bot.common.models.Users
object UserService {
fun saveUser(username: String, token: String, discordID: Long): User {
fun saveUser(username: String, token: String): User {
return transaction {
User.new {
this.username = username
this.token = token
this.discord = discordID
}
}
}
fun updateUser(user: User, chzzkId: String, username: String): User {
return transaction {
user.token = chzzkId
user.username = username
user
}
}
fun updateUser(user: User, discordID: Long): User {
return transaction {
user.discord = discordID
user.load(User::subordinates, User::managers)
user
}
}
fun getUser(id: Int): User? {
return transaction {
User.findById(id)
val user = User.find{ Users.id eq id }.firstOrNull()
user?.load(User::subordinates, User::managers)
user
}
}
fun getUser(discordID: Long): User? {
return transaction {
val users = User.find(Users.discord eq discordID)
users.firstOrNull()
val user = User.find{ Users.discord eq discordID }.firstOrNull()
user?.load(User::subordinates, User::managers)
user
}
}
fun getUser(chzzkID: String): User? {
return transaction {
val users = User.find(Users.token eq chzzkID)
users.firstOrNull()
val user = User.find{ Users.token eq chzzkID }.firstOrNull()
user?.load(User::subordinates, User::managers)
user
}
}
fun getUserWithGuildId(discordGuildId: Long): User? {
return transaction {
val users = User.find(Users.liveAlertGuild eq discordGuildId)
users.firstOrNull()
val user = User.find { Users.liveAlertGuild eq discordGuildId }.firstOrNull()
user?.load(User::subordinates, User::managers)
user
}
}
@@ -53,18 +70,31 @@ object UserService {
}
}
fun updateLiveAlert(id: Int, guildId: Long, channelId: Long, alertMessage: String?): User {
fun updateLiveAlert(user: User, guildId: Long, channelId: Long, alertMessage: String?): User {
return transaction {
val updated = Users.update({ Users.id eq id }) {
it[liveAlertGuild] = guildId
it[liveAlertChannel] = channelId
it[liveAlertMessage] = alertMessage ?: ""
}
user.liveAlertGuild = guildId
user.liveAlertChannel = channelId
user.liveAlertMessage = alertMessage ?: ""
if(updated == 0) throw RuntimeException("User not found! $id")
val users = User.find { Users.id eq id }
user.load(User::subordinates, User::managers)
return@transaction users.first()
user
}
}
fun setIsDisabled(user: User, disabled: Boolean): User {
return transaction {
user.isDisabled = disabled
user
}
}
fun setIsStartupDisabled(user: User, disabled: Boolean): User {
return transaction {
user.isDisableStartupMsg = disabled
user
}
}
}

View File

@@ -146,3 +146,22 @@ fun getStreamInfo(userId: String) : IData<IStreamInfo?> {
}
}
}
fun getUserInfo(userId: String): IData<Channel?> {
val url = "https://api.chzzk.naver.com/service/v1/channels/${userId}"
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).execute().use { response ->
try {
if(!response.isSuccessful) throw IOException("Unexpected code ${response.code}")
val body = response.body?.string()
val channel = gson.fromJson(body, object: TypeToken<IData<Channel?>>() {})
return channel
} catch(e: Exception) {
throw e
}
}
}

View File

@@ -15,7 +15,7 @@ data class YoutubeVideo(
val length: Int
)
val regex = ".*(?:youtu.be/|v/|u/\\w/|embed/|watch\\?v=|&v=)([^#&?]*).*".toRegex()
val regex = ".*(?:youtu.be/|v/|u/\\w/|embed/|watch\\?v=|&v=|music\\.youtube\\.com/.*?\\?v=)([^#&?]*).*".toRegex()
val durationRegex = """PT(\d+H)?(\d+M)?(\d+S)?""".toRegex()
val dotenv = dotenv {

View File

@@ -6,5 +6,9 @@ DB_PASS=chzzk
RUN_AGENT=false
YOUTUBE_API_KEY=
RAPID_KEY=
HOST=http://localhost:8080
FRONTEND=http://localhost:3000
NAVER_CLIENT_ID=
NAVER_CLIENT_SECRET=
NID_AUT=
NID_SES=

View File

@@ -10,7 +10,7 @@ repositories {
mavenCentral()
}
val ktorVersion = "2.3.12"
val ktorVersion = "3.0.1"
dependencies {
implementation("io.ktor:ktor-server-core:$ktorVersion")
@@ -18,22 +18,30 @@ dependencies {
implementation("io.ktor:ktor-server-websockets:$ktorVersion")
implementation("io.ktor:ktor-server-swagger:$ktorVersion")
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-server-cors:$ktorVersion")
implementation("io.ktor:ktor-server-swagger:$ktorVersion")
implementation("io.ktor:ktor-server-auth:$ktorVersion")
implementation("io.swagger.codegen.v3:swagger-codegen-generators:1.0.50")
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.swagger.codegen.v3:swagger-codegen-generators:1.0.54")
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect
implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.0")
implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.21")
// https://mvnrepository.com/artifact/io.insert-koin/koin-core
implementation("io.insert-koin:koin-core:4.0.0-RC1")
implementation("io.insert-koin:koin-core:4.0.0")
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
implementation("ch.qos.logback:logback-classic:1.5.6")
implementation("ch.qos.logback:logback-classic:1.5.12")
// https://mvnrepository.com/artifact/io.github.cdimascio/dotenv-kotlin
implementation("io.github.cdimascio:dotenv-kotlin:6.4.2")
implementation(project(":common"))

View File

@@ -1,24 +1,46 @@
package space.mori.chzzk_bot.webserver
import applicationHttpClient
import io.github.cdimascio.dotenv.dotenv
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.swagger.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import io.ktor.server.websocket.*
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.webserver.routes.*
import space.mori.chzzk_bot.webserver.utils.DiscordRatelimits
import wsSongListRoutes
import java.math.BigInteger
import java.security.SecureRandom
import java.time.Duration
import kotlin.time.toKotlinDuration
val server = embeddedServer(Netty, port = 8080) {
val dotenv = dotenv {
ignoreIfMissing = true
}
val redirects = mutableMapOf<String, String>()
val server = embeddedServer(Netty, port = 8080, ) {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(15)
timeout = Duration.ofSeconds(15)
pingPeriod = Duration.ofSeconds(15).toKotlinDuration()
timeout = Duration.ofSeconds(100).toKotlinDuration()
maxFrameSize = Long.MAX_VALUE
masking = false
contentConverter = KotlinxWebsocketSerializationConverter(Json)
@@ -30,22 +52,197 @@ val server = embeddedServer(Netty, port = 8080) {
isLenient = true
})
}
install(CORS) {
anyHost()
allowHeader(HttpHeaders.ContentType)
install(Sessions) {
cookie<UserSession>("user_session", storage = MariadbSessionStorage()) {}
}
install(Authentication) {
oauth("auth-oauth-discord") {
urlProvider = { "${dotenv["HOST"]}/auth/callback/discord" }
providerLookup = { OAuthServerSettings.OAuth2ServerSettings(
name = "discord",
authorizeUrl = "https://discord.com/oauth2/authorize",
accessTokenUrl = "https://discord.com/api/oauth2/token",
clientId = dotenv["DISCORD_CLIENT_ID"],
clientSecret = dotenv["DISCORD_CLIENT_SECRET"],
requestMethod = HttpMethod.Post,
defaultScopes = listOf(),
extraAuthParameters = listOf(
Pair("permissions", "826781943872"),
Pair("response_type", "code"),
Pair("integration_type", "0"),
Pair("scope", "guilds bot identify")
),
onStateCreated = { call, state ->
call.request.queryParameters["redirectUrl"]?.let {
redirects[state] = it
}
}
)}
client = applicationHttpClient
}
}
routing {
route("/auth") {
// discord login
authenticate("auth-oauth-discord") {
get("/login/discord") {
}
get("/callback/discord") {
try {
val principal = call.principal<OAuthAccessTokenResponse.OAuth2>()
val session = call.sessions.get<UserSession>()
val user = session?.id?.let { UserService.getUser(it) }
if(principal != null && session != null && user != null) {
try {
val accessToken = principal.accessToken
val userInfo = getDiscordUser(accessToken)
val guilds = getUserGuilds(accessToken)
userInfo?.user?.id?.toLong()?.let { id -> UserService.updateUser(user, id) }
call.sessions.set(UserSession(
session.state,
session.id,
guilds.filter {
it.owner
}.map { it.id }
))
redirects[principal.state]?.let { redirect ->
call.respondRedirect(redirect)
return@get
}
call.respondRedirect(getFrontendURL(""))
} catch(e: Exception) {
println(e.toString())
call.respondRedirect(getFrontendURL(""))
}
} else {
call.respondRedirect(getFrontendURL(""))
}
} catch(e: Exception) {
println(e.stackTrace)
}
}
}
// naver login
get("/login") {
val state = generateSecureRandomState()
// 세션에 상태 값 저장
call.sessions.set(UserSession(
state,
"",
listOf(),
))
// OAuth 제공자의 인증 URL 구성
val authUrl = URLBuilder("https://chzzk.naver.com/account-interlock").apply {
parameters.append("clientId", dotenv["NAVER_CLIENT_ID"]) // 비표준 파라미터 이름
parameters.append("redirectUri", "${dotenv["HOST"]}/auth/callback")
parameters.append("state", state)
// 추가적인 파라미터가 필요하면 여기에 추가
}.build().toString()
// 사용자에게 인증 페이지로 리다이렉트
call.respondRedirect(authUrl)
}
get("/callback") {
val receivedState = call.parameters["state"]
val code = call.parameters["code"]
// 세션에서 상태 값 가져오기
val session = call.sessions.get<UserSession>()
if (session == null || session.state != receivedState) {
call.respond(HttpStatusCode.BadRequest, "Invalid state parameter")
return@get
}
if (code == null) {
call.respond(HttpStatusCode.BadRequest, "Missing code parameter")
return@get
}
try {
// Access Token 요청
val tokenRequest = TokenRequest(
grantType = "authorization_code",
state = session.state,
code = code,
clientId = dotenv["NAVER_CLIENT_ID"],
clientSecret = dotenv["NAVER_CLIENT_SECRET"]
)
val response = applicationHttpClient.post("https://chzzk.naver.com/auth/v1/token") {
contentType(ContentType.Application.Json)
setBody(tokenRequest)
}
val tokenResponse = response.body<TokenResponse>()
if(tokenResponse.content == null) {
call.respond(HttpStatusCode.InternalServerError, "Failed to obtain access token")
return@get
}
// Access Token 사용: 예를 들어, 사용자 정보 요청
val userInfo = getChzzkUser(tokenResponse.content.accessToken)
if(userInfo.content != null) {
call.sessions.set(
UserSession(
session.state,
userInfo.content.channelId,
listOf()
)
)
call.respondRedirect(getFrontendURL(""))
}
} catch (e: Exception) {
e.printStackTrace()
call.respond(HttpStatusCode.InternalServerError, "Failed to obtain access token")
}
}
// common: logout
get("/logout") {
call.sessions.clear<UserSession>()
call.response.status(HttpStatusCode.OK)
return@get
}
}
apiRoutes()
apiSongRoutes()
apiCommandRoutes()
apiTimerRoutes()
apiDiscordRoutes()
wsTimerRoutes()
wsSongRoutes()
wsSongListRoutes()
swaggerUI("swagger-ui/index.html", "openapi/documentation.yaml") {
options {
version = "1.2.0"
}
}
}
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Patch)
allowMethod(HttpMethod.Post)
allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Get)
allowHost(dotenv["FRONTEND"] ?: "localhost:3000", schemes=listOf("https"))
allowCredentials = true
allowNonSimpleContentTypes = true
}
}
fun start() {
@@ -54,4 +251,179 @@ fun start() {
fun stop() {
server.stop()
}
fun getFrontendURL(path: String)
= "${if(dotenv["FRONTEND_HTTPS"].toBoolean()) "https://" else "http://" }${dotenv["FRONTEND"]}${path}"
@Serializable
data class UserSession(
val state: String,
val id: String,
val discordGuildList: List<String>,
)
@Serializable
data class TokenRequest(
val grantType: String,
val state: String,
val code: String,
val clientId: String,
val clientSecret: String
)
@Serializable
data class TokenResponse(
val code: Int,
val message: String?,
val content: TokenResponseBody?
)
@Serializable
data class TokenResponseBody(
val accessToken: String,
val tokenType: String,
val expiresIn: Int,
val refreshToken: String? = null
)
@Serializable
data class DiscordMeAPI(
val application: DiscordApplicationAPI,
val scopes: List<String>,
val user: DiscordUserAPI
)
@Serializable
data class DiscordApplicationAPI(
val id: String,
val name: String,
val icon: String,
val description: String,
val hook: Boolean,
val bot_public: Boolean,
val bot_require_code_grant: Boolean,
val verify_key: String
)
@Serializable
data class DiscordUserAPI(
val id: String,
val username: String,
val avatar: String,
val discriminator: String,
val global_name: String,
val public_flags: Int
)
@Serializable
data class DiscordGuildListAPI(
val id: String,
val name: String,
val icon: String?,
val banner: String?,
val owner: Boolean,
val permissions: Int,
val features: List<String>,
val roles: List<GuildRole>?
)
@Serializable
data class GuildRole(
val id: String,
val name: String,
val color: Int,
val mentionable: Boolean,
)
enum class ChannelType(val value: Int) {
GUILD_TEXT(0),
DM(1),
GUILD_VOICE(2),
GROUP_DM(3),
GUILD_CATEGORY(4),
GUILD_ANNOUNCEMENT(5),
ANNOUNCEMENT_THREAD(10),
PUBLIC_THREAD(11),
PRIVATE_THREAD(12),
GUILD_STAGE_VOICE(13),
GUILD_DIRECTORY(14),
GUILD_FORUM(15),
GUILD_MEDIA(16)
}
@Serializable
data class GuildChannel(
val id: String,
val type: Int,
val name: String?
)
suspend fun getDiscordUser(accessToken: String): DiscordMeAPI? {
if(DiscordRatelimits.isLimited()) {
delay(DiscordRatelimits.getRateReset())
}
val response: HttpResponse = applicationHttpClient.get("https://discord.com/api/oauth2/@me") {
headers {
append(HttpHeaders.Authorization, "Bearer $accessToken")
}
}
val rateLimit = response.headers["X-RateLimit-Limit"]?.toIntOrNull()
val remaining = response.headers["X-RateLimit-Remaining"]?.toIntOrNull()
val resetAfter = response.headers["X-RateLimit-Reset-After"]?.toDoubleOrNull()?.toLong()
DiscordRatelimits.setRateLimit(rateLimit, remaining, resetAfter)
return response.body<DiscordMeAPI?>()
}
suspend fun getUserGuilds(accessToken: String): List<DiscordGuildListAPI> {
if(DiscordRatelimits.isLimited()) {
delay(DiscordRatelimits.getRateReset())
}
val response = applicationHttpClient.get("https://discord.com/api/users/@me/guilds") {
headers {
append(HttpHeaders.Authorization, "Bearer $accessToken")
}
}
val rateLimit = response.headers["X-RateLimit-Limit"]?.toIntOrNull()
val remaining = response.headers["X-RateLimit-Remaining"]?.toIntOrNull()
val resetAfter = response.headers["X-RateLimit-Reset-After"]?.toDoubleOrNull()?.toLong()
DiscordRatelimits.setRateLimit(rateLimit, remaining, resetAfter)
return response.body<List<DiscordGuildListAPI>>()
}
@Serializable
data class ChzzkMeApi(
val channelId: String,
val channelName: String,
val nickname: String,
)
@Serializable
data class ChzzkApi<T>(
val code: Int,
val message: String?,
val content: T?
)
suspend fun getChzzkUser(accessToken: String): ChzzkApi<ChzzkMeApi> {
val response = applicationHttpClient.get("https://openapi.chzzk.naver.com/open/v1/users/me") {
headers {
append(HttpHeaders.Authorization, "Bearer $accessToken")
}
}
return response.body<ChzzkApi<ChzzkMeApi>>()
}
fun generateSecureRandomState(): String {
return BigInteger(130, SecureRandom()).toString(32)
}

View File

@@ -0,0 +1,41 @@
package space.mori.chzzk_bot.webserver
import io.ktor.server.sessions.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
import space.mori.chzzk_bot.common.models.Session
import space.mori.chzzk_bot.common.models.Sessions as SessionTable
class MariadbSessionStorage: SessionStorage {
override suspend fun invalidate(id: String) {
return transaction {
val session = Session.find(
SessionTable.key eq id
).firstOrNull()
session?.delete()
}
}
override suspend fun read(id: String): String {
return transaction {
val session = Session.find(SessionTable.key eq id).firstOrNull()
?: throw NoSuchElementException("Session $id not found")
session.value
}
}
override suspend fun write(id: String, value: String) {
return transaction {
val session = Session.find(SessionTable.key eq id).firstOrNull()
if (session == null) {
Session.new {
this.key = id
this.value = value
}
} else {
session.value = value
}
}
}
}

View File

@@ -0,0 +1,17 @@
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
@OptIn(ExperimentalSerializationApi::class)
val applicationHttpClient = HttpClient(CIO) {
install(ContentNegotiation) {
json(json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
explicitNulls = false
})
}
}

View File

@@ -0,0 +1,168 @@
package space.mori.chzzk_bot.webserver.routes
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.java.KoinJavaComponent.inject
import space.mori.chzzk_bot.common.events.CommandReloadEvent
import space.mori.chzzk_bot.common.events.CoroutinesEventBus
import space.mori.chzzk_bot.common.services.CommandService
import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.webserver.UserSession
fun Routing.apiCommandRoutes() {
val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
route("/commands") {
get("/{uid}") {
val uid = call.parameters["uid"]
if(uid == null) {
call.respond(HttpStatusCode.BadRequest, "UID is required")
return@get
}
val user = UserService.getUser(uid)
if(user == null) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@get
}
val commands = CommandService.getCommands(user)
call.respond(HttpStatusCode.OK, commands.map {
CommandsResponseDTO(it.command, it.content, it.failContent)
})
}
put("/{uid}") {
val uid = call.parameters["uid"]
val session = call.sessions.get<UserSession>()
val commandRequest = call.receive<CommandsRequestDTO>()
if(uid == null) {
call.respond(HttpStatusCode.BadRequest, "UID is required")
return@put
}
val user = UserService.getUser(uid)
if(user == null) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@put
}
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@put
}
CommandService.saveCommand(user,
commandRequest.label,
commandRequest.content,
commandRequest.failContent ?: ""
)
CoroutineScope(Dispatchers.Default).launch {
for(i: Int in 0..3) {
dispatcher.post(CommandReloadEvent(user.token))
}
}
call.respond(HttpStatusCode.OK)
}
post("/{uid}") {
val uid = call.parameters["uid"]
val session = call.sessions.get<UserSession>()
val commandRequest = call.receive<CommandsRequestDTO>()
if(uid == null) {
call.respond(HttpStatusCode.BadRequest, "UID is required")
return@post
}
val user = UserService.getUser(uid)
if(user == null) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@post
}
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@post
}
try {
CommandService.updateCommand(
user,
commandRequest.label,
commandRequest.content,
commandRequest.failContent ?: ""
)
CoroutineScope(Dispatchers.Default).launch {
for(i: Int in 0..3) {
dispatcher.post(CommandReloadEvent(user.token))
}
}
call.respond(HttpStatusCode.OK)
} catch(e: Exception) {
call.respond(HttpStatusCode.BadRequest)
}
}
delete("/{uid}") {
val uid = call.parameters["uid"]
val session = call.sessions.get<UserSession>()
val commandRequest = call.receive<CommandsRequestDTO>()
if(uid == null) {
call.respond(HttpStatusCode.BadRequest, "UID is required")
return@delete
}
val user = UserService.getUser(uid)
if(user == null) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@delete
}
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@delete
}
try {
CommandService.removeCommand(user, commandRequest.label)
CoroutineScope(Dispatchers.Default).launch {
for(i: Int in 0..3) {
dispatcher.post(CommandReloadEvent(user.token))
}
}
call.respond(HttpStatusCode.OK)
} catch(e: Exception) {
call.respond(HttpStatusCode.BadRequest)
}
}
}
}
@Serializable
data class CommandsRequestDTO(
val label: String,
val content: String,
val failContent: String?
)
@Serializable
data class CommandsResponseDTO(
val label: String,
val content: String,
val failContent: String?
)

View File

@@ -0,0 +1,120 @@
package space.mori.chzzk_bot.webserver.routes
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.java.KoinJavaComponent.inject
import space.mori.chzzk_bot.common.events.CoroutinesEventBus
import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.webserver.UserSession
import space.mori.chzzk_bot.webserver.utils.DiscordGuildCache
fun Route.apiDiscordRoutes() {
val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
route("/discord") {
get("/{uid}") {
val uid = call.parameters["uid"]
val session = call.sessions.get<UserSession>()
if(uid == null) {
call.respond(HttpStatusCode.BadRequest, "UID is required")
return@get
}
val user = UserService.getUser(uid)
if(user?.token == null) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@get
}
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@get
}
if (user.discord == null) {
call.respond(HttpStatusCode.NotFound)
return@get
}
call.respond(HttpStatusCode.OK, GuildSettings(
user.liveAlertGuild.toString(),
user.liveAlertChannel.toString(),
user.liveAlertMessage
))
return@get
}
post("/{uid}") {
val uid = call.parameters["uid"]
val session = call.sessions.get<UserSession>()
val body: GuildSettings = call.receive()
if(uid == null) {
call.respond(HttpStatusCode.BadRequest, "UID is required")
return@post
}
val user = UserService.getUser(uid)
if(user?.token == null) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@post
}
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@post
}
UserService.updateLiveAlert(user, body.guildId?.toLong() ?: 0L, body.channelId?.toLong() ?: 0L, body.message)
call.respond(HttpStatusCode.OK)
}
get("/guild/{gid}") {
val gid = call.parameters["gid"]
val session = call.sessions.get<UserSession>()
if(gid == null) {
call.respond(HttpStatusCode.BadRequest, "GID is required")
return@get
}
if(session == null) {
call.respond(HttpStatusCode.BadRequest, "Session is required")
return@get
}
val user = UserService.getUser(session.id)
if(user == null) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@get
}
val guild = DiscordGuildCache.getCachedGuilds(gid)
if(guild == null) {
call.respond(HttpStatusCode.NotFound)
return@get
}
call.respond(HttpStatusCode.OK, guild)
return@get
}
get("/guilds") {
val session = call.sessions.get<UserSession>()
if(session == null) {
call.respond(HttpStatusCode.BadRequest, "Session is required")
return@get
}
call.respond(HttpStatusCode.OK, DiscordGuildCache.getCachedGuilds(session.discordGuildList))
return@get
}
}
}
@Serializable
data class GuildSettings(
val guildId: String?,
val channelId: String?,
val message: String? = null,
)

View File

@@ -1,12 +1,17 @@
package space.mori.chzzk_bot.webserver.routes
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.java.KoinJavaComponent.inject
import space.mori.chzzk_bot.common.events.CoroutinesEventBus
import space.mori.chzzk_bot.common.services.SongConfigService
import space.mori.chzzk_bot.common.utils.getStreamInfo
import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.webserver.UserSession
import space.mori.chzzk_bot.webserver.utils.ChzzkUserCache
@Serializable
data class GetUserDTO(
@@ -25,13 +30,16 @@ data class GetSessionDTO(
val maxQueueSize: Int,
val maxUserSize: Int,
val isStreamerOnly: Boolean,
val isDisabled: Boolean
)
fun Routing.apiRoutes() {
val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
route("/") {
get {
call.respondText("Hello World!", status =
HttpStatusCode.OK)
HttpStatusCode.OK)
}
}
route("/health") {
@@ -47,8 +55,8 @@ fun Routing.apiRoutes() {
call.respondText("Require UID", status = HttpStatusCode.NotFound)
return@get
}
val user = getStreamInfo(uid)
if(user.content == null) {
val user = ChzzkUserCache.getCachedUser(uid)
if(user?.content == null) {
call.respondText("User not found", status = HttpStatusCode.NotFound)
return@get
} else {
@@ -63,38 +71,62 @@ fun Routing.apiRoutes() {
}
route("/user") {
get {
call.respondText("Require UID", status = HttpStatusCode.NotFound)
}
}
route("/session/{sid}") {
get {
val sid = call.parameters["sid"]
if(sid == null) {
call.respondText("Require SID", status = HttpStatusCode.NotFound)
val session = call.sessions.get<UserSession>()
if(session == null) {
call.respondText("No session found", status = HttpStatusCode.Unauthorized)
return@get
}
val user = SongConfigService.getUserByToken(sid)
val session = SongConfigService.getConfig(sid)
var user = UserService.getUser(session.id)
if(user == null) {
call.respondText("User not found", status = HttpStatusCode.NotFound)
return@get
} else {
val chzzkUser = getStreamInfo(user.token)
call.respond(HttpStatusCode.OK, GetSessionDTO(
chzzkUser.content!!.channel.channelId,
chzzkUser.content!!.channel.channelName,
chzzkUser.content!!.status == "OPEN",
chzzkUser.content!!.channel.channelImageUrl,
session!!.queueLimit,
session.personalLimit,
session.streamerOnly
))
user = UserService.saveUser("임시닉네임", session.id)
}
}
}
route("/session") {
get {
call.respondText("Require SID", status = HttpStatusCode.NotFound)
val songConfig = SongConfigService.getConfig(user)
val status = ChzzkUserCache.getCachedUser(session.id)
val returnUsers = mutableListOf<GetSessionDTO>()
if(status == null) {
call.respondText("No user found", status = HttpStatusCode.NotFound)
return@get
}
if (user.username == "임시닉네임") {
status.content?.channel?.let { it1 -> UserService.updateUser(user, it1.channelId, it1.channelName) }
}
returnUsers.add(GetSessionDTO(
status.content?.channel?.channelId ?: user.username,
status.content?.channel?.channelName ?: user.token,
status.content?.status == "OPEN",
status.content?.channel?.channelImageUrl ?: "",
songConfig.queueLimit,
songConfig.personalLimit,
songConfig.streamerOnly,
songConfig.disabled
))
val subordinates = transaction {
user.subordinates.toList()
}
returnUsers.addAll(subordinates.map {
val subStatus = ChzzkUserCache.getCachedUser(it.token)
return@map if (subStatus?.content == null) {
null
} else {
GetSessionDTO(
subStatus.content!!.channel.channelId,
subStatus.content!!.channel.channelName,
subStatus.content!!.status == "OPEN",
subStatus.content!!.channel.channelImageUrl,
0,
0,
false,
false
)
}
}.filterNotNull())
call.respond(HttpStatusCode.OK, returnUsers)
}
}
}

View File

@@ -8,6 +8,8 @@ import kotlinx.serialization.Serializable
import space.mori.chzzk_bot.common.models.SongList
import space.mori.chzzk_bot.common.services.SongListService
import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.common.utils.YoutubeVideo
import space.mori.chzzk_bot.webserver.utils.CurrentSong
@Serializable
data class SongsDTO(
@@ -18,6 +20,12 @@ data class SongsDTO(
val reqName: String
)
@Serializable
data class SongsResponseDTO(
val current: SongsDTO? = null,
val next: List<SongsDTO> = emptyList()
)
fun SongList.toDTO(): SongsDTO = SongsDTO(
this.url,
this.name,
@@ -26,6 +34,14 @@ fun SongList.toDTO(): SongsDTO = SongsDTO(
this.reqName
)
fun YoutubeVideo.toDTO(): SongsDTO = SongsDTO(
this.url,
this.name,
this.author,
this.length,
""
)
fun Routing.apiSongRoutes() {
route("/songs/{uid}") {
get {
@@ -37,7 +53,12 @@ fun Routing.apiSongRoutes() {
}
val songs = SongListService.getSong(user)
call.respond(HttpStatusCode.OK, songs.map { it.toDTO() })
call.respond(HttpStatusCode.OK,
SongsResponseDTO(
CurrentSong.getSong(user)?.toDTO(),
songs.map { it.toDTO() }
)
)
}
}
route("/songs") {

View File

@@ -0,0 +1,79 @@
package space.mori.chzzk_bot.webserver.routes
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.transactions.transaction
import space.mori.chzzk_bot.common.events.TimerType
import space.mori.chzzk_bot.common.services.TimerConfigService
import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.webserver.UserSession
fun Routing.apiTimerRoutes() {
route("/timerapi") {
get("/{uid}") {
val uid = call.parameters["uid"]
val session = call.sessions.get<UserSession>()
if(uid == null) {
call.respond(HttpStatusCode.BadRequest, "UID is required")
return@get
}
val user = UserService.getUser(uid)
if(user == null) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@get
}
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@get
}
val timerConfig = TimerConfigService.getConfig(user)
call.respond(HttpStatusCode.OK, TimerResponseDTO(timerConfig?.option ?: 0))
}
put("/{uid}") {
val uid = call.parameters["uid"]
val session = call.sessions.get<UserSession>()
val request = call.receive<TimerRequestDTO>()
if(uid == null) {
call.respond(HttpStatusCode.BadRequest, "UID is required")
return@put
}
val user = UserService.getUser(uid)
if(user == null) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@put
}
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@put
}
TimerConfigService.saveOrUpdateConfig(user, TimerType.entries[request.option])
call.respond(HttpStatusCode.OK)
}
}
}
@Serializable
data class TimerRequestDTO(
val option: Int
)
@Serializable
data class TimerResponseDTO(
val option: Int
)

View File

@@ -1,200 +1,291 @@
package space.mori.chzzk_bot.webserver.routes
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import io.ktor.server.websocket.*
import io.ktor.util.logging.Logger
import io.ktor.websocket.*
import io.ktor.websocket.Frame.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.koin.java.KoinJavaComponent.inject
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.common.events.*
import space.mori.chzzk_bot.common.models.Counters.withDefinition
import space.mori.chzzk_bot.common.models.SongList
import space.mori.chzzk_bot.common.models.SongLists.uid
import space.mori.chzzk_bot.common.models.User
import space.mori.chzzk_bot.common.services.SongConfigService
import space.mori.chzzk_bot.common.services.SongListService
import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.common.utils.YoutubeVideo
import space.mori.chzzk_bot.common.utils.getYoutubeVideo
import space.mori.chzzk_bot.webserver.UserSession
import space.mori.chzzk_bot.webserver.routes.SongResponse
import space.mori.chzzk_bot.webserver.routes.toSerializable
import space.mori.chzzk_bot.webserver.utils.CurrentSong
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
fun Routing.wsSongListRoutes() {
val sessions = ConcurrentHashMap<String, ConcurrentLinkedQueue<WebSocketServerSession>>()
val sessions = ConcurrentHashMap<String, WebSocketServerSession>()
val status = ConcurrentHashMap<String, SongType>()
val logger = LoggerFactory.getLogger("WSSongListRoutes")
val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
fun addSession(sid: String, session: WebSocketServerSession) {
sessions.computeIfAbsent(sid) { ConcurrentLinkedQueue() }.add(session)
fun addSession(uid: String, session: WebSocketServerSession) {
if (sessions[uid] != null) {
CoroutineScope(Dispatchers.Default).launch {
sessions[uid]?.close(
CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Duplicated sessions.")
)
}
}
sessions[uid] = session
}
fun removeSession(sid: String, session: WebSocketServerSession) {
sessions[sid]?.remove(session)
if(sessions[sid]?.isEmpty() == true) {
sessions.remove(sid)
fun removeSession(uid: String) {
sessions.remove(uid)
}
suspend fun waitForAck(ws: WebSocketServerSession, expectedType: Int): Boolean {
val timeout = 5000L // 5 seconds timeout
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < timeout) {
for (frame in ws.incoming) {
if (frame is Text) {
val message = frame.readText()
if(message == "ping") {
return true
}
val data = Json.decodeFromString<SongRequest>(message)
if (data.type == SongType.ACK.value) {
return true // ACK received
}
}
}
delay(100) // Check every 100 ms
}
return false // Timeout
}
suspend fun sendWithRetry(uid: String, res: SongResponse, maxRetries: Int = 5, delayMillis: Long = 3000L) {
var attempt = 0
var sentSuccessfully = false
while (attempt < maxRetries && !sentSuccessfully) {
val ws = sessions[uid]
try {
if(ws == null) {
delay(delayMillis)
continue
}
// Attempt to send the message
ws.sendSerialized(res)
logger.debug("Message sent successfully to $uid on attempt $attempt")
// Wait for ACK
val ackReceived = waitForAck(ws, res.type)
if (ackReceived == true) {
sentSuccessfully = true
} else {
logger.warn("ACK not received for message to $uid on attempt $attempt.")
}
} catch (e: Exception) {
attempt++
logger.warn("Failed to send message to $uid on attempt $attempt. Retrying in $delayMillis ms.")
logger.warn(e.stackTraceToString())
} finally {
// Wait before retrying
delay(delayMillis)
}
}
if (!sentSuccessfully) {
logger.error("Failed to send message to $uid after $maxRetries attempts.")
}
}
webSocket("/songlist/{sid}") {
val sid = call.parameters["sid"]
val session = sid?.let { SongConfigService.getConfig(it) }
val user = sid?.let {SongConfigService.getUserByToken(sid) }
if (sid == null) {
close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Invalid SID"))
return@webSocket
}
if (user == null || session == null) {
webSocket("/songlist") {
val session = call.sessions.get<UserSession>()
val user = session?.id?.let { UserService.getUser(it) }
if (user == null) {
close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Invalid SID"))
return@webSocket
}
addSession(sid, this)
val uid = user.token
if(status[sid] == SongType.STREAM_OFF) {
addSession(uid, this)
if (status[uid] == SongType.STREAM_OFF) {
CoroutineScope(Dispatchers.Default).launch {
sendSerialized(SongResponse(
SongType.STREAM_OFF.value,
user.token,
uid,
null,
null,
null,
null,
null
))
}
removeSession(sid, this)
removeSession(uid)
}
try {
for (frame in incoming) {
when(frame) {
is Frame.Text -> {
val data = frame.readText().let { Json.decodeFromString<SongRequest>(it) }
when (frame) {
is Text -> {
if (frame.readText().trim() == "ping") {
send("pong")
} else {
val data = frame.readText().let { Json.decodeFromString<SongRequest>(it) }
if(data.maxQueue != null && data.maxQueue > 0) SongConfigService.updateQueueLimit(user, data.maxQueue)
if(data.maxUserLimit != null && data.maxUserLimit > 0) SongConfigService.updatePersonalLimit(user, data.maxUserLimit)
if(data.isStreamerOnly != null) SongConfigService.updateStreamerOnly(user, data.isStreamerOnly)
if(data.type == SongType.ADD.value && data.url != null) {
try {
val youtubeVideo = getYoutubeVideo(data.url)
if (youtubeVideo != null) {
CoroutineScope(Dispatchers.Default).launch {
SongListService.saveSong(
user,
user.token,
data.url,
youtubeVideo.name,
youtubeVideo.author,
youtubeVideo.length,
user.username
)
dispatcher.post(
SongEvent(
user.token,
SongType.ADD,
user.token,
user.username,
youtubeVideo.name,
youtubeVideo.author,
youtubeVideo.length,
youtubeVideo.url
)
)
}
}
} catch(e: Exception) {
logger.debug("SongType.ADD Error: {} / {}", session.token, e)
}
}
else if(data.type == SongType.REMOVE.value && data.url != null) {
dispatcher.post(SongEvent(
user.token,
SongType.REMOVE,
null,
null,
null,
null,
0,
data.url
))
} else if(data.type == SongType.NEXT.value) {
val song = SongListService.getSong(user)[0]
SongListService.deleteSong(user, song.uid, song.name)
dispatcher.post(SongEvent(
user.token,
SongType.NEXT,
null,
null,
null,
null,
null,
null
))
// Handle song requests
handleSongRequest(data, user, dispatcher, logger)
}
}
is Frame.Ping -> send(Frame.Pong(frame.data))
else -> {
}
is Ping -> send(Pong(frame.data))
else -> ""
}
}
} catch(e: ClosedReceiveChannelException) {
} catch (e: ClosedReceiveChannelException) {
logger.error("Error in WebSocket: ${e.message}")
} finally {
removeSession(sid, this)
removeSession(uid)
}
}
dispatcher.subscribe(SongEvent::class) {
logger.debug("SongEvent: {} / {} {}", it.uid, it.type, it.name)
logger.debug("SongEvent: {} / {} {}", it.uid, it.type, it.current?.name)
CoroutineScope(Dispatchers.Default).launch {
val user = UserService.getUser(it.uid)
if(user != null) {
val session = SongConfigService.getConfig(user)
sessions[session.token ?: ""]?.forEach { ws ->
ws.sendSerialized(
SongResponse(
if (user != null) {
sendWithRetry(
user.token, SongResponse(
it.type.value,
it.uid,
it.reqUid,
it.current?.toSerializable(),
it.next?.toSerializable(),
it.delUrl
)
)
}
}
}
dispatcher.subscribe(TimerEvent::class) {
if (it.type == TimerType.STREAM_OFF) {
CoroutineScope(Dispatchers.Default).launch {
val user = UserService.getUser(it.uid)
if (user != null) {
sendWithRetry(
user.token, SongResponse(
it.type.value,
it.uid,
it.reqUid,
it.name,
it.author,
it.time,
it.url
null,
null,
null,
)
)
}
}
}
}
dispatcher.subscribe(TimerEvent::class) {
if(it.type == TimerType.STREAM_OFF) {
CoroutineScope(Dispatchers.Default).launch {
val user = UserService.getUser(it.uid)
if(user != null) {
val session = SongConfigService.getConfig(user)
}
sessions[session.token ?: ""]?.forEach { ws ->
ws.sendSerialized(
SongResponse(
it.type.value,
it.uid,
null,
null,
null,
null,
null
suspend fun handleSongRequest(
data: SongRequest,
user: User,
dispatcher: CoroutinesEventBus,
logger: Logger
) {
if (data.maxQueue != null && data.maxQueue > 0) SongConfigService.updateQueueLimit(user, data.maxQueue)
if (data.maxUserLimit != null && data.maxUserLimit > 0) SongConfigService.updatePersonalLimit(user, data.maxUserLimit)
if (data.isStreamerOnly != null) SongConfigService.updateStreamerOnly(user, data.isStreamerOnly)
if (data.isDisabled != null) SongConfigService.updateDisabled(user, data.isDisabled)
when (data.type) {
SongType.ADD.value -> {
data.url?.let { url ->
try {
val youtubeVideo = getYoutubeVideo(url)
if (youtubeVideo != null) {
CoroutineScope(Dispatchers.Default).launch {
SongListService.saveSong(
user,
user.token,
url,
youtubeVideo.name,
youtubeVideo.author,
youtubeVideo.length,
user.username
)
)
removeSession(session.token ?: "", ws)
dispatcher.post(
SongEvent(
user.token,
SongType.ADD,
user.token,
CurrentSong.getSong(user),
youtubeVideo
)
)
}
}
} catch (e: Exception) {
logger.debug("SongType.ADD Error: $uid $e")
}
}
}
SongType.REMOVE.value -> {
data.url?.let { url ->
val songs = SongListService.getSong(user)
val exactSong = songs.firstOrNull { it.url == url }
if (exactSong != null) {
SongListService.deleteSong(user, exactSong.uid, exactSong.name)
}
dispatcher.post(
SongEvent(
user.token,
SongType.REMOVE,
null,
null,
null,
url
)
)
}
}
SongType.NEXT.value -> {
val songList = SongListService.getSong(user)
var song: SongList? = null
var youtubeVideo: YoutubeVideo? = null
if (songList.isNotEmpty()) {
song = songList[0]
SongListService.deleteSong(user, song.uid, song.name)
}
song?.let {
youtubeVideo = YoutubeVideo(
song.url,
song.name,
song.author,
song.time
)
}
dispatcher.post(
SongEvent(
user.token,
SongType.NEXT,
song?.uid,
youtubeVideo
)
)
CurrentSong.setSong(user, youtubeVideo)
}
}
}
@@ -206,5 +297,6 @@ data class SongRequest(
val maxQueue: Int?,
val maxUserLimit: Int?,
val isStreamerOnly: Boolean?,
val remove: Int?
)
val remove: Int?,
val isDisabled: Boolean?,
)

View File

@@ -6,12 +6,14 @@ import io.ktor.websocket.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import org.koin.java.KoinJavaComponent.inject
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.common.events.*
import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.common.utils.YoutubeVideo
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
@@ -31,6 +33,41 @@ fun Routing.wsSongRoutes() {
}
}
suspend fun sendWithRetry(
session: WebSocketServerSession,
message: SongResponse,
maxRetries: Int = 3,
delayMillis: Long = 2000L
): Boolean {
var attempt = 0
while (attempt < maxRetries) {
try {
session.sendSerialized(message) // 메시지 전송 시도
return true // 성공하면 true 반환
} catch (e: Exception) {
attempt++
logger.info("Failed to send message on attempt $attempt. Retrying in $delayMillis ms.")
e.printStackTrace()
delay(delayMillis) // 재시도 전 대기
}
}
return false // 재시도 실패 시 false 반환
}
fun broadcastMessage(userId: String, message: SongResponse) {
val userSessions = sessions[userId]
userSessions?.forEach { session ->
CoroutineScope(Dispatchers.Default).launch {
val success = sendWithRetry(session, message)
if (!success) {
println("Removing session for user $userId due to repeated failures.")
userSessions.remove(session) // 실패 시 세션 제거
}
}
}
}
webSocket("/song/{uid}") {
val uid = call.parameters["uid"]
val user = uid?.let { UserService.getUser(it) }
@@ -53,8 +90,6 @@ fun Routing.wsSongRoutes() {
null,
null,
null,
null,
null
))
}
}
@@ -63,7 +98,9 @@ fun Routing.wsSongRoutes() {
for (frame in incoming) {
when(frame) {
is Frame.Text -> {
if(frame.readText().trim() == "ping") {
send("pong")
}
}
is Frame.Ping -> send(Frame.Pong(frame.data))
else -> {
@@ -81,47 +118,49 @@ fun Routing.wsSongRoutes() {
val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
dispatcher.subscribe(SongEvent::class) {
logger.debug("SongEvent: {} / {} {}", it.uid, it.type, it.name)
logger.debug("SongEvent: {} / {} {}", it.uid, it.type, it.current?.name)
CoroutineScope(Dispatchers.Default).launch {
sessions[it.uid]?.forEach { ws ->
ws.sendSerialized(SongResponse(
it.type.value,
it.uid,
it.reqUid,
it.name,
it.author,
it.time,
it.url
))
}
broadcastMessage(it.uid, SongResponse(
it.type.value,
it.uid,
it.reqUid,
it.current?.toSerializable(),
it.next?.toSerializable(),
it.delUrl
))
}
}
dispatcher.subscribe(TimerEvent::class) {
if(it.type == TimerType.STREAM_OFF) {
CoroutineScope(Dispatchers.Default).launch {
sessions[it.uid]?.forEach { ws ->
ws.sendSerialized(SongResponse(
it.type.value,
it.uid,
null,
null,
null,
null,
null
))
}
broadcastMessage(it.uid, SongResponse(
it.type.value,
it.uid,
null,
null,
null,
))
}
}
}
}
@Serializable
data class SerializableYoutubeVideo(
val url: String,
val name: String,
val author: String,
val length: Int
)
fun YoutubeVideo.toSerializable() = SerializableYoutubeVideo(url, name, author, length)
@Serializable
data class SongResponse(
val type: Int,
val uid: String,
val reqUid: String?,
val name: String?,
val author: String?,
val time: Int?,
val url: String?
val current: SerializableYoutubeVideo? = null,
val next: SerializableYoutubeVideo? = null,
val delUrl: String? = null
)

View File

@@ -13,12 +13,12 @@ import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.common.events.*
import space.mori.chzzk_bot.common.services.TimerConfigService
import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.webserver.utils.CurrentTimer
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
fun Routing.wsTimerRoutes() {
val sessions = ConcurrentHashMap<String, ConcurrentLinkedQueue<WebSocketServerSession>>()
val status = ConcurrentHashMap<String, TimerType>()
val logger = LoggerFactory.getLogger("WSTimerRoutes")
fun addSession(uid: String, session: WebSocketServerSession) {
@@ -45,17 +45,29 @@ fun Routing.wsTimerRoutes() {
}
addSession(uid, this)
val timer = CurrentTimer.getTimer(user)
if(status[uid] == TimerType.STREAM_OFF) {
if(timer?.type == TimerType.STREAM_OFF) {
CoroutineScope(Dispatchers.Default).launch {
sendSerialized(TimerResponse(TimerType.STREAM_OFF.value, null))
}
} else {
CoroutineScope(Dispatchers.Default).launch {
sendSerialized(TimerResponse(
TimerConfigService.getConfig(user)?.option ?: TimerType.REMOVE.value,
null
))
if (timer == null) {
sendSerialized(
TimerResponse(
TimerConfigService.getConfig(user)?.option ?: TimerType.REMOVE.value,
null
)
)
} else {
sendSerialized(
TimerResponse(
timer.type.value,
timer.time
)
)
}
}
}
@@ -63,7 +75,9 @@ fun Routing.wsTimerRoutes() {
for (frame in incoming) {
when(frame) {
is Frame.Text -> {
if(frame.readText().trim() == "ping") {
send("pong")
}
}
is Frame.Ping -> send(Frame.Pong(frame.data))
else -> {
@@ -82,7 +96,8 @@ fun Routing.wsTimerRoutes() {
dispatcher.subscribe(TimerEvent::class) {
logger.debug("TimerEvent: {} / {}", it.uid, it.type)
status[it.uid] = it.type
val user = UserService.getUser(it.uid)
CurrentTimer.setTimer(user!!, it)
CoroutineScope(Dispatchers.Default).launch {
sessions[it.uid]?.forEach { ws ->
ws.sendSerialized(TimerResponse(it.type.value, it.time ?: ""))

View File

@@ -0,0 +1,50 @@
package space.mori.chzzk_bot.webserver.utils
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.common.utils.IData
import space.mori.chzzk_bot.common.utils.IStreamInfo
import space.mori.chzzk_bot.common.utils.getStreamInfo
import space.mori.chzzk_bot.common.utils.getUserInfo
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
object ChzzkUserCache {
private val cache = ConcurrentHashMap<String, CachedUser>()
private const val EXP_SECONDS = 600L
private val mutex = Mutex()
private val logger = LoggerFactory.getLogger(this::class.java)
suspend fun getCachedUser(id: String): IData<IStreamInfo?>? {
val now = Instant.now()
var user = cache[id]
if(user == null || user.timestamp.plusSeconds(EXP_SECONDS).isBefore(now)) {
mutex.withLock {
if(user == null || user.timestamp.plusSeconds(EXP_SECONDS)?.isBefore(now) != false) {
var findUser = getStreamInfo(id)
if(findUser.content == null) {
val userInfo = getUserInfo(id)
if(userInfo.content == null) return null
findUser = IData(200, null, IStreamInfo(
channel = userInfo.content!!
))
}
user = CachedUser(findUser)
user.let { cache[id] = user }
}
}
}
return cache[id]?.user
}
}
data class CachedUser(
val user: IData<IStreamInfo?>,
val timestamp: Instant = Instant.now(),
)

View File

@@ -0,0 +1,19 @@
package space.mori.chzzk_bot.webserver.utils
import space.mori.chzzk_bot.common.models.User
import space.mori.chzzk_bot.common.utils.YoutubeVideo
import java.util.concurrent.ConcurrentHashMap
object CurrentSong {
private val currentSong = ConcurrentHashMap<String, YoutubeVideo>()
fun setSong(user: User, song: YoutubeVideo?) {
if(song == null) {
currentSong.remove(user.token ?: "")
} else {
currentSong[user.token ?: ""] = song
}
}
fun getSong(user: User) = currentSong[user.token ?: ""]
}

View File

@@ -0,0 +1,19 @@
package space.mori.chzzk_bot.webserver.utils
import space.mori.chzzk_bot.common.events.TimerEvent
import space.mori.chzzk_bot.common.models.User
import java.util.concurrent.ConcurrentHashMap
object CurrentTimer {
private val currentTimer = ConcurrentHashMap<String, TimerEvent>()
fun setTimer(user: User, timer: TimerEvent?) {
if(timer == null) {
currentTimer.remove(user.token ?: "")
} else {
currentTimer[user.token ?: ""] = timer
}
}
fun getTimer(user: User) = currentTimer[user.token ?: ""]
}

View File

@@ -0,0 +1,188 @@
package space.mori.chzzk_bot.webserver.utils
import applicationHttpClient
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.Serializable
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.webserver.*
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
object DiscordGuildCache {
private val cache = ConcurrentHashMap<String, CachedGuilds>()
private const val EXP_SECONDS = 600L
private val mutex = Mutex()
private val logger = LoggerFactory.getLogger(this::class.java)
suspend fun getCachedGuilds(guildId: String): Guild? {
val now = Instant.now()
var guild = cache[guildId]
if(guild == null || guild.timestamp.plusSeconds(EXP_SECONDS).isBefore(now) || !guild.isBotAvailable) {
mutex.withLock {
if(guild == null || guild!!.timestamp.plusSeconds(EXP_SECONDS).isBefore(now) || !guild!!.isBotAvailable) {
fetchAllGuilds()
guild = cache[guildId]
}
}
}
try {
if(guild == null) return null
if (guild!!.guild.roles.isEmpty()) {
val roles = fetchGuildRoles(guildId)
guild!!.guild.roles.addAll(roles)
}
if (guild!!.guild.channel.isEmpty()) {
val channels = fetchGuildChannels(guildId)
guild!!.guild.channel.addAll(channels)
}
} catch(e: Exception) {
logger.info("guild fetch is failed. ${e.stackTraceToString()}")
return null
}
return cache[guildId]?.guild
}
suspend fun getCachedGuilds(guildId: List<String>): List<Guild> {
return guildId.mapNotNull { getCachedGuilds(it) }
}
private suspend fun fetchGuilds(beforeGuildId: String? = null): List<DiscordGuildListAPI> {
if(DiscordRatelimits.isLimited()) {
delay(DiscordRatelimits.getRateReset())
}
val result = applicationHttpClient.get("https://discord.com/api/users/@me/guilds") {
headers {
append(HttpHeaders.Authorization, "Bot ${dotenv["DISCORD_TOKEN"]}")
}
parameter("limit", 200)
if (beforeGuildId != null) {
parameter("before", beforeGuildId)
}
}
val rateLimit = result.headers["X-RateLimit-Limit"]?.toIntOrNull()
val remaining = result.headers["X-RateLimit-Remaining"]?.toIntOrNull()
val resetAfter = result.headers["X-RateLimit-Reset-After"]?.toDoubleOrNull()?.toLong()?.plus(1L)
DiscordRatelimits.setRateLimit(rateLimit, remaining, resetAfter)
return result.body<List<DiscordGuildListAPI>>()
}
private suspend fun fetchGuildRoles(guildId: String): MutableList<GuildRole> {
if(DiscordRatelimits.isLimited()) {
delay(DiscordRatelimits.getRateReset())
}
try {
val result = applicationHttpClient.get("https://discord.com/api/guilds/${guildId}/roles") {
headers {
append(HttpHeaders.Authorization, "Bot ${dotenv["DISCORD_TOKEN"]}")
}
}
val rateLimit = result.headers["X-RateLimit-Limit"]?.toIntOrNull()
val remaining = result.headers["X-RateLimit-Remaining"]?.toIntOrNull()
val resetAfter = result.headers["X-RateLimit-Reset-After"]?.toDoubleOrNull()?.toLong()?.plus(1L)
DiscordRatelimits.setRateLimit(rateLimit, remaining, resetAfter)
if (result.status != HttpStatusCode.OK) {
logger.error("Failed to fetch data from Discord API. Status: ${result.status} ${result.bodyAsText()}")
return mutableListOf()
}
val parsed = result.body<MutableList<GuildRole>>()
return parsed
} catch(e: Exception) {
logger.info("fetchGuildRoles error: ${e.stackTraceToString()}")
return mutableListOf()
}
}
private suspend fun fetchGuildChannels(guildId: String): MutableList<GuildChannel> {
if(DiscordRatelimits.isLimited()) {
delay(DiscordRatelimits.getRateReset())
}
try {
val result = applicationHttpClient.get("https://discord.com/api/guilds/${guildId}/channels") {
headers {
append(HttpHeaders.Authorization, "Bot ${dotenv["DISCORD_TOKEN"]}")
}
}
val rateLimit = result.headers["X-RateLimit-Limit"]?.toIntOrNull()
val remaining = result.headers["X-RateLimit-Remaining"]?.toIntOrNull()
val resetAfter = result.headers["X-RateLimit-Reset-After"]?.toDoubleOrNull()?.toLong()?.plus(1L)
DiscordRatelimits.setRateLimit(rateLimit, remaining, resetAfter)
if (result.status != HttpStatusCode.OK) {
logger.error("Failed to fetch data from Discord API. Status: ${result.status} ${result.bodyAsText()}")
return mutableListOf()
}
val parsed = result.body<List<GuildChannel>>().filter { it.type == ChannelType.GUILD_TEXT.value }.toMutableList()
return parsed
} catch(e: Exception) {
logger.info("fetchGuildRoles error: ${e.stackTraceToString()}")
return mutableListOf()
}
}
private suspend fun fetchAllGuilds() {
var lastGuildId: String? = null
while (true) {
try {
val guilds = fetchGuilds(lastGuildId)
if (guilds.isEmpty()) {
break
}
guilds.forEach {
cache[it.id] = CachedGuilds(
Guild(it.id, it.name, it.icon, it.banner, it.roles?.toMutableList() ?: mutableListOf(), mutableListOf()),
Instant.now().plusSeconds(EXP_SECONDS),
true
)
}
lastGuildId = guilds.last().id
if(guilds.size <= 200) break
} catch(e: Exception) {
logger.info("Exception in discord caches. ${e.stackTraceToString()}")
return
}
}
}
fun addGuild(guilds: Map<String, Guild>) {
cache.putAll(guilds.map {
it.key to CachedGuilds(it.value, Instant.now().plusSeconds(EXP_SECONDS))
})
}
}
data class CachedGuilds(
val guild: Guild,
val timestamp: Instant = Instant.now(),
val isBotAvailable: Boolean = false,
)
@Serializable
data class Guild(
val id: String,
val name: String,
val icon: String?,
val banner: String?,
var roles: MutableList<GuildRole>,
var channel: MutableList<GuildChannel>
)

View File

@@ -0,0 +1,36 @@
package space.mori.chzzk_bot.webserver.utils
import java.time.Duration
import java.time.Instant
object DiscordRatelimits {
private var rateLimit = RateLimit(0, 5, Instant.now())
fun isLimited(): Boolean {
return rateLimit.remainin == 0
}
fun getRateReset(): Long {
val now = Instant.now()
val resetInstant = rateLimit.resetAfter
return if (resetInstant.isAfter(now)) {
Duration.between(now, resetInstant).toMillis()
} else {
0L // 이미 Rate Limit이 해제된 경우, 대기 시간은 0
}
}
private fun setRateLimit(rateLimit: RateLimit) {
this.rateLimit = rateLimit
}
fun setRateLimit(limit: Int?, remaining: Int?, resetAfter: Long?) {
return setRateLimit(RateLimit(limit ?: 0, remaining ?: 0, Instant.now().plusSeconds(resetAfter ?: 0L)))
}
}
data class RateLimit(
val limit: Int,
val remainin: Int,
val resetAfter: Instant,
)