Skip to content

Commit

Permalink
refactor code, fix bugs
Browse files Browse the repository at this point in the history
update keyword lists once a pull request is merged
  • Loading branch information
double-beep authored Jul 11, 2024
1 parent 5f714f8 commit 842b38d
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 139 deletions.
136 changes: 86 additions & 50 deletions src/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,95 +25,131 @@ interface ChatResponse {
time: number | null;
}

const charcoalRoomId = 11540, smokedetectorId = 120914, metasmokeId = 478536;
const charcoalHq = 11540;
const smokeyId = 120914;
const metasmokeId = 478536;

async function sendActionMessageToChat(element: Element): Promise<void> {
async function sendMessage(element: Element): Promise<void> {
// so that the text in the tooltip is consistent with what's being watched
const messageToSend = element.getAttribute('fire-tooltip');
const message = element.getAttribute('fire-tooltip');

const fkeyEl = document.querySelector<HTMLInputElement>('input[name="fkey"]');
const userFkey = fkeyEl?.value;
if (!userFkey) throw new Error('Chat fkey not found'); // chat message cannot be sent
else if (!messageToSend) throw new Error('No message found');
const fkey = fkeyEl?.value;

if (!fkey) throw new Error('Chat fkey not found'); // chat message cannot be sent
else if (!message) throw new Error('No message found');

const params = new FormData();
params.append('text', messageToSend);
params.append('fkey', userFkey);
params.append('text', message);
params.append('fkey', fkey);

const newMessageUrl = `/chats/${charcoalRoomId}/messages/new`;
const chatNewMessageCall = await fetch(newMessageUrl, {
const url = `/chats/${charcoalHq}/messages/new`;
const call = await fetch(url, {
method: 'POST',
body: params
});

if (chatNewMessageCall.status !== 200) {
throw new Error(`Failed to send message to chat. Returned error is ${chatNewMessageCall.status}`);
if (call.status !== 200 || !call.ok) {
throw new Error(
`Failed to send message to chat. Returned error is ${call.status}`
);
}

const chatResponse = await chatNewMessageCall.json() as ChatResponse;
const response = await call.json() as ChatResponse;

// if .id or .time are null, then something went wrong
if (!chatResponse.id || !chatResponse.time) throw new Error('Failed to send message to chat!');
if (!response.id || !response.time) {
throw new Error('Failed to send message to chat!');
}
}

export function addActionListener(element: Element | null,): void {
export function addListener(element: Element | null): void {
if (!element) return;

element.addEventListener('click', async () => {
try {
await sendActionMessageToChat(element);
await sendMessage(element);

toastr.success('Successfully sent message to chat.');
} catch (error) {
toastr.error(error as string);

console.error('Error while sending message to chat.', error);
}
});
}

function updateWatchesAndBlacklists(parsedContent: Document): void {
const messageText = parsedContent.body?.innerHTML || '';
const autoReloadOf = /SmokeDetector: Auto (?:un)?(?:watch|blacklist) of/;
const blacklistsReloaded = /Blacklists reloaded at/;
function updateKeywordLists(
regex: string,
action: 'watch' | 'unwatch' | 'blacklist' | 'unblacklist'
): void {
try {
const newRegex = new RegExp(regex, 'i');

// make sure the (un)watch/blacklist happened recently
if (!autoReloadOf.exec(messageText) || !blacklistsReloaded.exec(messageText)) return;
const compare = (regex: RegExp): boolean => regex.source !== newRegex.source;

try {
const regexText = parsedContent.querySelectorAll('code')[1].innerHTML;
const newRegex = new RegExp(regexText, 'i');
const anchorInnerHtml = parsedContent.querySelectorAll('a')?.[1].innerHTML;

const regexMatch = (regex: RegExp): boolean => regex.toString() !== newRegex.toString();
const isType = (regex: RegExp): boolean => Boolean(regex.exec(anchorInnerHtml));

const isWatch = isType(/Auto\swatch\sof\s/);
const isBlacklist = isType(/Auto\sblacklist\sof\s/);
const isUnwatch = isType(/Auto\sunwatch\sof\s/);
const isUnblacklist = isType(/Auto\sunblacklist\sof/);

if (isWatch) {
Domains.watchedWebsites.push(newRegex);
} else if (isBlacklist) {
// if it is a blacklist, also remove the item from the watchlist
Domains.watchedWebsites = Domains.watchedWebsites.filter(regexMatch);
Domains.blacklistedWebsites.push(newRegex);
} else if (isUnwatch) {
Domains.watchedWebsites = Domains.watchedWebsites.filter(regexMatch);
} else if (isUnblacklist) {
Domains.blacklistedWebsites = Domains.blacklistedWebsites.filter(regexMatch);
switch (action) {
case 'watch':
Domains.watched.push(newRegex);

break;
case 'blacklist':
// if it is a blacklist, also remove the item from the watchlist
Domains.watched = Domains.watched.filter(compare);
Domains.blacklisted.push(newRegex);

break;
case 'unwatch':
Domains.watched = Domains.watched.filter(compare);

break;
case 'unblacklist':
Domains.blacklisted = Domains.blacklisted.filter(compare);
break;
default:
}
} catch (error) {
return;
}
}

export function newChatEventOccurred({ event_type, user_id, content }: ChatParsedEvent): void {
if ((user_id !== smokedetectorId && user_id !== metasmokeId) || event_type !== 1) return;
function parseChatMessage(content: Document): void {
const message = content.body?.innerHTML || '';
const autoReloadOf = /SmokeDetector: Auto (?:un)?(?:watch|blacklist) of/;
const blacklistsReloaded = /Blacklists reloaded at/;

// make sure the (un)watch/blacklist happened recently
if (!autoReloadOf.test(message) || !blacklistsReloaded.test(message)) return;

const regexText = content.querySelectorAll('code')[1].innerHTML;
const anchorHtml = content.querySelectorAll('a')?.[1].innerHTML;
const action = (['watch', 'unwatch', 'blacklist', 'unblacklist'] as const)
.find(word => {
const regex = new RegExp(`Auto\\s${word}\\sof\\s`);

updateWatchesAndBlacklists(content);
return regex.test(anchorHtml);
}) || 'watch'; // watch by default

updateKeywordLists(regexText, action);
}

export function newChatEventOccurred({ event_type, user_id, content }: ChatParsedEvent): void {
if ((user_id !== smokeyId && user_id !== metasmokeId) || event_type !== 1) return;

parseChatMessage(content);

const message = content.body?.innerHTML || '';
// before updating Domains.pullRequests, make sure to update keyword lists
// based on the pr that was merged
const prId = Number(/Merge pull request #(\d+)/.exec(message)?.[1]);
const pr = Domains.pullRequests.find(({ id }) => id === prId);
if (pr && prId) {
const { regex, type } = pr;
updateKeywordLists(regex.source, type);
}

// don't wait for that to finish for the function to return
getUpdatedPrInfo(content)
.then(newGithubPrInfo => Domains.githubPullRequests = newGithubPrInfo || [])
getUpdatedPrInfo(message)
.then(info => Domains.pullRequests = info || [])
.catch(error => console.error(error));
}
72 changes: 38 additions & 34 deletions src/dom_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { GithubApiInformation, sdGithubRepo } from './github';
import { Domains } from './domain_stats';

interface Feedbacks {
count: number,
type: 'tp' | 'fp' | 'naa'
count: number;
type: 'tp' | 'fp' | 'naa';
}

export function getWaitGif(): HTMLImageElement {
Expand All @@ -32,20 +32,22 @@ export function getCross(): HTMLSpanElement {
return redCross;
}

function getButton(action: 'watch' | 'blacklist'): HTMLAnchorElement {
const button = document.createElement('a');
button.classList.add(`fire-extra-${action}`);
button.style.display = 'none';
button.innerHTML = '!!/${action}';

return button;
}

export function getWatchBlacklistButtons(): HTMLDivElement {
const container = document.createElement('div');

const watchButton = document.createElement('a');
watchButton.classList.add('fire-extra-watch');
watchButton.style.display = 'none';
watchButton.innerHTML = '!!/watch';
const watch = getButton('watch');
const blacklist = getButton('blacklist');

const blacklistButton = document.createElement('a');
blacklistButton.classList.add('fire-extra-blacklist');
blacklistButton.style.display = 'none';
blacklistButton.innerHTML = '!!/blacklist';

container.append(watchButton, blacklistButton);
container.append(watch, blacklist);

return container;
}
Expand All @@ -67,15 +69,15 @@ function getMsResultsElement(escapedDomain: string): HTMLDivElement {
}

function getSeResultsSpan(searchTerm: string): HTMLSpanElement {
const seResults = document.createElement('span');
seResults.classList.add('fire-extra-se-results');
const results = document.createElement('span');
results.classList.add('fire-extra-se-results');

const seResultsLink = document.createElement('a');
seResultsLink.href = getSeUrl(searchTerm);
seResultsLink.append(getWaitGif());
seResults.append(seResultsLink);
const link = document.createElement('a');
link.href = getSeUrl(searchTerm);
link.append(getWaitGif());
results.append(link);

return seResults;
return results;
}

export function getResultsContainer(term: string): HTMLElement {
Expand All @@ -84,10 +86,11 @@ export function getResultsContainer(term: string): HTMLElement {
const container = document.createElement('div');
container.style.marginRight = '7px';

const metasmokeResults = getMsResultsElement(escaped);
const stackResults = getSeResultsSpan(term);
const metasmoke = getMsResultsElement(escaped);
const stack = getSeResultsSpan(term);

container.append('(', metasmokeResults, ' | ', stackResults, ')');
// (MS: .., .., .. | SE: ..)
container.append('(', metasmoke, ' | ', stack, ')');

return container;
}
Expand All @@ -108,9 +111,9 @@ export function getInfoContainer(): HTMLDivElement {
return container;
}

export function createTag(tagName: string): HTMLSpanElement {
export function getTag(name: string): HTMLSpanElement {
const tag = document.createElement('span');
tag.innerHTML = `#${tagName}`;
tag.innerHTML = `#${name}`;
tag.classList.add('fire-extra-tag');

return tag;
Expand Down Expand Up @@ -151,24 +154,23 @@ export function getColouredSpans([tpCount, fpCount, naaCount]: number[]): Array<
return feedbacks.map(({ count, type }) => type ? getColouredSpan(count, type) : ', ');
}

const getGithubPrUrl = (prId: number): string => `//github.com/${sdGithubRepo}/pull/${prId}`;
const getPrTooltip = ({ id, regex, author, type }: GithubApiInformation): string =>
`${author} wants to ${type} ${regex.source} in PR#${id}`;

export function getPendingPrElement(githubPrOpenItem: GithubApiInformation): HTMLDivElement {
const prId = githubPrOpenItem.id;
export function getPendingPrElement(pr: GithubApiInformation): HTMLDivElement {
const { author, type, regex, id } = pr;

const container = document.createElement('div');

const anchor = document.createElement('a');
anchor.href = getGithubPrUrl(prId);
anchor.innerHTML = `PR#${prId}`;
anchor.setAttribute('fire-tooltip', getPrTooltip(githubPrOpenItem));
anchor.href = `//github.com/${sdGithubRepo}/pull/${id}`;
anchor.innerHTML = `PR#${id}`;

const text = `${author} wants to ${type} ${regex.source} in PR#${id}`;
anchor.setAttribute('fire-tooltip', text);

const approve = document.createElement('a');
approve.classList.add('fire-extra-approve');
approve.innerHTML = '!!/approve';
approve.setAttribute('fire-tooltip', `!!/approve ${prId}`);
approve.setAttribute('fire-tooltip', `!!/approve ${id}`);

container.append(anchor, ' pending ', approve);

Expand Down Expand Up @@ -205,7 +207,9 @@ export async function triggerDomainUpdate(domainIdsValid: number[]): Promise<str
.flatMap(([domainName, feedbackCount]) => {
const domainId = helpers.getDomainId(domainName);
const domainLi = document.getElementById(domainId);
if (!domainLi) return []; // in case the popup is closed before the process is complete

// in case the popup is closed before the process is complete
if (!domainLi) return [];

updateMsCounts(feedbackCount, domainLi);
Domains.allDomainInformation[domainName].metasmoke = feedbackCount;
Expand Down
36 changes: 18 additions & 18 deletions src/domain_stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,24 @@ export interface DomainStats {
export class Domains {
public static allDomainInformation: DomainStats = {}; // contains both the SE hit count and the MS feedbacks

public static watchedWebsites: RegExp[];
public static blacklistedWebsites: RegExp[];
public static githubPullRequests: GithubApiInformation[];
public static watched: RegExp[];
public static blacklisted: RegExp[];
public static pullRequests: GithubApiInformation[];

public static whitelistedDomains: string[];
public static whitelisted: string[];
public static redirectors: string[];

public static async fetchAllDomainInformation(): Promise<void> {
// nothing to do; all information is successfully fetched
if (this.watchedWebsites
&& this.blacklistedWebsites
&& this.githubPullRequests
&& this.whitelistedDomains
if (this.watched
&& this.blacklisted
&& this.pullRequests
&& this.whitelisted
&& this.redirectors) return;

// Those files are frequently updated, so they can't be in @resources
// Thanks tripleee! https://github.com/Charcoal-SE/halflife/blob/ab0fa5fc2a048b9e17762ceb6e3472e4d9c65317/halflife.py#L77
// Thanks tripleee!
// https://github.com/Charcoal-SE/halflife/blob/ab0fa5fc2a048b9e17762ceb6e3472e4d9c65317/halflife.py#L77
const [
watchedCall, blacklistedCall, prsCall, whitelistedCall, redirectorsCall
] = await Promise.all(([
Expand All @@ -60,11 +61,11 @@ export class Domains {
redirectorsCall.text()
]);

this.watchedWebsites = getRegexesFromTxtFile(watched, 2);
this.blacklistedWebsites = getRegexesFromTxtFile(blacklisted, 0);
this.githubPullRequests = parseApiResponse(prs);
this.watched = getRegexesFromTxtFile(watched, 2);
this.blacklisted = getRegexesFromTxtFile(blacklisted, 0);
this.pullRequests = parseApiResponse(prs);

this.whitelistedDomains = whitelisted.split('\n');
this.whitelisted = whitelisted.split('\n');
this.redirectors = redirectors.split('\n');
}

Expand All @@ -84,12 +85,11 @@ export class Domains {
const results = await getGraphQLInformation(domainIds);
if ('errors' in results) return {};

results.data.spam_domains.forEach(spamDomain => {
const tpPosts = spamDomain.posts.filter(post => post.is_tp).length;
const fpPosts = spamDomain.posts.filter(post => post.is_fp).length;
const naaPosts = spamDomain.posts.filter(post => post.is_naa).length;
results.data.spam_domains.forEach(({ posts, domain }) => {
const stats = (['tp', 'fp', 'naa'] as const)
.map(feedback => posts.filter(post => post[`is_${feedback}`]).length);

domainStats[spamDomain.domain] = [tpPosts, fpPosts, naaPosts];
domainStats[domain] = stats;
});
} catch (error) {
if (error instanceof Error) {
Expand Down
5 changes: 2 additions & 3 deletions src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,9 @@ export function parseApiResponse(jsonData: GithubApiResponse[]): GithubApiInform
});
}

export async function getUpdatedPrInfo(parsedContent: Document): Promise<GithubApiInformation[] | undefined> {
const messageText = parsedContent.body?.innerHTML || '';
export async function getUpdatedPrInfo(message: string): Promise<GithubApiInformation[] | undefined> {
const prChanged = /Closed pull request |Merge pull request|opened by SmokeDetector/;
if (!prChanged.test(messageText)) return;
if (!prChanged.test(message)) return;

const call = await fetch(githubUrls.api);
const response = await call.json() as GithubApiResponse[];
Expand Down
Loading

0 comments on commit 842b38d

Please sign in to comment.