// ==UserScript==
// @name DoubanPostEmotionDetection
// @name:zh-cn 利用 ChatGPT 对豆瓣帖子进行情绪分类过滤
// @namespace https://github.com/JimSunJing
// @version 0.1.1
// @description Identify and filter post emotion using ChatGPT. Target page: www.douban.com/group/
// @description:zh-cn 保存豆瓣广播内容到本地. 需要打开 douban.com/mine/statuses
// @author JimSunJing
// @include https://www.douban.com/group/*
// @exclude https://www.douban.com/group/*/topic
// @exclude https://www.douban.com/group/search*
// @exclude https://www.douban.com/group/topic/*
// @require https://unpkg.com/axios/dist/axios.min.js
// @license MIT
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(() => {
"use strict";
// ask user to set openAI api key
const requireApiKey = () => {
// use GM_setValue to store api key
let secret = prompt("请输入你的 Open AI api 密匙");
if (secret) {
GM_setValue("OPENAI_API_KEY", secret);
return true;
} else {
console.log("no input.");
return false;
}
};
// store posts
class Data {
constructor() {
this.postStore = [];
this.checkbox = [];
this.analyseButton;
this.analyseFlag = false;
}
addPost(posts) {
this.postStore = this.postStore.concat(posts);
}
getPostStore() {
return this.postStore;
}
clearPost() {
this.postStore = [];
}
analysed() {
return this.analyseFlag;
}
addCheckbox(checkbox) {
this.checkbox.push(checkbox);
}
resetCheckbox() {
this.checkbox.forEach((element) => {
element.checked = true;
});
}
addAnalyseResult(result) {
const emotionMap = new Map();
result.map((post) => {
emotionMap.set(post.id, post.emotion);
});
this.postStore = this.postStore.map((post) => {
return {
...post,
emotion: emotionMap.has(post.id)
? emotionMap.get(post.id)
: "neutral",
};
});
this.analyseFlag = true;
this.resetCheckbox();
console.log("addAnalyseResult update:", this.postStore);
}
setAnalyseButton(button) {
this.analyseButton = button;
}
toggleAnalyseButton(disabled) {
if (disabled === true) {
this.analyseButton.disabled = true;
} else {
this.analyseButton.disabled = false;
}
}
}
const current = new Data();
// extract douban group post title list from current page
const getPosts = () => {
if (current.getPostStore().length > 0) {
console.log("already parsed posts");
return;
}
// get all elements with class name 'td-subject'
let tdSubjects = document.querySelectorAll(".td-subject");
// 进入小组后 class 改变(why??)
if (tdSubjects.length === 0) {
tdSubjects = document.querySelectorAll("td.title");
}
// convert nodelist to array
const posts = [];
tdSubjects.forEach((tdSubject) => {
posts.push({
id: tdSubject.querySelector("a").getAttribute("href").split("/")[5],
title: tdSubject.querySelector("a").textContent.trim(),
href: tdSubject.querySelector("a").getAttribute("href"),
});
});
current.clearPost();
current.addPost(posts);
// console.log("posts", current.getPostStore());
};
// load init button to Page HTML
// inject style
const injectStyle = () => {
const style = document.createElement("style");
style.innerHTML = `
.greasy-button {
background-color: #f8fafc;
color: black;
padding: 3px 6px;
margin: 3px;
border-radius: 7px;
border: none;
}
.newContainer {
width: 90%;
height: 26px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: #f0f9ff;
padding: 10px;
margin: 10px 10px;
border-radius: 10px;
font-size: 14px;
}
.newInput {
margin: 3px 0px;
padding: 3px;
width: 70%;
border: 1px solid;
}
.newInput:hover {
outline: none
}
`;
document.head.appendChild(style);
};
// create button
const createBtn = (text = "") => {
let btn = document.createElement("button");
btn.classList.add("greasy-button");
btn.innerText = text;
return btn;
};
// create checkbox
const createEmotionCheckBox = (container) => {
let checkboxLabels = ["积极", "中立", "负面"];
let checkboxNames = ["positive", "neutral", "negative"];
for (let i = 0; i < checkboxLabels.length; i++) {
let label = document.createElement("label");
let checkbox = document.createElement("input");
checkbox.classList.add("emotion-checkbox");
checkbox.type = "checkbox";
checkbox.name = checkboxNames[i];
checkbox.checked = true;
current.addCheckbox(checkbox);
label.appendChild(checkbox);
label.appendChild(document.createTextNode(" " + checkboxLabels[i]));
container.appendChild(label);
checkbox.addEventListener("change", function () {
if (checkbox.checked) {
console.log(checkbox.name, "is checked");
showPost(checkbox.name);
} else {
console.log(checkbox.name, "is not checked");
hidePost(checkbox.name);
}
});
}
};
const addScriptBtn = () => {
// 在 我的小组讨论 下添加按钮
const aside = document.querySelector(".aside");
// 添加一个控制新增按钮的div
const newContainer = document.createElement("div");
newContainer.classList.add("newContainer");
aside.insertBefore(newContainer, aside.firstChild);
// 要求 ChatGPT 分析情绪按钮
const analyse = createBtn("AI分析");
analyse.addEventListener("click", analyseTitles);
current.setAnalyseButton(analyse);
newContainer.appendChild(analyse);
// 用户输入 API KEY 按钮
const changeAPIKEY = createBtn("输入API密匙");
changeAPIKEY.addEventListener("click", requireApiKey);
newContainer.appendChild(changeAPIKEY);
// 情绪分类选择 checkbox
const checkboxWrap = document.createElement("div");
checkboxWrap.classList.add("newContainer");
createEmotionCheckBox(checkboxWrap);
newContainer.insertAdjacentElement("afterend", checkboxWrap);
// notification
const noteWrap = document.createElement("div");
noteWrap.classList.add("newContainer");
noteWrap.style.justifyContent = "center";
const notification = document.createElement("p");
notification.id = "scriptNotification";
notification.innerText = "豆瓣帖子情绪过滤";
noteWrap.appendChild(notification);
checkboxWrap.insertAdjacentElement("afterend", noteWrap);
};
// notification message
const sendNotification = (msg) => {
document.getElementById("scriptNotification").innerText = msg;
};
// create api connect session using axios
const connectOpenAISession = () => {
if (!GM_getValue("OPENAI_API_KEY")) {
requireApiKey();
return;
}
const apiKey = GM_getValue("OPENAI_API_KEY");
return axios.create({
baseURL: "https://api.openai.com/v1/",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
});
};
// analyse with ChatGPT
const analyseTitles = async () => {
// get post titles
getPosts();
let prompt = `I want you to modify a JSON object array. You need to detect and classify each object's title text emotion, classify them into 3 types: 'positive', 'neutral', 'negative'. You need to add your classification result into the JSON object array as 'emotion' property, do not change other property. And then you need to only reply this JSON object array, when you reply you need to remove the title property. Only reply JSON object arrat, do not reply other text. Here are the JSON object array I provide: `;
const titleJson = JSON.stringify(
current.getPostStore().map((post, index) => ({
title: post.title,
id: post.id,
}))
);
prompt += titleJson;
console.log("prompt:", prompt);
// connect to open AI api
const openaiAxios = connectOpenAISession();
if (!openaiAxios) {
console.log("analyseTitles aborted");
}
// run title emotion analyse
sendNotification("AI正在分析...");
current.toggleAnalyseButton(true);
openaiAxios
.post("chat/completions", {
model: "gpt-3.5-turbo",
messages: [{ role: "user", content: prompt }],
temperature: 0.2,
})
.then((response) => {
// console.log(response.data);
current.toggleAnalyseButton(false);
const data = response.data.choices[0].message.content;
// regex for JSON array
const regex = /\[[^\]]*\{[^}]*\}[^\]]*\]/;
// Use the RegExp 'exec' method to extract the JSON object array
const match = regex.exec(data);
if (match !== null) {
const jsonArray = JSON.parse(match[0]);
// console.log("parse result array:", jsonArray);
current.addAnalyseResult(jsonArray);
} else {
throw new Error("No JSON object array found in the string.");
}
// save results, push finished notificatio to HTML
sendNotification("AI分析完毕!");
})
.catch((error) => {
console.error(error);
sendNotification("错误: " + error.message);
});
};
// hide unselected post
const hidePost = (emotion) => {
if (!current.analyseFlag) {
sendNotification("AI 还没分析.");
return;
}
try {
current
.getPostStore()
.filter((post) => post.emotion === emotion)
.map((post) => {
let postElement = document.querySelector(`a[href="${post.href}"]`)
.parentNode.parentNode;
if (postElement && !postElement.hasAttribute("hidden")) {
postElement.setAttribute("hidden", true);
}
});
} catch (error) {
console.log(error);
sendNotification("错误:", error);
}
};
// show post
const showPost = (emotion) => {
if (!current.analyseFlag) {
sendNotification("AI 还没分析.");
return;
}
try {
current
.getPostStore()
.filter((post) => post.emotion === emotion)
.map((post) => {
let postElement = document.querySelector(`a[href="${post.href}"]`)
.parentNode.parentNode;
if (postElement && postElement.hasAttribute("hidden")) {
postElement.removeAttribute("hidden");
}
});
} catch (error) {
console.log(error);
sendNotification("错误:", error);
}
};
const init = () => {
console.log("Hello, Douban Post Emotion Detection");
injectStyle();
addScriptBtn();
};
init();
})();