├── .gitignore ├── README.md ├── UI_Config.json ├── VIPTools_StreamlabsSystem.py ├── config.py ├── definitions.py └── lib ├── miscLib.py └── twitchLib.py /.gitignore: -------------------------------------------------------------------------------- 1 | data/* 2 | .idea 3 | .vscode 4 | token.txt 5 | settings.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Streamlabs-Chatbot-VIPTools 2 | ============================= 3 | 4 | A Python Script for Streamlabs Chatbot with some additional features for the (new) Twitch "VIP" role (Users can check-in every time you stream to earn the badge). 5 | 6 | How to install 7 | ---------------------------------- 8 | 9 | 1. Go to this directory (starting from the root where Streamlabs Chatbot is installed): 10 | 11 | ```plain 12 | Services\Scripts\ 13 | ``` 14 | 15 | 2. Create a new Folder called _"VIPTools"_ 16 | 3. Drop/clone all of the stuff from this Repository in there 17 | 4. Load the scripts in SL Chatbot 18 | 5. Important: Set your Channel-ID and API-Credentials in the settings of the script in SL Chatbot 19 | 20 | If you're having trouble with loading scripts in the SL Chatbot, see: https://www.youtube.com/watch?v=l3FBpY-0880 21 | 22 | How to use 23 | ------------ 24 | 25 | You don't really need to configure much besides the credentials mentioned above. 26 | Usage information will be added here later. 27 | 28 | **Important:** Don't delete your old VODs (whyever you would). This script is based on the saved VODs and their IDs. 29 | Maybe there will be a command in the future, to fix the checkins after a VOD was deleted. 30 | 31 | This Script only works when you have activated the saving of your past streams as VODs on Twitch. 32 | 33 | Changelog 34 | --------- 35 | 36 | **v0.0.1** 37 | 38 | * initial Build and commit 39 | 40 | **v0.1.0** 41 | 42 | * Released first working version with correct stream comparison 43 | * Added some QoL stuff, especially code cleanup and some additional chat responses (still hardcoded) 44 | * (Jokers to be added in one of the next releases) 45 | 46 | **v0.2.0** 47 | 48 | * Added joker functionality 49 | * Fixed some bugs related to stream ID comparison 50 | * Still needs to be tested in one of the next streams, expect some bugs 51 | 52 | **v0.2.1** 53 | 54 | * Fixes some bugs again, stream comparison and updating data file should be working fine now (but still needs to be tested a lot more #expectmorebugs) 55 | 56 | **v0.3.0** 57 | 58 | * Adds new Feature: "!resetcheckins" command for mods, to reset all checkins from the last stream to the current stream id (useful if you had a stream reconnect for example) 59 | 60 | **v0.4.0** 61 | 62 | * Adds new Feature: VIPStatusHandler. This one always adds your current VIP Status to the response message 63 | 64 | **v0.4.1** 65 | 66 | * Fixes a bug with the new VIPStatusHandler feature, which prevented new users to be added to the data file 67 | 68 | **v0.4.2** 69 | 70 | * Fixes a bug that the tool didn't check if the channels last video is a stream video or highlight (users lost jokers even if they checked in at the last stream, when a highlight as created in the meantime) 71 | 72 | **v0.4.3** 73 | 74 | * Fixes a bug where the VIP status wasn't correctly handled, when a user reaches VIP or is VIP already 75 | 76 | **v0.5.0** 77 | 78 | * Added new feature: automatic backups in archive folder on every unload of the script 79 | * Added new feature: automatic creation of data and archive folder on startup 80 | * Fixed a bug with !resetcheckins mod-command, that didn't handle checkins in the second last stream (the last "real" stream) 81 | * Renamed "!resetcheckins" mod-command to "!rcar" (for "reset checkins after reconnect" :P) 82 | * Some additional refactoring tasks and code quality improvements (like splitting up in some first modules) 83 | 84 | **v0.6.0** 85 | 86 | * Added new feature: Top10Vipcheckins command to view the current top checkins (maybe there will be another command to see the top 10 all-time check in streaks in the future, see below) 87 | 88 | **v0.7.0** 89 | 90 | * Added new feature: Top10VipcheckinsAlltime command to view the ALL-TIME top checkins with date of last highest streak checkin 91 | * Some adjustments on readme and config file 92 | 93 | **v1.0.0** 94 | 95 | * BREAKING: Switches/Migrates to the new and currently supported Twitch API (Helix). 96 | * From now on you have to set up your twitch dev application and set the credentials in the script settings! 97 | * Since the script settings are introduced now, we could provide further customization options in the future 98 | 99 | **Basic ideas and todo list while further developing this tool (in planned priority order)** 100 | 101 | * Set up initial project on github 102 | * Viewers can send a command to the chat once day / stream and after X ongoing "check ins" with this command they will be rewarded the VIP status (manually) 103 | * Maybe there could be something like joker: Let's say a user has 2 joker which will be used when he doesn't check in within the next stream in a row. After the joker count hits 0 he loses his collected streak. 104 | * Prevent command from being called when the stream isn't live (can't get stream object stuff) 105 | * Viewers can list all current VIPs via command and the current max number of VIPs for the channel (this is limited by twitch in different stages, see: https://help.twitch.tv/s/article/Managing-Roles-for-your-Channel#faq) Official Twitch function: "/vips" 106 | * If a viewer reaches the necessary goal it will set to "active vip status" in the data file of the script 107 | * Reconnect-Improvement: Overhaul of the "resetcheckins"-command for streamers: Two commands: Everyone who already checked in in the last stream, will just be set to the current stream id and date. Everyone who didn't yet checkin, but did in the second last stream, will be set to the last stream id and date (could be a stable v1.0.0 after that). 108 | * Automatically backup data files in archive folder with timestamp when stream starts (on script load) 109 | * Feature: Top10VipCheckins or something similar 110 | * Feature: Log highest checkinstreak in vipdata file (with date of last checkin of that streak) 111 | * Replace "in a row" with dynamic response (only if it's more then 1 checkIn in a row) 112 | * Clean up config file / make adjustable in chatbot settings 113 | * Make (all/some) texts localizable in settings? 114 | * Extend documentation in readme file 115 | * Automatically set as VIP for the channel when goal reached (only the streamer account can do this, so the script needs to send a command as streamer in the chat. Needs to be researched if this is possible via Twitch API) -------------------------------------------------------------------------------- /UI_Config.json: -------------------------------------------------------------------------------- 1 | { 2 | "output_file": "settings.json", 3 | "ChannelId": { 4 | "type": "textbox", 5 | "value": "", 6 | "label": "ChannelId", 7 | "tooltip": "The Channel-ID of your Twitch-Channel (See links below how to get it)", 8 | "group": "General" 9 | }, 10 | "AppClientId": { 11 | "type": "textbox", 12 | "value": "", 13 | "label": "AppClientId", 14 | "tooltip": "The App ClientId of your Twitch App (get it from https://dev.twitch.tv/console/apps)", 15 | "group": "General" 16 | }, 17 | "AppSecret": { 18 | "type": "textbox", 19 | "value": "", 20 | "label": "AppSecret", 21 | "tooltip": "The App Secret of your Twitch App (get it from https://dev.twitch.tv/console/apps)", 22 | "group": "General" 23 | }, 24 | "LinkChannelId": { 25 | "type": "button", 26 | "label": "Get your Channel-ID", 27 | "tooltip": "Opens https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/ in the default browser to get your Channel ID", 28 | "function": "LinkChannelId", 29 | "wsevent": "", 30 | "group": "Twitch / Helpful links" 31 | }, 32 | "LinkDevDashboard": { 33 | "type": "button", 34 | "label": "Twitch Dev-Dashboard", 35 | "tooltip": "Opens https://dev.twitch.tv/console/apps in the default browser to get your App-Information", 36 | "function": "LinkDevDashboard", 37 | "wsevent": "", 38 | "group": "Twitch / Helpful links" 39 | } 40 | } -------------------------------------------------------------------------------- /VIPTools_StreamlabsSystem.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | #--------------------------- 5 | # Import Libraries 6 | #--------------------------- 7 | import os 8 | import sys 9 | import json 10 | import time 11 | import codecs 12 | import collections 13 | from pprint import pprint 14 | from shutil import copyfile 15 | 16 | import clr 17 | clr.AddReference("IronPython.SQLite.dll") 18 | clr.AddReference("IronPython.Modules.dll") 19 | 20 | # Load own modules 21 | sys.path.append(os.path.dirname(__file__)) # point at current folder for classes / references 22 | sys.path.append(os.path.join(os.path.dirname(__file__), "lib")) # point at lib folder for classes / references 23 | 24 | from definitions import ROOT_DIR 25 | import config 26 | import miscLib 27 | import twitchLib 28 | 29 | 30 | #--------------------------- 31 | # [Required] Script Information (must be existing in this main file) 32 | # TODO: Some stuff from here should be moved to a GUI settings file later 33 | #--------------------------- 34 | ScriptName = config.ScriptName 35 | Website = config.Website 36 | Description = config.Description 37 | Creator = config.Creator 38 | Version = config.Version 39 | 40 | 41 | #--------------------------- 42 | # Log helper (For logging into Script Logs of the Chatbot) 43 | # Note that you need to pass the "Parent" object and use the normal "Parent.Log" function if you want to log something inside of a module 44 | #--------------------------- 45 | def Log(message): 46 | Parent.Log(ScriptName, str(message)) 47 | return 48 | 49 | #--------------------------------------- 50 | # Settings functions 51 | #--------------------------------------- 52 | class Settings: 53 | SettingsFile = config.SettingsFile 54 | # Loads settings from file if file is found if not uses default values 55 | 56 | # The 'default' variable names need to match UI_Config 57 | def __init__(self, SettingsFile=None): 58 | if SettingsFile and os.path.isfile(SettingsFile): 59 | with codecs.open(SettingsFile, encoding='utf-8-sig', mode='r') as f: 60 | self.__dict__ = json.load(f, encoding='utf-8-sig') 61 | 62 | # Reload settings on save through UI 63 | def ReloadSettings(self, jsonData): 64 | # Reload settings on save through UI 65 | self.__dict__ = json.loads(jsonData, encoding='utf-8-sig') 66 | Log("Settings saved") 67 | return 68 | 69 | ############################################# 70 | # START: Generic but edited Chatbot functions 71 | ############################################# 72 | 73 | #--------------------------- 74 | # [Required] Initialize Data (Only called on load of script) 75 | #--------------------------- 76 | def Init(): 77 | # Load in saved settings 78 | global UserSettings 79 | global Parent 80 | 81 | UserSettings = Settings(config.SettingsFile) 82 | Log("Settings loaded") 83 | 84 | # generate data and archive directory if they don't exist (uses VipdataBackupPath because it includes the data path) 85 | if (False == os.path.isdir(config.VipdataBackupPath)): 86 | os.makedirs(config.VipdataBackupPath) 87 | 88 | # Creates an empty data file if it doesn't exist 89 | if (False == os.path.isfile(config.VipdataFilepath)): 90 | # generate empty data file and save it 91 | data = {} 92 | with open(config.VipdataFilepath, 'w') as f: 93 | json.dump(data, f, indent=4) 94 | 95 | # Check and refresh access token 96 | twitchLib.CheckAndRefreshAccessToken(Parent, UserSettings) 97 | Log("Checked and refreshed Twitch access token if needed") 98 | 99 | Log("Script successfully initialized") 100 | 101 | return 102 | 103 | #--------------------------- 104 | # [Required] Execute Data / Process messages 105 | #--------------------------- 106 | def Execute(data): 107 | # call parse function if any of our defined commands is called 108 | # vipcheckin command called 109 | if (data.IsChatMessage() and data.GetParam(0).lower() == config.CommandVIPCheckIn): 110 | currentStreamObject = twitchLib.GetCurrentStreamObject(Parent, UserSettings) 111 | 112 | if (currentStreamObject == None): 113 | Parent.SendStreamMessage(config.ResponseOnlyWhenLive) 114 | return 115 | 116 | ParsedResponse = Parse(config.ResponseVIPCheckIn, config.CommandVIPCheckIn, data) # Parse response 117 | Parent.SendStreamMessage(ParsedResponse) # Send your message to chat 118 | 119 | # reset after reconnect command called 120 | if (data.IsChatMessage() and data.GetParam(0).lower() == config.CommandResetAfterReconnect): 121 | currentStreamObject = twitchLib.GetCurrentStreamObject(Parent, UserSettings) 122 | 123 | if (currentStreamObject == None): 124 | Parent.SendStreamMessage(config.ResponseOnlyWhenLive) 125 | return 126 | else: 127 | if (True == Parent.HasPermission(data.User, "Moderator", "")): 128 | ParsedResponse = Parse(config.ResponseResetAfterReconnect, config.CommandResetAfterReconnect, data) # Parse response 129 | Parent.SendStreamMessage("Now resetting the checkins from last stream due to a reconnect, please wait..") 130 | Parent.SendStreamMessage(ParsedResponse) # Send your message to chat 131 | else: 132 | Parent.SendStreamMessage(config.ResponsePermissionDeniedMod) 133 | return 134 | 135 | # reset checkIns command called 136 | if (data.IsChatMessage() and data.GetParam(0).lower() == config.CommandResetCheckIns): 137 | currentStreamObject = twitchLib.GetCurrentStreamObject(Parent, UserSettings) 138 | 139 | if (currentStreamObject == None): 140 | Parent.SendStreamMessage(config.ResponseOnlyWhenLive) 141 | return 142 | 143 | if (1 == ResetCheckinsForUser(data.User)): 144 | Parent.SendStreamMessage(config.ResponseResetCheckIns) # Send your message to chat 145 | 146 | # top10vipcheckins command called 147 | if (data.IsChatMessage() and data.GetParam(0).lower() == config.CommandTop10Vipcheckins): 148 | top10vipcheckinsMessage = GetTop10VipcheckinsWithData(False) 149 | Parent.SendStreamMessage(str(config.ResponseTop10Vipcheckins)) # Send your message to chat 150 | Parent.SendStreamMessage(str(top10vipcheckinsMessage)) # Send your message to chat 151 | 152 | # top10vipcheckinsalltime command called 153 | if (data.IsChatMessage() and data.GetParam(0).lower() == config.CommandTop10VipcheckinsAlltime): 154 | # make sure every user has a highest checkin streak and highest checkin streak date value (for older vipdata files) 155 | if (True == CheckAndFixAlltimeCheckins()): 156 | top10vipcheckinsAlltimeMessage = GetTop10VipcheckinsWithData(True) 157 | Parent.SendStreamMessage(str(config.ResponseTop10VipcheckinsAlltime)) # Send your message to chat 158 | Parent.SendStreamMessage(str(top10vipcheckinsAlltimeMessage)) # Send your message to chat 159 | else: 160 | Parent.SendStreamMessage("Error: Something went wrong when trying to fix the alltime checkins") 161 | 162 | return 163 | 164 | #--------------------------- 165 | # [Required] Tick method (Gets called during every iteration even when there is no incoming data) 166 | # Runs basically every millisecond since the script is activated^^ 167 | #--------------------------- 168 | def Tick(): 169 | return 170 | 171 | #--------------------------- 172 | # [Optional] Parse method (Allows you to create your own custom $parameters) 173 | # Here's where the magic happens, all the strings are sent and processed through this function 174 | # 175 | # Parent.FUNCTION allows to use functions of the Chatbot and other outside APIs (see: https://github.com/AnkhHeart/Streamlabs-Chatbot-Python-Boilerplate/wiki/Parent) 176 | # 177 | # ORIGINAL DEF: def Parse(parseString, userid, username, targetid, targetname, message): 178 | #--------------------------- 179 | def Parse(parseString, command, data): 180 | # if vipcheckin command is called 181 | if (command == config.CommandVIPCheckIn): 182 | parseString = UpdateDataFile(data.User) 183 | 184 | if ("error" != parseString): 185 | parseString = parseString + GetStats(data.User) 186 | 187 | # if CommandResetAfterReconnect is called 188 | if (command == config.CommandResetAfterReconnect): 189 | parseString = FixDatafileAfterReconnect() 190 | 191 | # after every necessary variable was processed: return the whole parseString, if it wasn't already 192 | return parseString 193 | 194 | #--------------------------- 195 | # [Optional] Reload Settings (Called when a user clicks the Save Settings button in the Chatbot UI) 196 | #--------------------------- 197 | def ReloadSettings(jsonData): 198 | # Reload saved settings 199 | UserSettings.ReloadSettings(jsonData) 200 | # End of ReloadSettings 201 | 202 | #--------------------------- 203 | # [Optional] Unload (Called when a user reloads their scripts or closes the bot / cleanup stuff) 204 | #--------------------------- 205 | def Unload(): 206 | Log("Script unloaded") 207 | BackupDataFile() 208 | return 209 | 210 | #--------------------------- 211 | # [Optional] ScriptToggled (Notifies you when a user disables your script or enables it) 212 | #--------------------------- 213 | def ScriptToggled(state): 214 | return 215 | 216 | ############################################# 217 | # END: Generic Chatbot functions 218 | ############################################# 219 | 220 | #--------------------------- 221 | # UpdateDataFile: Function for modfiying the file which contains the data, see data/vipdata.json 222 | # returns the parseString for parse(Function) 223 | #--------------------------- 224 | def UpdateDataFile(username): 225 | currentday = miscLib.GetCurrentDayFormattedDate() 226 | response = "error" 227 | 228 | # this loads the data of file vipdata.json into variable "data" 229 | with open(config.VipdataFilepath, 'r') as f: 230 | data = json.load(f) 231 | 232 | # check if the given username exists in data. -> user doesnt exist yet, create array of the user data with current default values, which will be stored in vipdata.json 233 | if (True == IsNewUser(username)): 234 | data[str(username.lower())] = {} 235 | data[str(username.lower())][config.JSONVariablesCheckInsInARow] = 1 236 | data[str(username.lower())][config.JSONVariablesLastCheckIn] = currentday 237 | data[str(username.lower())][config.JSONVariablesLastCheckInStreamId] = twitchLib.GetCurrentStreamId(Parent, UserSettings) 238 | data[str(username.lower())][config.JSONVariablesRemainingJoker] = 2 239 | data[str(username.lower())][config.JSONVariablesHighestCheckInStreak] = 1 240 | data[str(username.lower())][config.JSONVariablesHighestCheckInStreakDate] = currentday 241 | 242 | # directly return it, because "isnewstream" would be technically true as well but not correct in this case 243 | response = "Congratulations for your first check in, " + username + "! When you reach a streak of 30 check ins in a row, you'll have the chance to get the VIP badge (you have two jokers if you miss some streams). Good luck! Hint: type '/vips' to list all current VIPs of this channel. " 244 | 245 | # if the user already exists, update the user with added checkIn count, but we need to check here if it's the first beer today or not to set the right values 246 | else: 247 | if (data[str(username.lower())][config.JSONVariablesCheckInsInARow]): 248 | 249 | # for existing users: check and set highest streak to current streak 250 | if (config.JSONVariablesHighestCheckInStreak not in data[str(username.lower())] or data[str(username.lower())][config.JSONVariablesHighestCheckInStreak] < data[str(username.lower())][config.JSONVariablesCheckInsInARow]): 251 | data[str(username.lower())][config.JSONVariablesHighestCheckInStreak] = data[str(username.lower())][config.JSONVariablesCheckInsInARow] 252 | data[str(username.lower())][config.JSONVariablesHighestCheckInStreakDate] = data[str(username.lower())][config.JSONVariablesLastCheckIn] 253 | 254 | # new stream since last checkIn? 255 | if (True == IsNewStream(username)): 256 | 257 | # ongoing check in (no missed stream)? 258 | if (True == EqualsLastCheckinGivenStreamByListId(username, 1)): 259 | data[str(username.lower())][config.JSONVariablesCheckInsInARow] += 1 260 | data[str(username.lower())][config.JSONVariablesLastCheckIn] = currentday 261 | data[str(username.lower())][config.JSONVariablesLastCheckInStreamId] = twitchLib.GetCurrentStreamId(Parent, UserSettings) 262 | # only count highest streak counter up if it's actually lower than the checkins 263 | if (data[str(username.lower())][config.JSONVariablesHighestCheckInStreak] < data[str(username.lower())][config.JSONVariablesCheckInsInARow]): 264 | data[str(username.lower())][config.JSONVariablesHighestCheckInStreak] = data[str(username.lower())][config.JSONVariablesCheckInsInARow] 265 | data[str(username.lower())][config.JSONVariablesHighestCheckInStreakDate] = data[str(username.lower())][config.JSONVariablesLastCheckIn] 266 | 267 | response = username + ' just checked in for this stream! ' 268 | else: 269 | # joker available? 270 | if (GetJoker(username) > 0): 271 | data[str(username.lower())][config.JSONVariablesCheckInsInARow] += 1 272 | data[str(username.lower())][config.JSONVariablesLastCheckIn] = currentday 273 | data[str(username.lower())][config.JSONVariablesLastCheckInStreamId] = twitchLib.GetCurrentStreamId(Parent, UserSettings) 274 | data[str(username.lower())][config.JSONVariablesRemainingJoker] -= 1 275 | # only count highest streak counter up if it's actually lower than the checkins 276 | if (data[str(username.lower())][config.JSONVariablesHighestCheckInStreak] < data[str(username.lower())][config.JSONVariablesCheckInsInARow]): 277 | data[str(username.lower())][config.JSONVariablesHighestCheckInStreak] = data[str(username.lower())][config.JSONVariablesCheckInsInARow] 278 | data[str(username.lower())][config.JSONVariablesHighestCheckInStreakDate] = data[str(username.lower())][config.JSONVariablesLastCheckIn] 279 | 280 | response = username + ' just checked in for this stream, but needed to use a joker! ' 281 | else: 282 | data[str(username.lower())][config.JSONVariablesCheckInsInARow] = 1 283 | data[str(username.lower())][config.JSONVariablesLastCheckIn] = currentday 284 | data[str(username.lower())][config.JSONVariablesLastCheckInStreamId] = twitchLib.GetCurrentStreamId(Parent, UserSettings) 285 | data[str(username.lower())][config.JSONVariablesRemainingJoker] = 2 286 | 287 | response = "Daaamn " + username + ", you wasted all your jokers. Now you're starting from scratch! Come join again the next time and don't miss a stream again! " 288 | else: 289 | response = username + ' already checked in for this stream. Come join again the next time! ' 290 | 291 | # VIP Status Handler 292 | if (IsVip(username) == 0): 293 | if (config.JSONVariablesVIPStatus in data[str(username.lower())]): 294 | if (data[str(username.lower())][config.JSONVariablesCheckInsInARow] == 30): 295 | data[str(username.lower())][config.JSONVariablesVIPStatus] = 1 296 | response = "WHOOP! You've just made it and got 30 VIP check ins in a row. Get in contact with Dave and collect your VIP badge - congrats!" 297 | 298 | else: 299 | if (data[str(username.lower())][config.JSONVariablesVIPStatus] != 1 and data[str(username.lower())][config.JSONVariablesCheckInsInARow] >= 30): 300 | data[str(username.lower())][config.JSONVariablesVIPStatus] = 1 301 | else: 302 | data[str(username.lower())][config.JSONVariablesVIPStatus] = 0 303 | else: 304 | data[str(username.lower())][config.JSONVariablesVIPStatus] = 0 305 | 306 | # after everything was modified and updated, we need to write the stuff from our "data" variable to the vipdata.json file 307 | os.remove(config.VipdataFilepath) 308 | with open(config.VipdataFilepath, 'w') as f: 309 | json.dump(data, f, indent=4) 310 | 311 | return response + " | VIP-Status: " + config.VIPStatusLocalization[int(IsVip(username))] 312 | 313 | #--------------------------- 314 | # returns bool if it is a new stream or not 315 | #--------------------------- 316 | def IsNewStream(username): 317 | newStream = False 318 | 319 | # this loads the data of file vipdata.json into variable "data" 320 | with open(config.VipdataFilepath, 'r') as f: 321 | data = json.load(f) 322 | 323 | lastCheckInStreamId = data[str(username.lower())][config.JSONVariablesLastCheckInStreamId] 324 | currentStreamId = twitchLib.GetCurrentStreamId(Parent, UserSettings) 325 | 326 | if (currentStreamId != lastCheckInStreamId): 327 | return True 328 | 329 | return newStream 330 | 331 | #--------------------------- 332 | # returns bool if the last checkin was in the same stream as given by listid (offset of saved streams) 333 | #--------------------------- 334 | def EqualsLastCheckinGivenStreamByListId(username, listId): 335 | # this loads the data of file vipdata.json into variable "data" 336 | with open(config.VipdataFilepath, 'r') as f: 337 | data = json.load(f) 338 | 339 | lastCheckInStreamId = data[str(username.lower())][config.JSONVariablesLastCheckInStreamId] 340 | secondLastStreamId = twitchLib.GetAttributeByVideoListId("stream_id", listId, Parent, UserSettings) 341 | secondLastStreamId = twitchLib.GetAttributeByVideoListId("stream_id", listId, Parent, UserSettings) 342 | 343 | if (secondLastStreamId == lastCheckInStreamId): 344 | return True 345 | 346 | return False 347 | 348 | #--------------------------- 349 | # returns the string formatted streak (still hardcoded) 350 | #--------------------------- 351 | def GetStreak(username): 352 | 353 | with open(config.VipdataFilepath, 'r') as f: 354 | data = json.load(f) 355 | 356 | if str(username.lower()) not in data: 357 | streak = "1/30" 358 | else: 359 | streak = str(data[str(username.lower())][config.JSONVariablesCheckInsInARow]) + "/30" 360 | 361 | return streak 362 | 363 | #--------------------------- 364 | # returns the remaining joker int 365 | #--------------------------- 366 | def GetJoker(username): 367 | 368 | with open(config.VipdataFilepath, 'r') as f: 369 | data = json.load(f) 370 | 371 | if str(username.lower()) not in data: 372 | joker = 2 373 | else: 374 | joker = int(data[str(username.lower())][config.JSONVariablesRemainingJoker]) 375 | 376 | return joker 377 | 378 | 379 | 380 | #--------------------------- 381 | # IsNewUser 382 | #--------------------------- 383 | def IsNewUser(username): 384 | # this loads the data of file vipdata.json into variable "data" 385 | with open(config.VipdataFilepath, 'r') as f: 386 | data = json.load(f) 387 | 388 | if str(username.lower()) not in data: 389 | return True 390 | else: 391 | return False 392 | 393 | #--------------------------- 394 | # GetStats 395 | #--------------------------- 396 | def GetStats(username): 397 | return ' | Current streak: ' + str(GetStreak(username)) + ' | Remaining joker: ' + str(GetJoker(username)) 398 | 399 | #--------------------------- 400 | # FixDatafileAfterReconnect 401 | #--------------------------- 402 | def FixDatafileAfterReconnect(): 403 | # this loads the data of file vipdata.json into variable "data" 404 | with open(config.VipdataFilepath, 'r') as f: 405 | data = json.load(f) # dict 406 | 407 | for user in data: 408 | # if user already checked in before reconnection 409 | if (EqualsLastCheckinGivenStreamByListId(user.lower(), 1) == True): 410 | Log('This user checked in, in the last stream object and will be reset:') 411 | Log(user) 412 | data[user][config.JSONVariablesLastCheckInStreamId] = twitchLib.GetCurrentStreamId(Parent, UserSettings) 413 | data[user][config.JSONVariablesLastCheckIn] = miscLib.GetCurrentDayFormattedDate() 414 | # if user didn't check in before reconnection, but checked in in the last real stream (second last stream) 415 | if (EqualsLastCheckinGivenStreamByListId(user.lower(), 2) == True): 416 | Log('This User checked in in the second last stream object and will be reset:') 417 | Log(user) 418 | data[user][config.JSONVariablesLastCheckInStreamId] = twitchLib.GetAttributeByVideoListId("stream_id", 1, Parent, UserSettings) 419 | data[user][config.JSONVariablesLastCheckIn] = twitchLib.GetAttributeByVideoListId("created_at", 1, Parent, UserSettings)[0:10] # only use the first 10 digits, because working with RFC3339 in python is...... 420 | 421 | 422 | # after everything was modified and updated, we need to write the stuff from our "data" variable to the vipdata.json file 423 | os.remove(config.VipdataFilepath) 424 | with open(config.VipdataFilepath, 'w') as f: 425 | json.dump(data, f, indent=4) 426 | 427 | return "Okay, I've reset the checkins from last stream to the current stream." 428 | 429 | #--------------------------- 430 | # IsVip 431 | # 432 | # Returns 0 or 1 if a given user is a VIP or not (in our datafile) 433 | #--------------------------- 434 | def IsVip(username): 435 | with open(config.VipdataFilepath, 'r') as f: 436 | data = json.load(f) # dict 437 | 438 | if str(username.lower()) not in data: 439 | return 0 440 | 441 | if (config.JSONVariablesVIPStatus in data[str(username.lower())]): 442 | if (data[str(username.lower())][config.JSONVariablesVIPStatus] == 1): 443 | return 1 444 | 445 | return 0 446 | 447 | #--------------------------- 448 | # ResetCheckinsForUser (but not the vip status) 449 | # 450 | # Returns 1 if the function was successful or not 451 | #--------------------------- 452 | def ResetCheckinsForUser(username): 453 | # this loads the data of file vipdata.json into variable "data" 454 | with open(config.VipdataFilepath, 'r') as f: 455 | data = json.load(f) # dict 456 | 457 | data[str(username.lower())][config.JSONVariablesCheckInsInARow] = 1 458 | data[str(username.lower())][config.JSONVariablesLastCheckIn] = miscLib.GetCurrentDayFormattedDate() 459 | data[str(username.lower())][config.JSONVariablesLastCheckInStreamId] = twitchLib.GetCurrentStreamId(Parent, UserSettings) 460 | data[str(username.lower())][config.JSONVariablesRemainingJoker] = 2 461 | 462 | # after everything was modified and updated, we need to write the stuff from our "data" variable to the vipdata.json file 463 | os.remove(config.VipdataFilepath) 464 | with open(config.VipdataFilepath, 'w') as f: 465 | json.dump(data, f, indent=4) 466 | 467 | return 1 468 | 469 | #--------------------------- 470 | # BackupDataFile 471 | # 472 | # Backups the data file in the "archive" folder with current date and timestamp for ease of use 473 | #--------------------------- 474 | def BackupDataFile(): 475 | if (True == os.path.isfile(config.VipdataFilepath)): 476 | 477 | if (False == os.path.isdir(config.VipdataBackupPath)): 478 | os.makedirs(config.VipdataBackupPath) 479 | 480 | dstFilename = config.VipdataBackupFilePrefix + str(miscLib.GetCurrentDayFormattedDate()) + "_" + str(int(time.time())) + ".json" 481 | dstFilepath = os.path.join(config.VipdataBackupPath, dstFilename) 482 | copyfile(config.VipdataFilepath, dstFilepath) 483 | 484 | return 485 | 486 | #--------------------------- 487 | # GetTop10Vipcheckins 488 | # 489 | # Returns a list of all top 10 vipcheckin users sorted by checkins to be iterated 490 | # Param: "alltime = TRUE" returns the alltime top10vipcheckins (highest streak ever) 491 | #--------------------------- 492 | def GetTop10Vipcheckins(alltime): 493 | with open(config.VipdataFilepath, 'r') as f: 494 | data = json.load(f) 495 | 496 | # build sortableDict with user and checkin count like "user: checkins" 497 | sortableCheckinsDict = {} 498 | for user in data: 499 | # different list if alltime = True 500 | if (True == alltime): 501 | sortableCheckinsDict[user] = data[user][config.JSONVariablesHighestCheckInStreak] 502 | else: 503 | sortableCheckinsDict[user] = data[user][config.JSONVariablesCheckInsInARow] 504 | 505 | # sort it by checkins and put it in a list of max 10 items 506 | sortedCheckinsList = sorted(sortableCheckinsDict.items(), key=lambda x: x[1], reverse=True) 507 | sortedCheckinsDict = collections.OrderedDict(sortedCheckinsList) 508 | 509 | # only the first 10 items 510 | return sortedCheckinsDict.keys()[:10] 511 | 512 | #--------------------------- 513 | # GetTop10VipcheckinsWithData 514 | # 515 | # Returns a complete string of all top 10 vipcheckin users sorted by checkins and with data (checkins) 516 | # Param: bool "alltime = TRUE" returns the alltime top10vipcheckins (highest streak ever) 517 | #--------------------------- 518 | def GetTop10VipcheckinsWithData(alltime): 519 | top10Vipcheckins = GetTop10Vipcheckins(alltime) 520 | 521 | top10VipcheckinsWithData = "" 522 | 523 | # get data for response 524 | with open(config.VipdataFilepath, 'r') as f: 525 | data = json.load(f) 526 | 527 | position = 0 528 | for checkinUser in top10Vipcheckins: 529 | position += 1 530 | top10VipcheckinsWithData += "#" + str(position) + " " 531 | top10VipcheckinsWithData += str(checkinUser) 532 | top10VipcheckinsWithData += " (" 533 | # different output, when alltime = True 534 | if (True == alltime): 535 | top10VipcheckinsWithData += str(data[checkinUser][config.JSONVariablesHighestCheckInStreak]) + " at " + str(data[checkinUser][config.JSONVariablesHighestCheckInStreakDate]) 536 | else: 537 | top10VipcheckinsWithData += str(data[checkinUser][config.JSONVariablesCheckInsInARow]) 538 | 539 | top10VipcheckinsWithData += ")" 540 | top10VipcheckinsWithData += " [" + config.VIPStatusLocalizationSimple[int(IsVip(checkinUser))] + "]" 541 | 542 | # only display dash below last position 543 | if (position < 10): 544 | top10VipcheckinsWithData += " - " 545 | 546 | return top10VipcheckinsWithData 547 | 548 | #--------------------------- 549 | # CheckAndFixAlltimeCheckins 550 | # 551 | # Checks if there is a user left without alltime checkins (highest checkin streak) set and fixes it. 552 | # Returns true, if successfull 553 | # 554 | # ToDo: Meeseeks-Code -> Delete it since it's not actively useful? 555 | #--------------------------- 556 | def CheckAndFixAlltimeCheckins(): 557 | returnStatus = False 558 | 559 | # this loads the data of file vipdata.json into variable "data" 560 | with open(config.VipdataFilepath, 'r') as f: 561 | data = json.load(f) # dict 562 | 563 | for user in data: 564 | # if user doesn't have a HighestCheckInStreak set yet 565 | if (config.JSONVariablesHighestCheckInStreak not in data[str(user.lower())]): 566 | Log('Automatically set highestCheckInStreak for user:') 567 | Log(user) 568 | data[str(user.lower())][config.JSONVariablesHighestCheckInStreak] = data[str(user.lower())][config.JSONVariablesCheckInsInARow] 569 | data[str(user.lower())][config.JSONVariablesHighestCheckInStreakDate] = data[str(user.lower())][config.JSONVariablesLastCheckIn] 570 | 571 | returnStatus = True 572 | 573 | # after everything was modified and updated, we need to write the stuff from our "data" variable to the vipdata.json file 574 | os.remove(config.VipdataFilepath) 575 | with open(config.VipdataFilepath, 'w') as f: 576 | json.dump(data, f, indent=4) 577 | 578 | return returnStatus 579 | 580 | 581 | 582 | #--------------------------- 583 | # Helpful links / UI buttons 584 | #--------------------------- 585 | def OpenLink(link): 586 | os.system("explorer " + link) 587 | 588 | def LinkChannelId(): 589 | OpenLink("https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/") 590 | 591 | def LinkDevDashboard(): 592 | OpenLink("https://dev.twitch.tv/console/apps") -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | from definitions import ROOT_DIR 6 | 7 | #--------------------------- 8 | # [Required] Script Information 9 | # TODO: Some stuff from here should be moved to a GUI settings file later 10 | #--------------------------- 11 | 12 | ScriptName = "♦ VIPTools" 13 | Website = "https://twitch.tv/rialDave/" 14 | Description = "Adds new features for Twitchs VIP functionality (Users can check-in every time you stream to earn the badge)" 15 | Creator = "rialDave" 16 | Version = "0.7.0-dev" 17 | 18 | #--------------------------- 19 | # Global Variables 20 | # Some stuff from here should be moved to a GUI settings file later 21 | #--------------------------- 22 | 23 | VipdataFolder = "data" 24 | VipdataFilename = "vipdata.json" 25 | VipdataFilepath = os.path.join(ROOT_DIR, VipdataFolder, VipdataFilename) 26 | VipdataBackupFolder = "archive" # inside data path 27 | VipdataBackupFilePrefix = "vipdata_bak-" 28 | VipdataBackupPath = os.path.join(ROOT_DIR, VipdataFolder, VipdataBackupFolder) 29 | 30 | SettingsFile = os.path.join(os.path.dirname(__file__), "settings.json") 31 | 32 | TokenFile = os.path.join(os.path.dirname(__file__), "token.txt") 33 | 34 | VariableChannelName = "$channelName" 35 | VariableUser = "$user" 36 | VariableCheckInCount = "$checkInCount" 37 | VariableCheckInCountReadable = "$checkInCountReadable" 38 | VariableNeededCheckins = "$neededCheckIns" 39 | VIPStatusLocalization = { 40 | 0: "No VIP", 41 | 1: "VIP - but you can go on collecting check ins, if you want. Thank you for always being here! <3" 42 | } 43 | VIPStatusLocalizationSimple = { 44 | 0: "No VIP", 45 | 1: "VIP" 46 | } 47 | ApiVideoLimit = "10" 48 | 49 | # Configuration of keys in json file 50 | JSONVariablesCheckInsInARow = "check_ins_in_a_row" 51 | JSONVariablesLastCheckIn = "last_check_in" 52 | JSONVariablesLastCheckInStreamId = "last_check_in_streamid" 53 | JSONVariablesRemainingJoker = "remaining_joker" 54 | JSONVariablesVIPStatus = "vipstatus" 55 | JSONVariablesHighestCheckInStreak = "highest_check_in_streak" 56 | JSONVariablesHighestCheckInStreakDate = "highest_check_in_streak_date" 57 | 58 | #--------------------------- 59 | # Command settings and responses (caution: some of the response texts are overwritten later / not refactored yet) 60 | #--------------------------- 61 | 62 | CommandVIPCheckIn = "!vipcheckin" 63 | ResponseVIPCheckIn = "Great! " + VariableUser + " just checked in for the " + VariableCheckInCountReadable + " time in a row! Status: " + VariableCheckInCount + "/" + VariableNeededCheckins 64 | CommandResetAfterReconnect = "!rcar" 65 | ResponseResetAfterReconnect = "Okay, I've reset the checkins from last stream to the current stream." 66 | CommandResetCheckIns = "!resetvipcheckins" 67 | ResponseResetCheckIns = "Okay! Your check ins have been reset and you automatically checked in for this stream. Just send " + CommandVIPCheckIn + " again, the next time you're here again." 68 | CommandTop10Vipcheckins = "!top10vipcheckins" 69 | ResponseTop10Vipcheckins = "Alright, here are the top 10 craziest VIPCheckin guys (THANKS <3):" 70 | CommandTop10VipcheckinsAlltime = "!top10vipcheckinsalltime" 71 | ResponseTop10VipcheckinsAlltime = "Oh, All-time? Alright, here are the top 10 craziest VIPCheckin guys of ALL-TIME (THANKS <3):" 72 | 73 | ResponsePermissionDeniedMod = "Permission denied: You have to be a Moderator to use this command!" 74 | ResponseOnlyWhenLive = "ERROR: This command is only available, when the stream is live. Sorry!" 75 | 76 | # Twitch API-URLs 77 | ApiUrlCurrentStream = "https://api.twitch.tv/helix/streams" 78 | ApiUrlLastStreamsBase = "https://api.twitch.tv/helix/videos?limit=" + ApiVideoLimit -------------------------------------------------------------------------------- /definitions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | 6 | ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -------------------------------------------------------------------------------- /lib/miscLib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | #--------------------------- 5 | # miscLib Module 6 | # 7 | # Contains some helpful miscellaneous functions 8 | #--------------------------- 9 | 10 | import os 11 | import time 12 | from datetime import datetime 13 | 14 | #--------------------------- 15 | # returns the formatted date of current day 16 | #--------------------------- 17 | def GetCurrentDayFormattedDate(): 18 | currenttimestamp = int(time.time()) 19 | return datetime.fromtimestamp(currenttimestamp).strftime('%Y-%m-%d') 20 | -------------------------------------------------------------------------------- /lib/twitchLib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | #--------------------------- 5 | # twitchLib Module 6 | # 7 | # Contains some helpful functions for Twitch API stuff 8 | #--------------------------- 9 | 10 | import os 11 | import json 12 | import config 13 | 14 | #--------------------------- 15 | # returns the response from api request 16 | #--------------------------- 17 | def GetTwitchApiResponse(url, Parent, UserSettings): 18 | headers = { 19 | "Authorization": "Bearer " + GetCurrentAccessToken(), 20 | "Client-Id": UserSettings.AppClientId 21 | } 22 | return Parent.GetRequest(url, headers) 23 | 24 | #--------------------------- 25 | # returns the combined URL for last streams including the user defined ChannelId 26 | #--------------------------- 27 | def GetApiUrlLastStreams(UserSettings): 28 | return config.ApiUrlLastStreamsBase + "&user_id=" + UserSettings.ChannelId 29 | 30 | #--------------------------- 31 | # returns the combined URL for current stream of channel 32 | #--------------------------- 33 | def GetApiUrlCurrentStream(UserSettings): 34 | return config.ApiUrlCurrentStream + "?user_id=" + UserSettings.ChannelId 35 | 36 | #--------------------------- 37 | # helper function to log all variables of last stream object (debugging) 38 | #--------------------------- 39 | def LogAllVariablesOfVideoObject(videoObject, Parent): 40 | for attributes in videoObject: 41 | Parent.Log(Parent.ScriptName, attributes) 42 | 43 | return 44 | 45 | #--------------------------- 46 | # Gets specified attribute value of given stream by list id (offset index to the current stream) 47 | #--------------------------- 48 | def GetAttributeByVideoListId(attribute, listId, Parent, UserSettings): 49 | lastVideosObjectStorage = GetTwitchApiResponse(GetApiUrlLastStreams(UserSettings), Parent, UserSettings) 50 | videoObject = GetVideoOfVideoObjectStorageByListId(lastVideosObjectStorage, listId, Parent) 51 | 52 | return videoObject.get(str(attribute)) 53 | 54 | #--------------------------- 55 | # Gets stream id of current stream for channel 56 | #--------------------------- 57 | def GetCurrentStreamId(Parent, UserSettings): 58 | currentStreamObjectStorage = GetTwitchApiResponse(GetApiUrlCurrentStream(UserSettings), Parent, UserSettings) 59 | currentStreamObject = GetStreamObjectByObjectStorage(currentStreamObjectStorage, Parent) 60 | return int(currentStreamObject[0].get("id")) 61 | 62 | #--------------------------- 63 | # GetVideoOfVideoObjectStorageByListId 64 | # hint: listId 0 = current stream 65 | #--------------------------- 66 | def GetVideoOfVideoObjectStorageByListId(videoObjectStorage, listId, Parent): 67 | listId = int(listId) # let's be safe here 68 | 69 | parsedLastVideo = json.loads(videoObjectStorage) 70 | dataResponse = parsedLastVideo["response"] # str 71 | parsedDataResponse = json.loads(dataResponse) # dict, contents: _total, videos 72 | videosList = parsedDataResponse.get("data") # list 73 | 74 | while (int(videosList[listId].get("id")) == 1 or videosList[listId].get("status") == "recording"): 75 | 76 | if (listId >= int(Parent.ApiVideoLimit)): 77 | Parent.Log(Parent.ScriptName, "Failed to find valid stream object in list of defined last videos of channel") 78 | break 79 | 80 | listId += 1 81 | 82 | return videosList[listId] # dict 83 | 84 | #--------------------------- 85 | # GetStreamObjectByObjectStorage 86 | #--------------------------- 87 | def GetStreamObjectByObjectStorage(streamObjectStorage, Parent): 88 | parsedStreamObjectStorage = json.loads(streamObjectStorage) 89 | dataResponse = parsedStreamObjectStorage["response"] # str 90 | parsedDataResponse = json.loads(dataResponse) # dict 91 | return parsedDataResponse.get("data") 92 | 93 | def GetCurrentStreamObject(Parent, UserSettings): 94 | currentStreamObjectStorage = GetTwitchApiResponse(GetApiUrlCurrentStream(UserSettings), Parent, UserSettings) 95 | return GetStreamObjectByObjectStorage(currentStreamObjectStorage, Parent) 96 | 97 | # 98 | # Access Token Shizzle 99 | # 100 | 101 | def GetCurrentAccessToken(): 102 | f = open(config.TokenFile) 103 | return str(f.readlines()[0]) 104 | 105 | def IsTokenValid(Parent, Token): 106 | headers = {"Authorization": "Bearer " + Token} 107 | result = Parent.GetRequest("https://id.twitch.tv/oauth2/validate", headers) 108 | parsedResult = json.loads(result) 109 | 110 | if (200 == parsedResult["status"]): 111 | Parent.Log(config.ScriptName, "Token still valid") 112 | return True 113 | else: 114 | Parent.Log(config.ScriptName, "Token invalid, getting new one..") 115 | return False 116 | 117 | def GetNewAccessToken(Parent, UserSettings): 118 | result = Parent.PostRequest("https://id.twitch.tv/oauth2/token?client_id=" + UserSettings.AppClientId + "&client_secret=" + UserSettings.AppSecret + "&grant_type=client_credentials&scope=channel:manage:videos channel:manage:broadcast", {}, {}, True) 119 | parsedResult = json.loads(result) 120 | if (200 == parsedResult["status"]): 121 | dataResponse = parsedResult["response"] #str 122 | parsedDataResponse = json.loads(dataResponse) # dict 123 | 124 | return parsedDataResponse["access_token"] 125 | else: 126 | Parent.Log(config.ScriptName, "Got error when trying to get a new access token:" + str(parsedResult)) 127 | 128 | def WriteNewAccessToken(Parent, UserSettings): 129 | data = {"token": GetNewAccessToken(Parent, UserSettings)} 130 | with open(config.TokenFile, 'w') as f: 131 | f.write(GetNewAccessToken(Parent, UserSettings)) 132 | f.close 133 | Parent.Log(config.ScriptName, "Wrote new access token") 134 | return True 135 | 136 | def CheckAndRefreshAccessToken(Parent, UserSettings): 137 | if (True == os.path.isfile(config.TokenFile) and 0 != os.stat(config.TokenFile).st_size): 138 | if (False == IsTokenValid(Parent, GetCurrentAccessToken())): 139 | WriteNewAccessToken(Parent, UserSettings) 140 | else: 141 | Parent.Log(config.ScriptName, "Access token didn't exist yet, generating new one..") 142 | WriteNewAccessToken(Parent, UserSettings) --------------------------------------------------------------------------------