extend poll-checker to link polls to legislation, send vote comparison notifications
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { and, eq, inArray } from "drizzle-orm"
|
||||
import { and, eq, inArray, isNull } from "drizzle-orm"
|
||||
import { db } from "../db/client"
|
||||
import { legislationTexts, userVotes } from "../db/schema/legislation"
|
||||
import {
|
||||
deviceFollows,
|
||||
politicianMandates,
|
||||
@@ -21,6 +22,12 @@ const VOTE_LABELS: Record<string, string> = {
|
||||
no_show: "Nicht abgestimmt",
|
||||
}
|
||||
|
||||
const USER_VOTE_LABELS: Record<string, string> = {
|
||||
ja: "Ja",
|
||||
nein: "Nein",
|
||||
enthaltung: "Enthaltung",
|
||||
}
|
||||
|
||||
const MANDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
|
||||
|
||||
async function resolveMandateId(politicianId: number): Promise<number | null> {
|
||||
@@ -56,6 +63,131 @@ async function resolveMandateId(politicianId: number): Promise<number | null> {
|
||||
return mandateId
|
||||
}
|
||||
|
||||
async function linkPollToLegislation(pollId: number, pollLabel: string) {
|
||||
const unlinked = await db
|
||||
.select()
|
||||
.from(legislationTexts)
|
||||
.where(isNull(legislationTexts.pollId))
|
||||
|
||||
for (const leg of unlinked) {
|
||||
const legWords = leg.title
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter((w) => w.length > 4)
|
||||
const pollLower = pollLabel.toLowerCase()
|
||||
const matchCount = legWords.filter((w) => pollLower.includes(w)).length
|
||||
|
||||
if (
|
||||
matchCount >= 2 ||
|
||||
(legWords.length > 0 && matchCount / legWords.length > 0.5)
|
||||
) {
|
||||
await db
|
||||
.update(legislationTexts)
|
||||
.set({ pollId })
|
||||
.where(eq(legislationTexts.id, leg.id))
|
||||
console.log(
|
||||
`[poll-checker] linked poll ${pollId} to legislation ${leg.id}: ${leg.title}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendVoteComparisonNotifications(
|
||||
pollId: number,
|
||||
pollLabel: string,
|
||||
pollUrl: string | undefined,
|
||||
allVotes: VoteDetail[],
|
||||
) {
|
||||
const legislations = await db
|
||||
.select()
|
||||
.from(legislationTexts)
|
||||
.where(eq(legislationTexts.pollId, pollId))
|
||||
|
||||
if (legislations.length === 0) return
|
||||
|
||||
const legislation = legislations[0]
|
||||
|
||||
const votes = await db
|
||||
.select()
|
||||
.from(userVotes)
|
||||
.where(eq(userVotes.legislationId, legislation.id))
|
||||
|
||||
if (votes.length === 0) return
|
||||
|
||||
const votesByMandate = new Map<number, VoteDetail>()
|
||||
for (const v of allVotes) {
|
||||
votesByMandate.set(v.mandate.id, v)
|
||||
}
|
||||
|
||||
for (const userVote of votes) {
|
||||
const deviceId = userVote.deviceId
|
||||
|
||||
const subs = await db
|
||||
.select()
|
||||
.from(pushSubscriptions)
|
||||
.where(eq(pushSubscriptions.deviceId, deviceId))
|
||||
.limit(1)
|
||||
if (subs.length === 0) continue
|
||||
|
||||
const politicianFollows = await db
|
||||
.select({ entityId: deviceFollows.entityId })
|
||||
.from(deviceFollows)
|
||||
.where(
|
||||
and(
|
||||
eq(deviceFollows.deviceId, deviceId),
|
||||
eq(deviceFollows.type, "politician"),
|
||||
),
|
||||
)
|
||||
|
||||
const comparisons: string[] = []
|
||||
for (const pf of politicianFollows) {
|
||||
const mandateId = await resolveMandateId(pf.entityId)
|
||||
if (mandateId === null) continue
|
||||
const repVote = votesByMandate.get(mandateId)
|
||||
if (!repVote) continue
|
||||
|
||||
const repLabel = VOTE_LABELS[repVote.vote] ?? repVote.vote
|
||||
const fraction = repVote.fraction?.label
|
||||
? ` (${repVote.fraction.label})`
|
||||
: ""
|
||||
comparisons.push(`${repVote.mandate.label}${fraction}: ${repLabel}`)
|
||||
}
|
||||
|
||||
const userVoteLabel = USER_VOTE_LABELS[userVote.vote] ?? userVote.vote
|
||||
const body = `Du: ${userVoteLabel}\n${comparisons.length > 0 ? comparisons.join("\n") : "Keine deiner Abgeordneten haben abgestimmt"}`
|
||||
|
||||
const payload: PushPayload = {
|
||||
title: `Abstimmungsergebnis: ${pollLabel}`,
|
||||
body,
|
||||
url: pollUrl,
|
||||
tag: `vote-result-${pollId}`,
|
||||
}
|
||||
|
||||
const success = await sendPushNotification(
|
||||
{
|
||||
endpoint: subs[0].endpoint,
|
||||
p256dh: subs[0].p256dh,
|
||||
auth: subs[0].auth,
|
||||
},
|
||||
payload,
|
||||
)
|
||||
|
||||
if (!success) {
|
||||
console.log(
|
||||
`[poll-checker] removing expired subscription for device ${deviceId}`,
|
||||
)
|
||||
await db
|
||||
.delete(pushSubscriptions)
|
||||
.where(eq(pushSubscriptions.deviceId, deviceId))
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[poll-checker] poll ${pollId}: sent ${votes.length} vote comparison notification(s)`,
|
||||
)
|
||||
}
|
||||
|
||||
interface DeviceNotification {
|
||||
deviceId: string
|
||||
endpoint: string
|
||||
@@ -118,6 +250,8 @@ async function processPoll(
|
||||
fieldTopics: { id: number }[],
|
||||
pollUrl: string | undefined,
|
||||
) {
|
||||
await linkPollToLegislation(pollId, pollLabel)
|
||||
|
||||
const topicIds = fieldTopics.map((t) => t.id)
|
||||
if (topicIds.length === 0) return
|
||||
|
||||
@@ -226,4 +360,7 @@ async function processPoll(
|
||||
console.log(
|
||||
`[poll-checker] poll ${pollId}: sent ${notifications.length} notification(s)`,
|
||||
)
|
||||
|
||||
// send vote comparison notifications for users who voted on linked legislation
|
||||
await sendVoteComparisonNotifications(pollId, pollLabel, pollUrl, allVotes)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user