Twitch chat

Apps Twitch chat 0.1

Login or Register an account to download this content
rafffel submitted a new resource:

Twitch chat - twitch chat

Twitch chat

What the app do ?

It's provide to a streamer the abilities to have the online chat of his channel and reply with in-game app.

How it's work ?
It's using IRC api and URL api of twitch.

What are the features ?
-Chat message from stream channel
-Color lines by nickname ( 13 different colors)
-Reply from in-game
-Scroll the chat (scroll up , scroll down , scroll end)
-See status of stream (offline if not you will see the...

Read more about this resource...
 
It is working perfect for me but please, if possible add the resize option.
I just wish it is a bit smaller (narrow).
 
Working here just fine,

The twitch.ini file must be filled as it follows,

[MAIN]
host = irc.chat.twitch.tv
port = 6667
nick = yourchannelname
pass = oauth:key
chan = #yourchannellname

in my case

[MAIN]
host = irc.chat.twitch.tv
port = 6667
nick = llorchdurden
pass = oauth:key
chan = #llorchdurden

get your own key on

https://twitchapps.com/tmi/

:D
 
Hi, if anyone else is getting ERROR LOADING MODULE : sys.path.append('apps/python/twitch'), I solved it by moving a few lines in twitch.py further down:
Code:
import ac

PATH_APP = os.path.dirname(__file__)
path_stdlib=''
if platform.architecture()[0] == '64bit':
    path_stdlib = os.path.join(PATH_APP, 'stdlib64')
else:
    path_stdlib = os.path.join(PATH_APP, 'stdlib')
sys.path.insert(0, path_stdlib)
sys.path.insert(0, os.path.join(PATH_APP, 'third_party'))

os.environ['PATH'] = os.environ['PATH'] + ';.'

import urllib.request
import json
import time
from threading import Thread
 
Hi, if anyone else is getting ERROR LOADING MODULE : sys.path.append('apps/python/twitch'), I solved it by moving a few lines in twitch.py further down:
Code:
import ac

PATH_APP = os.path.dirname(__file__)
path_stdlib=''
if platform.architecture()[0] == '64bit':
    path_stdlib = os.path.join(PATH_APP, 'stdlib64')
else:
    path_stdlib = os.path.join(PATH_APP, 'stdlib')
sys.path.insert(0, path_stdlib)
sys.path.insert(0, os.path.join(PATH_APP, 'third_party'))

os.environ['PATH'] = os.environ['PATH'] + ';.'

import urllib.request
import json
import time
from threading import Thread

Jesus, thank you for posting this. I couldn't for the life of me get it to work.

To reword what he said, edit "twitch.py" with an editor like notepad.exe, sublime text, atom.io

Move lines 10 through 19 to below line 4.

Below is a screenshot of what it should look like.

fix-twitch-chat-assetto-corsa-mod.PNG
 
Hi, if anyone else is getting ERROR LOADING MODULE : sys.path.append('apps/python/twitch'), I solved it by moving a few lines in twitch.py further down:
Code:
import ac

PATH_APP = os.path.dirname(__file__)
path_stdlib=''
if platform.architecture()[0] == '64bit':
    path_stdlib = os.path.join(PATH_APP, 'stdlib64')
else:
    path_stdlib = os.path.join(PATH_APP, 'stdlib')
sys.path.insert(0, path_stdlib)
sys.path.insert(0, os.path.join(PATH_APP, 'third_party'))

os.environ['PATH'] = os.environ['PATH'] + ';.'

import urllib.request
import json
import time
from threading import Thread

your the man its wooooooorks thank you :)
 
1578082598102.png

You can add a SetFontSize line here and mess around with message count and height. Any messing I tried to do resulted in input box being lost, so this might not be the simplest thing to figure out. Good luck!

Oh, and there's also a message count of 10 hardcoded in a few more places, so you'd have to change that as well
 
So for some reason I just couldn't just leave it like that. I changed up a few things:
  • Font size, window width, opacity and line count are configurable
  • Window height is calculated from line count and font size
  • Font changed to Consolas to allow for more accurate row length calculation
  • When a message is too long, user name is only written on the first line. I prefer it this way
If anyone's more familiar with Python code standards, feel free to clean it up, it's starting to look messy. Also, apparently .py files are not allowed to be uploaded, so I'll just post the contents of configuration and Python file. Sorry for the big post
Code:
[MAIN]
host = irc.chat.twitch.tv
port = 6667
nick =
pass =
chan =

[WINDOW]
opacity=0.75
width = 650
line_count = 11
font_size = 18
Code:
import os
import sys
import platform
import ac

PATH_APP = os.path.dirname(__file__)
path_stdlib=''
if platform.architecture()[0] == '64bit':
    path_stdlib = os.path.join(PATH_APP, 'stdlib64')
else:
    path_stdlib = os.path.join(PATH_APP, 'stdlib')
sys.path.insert(0, path_stdlib)
sys.path.insert(0, os.path.join(PATH_APP, 'third_party'))

os.environ['PATH'] = os.environ['PATH'] + ';.'

import math
import urllib.request
import json
import time
from threading import Thread

import socket
import re
import configparser
import requests


configfile = os.path.join(PATH_APP, 'twitch.ini')
config = configparser.ConfigParser()
config.read(configfile)

HOST = config['MAIN']['host']
PORT = int(config['MAIN']['port'])
NICK = config['MAIN']['nick']
PASS = config['MAIN']['pass']
CHAN = config['MAIN']['chan']

ac.initFont(0, 'Consolas', 0, 0)

BG_OPACITY = float(config['WINDOW']['opacity'])
WINDOW_WIDTH = int(config['WINDOW']['width'])
LINE_COUNT = int(config['WINDOW']['line_count'])
FONT_SIZE = int(config['WINDOW']['font_size'])

SYMBOL_COUNT = math.floor((WINDOW_WIDTH-45)/0.55/FONT_SIZE) #Consolas has ratio of 0.55
MESSAGE_HEIGHT = FONT_SIZE + 2
WINDOW_HEIGHT = MESSAGE_HEIGHT * (LINE_COUNT) + 75 + math.floor(MESSAGE_HEIGHT / 2)


CHAT_MSG=re.compile(r"^:\w+!\w+@\w+\.tmi\.twitch\.tv PRIVMSG #\w+ :")
LOGGUED_MSG=re.compile(r"^:.*?001.*?:Welcome.*")
LOGGUEDFAIL_MSG=re.compile(r"^:.*?Login authentication failed")
APP_NAME = 'Twitch chat'
messageLabel = []
messageList = ['Welcome to the twitch app','you will see the messages from defined IRC channel','you can scroll with the arrows on the right','you can answer using the text box','Have a fun stream']
while len(messageList) < LINE_COUNT:
    messageList.append('')
colors = (
            (255, 0, 0),
            (255, 128, 0),
            (255, 255, 0),
            (128, 255, 0),
            (0, 255, 0),
            (0, 255, 128),
            (0, 255, 255),
            (0, 128, 255),
            (0, 0, 255),
            (128, 0, 255),
            (255, 0, 255),
            (255, 0, 128),
            (128, 128, 128),
            (255, 255, 255),
        )
linkColorNick = [('',0)]
curr_color = 0
offsetMessage = 0
elsaped_time = 0
viewers = 'offline'
logged = 0
call = 0
running = True
fetching = False
def acMain(ac_version):
    global appWindow,messageLabel,messageList,s,next,prev,end
    appWindow=ac.newApp(APP_NAME)
    ac.setSize(appWindow,WINDOW_WIDTH,WINDOW_HEIGHT)
    ac.drawBorder(appWindow,0)
    ac.setBackgroundOpacity(appWindow,BG_OPACITY)
    s = socket.socket()
    s.connect((HOST, PORT))
    s.send("PASS {}\r\n".format(PASS).encode("utf-8"))
    s.send("NICK {}\r\n".format(NICK).encode("utf-8"))
    s.send("JOIN {}\r\n".format(CHAN).encode("utf-8"))
    s.setblocking(False)
    for i in range(0,LINE_COUNT):                                       
        messageLabel.append(ac.addLabel(appWindow,messageList[i]))
        ac.setPosition(messageLabel[i],15,(i*MESSAGE_HEIGHT)+ 30 + math.floor(MESSAGE_HEIGHT / 2))         
        ac.setFontSize(messageLabel[i],FONT_SIZE)                 
        ac.setCustomFont(messageLabel[i], 'Consolas', 0, 0)
    input_text = ac.addTextInput(appWindow,"")
    ac.setPosition(input_text,15,(MESSAGE_HEIGHT * (LINE_COUNT + 1)) + 30)
    ac.setSize(input_text,WINDOW_WIDTH-46,20)
    ac.addOnValidateListener(input_text,onSendMessage)
    prevButton = ac.addButton(appWindow,'ᐃ')
    ac.setSize(prevButton,20,20)
    ac.setFontSize(prevButton,12)
    ac.setPosition(prevButton,WINDOW_WIDTH-30,30)
    ac.addOnClickedListener(prevButton,onClickPrev)
    nextButton = ac.addButton(appWindow,'ᐁ')
    ac.setSize(nextButton,20,20)
    ac.setFontSize(nextButton,12)
    ac.setPosition(nextButton,WINDOW_WIDTH-30,WINDOW_HEIGHT-55)
    ac.addOnClickedListener(nextButton,onClickNext)
    endButton = ac.addButton(appWindow,'ꓪ')
    ac.setSize(endButton,20,20)
    ac.setFontSize(endButton,12)
    ac.setPosition(endButton,WINDOW_WIDTH-30,WINDOW_HEIGHT-35)
    ac.addOnClickedListener(endButton,onClickEnd)
    displayRefresh()
    ac.console('Twitch app init done')
    return APP_NAME
def onClickPrev(v1,v2):
    global offsetMessage,messageList
    if (len(messageList) > LINE_COUNT) and (offsetMessage < (len(messageList)-LINE_COUNT)):
        offsetMessage += 1
        displayRefresh()
def onClickNext(v1,v2):
    global offsetMessage,messageList
    if offsetMessage > 0:
        offsetMessage -= 1
        displayRefresh()
def onClickEnd(v1,v2):
    global offsetMessage
    offsetMessage = 0
    displayRefresh()
def onSendMessage(message):
    global s,messageList
    s.send("PRIVMSG {} :{}\r\n".format(CHAN, message).encode("utf-8"))
    colorIndex = getUsernameColor(NICK)
    messageList.append((NICK+": "+message,colorIndex))
    displayRefresh()
def splitMessage(mess,user):
    if mess.strip() == "":
        return []
    
    mess = user + ': ' + mess
    splitted = []
    length_sup = True
    rest_mess = mess
    while length_sup == True:
        if len(rest_mess) > SYMBOL_COUNT:
            split_mess = rest_mess[0:SYMBOL_COUNT]
            rest_mess = rest_mess[SYMBOL_COUNT:]         
            splitted.append(split_mess)           
        else:
            if rest_mess.strip() != "":
                splitted.append(rest_mess)
            length_sup = False
    return splitted
def rangeColor(value):
    OldRange = (255 - 0) 
    NewRange = (1 - 0) 
    NewValue = (((value - 0) * NewRange) / OldRange) + 0
    return NewValue
def displayRefresh():
    global messageList,messageLabel,offsetMessage
    lenList = len(messageList)
    for i in range(0,LINE_COUNT):                                     
        curr_mess = messageList[i+(lenList-LINE_COUNT)-offsetMessage]
        if isinstance(curr_mess, tuple):
            message = curr_mess[0]
            ColorIndex = curr_mess[1]
            ac.setText(messageLabel[i], "{}".format(message))
            R = rangeColor(colors[ColorIndex][0])
            G = rangeColor(colors[ColorIndex][1])
            B = rangeColor(colors[ColorIndex][2])
            ac.setFontColor(messageLabel[i],R,G,B,1)           
        else:
            ac.setText(messageLabel[i], "{}".format(curr_mess))
def getUsernameColor(nick):
    global curr_color,linkColorNick
    color = None
    for i in linkColorNick:
        if i[0] == nick:
            color = i[1]
    if color == None:
        linkColorNick.append((nick,curr_color))
        color = curr_color
        curr_color += 1
        if curr_color > (len(colors)-1):
            curr_color = 0
    return color
def getActualFollow():
    global viewers,CHAN,PASS,fetching
    fetching = True
    try:
        headers = {'Accept': 'application/vnd.twitchtv.v3+json'}
        r = requests.get('https://api.twitch.tv/kraken/streams/'+CHAN[1:]+'?oauth_token='+PASS[6:], headers=headers)
        jsonData = r.json()
        ac.console('json {}'.format(jsonData))
        if jsonData['stream'] != None:
            viewers = str(jsonData['stream']['viewers'])
        else:
            viewers = 'offline'
    except:
        pass
    fetching = False
    
def acUpdate(deltaT):
    global messageList,messageLabel,s,appWindow,elsaped_time,viewers,CHAN,logged
    elsaped_time += deltaT
    if elsaped_time > 5 and logged == 1:
        if fetching == False:
            Thread(target=getActualFollow).start()
        ac.setTitle(appWindow,'Twitch chat {} : {}'.format(CHAN,viewers))
        elsaped_time = 0
        ac.setBackgroundOpacity(appWindow,BG_OPACITY)
    try:
        response = s.recv(512).decode("utf-8")
        if logged == 0:
            res = re.search(LOGGUED_MSG, response)
            if res != None:
                logged = 1
                messageList.append('logged in success')
                displayRefresh()
            res = re.search(LOGGUEDFAIL_MSG, response)
            if res != None:
                logged = 1
                messageList.append('login fail please check your credential information')
                displayRefresh()
        else:
            if response == "PING :tmi.twitch.tv\r\n":
                s.send("PONG :tmi.twitch.tv\r\n".encode("utf-8"))
            elif response != None:
                result = re.search(CHAT_MSG, response)
                if result != None:
                    result = result.group(0)
                    username = re.search(r"\w+", response).group(0) # return the entire match
                    colorIndex = getUsernameColor(username)
                    message = CHAT_MSG.sub("", response)
                    message_ready = splitMessage(message,username)
                    for i in message_ready:
                        messageList.append((i,colorIndex))
                    displayRefresh()
    except:
        pass
def acShutdown():
    global s,running
    running = False
    s.close()
 
So for some reason I just couldn't just leave it like that. I changed up a few things:
  • Font size, window width, opacity and line count are configurable
  • Window height is calculated from line count and font size
  • Font changed to Consolas to allow for more accurate row length calculation
  • When a message is too long, user name is only written on the first line. I prefer it this way
If anyone's more familiar with Python code standards, feel free to clean it up, it's starting to look messy. Also, apparently .py files are not allowed to be uploaded, so I'll just post the contents of configuration and Python file. Sorry for the big post
Code:
[MAIN]
host = irc.chat.twitch.tv
port = 6667
nick =
pass =
chan =

[WINDOW]
opacity=0.75
width = 650
line_count = 11
font_size = 18
Code:
import os
import sys
import platform
import ac

PATH_APP = os.path.dirname(__file__)
path_stdlib=''
if platform.architecture()[0] == '64bit':
    path_stdlib = os.path.join(PATH_APP, 'stdlib64')
else:
    path_stdlib = os.path.join(PATH_APP, 'stdlib')
sys.path.insert(0, path_stdlib)
sys.path.insert(0, os.path.join(PATH_APP, 'third_party'))

os.environ['PATH'] = os.environ['PATH'] + ';.'

import math
import urllib.request
import json
import time
from threading import Thread

import socket
import re
import configparser
import requests


configfile = os.path.join(PATH_APP, 'twitch.ini')
config = configparser.ConfigParser()
config.read(configfile)

HOST = config['MAIN']['host']
PORT = int(config['MAIN']['port'])
NICK = config['MAIN']['nick']
PASS = config['MAIN']['pass']
CHAN = config['MAIN']['chan']

ac.initFont(0, 'Consolas', 0, 0)

BG_OPACITY = float(config['WINDOW']['opacity'])
WINDOW_WIDTH = int(config['WINDOW']['width'])
LINE_COUNT = int(config['WINDOW']['line_count'])
FONT_SIZE = int(config['WINDOW']['font_size'])

SYMBOL_COUNT = math.floor((WINDOW_WIDTH-45)/0.55/FONT_SIZE) #Consolas has ratio of 0.55
MESSAGE_HEIGHT = FONT_SIZE + 2
WINDOW_HEIGHT = MESSAGE_HEIGHT * (LINE_COUNT) + 75 + math.floor(MESSAGE_HEIGHT / 2)


CHAT_MSG=re.compile(r"^:\w+!\w+@\w+\.tmi\.twitch\.tv PRIVMSG #\w+ :")
LOGGUED_MSG=re.compile(r"^:.*?001.*?:Welcome.*")
LOGGUEDFAIL_MSG=re.compile(r"^:.*?Login authentication failed")
APP_NAME = 'Twitch chat'
messageLabel = []
messageList = ['Welcome to the twitch app','you will see the messages from defined IRC channel','you can scroll with the arrows on the right','you can answer using the text box','Have a fun stream']
while len(messageList) < LINE_COUNT:
    messageList.append('')
colors = (
            (255, 0, 0),
            (255, 128, 0),
            (255, 255, 0),
            (128, 255, 0),
            (0, 255, 0),
            (0, 255, 128),
            (0, 255, 255),
            (0, 128, 255),
            (0, 0, 255),
            (128, 0, 255),
            (255, 0, 255),
            (255, 0, 128),
            (128, 128, 128),
            (255, 255, 255),
        )
linkColorNick = [('',0)]
curr_color = 0
offsetMessage = 0
elsaped_time = 0
viewers = 'offline'
logged = 0
call = 0
running = True
fetching = False
def acMain(ac_version):
    global appWindow,messageLabel,messageList,s,next,prev,end
    appWindow=ac.newApp(APP_NAME)
    ac.setSize(appWindow,WINDOW_WIDTH,WINDOW_HEIGHT)
    ac.drawBorder(appWindow,0)
    ac.setBackgroundOpacity(appWindow,BG_OPACITY)
    s = socket.socket()
    s.connect((HOST, PORT))
    s.send("PASS {}\r\n".format(PASS).encode("utf-8"))
    s.send("NICK {}\r\n".format(NICK).encode("utf-8"))
    s.send("JOIN {}\r\n".format(CHAN).encode("utf-8"))
    s.setblocking(False)
    for i in range(0,LINE_COUNT):                                     
        messageLabel.append(ac.addLabel(appWindow,messageList[i]))
        ac.setPosition(messageLabel[i],15,(i*MESSAGE_HEIGHT)+ 30 + math.floor(MESSAGE_HEIGHT / 2))       
        ac.setFontSize(messageLabel[i],FONT_SIZE)               
        ac.setCustomFont(messageLabel[i], 'Consolas', 0, 0)
    input_text = ac.addTextInput(appWindow,"")
    ac.setPosition(input_text,15,(MESSAGE_HEIGHT * (LINE_COUNT + 1)) + 30)
    ac.setSize(input_text,WINDOW_WIDTH-46,20)
    ac.addOnValidateListener(input_text,onSendMessage)
    prevButton = ac.addButton(appWindow,'ᐃ')
    ac.setSize(prevButton,20,20)
    ac.setFontSize(prevButton,12)
    ac.setPosition(prevButton,WINDOW_WIDTH-30,30)
    ac.addOnClickedListener(prevButton,onClickPrev)
    nextButton = ac.addButton(appWindow,'ᐁ')
    ac.setSize(nextButton,20,20)
    ac.setFontSize(nextButton,12)
    ac.setPosition(nextButton,WINDOW_WIDTH-30,WINDOW_HEIGHT-55)
    ac.addOnClickedListener(nextButton,onClickNext)
    endButton = ac.addButton(appWindow,'ꓪ')
    ac.setSize(endButton,20,20)
    ac.setFontSize(endButton,12)
    ac.setPosition(endButton,WINDOW_WIDTH-30,WINDOW_HEIGHT-35)
    ac.addOnClickedListener(endButton,onClickEnd)
    displayRefresh()
    ac.console('Twitch app init done')
    return APP_NAME
def onClickPrev(v1,v2):
    global offsetMessage,messageList
    if (len(messageList) > LINE_COUNT) and (offsetMessage < (len(messageList)-LINE_COUNT)):
        offsetMessage += 1
        displayRefresh()
def onClickNext(v1,v2):
    global offsetMessage,messageList
    if offsetMessage > 0:
        offsetMessage -= 1
        displayRefresh()
def onClickEnd(v1,v2):
    global offsetMessage
    offsetMessage = 0
    displayRefresh()
def onSendMessage(message):
    global s,messageList
    s.send("PRIVMSG {} :{}\r\n".format(CHAN, message).encode("utf-8"))
    colorIndex = getUsernameColor(NICK)
    messageList.append((NICK+": "+message,colorIndex))
    displayRefresh()
def splitMessage(mess,user):
    if mess.strip() == "":
        return []
  
    mess = user + ': ' + mess
    splitted = []
    length_sup = True
    rest_mess = mess
    while length_sup == True:
        if len(rest_mess) > SYMBOL_COUNT:
            split_mess = rest_mess[0:SYMBOL_COUNT]
            rest_mess = rest_mess[SYMBOL_COUNT:]       
            splitted.append(split_mess)         
        else:
            if rest_mess.strip() != "":
                splitted.append(rest_mess)
            length_sup = False
    return splitted
def rangeColor(value):
    OldRange = (255 - 0)
    NewRange = (1 - 0)
    NewValue = (((value - 0) * NewRange) / OldRange) + 0
    return NewValue
def displayRefresh():
    global messageList,messageLabel,offsetMessage
    lenList = len(messageList)
    for i in range(0,LINE_COUNT):                                   
        curr_mess = messageList[i+(lenList-LINE_COUNT)-offsetMessage]
        if isinstance(curr_mess, tuple):
            message = curr_mess[0]
            ColorIndex = curr_mess[1]
            ac.setText(messageLabel[i], "{}".format(message))
            R = rangeColor(colors[ColorIndex][0])
            G = rangeColor(colors[ColorIndex][1])
            B = rangeColor(colors[ColorIndex][2])
            ac.setFontColor(messageLabel[i],R,G,B,1)         
        else:
            ac.setText(messageLabel[i], "{}".format(curr_mess))
def getUsernameColor(nick):
    global curr_color,linkColorNick
    color = None
    for i in linkColorNick:
        if i[0] == nick:
            color = i[1]
    if color == None:
        linkColorNick.append((nick,curr_color))
        color = curr_color
        curr_color += 1
        if curr_color > (len(colors)-1):
            curr_color = 0
    return color
def getActualFollow():
    global viewers,CHAN,PASS,fetching
    fetching = True
    try:
        headers = {'Accept': 'application/vnd.twitchtv.v3+json'}
        r = requests.get('https://api.twitch.tv/kraken/streams/'+CHAN[1:]+'?oauth_token='+PASS[6:], headers=headers)
        jsonData = r.json()
        ac.console('json {}'.format(jsonData))
        if jsonData['stream'] != None:
            viewers = str(jsonData['stream']['viewers'])
        else:
            viewers = 'offline'
    except:
        pass
    fetching = False
  
def acUpdate(deltaT):
    global messageList,messageLabel,s,appWindow,elsaped_time,viewers,CHAN,logged
    elsaped_time += deltaT
    if elsaped_time > 5 and logged == 1:
        if fetching == False:
            Thread(target=getActualFollow).start()
        ac.setTitle(appWindow,'Twitch chat {} : {}'.format(CHAN,viewers))
        elsaped_time = 0
        ac.setBackgroundOpacity(appWindow,BG_OPACITY)
    try:
        response = s.recv(512).decode("utf-8")
        if logged == 0:
            res = re.search(LOGGUED_MSG, response)
            if res != None:
                logged = 1
                messageList.append('logged in success')
                displayRefresh()
            res = re.search(LOGGUEDFAIL_MSG, response)
            if res != None:
                logged = 1
                messageList.append('login fail please check your credential information')
                displayRefresh()
        else:
            if response == "PING :tmi.twitch.tv\r\n":
                s.send("PONG :tmi.twitch.tv\r\n".encode("utf-8"))
            elif response != None:
                result = re.search(CHAT_MSG, response)
                if result != None:
                    result = result.group(0)
                    username = re.search(r"\w+", response).group(0) # return the entire match
                    colorIndex = getUsernameColor(username)
                    message = CHAT_MSG.sub("", response)
                    message_ready = splitMessage(message,username)
                    for i in message_ready:
                        messageList.append((i,colorIndex))
                    displayRefresh()
    except:
        pass
def acShutdown():
    global s,running
    running = False
    s.close()

Did you ever get the number of viewers working properly? I've been trying forever and all it's telling me is that SSL Module in Python is not working for the line "requests.get('https://api.twitch.tv/kraken/streams/'+CHAN[1:]+'?oauth_token='+PASS[6:], headers=headers)". When I run it in visual studio, it works perfectly fine.

Edit, fixed it. Followed what Obbi did here, https://www.assettocorsa.net/forum/index.php?threads/http-request-in-python-app.62353/#post-1115038. Download and import the x86 & x64 versions of _ssl.pyd into the stdlib folders and it will work.
 
Last edited:
So for some reason I just couldn't just leave it like that. I changed up a few things:
  • Font size, window width, opacity and line count are configurable
  • Window height is calculated from line count and font size
  • Font changed to Consolas to allow for more accurate row length calculation
  • When a message is too long, user name is only written on the first line. I prefer it this way
If anyone's more familiar with Python code standards, feel free to clean it up, it's starting to look messy. Also, apparently .py files are not allowed to be uploaded, so I'll just post the contents of configuration and Python file. Sorry for the big post
Code:
[MAIN]
host = irc.chat.twitch.tv
port = 6667
nick =
pass =
chan =

[WINDOW]
opacity=0.75
width = 650
line_count = 11
font_size = 18
Code:
import os
import sys
import platform
import ac

PATH_APP = os.path.dirname(__file__)
path_stdlib=''
if platform.architecture()[0] == '64bit':
    path_stdlib = os.path.join(PATH_APP, 'stdlib64')
else:
    path_stdlib = os.path.join(PATH_APP, 'stdlib')
sys.path.insert(0, path_stdlib)
sys.path.insert(0, os.path.join(PATH_APP, 'third_party'))

os.environ['PATH'] = os.environ['PATH'] + ';.'

import math
import urllib.request
import json
import time
from threading import Thread

import socket
import re
import configparser
import requests


configfile = os.path.join(PATH_APP, 'twitch.ini')
config = configparser.ConfigParser()
config.read(configfile)

HOST = config['MAIN']['host']
PORT = int(config['MAIN']['port'])
NICK = config['MAIN']['nick']
PASS = config['MAIN']['pass']
CHAN = config['MAIN']['chan']

ac.initFont(0, 'Consolas', 0, 0)

BG_OPACITY = float(config['WINDOW']['opacity'])
WINDOW_WIDTH = int(config['WINDOW']['width'])
LINE_COUNT = int(config['WINDOW']['line_count'])
FONT_SIZE = int(config['WINDOW']['font_size'])

SYMBOL_COUNT = math.floor((WINDOW_WIDTH-45)/0.55/FONT_SIZE) #Consolas has ratio of 0.55
MESSAGE_HEIGHT = FONT_SIZE + 2
WINDOW_HEIGHT = MESSAGE_HEIGHT * (LINE_COUNT) + 75 + math.floor(MESSAGE_HEIGHT / 2)


CHAT_MSG=re.compile(r"^:\w+!\w+@\w+\.tmi\.twitch\.tv PRIVMSG #\w+ :")
LOGGUED_MSG=re.compile(r"^:.*?001.*?:Welcome.*")
LOGGUEDFAIL_MSG=re.compile(r"^:.*?Login authentication failed")
APP_NAME = 'Twitch chat'
messageLabel = []
messageList = ['Welcome to the twitch app','you will see the messages from defined IRC channel','you can scroll with the arrows on the right','you can answer using the text box','Have a fun stream']
while len(messageList) < LINE_COUNT:
    messageList.append('')
colors = (
            (255, 0, 0),
            (255, 128, 0),
            (255, 255, 0),
            (128, 255, 0),
            (0, 255, 0),
            (0, 255, 128),
            (0, 255, 255),
            (0, 128, 255),
            (0, 0, 255),
            (128, 0, 255),
            (255, 0, 255),
            (255, 0, 128),
            (128, 128, 128),
            (255, 255, 255),
        )
linkColorNick = [('',0)]
curr_color = 0
offsetMessage = 0
elsaped_time = 0
viewers = 'offline'
logged = 0
call = 0
running = True
fetching = False
def acMain(ac_version):
    global appWindow,messageLabel,messageList,s,next,prev,end
    appWindow=ac.newApp(APP_NAME)
    ac.setSize(appWindow,WINDOW_WIDTH,WINDOW_HEIGHT)
    ac.drawBorder(appWindow,0)
    ac.setBackgroundOpacity(appWindow,BG_OPACITY)
    s = socket.socket()
    s.connect((HOST, PORT))
    s.send("PASS {}\r\n".format(PASS).encode("utf-8"))
    s.send("NICK {}\r\n".format(NICK).encode("utf-8"))
    s.send("JOIN {}\r\n".format(CHAN).encode("utf-8"))
    s.setblocking(False)
    for i in range(0,LINE_COUNT):                                     
        messageLabel.append(ac.addLabel(appWindow,messageList[i]))
        ac.setPosition(messageLabel[i],15,(i*MESSAGE_HEIGHT)+ 30 + math.floor(MESSAGE_HEIGHT / 2))       
        ac.setFontSize(messageLabel[i],FONT_SIZE)               
        ac.setCustomFont(messageLabel[i], 'Consolas', 0, 0)
    input_text = ac.addTextInput(appWindow,"")
    ac.setPosition(input_text,15,(MESSAGE_HEIGHT * (LINE_COUNT + 1)) + 30)
    ac.setSize(input_text,WINDOW_WIDTH-46,20)
    ac.addOnValidateListener(input_text,onSendMessage)
    prevButton = ac.addButton(appWindow,'ᐃ')
    ac.setSize(prevButton,20,20)
    ac.setFontSize(prevButton,12)
    ac.setPosition(prevButton,WINDOW_WIDTH-30,30)
    ac.addOnClickedListener(prevButton,onClickPrev)
    nextButton = ac.addButton(appWindow,'ᐁ')
    ac.setSize(nextButton,20,20)
    ac.setFontSize(nextButton,12)
    ac.setPosition(nextButton,WINDOW_WIDTH-30,WINDOW_HEIGHT-55)
    ac.addOnClickedListener(nextButton,onClickNext)
    endButton = ac.addButton(appWindow,'ꓪ')
    ac.setSize(endButton,20,20)
    ac.setFontSize(endButton,12)
    ac.setPosition(endButton,WINDOW_WIDTH-30,WINDOW_HEIGHT-35)
    ac.addOnClickedListener(endButton,onClickEnd)
    displayRefresh()
    ac.console('Twitch app init done')
    return APP_NAME
def onClickPrev(v1,v2):
    global offsetMessage,messageList
    if (len(messageList) > LINE_COUNT) and (offsetMessage < (len(messageList)-LINE_COUNT)):
        offsetMessage += 1
        displayRefresh()
def onClickNext(v1,v2):
    global offsetMessage,messageList
    if offsetMessage > 0:
        offsetMessage -= 1
        displayRefresh()
def onClickEnd(v1,v2):
    global offsetMessage
    offsetMessage = 0
    displayRefresh()
def onSendMessage(message):
    global s,messageList
    s.send("PRIVMSG {} :{}\r\n".format(CHAN, message).encode("utf-8"))
    colorIndex = getUsernameColor(NICK)
    messageList.append((NICK+": "+message,colorIndex))
    displayRefresh()
def splitMessage(mess,user):
    if mess.strip() == "":
        return []
  
    mess = user + ': ' + mess
    splitted = []
    length_sup = True
    rest_mess = mess
    while length_sup == True:
        if len(rest_mess) > SYMBOL_COUNT:
            split_mess = rest_mess[0:SYMBOL_COUNT]
            rest_mess = rest_mess[SYMBOL_COUNT:]       
            splitted.append(split_mess)         
        else:
            if rest_mess.strip() != "":
                splitted.append(rest_mess)
            length_sup = False
    return splitted
def rangeColor(value):
    OldRange = (255 - 0)
    NewRange = (1 - 0)
    NewValue = (((value - 0) * NewRange) / OldRange) + 0
    return NewValue
def displayRefresh():
    global messageList,messageLabel,offsetMessage
    lenList = len(messageList)
    for i in range(0,LINE_COUNT):                                   
        curr_mess = messageList[i+(lenList-LINE_COUNT)-offsetMessage]
        if isinstance(curr_mess, tuple):
            message = curr_mess[0]
            ColorIndex = curr_mess[1]
            ac.setText(messageLabel[i], "{}".format(message))
            R = rangeColor(colors[ColorIndex][0])
            G = rangeColor(colors[ColorIndex][1])
            B = rangeColor(colors[ColorIndex][2])
            ac.setFontColor(messageLabel[i],R,G,B,1)         
        else:
            ac.setText(messageLabel[i], "{}".format(curr_mess))
def getUsernameColor(nick):
    global curr_color,linkColorNick
    color = None
    for i in linkColorNick:
        if i[0] == nick:
            color = i[1]
    if color == None:
        linkColorNick.append((nick,curr_color))
        color = curr_color
        curr_color += 1
        if curr_color > (len(colors)-1):
            curr_color = 0
    return color
def getActualFollow():
    global viewers,CHAN,PASS,fetching
    fetching = True
    try:
        headers = {'Accept': 'application/vnd.twitchtv.v3+json'}
        r = requests.get('https://api.twitch.tv/kraken/streams/'+CHAN[1:]+'?oauth_token='+PASS[6:], headers=headers)
        jsonData = r.json()
        ac.console('json {}'.format(jsonData))
        if jsonData['stream'] != None:
            viewers = str(jsonData['stream']['viewers'])
        else:
            viewers = 'offline'
    except:
        pass
    fetching = False
  
def acUpdate(deltaT):
    global messageList,messageLabel,s,appWindow,elsaped_time,viewers,CHAN,logged
    elsaped_time += deltaT
    if elsaped_time > 5 and logged == 1:
        if fetching == False:
            Thread(target=getActualFollow).start()
        ac.setTitle(appWindow,'Twitch chat {} : {}'.format(CHAN,viewers))
        elsaped_time = 0
        ac.setBackgroundOpacity(appWindow,BG_OPACITY)
    try:
        response = s.recv(512).decode("utf-8")
        if logged == 0:
            res = re.search(LOGGUED_MSG, response)
            if res != None:
                logged = 1
                messageList.append('logged in success')
                displayRefresh()
            res = re.search(LOGGUEDFAIL_MSG, response)
            if res != None:
                logged = 1
                messageList.append('login fail please check your credential information')
                displayRefresh()
        else:
            if response == "PING :tmi.twitch.tv\r\n":
                s.send("PONG :tmi.twitch.tv\r\n".encode("utf-8"))
            elif response != None:
                result = re.search(CHAT_MSG, response)
                if result != None:
                    result = result.group(0)
                    username = re.search(r"\w+", response).group(0) # return the entire match
                    colorIndex = getUsernameColor(username)
                    message = CHAT_MSG.sub("", response)
                    message_ready = splitMessage(message,username)
                    for i in message_ready:
                        messageList.append((i,colorIndex))
                    displayRefresh()
    except:
        pass
def acShutdown():
    global s,running
    running = False
    s.close()
hi mate, app works ok but the splitting "tool" does not work, also putting a number instead of the symbol_count variable
is there a way to solve this? thanks!

Edit: not true...writing from outside AC the splitting took works
 
Last edited:
Did you ever get the number of viewers working properly? I've been trying forever and all it's telling me is that SSL Module in Python is not working for the line "requests.get('https://api.twitch.tv/kraken/streams/'+CHAN[1:]+'?oauth_token='+PASS[6:], headers=headers)". When I run it in visual studio, it works perfectly fine.

Edit, fixed it. Followed what Obbi did here, https://www.assettocorsa.net/forum/index.php?threads/http-request-in-python-app.62353/#post-1115038. Download and import the x86 & x64 versions of _ssl.pyd into the stdlib folders and it will work.
I've downloaded and installed the x86 pyton version, copied the _ssl.pyd in stdlib folder
then
I've downloaded and installed the x64 pyton version, copied the _ssl.pyd in stdlib64 folder

viewers stills "offline"
there is other to do?

thanks!
 
Troubleshooting Twitch Chat with 2-Factor Authentication:

If you're using the Twitch Chat mod for Assetto Corsa and have recently enabled 2-factor authentication on your Twitch account, you may encounter issues with the mod failing to log in. To fix this issue, follow these steps:

  1. Create a new API key for your Twitch account with 2-factor authentication enabled.
  2. Update the Twitch Chat mod's configuration file with the new API key.
  3. Test the mod to confirm that it is now able to log in to your Twitch account with 2-factor authentication.
If you run into any further issues, reach out to the mod developer or the Assetto Corsa community for additional support or troubleshooting advice.
 

Latest News

Online or Offline racing?

  • 100% online racing

    Votes: 93 7.7%
  • 75% online 25% offline

    Votes: 128 10.6%
  • 50% online 50% offline

    Votes: 172 14.2%
  • 25% online 75% offline

    Votes: 341 28.2%
  • 100% offline racing

    Votes: 471 39.0%
  • Something else, explain in comment

    Votes: 4 0.3%
Back
Top