Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] Live poll: Multi-Question and Timed-Polls #13

Merged
merged 19 commits into from
Sep 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 223 additions & 41 deletions PollApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,24 @@ import {
IRead,
} from '@rocket.chat/apps-engine/definition/accessors';
import { App } from '@rocket.chat/apps-engine/definition/App';
import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
import { IAppInfo, RocketChatAssociationModel, RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata';
import { SettingType } from '@rocket.chat/apps-engine/definition/settings';
import {
IUIKitInteractionHandler,
UIKitBlockInteractionContext,
UIKitViewSubmitInteractionContext,
} from '@rocket.chat/apps-engine/definition/uikit';
import { addOptionModal } from './src/lib/addOptionModal';
import { createLivePollMessage } from './src/lib/createLivePollMessage';
import { createLivePollModal } from './src/lib/createLivePollModal';

import timeZones from './src/assets/timezones';
import { pollVisibility } from './src/definition';
import { createMixedVisibilityModal } from './src/lib/createMixedVisibilityModal';
import { createPollMessage } from './src/lib/createPollMessage';
import { createPollModal } from './src/lib/createPollModal';
import { finishPollMessage } from './src/lib/finishPollMessage';
import { nextPollMessage } from './src/lib/nextPollMessage';
import { updatePollMessage } from './src/lib/updatePollMessage';
import { votePoll } from './src/lib/votePoll';
import { PollCommand } from './src/PollCommand';
Expand Down Expand Up @@ -49,47 +53,149 @@ export class PollApp extends App implements IUIKitInteractionHandler {
additionalChoices?: string;
},
},
config?: {
mode?: string,
visibility?: string,
},
} = data.view as any;

if (!state) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: {
question: 'Error creating poll',
},
});
}

if (state.config && state.config.visibility !== pollVisibility.mixed) {
try {
await createPollMessage(data, read, modify, persistence, data.user.id);
} catch (err) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: {
question: 'Error creating poll',
},
errors: err,
});
}
} else {
// Open mixed visibility modal
try {
const modal = await createMixedVisibilityModal({ question: state.poll.question, persistence, modify, data });
await modify.getUiController().openModalView(modal, context.getInteractionData(), data.user);

if (state.config && state.config.visibility !== pollVisibility.mixed) {
try {
await createPollMessage(data, read, modify, persistence, data.user.id);
} catch (err) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: err,
});
}
} else {
// Open mixed visibility modal
try {
const modal = await createMixedVisibilityModal({ question: state.poll.question, persistence, modify, data });
await modify.getUiController().openModalView(modal, context.getInteractionData(), data.user);

return {
success: true,
};

} catch (err) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: err,
});
}
return {
success: true,
};

} catch (err) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: err,
});
}
}

return {
success: true,
};
success: true,
};
} else if (/create-live-poll-modal/.test(id)) {
const { state }: {
state: {
poll: {
question: string,
[option: string]: string,
},
config?: {
mode?: string,
visibility?: string,
},
},
} = data.view as any;
if (!state) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: {
option: 'Error creating poll',
},
});
}
const association = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, data.view.id);
const [readData] = await read.getPersistenceReader().readByAssociation(association) as any;
const polls = readData.polls || [];
const pollIndex = +readData.pollIndex + 1;
const totalPolls = +readData.totalPolls;
// Prompt user to enter values for poll if left blank
try {

if (!state.poll || !state.poll.question || state.poll.question.trim() === '') {
throw { question: 'Please type your question here' };
}
if (!state.poll || !state.poll.ttv || isNaN(+state.poll.ttv)) {
throw { ttv: 'Please enter a valid time for the poll to end' };
}
if (!state.poll['option-0'] || state.poll['option-0'] === '') {
throw {
'option-0': 'Please provide one more option',
};
}
if (!state.poll['option-1'] || state.poll['option-1'] === '') {
throw {
'option-1': 'Please provide one more option',
};
}
} catch (err) {
this.getLogger().log(err);
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: err,
});
}
polls.push(state);
readData.polls = polls;
readData.pollIndex = pollIndex;
readData.user = data.user;
readData.appId = data.appId;
readData.view = data.view;
readData.triggerId = data.triggerId;
await persistence.updateByAssociation(association, readData, true);
if (pollIndex === totalPolls) {
const pollId = `live-${Math.random().toString(36).slice(7)}`;
const livePollAssociation = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, pollId);
await persistence.createWithAssociation(readData, livePollAssociation);
try {
if (readData.save) {
const message = modify
.getCreator()
.startMessage()
.setSender(data.user)
.setText(`Live Poll has been saved with id ${pollId}. Use \`/poll live load ${pollId}\` to start.`)
.setUsernameAlias('Poll');

if (readData.room) {
message.setRoom(readData.room);
}
modify
.getNotifier()
.notifyUser(
data.user,
message.getMessage(),
);

} else {
await createLivePollMessage(data, read, modify, persistence, data.user.id, 0);
}
} catch (err) {
this.getLogger().log(err);
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: err,
});
}
} else {

const modal = await createLivePollModal({id: data.view.id, question: '', persistence, modify, data, pollIndex, totalPolls});
return context.getInteractionResponder().updateModalViewResponse(modal);
}
} else if (/create-mixed-visibility-modal/.test(id)) {

const { state }: {
Expand All @@ -116,11 +222,7 @@ export class PollApp extends App implements IUIKitInteractionHandler {
viewId: data.view.id,
errors: err,
});
}

return {
success: true,
};
}
} else if (/add-option-modal/.test(id)) {
const { state }: {
state: {
Expand Down Expand Up @@ -180,11 +282,47 @@ export class PollApp extends App implements IUIKitInteractionHandler {
}

case 'addChoice': {
const modal = await createPollModal({ id: data.container.id, data, persistence, modify, options: parseInt(String(data.value), 10) });

let modal;
if (data.value && data.value.includes('live-')) {
modal = await createLivePollModal({
id: data.container.id,
data, persistence, modify,
options: parseInt(data.value.split('-')[1], 10),
pollIndex: parseInt(data.value.split('-')[2], 10),
totalPolls: parseInt(data.value.split('-')[3], 10),
});
} else {
modal = await createPollModal({ id: data.container.id, data, persistence, modify, options: parseInt(String(data.value), 10) });
}
return context.getInteractionResponder().updateModalViewResponse(modal);
}

case 'nextPoll': {
try {
const logger = this.getLogger();
await nextPollMessage({ data, read, persistence, modify, logger });
} catch (e) {

const { room } = context.getInteractionData();
const errorMessage = modify
.getCreator()
.startMessage()
.setSender(context.getInteractionData().user)
.setText(e.message)
.setUsernameAlias('Poll');

if (room) {
errorMessage.setRoom(room);
}
modify
.getNotifier()
.notifyUser(
context.getInteractionData().user,
errorMessage.getMessage(),
);
}
break;
}
case 'addUserChoice': {
const modal = await addOptionModal({ id: data.container.id, read, modify });

Expand Down Expand Up @@ -224,15 +362,59 @@ export class PollApp extends App implements IUIKitInteractionHandler {
}

public async initialize(configuration: IConfigurationExtend): Promise<void> {
await configuration.slashCommands.provideSlashCommand(new PollCommand());
configuration.scheduler.registerProcessors([
{
id: 'nextPoll',
processor: async (jobContext, read, modify, http, persis) => {
try {
const logger = this.getLogger();
await nextPollMessage({ data: jobContext, read, persistence: persis, modify, logger });

} catch (e) {
const { room } = jobContext.room;
const errorMessage = modify
.getCreator()
.startMessage()
.setSender(jobContext.user)
.setText(e.message)
.setUsernameAlias('Poll');

if (room) {
errorMessage.setRoom(room);
}
await modify
.getNotifier()
.notifyUser(
jobContext.user,
errorMessage.getMessage(),
);
}
},
},
]);
await configuration.slashCommands.provideSlashCommand(new PollCommand(this));
await configuration.settings.provideSetting({
id : 'use-user-name',
i18nLabel: 'Use name attribute to display voters, instead of username',
i18nDescription: 'When checked, display voters as full user names instead of username',
i18nLabel: 'use_user_name_label',
i18nDescription: 'use_user_name_description',
required: false,
type: SettingType.BOOLEAN,
public: true,
packageValue: false,
});
await configuration.settings.provideSetting({
id : 'timezone',
i18nLabel: 'timezone_label',
i18nDescription: 'timezone_description',
required: true,
type: SettingType.SELECT,
public: true,
packageValue: 'America/Danmarkshavn',
value: 'America/Danmarkshavn',
values: timeZones.timeZones.map((tz) => ({
i18nLabel: `${tz.value} (UTC ${tz.offset >= 0 ? '+ ' + tz.offset : '- ' + Math.abs(tz.offset)} )`,
key: tz.utc[0],
})),
});
}
}
33 changes: 31 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "c33fa1a6-68a7-491e-bf49-9d7b99671c48",
"version": "2.0.0",
"requiredApiVersion": "^1.12.0",
"requiredApiVersion": "^1.23.0",
"iconFile": "icon.png",
"author": {
"name": "Diego Sampaio",
Expand All @@ -14,5 +14,34 @@
"description": "A simple app to create polls on Rocket.Chat. Use the slash command: /poll [Question?]",
"implements": [
"IUIKitInteractionHandler"
],
"permissions": [
Copy link

@murtaza98 murtaza98 Aug 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This app uses a lot of other permissions like slashcommand, message.read etc which we need to also declare over here. For a list of all permissions, please refer here and declare ONLY the permissions being used by this app.

PS: Earlier since this app was using apps-engine version < 1.20.0 which is when the permission system was introduced, we didn't have to explicitly declare any such permissions as the apps-engine assigns such apps default permission. However now, since our app is going to be using schedular apis which is on 1.23.0 version of apps-engine - we now need to declare all these permissions explicitly and if this isn't done, the app will start throwing permission errors

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the permissions. Please let me know if I missed any. Thanks.

{
"name": "scheduler"
},
{
"name": "user.read"
},
{
"name": "server-setting.read"
},
{
"name": "room.read"
},
{
"name": "message.read"
},
{
"name": "message.write"
},
{
"name": "slashcommand"
},
RonLek marked this conversation as resolved.
Show resolved Hide resolved
{
"name": "persistence"
},
{
"name": "ui.interact"
}
]
}
}
8 changes: 6 additions & 2 deletions i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"cmd_description": "Create a simple poll",
"params_example": "Type your question or fill it up on the form."
"cmd_description": "Create a simple poll or a Live Poll",
"params_example": "Type your question or fill it up on the form. Optionally follow with live <number> or live save <number> or live load <id>.",
"use_user_name_label": "Use name attribute to display voters, instead of username",
"use_user_name_description": "When checked, display voters as full user names instead of username",
"timezone_label": "TimeZone",
"timezone_description": "Timezone to display Poll finish time in"
}
Loading