-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDiscord_ChannelMessager.js
More file actions
269 lines (235 loc) · 9.17 KB
/
Copy pathDiscord_ChannelMessager.js
File metadata and controls
269 lines (235 loc) · 9.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// ==UserScript==
// @name Discord_ChannelMessager频道消息捕获器
// @namespace https://gist.github.com/ilpoint
// @version 0.1
// @description 捕获频道消息,导出CSV按时间排序,保留换行,开始/停止/清空
// @match https://discord.com/channels/*
// @license MIT
// @grant GM_addStyle
// ==/UserScript==
(function () {
'use strict';
// ==============================
// 状态变量
// ==============================
const captured = new Map(); // key: DOM消息元素id, value: {id, author, time, content}
let isRunning = false; // 轮询开关
let pollTimer = null; // 轮询定时器
let countSpan = null; // 面板计数元素
let toggleBtn = null; // 开始/停止按钮
// ==============================
// 从单个消息DOM元素提取信息
// ==============================
function extractMessage(msgElement) {
const msgId = msgElement.id;
if (!msgId || captured.has(msgId)) return null;
// 作者:使用原始简洁选择器,抓不到就显示 unknown
const authorEl = msgElement.querySelector(
'[class*="username_"], [id^="message-username-"], [class*="headerText_"]'
);
const author = authorEl ? authorEl.innerText.trim() : 'unknown';
// 时间:直接取 <time> 元素的文本内容
const timeEl = msgElement.querySelector('time');
const time = timeEl ? timeEl.innerText.trim() : '';
// 内容:保留原始换行,后续导出 CSV 时通过双引号包裹实现多行
const contentEl = msgElement.querySelector(
'[id^="message-content-"], [class*="messageContent_"]'
);
if (!contentEl) return null;
const content = contentEl.innerText.trim();
if (!content) return null;
return { id: msgId, author, time, content };
}
// ==============================
// 轮询扫描所有消息,增量捕获
// Discord 使用虚拟滚动与 DOM 复用,
// 新消息产生时往往修改现有节点而非插入新节点,
// 因此 MutationObserver 容易漏报,采用可靠的全量轮询。
// ==============================
function poll() {
if (!isRunning) return;
let newCount = 0;
const allMessages = document.querySelectorAll('[id^="chat-messages-"]');
allMessages.forEach(node => {
const info = extractMessage(node);
if (info) {
captured.set(info.id, info);
newCount++;
}
});
if (newCount > 0) {
updateCountDisplay();
showToast(`✨ +${newCount} 条`, 1000);
}
pollTimer = setTimeout(poll, 600);
}
// ==============================
// 开始 / 停止控制
// ==============================
function startCapture() {
if (isRunning) return;
isRunning = true;
poll(); // 立刻执行一轮扫描
updateBtnState();
showToast("开始捕获", 1000);
}
function stopCapture() {
if (!isRunning) return;
isRunning = false;
if (pollTimer) {
clearTimeout(pollTimer);
pollTimer = null;
}
updateBtnState();
showToast("已停止", 1000);
}
function toggleCapture() {
isRunning ? stopCapture() : startCapture();
}
function updateBtnState() {
if (!toggleBtn) return;
if (isRunning) {
toggleBtn.textContent = '⏹ 停止';
toggleBtn.style.background = '#da373c';
} else {
toggleBtn.textContent = '▶ 开始';
toggleBtn.style.background = '#23a55a';
}
}
// ==============================
// UI 辅助函数
// ==============================
function updateCountDisplay() {
if (countSpan) countSpan.textContent = captured.size;
}
let toastTimeout = null;
function showToast(message, duration = 1500) {
let toast = document.getElementById('dc-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'dc-toast';
Object.assign(toast.style, {
position: 'fixed',
bottom: '20px',
right: '20px',
background: '#2e3338',
color: '#fff',
padding: '6px 12px',
borderRadius: '8px',
fontSize: '12px',
zIndex: '10000000',
opacity: '0',
transition: 'opacity 0.2s',
pointerEvents: 'none',
fontFamily: 'system-ui'
});
document.body.appendChild(toast);
}
toast.textContent = message;
toast.style.opacity = '1';
clearTimeout(toastTimeout);
toastTimeout = setTimeout(() => { toast.style.opacity = '0'; }, duration);
}
// ==============================
// 导出 CSV(按消息 Snowflake ID 排序,保留换行)
// ==============================
function exportToCSV() {
if (captured.size === 0) {
showToast("无数据可导出");
return;
}
// 提取消息并按 Snowflake ID 升序排序
const messages = Array.from(captured.values());
messages.sort((a, b) => {
// 消息 ID 格式:chat-messages-频道/线程ID-消息SnowflakeID
// 取最后一段数字转为 BigInt 进行比较,保证时间顺序正确
const idA = BigInt(a.id.split('-').pop());
const idB = BigInt(b.id.split('-').pop());
if (idA < idB) return -1;
if (idA > idB) return 1;
return 0;
});
// 构建 CSV 行
const rows = messages.map(item => [
item.author,
item.time,
item.content.replace(/"/g, '""') // CSV 双引号转义
]);
const csvContent = '\uFEFF"作者","时间","内容"\n' +
rows.map(row => row.map(cell => `"${cell}"`).join(',')).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '_');
a.download = `discord_${timestamp}.csv`;
a.click();
URL.revokeObjectURL(url);
showToast(`✅ 导出 ${captured.size} 条消息 (已排序)`);
}
// ==============================
// 清空(自动停止捕获)
// ==============================
function clearAll() {
if (confirm(`清空 ${captured.size} 条消息并停止捕获?`)) {
stopCapture();
captured.clear();
updateCountDisplay();
showToast("已清空并停止");
}
}
// ==============================
// 创建控制面板(左上角悬浮)
// ==============================
function createPanel() {
GM_addStyle(`
.dc-panel {
position: fixed; top: 10px; left: 10px;
background: #1e1f22cc; backdrop-filter: blur(8px);
border-radius: 10px; padding: 6px 12px;
display: flex; gap: 10px; align-items: center;
font-family: system-ui; font-size: 12px; color: #dbdee1;
border: 1px solid #2b2d31; z-index: 1000000;
}
.dc-panel button {
background: #5865f2; border: none; color: #fff;
padding: 4px 8px; border-radius: 6px;
cursor: pointer; font-size: 11px; font-weight: 500;
}
.dc-panel button:hover { opacity: 0.85; }
.dc-panel .count {
background: #2b2d31; padding: 2px 8px;
border-radius: 20px; font-weight: bold; color: #ffb74d;
}
.dc-panel .clear-btn { background: #da373c; }
`);
const panel = document.createElement('div');
panel.className = 'dc-panel';
panel.innerHTML = `
<button id="dcToggle">▶ 开始</button>
<span class="count" id="dcCount">0</span>
<button id="dcExport">CSV</button>
<button id="dcClear" class="clear-btn">🗑️ 清空</button>
`;
document.body.appendChild(panel);
countSpan = document.getElementById('dcCount');
toggleBtn = document.getElementById('dcToggle');
updateCountDisplay();
document.getElementById('dcToggle').addEventListener('click', toggleCapture);
document.getElementById('dcExport').addEventListener('click', exportToCSV);
document.getElementById('dcClear').addEventListener('click', clearAll);
updateBtnState();
}
// ==============================
// 初始化
// ==============================
function init() {
createPanel();
showToast("就绪,点击「开始」捕获消息", 2000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();