Skip to content
Navigation Menu
{{ message }}
-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathMetaSmokeAPI.ts
More file actions
396 lines (318 loc) · 12.5 KB
/
Copy pathMetaSmokeAPI.ts
File metadata and controls
396 lines (318 loc) · 12.5 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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
import { Store, Cached } from './Store';
import {
AllFeedbacks,
PostType,
delay,
getFormDataFromObject,
} from '../shared';
import { Modals, Input, Buttons } from '@userscripters/stacks-helpers';
import { displayToaster, page } from '../AdvancedFlagging';
import Reporter from './Reporter';
import WebsocketUtils from './WebsocketUtils';
interface MetaSmokeApiItem {
id: number;
link: string;
}
interface MetaSmokeApiWrapper {
items: MetaSmokeApiItem[];
}
// key is the sitePostId, the value is the metasmokeId. That's all we need!
type MetasmokeData = Record<number, number>;
interface MetasmokeWsMessage {
type: string;
message: {
event_class: string;
event_type: string;
object: {
link: string;
};
};
}
export class MetaSmokeAPI extends Reporter {
public static accessToken: string;
public static isDisabled: boolean = Store.get<boolean>(Cached.Metasmoke.disabled) || false;
public smokeyId: number;
private static readonly appKey = '0a946b9419b5842f99b052d19c956302aa6c6dd5a420b043b20072ad2efc29e0';
private static readonly filter = 'GGJFNNKKJFHFKJFLJLGIJMFIHNNJNINJ';
private static readonly metasmokeIds: MetasmokeData = {};
private readonly failureMessage = 'Failed to report post to Smokey';
private readonly wsUrl = 'wss://metasmoke.erwaysoftware.com/cable';
private readonly wsAuth = JSON.stringify({
identifier: JSON.stringify({
channel: 'ApiChannel',
key: MetaSmokeAPI.appKey,
events: 'posts#create'
}),
command: 'subscribe'
});
constructor(
id: number,
private readonly postType: PostType,
private readonly deleted: boolean
) {
super('Smokey', id);
this.smokeyId = MetaSmokeAPI.metasmokeIds[this.id] ?? 0;
}
public static reset(): void {
Store.unset(Cached.Metasmoke.disabled);
Store.unset(Cached.Metasmoke.userKey);
}
public static async setup(): Promise<void> {
// Make sure we request it immediately
MetaSmokeAPI.accessToken = await MetaSmokeAPI.getUserKey();
}
public static async queryMetaSmokeInternal(urls?: string[]): Promise<void> {
if (MetaSmokeAPI.isDisabled) return;
// postIds as URLs, including questions
const urlString = urls ?? page.getAllPostIds(true, true).join(',');
// don't make the request if there aren't URLs
if (!urlString) return;
const parameters = Object.entries({
urls: urlString,
key: MetaSmokeAPI.appKey,
per_page: 1000,
filter: this.filter // only include id and link fields
})
.map(item => item.join('='))
.join('&');
try {
const url = `https://metasmoke.erwaysoftware.com/api/v2.0/posts/urls?${parameters}`;
const call = await fetch(url);
const result = await call.json() as MetaSmokeApiWrapper;
result.items.forEach(({ link, id }) => {
const postId = Number(/\d+$/.exec(link)?.[0]);
if (!postId) return;
MetaSmokeAPI.metasmokeIds[postId] = id;
});
} catch (error) {
displayToaster('Failed to get Metasmoke URLs.', 'danger');
console.error(error);
// if for whatever reason, info can't be fetched (e.g. MS is down)
// disable sending feedback and reporting posts to Smokey
MetaSmokeAPI.isDisabled = true;
}
}
public getQueryUrl(): string {
const path = this.postType === 'Answer' ? 'a' : 'questions';
return `//${window.location.hostname}/${path}/${this.id}`;
}
public reportReceived(event: MessageEvent<string>): number[] {
const data = JSON.parse(event.data) as MetasmokeWsMessage;
// https://github.com/Charcoal-SE/userscripts/blob/master/sim/sim.user.js#L381-L400
if (data.type) return []; // not interested
if (Store.dryRun) {
console.log('New post reported to Smokey', data);
}
const {
object,
event_class: evClass,
event_type: type
} = data.message;
// not interested
if (type !== 'create' || evClass !== 'Post') return [];
const link = object.link;
const url = new URL(link, location.href);
const postId = Number(/\d+/.exec(url.pathname)?.[0]);
// different sites
if (url.host !== location.host) return [];
return [ postId ];
}
public async reportRedFlag(): Promise<void> {
const urlString = this.getQueryUrl();
const { appKey, accessToken } = MetaSmokeAPI;
const url = 'https://metasmoke.erwaysoftware.com/api/w/post/report';
const data = {
post_link: urlString,
key: appKey,
token: accessToken
};
if (Store.dryRun) {
console.log('Report post via', url, data);
return;
}
const reportRequest = await fetch(
url,
{
method: 'POST',
body: getFormDataFromObject(data)
}
);
const requestResponse = await reportRequest.text();
// if the post is successfully reported, the response is a plain OK
if (!reportRequest.ok || requestResponse !== 'OK') {
console.error(`Failed to report post ${this.smokeyId} to Smokey`, requestResponse);
throw new Error(this.failureMessage);
}
}
public override canBeReported(): boolean {
return !MetaSmokeAPI.isDisabled;
}
public override wasReported(): boolean {
return Boolean(this.smokeyId);
}
public override showOnPopover(): boolean {
// valid everywhere, if not disabled
return !MetaSmokeAPI.isDisabled;
}
public override canSendFeedback(feedback: AllFeedbacks): boolean {
const { isDisabled, accessToken } = MetaSmokeAPI;
return !isDisabled // user must have MS enabled
&& Boolean(accessToken) // and must have authenticated with MS
&& (
Boolean(this.smokeyId) || ( // the post has been reported OR:
feedback === 'tpu-' // the feedback is tpu-
&& !this.deleted // AND the post is not deleted
)
);
}
public override async sendFeedback(feedback: string): Promise<void> {
const { appKey, accessToken } = MetaSmokeAPI;
// not reported, feedback is tpu AND the post isn't deleted => report it!
if (!this.smokeyId && feedback === 'tpu-' && !this.deleted) {
// see: https://chat.stackexchange.com/transcript/message/65076878
const wsUtils = new WebsocketUtils(this.wsUrl, this.id, this.progress, this.wsAuth);
const reportProgress = this.progress?.addSubItem('Sending report...');
try {
await this.reportRedFlag();
reportProgress?.completed();
} catch (error) {
wsUtils.closeWebsocket();
reportProgress?.failed();
throw error;
}
await wsUtils.waitForReport(event => this.reportReceived(event));
// https://chat.stackexchange.com/transcript/message/65097399
// wait 3 seconds so that SD can start watching for post deletion
await new Promise(resolve => setTimeout(resolve, 3 * 1000));
// don't send feedback to post if it was just reported
return;
}
// otherwise, send feedback
const data = {
type: feedback,
key: appKey,
token: accessToken
};
const url = `//metasmoke.erwaysoftware.com/api/w/post/${this.smokeyId}/feedback`;
if (Store.dryRun) {
console.log('Feedback to Smokey via', url, data);
return;
}
const feedbackRequest = await fetch(
url,
{
method: 'POST',
body: getFormDataFromObject(data)
}
);
const feedbackResponse = await feedbackRequest.json() as unknown;
if (!feedbackRequest.ok) {
console.error(`Failed to send feedback for ${this.smokeyId} to Smokey`, feedbackResponse);
throw new Error();
}
}
public override getIcon(): HTMLDivElement {
return this.createBotIcon(
this.smokeyId
? `//metasmoke.erwaysoftware.com/post/${this.smokeyId}`
: ''
);
}
public override getProgressMessage(feedback: string): string {
return this.wasReported() || feedback !== 'tpu-'
? super.getProgressMessage(feedback)
: 'Reporting post to Smokey';
}
private static getMetasmokeTokenPopup(): HTMLElement {
const codeInput = Input.makeStacksInput(
'advanced-flagging-metasmoke-token-input',
{ placeholder: 'Enter the code here', },
{
text: 'Metasmoke access token',
description: 'Once you\'ve authenticated Advanced Flagging with '
+ 'metasmoke, you\'ll be given a code; enter it below:'
}
);
const authModal = Modals.makeStacksModal(
'advanced-flagging-metasmoke-token-modal',
{
title: {
text: 'Authenticate MS with AF'
},
body: {
bodyHtml: codeInput
},
footer: {
buttons: [
{
element: Buttons.makeStacksButton(
'advanced-flagging-submit-code',
'Submit',
{ primary: true }
)
},
{
element: Buttons.makeStacksButton(
'advanced-flagging-dismiss-code-modal',
'Cancel',
),
hideOnClick: true
}
]
}
}
);
return authModal;
}
private static showMSTokenPopupAndGet(): Promise<string | undefined> {
return new Promise<string>(resolve => {
const popup = this.getMetasmokeTokenPopup();
StackExchange.helpers.showModal(popup);
popup
.querySelector('.s-btn__filled')
?.addEventListener('click', () => {
const input = popup.querySelector('input');
const token = input?.value;
// dismiss modal
popup.remove();
if (!token) return;
resolve(token);
});
});
}
private static async codeGetter(metaSmokeOAuthUrl: string): Promise<string | undefined> {
if (MetaSmokeAPI.isDisabled) return;
const authenticate = await StackExchange.helpers.showConfirmModal({
title: 'Setting up metasmoke',
bodyHtml: 'If you do not wish to connect, press cancel and this popup won\'t show up again. '
+ 'To reset configuration, see the footer of Stack Overflow.',
buttonLabel: 'Authenticate!'
});
// user doesn't wish to connect
if (!authenticate) {
Store.set(Cached.Metasmoke.disabled, true);
return;
}
window.open(metaSmokeOAuthUrl, '_blank');
await delay(100);
return await this.showMSTokenPopupAndGet();
}
private static async getUserKey(): Promise<string> {
while (typeof StackExchange.helpers.showConfirmModal === 'undefined') {
// eslint-disable-next-line no-await-in-loop
await delay(100);
}
const { appKey } = MetaSmokeAPI;
const url = `https://metasmoke.erwaysoftware.com/oauth/request?key=${appKey}`;
return await Store.getAndCache<string>(
Cached.Metasmoke.userKey,
async (): Promise<string> => {
const code = await MetaSmokeAPI.codeGetter(url);
if (!code) return '';
const tokenUrl = `//metasmoke.erwaysoftware.com/oauth/token?key=${appKey}&code=${code}`;
const tokenCall = await fetch(tokenUrl);
const { token } = await tokenCall.json() as { token: string };
return token;
});
}
}
You can’t perform that action at this time.
