2

I'm building a basic chatting program (Flack from CS50s web programming).

I have a dictionary where I'm storing channels & messages as key value pairs.

The messages are in a list so one key value pair would look like:

{"channelExample" : ["msg1", "msg2"]}.

I also have another variable that keeps track of the current room/channel the user is messaging in called currentRoom.

When a user submits a message, I'm attempting to update the messages in that channel by doing the following (emit is already imported & I've confirmed that currentRoom & the input message are string values):

@socketio.on("submit message")
def submitMessage(message):
    channels[currentRoom].append(message)
    emit("display message", message)

However I am being thrown an "Exception in thread..." error for channels[currentRoom].append(message)& I'm not sure why.

My full code in Flask:

import os

from flask import Flask, session, render_template, url_for, request, flash, redirect, jsonify
from flask_socketio import SocketIO, send, emit, join_room, leave_room


app = Flask(__name__)
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
socketio = SocketIO(app)

currentRoom = None
channels = {}

@app.route("/")
def index():
    return render_template("welcome.html", channels=channels)

@socketio.on("new channel")
def newChannel(channelName):
    # Store new channel to keep track of it
    channels.update( {channelName : []} )

@socketio.on("retrieve channels") 
def retrieveChannels():
    channelNames = []

    for channel in channels:
        channelNames.append(channel)

        emit("providing channels", channelNames)

@socketio.on("retrieve messages")    
def loadMessages(channelName):
    currentRoom = channelName

    channelMessages = channels[currentRoom]

    emit("load messages", channelMessages)

@socketio.on("submit message")
def submitMessage(message):
    channels[currentRoom].append(message)
    emit("display message", message)

Javascript:

document.addEventListener('DOMContentLoaded', () => {

    // Connect to websocket
    var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port);

    // When connected, 
    socket.on('connect', () => {

        var nameInput = document.querySelector("#usernameInput");
        var welcomeMessage = document.querySelector("#welcomeMessage");
        var createChannel = document.querySelector("#createChannel");
        var newChannelForm = document.querySelector("#newChannelForm");
        var newMessageForm = document.querySelector("#newMessageForm");

        function userExists() {
            // Check if user has come here before
            if (localStorage.getItem("username")) {
                // Display a welcome message
                welcomeMessage.innerHTML = `Welcome back ${localStorage.getItem("username")}!`;
                nameInput.style.display = "none";
                return true;
            }
            else {
                return false;
            }
        };

        function createChannelBtn(name) {
            // Create new channel & style it
            let newChannel = document.createElement("button");
            newChannel.id = name;
            newChannel.innerHTML = name;
            newChannel.className = "btn btn-block btn-outline-dark";
            newChannel.style.display = "block";

            // Attach to current list 
            document.querySelector("#channels").appendChild(newChannel);

            // When someone clicks the channel
            newChannel.onclick = () => {

                newChannel.classList.toggle("active");

                socket.emit("retrieve messages", newChannel.id);
                console.log("Retrieving messages!!!");

                socket.on("load messages", channelMessages => {
                    console.log("loading messages!");
                    for (let i = 0; i < channelMessages.length; i++) {
                        createMessage(channelMessages[i]);
                    }
                });

            };
        };

        function createMessage(messageContent) {
            let message = document.createElement("h6");
            message.innerHTML = messageContent;
            document.querySelector("#messageWindow").appendChild(message);
            console.log("Currently creating message!");
        };

        function loadChannels() {
            socket.emit("retrieve channels")

            socket.on("providing channels", channelNames => {
                for (let i = 0; i < channelNames.length; i++) {
                    createChannelBtn(channelNames[i]);
                }
            });
        };

        // Make sure the new channel form is not displayed until "Create channel" button is clicked
        newChannelForm.style.display = "none";

        // Check if user exists already in local storage
        userExists();

        loadChannels();

        // If someone submits a username...
        nameInput.addEventListener("click", () => {
            // if that username exists, do nothing
            if (userExists()) {
            }
            // else remember the username
            else {
                localStorage.setItem("username", document.querySelector("#user").value);
            }
        });

        // When someone wants to create a channel
        createChannel.addEventListener("click", () => {
            // Show form
            newChannelForm.style.display = "block";

            // When user inputs new channel name...
            newChannelForm.onsubmit = () => {

                // Retrieve their input
                var newChannelName = document.querySelector("#newChannel").value;

                // Create a new channel
                createChannelBtn(newChannelName);

                // Notify server to store new channel 
                socket.emit("new channel", newChannelName);

                // Clear input field
                document.querySelector("#newChannel").innerHTML = "";

                return false;

            };
        });

        newMessageForm.onsubmit = () => {
            let message = document.querySelector("#newMessage").value;
            console.log("You have entered " + message);

            socket.emit("submit message", message);
            console.log("Submitted message!");

            socket.on("display message", message => {
                createMessage(message);
                console.log("Displaying message!!");
            });

            return false;
        };

    });

    // DOM Ending Bracket
});

1 Answers1

0

Next time, post the full traceback, as well as all necessary files, as it will make it easier to debug your code. You say you verified that currentRoom is a string value, but perhaps only when receiving it from the browser. You don't check that in your submitMessage() function, and then you try to access your list of channels using None, as shown by the traceback which looks something like this:

Exception in thread Thread-13:
Traceback (most recent call last):
  File "C:\Users\Michael\AppData\Local\Programs\Python\Python37-32\lib\threading.py", line 917, in _bootstrap_inner
    self.run()
  File "C:\Users\Michael\AppData\Local\Programs\Python\Python37-32\lib\threading.py", line 865, in run
    self._target(*self._args, **self._kwargs)
  File "c:\Users\Michael\Desktop\sandbox\sandbox\lib\site-packages\socketio\server.py", line 640, in _handle_event_internal
    r = server._trigger_event(data[0], namespace, sid, *data[1:])
  File "c:\Users\Michael\Desktop\sandbox\sandbox\lib\site-packages\socketio\server.py", line 669, in _trigger_event
    return self.handlers[namespace][event](*args)
  File "c:\Users\Michael\Desktop\sandbox\sandbox\lib\site-packages\flask_socketio\__init__.py", line 280, in _handler
    *args)
  File "c:\Users\Michael\Desktop\sandbox\sandbox\lib\site-packages\flask_socketio\__init__.py", line 694, in _handle_event
    ret = handler(*args)
  File "c:\Users\Michael\Desktop\sandbox\sandbox.py", line 47, in submitMessage
    channels[currentRoom].append(message)
KeyError: None

That should alert you that your main concern is a KeyError, which is happening because you're passing in None to your channels dict, which means that currentRoom must be none.

So why is it happening? The issue lies on line 34:

32  @socketio.on("retrieve messages")    
33  def loadMessages(channelName):
34      currentRoom = channelName  # <--- Issue here
35  
36      channelMessages = channels[currentRoom]
37  
38      emit("load messages", channelMessages)

Python variables follow the LEGB rule, which means when resolving a variable name, the interpreter will first check the Local scope, then the Enclosing scope, then the Global scope, and finally the Built-in scope. However, when you assign a variable in a certain scope, Python will create the variable within that scope.

So, you've now created a new variable named currentRoom, which is a separate variable from the currentRoom defined at the top, and is only visible within the loadMessages() function! So, how do you let Python know that you are referring to that global variable when you assign within a function?

You use the global keyword:

@socketio.on("retrieve messages")    
def loadMessages(channelName):
    global currentRoom  # <--- This tells Python that `currentRoom` is a global variable
    currentRoom = channelName  # Assign to the global variable

    channelMessages = channels[currentRoom]

    emit("load messages", channelMessages)

Now, when we call submitMessage(), currentRoom now has an actual text value rather than just None, and the function succeeds without any error.

Michael Kolber
  • 1,309
  • 1
  • 14
  • 23