Delete_My_Comment

批量删除在别人个人资料下的留言

  1. // ==UserScript==
  2. // @name Delete_My_Comment
  3. // @name:zh-CN 批量删除个人资料留言
  4. // @namespace https://blog.chrxw.com
  5. // @version 1.1
  6. // @description 批量删除在别人个人资料下的留言
  7. // @description:zh-CN 批量删除在别人个人资料下的留言
  8. // @author Chr_
  9. // @include /https://steamcommunity\.com/(id|profiles)/[^\/]+/commenthistory/?/
  10. // @license AGPL-3.0
  11. // @icon https://blog.chrxw.com/favicon.ico
  12. // @grant GM_addStyle
  13. // ==/UserScript==
  14.  
  15. (async () => {
  16. 'use strict';
  17.  
  18. let GObjs = {};
  19.  
  20. addGui();
  21.  
  22. //添加UI
  23. function addGui() {
  24. function genBtn(name, foo) {
  25. const b = document.createElement('button');
  26. b.textContent = name;
  27. b.className = 'dmc_btn';
  28. b.addEventListener('click', foo);
  29. return b;
  30. }
  31. function genDiv(cls) {
  32. const d = document.createElement('div');
  33. d.className = cls ?? 'dmc_div';
  34. return d;
  35. }
  36. function genText() {
  37. const t = document.createElement('textarea');
  38. t.className = 'dmc_txt';
  39. return t;
  40. }
  41. function genNum() {
  42. const t = document.createElement('input');
  43. t.type = 'number';
  44. t.value = 0;
  45. t.min = 0;
  46. t.className = 'dmc_ipt';
  47. return t;
  48. }
  49. function genSpan(txt) {
  50. const t = document.createElement('span');
  51. t.textContent = txt;
  52. t.className = 'dmc_span';
  53. return t;
  54. }
  55.  
  56. const rightDiv = document.querySelector("div.rightbox");
  57. const divArea = genDiv('dmc_panel');
  58. const btnStart = genBtn('开始删除', async () => {
  59. const startPage = parseInt(iptPage.value);
  60. if (startPage !== startPage) {
  61. ShowAlertDialog("错误", "只能输入整数");
  62.  
  63. } else {
  64. const endPage = getPageCount();
  65. try {
  66. btnStart.textContent = '执行中';
  67. btnStart.disabled = true;
  68. await startDeleteComments(startPage, endPage);
  69. }
  70. finally {
  71. btnStart.textContent = '开始删除';
  72. btnStart.disabled = false;
  73. }
  74. }
  75. });
  76. const iptPage = genNum();
  77. const txtLog = genText();
  78. const divHide = genDiv('dmc_hide');
  79. rightDiv.appendChild(divArea);
  80. divArea.appendChild(btnStart);
  81. divArea.appendChild(genSpan('开始页码:'));
  82. divArea.appendChild(iptPage);
  83. divArea.appendChild(txtLog);
  84. divArea.appendChild(divHide);
  85. Object.assign(GObjs, { txtLog, divHide });
  86. }
  87.  
  88. //批量删除留言
  89. async function startDeleteComments(startPage, endPage) {
  90. const { divHide, txtLog } = GObjs;
  91. const genTmp = () => {
  92. const d = document.createElement('div');
  93. divHide.appendChild(d);
  94. return d;
  95. };
  96. const log = (msg) => {
  97. if (txtLog.value) {
  98. txtLog.value += "\n";
  99. }
  100. txtLog.value += msg;
  101. txtLog.scrollTop = txtLog.scrollHeight;
  102. };
  103. log(`1 开始运行, 页码设置: ${startPage} / ${endPage}`);
  104. const baseUri = location.origin + location.pathname;
  105.  
  106. for (let i = startPage; i <= endPage; i++) {
  107. log(`2 开始读取第 ${i} 页历史记录`);
  108. const historyUrl = `${baseUri}?p=${i}`;
  109. const response = await loadPage(historyUrl);
  110. if (response) {
  111. const tmp = genTmp();
  112. tmp.innerHTML = response;
  113. const links = fetchCommentFromHistory(tmp);
  114. divHide.removeChild(tmp);
  115.  
  116. const count = links.length;
  117. if (count > 0) {
  118. log(`3 获取了 ${links.length} 条留言记录`);
  119.  
  120. for (let link of links) {
  121. log('4 开始读取留言');
  122. const resp = await loadPage(link);
  123. if (response) {
  124. const ss = genTmp();
  125. ss.innerHTML = resp;
  126. const rst = fetchProfileComments(ss);
  127.  
  128. const tasks = [];
  129. const gids = new Set();
  130.  
  131. const cnt = rst.length;
  132. if (cnt > 0) {
  133. log(`5 获取了 ${cnt} 条留言记录`);
  134.  
  135. for (let [steamid, gid] of rst) {
  136. if (!gids.has(gid)) {
  137. gids.add(gid);
  138. tasks.push(makePromise(steamid, gid));
  139. }
  140. }
  141.  
  142. if (tasks.length > 0) {
  143. log(`5 总计 ${tasks.length} 条留言, 开始删除`);
  144. await Promise.all(tasks);
  145. } else {
  146. log('5 未找到可以删除的留言');
  147. }
  148.  
  149. } else {
  150. log('5 未找到留言记录');
  151. }
  152. divHide.removeChild(ss);
  153. }
  154. else {
  155. log('4 读取失败');
  156. }
  157. }
  158. }
  159. else {
  160. log('3 无留言记录,跳过');
  161. }
  162. } else {
  163. log('2 读取失败');
  164. }
  165. }
  166.  
  167. log('1 运行结束');
  168.  
  169. function makePromise(steamid, gid) {
  170. return new Promise((resolve, reject) => {
  171. deleteComment(steamid, gid)
  172. .then(() => {
  173. log(`> 删除留言 ${gid} 成功`);
  174.  
  175. }).catch((reason) => {
  176. log(`> 删除留言 ${gid} 失败`);
  177.  
  178. }).finally(() => {
  179. resolve();
  180. });
  181. });
  182. }
  183. }
  184.  
  185. //获取总页数
  186. function getPageCount() {
  187. const pages = document.querySelectorAll("div.pageLinks>a.pagelink");
  188. if (pages.length === 0) {
  189. return 0;
  190. } else {
  191. const lastPage = parseInt(pages[pages.length - 1].textContent.replace(/[,.]/g, ""));
  192. return lastPage === lastPage ? lastPage : 0;
  193. }
  194. }
  195.  
  196. //获取历史留言记录
  197. function fetchCommentFromHistory(element) {
  198. const comments = element.querySelectorAll("div.commenthistory_comment:not(.deleted) a");
  199. const matchLinks = new RegExp(/https:\/\/steamcommunity\.com\/(id|profiles)\/[^\/]+\/?\?tscn=\d+$/g);
  200. const result = [];
  201. for (let comment of comments) {
  202. const href = comment.href;
  203. if (matchLinks.test(href)) {
  204. result.push(href);
  205. }
  206. }
  207. return result;
  208. }
  209.  
  210. // 读取网页
  211. async function loadPage(href) {
  212. return new Promise((resolve, reject) => {
  213. fetch(href, {
  214. method: "GET",
  215. credentials: "include",
  216. })
  217. .then(async (response) => {
  218. if (response.ok) {
  219. const data = await response.text();
  220. resolve(data.trim());
  221. } else {
  222. resolve(null);
  223. }
  224. }).catch((err) => {
  225. console.error(err);
  226. resolve(null);
  227. });
  228. });
  229. }
  230.  
  231. // 匹配所有自己的留言
  232. function fetchProfileComments(element) {
  233. const comments = element.querySelectorAll("a.actionlink:not(.report_and_hide)");
  234. const matchLinks = new RegExp(/'(\S+)'/g);
  235. const result = [];
  236. for (let comment of comments) {
  237. const href = comment.href;
  238. const match = href.match(matchLinks);
  239. if (match && match.length >= 2) {
  240. const steamId = match[0].replace("Profile_", "").replace(/'/g, "");
  241. const commentId = match[1].replace(/'/g, "");
  242. result.push([steamId, commentId]);
  243. }
  244. }
  245. return result;
  246. }
  247.  
  248. const SessionId = document.cookie.replace(/(?:(?:^|.*;\s*)sessionid\s*\=\s*([^;]*).*$)|^.*$/, "$1");
  249.  
  250. // 删除指定留言
  251. async function deleteComment(steamId, gidComment) {
  252. const data = {
  253. "gidcomment": gidComment,
  254. "start": 0,
  255. "count": 6,
  256. "sessionid": SessionId,
  257. "feature2": -1,
  258. "lastvisit": 0
  259. };
  260. let s = '';
  261. for (let k in data) {
  262. s += `${k}=${data[k]}&`;
  263. }
  264. return new Promise((resolve, reject) => {
  265. fetch(`https://steamcommunity.com/comment/Profile/delete/${steamId}/-1/`, {
  266. method: "POST",
  267. credentials: "include",
  268. body: s,
  269. headers: {
  270. 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
  271. },
  272. })
  273. .then(async (response) => {
  274. if (response.ok) {
  275. const data = await response.json();
  276. const result = data.success ?? false;
  277. resolve(result);
  278. } else {
  279. resolve(false);
  280. }
  281. }).catch((err) => {
  282. reject(err);
  283. });
  284. });
  285. }
  286.  
  287. GM_addStyle(`
  288. .dmc_panel {
  289. padding: 15px;
  290. }
  291. .dmc_span {
  292. margin-left: 10px;
  293. margin-right: 10px;
  294. }
  295. .dmc_ipt {
  296. text-align: center;
  297. width: 50px;
  298. }
  299. .dmc_txt {
  300. margin-top: 10px;
  301. width: 100%;
  302. height: 300px;
  303. }
  304. .dmc_hide {
  305. display: none;
  306. }
  307. `);
  308.  
  309. })();
  310.