Twilio's Unofficial Vote for the Eurovision Song Contest

May 11, 2022
Written by
Naomi Pentrel
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Twilio's Unofficial Vote for the Eurovision Song Contest

Eurovision is near and while you can't officially vote just yet, we thought we'd allow the public to vote already - and not just for this year but for ALL Eurovision's that have happened since the beginning of Eurovision. That includes 2020 - the year Eurovision was cancelled.

How do I vote?

Text '2022' to one of the following numbers to get the list of all the 2022 participants and their youtube videos. Once you have picked a winner, text “2022 Country” to the same number (e.g. 2022 Germany).

Netherlands: +3197010253872

UK: +44 7401 199687

Sweden: +46 70 192 18 32

United States: +1 (267) 713-3577

What about the other years... Could we maybe vote for those too?

You can text any year from 1956 to 2022 to any of the above numbers and then vote for your favourite song.

How do I see the results?

You can see the results for 2022 here but you can also text “Results 2022” (or “Results YYYY”) to any of the above numbers to see who is currently in the lead.

Want to run your own Eurovision voting between your friends? 

Alright, let me show you how to set this up in 5 steps:

Step 1: Get the data set. 

The dataset for this project is publicly available at github.com/Spijkervet/eurovision_dataset. Download it here. If you would like to also add data for 2021 and 2022, you can get that data here and here.

Step 2: Set up the database.

MongoDB Atlas is the multi-cloud database service for MongoDB, and while we don’t need this service to be multi-cloud, Atlas makes it easy to host the database for this project in a fully managed environment - and it’s free. If you don’t have an account, sign up here.

Once you have created your account, follow these steps to create your Starter cluster (M0). You can select any available region, ideally one that is close to you. Name the cluster eurovision.

After the cluster is deployed, click on the connect button and follow the steps to connect with the mongo shell. It will ask you to create a username and password to access the database. You will also need to enable Network Access. Since this database does not require production-level settings, you can enable access from anywhere (0.0.0.0/0).

MongoDB Cluster configuration

After you have confirmed that you can successfully connect to the mongo shell, exit out of the shell and issue the following command:

mongoimport --uri "mongodb+srv://<db_username>@eurovision.<your_cluster_id>.mongodb.net/esc" -c contestants --type csv --file ~/<path_to_file>/contestants.csv --headerline

Replace <db_username> with the username you created for your database in the MongoDB Atlas UI. Replace <path_to_file> with the path to where you stored your contestants.csv file. Replace <your_cluster_id> with the alphanumeric characters from the MongoDB connection string you used to connect with the mongo shell. Enter the password for the database user when prompted.

You should see output like this:

2021-05-15T12:42:27.154+0200        connected to: mongodb+srv://[**REDACTED**]@eurovision.12abc.mongodb.net/esc
2021-05-15T12:42:30.089+0200        1603 document(s) imported successfully. 0 document(s) failed to import.

Your database is now set up.

Step 3: Get a Twilio Phone number

If you don’t have a Twilio account, sign up. Next, use the Twilio Console to acquire a Twilio number with texting capability. We’ll use this as our event number.

How to buy a Twilio phone number

Step 4: Set up the Twilio Function

Twilio functions is a serverless event-driven framework that allows you to run Node.js scripts. Go to the functions page. Add a new function and select the blank template.


Name the function “Eurovision Song Contest” and enter /esc in the path field.

In the code section, copy and paste the following code:

function createSongList(songs) {
    let response = "";
    let arrayLength = songs.length;
    for (var i = 0; i < arrayLength; i++) {
        let song = songs[i];
        // shorten youtube links
        youtube_short = song["youtube_url"].replace("https://youtube.com/watch?v=", "youtu.be/");
        response = response + song["to_country"] + ": " + youtube_short + "\n";
    }
    response.trim();
    return response;
}

function formatResults(results) {
    let response = "";
    let arrayLength = results.length;
    for (var i = 0; i < arrayLength; i++) {
        response = response + "Place " + (i+1) + ": " + results[i]["_id"] + "\n";
    }
    response.trim();
    return response;
}

exports.handler = function(context, event, callback) {
    
    const MongoClient = require('mongodb').MongoClient;
    const CryptoJS = require("crypto-js");
    const client = new MongoClient(context.MDB_URI); // Set up MongoClient
    const dbName = 'esc';                            // Database name
    const incoming_message = event.Body.trim()       // Remove extra whitespace

    // Connect to the datbase
    client.connect(function(err) {
        const db = client.db(dbName);
        const contestants_collection = db.collection('contestants');
        const votes_collection = db.collection('votes')
        let pipeline;

        let year = incoming_message.substring(0,4);
        let country;

        if (!isNaN(year)) {                         //  If it is a year
            if (incoming_message.length == 4) {     // send list of songs for 
                pipeline = [
                    { "$match": {"year": parseInt(year)} }
                ];
                contestants_collection.aggregate(pipeline).toArray(function(err, docs) {
                    let twiml = new Twilio.twiml.MessagingResponse();
                    let response;
                    if (docs && docs.length) {
                        response = createSongList(docs);
                        } else {
                        response = "Sorry! There is nothing in the database " + 
                            "for the year " + token + ". Please try a " +  
                            "different year (1956 - 2022).";
                    }
                        twiml.message(response);
                        callback(null, twiml);
                    client.close();
                });
                
            } else if (incoming_message.length > 4) {      // we have a country
                country = incoming_message.substring(5);   

                // create hash of phone number
                let hash = CryptoJS.MD5(event.From).toString();
                
                contestants_collection.findOne({
                // Check if there is an entry for the specified year and country
                    year: parseInt(year),
                    to_country: country
                }, function (err, doc) {
                    if (doc) {
                        // vote (overwrite if needed)
                        votes_collection.update({
                            "year": year,
                            "voter" : hash,
                        }, {
                            "$set": { "country": country }
                        }, {
                            "upsert": true
                        }, function(err, res) {
                            // send text to confirm vote
                            let twiml = new Twilio.twiml.MessagingResponse();
                            twiml.message("Thank you. Your vote for " + country +
                                " was registered. You can only vote once for " + year + ". If you" +
                                " vote again, your previous vote will no longer count. To" +
                                " get the current voting results, reply with \"Results " +
                                year + "\".");
                            callback(null, twiml);
                            client.close();                        
                        });
                    } else {
                        let twiml = new Twilio.twiml.MessagingResponse();
                        twiml.message("We could not find an entry for " + country + " in " + year + ". Sorry.");
                        callback(null, twiml);
                        client.close();
                    }
                });            
                
            } else {
                let twiml = new Twilio.twiml.MessagingResponse();
                twiml.message("Sorry. It looks like you tried to vote but the" +
                " input wasn't formatted in a way the service understood. " + 
                "Please try again and send a message with \"" +
                "2022 Country\". (for example: \"2022 Germany\").")
                callback(null, twiml);
                client.close();
            }

        } else {                                    // we have text
            if (incoming_message.startsWith("Results")
                && !isNaN(incoming_message.substring(8))) {  // send results
                let year = incoming_message.substring(8);
                votes_collection.aggregate([
                    { "$match": { "year": year } },
                    { "$group": { "_id": "$country", "count": { "$sum": 1 } } },
                    { "$sort": { "count": -1 } },
                    { "$limit": 10}
                ]).toArray(function(err, docs){
                    if (docs) {
                        console.log(docs);
                        let twiml = new Twilio.twiml.MessagingResponse();
                        let results = formatResults(docs);
                        twiml.message("The current top 10 for " + incoming_message.substring(8) +
                            " are:\n" + results)
                        callback(null, twiml);
                        client.close();
                    }                    
                })

            } else {
                let twiml = new Twilio.twiml.MessagingResponse();
                twiml.message("Welcome to The Unofficial Eurovision Vote - " +
                    "hosted by Twilio. Reply with \"2022\" (or another year) " +
                    "to get the list of participants. To vote send a message " +
                    "with \"2022 Country\". (for example: \"2022 Germany\"). " +
                    "To get the current voting results, reply with " +
                    "\"Results 2022\"")
                callback(null, twiml);
                client.close();
            }
        }
    });
};

Click save. Once we connect this function to your Twilio phone number in Step 5, the function will run whenever someone sends a message to your number. The function parses the incoming message:

  • If the message contains only a year, it gets the list of countries and the corresponding YouTube links for the performances for that year from the database and sends both in the reply to the user.
  • If a user sends a message containing a year and a country, the code checks whether there is an entry from that country in that year and if so it saves the vote in the database. To ensure users can only vote once, the code stores a hash of the user‘s phone number with the vote.
  • If a user sends a message with a year and “Results“, the function gets the current top 10 from the database and sends it to the user.
  • If the message doesn‘t match any of the above, it sends a welcome message with instructions.
  • If an error occurs, the function informs the user of the error.

Next, go to the Functions configure page. Add MDB_URI as an environment variable. This is the same connection string you used above - it looks something like this:

mongodb+srv://<db_username>:<db_password>@eurovision.<your_cluster_id>.mongodb.net/esc?retryWrites=true&w=majority

Then, add the MongoDB node.js module as a dependency by entering mongodb as the name and 3.6.6 as the version. Also, add the crypto-js module with the name crypto-js and the version 4.0.0. Click save. The mongodb module will allow you to connect to the database. The crypto-js module will allow you to hash phone numbers to keep everyone’s phone numbers private.

Adding environment variables in Twilio console

Step 5: Connect the Twilio Function to your Twilio Phone number

Go to the Twilio Console and click on your phone number. In the configuration screen, scroll to the Messaging options and configure it to use a Function when a message comes in. Select the function path /esc in the dropdown menu.

Configuring the messaging webhook in Twilio

You have now connected your Twilio function to your Twilio phone number.

Bonus Step: Let your friends know. 

That’s it! Give your friends the number and instructions and select your own winners!

I hope this project will bring you some joy, and I would love to hear who your winners are. Let me know on Twitter @naomi_pen.