Skip to content

Commit eec8147

Browse files
authored
Merge pull request #37 from acmucsd/aaron/fields
added challenge fields (formerly attachments)
2 parents 305281d + 8e6ff54 commit eec8147

File tree

8 files changed

+156
-3
lines changed

8 files changed

+156
-3
lines changed

src/database/models/Challenge.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { Category } from './Category';
1212
import { Flag, initFlag } from './Flag';
1313
import { ChallengeChannel, initChallengeChannel } from './ChallengeChannel';
14+
import { ChallengeField, initChallengeField } from './ChallengeField';
1415

1516
export interface ChallengeAttributes {
1617
id: number;
@@ -65,6 +66,18 @@ export class Challenge extends Model<ChallengeAttributes, ChallengeCreationAttri
6566
// declare removeFlags: HasManyRemoveAssociationsMixin<Flag, number>;
6667
declare createFlag: HasManyCreateAssociationMixin<Flag>;
6768
declare readonly Flags?: Flag[];
69+
70+
declare getChallengeFields: HasManyGetAssociationsMixin<ChallengeField>;
71+
// declare countChallengeFields: HasManyCountAssociationsMixin;
72+
// declare hasChallengeField: HasManyHasAssociationMixin<ChallengeField, number>;
73+
// declare hasChallengeFields: HasManyHasAssociationsMixin<ChallengeField, number>;
74+
// declare setChallengeFields: HasManySetAssociationsMixin<ChallengeField, number>;
75+
// declare addChallengeField: HasManyAddAssociationMixin<ChallengeField, number>;
76+
// declare addChallengeFields: HasManyAddAssociationsMixin<ChallengeField, number>;
77+
// declare removeChallengeField: HasManyRemoveAssociationMixin<ChallengeField, number>;
78+
// declare removeChallengeFields: HasManyRemoveAssociationsMixin<ChallengeField, number>;
79+
declare createChallengeField: HasManyCreateAssociationMixin<ChallengeField>;
80+
declare readonly ChallengeFields?: ChallengeField[];
6881
}
6982

7083
export function initChallenge(sequelize: Sequelize) {
@@ -113,4 +126,7 @@ export function initChallenge(sequelize: Sequelize) {
113126

114127
initFlag(sequelize);
115128
Challenge.hasMany(Flag);
129+
130+
initChallengeField(sequelize);
131+
Challenge.hasMany(ChallengeField);
116132
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { BelongsToGetAssociationMixin, DataTypes, Model, Optional, Sequelize } from 'sequelize';
2+
import { Challenge } from './Challenge';
3+
4+
interface ChallengeFieldAttributes {
5+
id: number;
6+
title: string;
7+
content: string;
8+
}
9+
10+
type ChallengeFieldCreationAttributes = Optional<ChallengeFieldAttributes, 'id'>;
11+
12+
export class ChallengeField
13+
extends Model<ChallengeFieldAttributes, ChallengeFieldCreationAttributes>
14+
implements ChallengeFieldAttributes
15+
{
16+
declare id: number;
17+
declare title: string;
18+
declare content: string;
19+
20+
// declare readonly createdAt: Date;
21+
// declare readonly updatedAt: Date;
22+
23+
declare getChallenge: BelongsToGetAssociationMixin<Challenge>;
24+
// declare setChallenge: BelongsToSetAssociationMixin<Challenge, number>;
25+
// declare createChallenge: BelongsToCreateAssociationMixin<Challenge>;
26+
// declare readonly challenge?: Challenge;
27+
}
28+
29+
export function initChallengeField(sequelize: Sequelize) {
30+
ChallengeField.init(
31+
{
32+
id: {
33+
type: DataTypes.INTEGER,
34+
autoIncrement: true,
35+
primaryKey: true,
36+
},
37+
title: {
38+
type: DataTypes.STRING,
39+
allowNull: false,
40+
},
41+
content: {
42+
type: DataTypes.STRING,
43+
allowNull: false,
44+
},
45+
},
46+
{
47+
sequelize,
48+
},
49+
);
50+
51+
ChallengeField.belongsTo(Challenge, {
52+
onDelete: 'CASCADE',
53+
foreignKey: {
54+
allowNull: false,
55+
},
56+
});
57+
}

src/discord/events/interaction/commands/challenge/challenge.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import set from './set';
44
import { ChatInputCommandDefinition } from '../../interaction';
55
import flag from './flag';
66
import fromcsv from './fromcsv';
7+
import field from './field';
78

89
export default {
910
name: 'challenge',
1011
description: 'Challenge management and submission',
11-
options: [add, del, set, flag, fromcsv],
12+
options: [add, del, set, flag, fromcsv, field],
1213
default_permission: false,
1314
} as ChatInputCommandDefinition;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ApplicationCommandOptionTypes } from 'discord.js/typings/enums';
2+
import { ExecutableSubCommandData, PopulatedCommandInteraction } from '../../../interaction';
3+
import { getChallengeByChannelContext } from '../../../../../util/ResourceManager';
4+
5+
export default {
6+
name: 'add',
7+
description: 'Add a new field to this challenge.',
8+
type: ApplicationCommandOptionTypes.SUB_COMMAND,
9+
options: [
10+
{
11+
name: 'title',
12+
description: "The field's title",
13+
type: ApplicationCommandOptionTypes.STRING,
14+
required: true,
15+
},
16+
{
17+
name: 'content',
18+
description: "The field's content",
19+
type: ApplicationCommandOptionTypes.STRING,
20+
required: true,
21+
},
22+
],
23+
async execute(interaction: PopulatedCommandInteraction) {
24+
const challenge = await getChallengeByChannelContext(interaction.channel);
25+
26+
await challenge.createChallengeField({
27+
title: interaction.options.getString('title', true),
28+
content: interaction.options.getString('content', true),
29+
});
30+
31+
return `Field has been added to challenge **${challenge.name}**.`;
32+
},
33+
} as ExecutableSubCommandData;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { ApplicationCommandOptionTypes } from 'discord.js/typings/enums';
2+
import { ExecutableSubCommandData, PopulatedCommandInteraction } from '../../../interaction';
3+
import { getChallengeByChannelContext } from '../../../../../util/ResourceManager';
4+
5+
export default {
6+
name: 'del',
7+
description: 'Deletes a field attached to this challenge.',
8+
type: ApplicationCommandOptionTypes.SUB_COMMAND,
9+
options: [
10+
{
11+
name: 'index',
12+
description: 'Indicate which field you want to delete (counting starts at 1)',
13+
type: ApplicationCommandOptionTypes.NUMBER,
14+
required: true,
15+
},
16+
],
17+
async execute(interaction: PopulatedCommandInteraction) {
18+
const challenge = await getChallengeByChannelContext(interaction.channel);
19+
20+
const fields = await challenge.getChallengeFields({ order: [['createdAt', 'ASC']] });
21+
const index = interaction.options.getNumber('index', true) - 1;
22+
23+
if (!fields || !fields[index]) throw new Error('no field with that index');
24+
await fields[index].destroy();
25+
26+
return `Field #${index + 1} has been removed from challenge **${challenge.name}**.`;
27+
},
28+
} as ExecutableSubCommandData;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { ApplicationCommandOptionTypes } from 'discord.js/typings/enums';
2+
import { ExecutableSubGroupData } from '../../../interaction';
3+
import add from './add';
4+
import del from './del';
5+
6+
export default {
7+
name: 'field',
8+
description: 'Challenge field management',
9+
type: ApplicationCommandOptionTypes.SUB_COMMAND_GROUP,
10+
options: [add, del],
11+
} as ExecutableSubGroupData;

src/discord/hooks/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { destroyChallengeChannel, refreshChallengeChannel } from './ChallengeCha
1414
import { Flag } from '../../database/models/Flag';
1515
import { destroyTeam, refreshTeam } from './TeamHooks';
1616
import { Team } from '../../database/models/Team';
17+
import { ChallengeField } from '../../database/models/ChallengeField';
1718

1819
export async function initHooks(client: Client<true>) {
1920
Ctf.beforeCreate((ctf) =>
@@ -65,6 +66,10 @@ export async function initHooks(client: Client<true>) {
6566
Flag.afterCreate((flag) => flag.getChallenge().then((chal) => refreshChallenge(chal, client)));
6667
Flag.afterUpdate((flag) => flag.getChallenge().then((chal) => refreshChallenge(chal, client)));
6768
Flag.afterDestroy((flag) => flag.getChallenge().then((chal) => refreshChallenge(chal, client)));
69+
// same for challenge fields
70+
ChallengeField.afterCreate((field) => field.getChallenge().then((chal) => refreshChallenge(chal, client)));
71+
ChallengeField.afterUpdate((field) => field.getChallenge().then((chal) => refreshChallenge(chal, client)));
72+
ChallengeField.afterDestroy((field) => field.getChallenge().then((chal) => refreshChallenge(chal, client)));
6873

6974
// TODO: after a flag gets captured, we queue up a periodic refresh
7075

src/discord/messages/ChallengeMessage.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ export async function setChallengeMessage(client: Client<true>, channel: TextCha
2727
challengeMessage.setFooter({ text: `By ${challenge.author}` });
2828
challengeMessage.setColor('#50c0bf');
2929

30-
// const attachments = await this.getAllAttachments();
31-
// attachments.forEach((attachment) => challengeMessage.addField(attachment.row.name, attachment.row.url));
30+
// add challenge fields
31+
const fields = await challenge.getChallengeFields();
32+
fields.forEach((field) => challengeMessage.addField(field.title, field.content));
33+
3234
const guild = await client.guilds.fetch(category.Ctf.guildSnowflake);
3335

3436
// complicated nested query to fetch the associated first blood user and team, if defined

0 commit comments

Comments
 (0)