-
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathspeech.html
More file actions
444 lines (389 loc) · 23.4 KB
/
speech.html
File metadata and controls
444 lines (389 loc) · 23.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
<!doctype html>
<html lang="ko">
<head>
<title>채팅 읽어주는 로봇</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xpressengine/xeicon@2/xeicon.min.css">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="./notifications.css?v=20210911" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/aws-sdk/2.840.0/aws-sdk.min.js" integrity="sha512-l299oP0ZZwuId6dh4uB/u5N4ZiMb1TSghnBfPLbRH8SDf4Z9Do73JY9FRbTW656STTe3gPDkV7jiUmsD++D4iA==" crossorigin="anonymous"></script>
<script src="https://rawcdn.githack.com/blueimp/JavaScript-MD5/v2.10.0/js/md5.min.js"></script>
<script src="./ChattyKathy.js?v=20210911"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootbox@5.5.3/bootbox.all.min.js"></script>
<script src="./tmi.min.js?v=20220302-renewal2"></script>
<script src="./notifications.js?v=20210911"></script>
<script src="./chzzk.js?v=20251002"></script>
<script src="./tts.js?v=20251002"></script>
</head>
<body>
<div id="divClick" style="display: block">활성화를 위해 마우스 클릭이 필요합니다.</div>
<div id="divContent" style="display: none;">
<div id="chat" style="overflow: hidden; word-wrap: break-word; width: 800px;">
<button type="button" class="btn btn-danger" disabled="disabled" id="btn-cancel" onclick="window.speechQueue = [];window.kathy.ShutUp();window.speechSynthesis.cancel();playText('큐를 비웠습니다',1.3,1,false,'SYSTEM')">초기화중</button>
<button type="button" class="btn btn-secondary" onclick="document.getElementById('last_read').innerHTML = '';" id="btn-clearlog">로그 비움</button>
<button type="button" class="btn btn-warning" onclick="localStorage.setItem('oauth','');location.reload();" id="btn-logout">로그아웃</button>
<button type="button" class="btn btn-success" onclick="window.change_channel();" id="btn-channel">채널 변경</button><br />
<input type="checkbox" id="chk-autoscroll" onclick="localStorage.setItem('autoscroll', this.checked ? 'true' : 'false');window.stopScroll = !this.checked;if(!window.stopScroll) scrollBottom();" /> 자동 스크롤 켜기<br /><br />
<h5>활성화된 언어</h5>
<input type="checkbox" id="chk-enable-kor" onclick="setLanguage('kor', this.checked)" /> 한국어(우선순위 : <span id="ord-kor">비활성화</span>)
<input type="checkbox" id="chk-enable-jpn" onclick="setLanguage('jpn', this.checked)" /> 일본어(우선순위 : <span id="ord-jpn">비활성화</span>)
<input type="checkbox" id="chk-enable-chn" onclick="setLanguage('chn', this.checked)" /> 중국어(우선순위 : <span id="ord-chn">비활성화</span>)<br />
<div id="last_read" style="width:100%; height: 200px; overflow-y: scroll;"></div>
</div>
<form style="margin:10px">
<div class="form-group">
<label for="polly_access_id">아마존 액세스 아이디</label>
<input type="text" class="form-control" id="polly_access_id" placeholder="Amazon Access ID">
</div>
<div class="form-group">
<label for="polly_access_key">아마존 액세스 키</label>
<input type="text" class="form-control" id="polly_access_key" placeholder="Amazon Access Key">
</div>
<div class="form-group row">
<div class="offset-sm-2 col-sm-10">
<button type="button" class="btn btn-primary" onclick="localStorage.setItem('polly_access_id', document.getElementById('polly_access_id').value);localStorage.setItem('polly_access_key', document.getElementById('polly_access_key').value);location.reload()">
정보 저장
</button>
</div>
</div>
</form>
</div>
<script>
// 저장소 포크후 개인적으로 사용시 아래 <custom-domain-here> 부분과 <custom-client-id-here> 부분을 변경하시면 됩니다. 트위치 개발자 사이트에서 앱 등록으로 client id를 발급받으실수 있습니다.
const client_id_list = {"chatreader-kor.vercel.app":"1pzur9zyr3z0l1829syennm09fl9yi", "lastorder.xyz":"bz5whp3i3ihi98e6e1jx3jkxnksyk1", "<custom-domain-here>":"<custom-client-id-here>"};
// ==================================
// !! 이 아래로는 수정하지 마세요! !!
// ==================================
window.platform = (getParams("platform") || "twitch").toLowerCase();
if (window.platform !== "chzzk") {
window.oauth_client_id = typeof client_id_list[location.hostname] === 'undefined' ? "" : client_id_list[location.hostname];
window.oauth_redirect_uri = location.origin + location.pathname;
} else {
window.oauth_client_id = "";
window.oauth_redirect_uri = "";
}
const url_string = window.location.href;
const url = new URL(url_string);
let client = null;
if ('speechSynthesis' in window) {
console.debug("TTS check done");
} else {
location.href = "./no_tts.html";
}
// 주소뒤 ?channel=(채널아이디) 로 지정
window.channelname = getParams("channel");
if (window.platform !== "chzzk") {
// 클라이언트 ID가 비어 있다면 쿼리스트링을 통해 받아오는것을 시도한다(포크후 소스코드 수정 없이도 이용 가능하도록)
if(!window.oauth_client_id) window.oauth_client_id = getParams("channel");
// 그래도 클라이언트 아이디가 비어 있다면 오류이다
if(!window.oauth_client_id) {
alert("Please set twitch client id before use!");
location.href = location.origin + "/error.html";
}
}
// 채널명 없으면 비워둠
if (window.channelname === null) window.channelname = "";
// 클릭 여부 저장
window.isclicked = false;
// 자동스크롤 중단 여부
window.stopScroll = localStorage.getItem("autoscroll") === "false";
if(!window.stopScroll) {
document.getElementById("chk-autoscroll").checked = true;
scrollBottom();
}
// oauth token으로 처음 페이지 접속시 트위치 로그인으로 생성함(로컬스토리지 저장, 로그아웃시 초기화됨)
window.oauth = localStorage.getItem("oauth") !== null ? localStorage.getItem("oauth") : "";
window.login = localStorage.getItem("login") !== null && localStorage.getItem("login") !== "undefined" ? localStorage.getItem("login") : "";
window.display_name = localStorage.getItem("display_name") !== null && localStorage.getItem("display_name") !== "undefined" ? localStorage.getItem("display_name") : "";
window.last_login_date = localStorage.getItem("last_login_date") !== null && localStorage.getItem("last_login_date") !== "undefined" ? parseInt(localStorage.getItem("last_login_date")) : -1;
// 주소뒤 ?debug=true 로 디버그모드 활성화
window.debugmode = getParams("debug") === "true";
// 읽을 채팅 최대 길이(!!tts maxlength (길이)로 변경가능)
window.maxlength = localStorage.getItem("tts_maxlength") !== null ? parseInt(localStorage.getItem("tts_maxlength")) : 40;
// 읽을 채팅 음량 (!!tts volume (음량)으로 변경가능)
window.volume = localStorage.getItem("tts_volume") !== null ? parseInt(localStorage.getItem("tts_volume")) : 100;
// 차단 목록(로컬스토리지 저장, !!tts ban (아이디) 로 밴하고 !!tts unban (아이디)로 밴 해제)
window.banlist = localStorage.getItem("tts_banlist_" + window.channelname) !== null ? localStorage.getItem("tts_banlist_" + window.channelname).split("|") : [];
// 허용 목록(로컬스토리지 저장, !!tts addlist (아이디) 로 추가하고 !!tts removelist (아이디)로 삭제)
window.whitelist = (localStorage.getItem("tts_whitelist_" + window.channelname) !== null && localStorage.getItem("tts_whitelist_" + window.channelname) !== "") ? localStorage.getItem("tts_whitelist_" + window.channelname).split("|") : [];
// 밴키워드(로컬스토리지 저장, !!tts ban (아이디) 로 밴하고 !!tts unban (아이디)로 밴 해제)
window.bankeyword = localStorage.getItem("tts_bankeyword_" + window.channelname) !== null ? localStorage.getItem("tts_bankeyword_" + window.channelname).split("|") : [];
// 기본적으로 밴할 리스트
window.banlist = window.banlist.concat(['Nightbot', '싹둑']);
// 기본적으로 밴할 키워드
window.bankeyword = window.bankeyword.concat(['섹스']);
// 언어 순서 설정
window.languagelist = localStorage.getItem("languagelist") !== null ? JSON.parse(localStorage.getItem("languagelist")) : ["kor","jpn"];
window.curOrd = 1;
for(idx in window.languagelist) {
document.getElementById("chk-enable-" + window.languagelist[idx]).checked = true;
document.getElementById("ord-" + window.languagelist[idx]).innerText = window.curOrd++;
}
// 각 리스트 모두 중복 제거
if(window.banlist.length != 0) {
window.banlist = window.banlist.filter(function(item, pos) {
return window.banlist.indexOf(item) == pos;
});
}
if(window.whitelist.length != 0) {
window.whitelist = window.whitelist.filter(function(item, pos) {
return window.whitelist.indexOf(item) == pos;
});
}
if(window.bankeyword.length != 0) {
window.bankeyword = window.bankeyword.filter(function(item, pos) {
return window.bankeyword.indexOf(item) == pos;
});
}
// 구독자 전용
window.tts_subonly = localStorage.getItem("tts_subonly") === "true";
// 개설자 전용
window.tts_founderonly = localStorage.getItem("tts_founderonly") === "true";
// 시청자 전용(스트리머만 제외)
window.tts_vieweronly = localStorage.getItem("tts_vieweronly") === "true";
// 보이스 개인화 설정
window.uniq_voice = localStorage.getItem("uniq_voice") !== "false";
// 기본 보이스
window.def_voice = localStorage.getItem("def_voice") !== null ? localStorage.getItem("def_voice") : "default";
// 초기화 성공 여부
window.initok = false;
window.mod_speed = 1;
window.nonmod_speed = 1.2;
speechSynthesis.cancel();
document.getElementById('polly_access_id').value = localStorage.getItem("polly_access_id");
document.getElementById('polly_access_key').value = localStorage.getItem("polly_access_key");
const awsCredentials = new AWS.Credentials(localStorage.getItem("polly_access_id"), localStorage.getItem("polly_access_key"));
const settings = {
awsCredentials: awsCredentials,
awsRegion: "us-west-2",
pollyVoiceId: "Seoyeon",
cacheSpeech: true
};
window.kathy = new ChattyKathy(settings);
window.users = [];
window.speechQueue = [];
// tokens before 21/10/7 00:00(GMT+9) are invalid due to twitch hacking
if (window.platform === "chzzk") {
window.client = { say: function() {} };
$(document).on('click', function() {
if(window.isclicked) return;
window.isclicked = true;
document.getElementById("divClick").style.display = "none";
document.getElementById("divContent").style.display = "block";
startChzzk();
});
} else if (window.oauth !== "" && window.login !== "" && window.last_login_date > 1633532400) {
$(document).on('click', function() {
if(window.isclicked) return;
window.isclicked = true;
document.getElementById("divClick").style.display = "none";
document.getElementById("divContent").style.display = "block";
if (window.channelname === "") {
location.href = url.origin + url.pathname + "?channel=" + window.login;
} else {
client = new tmi.Client({
options: { debug: true, messagesLogLevel: "info", clientId: window.oauth_client_id },
connection: {
reconnect: true,
secure: true
},
identity: {
username: window.login,
password: window.oauth
},
channels: [ window.channelname ]});
client.on("connected", (address, port) => {
checkTTS(window.channelname + " 채널에 연결되었습니다.");
});
client.on("message", function(channel, userstate, message, self) {
// 봇 자신의 메세지 혹은 채팅이 아닌 메세지는 무시
if (self || userstate["message-type"] !== "chat") return;
let msg = {}
msg.from = userstate["display-name"];
msg.text = message;
msg.color = userstate.color;
if(userstate['emotes-raw'] !== null) msg.emotes = userstate['emotes-raw'];
msg.action = userstate["message-type"] == "action";
msg.badges = userstate["badges-raw"] ?? "";
msg.streamer = msg.badges.indexOf("broadcaster/1") !== -1;
msg.mod = userstate.mod;
msg.sub = userstate.subscriber;
msg.turbo = userstate.turbo;
msg.room_id = userstate["room-id"];
msg.user_id = userstate["user-id"];
parseMessage(msg);
});
client.connect().catch(console.error);
}
});
} else {
// there's no oauth key
if (document.location.hash !== "" && document.location.hash.indexOf("access_token") !== -1) {
//user already authed
const rawauth = document.location.href.replace("#", "?");
const authobj = new URL(rawauth);
const oauth = getParams("access_token", rawauth);
const state = getParams("state", rawauth);
const localstate = localStorage.getItem("state");
const last_url = localStorage.getItem("last_url");
const last_url_obj = new URL(last_url);
document.body.innerHTML = '';
if (last_url_obj.origin !== authobj.origin) {
document.write("SECURITY ERROR");
} else {
if (localstate === null || localstate === "" || state !== localstate) {
document.write("잘못된 state값이 전달되었습니다. 페이지를 새로고침 해보세요.<br />Invalid state. please refresh and retry.")
} else {
localStorage.setItem("oauth", oauth);
localStorage.setItem("state", "");
localStorage.setItem("last_url", "");
fetch(
'https://api.twitch.tv/helix/users',
{
"headers": {
"Client-ID": window.oauth_client_id,
"Authorization": "Bearer " + oauth
}
}
)
.then(function(response) {
return response.json();
})
.then(function(result) {
localStorage.setItem("login", result.data[0].login);
localStorage.setItem("display_name", result.data[0].display_name);
localStorage.setItem("last_login_date", Math.floor(new Date().getTime() / 1000));
location.href = last_url;
});
}
}
} else {
//not authed yet
const state = md5(Date.now());
localStorage.setItem("state", state);
localStorage.setItem("last_url", location.href);
document.body.innerHTML = '';
document.write("트위치로 로그인해야 사용하실수 있습니다.<br />2021년 10월 7일 이전에 로그인하셨다면 트위치 해킹으로 인해 키가 유출되었을 가능성이 있어 재로그인하셔야 합니다.<br /><br /> <a href=\"https://id.twitch.tv/oauth2/authorize?response_type=token&client_id=" +
window.oauth_client_id +
"&redirect_uri=" +
window.oauth_redirect_uri +
"&scope=bits:read%20channel:read:hype_train%20channel:read:redemptions%20channel:read:subscriptions%20chat:read%20chat:edit&state=" +
state +
"\">트위치 아이디로 로그인</a>");
}
}
function startChzzk() {
if (!window.channelname) {
bootbox.alert("치지직 채널 ID가 필요합니다. 채널 변경 버튼으로 ID를 입력해주세요.");
return;
}
document.getElementById("btn-cancel").innerHTML = "초기화중";
document.getElementById("btn-cancel").disabled = true;
if (window.chzzkClient) {
window.chzzkClient.stop();
}
window.chzzkClient = new ChzzkChatClient(window.channelname, {
connect: function(info) {
const channelTitle = info?.channelName || window.channelname;
checkTTS(channelTitle + " 채널에 연결되었습니다.");
},
disconnect: function() {
window.error({
title: '연결 종료',
message: '치지직 채팅 연결이 종료되었습니다. 다시 연결을 시도합니다.'
});
},
chat: function(payload) {
handleChzzkChat(payload);
},
donation: function(payload) {
handleChzzkDonation(payload);
},
subscription: function(payload) {
handleChzzkSubscription(payload);
},
notice: function(payload) {
if (!payload || !payload.message) return;
const nickname = payload.profile?.nickname || "SYSTEM";
document.getElementById("last_read").innerHTML += "<b>[공지] " + nickname + "</b>:" + payload.message + "<br />\n";
},
systemMessage: function(payload) {
if (!payload || !payload.message) return;
document.getElementById("last_read").innerHTML += "<b>[시스템]</b>" + payload.message + "<br />\n";
},
error: function(err) {
window.error({
title: '치지직 오류',
message: err.message || '치지직 채팅 정보를 불러오지 못했습니다.'
});
}
});
window.chzzkClient.start().catch(function(err) {
window.error({
title: '치지직 오류',
message: err.message || '치지직 채팅 연결을 시작할 수 없습니다.'
});
});
}
function formatChzzkBadges(profile) {
const badges = [];
if (!profile) return badges;
if (profile.userRoleCode === 'streamer') badges.push('broadcaster/1');
if (profile.userRoleCode === 'streaming_chat_manager' || profile.userRoleCode === 'streaming_channel_manager' || profile.userRoleCode === 'manager') badges.push('moderator/1');
if (profile.streamingProperty && profile.streamingProperty.subscription) badges.push('subscriber/1');
if (profile.verifiedMark) badges.push('verified/1');
return badges;
}
function handleChzzkChat(payload) {
if (!payload || payload.hidden || !payload.message) return;
const profile = payload.profile || {};
const nickname = profile.nickname || "익명";
const badges = formatChzzkBadges(profile);
const msg = {
from: nickname,
text: payload.message,
color: profile.streamingProperty?.nicknameColor?.colorCode || "",
badges: badges.join(','),
streamer: badges.indexOf('broadcaster/1') !== -1,
mod: badges.indexOf('moderator/1') !== -1,
sub: badges.indexOf('subscriber/1') !== -1,
founder: false,
turbo: false,
room_id: window.chzzkClient?.chatChannelId || "",
user_id: profile.userIdHash || "",
action: false,
emotes: null
};
parseMessage(msg);
}
function handleChzzkDonation(payload) {
if (!payload) return;
const profile = payload.profile || payload.extras?.params?.registerChatProfile || {};
const nickname = profile?.nickname || "익명의 후원자";
const amount = payload.extras?.payAmount;
const donationText = payload.message || "";
const combined = amount ? `${nickname}님이 ${amount}원을 후원했습니다. ${donationText}` : `${nickname}님의 후원 ${donationText}`;
handleChzzkChat({
profile,
message: combined,
hidden: payload.hidden,
extras: payload.extras
});
}
function handleChzzkSubscription(payload) {
if (!payload) return;
const profile = payload.profile || {};
const nickname = profile.nickname || "시청자";
const month = payload.extras?.month ? `${payload.extras.month}개월` : "";
const subText = `${nickname}님이 ${month ? month + ' ' : ''}구독을 갱신했습니다.`;
handleChzzkChat({
profile,
message: subText,
hidden: payload.hidden
});
}
</script>
</body>
</html>