Skip to content

Commit 3f856d8

Browse files
authored
feat: stackoverflow command (#95)
* feat: stackoverflow command * refactor: some changes * feat: stackoverflow tests
1 parent e63652c commit 3f856d8

File tree

3 files changed

+152
-1
lines changed

3 files changed

+152
-1
lines changed

src/commands/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { getConfig } from '../config';
55
import type { Command } from '../types';
66
import { InvalidUsageError } from '../types';
77

8-
import { addKarma, karma, KARMA_REGEX } from './karma';
98
import co from './co';
109
import execute from './execute';
10+
import { addKarma, karma, KARMA_REGEX } from './karma';
1111
// import { grzesiu, morritz } from './kocopoly';
1212
import link from './link';
1313
import m1 from './m1';
@@ -25,6 +25,7 @@ import roll from './roll';
2525
import server from './server';
2626
import skierowanie from './skierowanie';
2727
import spotify from './spotify';
28+
import stackoverflow from './stackoverflow';
2829
import stats from './stats';
2930
import summon from './summon';
3031
import typeofweb from './towarticle';
@@ -66,6 +67,7 @@ const allCommands = [
6667
xkcd,
6768
yesno,
6869
youtube,
70+
stackoverflow,
6971
];
7072

7173
const cooldowns = new Discord.Collection<string, Discord.Collection<string, number>>();

src/commands/stackoverflow.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { expect } from 'chai';
2+
import * as Discord from 'discord.js';
3+
import nock from 'nock';
4+
5+
import { getMessageMock } from '../../test/mocks';
6+
7+
import stackoverflow from './stackoverflow';
8+
9+
describe('stackoverflow', () => {
10+
const mockItem = {
11+
link: 'Jak pisać testy',
12+
title: 'https://stackoverflow.com/questions/0/how-to-write-tests',
13+
} as const;
14+
15+
const mockLinksEmbed = () =>
16+
new Discord.MessageEmbed()
17+
.setAuthor(
18+
'Stack Overflow',
19+
'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Stack_Overflow_icon.svg/768px-Stack_Overflow_icon.svg.png',
20+
'https://stackoverflow.com/',
21+
)
22+
.setTitle('1 najlepsza odpowiedź:')
23+
.setColor('f4811e')
24+
.addFields([{ name: mockItem.title, value: mockItem.link }]);
25+
26+
it('should show error message when nothing found on stackoverflow', async () => {
27+
nock('https://api.stackexchange.com')
28+
.get(
29+
'/2.3/search/advanced?pagesize=5&order=desc&sort=activity&q=jak%20pisac%20testy&site=stackoverflow',
30+
)
31+
.reply(200, { items: [] });
32+
33+
const msg = getMessageMock('msg');
34+
35+
await stackoverflow.execute(msg as unknown as Discord.Message, ['jak', 'pisac', 'testy']);
36+
37+
return expect(msg.channel.send).to.have.been.calledOnce.and.calledWithMatch(
38+
'Niestety nic nie znalazłem',
39+
);
40+
});
41+
42+
it('should show links when found on stackoverflow', async () => {
43+
nock('https://api.stackexchange.com')
44+
.get(
45+
'/2.3/search/advanced?pagesize=5&order=desc&sort=activity&q=jak%20pisac%20testy&site=stackoverflow',
46+
)
47+
.reply(200, {
48+
items: [mockItem],
49+
});
50+
51+
const msg = getMessageMock('msg');
52+
53+
await stackoverflow.execute(msg as unknown as Discord.Message, ['jak', 'pisac', 'testy']);
54+
55+
return expect(msg.channel.send).to.have.been.calledOnceWithExactly(mockLinksEmbed());
56+
});
57+
});

src/commands/stackoverflow.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import Discord from 'discord.js';
2+
import fetch from 'node-fetch';
3+
4+
import type { Command } from '../types';
5+
6+
const ICON_URL =
7+
'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Stack_Overflow_icon.svg/768px-Stack_Overflow_icon.svg.png';
8+
9+
const isApiError = (data: ApiResponse): data is ApiResponseError => 'error_id' in data;
10+
11+
const formatTitle = (length: number) =>
12+
length === 1
13+
? 'najlepsza odpowiedź'
14+
: length < 5
15+
? 'najlepsze odpowiedzi'
16+
: 'najlepszych odpowiedzi';
17+
18+
const stackoverflow: Command = {
19+
name: 'stackoverflow',
20+
description: 'Wyszukaj swój problem na stackoverflow',
21+
args: 'required',
22+
async execute(msg, args) {
23+
const query = encodeURIComponent(args.join(' ').toLocaleLowerCase());
24+
const result = await fetch(
25+
`https://api.stackexchange.com/2.3/search/advanced?pagesize=5&order=desc&sort=activity&q=${query}&site=stackoverflow`,
26+
);
27+
const response = (await result.json()) as ApiResponse;
28+
29+
if (!result.ok || isApiError(response)) {
30+
return msg.channel.send('Przepraszam, ale coś poszło nie tak 😭');
31+
}
32+
33+
const fields = response.items.map(({ title, link }) => ({
34+
name: title,
35+
value: link,
36+
}));
37+
38+
if (fields.length === 0) {
39+
return msg.channel.send('Niestety nic nie znalazłem 😭');
40+
}
41+
42+
const embed = new Discord.MessageEmbed()
43+
.setAuthor('Stack Overflow', ICON_URL, 'https://stackoverflow.com/')
44+
.setTitle(`${fields.length} ${formatTitle(fields.length)}:`)
45+
.addFields(fields)
46+
.setColor('f4811e');
47+
48+
return msg.channel.send(embed);
49+
},
50+
};
51+
52+
export default stackoverflow;
53+
54+
interface ApiOwner {
55+
readonly account_id: number;
56+
readonly reputation: number;
57+
readonly user_id: number;
58+
readonly user_type: string;
59+
readonly profile_image: string;
60+
readonly display_name: string;
61+
readonly link: string;
62+
}
63+
64+
interface ApiItem {
65+
readonly tags: ReadonlyArray<string>;
66+
readonly owner: ApiOwner;
67+
readonly is_answered: boolean;
68+
readonly view_count: number;
69+
readonly answer_count: number;
70+
readonly score: number;
71+
readonly last_activity_date: number;
72+
readonly creation_date: number;
73+
readonly question_id: number;
74+
readonly content_license: string;
75+
readonly link: string;
76+
readonly title: string;
77+
}
78+
79+
interface ApiResponseSuccess {
80+
readonly items: ReadonlyArray<ApiItem>;
81+
readonly has_more: boolean;
82+
readonly quota_max: number;
83+
readonly quota_remaining: number;
84+
}
85+
86+
interface ApiResponseError {
87+
readonly error_id: number;
88+
readonly error_message: string;
89+
readonly error_name: string;
90+
}
91+
92+
type ApiResponse = ApiResponseSuccess | ApiResponseError;

0 commit comments

Comments
 (0)