NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
"use strict"; // ==UserScript== // @name DrrrUtil.js // @namespace https://github.com/rbtms/utils/tree/master/drrr_util // @version 0.4.1 // @description Multiple utilities for Drrr Chat // @author tounyuu // @homepageURL https://github.com/rbtms/utils/blob/master/drrr_util // @supportURL https://openuserjs.org/scripts/rbtms/DrrrUtil.js/issues // @icon http://drrrkari.com/css/icon_girl.png // @match http://drrrkari.com/room/ // @license GPL-3.0-only // @grant GM_notification // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant window.focus // ==/UserScript== /** * TODO * * - User update is not updated each request * - Set number of visible messages * - on_send doesn't change the bubble element text * - Think about some way of evading automatic disconnection * - Hide admin-related buttons from user menu when you are not admin * - Save a list of ips and their respective id? * - Configure theme select events * **/ var DrrrUtil; (function (DrrrUtil) { 'use strict'; /** * Global variables **/ let ROOM; let CONFIG; /** * Constants **/ const CSS_URL = Object.freeze({ tooltip: 'https://raw.githubusercontent.com/robotoms/utils/master/drrr_util/css/tooltip.css', greyscale: 'https://raw.githubusercontent.com/robotoms/utils/master/drrr_util/css/greyscale.css' }); /** * Classes **/ class Config { constructor() { if (!this.is_value('is_hover_menu')) { this.save_default(); } this.is_hover_menu = this.get_value('is_hover_menu'); this.is_autoban = this.get_value('is_autoban'); this.is_notify = this.get_value('is_notify'); this.is_talk_info = this.get_value('is_talk_info'); this.is_update_unread = this.get_value('is_update_unread'); this.is_modify_send = this.get_value('is_modify_send'); this.is_avoid_disconnection = this.get_value('is_avoid_disconnection'); this.theme = this.get_value('theme'); this.notify_triggers = this.get_value('notify_triggers'); this.autoban = this.get_value('autoban'); } is_value(key) { const value = GM_getValue(key); return value !== undefined; } get_value(key) { const value = GM_getValue(key); switch (value === undefined) { case true: throw Error('Proprety isn\'t stored: ' + key); default: return value; } } set_value(key, value) { GM_setValue(key, value); } set_data(json) { Object.keys(json).forEach((key) => { switch (this[key] === undefined) { case true: throw Error('Non-existent property: ' + key); default: this[key] = json[key]; } }); } save_default() { this.set_value('is_hover_menu', true); this.set_value('is_autoban', true); this.set_value('is_notify', true); this.set_value('is_talk_info', true); this.set_value('is_update_unread', true); this.set_value('is_modify_send', false); this.set_value('is_avoid_disconnection', false); this.set_value('theme', 'default'); this.set_value('notify_triggers', ['notifyme', '半角コンマで分別', 'こういう風に']); this.set_value('autoban', { kick: { msg: ['kickme', 'dontkickme', 'pleasedont'], name: ['getkicked'], ip: ['abcdefgh'] }, ban: { msg: ['banme'], name: ['getbanned'], ip: ['hgfedcba'] } }); } save() { this.set_value('is_hover_menu', this.is_hover_menu); this.set_value('is_autoban', this.is_autoban); this.set_value('is_notify', this.is_notify); this.set_value('is_talk_info', this.is_talk_info); this.set_value('is_update_unread', this.is_update_unread); this.set_value('is_modify_send', this.is_modify_send); this.set_value('is_avoid_disconnection', this.is_avoid_disconnection); this.set_value('theme', this.theme); this.set_value('notify_triggers', this.notify_triggers); this.set_value('autoban', this.autoban); } } class Room { constructor() { this.host = ''; this.talks = {}; this.users = {}; this.unread = 0; this.flags = { HAS_LOADED: false }; this.msg_field = $('[name=message]'); } // Hook outcoming requests hook_send(callback) { const _send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function (body) { const _body = callback(body); _send.call(this, _body); }; } // Hook completed requests hook_response(callback) { $.ajaxSetup({ 'complete': (xhr) => //(xhr, status) callback(xhr.responseJSON) }); } // Getters / Setters set_host(id) { this.host = id; } get_host() { return this.host; } has_talk(id) { return this.talks[id] !== undefined; } register_talk(talk) { this.talks[talk.id] = talk; } talk(id) { return this.talks[id]; } has_user(id) { return this.users[id] !== undefined; } register_user(user) { this.users[user.id] = user; } unregister_user(user) { delete this.users[user.id]; //// } user(id) { return this.users[id]; } user_with_name(name) { const users = this.users; const user_id = Object.keys(this.users).find((id) => name === users[id].name); switch (user_id === undefined) { case true: return undefined; default: return users[user_id]; } } increment_unread() { this.unread++; this.update_title(this.unread); } reset_unread() { this.unread = 0; this.update_title(this.unread); } is_flag(flag) { return this.flags[flag]; } set_flag(flag) { this.flags[flag] = true; } is_tab_hidden() { return document.hidden; //return (document.hidden || !!document.webkitHidden || document.msHidden); } update_title(n) { const room = this.room_name(); switch (n === 0) { case true: { console.log('RESET N'); document.title = room; break; } default: { console.log('UPDATE N', n); document.title = `${room} (${n})`; } } } // Own name as displayed inside the room own_name() { return $('.profname').text(); } im_host() { return this.own_id() === this.host; } // Own id own_id() { const name = this.own_name(); const users = this.users; const index = Object.keys(this.users).find((id) => users[id].name === name); switch (index !== undefined) { case true: return users[index].id; default: throw Error('User not found.'); } } room_name() { return $('#room_name').text().split(' ')[0]; } user_n() { const n_str = $('#room_name').text().split(' ')[1]; return n_str.substr(1, n_str.length - 2).split('/') .map((n) => parseInt(n)); } // Send an ajax post request post(json) { const url = 'http://drrrkari.com/room/?ajax=1'; const attr = Object.assign({ valid: 1 }, json); $.post(url, attr); /* .done ( (data) => console.log('Message success:', data) ) .fail ( (err) => console.error('Couldn\'t send message:', err) ) .always( () => console.log('Message sent:', msg) ); */ } // Send a message send_message(msg) { this.post({ message: msg }); } send_pm(msg, id) { this.post({ id: id, message: msg }); } change_user_limit(n) { this.post({ room_limit: n }); } // Inject a link element with the given url inject_css(url) { /* const style = $(document.createElement('LINK')) .attr('rel', 'stylesheet') .attr('type', 'text/css') .attr('href', url); console.log('INJECT CSS', url, style); $('head').append(style); */ $.ajax({ type: 'GET', url, success: function(data) { const style = document.createElement('STYLE'); style.innerHTML = data; $('head').append(style); /* var style = $(data).filter("style").get(0); document.getElementsByTagName("head")[0].appendChild(style); $("#header").html($(data).filter('#header').html()); $("#footer").html($(data).filter('#footer').html()); */ } }); } // Load a css in the CSS_URL constant set_css(theme) { console.log('SET CSS', theme); this.inject_css(CSS_URL[theme]); } // Get message field get_msg_field() { return this.msg_field.val(); } // Set message field add_msg_field(str) { this.msg_field.val(this.get_msg_field() + str); } focus_msg_field() { const textarea = $('#message textarea')[0]; const pos = 1000; // Arbitrary number textarea.focus(); textarea.selectionStart = pos; textarea.selectionEnd = pos; } // Convert epoch timestamps to locale time epoch_to_time(time) { const s = 1000; return (new Date(time * s)) .toLocaleTimeString(); } // Send a notification (untested on chrome) send_notification(options) { GM_notification(options); } // Send a private message to oneself every m minutes to stay alive avoid_disconnection(m) { const ms = 1000; const s = 60; const time = m * s * ms; setInterval(() => ROOM.send_pm('test', ROOM.own_id()), time); } config_textarea(label, id, data) { return $(document.createElement('DIV')).append($(document.createElement('LABEL')).attr('for', id).text(label), $(document.createElement('INPUT')).attr('id', id).val(data.join(',')).css({ 'width': '400px', 'height': '20px', 'padding-left': '5px', 'margin-left': '20px' })); } parse_textarea(line) { return line.split(/\s*,\s*/); } toggle_config_menu() { $('.submit input[name=post]').slideToggle(); // Post button $('#message textarea').slideToggle(); // Message field $('.userprof').slideToggle(); // User picture/name $('#config_menu').slideToggle(); // Configuration div } append_config() { const { is_notify, is_autoban } = CONFIG; const icon_url = 'https://i.imgsafe.org/9f/9f4ad930a2.png'; const hr_el = $(document.createElement('HR')) .css({ 'margin-top': '10px', 'margin-bottom': '10px' }); const config_div = $(document.createElement('DIV')) .attr('id', 'config_menu') .addClass('pannel hide') .append('<br>'); const notify_div = $(document.createElement('DIV')) .attr('id', 'notify_trigger_div') .addClass('pannel hide') .css({ 'margin-left': '50px', 'margin-top': '8px', 'margin-bottom': '5px' }) .append(this.config_textarea('通知トリガー', 'notify_triggers', CONFIG.notify_triggers)); const autoban_div = $(document.createElement('DIV')) .attr('id', 'autoban_div') .addClass('pannel hide') .css({ 'margin-left': '50px', 'margin-top': '8px', 'margin-bottom': '5px' }) .append($(document.createElement('SPAN')).text('キック'), this.config_textarea('名前', 'kick_name', CONFIG.autoban.kick.name), this.config_textarea('単語', 'kick_msg', CONFIG.autoban.kick.msg), this.config_textarea('IP', 'kick_ip', CONFIG.autoban.kick.ip), $(document.createElement('SPAN')).text('BAN'), this.config_textarea('名前', 'ban_name', CONFIG.autoban.ban.name), this.config_textarea('単語', 'ban_msg', CONFIG.autoban.ban.msg), this.config_textarea('IP', 'ban_ip', CONFIG.autoban.ban.ip)); const autoban_el = $(document.createElement('DIV')).append($(document.createElement('LABEL')).attr('for', 'is_autoban').text('自動キック'), $(document.createElement('INPUT')).css('margin-left', '10px') .attr({ type: 'checkbox', id: 'is_autoban', checked: is_autoban }), $(document.createElement('BUTTON')).text('設定') .css({ 'margin-left': '10px', 'margin-down': '5px', 'width': '40px' }) .on('click', () => autoban_div.slideToggle())); const notify_el = $(document.createElement('DIV')).append($(document.createElement('LABEL')).attr('for', 'is_notify').text('通知'), $(document.createElement('INPUT')).css('margin-left', '10px') .attr({ type: 'checkbox', id: 'is_notify', checked: is_notify }), $(document.createElement('BUTTON')).text('設定') .css({ 'margin-left': '10px', 'width': '40px' }) .on('click', () => notify_div.slideToggle())); const theme_el = $(document.createElement('DIV')).append($(document.createElement('LABEL')).attr('for', 'theme_select').text('テーマ'), $(document.createElement('SELECT')).attr('id', 'theme_select').css('margin-left', '10px').append($(document.createElement('OPTION')).text('デフォルト').val('default'), $(document.createElement('OPTION')).text('白黒').val('greyscale').on('click', () => ROOM.set_css('greyscale')))).css('padding-top', '5px'); const button_div = $(document.createElement('DIV')).append( // Save configuration $(document.createElement('BUTTON')) .text('保存') .css('width', '60px') .on('click', () => { CONFIG.set_data({ is_autoban: $('#is_autoban').prop('checked'), is_notify: $('#is_notify').prop('checked'), notify_triggers: this.parse_textarea($('#notify_triggers').val()), autoban: { kick: { name: this.parse_textarea($('#kick_name').val()), msg: this.parse_textarea($('#kick_msg').val()), ip: this.parse_textarea($('#kick_ip').val()) }, ban: { name: this.parse_textarea($('#ban_name').val()), msg: this.parse_textarea($('#ban_msg').val()), ip: this.parse_textarea($('#ban_ip').val()) } } }); CONFIG.save(); this.toggle_config_menu(); }), // Restore default configuration $(document.createElement('BUTTON')) .text('元設定に戻す') .css({ 'width': '110px', 'margin-left': '10px' }) .on('click', () => { CONFIG.save_default(); this.toggle_config_menu(); location.reload(); })); const icon = $(document.createElement('LI')).append($(document.createElement('IMG')).attr('src', icon_url)).on('click', this.toggle_config_menu); config_div.append(autoban_el, autoban_div, notify_el, notify_div, theme_el, hr_el, button_div, '<br>'); $('.message_box_inner').append(config_div); $('.menu li:eq(3)').after(icon); } } class JSONUtil { constructor(json) { this.json = json; } get_host() { return this.json.host; } // Format and get new talks new_talks() { const talks = this.json.talks; const new_talks = []; for (let i = 0; i < talks.length; i++) { const talk = new Talk(talks[i]); if (!talk.is_registered()) { new_talks.push(talk); } } return new_talks; } // Format and get new users new_users() { const users = this.json.users; const new_users = []; for (let i = 0; i < users.length; i++) { const user = new User(users[i]); if (!user.is_registered()) { new_users.push(user); } } return new_users; } } class Talk { constructor(talk) { const icon = talk.icon || 'girl'; const id = talk.id; this.uid = talk.uid; this.name = talk.name; this.message = talk.message; this.icon = talk.icon; this.time = talk.time; this.icon_el = $( $('#' + talk.id).children()[0] ); } // Check if the message contains user's name has_own_name() { return !!this.message.match(ROOM.own_name()); } // Check if the talk has been posted by the user is_me() { return this.uid === ROOM.own_id(); } // Check if the talk has been registered in the room is_registered() { return ROOM.has_talk(this.id); } // Register the talk in the room register() { ROOM.register_talk(this); } // Check if the talk's message contains a trigger of CONFIG.notify_triggers has_trigger() { const msg = this.message; return CONFIG.notify_triggers.some((trigger) => { const regex = new RegExp(trigger, 'i'); return regex.test(msg); }); } // Match the message against a list of words msg_matches(list) { const msg = this.message; return list.some((str) => { const regex = new RegExp(str, 'i'); return regex.test(msg); }); } // Search for a match for notify() try_notify() { if (!this.is_me() && this.has_trigger()) { this.notify(); } } // Notify the talk notify() { const icon = this.icon; const title = this.name; const msg = this.message; const icon_url = `http://drrrkari.com/css/icon_${icon}.png`; const options = { title: title, image: icon_url, highlight: true, text: msg, timeout: 5000, onclick: window.focus }; ROOM.send_notification(options); } // Log talk's info print_info() { console.log(); console.log(this.message); console.log('ID', this.id); console.log('UID', this.uid); console.log('TIME', ROOM.epoch_to_time(this.time)); console.log(); } // append_hover_menu() helper tooltip_header(text) { return $(document.createElement('DIV')) .addClass('talk_tooltip_header') .append($(document.createElement('SPAN')) .addClass('talk_tooltip_text') .addClass('noselect') .text(text)); } // append_hover_menu() helper tooltip_btn(text) { return $(document.createElement('BUTTON')) .addClass('talk_tooltip_btn') .text(text); } // Append user menu to the talk icon append_hover_menu() { const name = this.name; const time = this.time; const uid = this.uid; const prop_len = 10; const tooltip = $(document.createElement('DIV')) .addClass('talk_tooltip') .append(this.tooltip_header('ユーザーメニュ'), $(document.createElement('DIV')) .addClass('talk_tooltip_btn_div') .append(this.tooltip_btn('投稿時間: ' + ROOM.epoch_to_time(time)), this.tooltip_btn('UID: ' + (uid.substr(0, prop_len) || 'null')).on('click', (e) => { // copy ip to message box ROOM.add_msg_field(this.uid || 'null'); ROOM.focus_msg_field(); e.preventDefault(); e.stopPropagation(); }), this.tooltip_btn('内緒モード').on('click', (e) => { // Click on the target user $(`#user_list2 > li[name=${this.uid}]`).trigger('click'); // Open private window $('[name=pmbtn]').trigger('click'); e.preventDefault(); e.stopPropagation(); }), this.tooltip_btn('無視').on('click', (e) => { const user = ROOM.user(uid); if (user) { user.ignore(); } e.preventDefault(); e.stopPropagation(); }), this.tooltip_btn('キック').on('click', (e) => { const user = ROOM.user(uid); if (user) { user.kick(); } e.preventDefault(); e.stopPropagation(); }), this.tooltip_btn('バン').on('click', (e) => { const user = ROOM.user(uid); if (user) { user.ban(); } e.preventDefault(); e.stopPropagation(); }))); this.icon_el.on('click', () => { ROOM.add_msg_field(ROOM.get_msg_field() === '' ? `@${name} ` : ` @${name}`); ROOM.focus_msg_field(); }); this.icon_el.append(tooltip); } try_autoban() { if (ROOM.im_host()) { const kick_list = CONFIG.autoban.kick; const ban_list = CONFIG.autoban.ban; // By ip if (this.user_id_matches(kick_list.ip)) { const user = ROOM.user(this.id); if (user) { user.kick(); } } else if (this.user_id_matches(ban_list.ip)) { const user = ROOM.user(this.id); if (user) { user.ban(); } } // By message else if (this.msg_matches(kick_list.msg)) { const user = ROOM.user(this.id); if (user) { user.kick(); } } else if (this.msg_matches(ban_list.msg)) { const user = ROOM.user(this.id); if (user) { user.ban(); } } // Didn't succeed else { return false; } return true; } else { return false; } } } class User { constructor(user) { this.name = user.name; this.id = user.id; this.icon = user.icon; this.trip = user.trip; this.update = user.update; } // Check if it's registered in the room is_registered() { return ROOM.has_user(this.id); } // Register the user in the room register() { ROOM.register_user(this); } // Match user's name against a list of words name_matches(list) { const name = this.name; return list.some((str) => { const regex = new RegExp(str, 'i'); return regex.test(name); }); } // Match the user ip against a list of words user_id_matches(list) { const id = this.id; switch (id === '') { case true: return false; default: return list.some((_id) => _id === id); } } // Hide the talks from that user ignore() { alert('未実装!'); } // Kick the user from the room (owner mode) kick() { ROOM.post({ ban_user: this.id }); } // Ban the user from the room (owner mode) ban() { ROOM.post({ ban_user: this.id, block: 1 }); } // Automatically kick or ban a user given the keywords on CONFIG.autoban try_autoban() { if (ROOM.im_host()) { const kick_list = CONFIG.autoban.kick; const ban_list = CONFIG.autoban.ban; // By ip if (this.user_id_matches(kick_list.ip)) { this.kick(); } else if (this.user_id_matches(ban_list.ip)) { this.ban(); } // By name else if (this.name_matches(kick_list.name)) { this.kick(); } else if (this.name_matches(ban_list.name)) { this.ban(); } else { return false; } return true; } else { return false; } } } /** * Functions **/ function parse_send(body) { return body.split('&').map((pairs) => pairs.split('=')); } function join_send(parts) { return parts.map((pair) => pair.join('=')).join('&'); } /** * Handlers **/ // Triggered before send function on_send(body) { switch (body === null) { case true: return null; default: { console.log('SEND', body); switch (CONFIG.is_modify_send) { case true: { const parts = parse_send(body); const msg_pair = parts.find((arr) => arr[0] === 'message'); switch (msg_pair === undefined) { case true: return body; default: { // Modify msg let msg = decodeURI(msg_pair[1]).trim(); msg = 'abcd'; msg_pair[1] = encodeURI(msg + '\r\n'); return join_send(parts); } } } default: return body; } } } } // Triggered on request completion function on_response(json_room) { console.log('RESPONSE', json_room); //console.log(ROOM); const json = new JSONUtil(json_room); // Register new entries ROOM.set_host(json.get_host()); const users = json.new_users(); users.forEach((user) => user.register()); const talks = json.new_talks(); talks.forEach((talk) => talk.register()); // Send to handlers if (ROOM.is_flag('HAS_LOADED')) { if (users.length !== 0) { users.forEach((user) => { if (CONFIG.is_autoban) { user.try_autoban(); } handle_users(user); }); } if (talks.length !== 0) { talks.forEach((talk) => { if (CONFIG.is_notify) { talk.try_notify(); } if (talk.uid === '0') { handle_system_msg(talk.message); } else { if (CONFIG.is_autoban) { talk.try_autoban(); } if (CONFIG.is_hover_menu) { talk.append_hover_menu(); } if (CONFIG.is_talk_info) { talk.print_info(); } if (CONFIG.is_update_unread && ROOM.is_tab_hidden()) { console.log('NEW MSG!'); ROOM.increment_unread(); } else if (CONFIG.is_update_unread) { ROOM.reset_unread(); } handle_talks(talk); } }); } } else { // Initialization console.log('INITIALIZATION', talks, users); talks.forEach((talk) => { if (CONFIG.is_hover_menu) { talk.append_hover_menu(); } }); ROOM.set_flag('HAS_LOADED'); } } // Handle system messages function handle_system_msg(msg) { const hyphen_end = 3; const [name, event] = msg.substr(hyphen_end).split('さん'); console.log('SYSTEM', name, event); switch (event) { case 'が入室しました': break; case 'が退室しました': case 'の接続が切れました': { const user = ROOM.user_with_name(name); if (user) { ROOM.unregister_user(user); } break; } default: throw Error(`Unknown event: ${name} ${event}`); } } // Handle new talks function handle_talks(talk) { console.log('TALK', talk); } // Handle new users function handle_users(user) { console.log('USER', user); //user.kick(); } function main() { CONFIG = new Config(); ROOM = new Room(); // Hooks ROOM.hook_send(on_send); ROOM.hook_response(on_response); // Avoid disconnection if (CONFIG.is_avoid_disconnection) { const s = 10; ROOM.avoid_disconnection(s); } // CSS if (CONFIG.theme !== 'default') { ROOM.set_css(CONFIG.theme); } ROOM.set_css('tooltip'); // Configuration ROOM.append_config(); // Unread update ROOM.update_title(0); document.onfocus = ROOM.reset_unread; console.log('LOAD END'); } main(); })(DrrrUtil || (DrrrUtil = {}));