// ==UserScript==
// @name Amino Chat Grabber
// @namespace http://tampermonkey.net/
// @version 1.6
// @description A utility to grab and compile chat histories, for parsing, archiving or viewing in an accompanying WIP chat history viewer.
// @author Rasutei
// @match https://aminoapps.com
// @icon https://www.google.com/s2/favicons?sz=64&domain=aminoapps.com
// @grant none
// @license GNU GPLv3
// ==/UserScript==
/* eslint-disable curly */
const logcss = `
font-family: Hack, monospace;
text-shadow: 0 0 10px black, 0 0 10px black;
background: linear-gradient(to right, #4d94ff 0%, #4d94ff 8px, rgb(77 148 255 / 30%) 8px, transparent 50px);
color: #4d94ff;
padding: 2px 0 2px 30px;
`
const warncss = `
font-family: Hack, monospace;
text-shadow: 0 0 10px black, 0 0 10px black;
background: linear-gradient(to right, #ffa621 0%, #ffa621 0% 8px, rgb(255 166 33 / 30%) 8px, transparent 50px);
color: #ffa621;
padding: 2px 0 2px 30px;
`
const log = (e) => console.log('%c'+e, logcss)
const warn = (e) => console.log('%c'+e, warncss)
const warn_element = (e) => console.log('%c%o', warncss, e)
function Community(name){
this.name = name
this.link = ''
this.icon = ''
this.chats = []
}
function Entry(username){
this.username = username
this.link = ''
this.avatar = ''
this.cover = ''
this.oldest_timestamp = ''
this.history = []
}
function Message(){
this.type = ''
this.user = ''
this.content = ''
}
let chat
let msglist
let rb = 0
window.addEventListener('load', function(){
log('Chat Grabber: Loading...')
let hackfont = document.createElement('link')
hackfont.setAttribute('rel','stylesheet')
hackfont.setAttribute('type','text/css')
hackfont.setAttribute('href','//cdn.jsdelivr.net/npm/[email protected]/build/web/hack-subset.css')
document.head.appendChild(hackfont);
let btnCSS = `
box-sizing: border-box;
width: 260px;
background: rgb(10 10 10);
border: solid 2px hsl(var(--rainbow), 100%, 50%);
color: hsl(var(--rainbow), 100%, 90%);
font-family: Hack;
font-size: 14px;
margin-top: -2px;
cursor: pointer;
opacity: 0;
display: none;`
let wrapper = document.createElement("wrapper")
wrapper.className = "master-wrapper"
wrapper.style.cssText = `
position:fixed;
top:0;
left:0;
z-index:99999999;
display:flex;
flex-direction:column;`
wrapper.style.setProperty('--rainbow', '0deg')
let alwayson = document.createElement("div")
alwayson.style.order = 0
wrapper.appendChild(alwayson)
let toggle = document.createElement("button")
toggle.className = "ras_button toggle nohide"
toggle.innerText = '▣'
toggle.style.cssText = btnCSS
toggle.style.fontSize = '16px'
toggle.style.display = 'block'
toggle.style.float = 'left'
toggle.style.opacity = 1
// toggle.style.order = 0
toggle.style.width = '21px'
toggle.style.height = '21px'
toggle.style.padding = 0
toggle.style.margin = 0
toggle.onclick = function(){
wrapper.querySelectorAll(".ras_button").forEach(m => {
if (!m.classList.contains('nohide')){
m.style.opacity = +!+(m.style.opacity)
if (m.style.display != 'none') m.style.display = 'none'
else m.style.display = 'inline-block'
}
})
}
alwayson.appendChild(toggle)
let title = document.createElement("button")
title.className = "ras_button title nohide"
title.innerText = 'Chat Grabber 1.6, by Rasutei'
title.style.cssText = btnCSS
title.style.display = 'block'
title.style.opacity = 1
// title.style.order = 1
title.style.height = '21px'
title.style.width = '241px'
title.style.float = 'left'
title.style.padding = 0
title.style.margin = '0 0 0 -2px'
alwayson.appendChild(title)
let genBase = document.createElement("button")
genBase.className = "ras_button gen-base"
genBase.innerText = 'Generate base JSON structure'
genBase.onclick = GenBase
genBase.style.cssText = btnCSS
genBase.style.order = 2
genBase.style.opacity = 0
wrapper.appendChild(genBase)
let cmpCommunity = document.createElement("button")
cmpCommunity.className = "ras_button gen-community"
cmpCommunity.innerText = 'GenJSON cur. comm'
cmpCommunity.onclick = GrabCommunity
cmpCommunity.style.cssText = btnCSS
cmpCommunity.style.order = 3
cmpCommunity.style.opacity = 0
wrapper.appendChild(cmpCommunity)
let cmpCurrent = document.createElement("button")
cmpCurrent.className = "ras_button gen-chat"
cmpCurrent.innerText = 'GenJSON cur. chat'
cmpCurrent.onclick = GrabCurrent
cmpCurrent.style.cssText = btnCSS
cmpCurrent.style.order = 4
cmpCurrent.style.opacity = 0
wrapper.appendChild(cmpCurrent)
document.body.insertBefore(wrapper, document.body.firstChild)
setInterval(function(){
wrapper.style.setProperty('--rainbow', rb+++'deg')
if (rb == 360) rb = 0
}, 20)
log('Chat Grabber: Loaded.')
})
function Update(){
chat = document.querySelector("iframe").contentDocument
msglist = chat.querySelector(".message-list")
}
function GenBase(){
let gen =
`{
"gen_version": 1.6,
"cur_version": 1.6,
"communities":[
// Add JSON structures of communities
// here, separated by commas.
// Example structure:
// {
// "name": "",
// "link": "",
// "chats": [
// {
// "username": "",
// "link": "",
// "avatar": "",
// "cover": "",
// "oldest_timestamp": "",
// "history": [
// {
// "type": "", "user": "",
// "content": ""
// },
// [...]
// {
// "type": "", "user": "",
// "content": ""
// }
// ]
// },
// ]
// },
// [...]
// ### IMPORTANT! ###
// DELETE ALL COMMENTS BEFORE USE
// (Comments are lines starting in "//")
]
}
`
console.log(gen)
if(confirm('Done. Would you like to have the resulting JSON string copied to the clipboard?'))
setTimeout(() => navigator.clipboard.writeText(gen), 200)
else alert('JSON string not copied to the clipboard.\nAccess the browser\'s console to view or copy it.')
}
async function GrabCommunity(){
Update()
let entry = new Community(JSONSafe(chat.querySelector(".community-title").textContent.trim()))
/* Grab community name, link and icon */
entry.link = chat.querySelector(".community-title :first-child").href
entry.icon = chat.querySelector(".community-title img.logo").src
/* Post resulting entry */
let gen = JSON.stringify(entry, null, "\t")
console.log(gen)
/* Copying resulting entry to clipboard */
if(confirm('Done. Would you like to have the resulting JSON string copied to the clipboard?'))
setTimeout(() => navigator.clipboard.writeText(gen), 200)
else alert('JSON string not copied to the clipboard.\nAccess the browser\'s console to view or copy it.')
}
async function GrabCurrent(){
Update()
/* Create entry for current chat */
let user = JSONSafe(chat.querySelector(".thread-title").textContent.trim())
let entry = await new Entry(user)
/* Grab images */
chat.querySelector(".user-message:not(.from-me)").querySelector(".message-author.cover-img").click()
await new Promise(r => setTimeout(r, 500));
let profile = chat.querySelector(".user-profile")
if (profile.querySelector(".user-cover .img-cover"))
entry.cover = profile.querySelector(".user-cover .img-cover").src
if (profile.querySelector(".user-link"))
entry.link = profile.querySelector(".user-link").href
if (profile.querySelector(".avatar"))
entry.avatar = profile.querySelector(".avatar").src
/* Scroll chat history up as far as possible */
let message_count = 0;
let prv_msg_count;
while (message_count != prv_msg_count){
prv_msg_count = message_count
// console.log("Scrolling...")
msglist.scrollTo(top)
await new Promise(r => setTimeout(r, 300))
message_count = msglist.childElementCount
}
/* Grab oldest timestamp */
if (chat.querySelector(".timestamp"))
entry.oldest_timestamp = chat.querySelector(".timestamp").textContent.trim()
/* Compile messages */
let messages = Array.from(msglist.children);
messages.forEach(m => {
if (m.classList.contains("user-message")){
/* Create message object */
let msg = new Message()
/* Set message author */
if (m.classList.contains("from-me"))
msg.user = "Me"
else
msg.user = user
/* Set message content */
/* If message is a sticker */
if (m.querySelector(".sticker-message")){
msg.type = "sticker"
msg.content = m.querySelector(".sticker-message").firstChild.src
}
/* If message is an image */
else if (m.querySelector(".img-msg")){
msg.type = "image"
msg.content = m.querySelector(".img-msg").firstChild.src
}
/* If message is a voice message */
else if (m.querySelector(".voice-message-container")){
msg.type = "audio"
msg.content = m.querySelector(".voice-message-container audio").src
}
/* If message is text */
else if (m.querySelector(".text-msg")){
msg.type = "text"
msg.content = JSONSafe(m.querySelector(".text-msg").innerHTML)
/* Replace tags and restore formatting information */
msg.content = msg.content.replaceAll('<p>','')
msg.content = msg.content.replaceAll('</p>','\n')
msg.content = msg.content.replaceAll('<p class="', '[')
msg.content = msg.content.replaceAll('">', ']')
msg.content = msg.content.replaceAll('center', 'C')
msg.content = msg.content.replaceAll('italic', 'I')
msg.content = msg.content.replaceAll('bolder', 'B')
msg.content = msg.content.replaceAll('strike', 'S')
msg.content = msg.content.replaceAll('underline', 'U')
let toreplace = msg.content.substring(msg.content.indexOf('[')+1,msg.content.indexOf(']'))
msg.content = msg.content.replace(toreplace, toreplace.replaceAll(' ', ''))
/* Remove trailing line break */
msg.content = msg.content.substring(0, msg.content.length-1)
/* Try to check if message is meant to be a comment in a scene, or out-of-character */
let rawmsg = msg.content.substring(((msg.content.indexOf(']') == -1)? 0 : msg.content.indexOf(']')+2))
let cmt = rawmsg.startsWith('||') || rawmsg.startsWith('((') || rawmsg.endsWith('||') || rawmsg.endsWith('))')
if (cmt) msg.type += " comment"
}
/* If message is unknown type */
else{
warn("Uncaught message type: "+m.querySelector(".message-content :first-child").className)
console.group("Message")
warn_element(m)
console.groupEnd()
}
entry.history.push(msg)
}
})
/* Post resulting entry */
let gen = JSON.stringify(entry, null, "\t").replaceAll(',\n\t\t\t"user',', "user').replaceAll('},\n\t\t{','},{').replaceAll('\n','\n\t\t\t\t')
console.log(gen)
/* Copying resulting entry to clipboard */
if(confirm('Done. Would you like to have the resulting JSON string copied to the clipboard?'))
setTimeout(() => navigator.clipboard.writeText(gen), 200)
else alert('JSON string not copied to the clipboard. Access the browser\'s console to view or copy it.')
}
function JSONSafe(s){
return s.replaceAll('"', '\"')
}