',
17 | };
18 |
19 | this.setConf({
20 | permLevel: 2,
21 | });
22 | }
23 |
24 | run(msg, args) {
25 | let command = args.join(' ');
26 | let bot = this.bot;
27 | if (!command || command.length === 0) return;
28 |
29 | this._evalCommand(bot, msg, command, Log)
30 | .then((evaled) => {
31 | if (evaled && typeof evaled === 'string') {
32 | evaled = evaled.replace(this.tokenRegEx, '-- snip --').replace(this.pathRegEx, '.');
33 | }
34 |
35 | let message = ['`EVAL`', '```js', evaled !== undefined ? this._clean(evaled) : 'undefined', '```'].join('\n');
36 |
37 | return msg.channel.send(message);
38 | })
39 | .catch((error) => {
40 | if (error.stack) error.stack = error.stack.replace(this.pathRegEx, '.');
41 | let message = ['`EVAL`', '```js', this._clean(error) || error, '```'].join('\n');
42 | return msg.channel.send(message);
43 | });
44 | }
45 | _evalCommand(bot, msg, command, log) {
46 | return new Promise((resolve, reject) => {
47 | if (!log) log = Log;
48 | let code = command;
49 | try {
50 | var evaled = eval(code);
51 | if (evaled) {
52 | if (typeof evaled === 'object') {
53 | if (evaled._path) delete evaled._path;
54 | try {
55 | evaled = util.inspect(evaled, { depth: 0 });
56 | } catch (err) {
57 | evaled = JSON.stringify(evaled, null, 2);
58 | }
59 | }
60 | }
61 | resolve(evaled);
62 | } catch (error) {
63 | reject(error);
64 | }
65 | });
66 | }
67 |
68 | _clean(text) {
69 | if (typeof text === 'string') {
70 | return text
71 | .replace(/`/g, `\`${String.fromCharCode(8203)}`)
72 | .replace(/@/g, `@${String.fromCharCode(8203)}`)
73 | .replace('``', `\`${String.fromCharCode(8203)}\`}`);
74 | } else {
75 | return text;
76 | }
77 | }
78 | }
79 |
80 | module.exports = EvalCommand;
81 |
--------------------------------------------------------------------------------
/lib/Discord/Commands/Exec.js:
--------------------------------------------------------------------------------
1 | const { exec } = require('child_process');
2 | const Command = require('../Command');
3 | const Log = require('../../Util/Log');
4 |
5 | class ExecCommand extends Command {
6 | constructor(bot) {
7 | super(bot);
8 |
9 | this.props.help = {
10 | name: 'exec',
11 | description: 'Execute a command in bash',
12 | usage: 'exec ',
13 | };
14 | this.setConf({
15 | permLevel: 2,
16 | });
17 | }
18 | run(msg, args) {
19 | let command = args.join(' ');
20 |
21 | let runningMessage = ['`RUNNING`', '```xl', this._clean(command), '```'].join('\n');
22 |
23 | let messageToEdit;
24 |
25 | return msg.channel
26 | .send(runningMessage)
27 | .then((message) => {
28 | messageToEdit = message;
29 | })
30 | .then(() => this._exec(command))
31 | .then((stdout) => {
32 | stdout = stdout.substring(0, 1500);
33 |
34 | let message = ['`STDOUT`', '```sh', this._clean(stdout) || ' ', '```'].join('\n');
35 |
36 | messageToEdit.edit(message);
37 | })
38 | .catch((data) => {
39 | let { stdout, stderr } = data || {};
40 | if (stderr && stderr.stack) {
41 | Log.error(stderr);
42 | } else if (!stdout && !stderr && data) {
43 | throw data;
44 | }
45 |
46 | stderr = stderr ? stderr.substring(0, 800) : ' ';
47 | stdout = stdout ? stdout.substring(0, stderr ? stderr.length : 2046 - 40) : ' ';
48 |
49 | let message = ['`STDOUT`', '```sh', this._clean(stdout) || '', '```', '`STDERR`', '```sh', this._clean(stderr) || '', '```']
50 | .join('\n')
51 | .substring(0, 2000);
52 |
53 | if (messageToEdit) messageToEdit.edit(message);
54 | else msg.channel.send(message);
55 | });
56 | }
57 | _exec(cmd, opts = {}) {
58 | return new Promise((resolve, reject) => {
59 | exec(cmd, opts, (err, stdout, stderr) => {
60 | if (err) return reject({ stdout, stderr });
61 | resolve(stdout);
62 | });
63 | });
64 | }
65 | _clean(text) {
66 | if (typeof text === 'string') {
67 | return text.replace('``', `\`${String.fromCharCode(8203)}\``);
68 | } else {
69 | return text;
70 | }
71 | }
72 | }
73 |
74 | module.exports = ExecCommand;
75 |
--------------------------------------------------------------------------------
/lib/Discord/Commands/GitlabInit.js:
--------------------------------------------------------------------------------
1 | const Command = require('../Command');
2 | const Channel = require('../../Models/Channel');
3 | const Gitlab = require('../../Gitlab');
4 | const parse = require('../../Gitlab/parser');
5 | const punycode = require('punycode');
6 |
7 | class GitlabInitCommand extends Command {
8 | constructor(bot) {
9 | super(bot);
10 |
11 | this.props.help = {
12 | name: 'init',
13 | summary: 'Initialize repo events on the channel.',
14 | description: 'Initialize repo events on the channel.\nInsert "private" as 2nd argument if the repo is private',
15 | usage: 'init [private]',
16 | examples: ['init gitlab-org/gitlab-ce', 'init https://gitlab.com/gitlab-org/gitlab-ce', 'init user/privaterepo private'],
17 | };
18 |
19 | this.setConf({
20 | permLevel: 1,
21 | aliases: ['initialize'],
22 | guildOnly: true,
23 | });
24 | }
25 |
26 | async run(msg, args) {
27 | const repo = args[0];
28 | const isPrivate = args[1] && args[1].toLowerCase() === 'private';
29 |
30 | if (!repo) return this.errorUsage(msg);
31 |
32 | const repository = parse(punycode.toASCII(repo));
33 |
34 | const repoName = repository.name;
35 | const repoUser = repository.owner;
36 | const repoFullName = repository.repo && repository.repo.toLowerCase();
37 | if (!repository || !repoName || !repoUser) return this.errorUsage(msg);
38 |
39 | const embed = new this.embed().setTitle(`\`${repo}\`: ⚙ Working...`).setColor(0xfb9738);
40 | const workingMsg = await msg.channel.send({ embeds: [embed] });
41 |
42 | const conf = await Channel.find(msg.channel, ['repos']);
43 | const repos = conf.getRepos();
44 |
45 | if (!repository.isGitlab || isPrivate) {
46 | // GitlabCache.add(repository.repo);;
47 | const exists = repos.includes(repoFullName);
48 |
49 | if (exists) return this.commandError(msg, Gitlab.Constants.Errors.REPO_ALREADY_INITIALIZED(repository));
50 |
51 | return this.addRepo(workingMsg, conf, repository.repo);
52 | }
53 |
54 | return Gitlab.getRepo(repository.repo)
55 | .then(() => {
56 | const exists = repos.includes(repoFullName);
57 |
58 | if (exists) return this.commandError(msg, Gitlab.Constants.Errors.REPO_ALREADY_INITIALIZED(repository));
59 |
60 | return this.addRepo(workingMsg, conf, repository.repo);
61 | })
62 | .catch((err) => {
63 | if (!err) return;
64 |
65 | const res = err.response;
66 | const body = res?.data && JSON.parse(res?.data);
67 |
68 | if (res?.statusCode == 404)
69 | return this.commandError(
70 | msg,
71 | `The repository \`${repository.repo}\` could not be found!\nIf it's private, please run \`${this.bot.prefix}init ${repository.repo} private\`.`,
72 | res.statusMessage
73 | );
74 | else if (body)
75 | return this.commandError(
76 | msg,
77 | `Unable to get repository info for \`${repo}\`\n${(body && body.message) || ''}`,
78 | res.statusMessage
79 | );
80 | else return this.commandError(msg, err);
81 | });
82 | }
83 |
84 | addRepo(msg, conf, repo) {
85 | return conf.addRepo(repo.toLowerCase()).then(() => msg.edit({ embeds: [this._successMessage(repo, conf.get('useEmbed'))] }));
86 | }
87 |
88 | _successMessage(repo, hasEmbed) {
89 | const embed = new this.embed()
90 | .setColor(0x84f139)
91 | .setFooter({ text: this.bot.user.username })
92 | .setTitle(`\`${repo}\`: Successfully initialized repository events`)
93 | .setDescription(
94 | [
95 | 'The repository must a webhook pointing to ',
96 | '_Note that this is a \*new\* url -- make sure to update any old hooks in other repos if needed._',
97 | !hasEmbed
98 | ? 'To use embeds to have a nicer GitLab log, say `GL! conf set embed true` in this channel to enable embeds for the current channel.'
99 | : '',
100 | ].join('\n')
101 | );
102 | return embed;
103 | }
104 | }
105 |
106 | module.exports = GitlabInitCommand;
107 |
--------------------------------------------------------------------------------
/lib/Discord/Commands/GitlabInitOrg.js:
--------------------------------------------------------------------------------
1 | const Command = require('../Command');
2 | const Channel = require('../../Models/Channel');
3 | const Gitlab = require('../../Gitlab');
4 |
5 | class GitlabInitOrgCommand extends Command {
6 | constructor(bot) {
7 | super(bot);
8 |
9 | this.props.help = {
10 | name: 'initorg',
11 | summary: 'Initialize all repo events from a group on the channel.',
12 | usage: 'initorg ',
13 | examples: ['initorg YappyBots', 'initorg Discord'],
14 | };
15 |
16 | this.setConf({
17 | permLevel: 1,
18 | aliases: ['initializeorg', 'initgroup', 'initializegroup'],
19 | guildOnly: true,
20 | });
21 | }
22 |
23 | async run(msg, args) {
24 | const org = args[0];
25 | const organization = /^(?:https?:\/\/)?(?:gitlab.com\/)?(\S+)$/.exec(org);
26 |
27 | if (!org || !organization || !organization[0]) return this.errorUsage(msg);
28 |
29 | const orgName = organization[0];
30 | const workingMsg = await msg.channel.send({
31 | embeds: [
32 | {
33 | color: 0xfb9738,
34 | title: `Group \`${orgName}\`: ⚙ Working...`,
35 | },
36 | ],
37 | });
38 |
39 | const conf = await Channel.find(msg.channel);
40 | const repos = conf.getRepos();
41 |
42 | return Gitlab.getGroupProjects(orgName)
43 | .then((res) => {
44 | const r = res.body.filter((e) => !repos.includes(e.path_with_namespace.toLowerCase())).map((e) => e.path_with_namespace);
45 |
46 | return Promise.all(r.map((repo) => conf.addRepo(repo))).then(() => r);
47 | })
48 | .then((r) => workingMsg.edit({ embeds: [this._successMessage(orgName, r, conf.get('useEmbed'))] }))
49 | .catch((err) => {
50 | if (err.response.statusCode == 404) return this.commandError(msg, `Unable to initialize! Cannot find the \`${orgName}\` group!`);
51 |
52 | Log.error(err);
53 |
54 | return this.commandError(msg, `Unable to get group info for \`${orgName}\`\n${err.response.statusMessage}`);
55 | });
56 | }
57 |
58 | _successMessage(org, repos, hasEmbed) {
59 | const embed = new this.embed()
60 | .setColor(0x84f139)
61 | .setFooter({ text: this.bot.user.username })
62 | .setTitle(`\`${org}\`: Successfully initialized all public repository events`)
63 | .setDescription(
64 | [
65 | 'The repositories must all have a webhook pointing to ',
66 | !hasEmbed ? 'To use embeds to have a nicer Gitlab log, say `G! conf set embed true` in this channel.' : '',
67 | `Initialized repos: ${repos.length ? repos.map((e) => `\`${e}\``).join(', ') : 'None'}`,
68 | ].join('\n')
69 | );
70 | return embed;
71 | }
72 | }
73 |
74 | module.exports = GitlabInitOrgCommand;
75 |
--------------------------------------------------------------------------------
/lib/Discord/Commands/GitlabIssue.js:
--------------------------------------------------------------------------------
1 | const Command = require('../Command');
2 | const Channel = require('../../Models/Channel');
3 | const Gitlab = require('../../Gitlab');
4 |
5 | class GitlabIssue extends Command {
6 | constructor(bot) {
7 | super(bot);
8 |
9 | this.props.help = {
10 | name: 'issue',
11 | description: 'Search issues or get info about specific issue',
12 | usage: 'issue [query] [p(page)]',
13 | examples: ['issue 5', 'issue search error', 'issue search event p2'],
14 | };
15 |
16 | this.setConf({
17 | aliases: ['issues'],
18 | guildOnly: true,
19 | });
20 | }
21 |
22 | run(msg, args) {
23 | if (!args[0]) return this.errorUsage(msg);
24 |
25 | if (args[0] === 'search' && args.length > 1) return this._search(msg, args);
26 | if (args.length === 1) return this._issue(msg, args);
27 |
28 | return this.errorUsage(msg);
29 | }
30 |
31 | async _issue(msg, args) {
32 | const issueNumber = parseInt(args[0].replace(/#/g, ''));
33 | const conf = await Channel.find(msg.channel);
34 | const repository = conf.get('repo');
35 |
36 | if (!repository) return this.commandError(msg, Gitlab.Constants.Errors.NO_REPO_CONFIGURED(this));
37 | if (!issueNumber) return this.errorUsage(msg);
38 |
39 | return Gitlab.getProjectIssue(repository, null, issueNumber)
40 | .then((res) => {
41 | const issue = res.body;
42 | const description = issue.description;
43 | const [, imageUrl] = /!\[(?:.*?)\]\((.*?)\)/.exec(description) || [];
44 |
45 | const embed = new this.embed()
46 | .setTitle(`Issue \`#${issue.iid}\` - ${issue.title}`)
47 | .setURL(issue.web_url)
48 | .setDescription(`${description.slice(0, 2040)}\n\u200B`)
49 | .setColor('#84F139')
50 | .addFields([
51 | { name: 'Status', value: issue.state === 'opened' ? 'Open' : 'Closed', inline: true },
52 | { name: 'Labels', value: issue.labels.length ? issue.labels.map((e) => `\`${e}\``).join(', ') : 'None', inline: true },
53 | { name: 'Milestone', value: issue.milestone ? `${issue.milestone.title}` : 'None', inline: true },
54 | { name: 'Author', value: issue.author ? `[${issue.author.name}](${issue.author.web_url})` : 'Unknown', inline: true },
55 | { name: 'Assignee', value: issue.assignee ? `[${issue.assignee.name}](${issue.assignee.web_url})` : 'None', inline: true },
56 | { name: 'Comments', value: String(issue.user_notes_count), inline: true },
57 | ])
58 | .setFooter({ text: repository, iconURL: this.bot.user.avatarURL() });
59 | if (imageUrl) embed.setImage(imageUrl.startsWith('/') ? `https://gitlab.com/${repository}/${imageUrl}` : imageUrl);
60 |
61 | return msg.channel.send({ embeds: [embed] });
62 | })
63 | .catch((err) => {
64 | if (err.response?.statusCode === 404) {
65 | return this.commandError(msg, 'Issue Not Found', '404', repository);
66 | } else {
67 | return this.commandError(msg, err);
68 | }
69 | });
70 | }
71 |
72 | async _search(msg, args) {
73 | const page = args[args.length - 1].indexOf('p') === 0 ? parseInt(args[args.length - 1].slice(1)) : 1;
74 | const query = args.slice(1).join(' ').replace(`p${page}`, '');
75 |
76 | if (!query) return false;
77 |
78 | const conf = await Channel.find(msg.channel);
79 | const repository = conf.get('repo');
80 |
81 | if (!repository) return this.commandError(msg, Gitlab.Constants.Errors.NO_REPO_CONFIGURED(this));
82 |
83 | return Gitlab.getProjectIssues(repository, null, {
84 | page,
85 | per_page: 5,
86 | search: query,
87 | }).then((res) => {
88 | const totalPages = res.headers['x-total-pages'];
89 |
90 | let embed = new this.embed({
91 | title: `Issues - query \`${query}\``,
92 | description: '\u200B',
93 | })
94 | .setColor('#84F139')
95 | .setFooter(`${repository} ; page ${page} / ${totalPages === '0' ? 1 : totalPages}`);
96 |
97 | if (res.body && res.body.length) {
98 | res.body.forEach((issue) => {
99 | embed.description += `\n– [**\`#${issue.iid}\`**](${issue.web_url}) ${issue.title}`;
100 | });
101 | } else {
102 | embed.setDescription('No issues found');
103 | }
104 |
105 | msg.channel.send({ embeds: [embed] });
106 | });
107 | }
108 | }
109 |
110 | module.exports = GitlabIssue;
111 |
--------------------------------------------------------------------------------
/lib/Discord/Commands/GitlabMergeRequest.js:
--------------------------------------------------------------------------------
1 | const Command = require('../Command');
2 | const Channel = require('../../Models/Channel');
3 | const Gitlab = require('../../Gitlab');
4 |
5 | class GitlabIssue extends Command {
6 | constructor(bot) {
7 | super(bot);
8 |
9 | this.props.help = {
10 | name: 'mr',
11 | description: 'Search merge requests or get info about specific merge request',
12 | usage: 'mr [page]',
13 | examples: ['mr 5', 'mr list', 'mr list 2'],
14 | };
15 |
16 | this.setConf({
17 | aliases: ['mergerequest', 'merge'],
18 | guildOnly: true,
19 | });
20 | }
21 |
22 | run(msg, args) {
23 | if (!args[0]) return this.errorUsage(msg);
24 |
25 | if (args[0] === 'list') return this._list(msg, args);
26 | if (args.length === 1) return this._mr(msg, args);
27 |
28 | return this.errorUsage(msg);
29 | }
30 |
31 | async _mr(msg, args) {
32 | const mrNumber = parseInt(args[0].replace(/!/g, ''));
33 | const conf = await Channel.find(msg.channel);
34 | const repository = conf.get('repo');
35 |
36 | if (!repository) return this.commandError(msg, Gitlab.Constants.Errors.NO_REPO_CONFIGURED(this));
37 | if (!mrNumber) return this.errorUsage(msg);
38 |
39 | return Gitlab.getProjectMergeRequest(repository, null, mrNumber)
40 | .then((res) => {
41 | const mr = res.body;
42 | const description = mr.description;
43 | const [, imageUrl] = /!\[(?:.*?)\]\((.*?)\)/.exec(description) || [];
44 |
45 | const embed = new this.embed()
46 | .setTitle(`Merge Request \`#${mr.iid}\` - ${mr.title}`)
47 | .setURL(mr.web_url)
48 | .setDescription(`\u200B\n${description.slice(0, 2040)}\n\u200B`)
49 | .setColor('#84F139')
50 | .addFields([
51 | { name: 'Source', value: `${mr.author.username}:${mr.source_branch}`, inline: true },
52 | { name: 'Target', value: `${repository.split('/')[0]}:${mr.target_branch}`, inline: true },
53 | { name: 'State', value: mr.state === 'opened' ? 'Open' : `${mr.state[0].toUpperCase()}${mr.state.slice(1)}`, inline: true },
54 | { name: 'Status', value: mr.work_in_progress ? 'WIP' : 'Finished', inline: true },
55 | { name: 'Labels', value: mr.labels.length ? mr.labels.map((e) => `\`${e}\``).join(', ') : 'None', inline: true },
56 | { name: 'Milestone', value: mr.milestone ? `${mr.milestone.title}` : 'None', inline: true },
57 | { name: 'Comments', value: mr.user_notes_count, inline: true },
58 | ])
59 | .setFooter(repository, this.bot.user.avatarURL());
60 | if (imageUrl) embed.setImage(imageUrl.startsWith('/') ? `https://gitlab.com/${repository}/${imageUrl}` : imageUrl);
61 |
62 | return msg.channel.send({ embeds: [embed] });
63 | })
64 | .catch((err) => {
65 | const res = err.response;
66 |
67 | if (res?.statusCode === 404) return this.commandError(msg, 'Merge Request Not Found', '404', repository);
68 | if (res?.statusCode === 403) return this.error403(msg, repository);
69 |
70 | return this.commandError(msg, err);
71 | });
72 | }
73 |
74 | async _list(msg, args) {
75 | const page = args[1] ? parseInt(args) : 1;
76 | const conf = await Channel.find(msg.channel);
77 | const repository = conf.get('repo');
78 |
79 | if (!repository) return this.commandError(msg, Gitlab.Constants.Errors.NO_REPO_CONFIGURED(this));
80 |
81 | return Gitlab.getProjectMergeRequests(repository, null, {
82 | page,
83 | per_page: 5,
84 | })
85 | .then((res) => {
86 | const totalPages = res.headers['x-total-pages'];
87 |
88 | let embed = new this.embed({
89 | title: `Merge Requests`,
90 | description: '\u200B',
91 | })
92 | .setColor('#84F139')
93 | .setFooter(`${repository} ; page ${page} / ${totalPages === '0' ? 1 : totalPages}`);
94 |
95 | if (res.body && res.body.length) {
96 | res.body.forEach((mr) => {
97 | embed.description += `\n– [**\`#${mr.iid}\`**](${mr.web_url}) ${mr.title}`;
98 | });
99 | } else {
100 | embed.setDescription('No merge requests found');
101 | }
102 |
103 | msg.channel.send({ embeds: [embed] });
104 | })
105 | .catch((err) => {
106 | const res = err.response;
107 |
108 | if (res.statusCode === 403) return this.error403(msg, repository);
109 |
110 | return Promise.reject(err);
111 | });
112 | }
113 |
114 | error403(msg, r) {
115 | return this.commandError(msg, 'Merge requests are not visible to the public', '403', r);
116 | }
117 | }
118 |
119 | module.exports = GitlabIssue;
120 |
--------------------------------------------------------------------------------
/lib/Discord/Commands/GitlabRemove.js:
--------------------------------------------------------------------------------
1 | const Command = require('../Command');
2 | const Channel = require('../../Models/Channel');
3 | const ChannelRepo = require('../../Models/ChannelRepo');
4 | const parse = require('../../Gitlab/parser');
5 |
6 | class GitlabRemoveCommand extends Command {
7 | constructor(bot) {
8 | super(bot);
9 |
10 | this.props.help = {
11 | name: 'remove',
12 | summary: 'Remove repo events from the channel.',
13 | usage: 'remove [repo]',
14 | examples: ['remove', 'remove private/repo'],
15 | };
16 |
17 | this.setConf({
18 | permLevel: 1,
19 | guildOnly: true,
20 | });
21 | }
22 |
23 | async run(msg, args) {
24 | const conf = await Channel.find(msg.channel, ['repos']);
25 | const repos = conf && (await conf.getRepos());
26 | const repo = args[0] ? parse(args[0].toLowerCase()) : {};
27 | let repoFound = repo && !!repo.repo;
28 |
29 | if (!repos || !repos[0]) {
30 | return this.commandError(msg, "This channel doesn't have any GitLab events!");
31 | }
32 |
33 | if (repos.length > 1 && repo.repo) repoFound = repos.filter((e) => e.toLowerCase() === repo.repo.toLowerCase())[0];
34 | else if (repos.length === 1) repoFound = repos[0];
35 |
36 | if (args[0] && !repoFound) {
37 | return this.commandError(msg, `This channel doesn't have GitLab events for **${repo.repo || args[0]}**!`);
38 | } else if (repos.length && repos.length > 1 && !repoFound) {
39 | return this.commandError(msg, `Specify what GitLab repo event to remove! Current repos: ${repos.map((e) => `**${e}**`).join(', ')}`);
40 | }
41 |
42 | const workingMsg = await msg.channel.send({
43 | embeds: [new this.embed().setColor(0xfb9738).setTitle(`\`${repoFound}\`: ⚙ Working...`)],
44 | });
45 |
46 | return ChannelRepo.where('channel_id', conf.id)
47 | .where('name', repoFound)
48 | .destroy()
49 | .then(() => workingMsg.edit({ embeds: [this._successMessage(repoFound)] }))
50 | .catch((err) => {
51 | Log.error(err);
52 | return this.commandError(
53 | msg,
54 | `An error occurred while trying to remove repository events for **${repoFound}** in this channel.\n\`${err}\``
55 | );
56 | });
57 | }
58 |
59 | _successMessage(repo) {
60 | return new this.embed()
61 | .setColor(0x84f139)
62 | .setFooter({ text: this.bot.user.username })
63 | .setTitle(`\`${repo}\`: Successfully removed repository events`);
64 | }
65 | }
66 |
67 | module.exports = GitlabRemoveCommand;
68 |
--------------------------------------------------------------------------------
/lib/Discord/Commands/Help.js:
--------------------------------------------------------------------------------
1 | const { ChannelType } = require('discord.js');
2 | const Command = require('../Command');
3 |
4 | class HelpCommand extends Command {
5 | constructor(bot) {
6 | super(bot);
7 | this.props.help = {
8 | name: 'help',
9 | description: 'you all need some help',
10 | usage: 'help [command]',
11 | };
12 | this.props.conf.aliases = ['support'];
13 | }
14 |
15 | run(msg, args) {
16 | const commandName = args[0];
17 |
18 | if (!commandName) {
19 | const commands = this.bot.commands;
20 | let commandsForEveryone = commands.filter((e) => !e.conf.permLevel || e.conf.permLevel === 0);
21 | let commandsForAdmin = commands.filter((e) => e.conf.permLevel === 1);
22 | let commandsForOwner = commands.filter((e) => e.conf.permLevel === 2);
23 |
24 | if (msg.channel.type === ChannelType.DM) {
25 | commandsForEveryone = commandsForEveryone.filter((e) => !e.conf.guildOnly);
26 | }
27 |
28 | const embed = new this.embed()
29 | .setColor('#84F139')
30 | .setTitle(`Commands List`)
31 | .setDescription(
32 | [`Use \`${this.bot.prefix}help \` for details`, 'Or visit https://yappy.dsev.dev/gitlab/commands', '\u200B'].join('\n')
33 | )
34 | .setFooter({ text: this.bot.user.username, iconURL: this.bot.user.avatarURL() });
35 |
36 | embed.addFields([
37 | {
38 | name: '__Public__',
39 | value:
40 | commandsForEveryone
41 | .map((command) => {
42 | let help = command.help;
43 | return `\`${help.name}\`: ${help.summary || help.description}`;
44 | })
45 | .join('\n') || '\u200B',
46 | },
47 | ]);
48 |
49 | if (msg.client.permissions(msg) > 0 && commandsForAdmin.size) {
50 | embed.addFields([
51 | {
52 | name: '__Guild Administrator__',
53 | value:
54 | commandsForAdmin
55 | .map((command) => {
56 | let help = command.help;
57 | return `\`${help.name}\`: ${help.summary || help.description}`;
58 | })
59 | .join('\n') || '\u200B',
60 | },
61 | ]);
62 | }
63 |
64 | if (msg.client.permissions(msg) > 1 && commandsForOwner.size) {
65 | embed.addFields([
66 | {
67 | name: '__Bot Owner__',
68 | value:
69 | commandsForOwner
70 | .map((command) => {
71 | let help = command.help;
72 | return `\`${help.name}\`: ${help.summary || help.description}`;
73 | })
74 | .join('\n') || '\u200B',
75 | },
76 | ]);
77 | }
78 |
79 | return msg.channel.send({ embeds: [embed] });
80 | }
81 |
82 | const command =
83 | this.bot.commands.get(commandName) ||
84 | (this.bot.aliases.has(commandName) ? this.bot.commands.get(this.bot.aliases.get(commandName)) : null);
85 | if (!command) return this.commandError(msg, `Command \`${commandName}\` doesn't exist`);
86 |
87 | const embed = new this.embed()
88 | .setColor('#84F139')
89 | .setTitle(`Command \`${command.help.name}\``)
90 | .setDescription(`${command.help.description || command.help.summary}\n\u200B`)
91 | .setFooter({ text: this.bot.user.username, iconURL: this.bot.user.avatarURL() })
92 | .addFields(
93 | [
94 | { name: 'Usage', value: `\`${this.bot.prefix}${command.help.usage}\`` },
95 | command.conf.aliases?.length && { name: 'Aliases', value: command.conf.aliases.map((e) => `\`${e}\``).join(', ') },
96 | command.help.examples?.length && {
97 | name: 'Examples',
98 | value: command.help.examples.map((e) => `\`${this.bot.prefix}${e}\``).join('\n'),
99 | },
100 | { name: 'Permission', value: `${this._permLevelToWord(command.conf.permLevel)}\n\u200B`, inline: true },
101 | { name: 'Guild Only', value: command.conf.guildOnly ? 'Yes' : 'No', inline: true },
102 | ].filter(Boolean)
103 | );
104 |
105 | return msg.channel.send({ embeds: [embed] });
106 | }
107 | }
108 |
109 | module.exports = HelpCommand;
110 |
--------------------------------------------------------------------------------
/lib/Discord/Commands/Invite.js:
--------------------------------------------------------------------------------
1 | const Command = require('../Command');
2 |
3 | class InviteCommand extends Command {
4 | constructor(bot) {
5 | super(bot);
6 |
7 | this.props.help = {
8 | name: 'invite',
9 | description: 'get invite link',
10 | usage: 'invite',
11 | };
12 | }
13 |
14 | run(msg) {
15 | const botInviteLink = `https://discordapp.com/oauth2/authorize?permissions=67193856&scope=bot&client_id=${this.bot.user.id}`;
16 | const serverInviteLink = 'http://discord.gg/HHqndMG';
17 |
18 | const embed = new this.embed()
19 | .setTitle('Yappy, the GitLab Monitor')
20 | .setDescription(['__Invite Link__:', `**<${botInviteLink}>**`, '', '__Official Server__:', `**<${serverInviteLink}>**`].join('\n'))
21 | .setColor('#84F139')
22 | .setThumbnail(this.bot.user.avatarURL());
23 |
24 | return msg.author.send({ embeds: [embed] }).then(() =>
25 | msg.channel.send({
26 | embeds: [new this.embed().setTitle('Yappy, the GitLab Monitor').setDescription('📬 Sent invite link!')],
27 | })
28 | );
29 | }
30 | }
31 |
32 | module.exports = InviteCommand;
33 |
--------------------------------------------------------------------------------
/lib/Discord/Commands/Ping.js:
--------------------------------------------------------------------------------
1 | const Command = require('../Command');
2 |
3 | class PingCommand extends Command {
4 | constructor(bot) {
5 | super(bot);
6 | this.props.help = {
7 | name: 'ping',
8 | description: 'ping, pong',
9 | usage: 'ping',
10 | };
11 | }
12 | run(msg) {
13 | const startTime = msg.createdTimestamp;
14 | return msg.channel.send(`⏱ Pinging...`).then((message) => {
15 | const endTime = message.createdTimestamp;
16 | let difference = (endTime - startTime).toFixed(0);
17 | if (difference > 1000) difference = (difference / 1000).toFixed(0);
18 | let differenceText = endTime - startTime > 999 ? 's' : 'ms';
19 | return message.edit(
20 | `⏱ Ping, Pong! The message round-trip took ${difference} ${differenceText}. The heartbeat ping is ${this.bot.ws.ping.toFixed(0)}ms`
21 | );
22 | });
23 | }
24 | }
25 |
26 | module.exports = PingCommand;
27 |
--------------------------------------------------------------------------------
/lib/Discord/Commands/Reboot.js:
--------------------------------------------------------------------------------
1 | const Command = require('../Command');
2 |
3 | class RebootCommand extends Command {
4 | constructor(bot) {
5 | super(bot);
6 | this.props.help = {
7 | name: 'reboot',
8 | description: 'reboot bot',
9 | usage: 'ping',
10 | };
11 | this.setConf({
12 | permLevel: 2,
13 | });
14 | }
15 | run(msg) {
16 | return msg.channel
17 | .send({
18 | embeds: [
19 | {
20 | color: 0x2ecc71,
21 | title: 'Updating',
22 | description: 'Restarting...',
23 | },
24 | ],
25 | })
26 | .then(() => {
27 | Log.info('RESTARTING - Executed `reboot` command');
28 | process.exit();
29 | });
30 | }
31 | }
32 |
33 | module.exports = RebootCommand;
34 |
--------------------------------------------------------------------------------
/lib/Discord/Commands/Reload.js:
--------------------------------------------------------------------------------
1 | const Command = require('../Command');
2 |
3 | class ReloadCommand extends Command {
4 | constructor(bot) {
5 | super(bot);
6 | this.setHelp({
7 | name: 'reload',
8 | description: 'reloads a command, duh',
9 | usage: 'reload ',
10 | examples: ['reload stats', 'reload test'],
11 | });
12 | this.setConf({
13 | permLevel: 2,
14 | aliases: ['r'],
15 | });
16 | }
17 | run(msg, args) {
18 | let argName = args[0] ? args[0].toLowerCase() : null;
19 | let bot = this.bot;
20 | let command = bot.commands.get(argName);
21 | if (argName === 'all') {
22 | return this.reloadAllCommands(msg).catch((err) => this.sendError(`all`, null, err, msg));
23 | } else if (!command && bot.aliases.has(argName)) {
24 | command = bot.commands.get(bot.aliases.get(argName));
25 | } else if (!argName) {
26 | return this.errorUsage(msg);
27 | } else if (!command) {
28 | return msg.channel.send(`❌ Command \`${argName}\` doesn't exist`);
29 | }
30 | let fileName = command ? command.help.file : args[0];
31 | let cmdName = command ? command.help.name : args[0];
32 |
33 | msg.channel.send(`⚙ Reloading Command \`${cmdName}\`...`).then((m) => {
34 | bot.reloadCommand(fileName)
35 | .then(() => {
36 | m.edit(`✅ Successfully Reloaded Command \`${cmdName}\``);
37 | })
38 | .catch((e) => this.sendError(cmdName, m, e));
39 | });
40 | }
41 | sendError(t, m, e, msg) {
42 | let content = [`❌ Unable To Reload \`${t}\``, '```js', e.stack ? e.stack.replace(this._path, `.`) : e, '```'];
43 | if (m) {
44 | return m.edit(content);
45 | } else {
46 | return msg.channel.send(content);
47 | }
48 | }
49 | async reloadAllCommands(msg) {
50 | let m = await msg.channel.send(`⚙ Reloading All Commands...`);
51 | this.bot.commands.forEach((command) => {
52 | let cmdName = command.help.file || command.help.name;
53 | this.bot.reloadCommand(cmdName).catch((err) => {
54 | this.sendError(cmdName, null, err, msg);
55 | });
56 | });
57 | return m.edit(`✅ Successfully Reloaded All Commands`);
58 | }
59 | }
60 |
61 | module.exports = ReloadCommand;
62 |
--------------------------------------------------------------------------------
/lib/Discord/Commands/Stats.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment');
2 | const DiscordJS = require('discord.js');
3 | const Command = require('../Command');
4 | const pack = require('../../../package.json');
5 |
6 | require('moment-duration-format');
7 |
8 | const unit = ['', 'K', 'M', 'G', 'T', 'P'];
9 | const getUptime = (bot) => moment.duration(bot.uptime).format('d[ days], h[ hours], m[ minutes, and ]s[ seconds]');
10 | const bytesToSize = (input, precision) => {
11 | let index = Math.floor(Math.log(input) / Math.log(1024));
12 | if (unit >= unit.length) return `${input} B`;
13 | let msg = `${(input / Math.pow(1024, index)).toFixed(precision)} ${unit[index]}B`;
14 | return msg;
15 | };
16 |
17 | class StatsCommand extends Command {
18 | constructor(bot) {
19 | super(bot);
20 | this.props.help = {
21 | name: 'stats',
22 | description: 'Shows some stats of the bot... what else?',
23 | usage: 'stats',
24 | };
25 | this.setConf({
26 | aliases: ['info'],
27 | });
28 | }
29 | run(msg) {
30 | const bot = this.bot;
31 | const MemoryUsage = bytesToSize(process.memoryUsage().heapUsed, 3);
32 | const booted = bot.booted;
33 | const channels = bot.channels?.cache;
34 |
35 | const textChannels = channels?.filter((e) => e.type !== 'voice').size ?? '??';
36 | const voiceChannels = channels?.filter((e) => e.type === 'voice').size ?? '??';
37 |
38 | const embed = new this.embed()
39 | .setTitle('Stats')
40 | .setDescription('Bot statistics here.')
41 | .setColor('#84F139')
42 | .addFields([
43 | { name: '❯ Uptime', value: getUptime(bot), inline: true },
44 | { name: '❯ Booted', value: `${booted.date} ${booted.time}`, inline: true },
45 | { name: '❯ Memory Usage', value: MemoryUsage, inline: true },
46 | { name: '\u200B', value: '\u200B', inline: false },
47 | { name: '❯ Guilds', value: String(bot.guilds.cache?.size) ?? '???', inline: true },
48 | { name: '❯ Channels', value: `${channels.size} (${textChannels} text, ${voiceChannels} voice)`, inline: true },
49 | { name: '❯ Users', value: String(bot.users.cache?.size) ?? '???', inline: true },
50 | { name: '\u200B', value: '\u200B', inline: false },
51 | { name: '❯ Author', value: pack.author.replace(/<\S+[@]\S+[.]\S+>/g, ''), inline: true },
52 | { name: '❯ Version', value: pack.version, inline: true },
53 | { name: '❯ DiscordJS', value: `v${DiscordJS.version}`, inline: true },
54 | ]);
55 |
56 | return msg.channel.send({ embeds: [embed] });
57 | }
58 | }
59 |
60 | module.exports = StatsCommand;
61 |
--------------------------------------------------------------------------------
/lib/Discord/Commands/Update.js:
--------------------------------------------------------------------------------
1 | const { exec } = require('child_process');
2 | const path = require('path');
3 | const fs = require('fs');
4 | const jsondiffpatch = require('jsondiffpatch');
5 | const beforePackageJSON = require('../../../package.json');
6 | const Command = require('../Command');
7 |
8 | class UpdateCommand extends Command {
9 | constructor(bot) {
10 | super(bot);
11 |
12 | this.props.help = {
13 | name: 'update',
14 | description: 'update the bot',
15 | usage: 'update [commit/branch]',
16 | };
17 |
18 | this.setConf({
19 | permLevel: 2,
20 | });
21 | }
22 |
23 | run(msg, args) {
24 | const commitOrBranch = args[0];
25 | let embedData = {
26 | title: 'Updating',
27 | color: 0xfb9738,
28 | description: '\u200B',
29 | fields: [],
30 | footer: {
31 | text: this.bot.user.username,
32 | icon_url: this.bot.user.avatarURL(),
33 | },
34 | };
35 | let message;
36 |
37 | return msg.channel
38 | .send({
39 | embeds: [embedData],
40 | })
41 | .then((m) => {
42 | message = m;
43 | return this.exec('git pull');
44 | })
45 | .then((stdout) => {
46 | if (stdout.includes('Already up-to-date')) {
47 | return this.addFieldToEmbed(message, embedData, {
48 | name: 'Git Pull',
49 | value: 'Already up-to-date',
50 | }).then((m) => {
51 | embedData = m.embeds[0];
52 | return Promise.reject('No update');
53 | });
54 | }
55 | return this.addFieldToEmbed(message, embedData, {
56 | name: 'Git Pull',
57 | value: `\`\`\`sh\n${stdout}\n\`\`\``,
58 | });
59 | })
60 | .then(() => {
61 | if (commitOrBranch) return this.exec(`git checkout ${commitOrBranch}`);
62 | return;
63 | })
64 | .then(this.getDepsToInstall)
65 | .then((info) => {
66 | if (!info) return Promise.resolve();
67 | return this.addFieldToEmbed(message, embedData, {
68 | name: 'Dependencies',
69 | value: [
70 | ...(info.install.length ? ['**Install:**', [...info.install].map((e) => `- \`${e[0]}@${e[1]}\`\n`).join('')] : []),
71 | ...(info.update.length ? ['**Update:**', info.update.map((e) => `- \`${e[0]}@${e[1]} -> ${e[2]}\`\n`).join('')] : []),
72 | ...(info.remove.length ? ['**Remove:**', [...info.remove].map((e) => `- \`${e[0]}@${e[1]}\`\n`).join('')] : []),
73 | ].join('\n'),
74 | }).then(() => info);
75 | })
76 | .then((info) => {
77 | if (!info) return Promise.resolve();
78 | return this.installDeps(info).then((stdouts) =>
79 | this.addFieldToEmbed(message, embedData, {
80 | name: 'NPM',
81 | value: stdouts.map((stdout) => `\`\`\`sh\n${stdout.slice(0, 1000)}\n\`\`\``).join('\n') || 'No output',
82 | })
83 | );
84 | })
85 | .then(() =>
86 | message.channel.send({
87 | embeds: [
88 | {
89 | color: 0x2ecc71,
90 | title: 'Updating',
91 | description: 'Restarting...',
92 | },
93 | ],
94 | })
95 | )
96 | .then(() => {
97 | Log.info('RESTARTING - Executed `update` command');
98 | process.exit(0);
99 | })
100 | .catch((err) => {
101 | if (err === 'No update') return;
102 | return this.commandError(msg, err);
103 | });
104 | }
105 |
106 | getDepsToInstall() {
107 | return new Promise((resolve, reject) => {
108 | fs.readFile(path.resolve(__dirname, '../../../package.json'), (err, content) => {
109 | if (err) return reject(err);
110 | const afterPackageJSON = JSON.parse(content);
111 | delete afterPackageJSON.dependencies.debug;
112 | const diff = jsondiffpatch.diff(beforePackageJSON.dependencies, afterPackageJSON.dependencies);
113 | if (!diff) return resolve();
114 | let data = {
115 | install: Object.keys(diff)
116 | .filter((e) => diff[e].length === 1)
117 | .map((e) => [e, diff[e][0]]),
118 | update: Object.keys(diff)
119 | .filter((e) => diff[e].length === 2)
120 | .map((e) => [e, diff[e][0], diff[e][1]]),
121 | remove: Object.keys(diff)
122 | .filter((e) => diff[e].length === 3)
123 | .map((e) => [e, diff[e][0]]),
124 | };
125 | resolve(data);
126 | });
127 | });
128 | }
129 |
130 | async installDeps(data) {
131 | let stdouts = [
132 | data.install.length && (await this.exec(`npm i --no-progress ${data.install.map((e) => `${e[0]}@${e[1]}`).join(' ')}`)),
133 | data.update.length && (await this.exec(`npm upgrade --no-progress ${data.update.map((e) => `${e[0]}@${e[1]}`).join(' ')}`)),
134 | data.remove.length && (await this.exec(`npm rm --no-progress ${data.remove.map((e) => e[0]).join(' ')}`)),
135 | ];
136 | return stdouts.filter((e) => !!e);
137 | }
138 |
139 | addFieldToEmbed(message, data, field) {
140 | data.fields.push(field);
141 | return message.edit({ embeds: [data] });
142 | }
143 |
144 | exec(cmd, opts = {}) {
145 | return new Promise((resolve, reject) => {
146 | exec(cmd, opts, (err, stdout, stderr) => {
147 | if (err) return reject(stderr);
148 | resolve(stdout);
149 | });
150 | });
151 | }
152 | }
153 |
154 | module.exports = UpdateCommand;
155 |
--------------------------------------------------------------------------------
/lib/Discord/Module.js:
--------------------------------------------------------------------------------
1 | const { EmbedBuilder, Message } = require('discord.js');
2 |
3 | /**
4 | * Discord bot middleware, or module
5 | * @property {EmbedBuilder.constructor} embed
6 | * @property {Client} bot
7 | */
8 | class Module {
9 | /**
10 | * @param {Client} bot - discord bot
11 | */
12 | constructor(bot) {
13 | this.bot = bot;
14 | this._path = Log._path;
15 | this.embed = EmbedBuilder;
16 | }
17 |
18 | /**
19 | * Middleware's priority
20 | * @readonly
21 | * @type {number}
22 | */
23 | get priority() {
24 | return 0;
25 | }
26 |
27 | /**
28 | * Init module
29 | */
30 | init() {}
31 |
32 | /**
33 | * Bot's message middleware function
34 | * @param {Message} msg - the message
35 | * @param {string[]} args - message split by spaces
36 | * @param {function} next - next middleware pls <3
37 | */
38 | run() {
39 | throw new Error(`No middleware method was set up in module ${this.constructor.name}`);
40 | }
41 |
42 | /**
43 | * Function to shorten sending error messages
44 | * @param {Message} msg - message sent by user (for channel)
45 | * @param {string} str - error message to send user
46 | * @return {Promise}
47 | */
48 | moduleError(msg, str) {
49 | return msg.channel.send(`❌ ${str}`);
50 | }
51 |
52 | /**
53 | * Convert normal text to an embed object
54 | * @param {string} [title = 'Auto Generated Response'] - embed title
55 | * @param {string|string[]} text - embed description, joined with newline if array
56 | * @param {color} [color = '#84F139'] - embed color
57 | * @return {MessageBuilder}
58 | */
59 | textToEmbed(title = 'Auto Generated Response', text = null, color = '#84F139') {
60 | if (Array.isArray(text)) text = text.join('\n');
61 |
62 | const embed = new this.embed().setColor(color).setTitle(title).setFooter({
63 | text: this.bot.user.username,
64 | iconURL: this.bot.user.avatarURL(),
65 | });
66 |
67 | if (text) embed.setDescription(text);
68 |
69 | return embed;
70 | }
71 | }
72 |
73 | module.exports = Module;
74 |
--------------------------------------------------------------------------------
/lib/Discord/Modules/RunCommand.js:
--------------------------------------------------------------------------------
1 | const { ChannelType } = require('discord.js');
2 | const Module = require('../Module');
3 | const Logger = require('@YappyBots/addons').discord.logger;
4 |
5 | /**
6 | * @type {Logger}
7 | */
8 | let logger;
9 |
10 | class RunCommandModule extends Module {
11 | constructor(bot) {
12 | super(bot);
13 | logger = new Logger(bot, 'command');
14 | }
15 |
16 | get priority() {
17 | return 10;
18 | }
19 |
20 | /**
21 | * @inheritdoc
22 | */
23 | run(msg, args, next, command) {
24 | const bot = this.bot;
25 | const perms = bot.permissions(msg);
26 | let cmd;
27 |
28 | if (bot.commands.has(command)) {
29 | cmd = bot.commands.get(command);
30 | } else if (bot.aliases.has(command)) {
31 | cmd = bot.commands.get(bot.aliases.get(command));
32 | } else {
33 | return next();
34 | }
35 |
36 | if (msg.channel.type === ChannelType.DM && cmd.conf.guildOnly)
37 | return cmd.commandError(msg, `You can only run **${cmd.help.name}** in a guild.`);
38 |
39 | const hasPermission = perms >= cmd.conf.permLevel;
40 |
41 | logger.message(msg);
42 |
43 | if (!hasPermission)
44 | return cmd.commandError(msg, `Insufficient permissions! Must be **${cmd._permLevelToWord(cmd.conf.permLevel)}** or higher`);
45 |
46 | try {
47 | let commandRun = cmd.run(msg, args);
48 | if (commandRun && commandRun.catch) {
49 | commandRun.catch((e) => {
50 | logger.error(msg, e);
51 | return cmd.commandError(msg, e);
52 | });
53 | }
54 | } catch (e) {
55 | logger.error(msg, e);
56 | cmd.commandError(msg, e);
57 | }
58 | }
59 | }
60 |
61 | module.exports = RunCommandModule;
62 |
--------------------------------------------------------------------------------
/lib/Discord/Modules/UnhandledError.js:
--------------------------------------------------------------------------------
1 | const Module = require('../Module');
2 |
3 | class UnhandledErrorModule extends Module {
4 | run(msg, args, next, middleware, error) {
5 | if (!error) return;
6 | let embed = this.textToEmbed(
7 | `Yappy, the GitLab Monitor - Unhandled Error: \`${middleware ? middleware.constructor.name : msg.cleanContent}\``,
8 | '',
9 | '#CE0814'
10 | );
11 | if (typeof error === 'string') embed.setDescription(error);
12 |
13 | Log.error(error);
14 |
15 | return msg.channel.send({ embeds: [embed] });
16 | }
17 | }
18 |
19 | module.exports = UnhandledErrorModule;
20 |
--------------------------------------------------------------------------------
/lib/Discord/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { GatewayIntentBits, Partials, Options } = require('discord.js');
3 | const Client = require('./Client');
4 | const Log = require('../Util/Log');
5 | const bot = new Client({
6 | name: 'Yappy, the GitLab Monitor',
7 | owner: '175008284263186437',
8 |
9 | allowedMentions: { repliedUser: true },
10 |
11 | intents: [
12 | GatewayIntentBits.Guilds,
13 | GatewayIntentBits.GuildMessages,
14 | // GatewayIntentBits.MessageContent,
15 | // GatewayIntentBits.GuildMembers,
16 | GatewayIntentBits.GuildMessageReactions,
17 | GatewayIntentBits.DirectMessages,
18 | // GatewayIntentBits.GuildPresences,
19 | ],
20 | partials: [Partials.Channel],
21 | makeCache: Options.cacheWithLimits({
22 | ...Options.DefaultMakeCacheSettings,
23 | ReactionManager: 0,
24 | MessageManager: 50,
25 | GuildMemberManager: {
26 | maxSize: 100,
27 | keepOverLimit: (member) => member.id === bot.user.id,
28 | },
29 | }),
30 | });
31 | const logger = new (require('@YappyBots/addons').discord.logger)(bot, 'main');
32 | const TOKEN = process.env.DISCORD_TOKEN;
33 |
34 | const initialization = require('../Models/initialization');
35 |
36 | bot.booted = {
37 | date: new Date().toLocaleDateString(),
38 | time: new Date().toLocaleTimeString(),
39 | };
40 | bot.statuses = ['Online', 'Connecting', 'Reconnecting', 'Idle', 'Nearly', 'Offline'];
41 | bot.statusColors = ['lightgreen', 'orange', 'orange', 'orange', 'green', 'red'];
42 |
43 | bot.on('ready', () => {
44 | Log.info('Bot | Logged In');
45 | logger.log('Logged in', null, 'Green');
46 | initialization(bot);
47 | });
48 | bot.on('disconnect', (e) => {
49 | Log.warn(`Bot | Disconnected (${e.code}).`);
50 | logger.log('Disconnected', e.code, 'Orange');
51 | process.exit();
52 | });
53 | bot.on('error', (e) => {
54 | Log.error(e);
55 | logger.log(e.message || 'An error occurred', e.stack || e, 'Red');
56 | });
57 | bot.on('warn', (e) => {
58 | Log.warn(e);
59 | logger.log(e.message || 'Warning', e.stack || e, 'Orange');
60 | });
61 |
62 | bot.on('messageCreate', (msg) => {
63 | try {
64 | bot.run(msg);
65 | } catch (e) {
66 | bot.emit('error', e);
67 | }
68 | });
69 |
70 | bot.loadCommands(path.resolve(__dirname, 'Commands'));
71 | bot.loadModules(path.resolve(__dirname, 'Modules'));
72 |
73 | // === LOGIN ===
74 | Log.info(`Bot | Logging in with prefix ${bot.prefix}...`);
75 |
76 | bot.login(TOKEN).catch((err) => {
77 | Log.error('Bot: Unable to log in');
78 | Log.error(err);
79 | });
80 |
81 | module.exports = bot;
82 |
--------------------------------------------------------------------------------
/lib/Gitlab/Constants.js:
--------------------------------------------------------------------------------
1 | exports.Errors = {
2 | REQUIRE_QUERY: 'A query is required',
3 | NO_REPO_CONFIGURED: (e) =>
4 | `Repository for this channel hasn't been configured. Please tell the server owner that they need to run \`${e.bot.prefix}conf set repo \`.`,
5 | REPO_ALREADY_INITIALIZED: (e) => `Repository \`${e.repo}\` is already initialized in this channel`,
6 | };
7 |
8 | const api = `https://gitlab.com/api/v4`;
9 |
10 | exports.Endpoints = {
11 | projects: `${api}/projects`,
12 | Project: (projectID) => {
13 | if (projectID.repo) projectID = projectID.repo.replace(/\//g, '%2F');
14 | const base = `${api}/projects/${projectID}`;
15 | return {
16 | toString: () => base,
17 | issues: `${base}/issues`,
18 | Issue: (issueID) => `${base}/issues/${issueID}`,
19 | MergeRequest: (mrID) => `${base}/merge_requests/${mrID}`,
20 | MergeRequests: (params) => `${base}/merge_requests/${params ? '?' : ''}`,
21 | };
22 | },
23 | groups: `${api}/projects`,
24 | Group: (group) => {
25 | const base = `${api}/groups/${group}`;
26 | return {
27 | toString: () => base,
28 | projects: `${base}/projects`,
29 | };
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/lib/Gitlab/EventHandler.js:
--------------------------------------------------------------------------------
1 | const get = require('lodash/get');
2 | const fs = require('fs');
3 | const path = require('path');
4 | const bot = require('../Discord');
5 | const Log = require('../Util/Log');
6 | const parser = require('../Gitlab/parser');
7 |
8 | class Events {
9 | constructor() {
10 | this.events = {};
11 | this.eventDir = path.resolve(__dirname, './Events');
12 | this.eventsList = new Map();
13 |
14 | this.setup();
15 | }
16 |
17 | setGitlab(gitlab) {
18 | this.gitlab = gitlab;
19 | }
20 |
21 | setup() {
22 | fs.readdir(this.eventDir, (err, files) => {
23 | if (err) throw err;
24 |
25 | files.forEach((file) => {
26 | let eventName = file.replace(`.js`, ``);
27 | try {
28 | let event = require(`./Events/${eventName}`);
29 | this.eventsList.set(eventName, new event(this.gitlab, bot));
30 | Log.debug(`GitHub | Loaded Event ${eventName.replace(`-`, `/`)}`);
31 | } catch (e) {
32 | Log.error(`GitHub | Loaded Event ${eventName} ❌`);
33 | Log.error(e);
34 | }
35 | });
36 |
37 | return;
38 | });
39 | }
40 |
41 | use(data, eventName) {
42 | const action = data.object_attributes ? data.object_attributes.action : '';
43 | const noteableType = data.object_attributes && data.object_attributes.noteable_type ? data.object_attributes.noteable_type.toLowerCase() : '';
44 | eventName = eventName.replace(` Hook`, '').replace(/ /g, '_').toLowerCase();
45 | let event = action || noteableType ? `${eventName}-${action || noteableType}` : eventName;
46 | try {
47 | event = this.eventsList.get(event) || this.eventsList.get('Unknown');
48 | let text = event.text(data, eventName, action);
49 | return {
50 | embed: this.parseEmbed(event.embed(data, eventName, action), data),
51 | text: Array.isArray(text) ? text.join('\n') : text,
52 | };
53 | } catch (e) {
54 | Log.error(e);
55 | }
56 | }
57 |
58 | parseEmbed(embed, data) {
59 | if (!embed) return null;
60 |
61 | switch (embed.color) {
62 | case 'success':
63 | embed.color = 0x3ca553;
64 | break;
65 | case 'warning':
66 | embed.color = 0xfb5432;
67 | break;
68 | case 'danger':
69 | case 'error':
70 | embed.color = 0xce0814;
71 | break;
72 | default:
73 | if (embed.color) embed.color = typeof embed.color === 'string' ? parseInt(`0x${embed.color.replace(`0x`, ``)}`, 16) : embed.color;
74 | break;
75 | }
76 | const avatar = data.user ? data.user.avatar_url : data.user_avatar;
77 |
78 | embed.author = {
79 | name: data.user ? data.user.username : data.user_username || data.user_name,
80 | icon_url: avatar && avatar.startsWith('/') ? `https://gitlab.com${avatar}` : avatar,
81 | };
82 | embed.footer = {
83 | text: get(data, 'project.path_with_namespace') || parser.getRepo(get(data, 'repository.url')),
84 | };
85 | embed.url = embed.url || (data.object_attributes && data.object_attributes.url) || (data.project && data.project.web_url);
86 | embed.timestamp = new Date();
87 |
88 | if (embed.description) embed.description = embed.description.slice(0, 1000) + (embed.description.length > 1000 ? '...' : '');
89 |
90 | return embed;
91 | }
92 | }
93 |
94 | module.exports = new Events();
95 |
--------------------------------------------------------------------------------
/lib/Gitlab/EventResponse.js:
--------------------------------------------------------------------------------
1 | class EventResponse {
2 | constructor(bot, gitlab, info) {
3 | this.gitlab = gitlab;
4 | this.bot = bot;
5 | this._info = info;
6 | }
7 | get info() {
8 | return this._info;
9 | }
10 | }
11 |
12 | module.exports = EventResponse;
13 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/Unknown.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class Unkown extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: `This response is shown whenever an event fired isn't found.`,
7 | });
8 | }
9 | embed(data, eventName, actionName) {
10 | const action = actionName ? `/${actionName}` : '';
11 | return {
12 | color: 'danger',
13 | title: `Repository sent unknown event: \`${eventName}${action}\``,
14 | description: `This most likely means the developers have not gotten to styling this event.\nYou may want to disable this event if you don't want it with \`GL! conf filter events disable ${eventName}${action}\``,
15 | };
16 | }
17 | text(data, eventName, actionName) {
18 | const action = actionName ? `/${actionName}` : '';
19 | return [
20 | `🛑 An unknown event has been emitted.`,
21 | 'This most likely means the developers have not gotten to styling this event.',
22 | `The event in question was \`${eventName}${action}\``,
23 | ].join('\n');
24 | }
25 | }
26 |
27 | module.exports = Unkown;
28 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/issue-close.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class IssueClose extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event gets fired when an issue is closed',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const issue = data.object_attributes;
12 | return {
13 | color: 0xe9642d,
14 | title: `Closed issue #${issue.iid}: \`${issue.title}\``,
15 | description: issue.description,
16 | };
17 | }
18 |
19 | text(data) {
20 | const actor = data.user.name;
21 | const issue = data.object_attributes;
22 | return [`🛠 **${actor}** closed issue **#${issue.iid}** _${issue.title}_`, `<${issue.url}>`].join('\n');
23 | }
24 | }
25 |
26 | module.exports = IssueClose;
27 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/issue-open.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class IssueOpen extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event gets fired when a new issue is created',
7 | });
8 | }
9 |
10 | embed(data) {
11 | let issue = data.object_attributes;
12 | return {
13 | color: 0xe9642d,
14 | title: `Opened issue #${issue.iid}: \`${issue.title}\``,
15 | description: issue.description,
16 | };
17 | }
18 |
19 | text(data) {
20 | const actor = data.user.name;
21 | const issue = data.object_attributes;
22 | return [`🛠 **${actor}** opened issue **#${issue.iid}**`, ` ${issue.title}`, `<${issue.url}>`].join('\n');
23 | }
24 | }
25 |
26 | module.exports = IssueOpen;
27 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/issue-reopen.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class IssueReopen extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event gets fired when an issue is reopened',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const issue = data.object_attributes;
12 | return {
13 | color: 0xe9642d,
14 | title: `Reopened issue #${issue.iid} \`${issue.title}\``,
15 | description: issue.description,
16 | };
17 | }
18 |
19 | text(data) {
20 | const actor = data.user.name;
21 | const issue = data.object_attributes;
22 | return [`🛠 **${actor}** reopened issue **#${issue.iid}** _${issue.title}_`, `<${issue.url}>`].join('\n');
23 | }
24 | }
25 |
26 | module.exports = IssueReopen;
27 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/issue-update.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class IssueUpdate extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event gets fired when an issue is updated',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const issue = data.object_attributes;
12 | return {
13 | color: 0xe9642d,
14 | title: `Updated issue #${issue.iid} \`${issue.title}\``,
15 | description: issue.description,
16 | };
17 | }
18 |
19 | text(data) {
20 | const actor = data.user.name;
21 | const issue = data.object_attributes;
22 | return [`🛠 **${actor}** updated issue **#${issue.iid}** _${issue.title}_`, `<${issue.url}>`].join('\n');
23 | }
24 | }
25 |
26 | module.exports = IssueUpdate;
27 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/issue.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class Issue extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event gets fired when a test issue event is executed',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const issue = data.object_attributes;
12 |
13 | return {
14 | color: 0xfffc00,
15 | title: `Issue #${issue.iid}: ${issue.title}`,
16 | description: 'A test issue event was fired from the GitLab integrations page',
17 | fields: [
18 | {
19 | name: 'Description',
20 | value: `${issue.description.split('\n').slice(0, 4).join('\n').slice(0, 2000)}\n\u200B`,
21 | },
22 | {
23 | name: 'State',
24 | value: issue.state[0].toUpperCase() + issue.state.slice(1),
25 | inline: true,
26 | },
27 | {
28 | name: 'Created At',
29 | value: new Date(issue.created_at).toGMTString(),
30 | inline: true,
31 | },
32 | {
33 | name: 'Labels',
34 | value: data.labels && data.labels.length ? data.labels.map((l) => `\`${l.title}\``).join(', ') : 'None',
35 | },
36 | ],
37 | };
38 | }
39 |
40 | text(data) {
41 | const { user: actor, object_attributes: issue } = data;
42 |
43 | return [`🤦♂️ **${actor.name}** tested an issue event, and issue **#${issue.iid}** was sent.`, `<${issue.url}>`].join('\n');
44 | }
45 | }
46 |
47 | module.exports = Issue;
48 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/merge_request-approved.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class MergeRequestApproved extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event gets fired when a merge request is approved',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const mergeRequest = data.object_attributes;
12 | return {
13 | color: 0x149617,
14 | title: `Approved merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``,
15 | };
16 | }
17 |
18 | text(data) {
19 | const actor = data.user.name;
20 | const issue = data.object_attributes;
21 | return [`✔️ **${actor}** approved merge request **#${issue.iid}** _${issue.title}_`, `<${issue.url}>`].join('\n');
22 | }
23 | }
24 |
25 | module.exports = MergeRequestApproved;
26 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/merge_request-close.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class MergeRequestClose extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event gets fired when a new merge request is closed',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const mergeRequest = data.object_attributes;
12 | return {
13 | color: 0x149617,
14 | title: `Closed merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``,
15 | description: `${mergeRequest.description}\n\u200B`,
16 | fields: [
17 | {
18 | name: 'Source',
19 | value: `${mergeRequest.source.name}/${mergeRequest.source_branch}`,
20 | inline: true,
21 | },
22 | {
23 | name: 'Target',
24 | value: `${mergeRequest.target.name}/${mergeRequest.target_branch}`,
25 | inline: true,
26 | },
27 | {
28 | name: 'Status',
29 | value: mergeRequest.work_in_progress ? 'WIP' : 'Finished',
30 | inline: true,
31 | },
32 | ],
33 | };
34 | }
35 |
36 | text(data) {
37 | const actor = data.user.name;
38 | const mergeRequest = data.object_attributes;
39 | return [`🛠 **${actor}** closed merge request **#${mergeRequest.iid}**`, ` ${mergeRequest.title}`, `<${mergeRequest.url}>`].join(
40 | '\n'
41 | );
42 | }
43 | }
44 |
45 | module.exports = MergeRequestClose;
46 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/merge_request-merge.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class MergeRequestMerge extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event gets fired when a new merge request is merged',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const mergeRequest = data.object_attributes;
12 | const mergedCommitSha = mergeRequest.merge_commit_sha;
13 | return {
14 | color: 0x149617,
15 | title: `Merged merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``,
16 | description: `${mergeRequest.description}\n\u200B`,
17 | fields: [
18 | {
19 | name: 'Source',
20 | value: `${mergeRequest.source.name}/${mergeRequest.source_branch}`,
21 | inline: true,
22 | },
23 | {
24 | name: 'Target',
25 | value: `${mergeRequest.target.name}/${mergeRequest.target_branch}`,
26 | inline: true,
27 | },
28 | {
29 | name: 'Merge Commit',
30 | value: mergedCommitSha ? `\`${mergedCommitSha.slice(0, 7)}\`` : '???',
31 | inline: true,
32 | },
33 | ],
34 | };
35 | }
36 |
37 | text(data) {
38 | const actor = data.user.name;
39 | const mergeRequest = data.object_attributes;
40 | return [`🛠 **${actor}** merged merge request **#${mergeRequest.iid}**`, ` ${mergeRequest.title}`, `<${mergeRequest.url}>`].join(
41 | '\n'
42 | );
43 | }
44 | }
45 |
46 | module.exports = MergeRequestMerge;
47 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/merge_request-open.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 | const UrlRegEx = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
3 | const RemoveUrlEmbedding = (url) => `<${url}>`;
4 |
5 | class MergeRequestOpen extends EventResponse {
6 | constructor(...args) {
7 | super(...args, {
8 | description: 'This event gets fired when a new merge request is opened',
9 | });
10 | }
11 |
12 | embed(data) {
13 | const mergeRequest = data.object_attributes;
14 | const lastCommit = mergeRequest.last_commit;
15 | const lastCommitMessage = lastCommit.message.split('\n')[0].replace(UrlRegEx, RemoveUrlEmbedding);
16 | const lastCommitAuthor = lastCommit.author.name || data.user_username || data.user_name;
17 | return {
18 | color: 0x149617,
19 | title: `Opened merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``,
20 | description: `${mergeRequest.description}\n\u200B`,
21 | fields: [
22 | {
23 | name: 'Source',
24 | value: `${mergeRequest.source.name}/${mergeRequest.source_branch}`,
25 | inline: true,
26 | },
27 | {
28 | name: 'Target',
29 | value: `${mergeRequest.target.name}/${mergeRequest.target_branch}`,
30 | inline: true,
31 | },
32 | {
33 | name: 'Status',
34 | value: mergeRequest.work_in_progress ? 'WIP' : 'Finished',
35 | inline: true,
36 | },
37 | {
38 | name: 'Latest Commit',
39 | value: `[\`${lastCommit.id.slice(0, 7)}\`](${lastCommit.url}) ${lastCommitMessage} [${lastCommitAuthor}]`,
40 | },
41 | ],
42 | };
43 | }
44 |
45 | text(data) {
46 | const actor = data.user.name;
47 | const mergeRequest = data.object_attributes;
48 | return [`🛠 **${actor}** opened merge request **#${mergeRequest.iid}**`, ` ${mergeRequest.title}`, `<${mergeRequest.url}>`].join(
49 | '\n'
50 | );
51 | }
52 | }
53 |
54 | module.exports = MergeRequestOpen;
55 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/merge_request-reopen.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 | const UrlRegEx = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
3 | const RemoveUrlEmbedding = (url) => `<${url}>`;
4 |
5 | class MergeRequestReopen extends EventResponse {
6 | constructor(...args) {
7 | super(...args, {
8 | description: 'This event gets fired when a new merge request is reopened',
9 | });
10 | }
11 |
12 | embed(data) {
13 | const mergeRequest = data.object_attributes;
14 | const lastCommit = mergeRequest.last_commit;
15 | const lastCommitMessage = lastCommit.message.split('\n')[0].replace(UrlRegEx, RemoveUrlEmbedding);
16 | const lastCommitAuthor = lastCommit.author.name || data.user_username || data.user_name;
17 | return {
18 | color: 0x149617,
19 | title: `Repened merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``,
20 | description: `${mergeRequest.description}\n\u200B`,
21 | fields: [
22 | {
23 | name: 'Source',
24 | value: `${mergeRequest.source.name}/${mergeRequest.source_branch}`,
25 | inline: true,
26 | },
27 | {
28 | name: 'Target',
29 | value: `${mergeRequest.target.name}/${mergeRequest.target_branch}`,
30 | inline: true,
31 | },
32 | {
33 | name: 'Status',
34 | value: mergeRequest.work_in_progress ? 'WIP' : 'Finished',
35 | inline: true,
36 | },
37 | {
38 | name: 'Latest Commit',
39 | value: `[\`${lastCommit.id.slice(0, 7)}\`](${lastCommit.url}) ${lastCommitMessage} [${lastCommitAuthor}]`,
40 | },
41 | ],
42 | };
43 | }
44 |
45 | text(data) {
46 | const actor = data.user.name;
47 | const mergeRequest = data.object_attributes;
48 | return [`🛠 **${actor}** updated merge request **#${mergeRequest.iid}**`, ` ${mergeRequest.title}`, `<${mergeRequest.url}>`].join(
49 | '\n'
50 | );
51 | }
52 | }
53 |
54 | module.exports = MergeRequestReopen;
55 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/merge_request-unapproved.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class MergeRequestUnapproved extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event gets fired when a merge request is unapproved',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const mergeRequest = data.object_attributes;
12 | return {
13 | color: 0x149617,
14 | title: `Unapproved merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``,
15 | };
16 | }
17 |
18 | text(data) {
19 | const actor = data.user.name;
20 | const issue = data.object_attributes;
21 | return [`❌ **${actor}** unapproved merge request **#${issue.iid}** _${issue.title}_`, `<${issue.url}>`].join('\n');
22 | }
23 | }
24 |
25 | module.exports = MergeRequestUnapproved;
26 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/merge_request-update.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 | const UrlRegEx = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
3 | const RemoveUrlEmbedding = (url) => `<${url}>`;
4 |
5 | class MergeRequestUpdate extends EventResponse {
6 | constructor(...args) {
7 | super(...args, {
8 | description: 'This event gets fired when a merge request is updated',
9 | });
10 | }
11 |
12 | embed(data) {
13 | const mergeRequest = data.object_attributes;
14 | const lastCommit = mergeRequest.last_commit;
15 | const lastCommitMessage = lastCommit.message.split('\n')[0].replace(UrlRegEx, RemoveUrlEmbedding);
16 | const lastCommitAuthor = lastCommit.author.name || data.user_username || data.user_name;
17 | return {
18 | color: 0x149617,
19 | title: `updated merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``,
20 | description: `${mergeRequest.description}\n\u200B`,
21 | fields: [
22 | {
23 | name: 'Source',
24 | value: `${mergeRequest.source.name}/${mergeRequest.source_branch}`,
25 | inline: true,
26 | },
27 | {
28 | name: 'Target',
29 | value: `${mergeRequest.target.name}/${mergeRequest.target_branch}`,
30 | inline: true,
31 | },
32 | {
33 | name: 'Status',
34 | value: mergeRequest.work_in_progress ? 'WIP' : 'Finished',
35 | inline: true,
36 | },
37 | {
38 | name: 'Latest Commit',
39 | value: `[\`${lastCommit.id.slice(0, 7)}\`](${lastCommit.url}) ${lastCommitMessage} [${lastCommitAuthor}]`,
40 | },
41 | ],
42 | };
43 | }
44 |
45 | text(data) {
46 | const actor = data.user.name;
47 | const issue = data.object_attributes;
48 | return [`🛠 **${actor}** updated issue **#${issue.iid}** _${issue.title}_`, `<${issue.url}>`].join('\n');
49 | }
50 | }
51 |
52 | module.exports = MergeRequestUpdate;
53 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/note-commit.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class NoteCommit extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event gets fired when someone comments on a commit',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const comment = data.object_attributes;
12 | const sha = comment.commit_id.slice(0, 7);
13 | return {
14 | color: 0x996633,
15 | title: `Commented on commit \`${sha}\``,
16 | description: comment.note,
17 | };
18 | }
19 |
20 | text(data) {
21 | const actor = data.user.name;
22 | const comment = data.object_attributes;
23 | const sha = comment.commit_id.slice(0, 7);
24 | return [`**${actor}** commented on commit \`${sha}\``, ` ${comment.note.slice(0, 100).replace(/\n/g, '\n ')}`, `${comment.url}`].join(
25 | '\n'
26 | );
27 | }
28 | }
29 |
30 | module.exports = NoteCommit;
31 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/note-issue.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class NoteIssue extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event gets fired when someone comments on an issue',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const comment = data.object_attributes;
12 | const issue = data.issue;
13 | return {
14 | color: 0x996633,
15 | title: `Commented on issue #${issue.iid}: \`${issue.title}\``,
16 | description: comment.note,
17 | };
18 | }
19 |
20 | text(data) {
21 | const actor = data.user.name;
22 | const comment = data.object_attributes;
23 | const issue = data.issue;
24 | return [
25 | `**${actor}** commented on issue **#${issue.iid}** _${issue.title}_`,
26 | ` ${comment.note.slice(0, 100).replace(/\n/g, '\n ')}`,
27 | `${comment.url}`,
28 | ].join('\n');
29 | }
30 | }
31 |
32 | module.exports = NoteIssue;
33 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/note-mergerequest.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class NoteMergeRequest extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event gets fired when someone comments on a merge request',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const comment = data.object_attributes;
12 | const mergeRequest = data.merge_request;
13 | return {
14 | color: 0x996633,
15 | title: `Commented on merge request #${mergeRequest.iid}: \`${mergeRequest.title}\``,
16 | description: comment.note,
17 | };
18 | }
19 |
20 | text(data) {
21 | const actor = data.user.name;
22 | const comment = data.object_attributes;
23 | const mergeRequest = data.merge_request;
24 | return [
25 | `**${actor}** commented on merge request **#${mergeRequest.iid}** _${mergeRequest.title}_`,
26 | ` ${comment.note.slice(0, 100).replace(/\n/g, '\n ')}`,
27 | `${comment.url}`,
28 | ].join('\n');
29 | }
30 | }
31 |
32 | module.exports = NoteMergeRequest;
33 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/note-snippet.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class NoteSnippet extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event gets fired when someone comments on a code snippet',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const comment = data.object_attributes;
12 | const snippet = data.snippet;
13 | return {
14 | color: `#996633`,
15 | title: `Commented on snippet \`${snippet.title}\``,
16 | description: comment.note,
17 | };
18 | }
19 |
20 | text(data) {
21 | const actor = data.user.name;
22 | const comment = data.object_attributes;
23 | const snippet = data.snippet;
24 | return [
25 | `**${actor}** commented on snippet **#${snippet.title}**`,
26 | ` ${comment.note.slice(0, 100).replace(/\n/g, '\n ')}`,
27 | `${comment.url}`,
28 | ].join('\n');
29 | }
30 | }
31 |
32 | module.exports = NoteSnippet;
33 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/pipeline.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class Pipeline extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: `This event is fired when a pipeline job finishes.`,
7 | });
8 | }
9 | embed(data) {
10 | if (data.object_attributes.status !== 'success' && data.object_attributes.status !== 'failed') return;
11 |
12 | const branch = data.object_attributes.ref;
13 | const id = data.object_attributes.id;
14 | const result = data.object_attributes.status === 'success' ? 'passed' : 'failed';
15 | const color = result === 'passed' ? 'success' : 'error';
16 | const duration = data.object_attributes.duration;
17 |
18 | const url = `${data.project.web_url}/pipelines/${id}`;
19 | const description = `Pipeline [\`#${id}\`](${url}) of \`${branch}\` branch **${result}** in ${duration} seconds.`;
20 |
21 | return {
22 | color,
23 | description,
24 | };
25 | }
26 | text(data) {
27 | if (data.object_attributes.status !== 'success' && data.object_attributes.status !== 'failed') return;
28 |
29 | const actor = data.user.username;
30 | const branch = data.object_attributes.ref;
31 | const id = data.object_attributes.id;
32 | const result = data.object_attributes.status === 'success' ? 'passed' : 'failed';
33 | const icon = result === 'passed' ? ':white_check_mark:' : ':no_entry:';
34 | const duration = data.object_attributes.duration;
35 |
36 | const msg =
37 | `Pipeline \`#${id}\` of \`${branch}\` branch created by *${actor}* **${result}** in ${duration} seconds. ${icon}\n` +
38 | `<${data.project.web_url}/pipelines/${id}>`;
39 | return msg;
40 | }
41 | }
42 |
43 | module.exports = Pipeline;
44 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/push.js:
--------------------------------------------------------------------------------
1 | const GetBranchName = require('../../Util').GetBranchName;
2 | const EventResponse = require('../EventResponse');
3 |
4 | const UrlRegEx = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
5 | const RemoveUrlEmbedding = (url) => `<${url}>`;
6 |
7 | class Push extends EventResponse {
8 | constructor(...args) {
9 | super(...args, {
10 | description: `This event is fired when someone pushes commits to a branch.`,
11 | });
12 | }
13 | embed(data) {
14 | if (!data.commits || !data.commits.length) return;
15 |
16 | const branch = GetBranchName(data.ref);
17 | const commits = data.commits;
18 | const commitCount = data.total_commits_count;
19 | let pretext = commits
20 | .map((commit) => {
21 | const message = (commitCount === 1 ? commit.message : commit.message.split('\n')[0]).replace(UrlRegEx, RemoveUrlEmbedding);
22 | const author = commit.author.name || data.user_username || data.user_name;
23 | const sha = commit.id.slice(0, 7);
24 |
25 | return `[\`${sha}\`](${commit.url}) ${message} [${author}]`;
26 | })
27 | .slice(0, 5);
28 | const description = pretext.join('\n');
29 |
30 | return {
31 | color: 0x7289da,
32 | title: `Pushed ${commitCount} ${commitCount !== 1 ? 'commits' : 'commit'} to \`${branch}\``,
33 | url: `${data.project.web_url}/compare/${data.before.slice(0, 7)}...${data.after.slice(0, 7)}`,
34 | description,
35 | };
36 | }
37 | text(data) {
38 | if (!data.commits || !data.commits.length) return;
39 |
40 | const actor = data.user_username || data.user_name;
41 | const branch = GetBranchName(data.ref);
42 | const commits = data.commits || [];
43 | const commitCount = data.total_commits_count;
44 |
45 | if (!commitCount) return '';
46 |
47 | let commitArr = commits
48 | .map((commit) => {
49 | let commitMessage = commit.message.replace(/\n/g, '\n ').replace(UrlRegEx, RemoveUrlEmbedding);
50 | return ` \`${commit.id.slice(0, 7)}\` ${commitMessage} [${commit.author.name || actor}]`;
51 | })
52 | .slice(0, 5);
53 |
54 | return [
55 | `⚡ **${actor}** pushed ${commitCount} ${commitCount !== 1 ? 'commits' : 'commit'} to \`${branch}\``,
56 | ...commitArr,
57 | `${data.project.web_url}/compare/${data.before.slice(0, 7)}...${data.after.slice(0, 7)}`,
58 | ].join('\n');
59 |
60 | return msg;
61 | }
62 | }
63 |
64 | module.exports = Push;
65 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/tag_push.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class TagPush extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: `This event is fired when a tag is created/deleted`,
7 | });
8 | }
9 |
10 | embed(data) {
11 | const tag = data.ref ? data.ref.split('/')[2] : 'unknown';
12 | const sha = data.checkout_sha ? data.checkout_sha.slice(0, 7) : null;
13 | const message = data.messsage || '';
14 | const commitCount = data.total_commits_count;
15 | const isCreated = this.isCreated(data);
16 |
17 | return {
18 | color: 0xf0c330,
19 | title: `${this.isCreated(data) ? 'Created' : 'Deleted'} tag \`${tag}\` ${sha ? `from commit \`${sha}\`` : ''}${
20 | isCreated ? ` with ${commitCount} ${commitCount !== 1 ? 'commits' : 'commit'}` : ''
21 | }`,
22 | url: isCreated ? `${data.project.web_url}/tags/${tag}` : null,
23 | description: message,
24 | };
25 | }
26 |
27 | text(data) {
28 | const actor = data.user_username || data.user_name;
29 | const tag = data.ref ? data.ref.split('/')[2] : 'unknown';
30 | const sha = data.checkout_sha ? data.checkout_sha.slice(0, 7) : null;
31 | const message = data.messsage || '';
32 | const commitCount = data.total_commits_count;
33 | const isCreated = this.isCreated(data);
34 |
35 | return [
36 | `⚡ **${actor}** ${isCreated ? 'created' : 'deleted'} tag \`${tag}\` ${sha ? `from commit \`${sha}\`` : ''}${
37 | isCreated ? `with ${commitCount} ${commitCount !== 1 ? 'commits' : 'commit'}` : ''
38 | }`,
39 | ` ${message.split('\n')[0]}`,
40 | ` `,
41 | `${isCreated ? `<${data.project.web_url}/tags/${tag}>` : ''}`,
42 | ]
43 | .filter((e) => e !== '')
44 | .join('\n');
45 | }
46 |
47 | isCreated(data) {
48 | return data.before === '0000000000000000000000000000000000000000';
49 | }
50 | }
51 |
52 | module.exports = TagPush;
53 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/wiki_page-create.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class WikiPageCreate extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event is fired when a wiki page is created',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const page = data.object_attributes;
12 | return {
13 | color: 0x29bb9c,
14 | title: `Created wiki page \`${page.title}\``,
15 | description: page.content,
16 | };
17 | }
18 |
19 | text(data) {
20 | const actor = data.user.name;
21 | const page = data.object_attributes;
22 | return [
23 | `📰 **${actor}** created wiki page **${page.title}**`,
24 | (page.content ? ` ${page.content.slice(0, 100).replace(/\n/g, ' ')}\n` : '') + `<${page.url}>`,
25 | ].join('\n');
26 | }
27 | }
28 |
29 | module.exports = WikiPageCreate;
30 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/wiki_page-delete.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class WikiPageDelete extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event is fired when a wiki page is deleted',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const page = data.object_attributes;
12 | return {
13 | color: 0x29bb9c,
14 | title: `Deleted wiki page \`${page.title}\``,
15 | };
16 | }
17 |
18 | text(data) {
19 | const actor = data.user.name;
20 | const page = data.object_attributes;
21 | return [`📰 **${actor}** deleted wiki page **${page.title}**`].join('\n');
22 | }
23 | }
24 |
25 | module.exports = WikiPageDelete;
26 |
--------------------------------------------------------------------------------
/lib/Gitlab/Events/wiki_page-update.js:
--------------------------------------------------------------------------------
1 | const EventResponse = require('../EventResponse');
2 |
3 | class WikiPageUpdate extends EventResponse {
4 | constructor(...args) {
5 | super(...args, {
6 | description: 'This event is fired when a wiki page is updated',
7 | });
8 | }
9 |
10 | embed(data) {
11 | const page = data.object_attributes;
12 | return {
13 | color: 0x29bb9c,
14 | title: `Updated wiki page \`${page.title}\``,
15 | description: page.content,
16 | };
17 | }
18 |
19 | text(data) {
20 | const actor = data.user.name;
21 | const page = data.object_attributes;
22 | return [
23 | `📰 **${actor}** updated wiki page **${page.title}**`,
24 | ` ${page.content.slice(0, 100).replace(/\n/g, ' ')}`,
25 | `<${page.url}>`,
26 | ].join('\n');
27 | }
28 | }
29 |
30 | module.exports = WikiPageUpdate;
31 |
--------------------------------------------------------------------------------
/lib/Gitlab/index.js:
--------------------------------------------------------------------------------
1 | const EventHandler = require('./EventHandler');
2 | const got = require('got');
3 | const Constants = require('./Constants');
4 | const parse = require('./parser');
5 |
6 | /**
7 | * Gitlab class with custom helper methods, logs in immediatly if token is found
8 | */
9 | class Gitlab {
10 | constructor() {
11 | EventHandler.setGitlab(this.gitlab);
12 |
13 | this.Constants = Constants;
14 |
15 | Log.info(`GitLab | Set up!`);
16 | }
17 | /**
18 | * Get GitLab repository information
19 | * @param {String} ownerOrId - Repo's owner or full repository name/url
20 | * @param {String} [name] - Repo's name, required if ownerOrId is repo's owner
21 | * @return {Promise}
22 | */
23 | getRepo(ownerOrId, name) {
24 | const repoId = this._getRepoID(ownerOrId, name);
25 |
26 | return this._request(Constants.Endpoints.Project(repoId));
27 | }
28 |
29 | /**
30 | * Get GitLab repository's issues
31 | * @param {String} ownerOrId - repo's owner or full repo name/url
32 | * @param {String} [name] - repo's name, required if ownerOrId is repo's owner
33 | * @param {Object} [params = {}] - api params
34 | * @return {Promise}
35 | */
36 | getProjectIssues(ownerOrId, name, params) {
37 | const repoId = this._getRepoID(ownerOrId, name);
38 |
39 | return this._request(Constants.Endpoints.Project(repoId).issues, params);
40 | }
41 |
42 | /**
43 | * Get GitLab repository's specific issue
44 | * @param {String} ownerOrId - repo's owner or full repo name/url
45 | * @param {String} [name] - repo's name, required if ownerOrId is repo's owner
46 | * @param {String|Number} issueID - repo's issue
47 | * @return {Promise}
48 | */
49 | getProjectIssue(ownerOrId, name, issueID) {
50 | if (typeof issueID === 'string') issueID = parseInt(issueID);
51 | const repoId = this._getRepoID(ownerOrId, name);
52 |
53 | return this._request(Constants.Endpoints.Project(repoId).Issue(issueID));
54 | }
55 |
56 | /**
57 | * Get GitLab repository's issues
58 | * @param {String} ownerOrId - repo's owner or full repo name/url
59 | * @param {String} [name] - repo's name, required if ownerOrId is repo's owner
60 | * @param {Object} [params = {}] - api params
61 | * @return {Promise}
62 | */
63 | getProjectMergeRequests(ownerOrId, name, params) {
64 | const repoId = this._getRepoID(ownerOrId, name);
65 |
66 | return this._request(Constants.Endpoints.Project(repoId).MergeRequests(), params);
67 | }
68 |
69 | /**
70 | * Get GitLab repository's specific issue
71 | * @param {String} ownerOrId - repo's owner or full repo name/url
72 | * @param {String} [name] - repo's name, required if ownerOrId is repo's owner
73 | * @param {String|Number} issueID - repo's issue
74 | * @return {Promise}
75 | */
76 | getProjectMergeRequest(ownerOrId, name, issueID) {
77 | if (typeof issueID === 'string') issueID = parseInt(issueID);
78 |
79 | const repoId = this._getRepoID(ownerOrId, name);
80 |
81 | return this._request(Constants.Endpoints.Project(repoId).MergeRequest(issueID));
82 | }
83 |
84 | /**
85 | * Search GitLab organizations
86 | * @param {String} query - query
87 | * @return {Promise}
88 | */
89 | searchGroups(query) {
90 | return this._request(Constants.Endpoints.groups, {
91 | search: query,
92 | });
93 | }
94 |
95 | /**
96 | * Get GitLab organization's public projects
97 | * @param {String} org - organization name / id
98 | * @return {Promise}
99 | */
100 | getGroupProjects(org) {
101 | return this._request(Constants.Endpoints.Group(encodeURIComponent(org)).projects);
102 | }
103 |
104 | _getRepoID(ownerOrId, name) {
105 | const data = name && parse(`${ownerOrId}/${name}`);
106 | return encodeURIComponent(data ? data.repo : ownerOrId);
107 | }
108 |
109 | _request(url, searchParams) {
110 | return got(url.toString(), { searchParams, responseType: 'json' });
111 | }
112 | }
113 |
114 | module.exports = new Gitlab();
115 |
--------------------------------------------------------------------------------
/lib/Gitlab/parser.js:
--------------------------------------------------------------------------------
1 | const regex =
2 | /^(?:(?:https?:\/\/|git@)((?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))(?:\/|:))?([\w\.@\:\/\-~]+?)(\.git)?$/i;
3 |
4 | /**
5 | * Gitlab repository
6 | * @typedef {Object} GitlabParsedRepository
7 | * @property {String} repo full repository name, including owner & group
8 | * @property {String} repository same as .repo
9 | * @property {String} owner repo owner
10 | * @property {String} group repo group, if any
11 | * @property {String} name repo name
12 | */
13 |
14 | /**
15 | * Gitlab repository parser
16 | * @param {String} str input
17 | * @return {GitlabParsedRepository} output
18 | */
19 | module.exports = (str) => {
20 | if (!str || typeof str !== 'string' || !str.length) return {};
21 |
22 | const out = regex.exec(str);
23 |
24 | if (!out) return {};
25 |
26 | const repo = (out[2] && out[2].split('/')) || [];
27 | const parsed = {
28 | host: out[1],
29 | repo: out[2],
30 | };
31 |
32 | parsed.owner = repo[0];
33 | parsed.group = repo.length > 2 ? repo.slice(1, repo.length - 1) : null;
34 | parsed.name = repo[repo.length - 1];
35 | parsed.isGitlab = !parsed.host || parsed.host === 'gitlab.com';
36 |
37 | return parsed;
38 | };
39 |
40 | module.exports.getRepo = (str) => {
41 | const out = module.exports(str);
42 |
43 | return out && out.repo;
44 | };
45 |
--------------------------------------------------------------------------------
/lib/Models/Channel.js:
--------------------------------------------------------------------------------
1 | const bookshelf = require('.');
2 | const Model = require('./Model');
3 |
4 | require('./Guild');
5 | require('./ChannelRepo');
6 | require('./ChannelOrg');
7 |
8 | class Channel extends Model {
9 | get tableName() {
10 | return 'channels';
11 | }
12 |
13 | static get validKeys() {
14 | return ['repo', 'useEmbed'];
15 | }
16 |
17 | get casts() {
18 | return {
19 | useEmbed: 'boolean',
20 |
21 | eventsList: 'array',
22 | usersList: 'array',
23 | branchesList: 'array',
24 | };
25 | }
26 |
27 | guild() {
28 | return this.belongsTo('Guild');
29 | }
30 |
31 | repos() {
32 | return this.hasMany('ChannelRepo');
33 | }
34 |
35 | orgs() {
36 | return this.hasMany('ChannelOrg');
37 | }
38 |
39 | getRepos() {
40 | return this.related('repos').pluck('name');
41 | }
42 |
43 | getOrgs() {
44 | return this.related('org').pluck('name');
45 | }
46 |
47 | async addRepo(repo) {
48 | await this.related('repos').create({
49 | name: repo,
50 | });
51 |
52 | return this;
53 | }
54 |
55 | async addOrg(org) {
56 | await this.related('org').save({
57 | name: org,
58 | });
59 |
60 | return this;
61 | }
62 |
63 | static async find(channel, ...args) {
64 | let ch = await super.find(channel.id || channel, ...args);
65 |
66 | if (!ch && channel.id) ch = this.create(channel);
67 |
68 | return ch;
69 | }
70 |
71 | static create(channel) {
72 | if (!channel.guild) return Log.warn(`DB | Channels -- not creating channel \`${channel.id}\` w/o guild!`);
73 |
74 | Log.info(`DB | Channels + "${channel.guild?.name}"'s #${channel.name} (${channel.id})`);
75 |
76 | return this.forge({
77 | id: channel.id,
78 | name: channel.name,
79 |
80 | guildId: channel.guild?.id,
81 | }).save(null, {
82 | method: 'insert',
83 | });
84 | }
85 |
86 | /**
87 | * Delete channel
88 | * @param {external:Channel} channel
89 | * @param {boolean} [fail]
90 | */
91 | static delete(channel, fail = true) {
92 | Log.info(`DB | Channels - "${channel.guild.name}"'s #${channel.name} (${channel.id})`);
93 |
94 | return this.forge({
95 | id: channel.id,
96 | }).destroy({
97 | require: fail,
98 | });
99 | }
100 |
101 | static findByRepo(repo) {
102 | const r = repo.toLowerCase();
103 |
104 | return this.query((qb) => qb.join('channel_repos', 'channel_repos.channel_id', 'channels.id').where('channel_repos.name', r)).fetchAll();
105 | }
106 |
107 | static findByOrg(org) {
108 | const r = org.toLowerCase();
109 |
110 | return this.query((qb) => qb.join('channel_orgs', 'channel_orgs.channel_id', 'channels.id').where('channel_orgs.name', r)).fetchAll();
111 | }
112 |
113 | static async addRepoToChannel(channel, repo) {
114 | const ch = await this.find(channel);
115 |
116 | return ch && ch.addRepo(repo);
117 | }
118 | }
119 |
120 | module.exports = bookshelf.model('Channel', Channel);
121 |
--------------------------------------------------------------------------------
/lib/Models/ChannelOrg.js:
--------------------------------------------------------------------------------
1 | const bookshelf = require('.');
2 | const Model = require('./Model');
3 |
4 | require('./Channel');
5 |
6 | class ChannelOrg extends Model {
7 | get tableName() {
8 | return 'channel_orgs';
9 | }
10 |
11 | channel() {
12 | return this.belongsTo('Channel');
13 | }
14 | }
15 |
16 | module.exports = bookshelf.model('ChannelOrg', ChannelOrg);
17 |
--------------------------------------------------------------------------------
/lib/Models/ChannelRepo.js:
--------------------------------------------------------------------------------
1 | const bookshelf = require('.');
2 | const Model = require('./Model');
3 |
4 | require('./Channel');
5 |
6 | class ChannelRepo extends Model {
7 | get tableName() {
8 | return 'channel_repos';
9 | }
10 |
11 | channel() {
12 | return this.belongsTo('Channel');
13 | }
14 | }
15 |
16 | module.exports = bookshelf.model('ChannelRepo', ChannelRepo);
17 |
--------------------------------------------------------------------------------
/lib/Models/Guild.js:
--------------------------------------------------------------------------------
1 | const bookshelf = require('.');
2 | const Model = require('./Model');
3 |
4 | require('./Channel');
5 |
6 | class Guild extends Model {
7 | get tableName() {
8 | return 'guilds';
9 | }
10 |
11 | static get validKeys() {
12 | return [];
13 | }
14 |
15 | channels() {
16 | return this.belongsTo('Channel');
17 | }
18 |
19 | static create(guild) {
20 | Log.info(`DB | Guilds + Adding '${guild.name}' (${guild.id})`);
21 |
22 | return this.forge({
23 | id: guild.id,
24 | name: guild.name,
25 | }).save(null, {
26 | method: 'insert',
27 | });
28 | }
29 |
30 | /**
31 | * Delete guild
32 | * @param {external:Guild} guild
33 | * @param {boolean} [fail]
34 | */
35 | static delete(guild, fail = true) {
36 | Log.info(`DB | Guilds - Deleting '${guild.name}' (${guild.id})`);
37 |
38 | return this.forge({
39 | id: guild.id,
40 | }).destroy({
41 | require: fail,
42 | });
43 | }
44 | }
45 |
46 | module.exports = bookshelf.model('Guild', Guild);
47 |
--------------------------------------------------------------------------------
/lib/Models/Model.js:
--------------------------------------------------------------------------------
1 | const bookshelf = require('.');
2 | const _ = require('lodash');
3 |
4 | module.exports = class Model extends bookshelf.Model {
5 | static find(id, withRelated) {
6 | return this.forge({
7 | id,
8 | }).fetch({
9 | require: false,
10 | withRelated,
11 | });
12 | }
13 |
14 | parse(attrs) {
15 | const clone = _.mapKeys(attrs, function (value, key) {
16 | return _.camelCase(key);
17 | });
18 |
19 | if (this.casts)
20 | Object.keys(this.casts).forEach((key) => {
21 | const type = this.casts[key];
22 | const val = clone[key];
23 |
24 | if (type === 'boolean' && val !== undefined) {
25 | clone[key] = !(val === 'false' || val == 0);
26 | }
27 |
28 | if (type === 'array') {
29 | try {
30 | clone[key] = JSON.parse(val) || [];
31 | } catch (err) {
32 | clone[key] = [];
33 | }
34 | }
35 | });
36 |
37 | return clone;
38 | }
39 |
40 | format(attrs) {
41 | const clone = attrs;
42 |
43 | if (this.casts)
44 | Object.keys(this.casts).forEach((key) => {
45 | const type = this.casts[key];
46 | const val = clone[key];
47 |
48 | if (type === 'boolean' && val !== undefined) {
49 | clone[key] = Number(val === true || val === 'true');
50 | }
51 |
52 | if (type === 'array' && val) {
53 | clone[key] = JSON.stringify(val);
54 | }
55 | });
56 |
57 | return _.mapKeys(attrs, function (value, key) {
58 | return _.snakeCase(key);
59 | });
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/lib/Models/index.js:
--------------------------------------------------------------------------------
1 | const knex = require('knex')(require('../../knexfile'));
2 |
3 | const bookshelf = require('bookshelf')(knex);
4 |
5 | module.exports = bookshelf;
6 |
--------------------------------------------------------------------------------
/lib/Models/initialization.js:
--------------------------------------------------------------------------------
1 | const { default: PQueue } = require('p-queue');
2 | const Guild = require('./Guild');
3 | const Channel = require('./Channel');
4 | const { ChannelType } = require('discord.js');
5 |
6 | const loaded = { guilds: false, channels: false };
7 | const createQueue = () =>
8 | new PQueue({
9 | concurrency: 5,
10 | autoStart: false,
11 | });
12 |
13 | const addGuilds = async (bot) => {
14 | const queue = createQueue();
15 | const guilds = await Guild.fetchAll({});
16 |
17 | queue.addAll(bot.guilds.cache.filter((g) => g && g.id && !guilds.get(g.id)).map((g) => () => Guild.create(g)));
18 |
19 | await queue.start();
20 | };
21 |
22 | const addChannels = async (bot) => {
23 | const queue = createQueue();
24 | const channels = await Channel.fetchAll();
25 |
26 | queue.addAll(bot.channels.cache.filter((ch) => ch && ch.id && !channels.get(ch.id)).map((ch) => () => Channel.create(ch)));
27 |
28 | await queue.start();
29 | };
30 |
31 | module.exports = async (bot) => {
32 | if (!loaded.guilds) {
33 | loaded.guilds = true;
34 |
35 | bot.on('guildDelete', async (guild) => {
36 | if (!guild || !guild.available) return;
37 |
38 | await Guild.delete(guild, false);
39 | });
40 |
41 | bot.on('guildCreate', async (guild) => {
42 | if (!guild || !guild.available) return;
43 | if (await Guild.find(guild.id)) return;
44 |
45 | await Guild.create(guild);
46 | });
47 |
48 | await addGuilds(bot);
49 | }
50 |
51 | if (!loaded.channels) {
52 | loaded.channels = true;
53 |
54 | bot.on('channelDelete', async (channel) => {
55 | if (!channel || channel.type !== ChannelType.GuildText) return;
56 |
57 | await Channel.delete(channel, false);
58 | });
59 |
60 | bot.on('channelCreate', async (channel) => {
61 | if (!channel || channel.type !== ChannelType.GuildText) return;
62 | if (await Channel.find(channel.id)) return;
63 |
64 | await Channel.create(channel);
65 | });
66 |
67 | await addChannels(bot);
68 | }
69 | };
70 |
--------------------------------------------------------------------------------
/lib/Util/Branch.js:
--------------------------------------------------------------------------------
1 | module.exports = function GetBranchName(ref) {
2 | if (!ref) {
3 | return 'unknown';
4 | }
5 |
6 | // Slice ref/heads and leave the rest, it should be a branch name
7 | // for example refs/heads/feature/4 -> feature/4
8 | return ref.split('/').slice(2).join('/');
9 | };
10 |
--------------------------------------------------------------------------------
/lib/Util/Log.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const util = require('util');
3 | const winston = require('winston');
4 | const moment = require('moment');
5 | const cleanStack = require('clean-stack');
6 | const PrettyError = require('pretty-error');
7 | const pe = new PrettyError();
8 |
9 | const { BaseError: ShapeshiftBaseError } = require('@sapphire/shapeshift');
10 |
11 | pe.alias(process.cwd(), '.');
12 | pe.skipPackage('discord.js', 'ws');
13 |
14 | pe.appendStyle({
15 | 'pretty-error > trace > item': {
16 | marginBottom: 0,
17 | },
18 | });
19 |
20 | class Log {
21 | constructor() {
22 | this._colors = {
23 | error: 'red',
24 | warn: 'yellow',
25 | info: 'cyan',
26 | debug: 'green',
27 | message: 'white',
28 | verbose: 'grey',
29 | };
30 | this.logger = new winston.Logger({
31 | levels: {
32 | error: 0,
33 | warn: 1,
34 | info: 2,
35 | message: 3,
36 | verbose: 4,
37 | debug: 5,
38 | silly: 6,
39 | },
40 | transports: [
41 | new winston.transports.Console({
42 | colorize: true,
43 | prettyPrint: true,
44 | timestamp: () => moment().format('MM/D/YY HH:mm:ss'),
45 | align: true,
46 | level: process.env.LOG_LEVEL || 'info',
47 | }),
48 | ],
49 | exitOnError: false,
50 | });
51 |
52 | winston.addColors(this._colors);
53 |
54 | this.error = this.error.bind(this);
55 | this.warn = this.warn.bind(this);
56 | this.info = this.info.bind(this);
57 | this.verbose = this.verbose.bind(this);
58 | this.debug = this.debug.bind(this);
59 | this.silly = this.silly.bind(this);
60 |
61 | this._token = process.env.DISCORD_TOKEN;
62 | this._tokenRegEx = new RegExp(this._token, 'g');
63 | }
64 | error(error, ...args) {
65 | if (error.stack) error.stack = cleanStack(error.stack);
66 |
67 | // Do not pretty-render validation errors, as that gets rid of the actual error message!
68 | // Winston also seems to break display
69 | if (error instanceof ShapeshiftBaseError) {
70 | console.error(error);
71 | return this;
72 | }
73 |
74 | if (error instanceof Error) error = pe.render(error);
75 |
76 | this.logger.error(error, ...args);
77 | return this;
78 | }
79 | warn(warn, ...args) {
80 | this.logger.warn(warn, ...args);
81 | return this;
82 | }
83 | info(...args) {
84 | this.logger.info(...args);
85 | return this;
86 | }
87 | verbose(...args) {
88 | this.logger.verbose(...args);
89 | return this;
90 | }
91 | debug(arg, ...args) {
92 | if (typeof arg === 'object') arg = util.inspect(arg, { depth: 0 });
93 |
94 | this.logger.debug(arg, ...args);
95 | return this;
96 | }
97 | silly(...args) {
98 | this.logger.silly(...args);
99 | return this;
100 | }
101 | }
102 |
103 | module.exports = new Log();
104 |
--------------------------------------------------------------------------------
/lib/Util/MergeDefault.js:
--------------------------------------------------------------------------------
1 | module.exports = function merge(def, given) {
2 | if (!given) return def;
3 | for (const key in def) {
4 | if (!{}.hasOwnProperty.call(given, key)) {
5 | given[key] = def[key];
6 | } else if (given[key] === Object(given[key])) {
7 | given[key] = merge(def[key], given[key]);
8 | }
9 | }
10 |
11 | return given;
12 | };
13 |
--------------------------------------------------------------------------------
/lib/Util/Pad.js:
--------------------------------------------------------------------------------
1 | const Pad = (string, length) => string + ' '.repeat(length - string.length);
2 |
3 | module.exports = Pad;
4 |
--------------------------------------------------------------------------------
/lib/Util/YappyGitLab.js:
--------------------------------------------------------------------------------
1 | const { exec } = require('child_process');
2 | const path = require('path');
3 | const Log = require('./Log');
4 |
5 | class YappyGitLab {
6 | constructor() {
7 | this.directories = {
8 | root: path.resolve(__dirname, '../../'),
9 | Discord: path.resolve(__dirname, '../Discord'),
10 | DiscordCommands: path.resolve(__dirname, '../Discord/Commands'),
11 | Github: path.resolve(__dirname, '../Github'),
12 | Models: path.resolve(__dirname, '../Models'),
13 | Util: __dirname,
14 | };
15 |
16 | this.git = {
17 | release: '???',
18 | commit: '???',
19 | };
20 |
21 | this._setCommit().catch(Log.error);
22 | this._setRelease().catch(Log.error);
23 | }
24 |
25 | async execSync(command) {
26 | return new Promise((resolve, reject) => {
27 | exec(
28 | command,
29 | {
30 | cwd: this.directories.root,
31 | },
32 | (err, stdout, stderr) => {
33 | if (err) return reject(stderr);
34 | resolve(stdout);
35 | }
36 | );
37 | });
38 | }
39 |
40 | async _setRelease() {
41 | try {
42 | this.git.release = await this.execSync(`git describe --abbrev=0 --tags`);
43 | } catch (e) {
44 | if (e.includes('fatal: No names found, cannot describe anything.')) this.git.release = 'Unreleased';
45 | else Log.error(typeof e === 'object' ? e : new Error(e));
46 | }
47 | }
48 | async _setCommit() {
49 | this.git.commit = (await this.execSync(`git rev-parse HEAD`)).replace(/\n/, '');
50 | }
51 | }
52 |
53 | module.exports = new YappyGitLab();
54 |
--------------------------------------------------------------------------------
/lib/Util/filter.js:
--------------------------------------------------------------------------------
1 | const isFound = (data, item) => data.includes(item) || data.includes(item.split('/')[0]);
2 |
3 | module.exports = {
4 | whitelist:
5 | (data = []) =>
6 | (item) =>
7 | item ? isFound(data, item) : true,
8 | blacklist:
9 | (data = []) =>
10 | (item) =>
11 | item ? !isFound(data, item) : true,
12 | };
13 |
--------------------------------------------------------------------------------
/lib/Util/index.js:
--------------------------------------------------------------------------------
1 | const Pad = require('./Pad');
2 | const MergeDefault = require('./MergeDefault');
3 | const GetBranchName = require('./Branch');
4 |
5 | module.exports = { Pad, MergeDefault, GetBranchName };
6 |
--------------------------------------------------------------------------------
/lib/Web.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 | const get = require('lodash/get');
4 | const GitlabEventHandler = require('./Gitlab/EventHandler');
5 | const bot = require('./Discord');
6 | const addons = require('@YappyBots/addons');
7 |
8 | const GetBranchName = require('./Util').GetBranchName;
9 | const filter = require('./Util/filter');
10 | const parser = require('./Gitlab/parser');
11 |
12 | const Channel = require('./Models/Channel');
13 | const ChannelRepo = require('./Models/ChannelRepo');
14 |
15 | const app = express();
16 | const port = process.env.WEB_PORT || process.env.PORT || 8080;
17 | const ip = process.env.WEB_IP || process.env.IP || null;
18 |
19 | app.set('view engine', 'hbs');
20 |
21 | app.use(
22 | bodyParser.urlencoded({
23 | extended: true,
24 | limit: '5mb',
25 | })
26 | );
27 |
28 | app.use(
29 | bodyParser.json({
30 | limit: '5mb',
31 | })
32 | );
33 |
34 | app.use((req, res, next) => {
35 | if (req.headers['content-type'] === 'application/x-www-form-urlencoded' && req.body && req.body.payload) {
36 | req.body = JSON.parse(req.body.payload);
37 | }
38 | next();
39 | });
40 |
41 | app.get('/', async (req, res) => {
42 | const repos = await ChannelRepo.count();
43 | const status = bot.statuses[bot.ws.status];
44 | const statusColor = bot.statusColors[bot.ws.status];
45 |
46 | res.render('index', {
47 | bot,
48 | repos,
49 | status,
50 | statusColor,
51 | layout: 'layout',
52 |
53 | counts: {
54 | guilds: bot.guilds?.cache?.size ?? '???',
55 | channels: bot.channels?.cache?.size ?? '???',
56 | users: bot.users?.cache?.size ?? '???',
57 | },
58 | });
59 | });
60 |
61 | app.post('/', async (req, res) => {
62 | const event = req.headers['x-gitlab-event'];
63 | const eventName = event && event.replace(` Hook`, '').replace(/ /g, '_').toLowerCase();
64 | const data = req.body;
65 |
66 | if (!event || !data || (!data.project && !data.repository)) return res.status(403).send('Invalid data. Plz use Gitlab webhooks.');
67 |
68 | const repo = get(data, 'project.path_with_namespace') || parser.getRepo(get(data, 'repository.url'));
69 | const channels = (repo && (await Channel.findByRepo(repo))) || [];
70 |
71 | const action = get(data, 'object_attributes.action');
72 | const actionText = action ? `/${action}` : '';
73 |
74 | Log.verbose(`GitLab | ${repo} - ${eventName}${actionText} (${channels.length} channels)`);
75 |
76 | res.send(`${repo} : Received ${eventName}${actionText}, emitting to ${channels.length} channels...`);
77 |
78 | const eventResponse = GitlabEventHandler.use(data, event);
79 |
80 | if (!eventResponse) return res.status(500).send('An error occurred when generating the Discord message');
81 | if (!eventResponse.embed && !eventResponse.text) return Log.warn(`GitLab | ${repo} - ${eventName}${actionText} ignored`);
82 |
83 | const handleError = (resp, channel) => {
84 | const err = (resp && resp.body) || resp;
85 | const errors = ['Forbidden', 'Missing Access'];
86 | if (!res || !err) return;
87 | if (channel?.guild?.owner && (errors.includes(err.message) || (err.error && errors.includes(err.error.message)))) {
88 | channel.guild.owner.send(`**ERROR:** Yappy GitLab doesn't have permissions to read/send messages in ${channel}`);
89 | } else {
90 | channel.guild?.owner?.send(
91 | [
92 | `**ERROR:** An error occurred when trying to read/send messages in ${channel}.`,
93 | "Please report this to the bot's developer\n",
94 | '```js\n',
95 | err,
96 | '\n```',
97 | ].join(' ')
98 | );
99 | Log.error(err);
100 | }
101 | };
102 |
103 | const actor = {
104 | name: get(data, 'user.username') || data.user_username,
105 | id: get(data, 'user.id') || data.user_id,
106 | };
107 | const branch = data.ref ? GetBranchName(data.ref) : data.object_attributes && data.object_attributes.ref;
108 |
109 | channels.forEach(async (conf) => {
110 | const wantsEmbed = conf.get('useEmbed');
111 | const channel = await bot.channels.fetch(conf.id).catch((err) => {
112 | if (err.message === 'Unknown Channel') {
113 | Log.warn(`Unable to fetch channel ${conf.id} -- ${err.name} ${err.message} (${err.status})`);
114 | } else {
115 | Log.error(err);
116 | }
117 | });
118 |
119 | if (!channel) return;
120 |
121 | if (
122 | !filter[conf.get('eventsType')](conf.get('eventsList'))(eventName + actionText) ||
123 | !filter[conf.get('usersType')](conf.get('usersList'))(actor.name) ||
124 | !filter[conf.get('branchesType')](conf.get('branchesList'))(branch)
125 | ) {
126 | return;
127 | }
128 |
129 | try {
130 | if (wantsEmbed) await channel.send({ embeds: [eventResponse.embed] });
131 | else await channel.send(`**${repo}**: ${eventResponse.text}`);
132 | } catch (err) {
133 | handleError(err, channel);
134 | }
135 | });
136 | });
137 |
138 | app.use(
139 | addons.express.middleware(
140 | bot,
141 | {
142 | Channel: require('./Models/Channel'),
143 | Guild: require('./Models/Guild'),
144 | },
145 | {
146 | CLIENT_ID: process.env.DISCORD_CLIENT_ID,
147 | CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET,
148 | host: process.env.BDPW_KEY ? 'https://yappy.dsev.dev/gitlab' : `http://localhost:${port}`,
149 | }
150 | )
151 | );
152 |
153 | app.use((err, req, res, next) => {
154 | if (err) Log.error(err);
155 | res.status(500);
156 | res.send(err.stack);
157 | });
158 |
159 | app.listen(port, ip, () => {
160 | Log.info(`Express | Listening on ${ip || 'localhost'}:${port}`);
161 | });
162 |
163 | module.exports = app;
164 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | global.Log = require('./Util/Log');
4 |
5 | exports.Web = require('./Web');
6 | exports.Models = require('./Models');
7 | exports.Util = require('./Util');
8 | if (process.env.IS_DOCKER !== 'true') exports.YappyGitLab = require('./Util/YappyGitLab');
9 | exports.Discord = require('./Discord');
10 | exports.Gitlab = require('./Gitlab');
11 |
12 | process.on('unhandledRejection', Log.error);
13 | process.on('uncaughtException', Log.error);
14 |
15 | /**
16 | * Discord.JS's Client
17 | * @external {Client}
18 | * @see {@link https://discord.js.org/#/docs/main/master/class/Client}
19 | */
20 |
21 | /**
22 | * Discord.JS's Guild
23 | * @external {Guild}
24 | * @see {@link https://discord.js.org/#/docs/main/master/class/Guild}
25 | */
26 |
27 | /**
28 | * Discord.JS's Channel
29 | * @external {Channel}
30 | * @see {@link https://discord.js.org/#/docs/main/master/class/Channel}
31 | */
32 |
33 | /**
34 | * Discord.JS's Message
35 | * @external {Message}
36 | * @see {@link https://discord.js.org/#/docs/main/master/class/Message}
37 | */
38 |
39 | /**
40 | * Discord.JS's Collection
41 | * @external {Collection}
42 | * @see {@link https://discord.js.org/#/docs/main/master/class/Collection}
43 | */
44 |
45 | /**
46 | * Discord.JS's Client Options
47 | * @external {ClientOptions}
48 | * @see {@link https://discord.js.org/#/docs/main/master/typedef/ClientOptions}
49 | */
50 |
51 | /**
52 | * Discord.JS's Color Resolvable
53 | * @external {ColorResolvable}
54 | * @see {@link https://discord.js.org/#/docs/main/master/typedef/ColorResolvable}
55 | */
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@YappyBots/GitLab",
3 | "version": "1.9.1",
4 | "description": "A GitLab repo monitor bot for Discord",
5 | "main": "lib/index.js",
6 | "private": true,
7 | "scripts": {
8 | "start": "node lib/index.js",
9 | "lint": "prettier --single-quote --trailing-comma es5 --print-width 150 --tab-width 4 --write lib db/migrations",
10 | "docs": "docgen --source lib --jsdoc .jsdoc.json --custom docs/index.yml --output docs/docs.json",
11 | "docs:test": "docgen --source lib --jsdoc .jsdoc.json --custom docs/index.yml",
12 | "db:migrate": "knex migrate:latest"
13 | },
14 | "repository": {
15 | "url": "https://github.com/YappyBots/YappyGitLab",
16 | "type": "git"
17 | },
18 | "author": "David Sevilla Martin ",
19 | "license": "MIT",
20 | "dependencies": {
21 | "@YappyBots/addons": "github:YappyBots/yappy-addons#1107d5d",
22 | "body-parser": "^2.2.0",
23 | "bookshelf": "^1.2.0",
24 | "bufferutil": "^4.0.9",
25 | "chalk": "^4.0.0",
26 | "clean-stack": "^3.0.0",
27 | "cookie-parser": "^1.4.6",
28 | "discord.js": "^14.19.3",
29 | "dotenv": "^16.0.0",
30 | "express": "^4.17.2",
31 | "got": "^11.8.6",
32 | "hbs": "^4.2.0",
33 | "jsondiffpatch": "^0.4.1",
34 | "knex": "^3.1.0",
35 | "moment": "^2.29.1",
36 | "moment-duration-format": "^2.3.2",
37 | "p-queue": "^6.3.0",
38 | "parse-github-url": "^1.0.2",
39 | "pretty-error": "^4.0.0",
40 | "punycode": "^2.1.1",
41 | "sqlite3": "^5.1.6",
42 | "winston": "^2.4.4",
43 | "zlib-sync": "^0.1.10"
44 | },
45 | "devDependencies": {
46 | "prettier": "^3.5.3"
47 | },
48 | "overrides": {
49 | "swag": {
50 | "handlebars": "^4.7.7"
51 | },
52 | "bookshelf": {
53 | "knex": "$knex"
54 | },
55 | "@YappyBots/addons": {
56 | "got": "$got"
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/views/index.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Introducing Yappy, the GitLab Monitor
11 |
12 |
13 |
14 | It's time to say hello to GitLab repo events right in your Discord server.
15 | Simply set up webhooks to a server, init repo in channel, and you're good-to-go!
16 |
17 |
18 |
19 |
20 |
21 | Guilds
22 | {{counts.guilds}}
23 |
24 |
25 |
26 |
27 | Channels
28 | {{counts.channels}}
29 |
30 |
31 |
32 |
33 | Users
34 | {{counts.users}}
35 |
36 |
37 |
38 |
39 | Repos
40 | {{repos}}
41 |
42 |
43 |
44 |
45 |
46 | Add Yappy GitLab
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/views/layout.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{#if title}}{{title}}{{else}}Yappy, the GitLab Monitor{{/if}}
8 |
9 |
10 |
11 |
36 |
37 |
38 |
39 |
40 |
41 |
59 |
60 |
61 |
62 |
63 | {{{body}}}
64 |
65 |
66 |
67 |
68 |
69 |
70 | - Copyright © 2020 David Sevilla Martin
71 | - Made with Bulma
72 | - Made for Discord
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------