// ==UserScript==
// @name Twitch Chat Filter
// @namespace TwitchChatFilterScript
// @version 0.8
// @description Twitchのチャット欄にNG機能を追加します。Chat Filter for Twitch chat
// @author bd
// @match https://www.twitch.tv/*
// @icon https://www.google.com/s2/favicons?domain=twitch.tv
// @license MIT
// @noframes
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
const Config = {
BannedWord: GM_getValue("TCO_BannedWord"),
BannedUser: GM_getValue("TCO_BannedUser"),
AutoBan: false,
Load: () => {
if(Config.BannedWord == null){
Config.BannedWord = "";
}
if(Config.BannedUser == null){
Config.BannedUser = "";
}
},
Save: () => {
GM_setValue("TCO_BannedWord", Config.BannedWord);
GM_setValue("TCO_BannedUser", Config.BannedUser);
},
AddBannedWord: (word) => {
Config.BannedWord += word + "\n";
},
AddBannedUser: (id) => {
Config.BannedUser += id + "\n";
},
}
const ChatFieldObserver = new MutationObserver(function(mutations){
mutations.forEach(function(e){
let chat = e.addedNodes;
//console.log(chat);
for(let i = 0; i < chat.length; i++){
if(chat[i].className != ClassName.AddedChat()){
continue;
}
try {
const userInfo = GetUserInfo(chat[i]);
const textContainer = GetChildElementsByAttribute(
Element.GetMessageElement(chat[i]),
AttributeName.TextContainer(),
AttributeName.TextContainerValue()
);//ここまで
if(IsBannedWord(textContainer[0].innerText) || IsBannedUser(userInfo.id) || IsBannedWorldPerfect(textContainer[0].innerText)){
HideElement(chat[i]);
ShowBannedChat(textContainer[0].innerText, userInfo.id);
AddBannedCount();
}
if(IsBannedWord(textContainer[0].innerText)){
if(Config.AutoBan && !IsBannedUser(userInfo.id)){
Config.AddBannedUser(userInfo.id);
Config.Save();
LoadPanelValue();
console.log('added');
}
}
PutBanButton(textContainer[0]);
SetBanButtonEvent(chat[i], userInfo.id);
//console.log(userInfo.name);
//console.log(userInfo.id);
//console.log(textContainer);
}
catch ( e ) {
console.error(e.message);
}
finally{
continue;
}
}
})
});
//頻繁に変わりそうなクラス名など
const ClassName = {
//配信時:アーカイブ時
ChatField: ()=>{return (isStreaming())?"chat-scrollable-area__message-container":"video-chat__message-list-wrapper"},
DisplayName: "chat-author__display-name",
AddedChat: ()=>{return (isStreaming())?"Layout-sc-1xcs6mc-0":"InjectLayout-sc-1i43xsx-0 bQEtql"},
ChatMessageContainer: () => {return (isStreaming())?"chat-line__no-background":"video-chat__message"},
BottomBar: () => {return (isStreaming())?"Layout-sc-1xcs6mc-0 bKPhAm":"Layout-sc-1xcs6mc-0 bZpfnT"},
}
const AttributeName = {
TextContainer: () => {return (isStreaming())?"data-a-target":"data-a-target"},
//TextContainerValue: () => {return (isStreaming())?"chat-message-text":"chat-message-text"},
TextContainerValue: () => {return (isStreaming())?"chat-line-message-body":"chat-message-text"},
}
const Element = {
GetChatField: () => {
return (isStreaming())?
document.getElementsByClassName(ClassName.ChatField())[0]:
document.getElementsByClassName(ClassName.ChatField())[0].firstChild.firstChild
},
GetMessageElement: (chat) => {
return (isStreaming())?
chat.getElementsByClassName(ClassName.ChatMessageContainer())[0].lastChild:
chat.getElementsByClassName(ClassName.ChatMessageContainer())[0].lastChild
}
}
//現在のページの配信状態を判別
let isStreaming = () =>{
let pathname = location.pathname;
let path = pathname.split('/');
return path.length === 2;
/*
// 配信時
if(path.length == 2){
return true;
}
// 配信なしorアーカイブ
else{
return false;
}
return false;*/
};
let settingPanelActive = false;
let bannedCount = 0;
let waitInterval;
window.onload = function() {
console.log(location.pathname);
console.log(isStreaming());
WaitPageLoaded();
}
function WaitPageLoaded()
{
let count = 1;
clearInterval(waitInterval);
waitInterval = setInterval(function(){
count++;
//console.log(ClassName.ChatField());
//発見時
if(Element.GetChatField() !== undefined &&
document.getElementsByClassName(ClassName.BottomBar())[0] !== undefined){
log('Element detected.');
Initialize();
count = 0;
clearInterval(waitInterval);
}
//発見不可
if(10 < count){
log('Element cannot be found.');
count = 0;
clearInterval(waitInterval);
}
},1000);
}
function Initialize(){
Config.Load();
ChatFieldObserver.disconnect();
ChatFieldObserver.observe(
Element.GetChatField(),
{childList: true}
);
//console.log(Element.GetChatField());
PutSettingPanel();
SetPanelEvent();
LoadPanelValue();
SetAutoBanEvent();
}
//NGワードの判定する
function IsBannedWord(text){
if(Config.BannedWord == ""){
return false;
}
let BannedWord = Config.BannedWord.split(/\r\n|\n/);
for(let i = 0; i < BannedWord.length; i++){
if(BannedWord[i] == ""){
continue;
}
let result = text.match(BannedWord[i])
if(result != null){
return true;
break;
}
}
return false;
}
function IsBannedWorldPerfect(text){
if(text == "あ" || text == "a"){
return true;
}
else{
return false;
}
}
//NGユーザの判定する
function IsBannedUser(id){
if(Config.BannedUser == ""){
return false;
}
let BannedUser = Config.BannedUser.split(/\r\n|\n/);
for(let i = 0; i < BannedUser.length; i++){
if(BannedUser[i] == ""){
continue;
}
let result = id.match(BannedUser[i])
if(result != null){
return true;
break;
}
}
return false;
}
//指定のエレメントを非表示
function HideElement(element){
element.style.display = "none";
}
function ShowBannedChat(text, id){
if(15 < text.length)text = text.substr(0, 15) + "..";
let html = document.getElementById("tco-banned-chat").innerHTML;
html = id + ": " + text + "\n" + html;
document.getElementById("tco-banned-chat").innerHTML = html.substr(0, 150);
}
function PutSettingPanel(){
const bottomBar = document.getElementsByClassName(ClassName.BottomBar())[0];
const HTML =`<div class="tco-panel" id="tco-panel">
<button class="ScCoreButton-sc-1qn4ixc-0 jGqsfG ScButtonIcon-sc-o7ndmn-0 fNzXyu" data-a-target="setting-panel-button" id="tco-panel-button">
<span>設定</span>
</button>
<div class="tco-panel-background" id="tco-panel-background" style="
position: absolute;
top: -300px;
width: 340px;
height: auto;
left: 500px;
background-color: black;
opacity: 0.8;
display: none;
flex-direction: row;
justify-content: center;
">
<div style="
display: flex;
flex-direction: column;
width: 100%;
">
<span>NGワード<font color="red">*</font></span>
<div>
<textarea name="tco-banned-words" id="tco-banned-words" rows="8"></textarea>
</div>
<span>NGユーザー<font color="red">*</font></span><span id="tco-users-count">人</span>
<div>
<textarea name="tco-banned-users" id="tco-banned-users" rows="8"></textarea>
</div>
<div>
<input type="checkbox" class="tco-input-checkbox" id="tco-input-checkbox-put-button" checked="true"><label for="tco-input-checkbox-put-button">NGボタンを表示する</label>
</div>
<div>
<input type="checkbox" class="tco-input-checkbox" id="tco-input-checkbox-auto-ban"><label for="tco-input-checkbox-auto-ban">NGワードの発言者を自動でNGユーザーに追加</label>
</div>
</div>
<div style="
display: flex;
flex-direction: column;
width: 100%;
">
<span id="tco-banned-count">0個のゴミを非表示にしました</span>
<span>↓以下ゴミ共のコメント↓</span>
<span id="tco-banned-chat" style="white-space: pre-line;color: darkgrey;font-size: 11px;"></span>
<div style="
height: 100%;
display: flex;
align-items: flex-end;
justify-content: flex-end;
margin: 10px;">
<input type="button" class="tco-save-button" id="tco-save-button" value="保存">
</div>
</div>
</div>
</div>
`
bottomBar.insertAdjacentHTML("afterbegin", HTML)
}
function LoadPanelValue(){
document.getElementById("tco-banned-words").value = Config.BannedWord;
document.getElementById("tco-banned-users").value = Config.BannedUser;
let bannedUser = Config.BannedUser.split(/\r\n|\n/);
document.getElementById("tco-users-count").innerHTML = Config.BannedUser.split(/\r\n|\n/).length.toString() + "人";
}
function GetPanelInfo(){
let _bannedWord = document.getElementById("tco-banned-words").value;
let _bannedUser = document.getElementById("tco-banned-users").value;
let result = {
bannedWord: _bannedWord,
bannedUser: _bannedUser,
}
return result;
}
//設定パネルのイベントなどを設定
function SetPanelEvent(){
document.getElementById("tco-panel-button").onclick = () => {
if(settingPanelActive){
document.getElementById("tco-panel-background").style.display = "none";
settingPanelActive = false;
}else{
document.getElementById("tco-panel-background").style.display = "flex";
settingPanelActive = true;
}
}
document.getElementById("tco-save-button").onclick = () => {
const panelInfo = GetPanelInfo();
Config.BannedWord = panelInfo.bannedWord;
Config.BannedUser = panelInfo.bannedUser;
Config.Save();
LoadPanelValue();
}
}
//チャットに表示するNGボタンを設置
function PutBanButton(container){
const html =
`<span style="left: 90%;">
<button aria-label="NGに入れる" class="tco-ban-button" id="tco-ban-button" style="padding: 0px;width: 14px; height: 14px;" >
<div style="width: 100%; height: 14px;">
<div class="tw-align-items-center tw-full-width tw-icon tw-icon--fill tw-inline-flex">
<svg class="tw-icon__svg" width="100%" height="100%" version="1.1" viewBox="0 0 512 512" x="0px" y="0px" style="fill: var(--color-fill-button-icon-hover);"><path d="M437.023,74.977c-99.984-99.969-262.063-99.969-362.047,0c-99.969,99.984-99.969,262.063,0,362.047c99.969,99.969,262.078,99.969,362.047,0S536.992,174.945,437.023,74.977z M137.211,137.211c54.391-54.391,137.016-63.453,201.016-27.531L109.68,338.227C73.758,274.227,82.82,191.602,137.211,137.211z M374.805,374.789c-54.391,54.391-137.031,63.469-201.031,27.547l228.563-228.563C438.258,237.773,429.18,320.414,374.805,374.789z" fill-rule="evenodd"></path></svg>
</div>
</div>
</div>
</button>
</span>`;
container.insertAdjacentHTML("afterend",html);
}
function SetBanButtonEvent(chat, id){
chat.getElementsByClassName("tco-ban-button")[0].onclick = () =>{
HideElement(chat);
Config.AddBannedUser(id);
Config.Save();
LoadPanelValue();
};
}
function ToggleAutoBan(){
const checkbox = document.getElementById('tco-input-checkbox-auto-ban');
Config.AutoBan = checkbox.checked;
}
function SetAutoBanEvent(){
const checkbox = document.getElementById('tco-input-checkbox-auto-ban');
checkbox.addEventListener('click', ToggleAutoBan);
}
function AddBannedCount(){
bannedCount++;
document.getElementById("tco-banned-count").innerHTML = bannedCount + "個のゴミを非表示にしました";
}
//チャット要素のメッセージ内容を取得し、HTML化して返す。
function GetChatMessage(chat){
const messageContainer = document.getElementsByClassName(ClassName.ChatMessageContainer())[0];
//console.log(messageContainer);
}
//チャット要素のユーザー情報を取得し返す
function GetUserInfo(chat){
//console.log(chat.getElementsByClassName(ClassName.DisplayName)[0]);
const _name = chat.getElementsByClassName(ClassName.DisplayName)[0].textContent;
const _id = chat.getElementsByClassName(ClassName.DisplayName)[0].getAttribute("data-a-user");
//console.log(_name);
//console.log(_id);
const result = {
name: _name,
id: _id
};
return result;
}
//チャット要素の子要素を属性値で絞り、結果をArrayで返します。
function GetChildElementsByAttribute(element, attribute, value){
let result = [];
element.childNodes.forEach((e) => {
if(e.getAttribute(attribute) == value){
result.push(e);
}
});
return result;
}
function log(text){
console.log("【TCO】"+text);
}
})();