diff options
-rw-r--r-- | readme.txt | 1 | ||||
-rw-r--r-- | source.rkt | 149 |
2 files changed, 150 insertions, 0 deletions
diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..fdb5cc1 --- /dev/null +++ b/readme.txt @@ -0,0 +1 @@ +to do: write readme. diff --git a/source.rkt b/source.rkt new file mode 100644 index 0000000..1560993 --- /dev/null +++ b/source.rkt @@ -0,0 +1,149 @@ +#lang racket + +(require net/url) +(require json) +(require db) + +(define polls-per-day 4) ; configurable, set to any positive integer +(define midnight-seconds (* 4 3600)) ; UTC-4, adjust accordingly +(define channel-id "CHANNEL ID") ;replace this with the channel id, as a string +(define bot-token "BOT TOKEN") ; replace this with the bot token, as a string + +(when (or (eq? channel-id "CHANNEL ID") (eq? bot-token "BOT TOKEN")) + (display "Please set the channel-id and bot-token variables.") + (newline) + (exit)) + +(define dbc (sqlite3-connect #:database "database.db" #:mode 'create)) + +(unless (table-exists? dbc "entries") + (unless (file-exists? "entries.txt") + (display "Please put the entries into a file named entries.txt, separated by newlines.") + (newline) + (exit)) + (start-transaction dbc) + (query-exec dbc "create table entries (name text unique not null, round int not null)") + (query-exec dbc "create table open_polls (round int not null, msg_id text not null)") + (query-exec dbc "create table closed_polls (round int not null, entry_1 text not null, entry_2 text not null, voters_1 text not null, voters_2 text not null)") + (for ((entry (string-split (port->string (open-input-file "entries.txt") #:close? #t) "\n"))) + (query-exec dbc "insert into entries (name, round) values (?, 1)" entry)) + (commit-transaction dbc)) + +(define (api-get endpoint) + (string->jsexpr + (port->string + (get-pure-port + (string->url (string-append "https://discord.com/api/v10" endpoint)) + (list (string-append "Authorization: Bot " bot-token))) + #:close? #t))) + +(define (api-post endpoint content) + (string->jsexpr + (port->string + (post-pure-port + (string->url (string-append "https://discord.com/api/v10" endpoint)) + (string->bytes/utf-8 (jsexpr->string content)) + (list + (string-append "Authorization: Bot " bot-token) + "Content-Type: application/json")) + #:close? #t))) + +(define (get-completed-poll-answers msg-id (sleep-time 1)) + (let ((resp (api-get (string-append "/channels/" channel-id "/messages/" msg-id)))) + (if (hash-ref (hash-ref (hash-ref resp 'poll) 'results) 'is_finalized) + (hash-ref (hash-ref resp 'poll) 'answers) + (begin + (sleep sleep-time) + (get-completed-poll-answers msg-id (* sleep-time 2)))))) + +(define (get-users msg-id answer-id (so-far '()) (last-user #f)) + (let* ((base-endpoint + (string-append + "/channels/" channel-id "/polls/" msg-id "/answers/" (number->string answer-id) "?limit=100")) + (resp (api-get (if last-user (string-append base-endpoint "&after=" last-user) base-endpoint))) + (users (hash-ref resp 'users)) + (user-ids (map (lambda (s) (hash-ref s 'id)) users))) + (if (empty? user-ids) + so-far + (get-users msg-id answer-id (append user-ids so-far) (last user-ids))))) + +(define (process-completed-poll round msg-id) + (api-post (string-append "/channels/" channel-id "/polls/" msg-id "/expire") (make-hash)) + (let* ((answers (get-completed-poll-answers msg-id)) + (e1id (hash-ref (car answers) 'answer_id)) + (e2id (hash-ref (cadr answers) 'answer_id)) + (e1t (hash-ref (hash-ref (car answers) 'poll_media) 'text)) + (e2t (hash-ref (hash-ref (cadr answers) 'poll_media) 'text)) + (e1u (get-users msg-id e1id)) + (e2u (get-users msg-id e2id)) + (e1w (>= (length e1u) (length e2u))) + (e2w (>= (length e2u) (length e1u)))) + (start-transaction dbc) + (query-exec dbc + "insert into closed_polls (round, entry_1, entry_2, voters_1, voters_2) values (?, ?, ?, ?, ?)" + round e1t e2t (string-join e1u ",") (string-join e2u ",")) + (query-exec dbc "delete from open_polls where msg_id = ?" msg-id) + (query-exec dbc "update entries set round = ? where name = ?" (if e1w (+ round 1) 0) e1t) + (query-exec dbc "update entries set round = ? where name = ?" (if e2w (+ round 1) 0) e2t) + (commit-transaction dbc))) + +(define (create-poll e1 e2 round poll-number) + (let ((resp (api-post + (string-append "/channels/" channel-id "/messages") + (hash + 'poll (hash + 'question (hash 'text (string-append "Today's Poll #" (number->string poll-number))) + 'answers (list (hash 'poll_media (hash 'text e1)) (hash 'poll_media (hash 'text e2))) + 'duration 23))))) + (query-exec dbc "insert into open_polls (round, msg_id) values (?, ?)" round (hash-ref resp 'id)))) + +(define (send-message msg) + (api-post + (string-append "/channels/" channel-id "/messages") + (hash 'content msg))) + +(define (sleep-until-next) + (sleep 2) + (sleep (modulo (- midnight-seconds (current-seconds)) 86400))) + +(sleep-until-next) +(for ((row (query-rows dbc "select * from open_polls"))) + (process-completed-poll (vector-ref row 0) (vector-ref row 1))) + +(define on-round (apply min (query-list dbc "select distinct round from entries where round > 0"))) +(define this-round (shuffle (query-list dbc "select name from entries where round = ?" on-round))) + +(define (main-loop) + (define last-remaining #f) + (when (= 1 (length this-round)) + (set! last-remaining (car this-round)) + (query-exec dbc "update entries set round = ? where name = ?" (+ 1 on-round) (car this-round)) + (set! this-round '())) + (when (= 0 (length this-round)) + (set! on-round (+ 1 on-round)) + (set! this-round (shuffle (query-list dbc "select name from entries where round = ?" on-round))) + (when (= 1 (length this-round)) + (send-message (string-append "# WINNER WINNER\n## " (string-upcase (car this-round)))) + (exit)) + (send-message + (string-append + "# ROUND " (number->string on-round) + (if last-remaining + (string-append + "\nThe only entry remaining in the previous round was \"" + last-remaining "\", so it advanced. ") + "\n") + (number->string (length this-round)) " entries remain:\n* " + (string-join this-round "\n* ")))) + (if (file-exists? "SKIP-TODAY") + (send-message "There is no poll today :)") + (for ((i (range polls-per-day))) + (when (> (length this-round) 1) + (create-poll (car this-round) (cadr this-round) on-round (+ 1 i)) + (set! this-round (cddr this-round))))) + (sleep-until-next) + (for ((row (query-rows dbc "select * from open_polls"))) + (process-completed-poll (vector-ref row 0) (vector-ref row 1))) + (main-loop)) + +(main-loop) |