Bridge polls from Matrix as pseudo-polls on Discord (with an embed). Not 100% working.
Co-authored-by: Cadence Ember <cloudrac3r@vivaldi.net>
This commit is contained in:
committed by
Cadence Ember
parent
e565342ac8
commit
afca4de6b6
@@ -22,8 +22,8 @@ const editMessage = sync.require("../../d2m/actions/edit-message")
|
||||
const emojiSheet = sync.require("../actions/emoji-sheet")
|
||||
|
||||
/**
|
||||
* @param {{poll?: Ty.SendingPoll} & DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[], pendingFiles?: ({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer | stream.Readable})[]}} message
|
||||
* @returns {Promise<{poll?: Ty.SendingPoll} & DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]}>}
|
||||
* @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[], pendingFiles?: ({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer | stream.Readable})[]}} message
|
||||
* @returns {Promise<DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]}>}
|
||||
*/
|
||||
async function resolvePendingFiles(message) {
|
||||
if (!message.pendingFiles) return message
|
||||
@@ -71,6 +71,7 @@ async function sendEvent(event) {
|
||||
}
|
||||
/** @type {DiscordTypes.APIGuildTextChannel} */ // @ts-ignore
|
||||
const channel = discord.channels.get(channelID)
|
||||
// @ts-ignore
|
||||
const guild = discord.guilds.get(channel.guild_id)
|
||||
assert(guild)
|
||||
const historicalRoomIndex = select("historical_channel_room", "historical_room_index", {room_id: event.room_id}).pluck().get()
|
||||
@@ -133,12 +134,25 @@ async function sendEvent(event) {
|
||||
}, guild, null)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (message.poll){ // Need to store answer mapping in the database.
|
||||
for (let i=0; i<message.poll.answers.length; i++){
|
||||
db.prepare("INSERT INTO poll_option (message_id, matrix_option, discord_option) VALUES (?, ?, ?)").run(messageResponse.id, message.poll.answers[i].matrix_option, messageResponse.poll.answers[i].answer_id.toString())
|
||||
if (event.type === "org.matrix.msc3381.poll.start") { // Need to store answer mapping in the database.
|
||||
db.transaction(() => {
|
||||
const messageID = messageResponses[0].id
|
||||
db.prepare("INSERT INTO poll (message_id, max_selections, question_text, is_closed) VALUES (?, ?, ?, 0)").run(
|
||||
messageID,
|
||||
event.content["org.matrix.msc3381.poll.start"].max_selections,
|
||||
event.content["org.matrix.msc3381.poll.start"].question["org.matrix.msc1767.text"]
|
||||
)
|
||||
for (const [i, option] of Object.entries(event.content["org.matrix.msc3381.poll.start"].answers)) {
|
||||
db.prepare("INSERT INTO poll_option (message_id, matrix_option, option_text, seq) VALUES (?, ?, ?, ?)").run(
|
||||
messageID,
|
||||
option.id,
|
||||
option["org.matrix.msc1767.text"],
|
||||
i
|
||||
)
|
||||
}
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
for (const user of ensureJoined) {
|
||||
|
||||
@@ -17,7 +17,7 @@ async function updateVote(event) {
|
||||
db.prepare("DELETE FROM poll_vote WHERE discord_or_matrix_user_id = ? AND message_id = ?").run(event.sender, messageID) // Clear all the existing votes, since this overwrites. Technically we could check and only overwrite the changes, but the complexity isn't worth it.
|
||||
|
||||
event.content["org.matrix.msc3381.poll.response"].answers.map(answer=>{
|
||||
db.prepare("INSERT OR IGNORE INTO poll_vote (discord_or_matrix_user_id, message_id, vote) VALUES (?, ?, ?)").run(event.sender, messageID, answer)
|
||||
db.prepare("INSERT OR IGNORE INTO poll_vote (discord_or_matrix_user_id, message_id, matrix_option) VALUES (?, ?, ?)").run(event.sender, messageID, answer)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ const dUtils = sync.require("../../discord/utils")
|
||||
const file = sync.require("../../matrix/file")
|
||||
/** @type {import("./emoji-sheet")} */
|
||||
const emojiSheet = sync.require("./emoji-sheet")
|
||||
/** @type {import("./poll-components")} */
|
||||
const pollComponents = sync.require("./poll-components")
|
||||
/** @type {import("../actions/setup-emojis")} */
|
||||
const setupEmojis = sync.require("../actions/setup-emojis")
|
||||
/** @type {import("../../d2m/converters/user-to-mxid")} */
|
||||
@@ -551,8 +553,8 @@ async function eventToMessage(event, guild, channel, di) {
|
||||
const pendingFiles = []
|
||||
/** @type {DiscordTypes.APIUser[]} */
|
||||
const ensureJoined = []
|
||||
/** @type {Ty.SendingPoll} */
|
||||
let poll = null
|
||||
/** @type {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody?} */
|
||||
let pollMessage = null
|
||||
|
||||
// Convert content depending on what the message is
|
||||
// Handle images first - might need to handle their `body`/`formatted_body` as well, which will fall through to the text processor
|
||||
@@ -632,21 +634,17 @@ async function eventToMessage(event, guild, channel, di) {
|
||||
pendingFiles.push({name: filename, mxc: event.content.url})
|
||||
|
||||
} else if (event.type === "org.matrix.msc3381.poll.start") {
|
||||
content = ""
|
||||
const pollContent = event.content["org.matrix.msc3381.poll.start"] // just for convenience
|
||||
let allowMultiselect = (pollContent.max_selections != 1)
|
||||
let answers = pollContent.answers.map(answer=>{
|
||||
return {poll_media: {text: answer["org.matrix.msc1767.text"]}, matrix_option: answer["id"]}
|
||||
})
|
||||
poll = {
|
||||
question: {
|
||||
text: event.content["org.matrix.msc3381.poll.start"].question["org.matrix.msc1767.text"]
|
||||
},
|
||||
answers: answers,
|
||||
duration: 768, // Maximum duration (32 days). Matrix doesn't allow automatically-expiring polls, so this is the only thing that makes sense to send.
|
||||
allow_multiselect: allowMultiselect,
|
||||
layout_type: 1
|
||||
}
|
||||
const isClosed = false;
|
||||
const maxSelections = pollContent.max_selections || 1
|
||||
const questionText = pollContent.question["org.matrix.msc1767.text"]
|
||||
const pollOptions = pollContent.answers.map(answer => ({
|
||||
matrix_option: answer.id,
|
||||
option_text: answer["org.matrix.msc1767.text"],
|
||||
count: 0 // no votes initially
|
||||
}))
|
||||
content = ""
|
||||
pollMessage = pollComponents.getPollComponents(isClosed, maxSelections, questionText, pollOptions)
|
||||
|
||||
} else {
|
||||
// Handling edits. If the edit was an edit of a reply, edits do not include the reply reference, so we need to fetch up to 2 more events.
|
||||
@@ -980,7 +978,7 @@ async function eventToMessage(event, guild, channel, di) {
|
||||
|
||||
// Split into 2000 character chunks
|
||||
const chunks = chunk(content, 2000)
|
||||
/** @type {({poll?: Ty.SendingPoll} & DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]})[]} */
|
||||
/** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]})[]} */
|
||||
const messages = chunks.map(content => ({
|
||||
content,
|
||||
allowed_mentions: {
|
||||
@@ -1003,13 +1001,12 @@ async function eventToMessage(event, guild, channel, di) {
|
||||
messages[0].pendingFiles = pendingFiles
|
||||
}
|
||||
|
||||
if (poll) {
|
||||
if (!messages.length) messages.push({
|
||||
content: " ", // stopgap, remove when library updates
|
||||
if (pollMessage) {
|
||||
messages.push({
|
||||
...pollMessage,
|
||||
username: displayNameShortened,
|
||||
avatar_url: avatarURL
|
||||
})
|
||||
messages[0].poll = poll
|
||||
}
|
||||
|
||||
const messagesToEdit = []
|
||||
|
||||
103
src/m2d/converters/poll-components.js
Normal file
103
src/m2d/converters/poll-components.js
Normal file
@@ -0,0 +1,103 @@
|
||||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
|
||||
/**
|
||||
* @param {boolean} isClosed
|
||||
* @param {{matrix_option: string, option_text: string, count: number}[]} pollOptions already sorted correctly
|
||||
* @returns {DiscordTypes.APIMessageTopLevelComponent[]}
|
||||
*/
|
||||
function optionsToComponents(isClosed, pollOptions) {
|
||||
const topAnswers = pollOptions.toSorted((a, b) => b.count - a.count)
|
||||
/** @type {DiscordTypes.APIMessageTopLevelComponent[]} */
|
||||
return pollOptions.map(option => {
|
||||
const winningOrTied = option.count && topAnswers[0].count === option.count
|
||||
return {
|
||||
type: DiscordTypes.ComponentType.Container,
|
||||
components: [{
|
||||
type: DiscordTypes.ComponentType.Section,
|
||||
components: [{
|
||||
type: DiscordTypes.ComponentType.TextDisplay,
|
||||
content: option.option_text
|
||||
}],
|
||||
accessory: {
|
||||
type: DiscordTypes.ComponentType.Button,
|
||||
style: winningOrTied ? DiscordTypes.ButtonStyle.Success : DiscordTypes.ButtonStyle.Secondary,
|
||||
label: option.count.toString(),
|
||||
custom_id: option.matrix_option,
|
||||
disabled: isClosed
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} isClosed
|
||||
* @param {number} maxSelections
|
||||
* @param {string} questionText
|
||||
* @param {{matrix_option: string, option_text: string, count: number}[]} pollOptions already sorted correctly
|
||||
* @returns {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody}
|
||||
*/
|
||||
function getPollComponents(isClosed, maxSelections, questionText, pollOptions) {
|
||||
/** @type {DiscordTypes.APIMessageTopLevelComponent} */
|
||||
let headingComponent
|
||||
if (isClosed) {
|
||||
const multiSelectString =
|
||||
( maxSelections === 1 ? "-# ~~Select one answer~~"
|
||||
: maxSelections >= pollOptions.length ? "-# ~~Select one or more answers~~"
|
||||
: `-# ~~Select up to ${maxSelections} answers~~`)
|
||||
headingComponent = { // This one is for the poll heading.
|
||||
type: DiscordTypes.ComponentType.Section,
|
||||
components: [
|
||||
{
|
||||
type: DiscordTypes.ComponentType.TextDisplay,
|
||||
content: `## ${questionText}`
|
||||
},
|
||||
{
|
||||
type: DiscordTypes.ComponentType.TextDisplay,
|
||||
content: multiSelectString
|
||||
}
|
||||
],
|
||||
accessory: {
|
||||
type: DiscordTypes.ComponentType.Button,
|
||||
style: DiscordTypes.ButtonStyle.Secondary,
|
||||
custom_id: "vote",
|
||||
label: "Voting closed!",
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const multiSelectString =
|
||||
( maxSelections === 1 ? "-# Select one answer"
|
||||
: maxSelections >= pollOptions.length ? "-# Select one or more answers"
|
||||
: `-# Select up to ${maxSelections} answers`)
|
||||
headingComponent = { // This one is for the poll heading.
|
||||
type: DiscordTypes.ComponentType.Section,
|
||||
components: [
|
||||
{
|
||||
type: DiscordTypes.ComponentType.TextDisplay,
|
||||
content: `## ${questionText}`
|
||||
},
|
||||
{
|
||||
type: DiscordTypes.ComponentType.TextDisplay,
|
||||
content: multiSelectString
|
||||
}
|
||||
],
|
||||
accessory: {
|
||||
type: DiscordTypes.ComponentType.Button,
|
||||
style: DiscordTypes.ButtonStyle.Primary,
|
||||
custom_id: "vote",
|
||||
label: "Vote!"
|
||||
}
|
||||
}
|
||||
}
|
||||
const optionComponents = optionsToComponents(isClosed, pollOptions)
|
||||
return {
|
||||
flags: DiscordTypes.MessageFlags.IsComponentsV2,
|
||||
components: [headingComponent, ...optionComponents]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.getPollComponents = getPollComponents
|
||||
Reference in New Issue
Block a user