1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
|
// @ts-check
const DiscordTypes = require("discord-api-types/v10")
const {discord, sync, select, from, db} = require("../../passthrough")
const assert = require("assert/strict")
const {id: botID} = require("../../../addbot")
const {InteractionMethods} = require("snowtransfer")
/** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api")
/** @type {import("../../matrix/utils")} */
const utils = sync.require("../../matrix/utils")
/** @type {import("../../m2d/converters/poll-components")} */
const pollComponents = sync.require("../../m2d/converters/poll-components")
/** @type {import("../../d2m/actions/poll-vote")} */
const vote = sync.require("../../d2m/actions/poll-vote")
/**
* @param {DiscordTypes.APIMessageComponentButtonInteraction} interaction
* @param {{api: typeof api}} di
* @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters<InteractionMethods[k]>[2]}>}
*/
async function* _interact({data, message, member, user}, {api}) {
if (!member?.user) return
const userID = member.user.id
const pollRow = select("poll", ["question_text", "max_selections"], {message_id: message.id}).get()
if (!pollRow) return
// Definitely supposed to be a poll button click. We can use assertions now.
const matrixPollEvent = select("event_message", "event_id", {message_id: message.id}).pluck().get()
assert(matrixPollEvent)
const maxSelections = pollRow.max_selections
const alreadySelected = select("poll_vote", "matrix_option", {discord_or_matrix_user_id: userID, message_id: message.id}).pluck().all()
// Show modal (if no capacity or if requested)
if (data.custom_id === "POLL_VOTE" || (maxSelections > 1 && alreadySelected.length === maxSelections)) {
const options = select("poll_option", ["matrix_option", "option_text", "seq"], {message_id: message.id}, "ORDER BY seq").all().map(option => ({
value: option.matrix_option,
label: option.option_text,
default: alreadySelected.includes(option.matrix_option)
}))
const checkboxGroupExtras = maxSelections === 1 && options.length > 1 ? {} : {
/** @type {DiscordTypes.ComponentType.CheckboxGroup} */
type: DiscordTypes.ComponentType.CheckboxGroup,
min_values: 0,
max_values: maxSelections
}
return yield {createInteractionResponse: {
type: DiscordTypes.InteractionResponseType.Modal,
data: {
custom_id: "POLL_MODAL",
title: "Poll",
components: [{
type: DiscordTypes.ComponentType.TextDisplay,
content: `-# ${pollComponents.getMultiSelectString(pollRow.max_selections, options.length)}`
}, {
type: DiscordTypes.ComponentType.Label,
label: pollRow.question_text,
component: {
type: DiscordTypes.ComponentType.RadioGroup,
custom_id: "POLL_MODAL_SELECTION",
options,
required: false,
...checkboxGroupExtras
}
}]
}
}}
}
if (data.custom_id === "POLL_MODAL") {
// Clicked options via modal
/** @type {DiscordTypes.APIModalSubmitRadioGroupComponent | DiscordTypes.APIModalSubmitCheckboxGroupComponent} */ // @ts-ignore - close enough to the real thing
const component = data.components[1].component
assert.equal(component.custom_id, "POLL_MODAL_SELECTION")
const values = "values" in component ? component.values : [component.value]
// Replace votes with selection
db.transaction(() => {
db.prepare("DELETE FROM poll_vote WHERE message_id = ? AND discord_or_matrix_user_id = ?").run(message.id, userID)
for (const option of values) {
db.prepare("INSERT OR IGNORE INTO poll_vote (discord_or_matrix_user_id, message_id, matrix_option) VALUES (?, ?, ?)").run(userID, message.id, option)
}
})()
// Update counts on message
yield {createInteractionResponse: {
type: DiscordTypes.InteractionResponseType.UpdateMessage,
data: pollComponents.getPollComponentsFromDatabase(message.id)
}}
// Sync changes to Matrix
await vote.sendVotes(member.user, message.channel_id, message.id, matrixPollEvent)
} else {
// Clicked buttons on message
const optionPrefix = "POLL_OPTION#" // we use a prefix to prevent someone from sending a Matrix poll that intentionally collides with other elements of the embed
const matrixOption = select("poll_option", "matrix_option", {matrix_option: data.custom_id.substring(optionPrefix.length), message_id: message.id}).pluck().get()
assert(matrixOption)
// Remove a vote
if (alreadySelected.includes(matrixOption)) {
db.prepare("DELETE FROM poll_vote WHERE discord_or_matrix_user_id = ? AND message_id = ? AND matrix_option = ?").run(userID, message.id, matrixOption)
}
// Replace votes (if only one selection is allowed)
else if (maxSelections === 1 && alreadySelected.length === 1) {
db.transaction(() => {
db.prepare("DELETE FROM poll_vote WHERE message_id = ? AND discord_or_matrix_user_id = ?").run(message.id, userID)
db.prepare("INSERT OR IGNORE INTO poll_vote (discord_or_matrix_user_id, message_id, matrix_option) VALUES (?, ?, ?)").run(userID, message.id, matrixOption)
})()
}
// Add a vote (if capacity)
else if (alreadySelected.length < maxSelections) {
db.prepare("INSERT OR IGNORE INTO poll_vote (discord_or_matrix_user_id, message_id, matrix_option) VALUES (?, ?, ?)").run(userID, message.id, matrixOption)
}
// Update counts on message
yield {createInteractionResponse: {
type: DiscordTypes.InteractionResponseType.UpdateMessage,
data: pollComponents.getPollComponentsFromDatabase(message.id)
}}
// Sync changes to Matrix
await vote.sendVotes(member.user, message.channel_id, message.id, matrixPollEvent)
}
}
/* c8 ignore start */
/** @param {DiscordTypes.APIMessageComponentButtonInteraction} interaction */
async function interact(interaction) {
for await (const response of _interact(interaction, {api})) {
if (response.createInteractionResponse) {
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, response.createInteractionResponse)
} else if (response.editOriginalInteractionResponse) {
await discord.snow.interaction.editOriginalInteractionResponse(botID, interaction.token, response.editOriginalInteractionResponse)
}
}
}
module.exports.interact = interact
module.exports._interact = _interact
|