├── .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)
--------------------------------------------------------------------------------