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
co-authored by
Cadence Ember
parent
e565342ac8
commit
afca4de6b6
@@ -1,6 +1,9 @@
|
||||
// @ts-check
|
||||
|
||||
const assert = require("assert").strict
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {Semaphore} = require("@chriscdn/promise-semaphore")
|
||||
const {scheduler} = require("timers/promises")
|
||||
|
||||
const passthrough = require("../../passthrough")
|
||||
const {discord, sync, db, select, from} = passthrough
|
||||
@@ -11,71 +14,81 @@ const registerUser = sync.require("./register-user")
|
||||
/** @type {import("./create-room")} */
|
||||
const createRoom = sync.require("../actions/create-room")
|
||||
|
||||
const inFlightPollVotes = new Set()
|
||||
const inFlightPollSema = new Semaphore()
|
||||
|
||||
/**
|
||||
* @param {import("discord-api-types/v10").GatewayMessagePollVoteAddDispatch["d"]} data
|
||||
*/
|
||||
async function addVote(data){
|
||||
const parentID = from("event_message").join("poll_option", "message_id").pluck("event_id").where({message_id: data.message_id, event_type: "org.matrix.msc3381.poll.start"}).get() // Currently Discord doesn't allow sending a poll with anything else, but we bridge it after all other content so reaction_part: 0 is the part that will have the poll.
|
||||
if (!parentID) return // Nothing can be done if the parent message was never bridged.
|
||||
const pollEventID = from("event_message").join("poll_option", "message_id").pluck("event_id").where({message_id: data.message_id, event_type: "org.matrix.msc3381.poll.start"}).get() // Currently Discord doesn't allow sending a poll with anything else, but we bridge it after all other content so reaction_part: 0 is the part that will have the poll.
|
||||
if (!pollEventID) return // Nothing can be done if the parent message was never bridged.
|
||||
|
||||
let realAnswer = select("poll_option", "matrix_option", {message_id: data.message_id, discord_option: data.answer_id.toString()}).pluck().get() // Discord answer IDs don't match those on Matrix-created polls.
|
||||
assert(realAnswer)
|
||||
db.prepare("INSERT OR IGNORE INTO poll_vote (discord_or_matrix_user_id, message_id, vote) VALUES (?, ?, ?)").run(data.user_id, data.message_id, realAnswer)
|
||||
return modifyVote(data, parentID)
|
||||
db.prepare("INSERT OR IGNORE INTO poll_vote (discord_or_matrix_user_id, message_id, matrix_option) VALUES (?, ?, ?)").run(data.user_id, data.message_id, realAnswer)
|
||||
return debounceSendVotes(data, pollEventID)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord-api-types/v10").GatewayMessagePollVoteRemoveDispatch["d"]} data
|
||||
*/
|
||||
async function removeVote(data){
|
||||
const parentID = from("event_message").join("poll_option", "message_id").pluck("event_id").where({message_id: data.message_id, event_type: "org.matrix.msc3381.poll.start"}).get()
|
||||
if (!parentID) return
|
||||
const pollEventID = from("event_message").join("poll_option", "message_id").pluck("event_id").where({message_id: data.message_id, event_type: "org.matrix.msc3381.poll.start"}).get()
|
||||
if (!pollEventID) return
|
||||
|
||||
let realAnswer = select("poll_option", "matrix_option", {message_id: data.message_id, discord_option: data.answer_id.toString()}).pluck().get() // Discord answer IDs don't match those on Matrix-created polls.
|
||||
assert(realAnswer)
|
||||
db.prepare("DELETE FROM poll_vote WHERE discord_or_matrix_user_id = ? AND message_id = ? AND vote = ?").run(data.user_id, data.message_id, realAnswer)
|
||||
return modifyVote(data, parentID)
|
||||
db.prepare("DELETE FROM poll_vote WHERE discord_or_matrix_user_id = ? AND message_id = ? AND matrix_option = ?").run(data.user_id, data.message_id, realAnswer)
|
||||
return debounceSendVotes(data, pollEventID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiple-choice polls send all the votes at the same time. This debounces and sends the combined votes.
|
||||
* In the meantime, the combined votes are assembled in the `poll_vote` database table by the above functions.
|
||||
* @param {import("discord-api-types/v10").GatewayMessagePollVoteAddDispatch["d"]} data
|
||||
* @param {string} parentID
|
||||
* @param {string} pollEventID
|
||||
* @return {Promise<string>} event ID of Matrix vote
|
||||
*/
|
||||
async function modifyVote(data, parentID) {
|
||||
async function debounceSendVotes(data, pollEventID) {
|
||||
return await inFlightPollSema.request(async () => {
|
||||
await scheduler.wait(1000) // Wait for votes to be collected
|
||||
|
||||
if (inFlightPollVotes.has(data.user_id+data.message_id)) { // Multiple votes on a poll, and this function has already been called on at least one of them. Need to add these together so we don't ignore votes if someone is voting rapid-fire on a bunch of different polls.
|
||||
return;
|
||||
const user = await discord.snow.user.getUser(data.user_id) // Gateway event doesn't give us the object, only the ID.
|
||||
return await sendVotes(user, data.channel_id, data.message_id, pollEventID)
|
||||
}, `${data.user_id}/${data.message_id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.APIUser} user
|
||||
* @param {string} channelID
|
||||
* @param {string} pollMessageID
|
||||
* @param {string} pollEventID
|
||||
*/
|
||||
async function sendVotes(user, channelID, pollMessageID, pollEventID) {
|
||||
const latestRoomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
|
||||
const matchingRoomID = from("message_room").join("historical_channel_room", "historical_room_index").where({message_id: pollMessageID}).pluck("room_id").get()
|
||||
if (!latestRoomID || latestRoomID !== matchingRoomID) { // room upgrade mid-poll??
|
||||
db.prepare("UPDATE poll SET is_closed = 1 WHERE message_id = ?").run(pollMessageID)
|
||||
return
|
||||
}
|
||||
|
||||
inFlightPollVotes.add(data.user_id+data.message_id)
|
||||
const senderMxid = await registerUser.ensureSimJoined(user, matchingRoomID)
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)) // Wait a second.
|
||||
|
||||
const user = await discord.snow.user.getUser(data.user_id) // Gateway event doesn't give us the object, only the ID.
|
||||
|
||||
const roomID = await createRoom.ensureRoom(data.channel_id)
|
||||
const senderMxid = await registerUser.ensureSimJoined(user, roomID)
|
||||
|
||||
let answersArray = select("poll_vote", "vote", {discord_or_matrix_user_id: data.user_id, message_id: data.message_id}).pluck().all()
|
||||
|
||||
const eventID = await api.sendEvent(roomID, "org.matrix.msc3381.poll.response", {
|
||||
const answersArray = select("poll_vote", "matrix_option", {discord_or_matrix_user_id: user.id, message_id: pollMessageID}).pluck().all()
|
||||
const eventID = await api.sendEvent(matchingRoomID, "org.matrix.msc3381.poll.response", {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: parentID,
|
||||
event_id: pollEventID,
|
||||
},
|
||||
"org.matrix.msc3381.poll.response": {
|
||||
answers: answersArray
|
||||
}
|
||||
}, senderMxid)
|
||||
|
||||
inFlightPollVotes.delete(data.user_id+data.message_id)
|
||||
|
||||
return eventID
|
||||
|
||||
}
|
||||
|
||||
module.exports.addVote = addVote
|
||||
module.exports.removeVote = removeVote
|
||||
module.exports.modifyVote = modifyVote
|
||||
module.exports.debounceSendVotes = debounceSendVotes
|
||||
module.exports.sendVotes = sendVotes
|
||||
Reference in New Issue
Block a user