diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..b378cf8 --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +compile1.bat +compile2.bat +FILEINFO +theme/default/config.xml \ No newline at end of file diff --git a/FILEINFO b/FILEINFO new file mode 100644 index 0000000..f01de13 --- /dev/null +++ b/FILEINFO @@ -0,0 +1,34 @@ +□ excluded +■ included + +■client.html ── executable +□comfile.bat ── source js를 browerify +■readme.txt ── 사용법 및 버전정보 +□FILEINFO ── this +■images +├ badge +│ ├ bits*.png ── 비트 도네이션 뱃지(18개 항목) +│ ├ broadcaster.png ── 스트리머 뱃지 +│ ├ mod.png ── 진은검 뱃지 +│ ├ prime.png ── 왕관(트위치 프라임) 뱃지 +│ ├ subs*.png ── 구독자 뱃지(1개 항목) +│ ├ turbo.png ── 트위치 터보 뱃지 +│ └ verified.png ── 인증됨 뱃지 +├ cheer +│ ├ *1.png ─┬ 테마별, 금액별 후원 아이콘 이미지 +│ ├ *100.png │ +│ ├ *1000.png │ +│ ├ *5000.png │ +│ └ *10000.png ─┘ +└ dccon + ├ *.png ─┬ 디씨콘 이미지 + └ *.gif ─┘ +■lib +├ browerified.js ── source js를 browerify한 결과 파일 +├ config.js ── 설정 저장 파일 +└ dccon_list.js ── 디씨콘 목록 저장 파일 +□source +└ main.js ── core nodeJS for twitch IRC +■theme +└ default ── 기본 테마 + └ theme.css ── 테마 메인파일 \ No newline at end of file diff --git a/README.md b/README.md index af81198..5482ab1 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ -# BridgeBBCC +Bridge BBCC +=== +version 0.2.0 + + + +## Usage +To compile browserified.js, just use +``` +npm start +``` +If you don't want to modify source files, just use released version. + +readme.txt 참조 +윈도우, XSplit 외의 환경에서 동작이 보증되지 않음 + + + +## License +MIT + +## Reference +[JSAssist Open DCcon](https://github.com/rishubil/jsassist-open-dccon) +By [rishubil]((https://github.com/rishubil), 2017, MIT License. + +[Twitch Developer Documentation](https://dev.twitch.tv/docs) \ No newline at end of file diff --git a/client.html b/client.html new file mode 100644 index 0000000..6e434ea --- /dev/null +++ b/client.html @@ -0,0 +1,16 @@ + + + + + + + + + +
+
+ + + + + \ No newline at end of file diff --git a/compile1.bat b/compile1.bat new file mode 100644 index 0000000..85c77d5 --- /dev/null +++ b/compile1.bat @@ -0,0 +1,3 @@ +@echo off + +browserify source/main.js -o lib/browserified.js \ No newline at end of file diff --git a/compile2.bat b/compile2.bat new file mode 100644 index 0000000..7aa3570 --- /dev/null +++ b/compile2.bat @@ -0,0 +1,11 @@ +@echo off + +xcopy client.html ..\BridgeBBCC /K /Y +xcopy readme.txt ..\BridgeBBCC /K /Y +xcopy theme ..\BridgeBBCC\theme /K /E /Y +xcopy lib\browserified.js ..\BridgeBBCC\lib /K /Y + +xcopy client.html ..\BridgeBBCC_yeokka_edition /K /Y +xcopy readme.txt ..\BridgeBBCC_yeokka_edition /K /Y +xcopy theme ..\BridgeBBCC_yeokka_edition\theme /K /E /Y +xcopy lib\browserified.js ..\BridgeBBCC_yeokka_edition\lib /K /Y diff --git a/images/badge/bits1.png b/images/badge/bits1.png new file mode 100644 index 0000000..37e924c Binary files /dev/null and b/images/badge/bits1.png differ diff --git a/images/badge/bits100.png b/images/badge/bits100.png new file mode 100644 index 0000000..3019980 Binary files /dev/null and b/images/badge/bits100.png differ diff --git a/images/badge/bits1000.png b/images/badge/bits1000.png new file mode 100644 index 0000000..d6c0ed0 Binary files /dev/null and b/images/badge/bits1000.png differ diff --git a/images/badge/bits10000.png b/images/badge/bits10000.png new file mode 100644 index 0000000..a9d6862 Binary files /dev/null and b/images/badge/bits10000.png differ diff --git a/images/badge/bits100000.png b/images/badge/bits100000.png new file mode 100644 index 0000000..adc2543 Binary files /dev/null and b/images/badge/bits100000.png differ diff --git a/images/badge/bits1000000.png b/images/badge/bits1000000.png new file mode 100644 index 0000000..e2fcf6e Binary files /dev/null and b/images/badge/bits1000000.png differ diff --git a/images/badge/bits200000.png b/images/badge/bits200000.png new file mode 100644 index 0000000..cf0447a Binary files /dev/null and b/images/badge/bits200000.png differ diff --git a/images/badge/bits25000.png b/images/badge/bits25000.png new file mode 100644 index 0000000..208740b Binary files /dev/null and b/images/badge/bits25000.png differ diff --git a/images/badge/bits300000.png b/images/badge/bits300000.png new file mode 100644 index 0000000..57a78f3 Binary files /dev/null and b/images/badge/bits300000.png differ diff --git a/images/badge/bits400000.png b/images/badge/bits400000.png new file mode 100644 index 0000000..cf07275 Binary files /dev/null and b/images/badge/bits400000.png differ diff --git a/images/badge/bits5000.png b/images/badge/bits5000.png new file mode 100644 index 0000000..89a7a40 Binary files /dev/null and b/images/badge/bits5000.png differ diff --git a/images/badge/bits50000.png b/images/badge/bits50000.png new file mode 100644 index 0000000..a8d1396 Binary files /dev/null and b/images/badge/bits50000.png differ diff --git a/images/badge/bits500000.png b/images/badge/bits500000.png new file mode 100644 index 0000000..255d203 Binary files /dev/null and b/images/badge/bits500000.png differ diff --git a/images/badge/bits600000.png b/images/badge/bits600000.png new file mode 100644 index 0000000..43d76c2 Binary files /dev/null and b/images/badge/bits600000.png differ diff --git a/images/badge/bits700000.png b/images/badge/bits700000.png new file mode 100644 index 0000000..100d899 Binary files /dev/null and b/images/badge/bits700000.png differ diff --git a/images/badge/bits75000.png b/images/badge/bits75000.png new file mode 100644 index 0000000..7a2bc5a Binary files /dev/null and b/images/badge/bits75000.png differ diff --git a/images/badge/bits800000.png b/images/badge/bits800000.png new file mode 100644 index 0000000..3ffb689 Binary files /dev/null and b/images/badge/bits800000.png differ diff --git a/images/badge/bits900000.png b/images/badge/bits900000.png new file mode 100644 index 0000000..359d6e7 Binary files /dev/null and b/images/badge/bits900000.png differ diff --git a/images/badge/broadcaster.png b/images/badge/broadcaster.png new file mode 100644 index 0000000..de84119 Binary files /dev/null and b/images/badge/broadcaster.png differ diff --git a/images/badge/mod.png b/images/badge/mod.png new file mode 100644 index 0000000..16a8750 Binary files /dev/null and b/images/badge/mod.png differ diff --git a/images/badge/prime.png b/images/badge/prime.png new file mode 100644 index 0000000..4038afb Binary files /dev/null and b/images/badge/prime.png differ diff --git a/images/badge/subs0.png b/images/badge/subs0.png new file mode 100644 index 0000000..b380f03 Binary files /dev/null and b/images/badge/subs0.png differ diff --git a/images/badge/subs1.png b/images/badge/subs1.png new file mode 100644 index 0000000..b380f03 Binary files /dev/null and b/images/badge/subs1.png differ diff --git a/images/badge/subs12.png b/images/badge/subs12.png new file mode 100644 index 0000000..b380f03 Binary files /dev/null and b/images/badge/subs12.png differ diff --git a/images/badge/subs24.png b/images/badge/subs24.png new file mode 100644 index 0000000..b380f03 Binary files /dev/null and b/images/badge/subs24.png differ diff --git a/images/badge/subs3.png b/images/badge/subs3.png new file mode 100644 index 0000000..b380f03 Binary files /dev/null and b/images/badge/subs3.png differ diff --git a/images/badge/subs6.png b/images/badge/subs6.png new file mode 100644 index 0000000..b380f03 Binary files /dev/null and b/images/badge/subs6.png differ diff --git a/images/badge/turbo.png b/images/badge/turbo.png new file mode 100644 index 0000000..6a6ed61 Binary files /dev/null and b/images/badge/turbo.png differ diff --git a/images/badge/verified.png b/images/badge/verified.png new file mode 100644 index 0000000..4cada70 Binary files /dev/null and b/images/badge/verified.png differ diff --git a/images/cheer/4Head1.gif b/images/cheer/4Head1.gif new file mode 100644 index 0000000..d5125f8 Binary files /dev/null and b/images/cheer/4Head1.gif differ diff --git a/images/cheer/4Head100.gif b/images/cheer/4Head100.gif new file mode 100644 index 0000000..e5be64d Binary files /dev/null and b/images/cheer/4Head100.gif differ diff --git a/images/cheer/4Head1000.gif b/images/cheer/4Head1000.gif new file mode 100644 index 0000000..4827db3 Binary files /dev/null and b/images/cheer/4Head1000.gif differ diff --git a/images/cheer/4Head10000.gif b/images/cheer/4Head10000.gif new file mode 100644 index 0000000..8bf0711 Binary files /dev/null and b/images/cheer/4Head10000.gif differ diff --git a/images/cheer/4Head5000.gif b/images/cheer/4Head5000.gif new file mode 100644 index 0000000..901d19e Binary files /dev/null and b/images/cheer/4Head5000.gif differ diff --git a/images/cheer/FailFish1.gif b/images/cheer/FailFish1.gif new file mode 100644 index 0000000..023f8f7 Binary files /dev/null and b/images/cheer/FailFish1.gif differ diff --git a/images/cheer/FailFish100.gif b/images/cheer/FailFish100.gif new file mode 100644 index 0000000..f37c904 Binary files /dev/null and b/images/cheer/FailFish100.gif differ diff --git a/images/cheer/FailFish1000.gif b/images/cheer/FailFish1000.gif new file mode 100644 index 0000000..8b0622e Binary files /dev/null and b/images/cheer/FailFish1000.gif differ diff --git a/images/cheer/FailFish10000.gif b/images/cheer/FailFish10000.gif new file mode 100644 index 0000000..148d04a Binary files /dev/null and b/images/cheer/FailFish10000.gif differ diff --git a/images/cheer/FailFish5000.gif b/images/cheer/FailFish5000.gif new file mode 100644 index 0000000..df8cd42 Binary files /dev/null and b/images/cheer/FailFish5000.gif differ diff --git a/images/cheer/Kappa1.gif b/images/cheer/Kappa1.gif new file mode 100644 index 0000000..0daccad Binary files /dev/null and b/images/cheer/Kappa1.gif differ diff --git a/images/cheer/Kappa100.gif b/images/cheer/Kappa100.gif new file mode 100644 index 0000000..b26e232 Binary files /dev/null and b/images/cheer/Kappa100.gif differ diff --git a/images/cheer/Kappa1000.gif b/images/cheer/Kappa1000.gif new file mode 100644 index 0000000..761d7bc Binary files /dev/null and b/images/cheer/Kappa1000.gif differ diff --git a/images/cheer/Kappa10000.gif b/images/cheer/Kappa10000.gif new file mode 100644 index 0000000..65f80e6 Binary files /dev/null and b/images/cheer/Kappa10000.gif differ diff --git a/images/cheer/Kappa5000.gif b/images/cheer/Kappa5000.gif new file mode 100644 index 0000000..7e59246 Binary files /dev/null and b/images/cheer/Kappa5000.gif differ diff --git a/images/cheer/Kreygasm1.gif b/images/cheer/Kreygasm1.gif new file mode 100644 index 0000000..83d6add Binary files /dev/null and b/images/cheer/Kreygasm1.gif differ diff --git a/images/cheer/Kreygasm100.gif b/images/cheer/Kreygasm100.gif new file mode 100644 index 0000000..7e32c82 Binary files /dev/null and b/images/cheer/Kreygasm100.gif differ diff --git a/images/cheer/Kreygasm1000.gif b/images/cheer/Kreygasm1000.gif new file mode 100644 index 0000000..13b5dbe Binary files /dev/null and b/images/cheer/Kreygasm1000.gif differ diff --git a/images/cheer/Kreygasm10000.gif b/images/cheer/Kreygasm10000.gif new file mode 100644 index 0000000..264f740 Binary files /dev/null and b/images/cheer/Kreygasm10000.gif differ diff --git a/images/cheer/Kreygasm5000.gif b/images/cheer/Kreygasm5000.gif new file mode 100644 index 0000000..7504aef Binary files /dev/null and b/images/cheer/Kreygasm5000.gif differ diff --git a/images/cheer/MrDestructoid1.gif b/images/cheer/MrDestructoid1.gif new file mode 100644 index 0000000..10156c8 Binary files /dev/null and b/images/cheer/MrDestructoid1.gif differ diff --git a/images/cheer/MrDestructoid100.gif b/images/cheer/MrDestructoid100.gif new file mode 100644 index 0000000..29b241e Binary files /dev/null and b/images/cheer/MrDestructoid100.gif differ diff --git a/images/cheer/MrDestructoid1000.gif b/images/cheer/MrDestructoid1000.gif new file mode 100644 index 0000000..53cd030 Binary files /dev/null and b/images/cheer/MrDestructoid1000.gif differ diff --git a/images/cheer/MrDestructoid10000.gif b/images/cheer/MrDestructoid10000.gif new file mode 100644 index 0000000..f0cd7a9 Binary files /dev/null and b/images/cheer/MrDestructoid10000.gif differ diff --git a/images/cheer/MrDestructoid5000.gif b/images/cheer/MrDestructoid5000.gif new file mode 100644 index 0000000..86a485c Binary files /dev/null and b/images/cheer/MrDestructoid5000.gif differ diff --git a/images/cheer/NotLikeThis1.gif b/images/cheer/NotLikeThis1.gif new file mode 100644 index 0000000..2b4efc1 Binary files /dev/null and b/images/cheer/NotLikeThis1.gif differ diff --git a/images/cheer/NotLikeThis100.gif b/images/cheer/NotLikeThis100.gif new file mode 100644 index 0000000..a0ab397 Binary files /dev/null and b/images/cheer/NotLikeThis100.gif differ diff --git a/images/cheer/NotLikeThis1000.gif b/images/cheer/NotLikeThis1000.gif new file mode 100644 index 0000000..645b9fd Binary files /dev/null and b/images/cheer/NotLikeThis1000.gif differ diff --git a/images/cheer/NotLikeThis10000.gif b/images/cheer/NotLikeThis10000.gif new file mode 100644 index 0000000..ff19206 Binary files /dev/null and b/images/cheer/NotLikeThis10000.gif differ diff --git a/images/cheer/NotLikeThis5000.gif b/images/cheer/NotLikeThis5000.gif new file mode 100644 index 0000000..3049bc1 Binary files /dev/null and b/images/cheer/NotLikeThis5000.gif differ diff --git a/images/cheer/PJSalt1.gif b/images/cheer/PJSalt1.gif new file mode 100644 index 0000000..ae8bd07 Binary files /dev/null and b/images/cheer/PJSalt1.gif differ diff --git a/images/cheer/PJSalt100.gif b/images/cheer/PJSalt100.gif new file mode 100644 index 0000000..94e9c12 Binary files /dev/null and b/images/cheer/PJSalt100.gif differ diff --git a/images/cheer/PJSalt1000.gif b/images/cheer/PJSalt1000.gif new file mode 100644 index 0000000..d6daac6 Binary files /dev/null and b/images/cheer/PJSalt1000.gif differ diff --git a/images/cheer/PJSalt10000.gif b/images/cheer/PJSalt10000.gif new file mode 100644 index 0000000..3cc3f42 Binary files /dev/null and b/images/cheer/PJSalt10000.gif differ diff --git a/images/cheer/PJSalt5000.gif b/images/cheer/PJSalt5000.gif new file mode 100644 index 0000000..38a27e7 Binary files /dev/null and b/images/cheer/PJSalt5000.gif differ diff --git a/images/cheer/SwiftRage1.gif b/images/cheer/SwiftRage1.gif new file mode 100644 index 0000000..494e3e7 Binary files /dev/null and b/images/cheer/SwiftRage1.gif differ diff --git a/images/cheer/SwiftRage100.gif b/images/cheer/SwiftRage100.gif new file mode 100644 index 0000000..f390c31 Binary files /dev/null and b/images/cheer/SwiftRage100.gif differ diff --git a/images/cheer/SwiftRage1000.gif b/images/cheer/SwiftRage1000.gif new file mode 100644 index 0000000..c1b74d5 Binary files /dev/null and b/images/cheer/SwiftRage1000.gif differ diff --git a/images/cheer/SwiftRage10000.gif b/images/cheer/SwiftRage10000.gif new file mode 100644 index 0000000..e010749 Binary files /dev/null and b/images/cheer/SwiftRage10000.gif differ diff --git a/images/cheer/SwiftRage5000.gif b/images/cheer/SwiftRage5000.gif new file mode 100644 index 0000000..28c9394 Binary files /dev/null and b/images/cheer/SwiftRage5000.gif differ diff --git a/images/cheer/TriHard1.gif b/images/cheer/TriHard1.gif new file mode 100644 index 0000000..3234bc6 Binary files /dev/null and b/images/cheer/TriHard1.gif differ diff --git a/images/cheer/TriHard100.gif b/images/cheer/TriHard100.gif new file mode 100644 index 0000000..1554a48 Binary files /dev/null and b/images/cheer/TriHard100.gif differ diff --git a/images/cheer/TriHard1000.gif b/images/cheer/TriHard1000.gif new file mode 100644 index 0000000..4d71b59 Binary files /dev/null and b/images/cheer/TriHard1000.gif differ diff --git a/images/cheer/TriHard10000.gif b/images/cheer/TriHard10000.gif new file mode 100644 index 0000000..3c5782e Binary files /dev/null and b/images/cheer/TriHard10000.gif differ diff --git a/images/cheer/TriHard5000.gif b/images/cheer/TriHard5000.gif new file mode 100644 index 0000000..f786101 Binary files /dev/null and b/images/cheer/TriHard5000.gif differ diff --git a/images/cheer/VoHiYo1.gif b/images/cheer/VoHiYo1.gif new file mode 100644 index 0000000..0b71d70 Binary files /dev/null and b/images/cheer/VoHiYo1.gif differ diff --git a/images/cheer/VoHiYo100.gif b/images/cheer/VoHiYo100.gif new file mode 100644 index 0000000..d642413 Binary files /dev/null and b/images/cheer/VoHiYo100.gif differ diff --git a/images/cheer/VoHiYo1000.gif b/images/cheer/VoHiYo1000.gif new file mode 100644 index 0000000..90c405c Binary files /dev/null and b/images/cheer/VoHiYo1000.gif differ diff --git a/images/cheer/VoHiYo10000.gif b/images/cheer/VoHiYo10000.gif new file mode 100644 index 0000000..8253fa4 Binary files /dev/null and b/images/cheer/VoHiYo10000.gif differ diff --git a/images/cheer/VoHiYo5000.gif b/images/cheer/VoHiYo5000.gif new file mode 100644 index 0000000..155e05a Binary files /dev/null and b/images/cheer/VoHiYo5000.gif differ diff --git a/images/cheer/bday1.gif b/images/cheer/bday1.gif new file mode 100644 index 0000000..f8110c0 Binary files /dev/null and b/images/cheer/bday1.gif differ diff --git a/images/cheer/bday100.gif b/images/cheer/bday100.gif new file mode 100644 index 0000000..b7cb3b0 Binary files /dev/null and b/images/cheer/bday100.gif differ diff --git a/images/cheer/bday1000.gif b/images/cheer/bday1000.gif new file mode 100644 index 0000000..a28ce07 Binary files /dev/null and b/images/cheer/bday1000.gif differ diff --git a/images/cheer/bday10000.gif b/images/cheer/bday10000.gif new file mode 100644 index 0000000..b6fd31f Binary files /dev/null and b/images/cheer/bday10000.gif differ diff --git a/images/cheer/bday5000.gif b/images/cheer/bday5000.gif new file mode 100644 index 0000000..e910af2 Binary files /dev/null and b/images/cheer/bday5000.gif differ diff --git a/images/cheer/cheer1.gif b/images/cheer/cheer1.gif new file mode 100644 index 0000000..6cde9d8 Binary files /dev/null and b/images/cheer/cheer1.gif differ diff --git a/images/cheer/cheer100.gif b/images/cheer/cheer100.gif new file mode 100644 index 0000000..4b205f9 Binary files /dev/null and b/images/cheer/cheer100.gif differ diff --git a/images/cheer/cheer1000.gif b/images/cheer/cheer1000.gif new file mode 100644 index 0000000..00c8b60 Binary files /dev/null and b/images/cheer/cheer1000.gif differ diff --git a/images/cheer/cheer10000.gif b/images/cheer/cheer10000.gif new file mode 100644 index 0000000..b49a007 Binary files /dev/null and b/images/cheer/cheer10000.gif differ diff --git a/images/cheer/cheer5000.gif b/images/cheer/cheer5000.gif new file mode 100644 index 0000000..02f0f9d Binary files /dev/null and b/images/cheer/cheer5000.gif differ diff --git "a/images/dccon/\352\262\211\353\260\224\354\206\215\354\264\211.gif" "b/images/dccon/\352\262\211\353\260\224\354\206\215\354\264\211.gif" new file mode 100644 index 0000000..00da8a6 Binary files /dev/null and "b/images/dccon/\352\262\211\353\260\224\354\206\215\354\264\211.gif" differ diff --git "a/images/dccon/\354\206\214\353\213\211\352\262\260\355\230\274\353\260\230\354\247\200.png" "b/images/dccon/\354\206\214\353\213\211\352\262\260\355\230\274\353\260\230\354\247\200.png" new file mode 100644 index 0000000..3300749 Binary files /dev/null and "b/images/dccon/\354\206\214\353\213\211\352\262\260\355\230\274\353\260\230\354\247\200.png" differ diff --git "a/images/dccon/\354\206\214\353\213\211\353\247\201\354\227\206\354\260\220.gif" "b/images/dccon/\354\206\214\353\213\211\353\247\201\354\227\206\354\260\220.gif" new file mode 100644 index 0000000..d0e9f0c Binary files /dev/null and "b/images/dccon/\354\206\214\353\213\211\353\247\201\354\227\206\354\260\220.gif" differ diff --git "a/images/dccon/\354\243\274\354\235\270\353\213\230.png" "b/images/dccon/\354\243\274\354\235\270\353\213\230.png" new file mode 100644 index 0000000..944260a Binary files /dev/null and "b/images/dccon/\354\243\274\354\235\270\353\213\230.png" differ diff --git a/lib/browserified.js b/lib/browserified.js new file mode 100644 index 0000000..39db94a --- /dev/null +++ b/lib/browserified.js @@ -0,0 +1,863 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o/g,">"); + } + if (typeof applyMessage != "undefined") { + chatMessageBox.innerHTML = applyMessage(message, data); + if (data.noDisplay) { return null; } + } + else { chatMessageBox.innerHTML = message; } + + if (data) { + if (data.color && configData.personalColor) { + chatNicknameBox.style.color = data.color; + } + if (data.badges && configData.badgeVisible) { + data.badges.toString().split(",").forEach( function(element, index, array) { + var chatBadge = document.createElement("img"); + + if (element.search("broadcaster") == 0) { // 스트리머(/1) + chatBadge.src = "./images/badge/broadcaster.png"; + } + else if (element.search("moderator") == 0) { // 진은검(/1) + chatBadge.src = "./images/badge/mod.png"; + } + else if (element.search("partner") == 0) { // 인증됨(/1) + chatBadge.src = "./images/badge/verified.png"; + } + else if (element.search("premium") == 0) { // 프라임(/1) + chatBadge.src = "./images/badge/prime.png"; + } + else if (element.search("turbo") == 0) { // 터보(/1) + chatBadge.src = "./images/badge/turbo.png"; + } + else { + var value = element.replace(/[^1-9]*(\d)/, "$1"); + if (element.search("subscriber") == 0) { // 구독자(/0, /3, /6, ...) + switch (value) + { + case 0: // 별 + chatBadge.src = "./images/badge/subs.png"; + break; + default: // 개월별 구독콘 + chatBadge.src = "./images/badge/subs" + value + ".png"; + break; + } + } + else if (element.search("bits") == 0) { // 비트 뱃지(/1, /10, /100, ...) + var value = element.replace(/[^1-9]*(\d)/, "$1"); + chatBadge.src = "./images/badge/bits" + value + ".png"; + } + } + + chatBadgeBox.appendChild(chatBadge); + } ); + } else { + chatBadgeBox.classList.add("empty"); + } + if (data.clip) { + chatMessageBox.innerHTML = data.clip + chatMessageBox.innerHTML; + } + if (data.submonths) { + chatMessageBox.innerHTML = + '
' + + configData.subMonthsMsg.replace("{months}", data.submonths) + + "
" + chatMessageBox.innerHTML; + } + if (data.cheers) { + chatMessageBox.innerHTML = + '
' + + configData.cheersMsg.replace("{bits}", data.cheers) + + "
" + chatMessageBox.innerHTML; + } + } + if ( chatMessageBox.innerHTML.replace(/(<[^>]*>)|\s/g,"").length == 0) { + chatMessageBox.classList.add("image_only"); + } + + + // 페이지에 Element 연결 + chatInnerBox.appendChild(chatNicknameBox); + chatInnerBox.appendChild(chatBadgeBox); + chatOuterBox.appendChild(chatInnerBox); + chatOuterBox.inner = chatInnerBox; + chatOuterBox.appendChild(chatMessageBox); + chatOuterBox.msg = chatMessageBox; + document.getElementById("chat_wrapper").appendChild(chatOuterBox); + + + // 메세지 타임아웃 설정 + var fadeoutTime = 0; + if (configData.msgExistDuration) { fadeoutTime += configData.msgExistDuration * 1000; } + if (configData.msgAniDuration) { fadeoutTime += configData.msgAniDuration * 1000; } + + if (fadeoutTime != 0) { + setTimeout( function() { + chatOuterBox.remove(); + --numChat; + }, fadeoutTime ); + } + + // 넘치는 메세지를 삭제 + if((++numChat > configData.numChatMax)) { + var first = document.getElementsByClassName("chat_outer_box")[0]; + document.getElementById("chat_wrapper").removeChild(first); + --numChat; + } + + return chatOuterBox; +} + +var concatChatMessage = function(nick, message, data) { + var lChild = document.getElementById("chat_wrapper").lastChild; + if (lChild && lChild.getElementsByClassName("chat_nickname_box")[0].innerHTML == nick) { + if (typeof applyMessage != "undefined") { message = applyMessage(message, data); } + with (lChild.getElementsByClassName("chat_msg_box")[0]) { + innerHTML += "\n\n" + message; + style.maxHeight = "none"; + } + } + else { addChatMessage.apply(this, arguments); } +} + +var banChatMessage = function(nick) { + var children = document.getElementsByClassName("user_"+nick); + if (children && children.length > 0) { + console.log(children); + for (var index in children) { + if(Number(index) == NaN) { continue; } + children[index].getElementsByClassName("chat_msg_box")[0].innerHTML = + "<message deleted>"; + } + } +} + +var applyReplace = function(message, data){ return message; }; +var applyCheerIcon = function(message, data){ return message; }; +var applyTwitchCon = function(message, data){ return message; }; +var applyDcCon = function(message, data){ return message; }; +var applyMessage = function(message, data) { + message = applyReplace(message, data); + message = applyCheerIcon(message, data); + message = applyTwitchCon(message, data); + message = applyDcCon(message, data); + + var twipApply = require("./twip_apply.js"); + if (twipApply) { message = twipApply.apply(message, data); } + return message; +} + + + +/* 설정 파일 확인 및 디버그 내용 출력 함수 정의 */ +var completeCount = 0; +var checkComplete = function() { + /* CSS, 디씨콘, 이모티콘+구독콘, 후원아이콘, 설정, 그리고 서버 */ + var num = 5 + configData.channel.match(/#/g).length; + if (++completeCount == num) { + var chat = addChatMessage("", + '
' + + "┌────────────┐\n" + + "│ B r i d g e │\n" + + "│ ■■□□ □□■■ │\n" + + "│ ■ □ □ □ ■ │\n" + + "│ ■■□□ □ ■ │\n" + + "│ ■ □ □ □ ■ │\n" + + "│ ■■□□□ □□■■。│\n" + + "└────────────┘
", + { badges:[], escape:false } + ) + if (chat != null) { + chat.inner.style.display = "none"; + chat.msg.style.maxHeight = "none"; + } + checkComplete = function(){}; + } +} +if (typeof configData == "undefined") { configData = {}; } +debugLog = function(dat) {}; +{ + var configLoadMessage = ""; + + if (Object.keys(configData).length < Object.keys(configDefault).length) { + if (Object.keys(configData).length == 0) { + configLoadMessage = "설정 파일(lib/config.js)을 로드하는 데 문제가 생겨 기본 설정을 사용합니다. "; + } + else { configLoadMessage = "일부 설정을 찾을 수 없어 기본값을 사용합니다. "; } + + Object.keys(configDefault).forEach( function(element) { + if (configData[element] == undefined) configData[element] = configDefault[element]; + } ); + } + + if (configData.debugLevel != 0) { + if (configData.debugLevel == 1) { debugLog = function(dat) { console.log(dat); }; } + else { + debugLog = function(dat, unConcat) { + if (unConcat) { + addChatMessage("DEBUG", dat, + { badges:["moderator/1"], color:"red", escape:false }); + } + else { + concatChatMessage("DEBUG", dat, + { badges:["moderator/1"], color:"red", escape:false }); + } + }; + } + } + + debugLog(configLoadMessage + "설정을 불러왔습니다."); + checkComplete(); +} + + + +/* 특정 메세지 대체 */ +if ( (configData.replaceMsgs) && (configData.replaceMsgs.length>0) ) { + applyReplace = function(message, data) { + for(var index in configData.replaceMsgs) { + var msg = configData.replaceMsgs[index]; + if ( (!msg.nick) || (msg.nick == data.nick) ) { + if ( (msg.to=="{no_display}") && (message.match(msg.orig)!=null) ) { + data.noDisplay = true; + return message; + } + + message = message.replace(msg.orig, msg.to); + } + } + return message; + }; +} + + + +/* CSS 로드 */ +{ + var loadCss = function() { + + document.head.appendChild( function() { + var ret = document.createElement("link"); + ret.rel = "stylesheet"; + ret.href = "theme/" + configData.theme + "/theme.css"; + ret.onload = function() { + debugLog(configData.themeName + " 테마를 적용했습니다."); + checkComplete(); + }; + return ret; + }() ); + }(); +} + + + +/* 비트 후원 메시지 아이콘으로 변경*/ +var cheerList = [ + "cheer", "bday", "Kappa", "TriHard", "Kreygasm", + "4Head", "SwiftRage", "NotLikeThis", "FailFish", "VoHiYo", + "PJSalt", "MrDestructoid" ]; +var cheerRegExp = new RegExp( function() { + var ret = ""; + for(var index in cheerList) { ret += "(" + cheerList[index] + ")|"; } + return ret.slice(0,-1); +}(), "g"); + +if (configData.loadCheerImgs) { + applyCheerIcon = function(message, data) { + if( (!data.cheers) && (data.cheers=="") ) { return message; } + + var regExp = new RegExp("(" + cheerRegExp.source + ")(\\d+) ", "ig"); + var matches = message.match(regExp); + var newMessage = ""; + + if(matches == null) { return message; } + for(var index in matches) { + message = message.split(matches[index]); + newMessage += message.shift(); + message = message.join(matches[index]); + + var prefix = matches[index].replace(/\d+ $/, ""); + var amount = matches[index].split(prefix)[1].replace(" ",""); + prefix = prefix.toLowerCase(); + newMessage += + '
' + amount + "
"; + } + + return newMessage + message; + }; + + debugLog("후원 아이콘을 불러왔습니다."); + checkComplete(); +} +else { + debugLog("설정에 의해 후원 아이콘을 불러오지 않았습니다."); + checkComplete(); +} + + + +/* 디씨콘 및 구독콘 로드 및 적용 */ +if (configData.loadTwitchCons) { + var twitchConsUrlTemplate = "https://static-cdn.jtvnw.net/emoticons/v1/"; + + if (configData.consRealSubsOnly) { + applyTwitchCon = function(message, data) { + if ( !(data && data.emotes) || (data.emotes.length==0) ) { return message; } + + // 받은 emotes 데이터를 가공 + var rawEmotes = data.emotes.replace(/-/g, "~").split("/"); + var emotesMap = {}; + for(var index in rawEmotes) { + var element = rawEmotes[index]; + emotesMap[element.split(":")[0]] = element.split(":")[1].split(","); + } + + var rangeIds = []; + for(var id in emotesMap) { + for(var range in emotesMap[id]) { + rangeIds.push([emotesMap[id][range], id]); + } + } + rangeIds.sort( function(a,b) { + return (Number(a[0].split("~")[0]) > Number(b[0].split("~")[0])? 1: 0); + } ); + rangeIds.unshift(["-1~-1", ""]); + + var rangeIdsLength = rangeIds.length - 1; + for(var index=0; index'; + } + else { + newMessage = newMessage + + message.slice( + Number(rangeIds[index][0].split("~")[0]), + Number(rangeIds[index][0].split("~")[1]) + ); + } + } + + return newMessage; + } + + debugLog("트위치 이모티콘과 구독콘을 적용했습니다."); + checkComplete(); + } + else { + var isTwitchConsCalled = { emotes:false, subs:false }; + var isTwitchConsLoaded = { emotes:false, subs:false }; + var consIdMap = {}; + + var twitchEmoteConRequest = new window.XMLHttpRequest(); + twitchEmoteConRequest.open( + "GET", "https://twitchemotes.com/api_cache/v3/global.json", true); + var twitchSubsConRequest = new window.XMLHttpRequest(); + twitchSubsConRequest.open( + "GET", "https://twitchemotes.com/api_cache/v3/subscriber.json", true); + + var twitchConsApply = function() { + applyTwitchCon = function(message, data) { + var messageArray = message.split(/(^| )([^ ]*)/g); + while(messageArray.indexOf("") != -1) { + messageArray.splice(messageArray.indexOf(""),1); + } + for(var index in messageArray) { + if (consIdMap.hasOwnProperty(messageArray[index])) { + for (var keyword in consIdMap) { + if (messageArray[index] == keyword) { + messageArray[index] = + ''; + } + } + } + } + + return messageArray.join(""); + }; + + var loadMsg = "을 적용했습니다."; + if (isTwitchConsLoaded.subs) { + if (isTwitchConsLoaded.emotes) { loadMsg = "트위치 이모티콘과 구독콘" + loadMsg; } + else { loadMsg = "트위치 구독콘" + loadMsg; } + } + else { + if (isTwitchConsLoaded.emotes) { loadMsg = "트위치 이모티콘" + loadMsg; } + else { + debugLog("트위치 이모티콘과 구독콘을 적용할 수 없었습니다."); + return; + } + } + debugLog("불러온 " + loadMsg); + if(isTwitchConsLoaded.subs && isTwitchConsLoaded.emotes) { checkComplete(); } + }; + + twitchEmoteConRequest.onreadystatechange = function(evt) { + if (twitchEmoteConRequest.readyState == 4) { + if (twitchEmoteConRequest.status == 200) { + var data = JSON.parse(twitchEmoteConRequest.responseText); + + for (var keyword in data) { + consIdMap[keyword] = data[keyword].id; + } + + isTwitchConsLoaded.emotes = true; + debugLog("트위치 이모티콘을 불러왔습니다."); + } + else { + debugLog( + "트위치 이모티콘을 불러오는 데 실패했습니다." + + "\n에러 코드 " + twitchEmoteConRequest.status); + } + + isTwitchConsCalled.emotes = true; + if(isTwitchConsCalled.emotes && isTwitchConsCalled.subs) { twitchConsApply(); } + } + }; + twitchSubsConRequest.onreadystatechange = function(evt) { + if (twitchSubsConRequest.readyState == 4) { + if (twitchSubsConRequest.status == 200) { + var data = JSON.parse(twitchSubsConRequest.responseText); + + for (var conChannel in data) { + var targetChannel = data[conChannel]; + for (var index in targetChannel.emotes) { + var con = targetChannel.emotes[index]; + consIdMap[con.code] = con.id; + } + } + + isTwitchConsLoaded.subs = true; + debugLog("트위치 구독콘을 불러왔습니다."); + } + else { + debugLog( + "트위치 구독콘을 불러오는 데 실패했습니다." + + "\n에러 코드 " + twitchSubsConRequest.status); + } + + isTwitchConsCalled.subs = true; + if(isTwitchConsCalled.emotes && isTwitchConsCalled.subs) { twitchConsApply(); } + } + }; + + twitchEmoteConRequest.send(null); + twitchSubsConRequest.send(null); + } +} +else { + debugLog("설정에 따라 트위치 이모티콘과 구독콘을 불러오지 않았습니다."); + checkComplete(); +} + +dcConsData = []; +if (configData.loadDcCons) { + var dcConsSubURI = "images/"; + var dcConsMainURI = ""; + if (configData.dcConsURI == "") { + configData.dcConsURI = "./"; + dcConsSubURI = "images/dccon/"; + dcConsMainURI = "lib/"; + } + else if (configData.dcConsURI[configData.dcConsURI.length-1] != "/") { + configData.dcConsURI += "/"; + } + + var dcConScript = document.createElement("script"); + dcConScript.type = "text/javascript"; + dcConScript.charset = "utf-8"; + dcConScript.src = configData.dcConsURI + dcConsMainURI + "dccon_list.js"; + document.body.appendChild(dcConScript); + + dcConScript.onload = function() { + if (dcConsData.length == 0) { debugLog("디씨콘을 불러오는 데 실패했습니다."); } + else { + var keywords = []; + for (var index in dcConsData) { + for (var index2 in dcConsData[index].keywords) { + keywords.push(dcConsData[index].keywords[index2]); + } + } + keywords.sort(function(a,b) { return a.length < b.length; } ); + + applyDcCon = function(message, data) { + for (var index in keywords) { + var keyword = keywords[index]; + if (message.indexOf("~" + keyword) != -1) { + message = message.split("~" + keyword).join( + ''); + } + } + + return message; + }; + debugLog("디씨콘을 적용했습니다."); + checkComplete(); + } + }; +} +else { + debugLog("설정에 따라 디씨콘을 적용하지 않았습니다."); + checkComplete(); +} + + + +/* IRC 클라이언트 설정 */ +var joinCount = 0; +defaultColors = [ + "#FF0000", "#0000FF", "#00FF00", "#B22222", "#FF7F50", + "#9ACD32", "#FF4500", "#2E8B57", "#DAA520", "#D2691E", + "#5F9EA0", "#1E90FF", "#FF69B4", "#8A2BE2", "#00FF7F"]; +debugLog("트위치에 접속을 시도합니다."); +client = (function() { + var ws = new WebSocket(configData.webSocket); + ws.onopen = function() { + ws.send("PASS " + configData.pass + "\r\n"); + ws.send("NICK " + configData.nick + "\r\n"); + ws.send('CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership' + "\r\n"); + ws.send("JOIN " + configData.channel + "\r\n"); + } + ws.onmessage = function(evt) { + var lines = evt.data.toString().split(/\r\n|\r|\n/); + lines.pop(); + lines.forEach( function(element) { + var line = JSON.stringify(element).slice(1,-1); + var args = line.replace(/^(:|@)/m, "").split(" "); + + if (line[0] == ":") { + // IRC 명령어 처리 + switch (args[1]) { + case "421": // 잘못된 명령어를 보냈을 때 + case "001": // 웰컴 메세지 + case "002": // 호스트 알림 + case "003": // 서버 상태태 + case "004": // 접속초기메세지 끝 + case "375": // MOTD(공지사항) 시작 + case "372": // MOTD + case "376": // MOTD 끝 + case "CAP": // 트위치 명령어 수신 확인 메세지 + case "353": // 접속자 목록(로드가 안된상태라 justinfan뿐이지만) + case "366": // 이름 목록 끝 + case "MODE": // 관리자 권한 감지 + case "PART": // 서버에서 유저 접속 해제 + case "HOSTTARGET": + // 호스팅에 변화가 생겼을 때 + break; + + case "JOIN": // 서버에 유저가 접속 + if(args[0].search("justinfan") != -1) { + debugLog( + args[2].substring(1) + "에 접속했습니다."); + } + if(++joinCount <= configData.channel.match(/#/g).length) { + checkComplete(); + } + break; + + default: // 미처리 메세지 + if (configData.allMessageHandle) { + debugLog("처리되지 않은 메세지를 수신했습니다.
" + line); + } + break; + } + } + else if (line[0] == "@") { + // 트위치 명령어 처리 + var data = {}; + var twitchArgs = {}; + args.shift().split(";").forEach( function(element) { + var keyval = element.split("="); + twitchArgs[keyval[0]] = keyval[1]; + } ); + + switch(args[1]) { + case "ROOMSTATE": // 방 정보 + break; + + + + case "USERNOTICE": + if (twitchArgs.msg-param-months && twitchArgs.msg-param-months!="") { + // 구독 메세지 수신 + data.subMonths = Number(twitchArgs.msg-param-months); + } + else { + if (configData.allMessageHandle) { + debugLog("처리되지 않은 메세지를 수신했습니다.
" + line); + } + break; + } + + case "PRIVMSG": // 채팅 수신 + // 이름 지정 + var nick = args[0].split(/[!@]/g)[1]; + var displayNick = twitchArgs["display-name"]; + var realNick = ""; + if (configData.useDisplayName) { + if (displayNick.replace(/\s/g, "")!="") { realNick = displayNick; } + } + else { realNick = nick; } + + var message = args.slice(3).join(" ").substring(1); + data.badges = twitchArgs.badges; + data.color = twitchArgs.color; + data.emotes = twitchArgs.emotes; + data.nick = nick; + + // muteUser 적용 + if (configData.muteUser) { + var match = configData.muteUser.find( function(element) { + return (element == displayNick) || (element == nick); + } ); + + if (match != undefined) break; + } + + // 클립 링크 파싱 + var clip = message.match(/(https?:\/\/)?clips\.[a-zA-Z./]*/g); + if (clip!=null) { + message = message.replace(clip[0], ""); + + if (configData.loadClipPreview) { + var clipRequest = new window.XMLHttpRequest(); + clipRequest.open("GET", clip[0], true); + clipRequest.responseType = "document"; + + var handler = function(evt) { + if(clipRequest.readyState == 4) { + if(clipRequest.status == 200) { + var src, title, uploader; + var metas = clipRequest.responseXML.getElementsByTagName("meta"); + for(var i=0; i
' + title + + '
' + uploader + '
'; + } + else { + data["clip"] = '
Invalid Clip
'; + clipRequest.removeEventListener("readystatechange", handler); + } + addChatMessage(realNick, message, data); + } + }; + + clipRequest.addEventListener('readystatechange', handler); + clipRequest.send(null); + break; + } + else { + data["clip"] = + '
' + + configData.clipReplaceMsg + '
'; + } + } + + // 유저 이름색 지정 + if (!data.color || data.color=="") { + var n = nick.charCodeAt(0) + nick.charCodeAt(1)*new Date().getDate(); + data.color = defaultColors[n % defaultColors.length]; + } + + // 비트 메세지 파싱 + if (message.match(cheerRegExp) != null) { + var cheers = message.match( + new RegExp("(" + cheerRegExp.source + ")\\d+ ", "ig")); + var cheer = 0; + for(var index in cheers) { + cheer += Number(cheers[index].replace(cheerRegExp, "")); + } + data.cheers = cheer; + } + + // 메세지 출력 + addChatMessage(realNick, message, data); + break; + + case "NOTICE": // 공지 메세지 + switch(twitchArgs["msg-id"]) { + case "host_off": // 호스팅을 끊었을 때 + case "host_target_went_offline": + // 호스팅이 끊겼을 때 + debugLog("호스팅이 종료되었습니다."); + break; + + case "host_on": // 호스팅되었을 때 + debugLog( + args[5].slice(0,-1) + " 호스팅 중.\n" + + "호스팅중인 채팅의 전송은 지원하지 않습니다."); + break; + + } + break; + + case "CLEARCHAT": // 매니저가 /clear 했을 때 + if(args.length == 4) { + banChatMessage(args[3].substring(1)); + } + else { + document.getElementById("chat_wrapper").innerHTML = ""; + numChat = 0; + } + break; + + default: // 미처리 메세지 + if (configData.allMessageHandle) { + debugLog("처리되지 않은 메세지를 수신했습니다.
" + line); + } + break; + } + } + else { + // 서버 연결상태 확인용 ping-pong + if (args[0] == "PING") { ws.send("PONG :tmi.witch.tv\r\n"); } + else if (configData.allMessageHandle) { + debugLog("처리되지 않은 메세지를 수신했습니다.
" + line); + } + } + } ); + } + ws.onclose = function() { + debugLog( + "채팅 서버와의 연결이 종료되었습니다.
" + + configData.retryInterval + "초 후 재접속을 시도합니다."); + setTimeout( + function() { client(); }, + configData.retryInterval * 1000 ); + } +}) (); +},{"./twip_apply.js":2}],2:[function(require,module,exports){ +var twipMsg = + configData&&configData.twipCheersMsg? + configData.twipCheersMsg: + configData.cheersMsg.replace("{bits}", "{wons}").replace(/\s*비트/, "원"); + +exports.apply = function(message, data) { + if (data.nick == "twipkr") { + if ( (message.indexOf("ACTION")!=-1) && (message.indexOf("후원함!")!=-1) ) { + message = message.replace(/\\u0001/g, ""). + replace("ACTION", "").replace("을 후원함!", "원을 후원함!"); + + var amount = message.match(/ (\d+,)*\d+원을 후원함!/g)[0]. + split(" ")[1].replace(/[^\d]/g, ""); + + message = '
' + + twipMsg.replace("{wons}", amount) + + "
" + message; + } + } + + return message; +} +},{}]},{},[1]); diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..a0ce1d5 --- /dev/null +++ b/lib/config.js @@ -0,0 +1,31 @@ +configData = { + numChatMax : 20, + personalColor : /*false*/true, + badgeVisible : true, + theme : "default", + themeName : "", + msgExistDuration : 0, + msgAniDuration : 0, + debugLevel : 2, + useDisplayName : true, + loadCheerImgs : true, + loadTwitchCons : true, + consRealSubsOnly : true, + loadDcCons : true, + dcConsURI : "", + subMonthsMsg : "☆ {months} 개월 구독! ☆", + cheersMsg : "☆ {bits} 비트 후원 ! ☆", + loadClipPreview : true, + clipReplaceMsg : "[ 클립 ]", + webSocket : "wss://irc-ws.chat.twitch.tv:443", + nick : "justinfan16831", + pass : "foobar", + channel : "#mr_watert", + retryInterval : 2, + allMessageHandle : /*false*/true, + muteUser : ["Nightbot"], + deleteBanMsg : true, + replaceMsgs : [ + {orig:/^![a-zA-Z]+/, to:"{no_display}"} // 봇 호출 영문 메세지 미표시 + ] +}; \ No newline at end of file diff --git a/lib/dccon_list.js b/lib/dccon_list.js new file mode 100644 index 0000000..7bbc6b4 --- /dev/null +++ b/lib/dccon_list.js @@ -0,0 +1,6 @@ +dcConsData = [ + {name:"소닉링없찐.gif", keywords:["링없찐"], tags:["소닉", "소닉매니아"]}, + {name:"소닉결혼반지.png", keywords:["결혼반지"], tags:["소닉", "소닉매니아"]}, + {name:"주인님.png", keywords:["주인님"], tags:["자살"]}, + {name:"겉바속촉.gif", keywords:["겉바속촉", "가로쉬"], tags:["와우", "헬스크림"]} +]; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..cc7ef88 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "BridgeBBCC", + "version": "0.2.0", + "description": "Bridge Between BroadCasting and twitch Chat", + "keywords": [ "twitch", "chat", "IRC" ], + "bugs": { + "url": "https://github.com/krynen/BridgeBBCC" + }, + "license": "MIT", + "author": "Mr_WaterT", + "dependencies": { + "irc": "~14.4.0" + }, + "engines": { + "node": "~8.3.0" + }, + "scripts": { + "start": "browserify source/main.js -o lib/browserified.js" + } +} diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..035c2eb --- /dev/null +++ b/readme.txt @@ -0,0 +1,141 @@ +************************************************************ +* ┌────────────┐ * +* │ B r i d g e │ * +* │ ■■□□ □□■■ │ * +* │ ■ □ □ □ ■ │ * +* │ ■■□□ □ ■ │ * +* │ ■ □ □ □ ■ │ * +* │ ■■□□□ □□■■。│ * +* └────────────┘ * +* written by. Mr_WaterT * +* * +* https://github.com/krynen/BridgeBBCC * +* * +************************* 사 용 법 ************************* +* * +* 1. 아래를 참고해 lib/config.js의 내용을 조정 * +* * +* 2. 송출 소프트에어의 Webpage Source에 client.html을 추가 * +* * +* 3. 로고 표시로 로딩완료를 확인한 후 방송하기 * +* * +* * +********************** config.js 설정 ********************** +* * +* ★표기된 설정 : 건드려볼만함 * +* ☆표기된 설정 : 건드리기 좀 어려울 수 있음 * +* 표기되지 않은 설정 : 건드릴 이유 없음 * +* * +* * +* numChatMax 한번에 표시될 최대 메세지 수 * +* * +* ★personalColor 이름 색을 트위치와 같게 * +* true, false * +* * +* ★badgeVisible 왕관 등 뱃지를 표시할지 * +* true, false * +* * +* ★theme 사용할 테마 * +* theme 폴더 안의 폴더명 입력 * +* "테마 이름" * +* * +* debugLevel 초기 로딩 메세지를 포함한 * +* 디버그메세지의 표시 여부 * +* 0(안함) * +* 1(개발자도구에 로그만 남김) * +* 2(메세지로 출력) * +* * +* ★loadCheerImgs 비트 도네이션 텍스트를 * +* gif아이콘으로 표시할지 * +* true, false * +* * +* ★loadTwitchCons 트위치 이모티콘과 구독콘을 * +* 이미지로 표시할지 * +* true, false * +* * +* ★consRealSubsOnly실제 구독자만 구독콘을 쓸 수 있게할지 * +* (초기 로딩이 빨라짐) * +* true, false * +* * +* ★loadDcCons 디씨콘을 이미지로 표시할지 * +* true, false * +* * +* ☆dcConsURI 디씨콘을 불러올 주소 * +* "a.b/"일 경우 a.b/에 dccon_list.js를, * +* a.b/images/에 이미지를. * +* 자신의 pc에서 불러올 경우 "" * +* * +* ★subMonthsMsg 구독 기념 메세지의 강조 문구 * +* {months}는 개월수로 대체된다 * +* * +* ★cheersMsg 비트 도네이션 메세지의 강조 문구 * +* {bits}는 후원 금액 수치로 대체된다 * +* * +* ★loadClipPreview 클립 미리보기를 표시할 지 * +* true(이미지, 제목 표시) * +* false(↓의 메세지만 표시) * +* * +* ★clipReplaceMsg 클립 미리보기의 대체 텍스트 * +* ↑가 true이면 그냥 두면 됨 * +* * +* ★★channel 접속할 채널 * +* "#아이디" * +* * +* retryInterval 접속이 끊겼을 때 재접속 시도 시간 간격 * +* 초 * +* * +* ★muteUser 특정 유저의 메세지를 표시하지 않기 * +* ["아이디", "한글닉도OK", "..."] * +* * +* ★deleteBanMsg 밴된 유저의 메세지를 * +* 로 처리하기 * +* true, false * +* * +* ☆replaceMsgs 특정 텍스트를 변환하기 * +* [{orig:"이전1", to:"이후1"}, * +* {orig:"이전2", to:"이후2"}] * +* 정규표현식을 알아야 사용할만 함 * +* * +* nick:"영어아이디"를 포함하면 * +* 특정유저의 메세지만 변환할 수 있음 * +* * +************************************************************ + + +*********************** Version Info *********************** +* * +* ********************************************** * +* * 2.0.0 : 5th Sep 2017 * * +* ********************************************** * +* 추가된 기능 * +* - 채팅 내 구독콘 및 디씨콘 이용 가능 * +* - 밴 유저 메세지 삭제 * +* - 클립 링크 미리보기 * +* - 구독 및 후원 메세지 강조 * +* - 메세지 텍스트 변환 * +* * +* 오류 수정 및 변경된 기능 * +* - 일부 유저의 이름이 표시되지 않는 문제 수정 * +* - 이름 색을 바꿔본 적 없는 유저의 이름색은 랜덤으로 설정 * +* * +* * +* ********************************************** * +* * 1.0.0 : 29th Aug 2017 (Pre released) * * +* ********************************************** * +* 제공된 기능 * +* - 핵심 기능: 트위치 채팅에 접속해 * +* 메세지를 html로 띄우기 * +* - 유저의 로컬네임(한글닉)을 이름으로 표시 * +* - 유저 이름 색 반영 가능 * +* 유저 뱃지 표시 설정 가능 * +* CSS 테마 사용 가능 * +* * +* * +********************** 추가 예정 기능 ********************** +* * +* - 테마 파일과 설정파일의 연동 * +* - 설정 조정을 위한 GUI * +* - 사용중인 디씨콘 목록을 확인할 수 있는 별도 페이지 * +* * +* * +************************************************************ diff --git a/source/main.js b/source/main.js new file mode 100644 index 0000000..3d31f4d --- /dev/null +++ b/source/main.js @@ -0,0 +1,838 @@ +/* 기본 설정 */ +configDefault = { + numChatMax : 20, // html에 한꺼번에 표시될 수 있는 메세지의 최대 갯수 + personalColor : false, /* 이름 색깔을 트위치 이름색과 일치시킬지 + theme에서 제한 가능 */ + badgeVisible : false, /* 구독, 비트 등 뱃지를 표시할지 + theme에서 제한 가능 */ + theme : "default", // 사용할 테마. theme\테마\*의 파일을 사용 + themeName : "", /* 테마의 이름 + theme로부터 import */ + msgExistDuration : 0, // 메세지가 애니메이션을 빼면 얼마나 오래 표시될 지 + msgAniDuration : 0, /* 메세지 표시 애니메이션의 소요시간 + theme로부터 import */ + debugLevel : 2, // 0:미표시, 1:console.log, 2:addChatMessage + useDisplayName : true, // 한글 닉네임으로 이름을 표시할지 + loadCheerImgs : true, // 비트 후원채팅을 이미지로 표시할지 + loadTwitchCons : true, // 트위치 이모티콘과 구독콘을 불러올지 + consRealSubsOnly : true, /* 실제 구독자만 구독콘을 쓸 수 있게 할지 + 로딩이 빨라짐 */ + loadDcCons : true, // 디씨콘을 불러올지 + dcConsURI : "", /* 불러올 디씨콘 Uri. + 로컬 디씨콘을 이용할 경우 공백으로 둔다. */ + subMonthsMsg : "☆ {months} 개월 구독! ☆", + // 구독 메세지를 받았을 때 추가로 출력할 텍스트 + cheersMsg : "☆ {bits} 비트 후원 ! ☆", + // 비트 후원을 받았을 때 추가로 출력할 텍스트 + loadClipPreview : true, // 클립 미리보기를 이미지로 표시할지 + clipReplaceMsg : "[ 클립 ]", // 클립 미리보기를 사용하지 않을 때의 대체 텍스트 + webSocket : "wss://irc-ws.chat.twitch.tv:443", + /* 접속할 웹소켓 + const value */ + nick : "justinfan00000", // 트위치 IRC에서 이용할 gust nickname + pass : "foobar", // 트위치 IRC에서 이용할 guest password + channel : "#mr_watert", /* 접속할 채널 + "#id1,#id2,.."으로 여러 채널에 접속 가능 */ + retryInterval : 3, // 접속에 끊겼을 때 재접속 시도 간격(초) + allMessageHandle : false, // IRC로부터 받은 처리되지 않은 메세지를 html에 표시 + muteUser : ["Nightbot"], /* html에 표시하지 않을 유저 nickname + display-name과 트위치 id를 모두 사용 가능 */ + deleteBanMsg : true, // ban된 유저의 메세지를 지우기 + replaceMsgs : [] // 봇 메세지 등을 대체 +}; + + + +/* 메세지 출력 함수 정의 */ +var numChat = 0; +/* addChatMessage(nick, message[, data]) */ +addChatMessage = function(nick, message, data) { + + // DOM Element 생성 + var chatNicknameBox = document.createElement("div"); + chatNicknameBox.classList.add("chat_nickname_box"); + var chatBadgeBox = document.createElement("div"); + chatBadgeBox.classList.add("chat_badge_box"); + var chatInnerBox = document.createElement("div"); + chatInnerBox.classList.add("chat_inner_box"); + var chatMessageBox = document.createElement("div"); + chatMessageBox.classList.add("chat_msg_box"); + var chatOuterBox = document.createElement("div"); + chatOuterBox.classList.add("chat_outer_box"); + if (data.clip) { chatOuterBox.classList.add("clip_included"); } + if (data.nick) { chatOuterBox.classList.add("user_"+data.nick); } + + + // Element에 내용 추가 + chatNicknameBox.innerHTML = nick; + message = message.replace(/\\\"/g, '"').replace(/\\\\/g, "\\"); + if ((data.escape == undefined) || (data.escape == true)) { + message = message.replace(//g,">"); + } + if (typeof applyMessage != "undefined") { + chatMessageBox.innerHTML = applyMessage(message, data); + if (data.noDisplay) { return null; } + } + else { chatMessageBox.innerHTML = message; } + + if (data) { + if (data.color && configData.personalColor) { + chatNicknameBox.style.color = data.color; + } + if (data.badges && configData.badgeVisible) { + data.badges.toString().split(",").forEach( function(element, index, array) { + var chatBadge = document.createElement("img"); + + if (element.search("broadcaster") == 0) { // 스트리머(/1) + chatBadge.src = "./images/badge/broadcaster.png"; + } + else if (element.search("moderator") == 0) { // 진은검(/1) + chatBadge.src = "./images/badge/mod.png"; + } + else if (element.search("partner") == 0) { // 인증됨(/1) + chatBadge.src = "./images/badge/verified.png"; + } + else if (element.search("premium") == 0) { // 프라임(/1) + chatBadge.src = "./images/badge/prime.png"; + } + else if (element.search("turbo") == 0) { // 터보(/1) + chatBadge.src = "./images/badge/turbo.png"; + } + else { + var value = element.replace(/[^1-9]*(\d)/, "$1"); + if (element.search("subscriber") == 0) { // 구독자(/0, /3, /6, ...) + switch (value) + { + case 0: // 별 + chatBadge.src = "./images/badge/subs.png"; + break; + default: // 개월별 구독콘 + chatBadge.src = "./images/badge/subs" + value + ".png"; + break; + } + } + else if (element.search("bits") == 0) { // 비트 뱃지(/1, /10, /100, ...) + var value = element.replace(/[^1-9]*(\d)/, "$1"); + chatBadge.src = "./images/badge/bits" + value + ".png"; + } + } + + chatBadgeBox.appendChild(chatBadge); + } ); + } else { + chatBadgeBox.classList.add("empty"); + } + if (data.clip) { + chatMessageBox.innerHTML = data.clip + chatMessageBox.innerHTML; + } + if (data.submonths) { + chatMessageBox.innerHTML = + '
' + + configData.subMonthsMsg.replace("{months}", data.submonths) + + "
" + chatMessageBox.innerHTML; + } + if (data.cheers) { + chatMessageBox.innerHTML = + '
' + + configData.cheersMsg.replace("{bits}", data.cheers) + + "
" + chatMessageBox.innerHTML; + } + } + if ( chatMessageBox.innerHTML.replace(/(<[^>]*>)|\s/g,"").length == 0) { + chatMessageBox.classList.add("image_only"); + } + + + // 페이지에 Element 연결 + chatInnerBox.appendChild(chatNicknameBox); + chatInnerBox.appendChild(chatBadgeBox); + chatOuterBox.appendChild(chatInnerBox); + chatOuterBox.inner = chatInnerBox; + chatOuterBox.appendChild(chatMessageBox); + chatOuterBox.msg = chatMessageBox; + document.getElementById("chat_wrapper").appendChild(chatOuterBox); + + + // 메세지 타임아웃 설정 + var fadeoutTime = 0; + if (configData.msgExistDuration) { fadeoutTime += configData.msgExistDuration * 1000; } + if (configData.msgAniDuration) { fadeoutTime += configData.msgAniDuration * 1000; } + + if (fadeoutTime != 0) { + setTimeout( function() { + chatOuterBox.remove(); + --numChat; + }, fadeoutTime ); + } + + // 넘치는 메세지를 삭제 + if((++numChat > configData.numChatMax)) { + var first = document.getElementsByClassName("chat_outer_box")[0]; + document.getElementById("chat_wrapper").removeChild(first); + --numChat; + } + + return chatOuterBox; +} + +var concatChatMessage = function(nick, message, data) { + var lChild = document.getElementById("chat_wrapper").lastChild; + if (lChild && lChild.getElementsByClassName("chat_nickname_box")[0].innerHTML == nick) { + if (typeof applyMessage != "undefined") { message = applyMessage(message, data); } + with (lChild.getElementsByClassName("chat_msg_box")[0]) { + innerHTML += "\n\n" + message; + style.maxHeight = "none"; + } + } + else { addChatMessage.apply(this, arguments); } +} + +var banChatMessage = function(nick) { + var children = document.getElementsByClassName("user_"+nick); + if (children && children.length > 0) { + console.log(children); + for (var index in children) { + if(Number(index) == NaN) { continue; } + children[index].getElementsByClassName("chat_msg_box")[0].innerHTML = + "<message deleted>"; + } + } +} + +var applyReplace = function(message, data){ return message; }; +var applyCheerIcon = function(message, data){ return message; }; +var applyTwitchCon = function(message, data){ return message; }; +var applyDcCon = function(message, data){ return message; }; +var applyMessage = function(message, data) { + message = applyReplace(message, data); + message = applyCheerIcon(message, data); + message = applyTwitchCon(message, data); + message = applyDcCon(message, data); + + var twipApply = require("./twip_apply.js"); + if (twipApply) { message = twipApply.apply(message, data); } + return message; +} + + + +/* 설정 파일 확인 및 디버그 내용 출력 함수 정의 */ +var completeCount = 0; +var checkComplete = function() { + /* CSS, 디씨콘, 이모티콘+구독콘, 후원아이콘, 설정, 그리고 서버 */ + var num = 5 + configData.channel.match(/#/g).length; + if (++completeCount == num) { + var chat = addChatMessage("", + '
' + + "┌────────────┐\n" + + "│ B r i d g e │\n" + + "│ ■■□□ □□■■ │\n" + + "│ ■ □ □ □ ■ │\n" + + "│ ■■□□ □ ■ │\n" + + "│ ■ □ □ □ ■ │\n" + + "│ ■■□□□ □□■■。│\n" + + "└────────────┘
", + { badges:[], escape:false } + ) + if (chat != null) { + chat.inner.style.display = "none"; + chat.msg.style.maxHeight = "none"; + } + checkComplete = function(){}; + } +} +if (typeof configData == "undefined") { configData = {}; } +debugLog = function(dat) {}; +{ + var configLoadMessage = ""; + + if (Object.keys(configData).length < Object.keys(configDefault).length) { + if (Object.keys(configData).length == 0) { + configLoadMessage = "설정 파일(lib/config.js)을 로드하는 데 문제가 생겨 기본 설정을 사용합니다. "; + } + else { configLoadMessage = "일부 설정을 찾을 수 없어 기본값을 사용합니다. "; } + + Object.keys(configDefault).forEach( function(element) { + if (configData[element] == undefined) configData[element] = configDefault[element]; + } ); + } + + if (configData.debugLevel != 0) { + if (configData.debugLevel == 1) { debugLog = function(dat) { console.log(dat); }; } + else { + debugLog = function(dat, unConcat) { + if (unConcat) { + addChatMessage("DEBUG", dat, + { badges:["moderator/1"], color:"red", escape:false }); + } + else { + concatChatMessage("DEBUG", dat, + { badges:["moderator/1"], color:"red", escape:false }); + } + }; + } + } + + debugLog(configLoadMessage + "설정을 불러왔습니다."); + checkComplete(); +} + + + +/* 특정 메세지 대체 */ +if ( (configData.replaceMsgs) && (configData.replaceMsgs.length>0) ) { + applyReplace = function(message, data) { + for(var index in configData.replaceMsgs) { + var msg = configData.replaceMsgs[index]; + if ( (!msg.nick) || (msg.nick == data.nick) ) { + if ( (msg.to=="{no_display}") && (message.match(msg.orig)!=null) ) { + data.noDisplay = true; + return message; + } + + message = message.replace(msg.orig, msg.to); + } + } + return message; + }; +} + + + +/* CSS 로드 */ +{ + var loadCss = function() { + + document.head.appendChild( function() { + var ret = document.createElement("link"); + ret.rel = "stylesheet"; + ret.href = "theme/" + configData.theme + "/theme.css"; + ret.onload = function() { + debugLog(configData.themeName + " 테마를 적용했습니다."); + checkComplete(); + }; + return ret; + }() ); + }(); +} + + + +/* 비트 후원 메시지 아이콘으로 변경*/ +var cheerList = [ + "cheer", "bday", "Kappa", "TriHard", "Kreygasm", + "4Head", "SwiftRage", "NotLikeThis", "FailFish", "VoHiYo", + "PJSalt", "MrDestructoid" ]; +var cheerRegExp = new RegExp( function() { + var ret = ""; + for(var index in cheerList) { ret += "(" + cheerList[index] + ")|"; } + return ret.slice(0,-1); +}(), "g"); + +if (configData.loadCheerImgs) { + applyCheerIcon = function(message, data) { + if( (!data.cheers) && (data.cheers=="") ) { return message; } + + var regExp = new RegExp("(" + cheerRegExp.source + ")(\\d+) ", "ig"); + var matches = message.match(regExp); + var newMessage = ""; + + if(matches == null) { return message; } + for(var index in matches) { + message = message.split(matches[index]); + newMessage += message.shift(); + message = message.join(matches[index]); + + var prefix = matches[index].replace(/\d+ $/, ""); + var amount = matches[index].split(prefix)[1].replace(" ",""); + prefix = prefix.toLowerCase(); + newMessage += + '
' + amount + "
"; + } + + return newMessage + message; + }; + + debugLog("후원 아이콘을 불러왔습니다."); + checkComplete(); +} +else { + debugLog("설정에 의해 후원 아이콘을 불러오지 않았습니다."); + checkComplete(); +} + + + +/* 디씨콘 및 구독콘 로드 및 적용 */ +if (configData.loadTwitchCons) { + var twitchConsUrlTemplate = "https://static-cdn.jtvnw.net/emoticons/v1/"; + + if (configData.consRealSubsOnly) { + applyTwitchCon = function(message, data) { + if ( !(data && data.emotes) || (data.emotes.length==0) ) { return message; } + + // 받은 emotes 데이터를 가공 + var rawEmotes = data.emotes.replace(/-/g, "~").split("/"); + var emotesMap = {}; + for(var index in rawEmotes) { + var element = rawEmotes[index]; + emotesMap[element.split(":")[0]] = element.split(":")[1].split(","); + } + + var rangeIds = []; + for(var id in emotesMap) { + for(var range in emotesMap[id]) { + rangeIds.push([emotesMap[id][range], id]); + } + } + rangeIds.sort( function(a,b) { + return (Number(a[0].split("~")[0]) > Number(b[0].split("~")[0])? 1: 0); + } ); + rangeIds.unshift(["-1~-1", ""]); + + var rangeIdsLength = rangeIds.length - 1; + for(var index=0; index'; + } + else { + newMessage = newMessage + + message.slice( + Number(rangeIds[index][0].split("~")[0]), + Number(rangeIds[index][0].split("~")[1]) + ); + } + } + + return newMessage; + } + + debugLog("트위치 이모티콘과 구독콘을 적용했습니다."); + checkComplete(); + } + else { + var isTwitchConsCalled = { emotes:false, subs:false }; + var isTwitchConsLoaded = { emotes:false, subs:false }; + var consIdMap = {}; + + var twitchEmoteConRequest = new window.XMLHttpRequest(); + twitchEmoteConRequest.open( + "GET", "https://twitchemotes.com/api_cache/v3/global.json", true); + var twitchSubsConRequest = new window.XMLHttpRequest(); + twitchSubsConRequest.open( + "GET", "https://twitchemotes.com/api_cache/v3/subscriber.json", true); + + var twitchConsApply = function() { + applyTwitchCon = function(message, data) { + var messageArray = message.split(/(^| )([^ ]*)/g); + while(messageArray.indexOf("") != -1) { + messageArray.splice(messageArray.indexOf(""),1); + } + for(var index in messageArray) { + if (consIdMap.hasOwnProperty(messageArray[index])) { + for (var keyword in consIdMap) { + if (messageArray[index] == keyword) { + messageArray[index] = + ''; + } + } + } + } + + return messageArray.join(""); + }; + + var loadMsg = "을 적용했습니다."; + if (isTwitchConsLoaded.subs) { + if (isTwitchConsLoaded.emotes) { loadMsg = "트위치 이모티콘과 구독콘" + loadMsg; } + else { loadMsg = "트위치 구독콘" + loadMsg; } + } + else { + if (isTwitchConsLoaded.emotes) { loadMsg = "트위치 이모티콘" + loadMsg; } + else { + debugLog("트위치 이모티콘과 구독콘을 적용할 수 없었습니다."); + return; + } + } + debugLog("불러온 " + loadMsg); + if(isTwitchConsLoaded.subs && isTwitchConsLoaded.emotes) { checkComplete(); } + }; + + twitchEmoteConRequest.onreadystatechange = function(evt) { + if (twitchEmoteConRequest.readyState == 4) { + if (twitchEmoteConRequest.status == 200) { + var data = JSON.parse(twitchEmoteConRequest.responseText); + + for (var keyword in data) { + consIdMap[keyword] = data[keyword].id; + } + + isTwitchConsLoaded.emotes = true; + debugLog("트위치 이모티콘을 불러왔습니다."); + } + else { + debugLog( + "트위치 이모티콘을 불러오는 데 실패했습니다." + + "\n에러 코드 " + twitchEmoteConRequest.status); + } + + isTwitchConsCalled.emotes = true; + if(isTwitchConsCalled.emotes && isTwitchConsCalled.subs) { twitchConsApply(); } + } + }; + twitchSubsConRequest.onreadystatechange = function(evt) { + if (twitchSubsConRequest.readyState == 4) { + if (twitchSubsConRequest.status == 200) { + var data = JSON.parse(twitchSubsConRequest.responseText); + + for (var conChannel in data) { + var targetChannel = data[conChannel]; + for (var index in targetChannel.emotes) { + var con = targetChannel.emotes[index]; + consIdMap[con.code] = con.id; + } + } + + isTwitchConsLoaded.subs = true; + debugLog("트위치 구독콘을 불러왔습니다."); + } + else { + debugLog( + "트위치 구독콘을 불러오는 데 실패했습니다." + + "\n에러 코드 " + twitchSubsConRequest.status); + } + + isTwitchConsCalled.subs = true; + if(isTwitchConsCalled.emotes && isTwitchConsCalled.subs) { twitchConsApply(); } + } + }; + + twitchEmoteConRequest.send(null); + twitchSubsConRequest.send(null); + } +} +else { + debugLog("설정에 따라 트위치 이모티콘과 구독콘을 불러오지 않았습니다."); + checkComplete(); +} + +dcConsData = []; +if (configData.loadDcCons) { + var dcConsSubURI = "images/"; + var dcConsMainURI = ""; + if (configData.dcConsURI == "") { + configData.dcConsURI = "./"; + dcConsSubURI = "images/dccon/"; + dcConsMainURI = "lib/"; + } + else if (configData.dcConsURI[configData.dcConsURI.length-1] != "/") { + configData.dcConsURI += "/"; + } + + var dcConScript = document.createElement("script"); + dcConScript.type = "text/javascript"; + dcConScript.charset = "utf-8"; + dcConScript.src = configData.dcConsURI + dcConsMainURI + "dccon_list.js"; + document.body.appendChild(dcConScript); + + dcConScript.onload = function() { + if (dcConsData.length == 0) { debugLog("디씨콘을 불러오는 데 실패했습니다."); } + else { + var keywords = []; + for (var index in dcConsData) { + for (var index2 in dcConsData[index].keywords) { + keywords.push(dcConsData[index].keywords[index2]); + } + } + keywords.sort(function(a,b) { return a.length < b.length; } ); + + applyDcCon = function(message, data) { + for (var index in keywords) { + var keyword = keywords[index]; + if (message.indexOf("~" + keyword) != -1) { + message = message.split("~" + keyword).join( + ''); + } + } + + return message; + }; + debugLog("디씨콘을 적용했습니다."); + checkComplete(); + } + }; +} +else { + debugLog("설정에 따라 디씨콘을 적용하지 않았습니다."); + checkComplete(); +} + + + +/* IRC 클라이언트 설정 */ +var joinCount = 0; +defaultColors = [ + "#FF0000", "#0000FF", "#00FF00", "#B22222", "#FF7F50", + "#9ACD32", "#FF4500", "#2E8B57", "#DAA520", "#D2691E", + "#5F9EA0", "#1E90FF", "#FF69B4", "#8A2BE2", "#00FF7F"]; +debugLog("트위치에 접속을 시도합니다."); +client = (function() { + var ws = new WebSocket(configData.webSocket); + ws.onopen = function() { + ws.send("PASS " + configData.pass + "\r\n"); + ws.send("NICK " + configData.nick + "\r\n"); + ws.send('CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership' + "\r\n"); + ws.send("JOIN " + configData.channel + "\r\n"); + } + ws.onmessage = function(evt) { + var lines = evt.data.toString().split(/\r\n|\r|\n/); + lines.pop(); + lines.forEach( function(element) { + var line = JSON.stringify(element).slice(1,-1); + var args = line.replace(/^(:|@)/m, "").split(" "); + + if (line[0] == ":") { + // IRC 명령어 처리 + switch (args[1]) { + case "421": // 잘못된 명령어를 보냈을 때 + case "001": // 웰컴 메세지 + case "002": // 호스트 알림 + case "003": // 서버 상태태 + case "004": // 접속초기메세지 끝 + case "375": // MOTD(공지사항) 시작 + case "372": // MOTD + case "376": // MOTD 끝 + case "CAP": // 트위치 명령어 수신 확인 메세지 + case "353": // 접속자 목록(로드가 안된상태라 justinfan뿐이지만) + case "366": // 이름 목록 끝 + case "MODE": // 관리자 권한 감지 + case "PART": // 서버에서 유저 접속 해제 + case "HOSTTARGET": + // 호스팅에 변화가 생겼을 때 + break; + + case "JOIN": // 서버에 유저가 접속 + if(args[0].search("justinfan") != -1) { + debugLog( + args[2].substring(1) + "에 접속했습니다."); + } + if(++joinCount <= configData.channel.match(/#/g).length) { + checkComplete(); + } + break; + + default: // 미처리 메세지 + if (configData.allMessageHandle) { + debugLog("처리되지 않은 메세지를 수신했습니다.
" + line); + } + break; + } + } + else if (line[0] == "@") { + // 트위치 명령어 처리 + var data = {}; + var twitchArgs = {}; + args.shift().split(";").forEach( function(element) { + var keyval = element.split("="); + twitchArgs[keyval[0]] = keyval[1]; + } ); + + switch(args[1]) { + case "ROOMSTATE": // 방 정보 + break; + + + + case "USERNOTICE": + if (twitchArgs.msg-param-months && twitchArgs.msg-param-months!="") { + // 구독 메세지 수신 + data.subMonths = Number(twitchArgs.msg-param-months); + } + else { + if (configData.allMessageHandle) { + debugLog("처리되지 않은 메세지를 수신했습니다.
" + line); + } + break; + } + + case "PRIVMSG": // 채팅 수신 + // 이름 지정 + var nick = args[0].split(/[!@]/g)[1]; + var displayNick = twitchArgs["display-name"]; + var realNick = ""; + if (configData.useDisplayName) { + if (displayNick.replace(/\s/g, "")!="") { realNick = displayNick; } + } + else { realNick = nick; } + + var message = args.slice(3).join(" ").substring(1); + data.badges = twitchArgs.badges; + data.color = twitchArgs.color; + data.emotes = twitchArgs.emotes; + data.nick = nick; + + // muteUser 적용 + if (configData.muteUser) { + var match = configData.muteUser.find( function(element) { + return (element == displayNick) || (element == nick); + } ); + + if (match != undefined) break; + } + + // 클립 링크 파싱 + var clip = message.match(/(https?:\/\/)?clips\.[a-zA-Z./]*/g); + if (clip!=null) { + message = message.replace(clip[0], ""); + + if (configData.loadClipPreview) { + var clipRequest = new window.XMLHttpRequest(); + clipRequest.open("GET", clip[0], true); + clipRequest.responseType = "document"; + + var handler = function(evt) { + if(clipRequest.readyState == 4) { + if(clipRequest.status == 200) { + var src, title, uploader; + var metas = clipRequest.responseXML.getElementsByTagName("meta"); + for(var i=0; i
' + title + + '
' + uploader + '
'; + } + else { + data["clip"] = '
Invalid Clip
'; + clipRequest.removeEventListener("readystatechange", handler); + } + addChatMessage(realNick, message, data); + } + }; + + clipRequest.addEventListener('readystatechange', handler); + clipRequest.send(null); + break; + } + else { + data["clip"] = + '
' + + configData.clipReplaceMsg + '
'; + } + } + + // 유저 이름색 지정 + if (!data.color || data.color=="") { + var n = nick.charCodeAt(0) + nick.charCodeAt(1)*new Date().getDate(); + data.color = defaultColors[n % defaultColors.length]; + } + + // 비트 메세지 파싱 + if (message.match(cheerRegExp) != null) { + var cheers = message.match( + new RegExp("(" + cheerRegExp.source + ")\\d+ ", "ig")); + var cheer = 0; + for(var index in cheers) { + cheer += Number(cheers[index].replace(cheerRegExp, "")); + } + data.cheers = cheer; + } + + // 메세지 출력 + addChatMessage(realNick, message, data); + break; + + case "NOTICE": // 공지 메세지 + switch(twitchArgs["msg-id"]) { + case "host_off": // 호스팅을 끊었을 때 + case "host_target_went_offline": + // 호스팅이 끊겼을 때 + debugLog("호스팅이 종료되었습니다."); + break; + + case "host_on": // 호스팅되었을 때 + debugLog( + args[5].slice(0,-1) + " 호스팅 중.\n" + + "호스팅중인 채팅의 전송은 지원하지 않습니다."); + break; + + } + break; + + case "CLEARCHAT": // 매니저가 /clear 했을 때 + if(args.length == 4) { + banChatMessage(args[3].substring(1)); + } + else { + document.getElementById("chat_wrapper").innerHTML = ""; + numChat = 0; + } + break; + + default: // 미처리 메세지 + if (configData.allMessageHandle) { + debugLog("처리되지 않은 메세지를 수신했습니다.
" + line); + } + break; + } + } + else { + // 서버 연결상태 확인용 ping-pong + if (args[0] == "PING") { ws.send("PONG :tmi.witch.tv\r\n"); } + else if (configData.allMessageHandle) { + debugLog("처리되지 않은 메세지를 수신했습니다.
" + line); + } + } + } ); + } + ws.onclose = function() { + debugLog( + "채팅 서버와의 연결이 종료되었습니다.
" + + configData.retryInterval + "초 후 재접속을 시도합니다."); + setTimeout( + function() { client(); }, + configData.retryInterval * 1000 ); + } +}) (); \ No newline at end of file diff --git a/source/twip_apply.js b/source/twip_apply.js new file mode 100644 index 0000000..2e81c6d --- /dev/null +++ b/source/twip_apply.js @@ -0,0 +1,22 @@ +var twipMsg = + configData&&configData.twipCheersMsg? + configData.twipCheersMsg: + configData.cheersMsg.replace("{bits}", "{wons}").replace(/\s*비트/, "원"); + +exports.apply = function(message, data) { + if (data.nick == "twipkr") { + if ( (message.indexOf("ACTION")!=-1) && (message.indexOf("후원함!")!=-1) ) { + message = message.replace(/\\u0001/g, ""). + replace("ACTION", "").replace("을 후원함!", "원을 후원함!"); + + var amount = message.match(/ (\d+,)*\d+원을 후원함!/g)[0]. + split(" ")[1].replace(/[^\d]/g, ""); + + message = '
' + + twipMsg.replace("{wons}", amount) + + "
" + message; + } + } + + return message; +} \ No newline at end of file diff --git a/theme/default/theme.css b/theme/default/theme.css new file mode 100644 index 0000000..10e597c --- /dev/null +++ b/theme/default/theme.css @@ -0,0 +1,167 @@ +/* 폰트 로드 */ +@import url(https://fonts.googleapis.com/earlyaccess/jejugothic.css); +@import url(https://fonts.googleapis.com/earlyaccess/notosanskr.css); +@import url(https://fonts.googleapis.com/earlyaccess/cwtexyen.css); + + +/* 위치 기본 설정 */ +html +{ + /* 송출 소프트웨어의 disable scrollbar 여부에 상관 없이 스크롤바를 나타나지 않게 함 */ + overflow:hidden; +} + +body +{ + position:absolute; + + /* html의 기본 여백 제거 */ + margin:0; right:0; left:0; + /* 채팅 메세지가 아래에서부터 나타나도록 */ + bottom:0; +} + +img.twitch_emote +{ + /* 최대(기본) 크기는 112px */ + max-height:50px; +} + + +/* 구독자 및 후원 메세지 설정 */ +/* + 구독 메세지 +
+
(configData.cheersMsg)
+ 우리 스트리머 사랑해!! +
+ +
(configData.subMonthsMsg)
+ 구독인 줄 알았지만 테스트였습니다 Kappa Kappa +
+*/ +@keyframes SubsAni { 50% { color:red; } } +.chat_msg_box .chat_subscribe_box, +.chat_msg_box .chat_cheer_box { text-align:center; + animation:SubsAni 6 1s ease; } +/* 후원 금액 */ +.chat_cheer_text { color:red; display:inline-block; } +/* 후원 아이콘 */ +.chat_cheer_text img.cheer_icon { max-height:1em; vertical-align: middle; } + + + +/* 배경 설정. 전체적인 배경이 필요할 때 이용 */ +/* +
+
+ ... +*/ +#background { position:absolute; left:0; right:0; top:0; bottom:0; } + + + +/* 폰트 설정 */ +html { font-family:'cwtexyen', 'Jeju Gothic', serif; + font-size:20px; color: white; + text-shadow: + -1px -1px 1px black, -1px 0 1px black, + -1px 1px 1px black, 0 1px 1px black, + 1px 1px 1px black, 1px 0 1px black, + 1px -1px 1px black, 0 -1px 1px black, + + -1px -1px 3px grey, -1px 0 3px grey, + -1px 1px 3px grey, 0 1px 3px grey, + 1px 1px 3px grey, 1px 0 3px grey, + 1px -1px 3px grey, 0 -1px 3px grey; } +.chat_nickname_box { font-size:0.8em; } + + +.chat_outer_box { position:relative; z-index:0; overflow:hidden; + margin-top:8px; border:1px solid grey; padding:4px; } +.chat_inner_box { margin-bottom:3px; } + +.chat_nickname_box, +.chat_badge_box { display:inline-block; } +.chat_badge_box { float:right; } + +.chat_msg_box { white-space:pre-line; word-break:break-word; + max-height:100px; } +.chat_msg_box.image_only { text-align:center; } + + + +/* 클립이 포함된 채팅 메세지에 대한 설정 */ +/* 크롬 등 보안설정된 브라우저에선 디버그가 불가능하므로 유의 */ +/* +
+
+ ... +
+
+ +
이걸 스트리머가 또
+
Clipped by 맛물
+
+ ... + 클립 로드에 실패했을 경우 +
+ Invalid Clip +
+ ... + 클립 미리보기를 미표시설정했을 경우(configData.loadClipPreview == false) +
+ (configData.clipReplaceMsg) +
+ ... +*/ +.clip_included .chat_msg_box +{ + /* 클립과 일반 텍스트를 같이 보냈을 경우를 위해 높이를 높여준다 */ + max-height:200px; +} + +.chat_clip_box +{ + /* invalid clip은 이미지를 포함하지 않기 때문에 max height로 지정 */ + /* invalid clip의 경우 .chat_clip_box에 .invalid를 포함하므로 이를 이용할 수도 있음 */ + max-height:100px; + + /* .chat_clip_by의 absolute position을 위해 설정 */ + position:relative; +} + +.chat_clip_box img +{ + /* 사실상 이 속성으로 .chat_clip_box의 높이를 조절 */ + height:100px; + /* .chat_clip_box의 나머지 DOM을 오른쪽에 배치시키기 위해 float을 이용 */ + float:left; +} + +.chat_clip_title +{ + /* 가로세로로 모두 가운데정렬하기 위해 사용 */ + line-height:100px; + text-align:center; +} + +.chat_clip_by +{ + /* .chat_clip_box의 우하단에 배치 */ + position:absolute; bottom:0; right:0; + /* config client로 조절하지 않는 글꼴크기이므로 .chat_msg_box에서 상속받아 조절 */ + font-size:0.7em; +} + +.chat_clip_box.text_only +{ + text-align:center; +} \ No newline at end of file