68 Commits 66e3399f4f ... c2039cdb31

Autore SHA1 Messaggio Data
  deadtom c2039cdb31 Increased alert display time to 15 seconds 3 settimane fa
  deadtom 508cbd0419 Now moves kofi related variables from config.py to alerts.py, and adds gb_name dicitonary entry to alerts.py 3 settimane fa
  deadtom 5f438927de Updated version numbers 3 settimane fa
  deadtom 2b6e064ac1 finished givebutter functions changes 3 settimane fa
  deadtom fe08aadfb7 Changed some debug log entries to info 3 settimane fa
  deadtom 4df5c6b09b Removed last of the givebutter todos 3 settimane fa
  deadtom bf463930bd Changed all except Error entries to except Exception 3 settimane fa
  deadtom 6632e7fef2 Added template for GiveButter alert overlays 3 settimane fa
  deadtom 2d4873e342 Added missing period 4 settimane fa
  deadtom 4ae2b94766 Updated screenshots 4 settimane fa
  deadtom 8d6fc11c02 Added attribution for people that helped me test. 4 settimane fa
  deadtom 3e5563eadd Reworded and reorganized to accommodate GiveButter instructions. 4 settimane fa
  deadtom a1e4ddf8ee Removed completed items 4 settimane fa
  deadtom 083fa93324 More formatting 4 settimane fa
  deadtom 3aa563260d Got basic GiveButter functionality working 4 settimane fa
  deadtom 75579cc410 Some minor formatting changes 4 settimane fa
  deadtom b33650ad66 Lots of formatting, finished donations, added a footer 4 settimane fa
  deadtom 86e78954e2 Added footer styling 4 settimane fa
  deadtom 3c280bb008 Fine tuned anonymous donation handling 4 settimane fa
  deadtom f4dfae64da Revert "Updated version numbers" 4 settimane fa
  deadtom 0130c7668c Updated version numbers 4 settimane fa
  deadtom 7235beca9b Removed signature validation, since GB is no longer signing webhook payloads. 4 settimane fa
  DeadTOm 95436e6aa2 Added line stating that the page autorefreshes. Hopefully helpful for people using a screen-reader. 4 settimane fa
  deadtom 643e3dc868 Changed kofi donation_points to int, so the math gets done properly 4 settimane fa
  deadtom 713e909640 Got a proper payload from GiveButter. Modified relevant functions accordingly 4 settimane fa
  deadtom 5664ee4295 Added anonymous donor idea 4 settimane fa
  DeadTOm 7cf0f79ba1 Aaaand added the GB form back in 4 settimane fa
  DeadTOm d92e5dd606 Added additional line in case variable is False instead of empty, and deal with it properly 4 settimane fa
  DeadTOm e877b6d665 Fixed typo 4 settimane fa
  deadtom 44ce49dc4f Updated version number 4 settimane fa
  deadtom d3cd17642f Removed GiveButter form, until it can be properly tested 4 settimane fa
  deadtom 267454a9b4 Set integration to False by default 4 settimane fa
  deadtom 563a5d4612 Fix missing donation_service variable assignment 1 mese fa
  deadtom d664c15697 Removed additional period from the end of the help message 1 mese fa
  deadtom 8fad703374 More kofi/donation rewording, and commented out some testing code 1 mese fa
  deadtom c460898df0 Replaced OCB with OwnchatBot in a few places, for clarity 1 mese fa
  deadtom ed948e1d7e Updated kofi variable names, and added givebutter messages 1 mese fa
  deadtom a93a86ad31 Updated kofi variable names 1 mese fa
  deadtom 75fb3d5677 Set default donations to False 1 mese fa
  deadtom a6f9c4e56e Removed edit button from temp viewer account line, to avoid confusion 1 mese fa
  deadtom 0d452e83ae Added better instructions for merging temp accounts with viewer accounts 1 mese fa
  deadtom 3a2a6d102c Updated gb variable names 1 mese fa
  deadtom fbd3167c41 Fixed False or empty python variables being converted to blank bash variables in kofi.py 1 mese fa
  deadtom 316fd13a18 Fixed missing test to properly fill ko-fi enable checkbox based on kofi.py 1 mese fa
  deadtom a0ed06b562 Fixed default integration value to be False instead of blank. 1 mese fa
  deadtom fcaade6012 Removed some items 1 mese fa
  deadtom 70778483dd Fixed activeTab problem. 1 mese fa
  deadtom 29a8b4f8aa Added a thing 1 mese fa
  deadtom 22ada96272 Moved viewer queue to it's own page, removed auto-refresh from viewer panel 1 mese fa
  deadtom 199c93e8bd More stuff to do 1 mese fa
  deadtom 3345970546 Added logos to the donations tab 1 mese fa
  deadtom 98f12ba08d More GiveButter configuration 1 mese fa
  DeadTOm c52d91137b Made sure that anonymous donations/subscriptions are kept anonymous in the logs. 1 mese fa
  deadtom 5923918b07 Added some stuff. Removed some stuff. 1 mese fa
  deadtom f18b7ac1de Created a donations tab. Moved all kofi related stuff there, and added givebutter. 1 mese fa
  deadtom 221454d19e Created new default config files 1 mese fa
  deadtom 2948a11010 Moves values from config.py to kofi.py. Copies default givebutter.py into instance folder. 1 mese fa
  deadtom 403e54e445 Added an item 1 mese fa
  deadtom b58e6dc0b3 Removed color entry 1 mese fa
  deadtom 12d0876c6e Basic GiveButter integration is functional. Still needs gui settings tab. 1 mese fa
  deadtom 0874d5852f Added default givebutter config 1 mese fa
  deadtom 2949b89f6f tab indexes set up, for proper navigation using the tag key 1 mese fa
  deadtom 2cbebb0254 Colors adjusted to be colorblind friendly 1 mese fa
  deadtom 8ea6e3b0df Restored tabcontent background and text colors 1 mese fa
  deadtom 12b30efbfb Added new notes 1 mese fa
  deadtom 061c4abac6 screen reader compliance 1 mese fa
  deadtom e4f5aadc1c Span tag in the wrong place on to-do list form 1 mese fa
  deadtom 2c147bd557 Removed unnecessary comments 1 mese fa

+ 35 - 9
README.md

@@ -11,7 +11,9 @@ A feature rich chatbot for Owncast.
 - [Upgrading](#upgrading)
 - [Running OwnchatBot](#running-ownchatbot)
 - [Final Configuration](#final-configuration)
+* [Donations](#donations)
 - [Kofi Integration](#kofi-integration)
+- [GiveButter Integration](#givebutter-integration)
 - [Overlays](#overlays)
 - [To-do List](#to-do-list)
 - [Alerts](#alerts)
@@ -40,8 +42,8 @@ A feature rich chatbot for Owncast.
  * Add/edit/remove rewards and reward categories.
  * Viewer management for manually adjusting viewers' points.
  * A queue manager, where the streamer can mark rewards fulfilled as they go, or refund rewards if needed.
-* Kofi integration.
- * Your viewers can get points for donating via Kofi, and as part of their monthly Kofi membership.
+* Kofi and GiveButter integration.
+ * Your viewers can get points for donating via Kofi or GiveButter, and as part of their monthly Kofi membership.
 * To-do list. Add tasks and cross them off as you complete them.
  * Also has an overlay to display the list on your stream.
 
@@ -149,28 +151,48 @@ You need to create a button on your Owncast page, so your viewers can access the
     ```
 5. Configure the remaining fields/options as desired.
 
+### Donations
+Only Owncast authenticated users can get donation benefits. So the first thing viewers need to do is authenticate their username with Owncast.
+
+OwnchatBot associates viewer accounts with Ko-fi and GiveButter accounts using the viewer's email address. So in order for viewers to get donation benefits in-stream, they must enter their email address in OwnchatBot viewer panel -> OwnchatBot Info -> Donations.
+
+If a viewer donates/subscribes before entering their email address, OwnchatBot creates a temporary entry with their email and points award, and then applies it once the viewer enters their email address into the viewer panel. If there is any sort of mix-up here, the streamer can manually connect the viewer's email and account via the management panel by entering the email into the viewers account. OwnchatBot will do the merge automatically. There are more details about this in the Manage Accounts panel
+
+Email addresses are **ONLY** used for donation integration. They are not sent to any other individual or company, will not be used to create or send mailing lists of any kind, or for any other purposes. Ever.
+
+Ko-fi has support for anonymous donations built right in, but GiveButter doesn't yet. So as a work-around, I've put instructions for setting up GiveButter custom fields at the bottom of the Donations management panel. Keep in mind this only makes them anonymous for OwnchatBot. All details are still visible in the GiveButter dashboard.
+Ownchatbot will still send a chat alert for anonymous donations, but no identifying information is included. OwnchatBot also does not log any identifying information related anonymous donations.
+
 ### Kofi Integration
 
 #### Make sure you don't lose your viewers' benefits! It is extremely important that you back up your instance folder daily, if not hourly.
 
-Integration is accomplished via a webhook triggered by Kofi every time a donation or subscription is made. Again, this webhook is your external OwnchatBot server address with `/kofiHook` appended.
+Integration is accomplished via a webhook triggered by Ko-fi every time a donation or subscription is made. Again, this webhook is your external OwnchatBot server address with `/kofiHook` appended.
 
-Paste the webhook address into Kofi -> More -> API -> Webhooks -> Webhook URL.
+Paste the webhook address into Ko-fi -> More -> API -> Webhooks -> Webhook URL.
   ```
   https://<your_external.ownchatbot.url>/kofiHook
   ```
 
 Click "Update", but stay on this page.
 
-You need the verification token from Kofi. Kofi sends this token when it triggers the webhook.
+You need the verification token from Ko-fi. Ko-fi sends this token when it triggers the webhook.
 Below "Webhook URL", click on "Advanced".
-A pre-generated token will already be there. Copy that token, and paste it into the OCB Management Panel -> Settings -> Kofi Integration -> Verification Token. Then click "Save Changes".
+A pre-generated token will already be there. Copy that token, and paste it into the OCB Management Panel -> Donations -> Kofi -> Verification Token. Then click "Save".
 
-OwnchatBot associates viewer accounts with Kofi accounts using the viewer's email address. So in order for viewers to get Kofi benefits in-stream, they must enter their email address in OwnchatBot viewer panel -> OwnchatBot Info -> Kofi. It **must** be the same email address associated with their Kofi account.
+### GiveButter Integration
 
-If a viewer donates/subscribes before entering their email address, OwnchatBot creates a temporary entry with their email and points award, and then applies it once the viewer enters their email address into the viewer panel. If there is any sort of mix-up here, the streamer can manually connect the viewer's email and account via the management panel by entering the email into the viewers account. OwnchatBot will do the merge automatically.
+#### Make sure you don't lose your viewers' benefits! It is extremely important that you back up your instance folder daily, if not hourly.
 
-Email addresses are **ONLY** used for Kofi integration. They are not sent to any other individual or company, will not be used to create or send mailing lists of any kind, or for any other purposes. Ever.
+Integration is accomplished via a webhook triggered by Kofi every time a donation or subscription is made. This webhook is also your external OwnchatBot server address with `/gbHook` appended.
+
+Enter this in the GiveButter dashboard -> Settings -> Developers -> Webhooks -> +New Webhook.
+  ```
+  https://<your_external.ownchatbot.url>/gbHook
+  ```
+You need a signing secret from GiveButter. This is sent when it triggers the webhook.
+To get this, go to the Webhooks page in the GiveButter dashboard, and click on the eye icon (View) next to the webhook you set up for OwnchatBot.
+Copy the signing secret, and paste it into the OCB Management Panel -> Donations -> GiveButter -> Signing Secret. Then click "Save".
 
 ## Overlays
 To display vote and goal progress on-screen, in your stream, create two browser sources in your streaming software.
@@ -291,6 +313,10 @@ At the time of this writing, it is released under a [Creative Commons CC-BY 3](h
 
 The basic layout of the user and management panels was copied from the [tab bar tutorial on w3schools.com](https://www.w3schools.com/howto/howto_js_tabs.asp).
 
+Screen reader readability tested by [The esoteric programmer](https://social.stealthy.club/@esoteric_programmer).
+
+Various testing for installation and GiveButter integration was done with the help of [Ozoned](https://btfree.social/@ozoned).
+
 The overlays screenshot was provided by [Wonderwmn](https://mas.to/@wonderwmn), from one of her co-working streams.
 
 The food that keeps me alive during all-weekend coding sessions is also provided by [Wonderwmn](https://mas.to/@wonderwmn).

+ 0 - 2
TODO.md

@@ -1,3 +1 @@
-* Color blind friendly
-* GiveButter integration
 * Short video tutorials on configuring OwnchatBot.

+ 1 - 1
install.sh

@@ -46,7 +46,7 @@ update_config() {  # Generate key for SECRET_KEY
     local key_value="$2"
     local bak_file="instance/config.py.bak"
 
-    echo "Go to Go to your Owncast Admin panel -> Integrations -> Access Tokens, and create an access token for OwnchatBot"
+    echo "Go to your Owncast Admin panel -> Integrations -> Access Tokens, and create an access token for OwnchatBot"
 
     # Update the config file
     if sed -i.bak "s|$key_name = ''|$key_name = '$key_value'|" "instance/config.py"; then

+ 3 - 1
ownchatbot/__init__.py

@@ -35,6 +35,8 @@ def create_app(test_config=None):
     app.config.from_pyfile('todo.py', silent=True)
     app.config.from_object('ownchatbot.defaults.alerts')
     app.config.from_pyfile('alerts.py', silent=True)
+    app.config.from_object('ownchatbot.defaults.givebutter')
+    app.config.from_pyfile('givebutter.py', silent=True)
     app.config['ASSETS_FOLDER'] = os.path.join(app.instance_path, 'assets')
 
     gunicorn_logger = logging.getLogger('gunicorn.error')
@@ -60,7 +62,7 @@ def create_app(test_config=None):
             send_system_chat(message)
             current_index = (current_index + 1) % len(announcements)
         except Exception as a_error:
-            app.logger.error(f'Couldn\'t make announcement: {a_error.args[0]}')
+            app.logger.error(f'Couldn\'t make announcement: {a_error}')
                 
     def award_job():
         with app.app_context():

+ 14 - 17
ownchatbot/bot_messages.py

@@ -137,35 +137,32 @@ def do_reward(message, user_id):  # Parse the chat command
         send_private_chat(user_id, f'\"{prefix}{reward}\", {username}? No such reward.')
 
 
-def help_message(user_id):
+def help_message(user_id):  # Build help message
     prefix = current_app.config['PREFIX']
     kofi_settings = current_app.config['KOFI_SETTINGS']
-    kofi_integration = current_app.config['KOFI_INTEGRATION']
+    kofi_integration = current_app.config['KOFI_SETTINGS']['integration']
+    gb_settings = current_app.config['GB_SETTINGS']
+    gb_integration = current_app.config['GB_SETTINGS']['integration']
     message = f'You get {current_app.config["POINTS_AWARD"]} points for every {current_app.config["POINTS_INTERVAL"]} minutes you\'re in chat.<br> \
             You can see your points, the rewards queue, and other helpful information by clicking on the \"Points Rewards\" button.<br><br> \
             <b><u>Chat commands:</u></b><br> \
             <b>{prefix}help</b> to see this help message.<br> \
             <b>{prefix}points</b> to see your points.<br> \
             <b>{prefix}rewards</b> to see a list of currently active rewards.'
-    if kofi_integration:
+    if kofi_integration or gb_integration:
         message = f'{message}<br><br>\
-            <b><u>Kofi is enabled!</b></u><br>\
-            Authenticate with Owncast, and enter your email address into the Stream Rewards Info page to get Kofi perks.'
+            <b><u>Donations are enabled!</b></u><br>\
+            Authenticate with Owncast, and register your email address on the OwnchatBot Info page to get donation perks.'
         if kofi_settings['donations']:
-            if kofi_settings["donation_points"] == 1:
-                d_points_label = 'point'
-            else:
-                d_points_label = 'points'
-            if kofi_settings["sub_points"] == 1:
-                s_points_label = 'point'
-            else:
-                s_points_label = 'points'
             message = f'{message}<br>\
-            You\'ll recieve {kofi_settings["donation_points"]} {d_points_label} for every dollar you donate on Kofi'
+            You\'ll recieve {porps(kofi_settings["donation_points"])} for every dollar you donate on Kofi'
         if kofi_settings['subs']:
-            message = f'{message}, and {kofi_settings["sub_points"]} {s_points_label} every month for subscribing to my Kofi page.'
+            message = f'{message}, and {porps(kofi_settings["sub_points"])} every month for subscribing to my Kofi page.'
+        if gb_settings['donations']:
+            message = f'{message}<br>\
+            You\'ll recieve {porps(gb_settings["donation_points"])} for every dollar you donate on GiveButter.'
         else:
-            message = f'{message}.'
+            message = f'{message}'
     send_private_chat(user_id, message)
 
 
@@ -180,7 +177,7 @@ ANNOUNCE_INTERVAL = {announce_dict['ANNOUNCE_INTERVAL']}  # How long, in minutes
             f.write(new_announce)
             f.close()
     except Exception as saerror:
-        current_app.logger.error(f'Couldn\'t save announce.py: {saerror.args[0]}')
+        current_app.logger.error(f'Couldn\'t save announce.py: {saerror}')
         return False
 
     current_app.config.from_pyfile('announce.py', silent=True)

+ 16 - 16
ownchatbot/db.py

@@ -17,7 +17,7 @@ def rem_from_queue(reward_name):  # Remove a reward from the queue
         )
         db.commit()
     except sqlite3.Error as rfqerror:
-        current_app.logger.error(f'Couldn\'t remove {reward_name} from reward queue: {rfqerror.args[0]}')
+        current_app.logger.error(f'Couldn\'t remove {reward_name} from reward queue: {rfqerror}')
         return False
     return True
 
@@ -96,7 +96,7 @@ def clear_reward_queue():  # Completely clear the reward queue
         )
         db.commit()
     except sqlite3.Error as serror:
-        current_app.logger.error(f'Couldn\'t clear reward queue: {serror.args[0]}')
+        current_app.logger.error(f'Couldn\'t clear reward queue: {serror}')
         return False
     return True
 
@@ -110,7 +110,7 @@ def clear_fulfilled_rewards():  # Clears only fulfilled rewards from the queue
         )
         db.commit()
     except sqlite3.Error as serror:
-        current_app.logger.error(f'Couldn\'t clear fulfilled rewards: {serror.args[0]}')
+        current_app.logger.error(f'Couldn\'t clear fulfilled rewards: {serror}')
         return False
     return True
 
@@ -122,7 +122,7 @@ def rem_all_votes():  # USED TO BE "clear_votes" Clear all votes from the databa
         db.execute("DELETE FROM votes")
         db.commit()
     except sqlite3.Error as cverror:
-        current_app.logger.error(f'Couldn\'t clear all votes: {cverror.args[0]}')
+        current_app.logger.error(f'Couldn\'t clear all votes: {cverror}')
         return False
     if put_votes(db):
         return True
@@ -135,7 +135,7 @@ def rem_vote():  # Remove a single vote from the database
         db.execute("DELETE FROM votes WHERE name = ?", ('vote',))
         db.commit()
     except sqlite3.Error as rverror:
-        current_app.logger.error(f'Couldn\'t remove \"{vote}\" from database: {rverror.args[0]}')
+        current_app.logger.error(f'Couldn\'t remove \"{vote}\" from database: {rverror}')
         return False
     if put_votes(db):
         return True
@@ -153,7 +153,7 @@ def is_cool(reward_name):  # Check if a reward is cooling down.
                 )
             current_cds = cursor.fetchall()
         except sqlite3.Error as icerror:
-            current_app.logger.error(f'Couldn\'t get \"{reward_name}\" from database: {icerror.args[0]}')
+            current_app.logger.error(f'Couldn\'t get \"{reward_name}\" from database: {icerror}')
         if current_cds:
             last_time = current_cds[0][0]
             hot_time = current_time - last_time
@@ -169,7 +169,7 @@ def is_cool(reward_name):  # Check if a reward is cooling down.
                     db.commit()
                     return True, 0
                 except sqlite3.Error as scerror:
-                    current_app.logger.error(f'Couldn\'t update \"{reward_name}\"\'s cooldown time in the database: {scerror.args[0]}')
+                    current_app.logger.error(f'Couldn\'t update \"{reward_name}\"\'s cooldown time in the database: {scerror}')
         else:  # If it is not in the database, add it and return True
             try:
                 db.execute(
@@ -179,7 +179,7 @@ def is_cool(reward_name):  # Check if a reward is cooling down.
                 db.commit()
                 return True, 0
             except sqlite3.Error as scerror:
-                current_app.logger.error(f'Couldn\'t add \"{reward_name}\" to database: {scerror.args[0]}')
+                current_app.logger.error(f'Couldn\'t add \"{reward_name}\" to database: {scerror}')
     else:  # If the redeem has no cooldown
         return True, 0
 
@@ -192,7 +192,7 @@ def rem_cool(reward_name):  # Remove a reward from the database
             )
         current_cds = cursor.fetchall()
     except sqlite3.Error as icerror:
-        current_app.logger.error(f'Couldn\'t remove \"{reward_name}\" from database: {icerror.args[0]}')
+        current_app.logger.error(f'Couldn\'t remove \"{reward_name}\" from database: {icerror}')
         return False
     return True
 
@@ -208,7 +208,7 @@ def put_votes(db):  # Reread votes from rewards.py, and sync with database
                 )
                 db.commit()
             except sqlite3.Error as serror:
-                current_app.logger.error(f'Couldn\'t insert \"{vote}\" into database: {serror.args[0]}')
+                current_app.logger.error(f'Couldn\'t insert \"{vote}\" into database: {serror}')
                 return False
     return True
 
@@ -230,7 +230,7 @@ def reread_votes():  # Reread votes from rewards.py, and sync with database
             cursor.execute("DELETE FROM votes WHERE name = ?", (vote,))
         db.commit()
     except sqlite3.Error as serror:
-        current_app.logger.error(f'Couldn\'t clear deleted votes from database: {serror.args[0]}')
+        current_app.logger.error(f'Couldn\'t clear deleted votes from database: {serror}')
         return False
 
     try:  # Add new votes found in rewards.py
@@ -255,7 +255,7 @@ def reread_votes():  # Reread votes from rewards.py, and sync with database
                     )
                 db.commit()
     except sqlite3.Error as serror:
-        current_app.logger.error(f'Couldn\'t insert \"{vote}\" into database: {serror.args[0]}')
+        current_app.logger.error(f'Couldn\'t insert \"{vote}\" into database: {serror}')
         return False
     return True
 
@@ -277,7 +277,7 @@ def reread_goals():  # Reread goals from rewards.py, and sync with database
             cursor.execute("DELETE FROM goals WHERE name = ?", (goal,))
         db.commit()
     except sqlite3.Error as serror:
-        current_app.logger.error(f'Couldn\'t clear removed goals from database: {serror.args[0]}')
+        current_app.logger.error(f'Couldn\'t clear removed goals from database: {serror}')
         return False
 
     try:  # Add new goals found in rewards.py
@@ -300,7 +300,7 @@ def reread_goals():  # Reread goals from rewards.py, and sync with database
                     )
         db.commit()
     except sqlite3.Error as serror:
-        current_app.logger.error(f'Couldn\'t insert \"{reward}\" into database: {serror.args[0]}')
+        current_app.logger.error(f'Couldn\'t insert \"{reward}\" into database: {serror}')
         return False
     return True
 
@@ -321,7 +321,7 @@ def reset_goal(goal):  # Set goal progress back to zero
         db.commit()
         return True
     except sqlite3.Error as rgerror:
-        current_app.logger.error(f'Couldn\'t reset {goal} in the goals table: {rgerror.args[0]}')
+        current_app.logger.error(f'Couldn\'t reset {goal} in the goals table: {rgerror}')
         return False
 
 
@@ -339,7 +339,7 @@ def reset_vote(vote):
                     )
             db.commit()
         except sqlite3.Error as rverror:
-            current_app.logger.error(f'Couldn\'t reset {vote} in the votes table: {rverror.args[0]}')
+            current_app.logger.error(f'Couldn\'t reset {vote} in the votes table: {rverror}')
             return False
     return True
 

+ 1 - 1
ownchatbot/defaults/alerts.py

@@ -1,2 +1,2 @@
-ALERTS = {'m_name': '', 'm_reward': '', 'g_name': '', 'g_reward': '', 'follower': ''}
+ALERTS = {'m_name': '', 'm_reward': '', 'g_name': '', 'g_reward': '', 'follower': '', 'gb_name': ''}
 

+ 0 - 2
ownchatbot/defaults/config.py

@@ -9,5 +9,3 @@ POINTS_INTERVAL = 5  # How long, in minutes, between points awards
 POINTS_AWARD = 5  # How many points awarded each interval?
 SECRET_KEY = ''  # Needed for internal flask stuff. Generated during setup. DO NOT CHANGE
 PREFIX = '!'  # Preceeds commands, so OwnchatBot knows what is a command
-KOFI_TOKEN = ''  # Needed to validate Ko-fi with OCB webhook. Get from Ko-fi Settings -> More -> API -> Webhooks -> Advanced - Verification Token.
-KOFI_INTEGRATION = False  # Integrate OwnchatBot with Ko-fi

+ 7 - 0
ownchatbot/defaults/givebutter.py

@@ -0,0 +1,7 @@
+GB_SETTINGS = {
+     "integration": False,
+     "secret": "",
+     'donations': False,
+     'donation_points': 100,
+     'gb_url': ''
+}

+ 3 - 1
ownchatbot/defaults/kofi.py

@@ -1,5 +1,7 @@
 KOFI_SETTINGS = {
-    "donations": True,  # Reward donations with points
+    "integration": False,  # Enable Ko-fi integration
+    "token": "",  # Needed to validate Ko-fi with OCB webhook. Get from Ko-fi Settings -> More -> API -> Webhooks -> Advanced - Verification Token.
+    "donations": False,  # Reward donations with points
     "donation_points": 100,  # How many points per dollar donated?
     "subs": False,  # Reward subscriptions with points
     "sub_points": 1000,  # How many points per month?

+ 46 - 31
ownchatbot/kofi_handlers.py → ownchatbot/donation_handlers.py

@@ -4,13 +4,15 @@ from ownchatbot.db import get_db
 from ownchatbot.user_handlers import get_id_by_email, award_chat_points, add_email_to_points, get_all_users_with_user_id
 from ownchatbot.owncast_com import send_chat, send_private_chat
 from ownchatbot.bot_messages import porps
+from ownchatbot.reward_handlers import save_alerts
 import json
 import os
 
 
-def accept_donation(donation_info, donation_points):
+def accept_donation(donation_info, donation_points, donation_service):
     try:
         db = get_db()
+        alerts_dict = current_app.config['ALERTS']
         is_public = donation_info[0]
         email = donation_info[2]
         amount = donation_info[3]
@@ -21,29 +23,38 @@ def accept_donation(donation_info, donation_points):
         if not ids:  # If no id found with that email address
             if add_email_to_points(db, email, points):  # Create empty account with email and points
                 name = 'Someone'
+                alerts_dict['gb_name'] = 'New Donation!'
                 current_app.logger.info(f'No user with email \"{email}\" found in database, created empty account.')
         else:  # Grant points to the corresponding id
             for id in ids:
                 if award_chat_points(db, id[0], points):  # Grant points
                     for user in get_all_users_with_user_id(db, id[0]):
                         name = user[1]
+                        alerts_dict['gb_name'] = name
+                        if not name:
+                            name = 'Someone'
+                            alerts_dict['gb_name'] = 'New Donation!'
                     current_app.logger.info(f'Granted user id {id[0]} {porps(points)} for their ${amount} donation.')
         if is_public:
-            message = f'{name} got {porps(points)} for donating ${amount} on Kofi!'
-            current_app.logger.info(f'Public donation of ${amount} received from {name}')
+            message = f'{name} got {porps(points)} for donating ${amount} on {donation_service}!'
+            alerts_dict['gb_name'] = name
+            current_app.logger.info(f'Public donation of ${amount} received from {name} via {donation_service}. Granted {porps(points)}.')
         else:
-            message = None
-            current_app.logger.info(f'Private donation of ${amount} received from {name}')
-        if message is not None:  # Only send chat message if it's a public donation
-            send_chat(message)
+            message = f'We got an anonymous donation of ${amount}!'
+            alerts_dict['gb_name'] = 'New Donation!'
+            current_app.logger.info(f'Anonymous donation of ${amount} received via {donation_service}')
+        if donation_service == 'GiveButter':  # If a GiveButter donation, update alerts.py, to trigger alert
+            save_alerts(alerts_dict)
+        send_chat(message)  # Send message to chat
         return True
     except Exception as aderror:
-        current_app.logger.error(f'General Exception: {aderror}')
+        current_app.logger.error(f'A general exception occurred in accept_donation: {aderror}')
 
 
-def accept_sub(sub_info, sub_points):
+def accept_kofi_sub(sub_info, sub_points):
     try:
         db = get_db()
+        donation_service = "Ko-fi"
         is_public = sub_info[0]
         name = sub_info[1]
         email = sub_info[2]
@@ -57,7 +68,7 @@ def accept_sub(sub_info, sub_points):
         if not ids:  # If no id found with that email address
             if add_email_to_points(db, email, points):  # Create empty account with email and points
                 name = 'Someone'
-                current_app.logger.info(f'No user with email \"{email}\" found in database, created empty account.')
+                current_app.logger.info(f'No user with email \"{email}\" found in database, created empty Owncast account.')
         else:  # Grant points to the corresponding id
             for id in ids:
                 if award_chat_points(db, id[0], points):  # Grant points
@@ -67,32 +78,32 @@ def accept_sub(sub_info, sub_points):
         if is_public:
             if not name:  # If no name in points table
                 name = 'Someone'
-            if first_sub:
-                message = f'{name} got {porps(points)} for their one month membership on Kofi!'
+            if first_sub:  # Kofi-only field
+                message = f'{name} got {porps(points)} for their one month membership on {donation_service}!'
                 current_app.logger.info(f'Public subscription received from {name}')
             else:
-                message = f'{name} got {porps(points)} for renewing their membership on Kofi!'
-                current_app.logger.info(f'Public subscription renewal received from {name}')
+                message = f'{name} got {porps(points)} for renewing their membership on {donation_service}!'
+                current_app.logger.info(f'Anonymous {donation_service} subcription renewal received.')
             send_chat(message)  # Send message publicly if a public membership
         else:
             if not ids:
-                current_app.logger.info(f'No account to associate with.')
+                current_app.logger.info(f'No Owncast account to associate {donation_service} subscription with.')
                 # return True
             if not name:  # If no name in points table
                 name = sub_info[1]  # Assign name from Kofi response
             if first_sub:
-                message = f'Thanks so much for your subscribing to my Kofi! You\'ve been awarded {porps(points)}!'
-                current_app.logger.info(f'Private subscription received from {name}')
+                message = f'Thanks so much for your subscribing to my {donation_service}! You\'ve been awarded {porps(points)}!'
+                current_app.logger.info(f'Anonymous {donation_service} subscription received.')
             else:
-                message = f'Thanks so much for renewing your membership to my Kofi! You\'ve been awarded {porps(points)}!'
-                current_app.logger.info(f'Private subscription renewal received from {name}')
+                message = f'Thanks so much for renewing your membership to my {donation_service}! You\'ve been awarded {porps(points)}!'
+                current_app.logger.info(f'Anonymous {donation_service} subscription renewal received.')
             send_private_chat(id[0], message)
         return True
     except Exception as aderror:
-        current_app.logger.error(f'General Exception: {aderror}')
+        current_app.logger.error(f'A general exception occurred in accept_kofi_sub: {aderror}')
 
 
-def save_kofi_settings(ksettings_info):  # Write rewards to kofi.py
+def save_kofi_settings(ksettings_info):  # Write settings to kofi.py
     settings_file = os.path.join(current_app.instance_path, 'kofi.py')
     try:
         with open(settings_file, 'w') as f:
@@ -100,17 +111,21 @@ def save_kofi_settings(ksettings_info):  # Write rewards to kofi.py
         f.close
         current_app.config.from_pyfile('kofi.py', silent=True)  # Reread kofi.py into the app
     except Exception as sks_error:
-        current_app.logger.error(f'Couldn\'t save kofi.py: {sks_error.args[0]}')
+        current_app.logger.error(f'Couldn\'t save kofi.py: {sks_error}')
         return False
-    
+
     return True
 
 
-def kofi_pngs():  # Create a list of all pngs in the kofi img dir
-    png_dir = 'static/img/kofi'
-    png_dir = os.path.join(current_app.root_path, png_dir)
-    png_files = []
-    for file in os.listdir(png_dir):
-        if file.lower().endswith('.png'):
-            png_files.append(file)
-    return png_files
+def save_gb_settings(gbsettings_info):  # Write settings to givebutter.py
+    settings_file = os.path.join(current_app.instance_path, 'givebutter.py')
+    try:
+        with open(settings_file, 'w') as f:
+            f.write(f'GB_SETTINGS = {gbsettings_info}')
+        f.close
+        current_app.config.from_pyfile('givebutter.py', silent=True)  # Reread givebutter.py into the app
+    except Exception as sgbs_error:
+        current_app.logger.error(f'Couldn\'t save givebutter.py: {sgbs_error}')
+        return False
+
+    return True

+ 7 - 7
ownchatbot/owncast_com.py

@@ -21,7 +21,7 @@ def get_client_id(user_id):
             current_app.logger.info('Not a connected client. Can\'t get client ID.')
             return False
     except requests.exceptions.RequestException as gcierror:
-        current_app.logger.error(f'Couldn\'t get client id from Owncast: {gcierror.args[0]}')
+        current_app.logger.error(f'Couldn\'t get client id from Owncast: {gcierror}')
         return False
 
 
@@ -36,7 +36,7 @@ def live_now():  # Check if stream is live
             else:
                 return False
         except requests.exceptions.RequestException as cserror:
-            current_app.logger.error(f'Couldn\'t check if stream is live: {cserror.args[0]}')
+            current_app.logger.error(f'Couldn\'t check if stream is live: {cserror}')
             return False
     else:
         current_app.logger.error('Owncast url is not set. Can\'t check if stream is live.')
@@ -52,7 +52,7 @@ def award_points(db):  # Award points to users
     try:
         response = requests.get(url, headers=headers)
     except requests.exceptions.RequestException as aperror:
-        current_app.logger.error(f'Couldn\'t get user info: {aperror.args[0]}')
+        current_app.logger.error(f'Couldn\'t get user info: {aperror}')
         return
     if response.status_code != 200:
         current_app.logger.error(f'Couldn\'t award points: {response.status_code}.')
@@ -71,7 +71,7 @@ def send_chat(message):  # Send message to owncast chat
     try:
         response = requests.post(url, headers=headers, json={'body': message})
     except requests.exceptions.RequestException as scerror:
-        current_app.logger.error(f'Couldn\'t send \"{message}\" to Owncast: {scerror.args[0]}')
+        current_app.logger.error(f'Couldn\'t send \"{message}\" to Owncast: {scerror}')
         return
     if response.status_code != 200:
         current_app.logger.error(f'Couldn\'t send \"{message}\" to Owncast: {response.status_code}.')
@@ -90,9 +90,9 @@ def send_private_chat(user_id, message):
         try:
             response = requests.post(url, headers=headers, json={'body': message})
         except requests.exceptions.RequestException as swerror:
-            current_app.logger.error(f'Couldn\'t send \"{message}\" to Owncast: {swerror.args[0]}')
+            current_app.logger.error(f'Couldn\'t send \"{message}\" to Owncast: {swerror}')
         except Exception as swerror:
-            current_app.logger.error(f'Couldn\'t send \"{message}\" to Owncast: {swerror.args[0]}')
+            current_app.logger.error(f'Couldn\'t send \"{message}\" to Owncast: {swerror}')
         if response.status_code != 200:
             current_app.logger.error(f'Couldn\'t send \"{message}\" to Owncast: {response.status_code}.')
     else:
@@ -108,7 +108,7 @@ def send_system_chat(message):
     try:
         response = requests.post(url, headers=headers, json={'body': message})
     except requests.exceptions.RequestException as swerror:
-        current_app.logger.error(f'Couldn\'t send {message} to Owncast: {swerror.args[0]}')
+        current_app.logger.error(f'Couldn\'t send {message} to Owncast: {swerror}')
         sys.exit()
     if response.status_code != 200:
         current_app.logger.error(f'Couldn\'t send {message} to Owncast: {response.status_code}.')

+ 40 - 42
ownchatbot/reward_handlers.py

@@ -24,8 +24,8 @@ def check_vote(db, vote_name, user_id):  # Check if user has already voted on th
             return False
         if row[0] == user_id:
             return True
-    except Error as cverror:
-        current_app.logger.error(f'Couldn\'t check if {user_id} already voted on \"{vote_name}\": {cverror.args[0]}')
+    except Exception as cverror:
+        current_app.logger.error(f'Couldn\'t check if {user_id} already voted on \"{vote_name}\": {cverror}')
 
 
 def add_to_vote(db, vote_name, user_id):  # Add a count to a vote
@@ -45,8 +45,8 @@ def add_to_vote(db, vote_name, user_id):  # Add a count to a vote
             )
             db.commit()
             return True
-    except Error as terror:
-        current_app.logger.error(f'Couldn\'t add to \"{vote_name}\" vote: {terror.args[0]}')
+    except Exception as terror:
+        current_app.logger.error(f'Couldn\'t add to \"{vote_name}\" vote: {terror}')
     return False
 
 
@@ -58,8 +58,8 @@ def add_to_queue(db, user_id, reward_name):  # Add a reward to the queue
             )
         db.commit()
         return True
-    except Error as qerror:
-        current_app.logger.error(f'Couldn\'t add to reward \"{reward_name}\" for {user_id} queue: {qerror.args[0]}')
+    except Exception as qerror:
+        current_app.logger.error(f'Couldn\'t add to reward \"{reward_name}\" for {user_id} queue: {qerror}')
     return False
 
 
@@ -67,7 +67,7 @@ def run_script(reward_name, script_cmd):  # Run a script form a special reward
     try:
         subprocess.check_call(script_cmd, shell=True)
     except Exception as scerror:
-        current_app.logger.error(f'Couldn\'t run script \"{reward_name}\": {scerror.args[0]}')
+        current_app.logger.error(f'Couldn\'t run script \"{reward_name}\": {scerror}')
         return False
     return True
 
@@ -94,8 +94,8 @@ def add_to_goal(db, user_id, reward_name, points_contributed):  # Add a contribu
             )
             db.commit()
             return True
-    except Error as gerror:
-        current_app.logger.error(f'Couldn\'t update goal: {gerror.args[0]}')
+    except Exception as gerror:
+        current_app.logger.error(f'Couldn\'t update goal: {gerror}')
         return False
 
 
@@ -112,8 +112,8 @@ def goal_left(db, reward_name):
             progress, target = row
             left = target - progress
         return left
-    except Error as glerror:
-        current_app.logger.error(f'Couldn\'t check progress for \"{reward_name}\" goal: {glerror.args[0]}')
+    except Exception as glerror:
+        current_app.logger.error(f'Couldn\'t check progress for \"{reward_name}\" goal: {glerror}')
 
 
 def goal_reached(db, reward_name):  # Set a goal as completed
@@ -127,8 +127,8 @@ def goal_reached(db, reward_name):  # Set a goal as completed
             current_app.logger.error(f'{reward_name} not found in goal table.')
         else:
             return row[0]
-    except Error as grerror:
-        current_app.logger.error(f'Couldn\'t check if goal was met: {grerror.args[0]}')
+    except Exception as grerror:
+        current_app.logger.error(f'Couldn\'t check if goal was met: {grerror}')
         return False
 
 
@@ -195,8 +195,8 @@ def was_milestone_reached(db, reward_name):  # Check if a milestone was reached
             else:
                 return False
         return False
-    except Error as wmrerror:
-        current_app.logger.error(f'Couldn\'t check if a milestone was reached: {wmrerror.args[0]}')
+    except Exception as wmrerror:
+        current_app.logger.error(f'Couldn\'t check if a milestone was reached: {wmrerror}')
         return False
 
 
@@ -219,8 +219,8 @@ def was_goal_reached(db, reward_name):  # Check if a goal was reached
                 db.commit()
                 return True
         return False
-    except Error as wgrerror:
-        current_app.logger.error(f'Couldn\'t mark goal met: {wgrerror.args[0]}')
+    except Exception as wgrerror:
+        current_app.logger.error(f'Couldn\'t mark goal met: {wgrerror}')
         return False
 
 
@@ -230,8 +230,8 @@ def all_votes(db):  # Get all the votes
             "SELECT votes.name, votes.count, votes.info FROM votes"
         )
         return cursor.fetchall()
-    except Error as aterror:
-        current_app.logger.error(f'Couldn\'t select all votes: {aterror.args[0]}')
+    except Exception as aterror:
+        current_app.logger.error(f'Couldn\'t select all votes: {aterror}')
 
 
 def refund_reward(db, reward_id):  # Refund a user for a particular reward
@@ -244,8 +244,8 @@ def refund_reward(db, reward_id):  # Refund a user for a particular reward
         db.commit()
         current_app.logger.info(f'Set {reward_id} as refunded in the database')
         return True
-    except Error as rferror:
-        current_app.logger.error(f'Couldn\'t refund reward id {reward_id}: {rferror.args[0]}')
+    except Exception as rferror:
+        current_app.logger.error(f'Couldn\'t refund reward id {reward_id}: {rferror}')
         return False
 
 
@@ -259,8 +259,8 @@ def fulfill_reward(db, reward_id):  # Mark a reward as fulfilled in the database
         db.commit()
         current_app.logger.info(f'{reward_id} marked fulfilled.')
         return True
-    except Error as frerror:
-        current_app.logger.error(f'Couldn\'t  fulfill reward id {reward_id}: {frerror.args[0]}')
+    except Exception as frerror:
+        current_app.logger.error(f'Couldn\'t  fulfill reward id {reward_id}: {frerror}')
         return False
 
 
@@ -283,7 +283,7 @@ def all_goals(db):  # Get all the goals
         )
         return cursor.fetchall()
     except Exception as agerror:
-        current_app.logger.error(f'Couldn\'t select all goals: {agerror.args[0]}')
+        current_app.logger.error(f'Couldn\'t select all goals: {agerror}')
 
 
 def all_active_goals(db):  # Get only active goals
@@ -330,7 +330,7 @@ def save_alerts(alerts_dict):  # Write alerts to alerts.py
         af.close
         current_app.logger.info('Saved new alerts.py.')
     except Exception as saerror:
-        current_app.logger.error(f'Couldn\'t save alerts.py: {saerror.args[0]}')
+        current_app.logger.error(f'Couldn\'t save alerts.py: {saerror}')
         return False
 
     current_app.config.from_pyfile('alerts.py', silent=True)  # Reread alerts.py into the app
@@ -365,7 +365,7 @@ def save_rewards(reward_info):  # Write rewards to rewards.py
             rf.write(f'REWARDS = {new_rewards}')
             rf.close()
     except Exception as srerror:
-        current_app.logger.error(f'Couldn\'t save rewards.py: {srerror.args[0]}')
+        current_app.logger.error(f'Couldn\'t save rewards.py: {srerror}')
         return False
 
     return True
@@ -382,18 +382,16 @@ OWNCAST_URL = '{config_dict['OWNCAST_URL']}'\n\
 # OwnchatBot Configuration \n\
 OCB_URL = '{config_dict['OCB_URL']}'\n\
 SECRET_KEY = '{secret_key}'  # Needed for internal Flask stuff. DO NOT DELETE.\n\
-POINTS_INTERVAL = {config_dict['POINTS_INTERVAL']}  # How long, in seconds, between points awards\n\
-POINTS_AWARD = {config_dict['POINTS_AWARD']}  # How many points awarded each interval?\n\
-PREFIX = '{config_dict['PREFIX']}'  # Preceeds commands, so OwnchatBot knows what is a command\n\
-KOFI_TOKEN = '{config_dict['KOFI_TOKEN']}'  # Needed to validate Ko-fi with OCB webhook. Get from Ko-fi Settings -> More -> API -> Webhooks -> Advanced - Verification Token.\n\
-KOFI_INTEGRATION = {config_dict['KOFI_INTEGRATION']}  # Integrate OwnchatBot with Ko-fi"
+POINTS_INTERVAL = {config_dict['POINTS_INTERVAL']}\n\
+POINTS_AWARD = {config_dict['POINTS_AWARD']}\n\
+PREFIX = '{config_dict['PREFIX']}'"
 
     try:
         with open(settings_file, 'w') as cf:
             cf.write(new_settings)
         cf.close
     except Exception as scerror:
-        current_app.logger.error(f'Couldn\'t save config.py: {scerror.args[0]}')
+        current_app.logger.error(f'Couldn\'t save config.py: {scerror}')
         return False
 
     current_app.config.from_pyfile('config.py', silent=True)  # Reread config.py into the app
@@ -416,8 +414,8 @@ def reread_categories():  # Read _CAT varaibles and write to categories.py
         f.close
 
         current_app.config.from_pyfile('categories.py', silent=True)  # Reread categories into the app
-    except Error as rcerror:
-        current_app.logger.error(f'Couldn\'t reread categories: {rcerror.args[0]}')
+    except Exception as rcerror:
+        current_app.logger.error(f'Couldn\'t reread categories: {rcerror}')
 
 
 def activate_category(category):  # Move an item from the ACTIVE_CAT list to the INACTIVE_CAT list
@@ -428,8 +426,8 @@ def activate_category(category):  # Move an item from the ACTIVE_CAT list to the
         active_categories.append(category)  # Add to ACTIVE_CAT
         inactive_categories.remove(category)  # Remove from INACTIVE_CAT
         reread_categories()
-    except Error as acerror:
-        current_app.logger.error(f'Couldn\'t activate {category}: {acerror.args[0]}')
+    except Exception as acerror:
+        current_app.logger.error(f'Couldn\'t activate {category}: {acerror}')
 
 
 def deactivate_category(category):  # Move an item from the INACTIVE_CAT list to the ACTIVE_CAT list
@@ -440,8 +438,8 @@ def deactivate_category(category):  # Move an item from the INACTIVE_CAT list to
         active_categories.remove(category)  # Remove from ACTIVE_CAT
         inactive_categories.append(category)  # Add to INACTIVE_CAT
         reread_categories()
-    except Error as dcerror:
-        current_app.logger.error(f'Couldn\'t deactivate {category}: {dcerror.args[0]}')
+    except Exception as dcerror:
+        current_app.logger.error(f'Couldn\'t deactivate {category}: {dcerror}')
 
 
 def get_queue(db):  # Get the reward queue and the username
@@ -453,8 +451,8 @@ def get_queue(db):  # Get the reward queue and the username
             on reward_queue.user_id = points.id"""
         )
         return cursor.fetchall()
-    except Error as gqerror:
-        current_app.logger.error(f'Couldn\'t get queue: {gqerror.args[0]}')
+    except Exception as gqerror:
+        current_app.logger.error(f'Couldn\'t get queue: {gqerror}')
 
 
 def is_reward_active(reward_name):  # Check if reward is active
@@ -472,5 +470,5 @@ def is_reward_active(reward_name):  # Check if reward is active
             else:
                 return True
         return None
-    except Error as iraerror:
-        current_app.logger.error(f'Couldn\'t check if {reward_name} is active: {iraerror.args[0]}')
+    except Exception as iraerror:
+        current_app.logger.error(f'Couldn\'t check if {reward_name} is active: {iraerror}')

BIN
ownchatbot/static/img/givebutter.png


+ 15 - 0
ownchatbot/static/style.css

@@ -87,6 +87,14 @@ img {
     background-color: #002e63;
     text-align: left;
 }
+    
+.queuecontent {
+    display: block;
+    padding: 6px 12px;
+    border-top: none;
+    background-color: #002e63;
+    text-align: left;
+}
 
 .button {
     border-radius: 25px;
@@ -157,3 +165,10 @@ a:visited {
     z-index: 1;
 }
 
+footer {
+    text-align: center;
+    align-items: center;
+    padding: 5px;
+    background-color: #2a52be;
+}
+

+ 21 - 21
ownchatbot/templates/add.html

@@ -13,20 +13,20 @@
     <div class="panel">
         <div class="navbar">
             <p></p>
-            <img src="/static/img/ownchatbotwide.png">
+            <img alt="Ownchat Bot logo, whimsical robot" src="/static/img/ownchatbotwide.png">
         </div>
         <h1>Create a {{ reward_type }}</h1>
-        <form method="POST">
+        <form id="Add reward/category" method="POST">
             <table>
                 <tr>
                     <td><label for="name">Name:</label></td>
                     <td>
-                        <input type="text" name="name" required>
+                        <input id="name" type="text" name="name" required>
                         {% with messages = get_flashed_messages(with_categories=true) %}
                             {% if messages %}
                                 {% for category, message in messages %}
                                     {% if category == 'error' %}
-                                        <span style="color: red;">{{ message }}</span>
+                                        <span style="color: orange;">{{ message }}</span>
                                     {% endif %}
                                 {% endfor %}
                             {% endif %}
@@ -38,22 +38,22 @@
                     {% if reward_type == "goal" %}
                         <tr>
                             <td><label for="target">Target:</label></td>
-                            <td><input type="number" name="target" required></td>                    
+                            <td><input id="target" type="number" name="target" required></td>
                         </tr>
                     {% else %}
                         <tr>
                             <td><label for="price">Price:</label></td>
-                            <td><input type="number" name="price" required></td>
+                            <td><input id="price" type="number" name="price" required></td>
                         </tr>
                     {% endif %}
                     <tr>
                         <td><label for="info">Description:</label></td>
-                        <td><textarea name="info" rows="1" cols="50" required></textarea></td>
+                        <td><textarea id="info" name="info" rows="1" cols="50" required></textarea></td>
                     </tr>
                     {% if reward_type == "special" %}
                         <tr>
                             <td><label for="cmd">Command:</label></td>
-                            <td><input type="text" name="cmd" required></td>
+                            <td><input id="cmd" type="text" name="cmd" required></td>
                         </tr>
                     {% endif %}
                     {% if reward_type != "category" %}
@@ -62,7 +62,7 @@
                         {% else %}
                             <tr>
                                 <td><label for="cooldown">Cool down:</label></td>
-                                <td><input type="number" name="cooldown" maxlength="2" size="2" value="0" required> minutes until this reward can be redeemed again, after each use.</td>
+                                <td><input id="cooldown" type="number" name="cooldown" maxlength="2" size="2" value="0" required> minutes until this reward can be redeemed again, after each use.</td>
                             </tr>
                         {% endif %}
                     {% endif %}
@@ -79,13 +79,13 @@
                                             <label for="milestone1_desc">Reward:</label>
                                         </td>
                                         <td>
-                                            <input type="text" name="milestone1_desc" size="30">
+                                            <input id="milestone1_desc" type="text" name="milestone1_desc" size="30">
                                         </td>
                                         <td>
                                             <label for="milestone1_points"> at </label>
                                         </td>
                                         <td>
-                                            <input type="number" name="milestone1_points" maxlength="4" size="4"> points reached.
+                                            <input id="milestone1_points" type="number" name="milestone1_points" maxlength="4" size="4"> points reached.
                                         </td>
                                     </tr>
                                     <tr>
@@ -93,13 +93,13 @@
                                             <label for="milestone2_desc">Reward:</label>
                                         </td>
                                         <td>
-                                            <input type="text" name="milestone2_desc" size="30">
+                                            <input id="milestone2_desc" type="text" name="milestone2_desc" size="30">
                                         </td>
                                         <td>
                                             <label for="milestone2_points"> at </label>
                                         </td>
                                         <td>
-                                            <input type="number" name="milestone2_points" maxlength="4" size="4"> points reached.
+                                            <input id="milestone2_points" type="number" name="milestone2_points" maxlength="4" size="4"> points reached.
                                         </td>
                                     </tr>
                                     <tr>
@@ -107,13 +107,13 @@
                                             <label for="milestone3_desc">Reward:</label>
                                         </td>
                                         <td>
-                                            <input type="text" name="milestone3_desc" size="30">
+                                            <input id="milestone3_desc" type="text" name="milestone3_desc" size="30">
                                         </td>
                                         <td>
                                             <label for="milestone3_points"> at </label>
                                         </td>
                                         <td>
-                                            <input type="number" name="milestone3_points" maxlength="4" size="4"> points reached.
+                                            <input id="milestone3_points" type="number" name="milestone3_points" maxlength="4" size="4"> points reached.
                                         </td>
                                     </tr>
                                 </table>
@@ -123,16 +123,16 @@
                     
                     <tr>              
                         <td>Categories</td>
-                        <td>Categories in <span style="color: red;">red</span> are inactive.
+                        <td>Categories in <span style="color: orange;">orange</span> are inactive.
                             <table>
                                 {% for cat in all_cats %}
                                 <tr>
                                     {% if cat in active_categories %}
                                     <td><label for="category">{{ cat }}:</label></td>
                                     {% else %}
-                                    <td><label for="category"><span style="color: red;">{{ cat }}</span>:</label></td>
+                                    <td><label for="category"><span style="color: orange;">{{ cat }}</span>:</label></td>
                                     {% endif %}
-                                    <td><input type="checkbox" name="category" value="{{ cat }}"></td>
+                                    <td><input id="category" type="checkbox" name="category" value="{{ cat }}"></td>
                                 </tr>
                                 {% endfor %}
                             </table>
@@ -141,11 +141,11 @@
                 {% endif %}  
             </table>
             {% if reward_type == "category" %}
-                <br>By default, new categories are <span style="color: red;">inactive</span>.<br>
+                <br>By default, new categories are <span style="color: orange;">inactive</span>.<br>
             {% else %}
-                <br>Your {{ reward_type }} will be <span style="color: red;">inactive</span>, until you add it to an active category.
+                <br>Your {{ reward_type }} will be <span style="color: orange;">inactive</span>, until you add it to an active category.
             {% endif %}
-            <br><button class="button button2" type="submit">Create {{ reward_type }}</button><br>
+            <br><button id="Add reward/category" class="button button2" type="submit">Create {{ reward_type }}</button><br>
         </form>
 	{% if reward_type == "category"%}
         	<a href="{{ url_for('web_panels.mgmt', activeTab='categories') }}">Cancel</a>

+ 18 - 18
ownchatbot/templates/edit.html

@@ -8,31 +8,31 @@
     <div class="panel">
     <div class="navbar">
         <p>Edit {{ reward_data["type"] }} "{{ reward_name }}"</p>
-        <img src="/static/img/ownchatbotwide.png">
+        <img alt="Ownchat Bot logo, whimsical robot" src="/static/img/ownchatbotwide.png">
     </div>
         <body style="text-align: left;">
-            <form method="POST">
+            <form id="Edit reward/category" method="POST">
                 <table>
                     {% if reward_data["type"] == "goal" %}
                     <tr>
                         <td> <label for="target">Target:</label> </td>
-                        <td> <input type="number" name="target" value="{{ reward_data["target"] }}" required> </td>
+                        <td> <input id="target" type="number" name="target" value="{{ reward_data["target"] }}" required> </td>
                     </tr>
                     {% else %}
                     <tr>
                         <td> <label for="price">Price:</label> </td>
-                        <td> <input type="number" name="price" value="{{ reward_data["price"] }}" required> </td>
+                        <td> <input id="price" type="number" name="price" value="{{ reward_data["price"] }}" required> </td>
                     </tr>
                     {% endif %}
                         <input type="hidden" name="type" value="{{ reward_data["type"] }}"><br>
                     <tr>
                         <td> <label for="info">Info:</label> </td>
-                        <td> <textarea name="info" rows="1" cols="50" required>{{ reward_data["info"] }}</textarea> </td>
+                        <td> <textarea id="info" name="info" rows="1" cols="50" required>{{ reward_data["info"] }}</textarea> </td>
                     </tr>
                     {% if reward_data["type"] == "special" %}
                     <tr>
                         <td> <label for="cmd">Command:</label> </td>
-                        <td> <input type="text" name="cmd" value="{{ reward_data["cmd"] }}" required> </td>
+                        <td> <input id="cmd" type="text" name="cmd" value="{{ reward_data["cmd"] }}" required> </td>
                     </tr>
                     {% endif %}
                     {% if reward_data["type"] != "category"%}
@@ -41,7 +41,7 @@
                         {% else %}
                         <tr>
                             <td> <label for="cooldown">Cool down:</label> </td>
-                            <td> <input type="number" name="cooldown" maxlength="2" size="2" value="{{ reward_data["cooldown"] }}" required> minutes until this reward can be redeemed again, after each use.</td>
+                            <td> <input id="cooldown" type="number" name="cooldown" maxlength="2" size="2" value="{{ reward_data["cooldown"] }}" required> minutes until this reward can be redeemed again, after each use.</td>
                         </tr>
                         {% endif %}
                     {% endif %}
@@ -58,13 +58,13 @@
                                             <label for="milestone1_desc">Reward:</label>
                                         </td>
                                         <td>
-                                            <input type="text" name="milestone1_desc" size="30" value="{{ reward_data["milestones"]["milestone1"][0] }}">
+                                            <input id="milestone1_desc" type="text" name="milestone1_desc" size="30" value="{{ reward_data["milestones"]["milestone1"][0] }}">
                                         </td>
                                         <td>
                                             <label for="milestone1_points"> at </label>
                                         </td>
                                         <td>
-                                            <input type="number" name="milestone1_points" maxlength="4" size="4" value="{{ reward_data["milestones"]["milestone1"][1] }}"> points reached.
+                                            <input id="milestone1_points" type="number" name="milestone1_points" maxlength="4" size="4" value="{{ reward_data["milestones"]["milestone1"][1] }}"> points reached.
                                         </td>
                                     </tr>
                                     <tr>
@@ -72,13 +72,13 @@
                                             <label for="milestone2_desc">Reward:</label>
                                         </td>
                                         <td>
-                                            <input type="text" name="milestone2_desc" size="30" value="{{ reward_data["milestones"]["milestone2"][0] }}">
+                                            <input id="milestone2_desc" type="text" name="milestone2_desc" size="30" value="{{ reward_data["milestones"]["milestone2"][0] }}">
                                         </td>
                                         <td>
                                             <label for="milestone2_points"> at </label>
                                         </td>
                                         <td>
-                                            <input type="number" name="milestone2_points" maxlength="4" size="4" value="{{ reward_data["milestones"]["milestone2"][1] }}"> points reached.
+                                            <input id="milestone2_points" type="number" name="milestone2_points" maxlength="4" size="4" value="{{ reward_data["milestones"]["milestone2"][1] }}"> points reached.
                                         </td>
                                     </tr>
                                     <tr>
@@ -86,13 +86,13 @@
                                             <label for="milestone3_desc">Reward:</label>
                                         </td>
                                         <td>
-                                            <input type="text" name="milestone3_desc" size="30" value="{{ reward_data["milestones"]["milestone3"][0] }}">
+                                            <input ="milestone3_desc" type="text" name="milestone3_desc" size="30" value="{{ reward_data["milestones"]["milestone3"][0] }}">
                                         </td>
                                         <td>
                                             <label for="milestone3_points"> at </label>
                                         </td>
                                         <td>
-                                            <input type="number" name="milestone3_points" maxlength="4" size="4" value="{{ reward_data["milestones"]["milestone3"][1] }}"> points reached.
+                                            <input ="milestone3_points" type="number" name="milestone3_points" maxlength="4" size="4" value="{{ reward_data["milestones"]["milestone3"][1] }}"> points reached.
                                         </td>
                                     </tr>
                                 </table>
@@ -102,19 +102,19 @@
                     
                     <tr>
                         <td>Categories</td>
-                        <td>Categories in <span style="color: red;">red</span> are inactive.
+                        <td>Categories in <span style="color: orange;">orange</span> are inactive.
                             <table>
                                 {% for cat in all_cats %}
                                     <tr>
                                         {% if cat in active_categories %}
                                         <td> <label for="category">{{ cat }}:</label> </td>
                                         {% else %}
-                                        <td> <label for="category"><span style="color: red;">{{ cat }}</span>:</label> </td>
+                                        <td> <label for="category"><span style="color: orange;">{{ cat }}</span>:</label> </td>
                                         {% endif %}
                                         {% if cat in reward_data["categories"] %}
-                                            <td> <input type="checkbox" name="category" value="{{ cat }}" checked> </td>
+                                            <td> <input id="category" type="checkbox" name="category" value="{{ cat }}" checked> </td>
                                         {% else %}
-                                            <td> <input type="checkbox" name="category" value="{{ cat }}"> </td>
+                                            <td> <input id="category" type="checkbox" name="category" value="{{ cat }}"> </td>
                                         {% endif %}
                                     </tr>
                                 {% endfor %}
@@ -122,7 +122,7 @@
                         </td>
                     </tr>
                 </table>
-                <br><button class="button button2" type="submit">Save Changes</button><br>
+                <br><button id="Edit reward/category" class="button button2" type="submit">Save Changes</button><br>
             </form>
 		{% if reward_data["type"] == "category"%}
 			<a href="{{ url_for('web_panels.mgmt', activeTab='categories') }}">Cancel</a>

+ 5 - 5
ownchatbot/templates/edit_account.html

@@ -8,23 +8,23 @@
     <div class="panel">
     <div class="navbar">
         <p>Edit {{ name }}'s Account</p>
-        <img src="/static/img/ownchatbotwide.png">
+        <img alt="Ownchat Bot logo, whimsical robot" src="/static/img/ownchatbotwide.png">
     </div>
         <body>
-            <form method="POST">
+            <form id="edit account" method="POST">
                 <table>
                     <tr style="border-bottom: none;">
                         <td> <label for="points">Points:</label> </td>
-                        <td> <input type="number" name="newpoints" value="{{ points }}"> </td>
+                        <td> <input id="points" type="number" name="newpoints" value="{{ points }}"> </td>
                         
                         <td> <label for="email">Email:</label> </td>
-                        <td> <input type="text" name="newemail" value="{{ email }}"  size="40"> </td>
+                        <td> <input id="email" type="text" name="newemail" value="{{ email }}"  size="40"> </td>
 
                         <input type="hidden" name="user_id" value="{{ user_id }}">
                         <input type="hidden" name="name" value="{{ name }}">
                     </tr>
                 </table>
-                <br><button class="button button2" type="submit">Save</button><br>
+                <br><button id="edit account" class="button button2" type="submit">Save</button><br>
             </form>
             <a href="{{ url_for('web_panels.mgmt', activeTab='accounts') }}">Cancel</a>
         </body>

+ 1 - 3
ownchatbot/templates/follower.html

@@ -1,7 +1,5 @@
 <!DOCTYPE html>
 <html lang="en">
-<!-- Polls the check_followers route every three seconds. When a new follower is found,
-the alert is displayed for ten seconds. -->
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -48,7 +46,7 @@ the alert is displayed for ten seconds. -->
                             alertVid.style.display = 'none';
                             alertImg.style.display = 'none';
                             nameBox.style.display = 'none';
-                        }, 10000); // Visible for 10 seconds
+                        }, 15000); // Visible for 15 seconds
                     }
                 });
         }, 3000); // Check for new followers every 3 seconds

+ 0 - 1
ownchatbot/templates/list.html

@@ -7,7 +7,6 @@
     <style>
         body {
             background-color: transparent;
-            color: #ffd39a;
             width: 400px;
         }
     </style>

+ 218 - 119
ownchatbot/templates/mgmt.html

@@ -9,19 +9,17 @@
     
     <div class="navbar">
         <div class="tab">
-            <button class="tablinks" onclick="window.open('{{ url_for('web_panels.mgmtqueue') }}', '_blank')">Rewards Queue Management</button>
-            <button class="tablinks" data-tab="managerewards" onclick="openTab(event, 'managerewards')">Rewards</button>
-            <button class="tablinks" data-tab="categories" onclick="openTab(event, 'categories')">Categories</button>
-            <button class="tablinks" data-tab="accounts" onclick="openTab(event, 'accounts')">Manage Accounts</button>
-            <button class="tablinks" data-tab="announcements" onclick="openTab(event, 'announcements')">Announcements</button>
-            <button class="tablinks" data-tab="todolist" onclick="openTab(event, 'todolist')">To-Do List</button>
-            <button class="tablinks" data-tab="alerts" onclick="openTab(event, 'alerts')">Alerts</button>
-            <button class="tablinks" data-tab="settings" onclick="openTab(event, 'settings')">Settings</button>
-            {% if kofi_integration %}
-                <button class="tablinks" data-tab="kofi-settings" onclick="openTab(event, 'kofi-settings')">Kofi Settings</button>
-            {% endif %}
+            <button tabindex="0" class="tablinks" onclick="window.open('{{ url_for('web_panels.mgmtqueue') }}', '_blank')">Rewards Queue Management</button>
+            <button tabindex="1" class="tablinks" data-tab="managerewards" onclick="openTab(event, 'managerewards')">Rewards</button>
+            <button tabindex="2" class="tablinks" data-tab="categories" onclick="openTab(event, 'categories')">Categories</button>
+            <button tabindex="3" class="tablinks" data-tab="accounts" onclick="openTab(event, 'accounts')">Manage Accounts</button>
+            <button tabindex="4" class="tablinks" data-tab="announcements" onclick="openTab(event, 'announcements')">Announcements</button>
+            <button tabindex="5" class="tablinks" data-tab="todolist" onclick="openTab(event, 'todolist')">To-Do List</button>
+            <button tabindex="6" class="tablinks" data-tab="alerts" onclick="openTab(event, 'alerts')">Alerts</button>
+            <button tabindex="7" class="tablinks" data-tab="donations" onclick="openTab(event, 'donations')">Donations</button>
+            <button tabindex="8" class="tablinks" data-tab="settings" onclick="openTab(event, 'settings')">Settings</button>
         </div>
-        <img src="/static/img/ownchatbotwide.png">
+        <img alt="Ownchat Bot logo, whimsical robot" src="/static/img/ownchatbotwide.png">
     </div>
 
     <div id='managerewards' class="tabcontent">
@@ -62,22 +60,22 @@
                 </tr>
                 <tr>
                     <td>
-                        <a href="{{ url_for('web_panels.add', reward_type='redeem') }}"><button class="button button2" onclick="openTab(event, 'managerewards')">Create a Redeem</button></a><br>
+                        <a href="{{ url_for('web_panels.add', reward_type='redeem') }}"><button tabindex="8" class="button button2" onclick="openTab(event, 'managerewards')">Create a Redeem</button></a><br>
                     </td>
                     <td>
-                        <a href="{{ url_for('web_panels.add', reward_type='special') }}"><button class="button button2" onclick="openTab(event, 'managerewards')">Create a Special</button></a><br>
+                        <a href="{{ url_for('web_panels.add', reward_type='special') }}"><button tabindex="9" class="button button2" onclick="openTab(event, 'managerewards')">Create a Special</button></a><br>
                     </td>
                     <td>
-                        <a href="{{ url_for('web_panels.add', reward_type='vote') }}"><button class="button button2" onclick="openTab(event, 'managerewards')">Create a Vote</button></a><br>
+                        <a href="{{ url_for('web_panels.add', reward_type='vote') }}"><button tabindex="10" class="button button2" onclick="openTab(event, 'managerewards')">Create a Vote</button></a><br>
                     </td>
                     <td>
-                        <a href="{{ url_for('web_panels.add', reward_type='goal') }}"><button class="button button2" onclick="openTab(event, 'managerewards')">Create a Goal</button></a><br>
+                        <a href="{{ url_for('web_panels.add', reward_type='goal') }}"><button tabindex="11" class="button button2" onclick="openTab(event, 'managerewards')">Create a Goal</button></a><br>
                     </td>
                 </tr>
             </table>
             <br>
             {% if rewards %}
-            Rewards in <span style="color: red !important;">red</span> are inactive. To activate a reward, add it to an active category.
+            Rewards in <span style="color: orange !important;">orange</span> are inactive. To activate a reward, add it to an active category.
             <table>
                 <thead>
                     <tr>
@@ -95,7 +93,7 @@
                     {% if reward in active_rewards %}
                     <tr>
                     {% else %}
-                    <tr style="color: red;">
+                    <tr style="color: orange;">
                     {% endif %}
                         <td>{{ prefix }}{{ reward }}</td>
                         <td>{{ reward_info["price"] }}</td>
@@ -108,8 +106,8 @@
                         {% endif %}
                         <td>{{ reward_info["categories"] | join(', ') }}</td>
                         <td>
-                            <a href="{{ url_for('web_panels.edit', reward_name=reward) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: green;">Edit</span></button></a>&nbsp
-                            <a href="{{ url_for('web_panels.delete', reward_name=reward) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: red;">Delete</span></button></a>
+                            <a href="{{ url_for('web_panels.edit', reward_name=reward) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: yellow;">Edit</span></button></a>&nbsp
+                            <a href="{{ url_for('web_panels.delete', reward_name=reward) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: orange;">Delete</span></button></a></td>
                     {% endif %}
                     </tr>
                 {% endfor %}
@@ -133,16 +131,16 @@
                     {% if reward in active_rewards %}
                     <tr>
                     {% else %}
-                    <tr style="color: red;">
+                    <tr style="color: orange;">
                     {% endif %}
                         <td>{{ prefix }}{{ reward }}</td>
                         <td>{{ reward_info["target"] }}</td>
                         <td>{{ reward_info["info"] }}</td>
                         <td>{{ reward_info["categories"] | join(', ') }}</td>
                         <td>
-                            <a href="{{ url_for('web_panels.edit', reward_name=reward) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: green;">Edit</span></button></a>&nbsp
-                            <a href="{{ url_for('web_panels.delete', reward_name=reward) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: red;">Delete</span></button></a>&nbsp
-                            <a href="{{ url_for('web_panels.reset', reward_name=reward, reward_type=reward_info["type"]) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: orange;">Reset</span></button></a>
+                            <a href="{{ url_for('web_panels.edit', reward_name=reward) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: orange;">Edit</span></button></a>&nbsp
+                            <a href="{{ url_for('web_panels.delete', reward_name=reward) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: yellow;">Delete</span></button></a>&nbsp
+                            <a href="{{ url_for('web_panels.reset', reward_name=reward, reward_type=reward_info["type"]) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: #295e11;">Reset</span></button></a>
                     {% endif %}
                     </tr>
                 {% endfor %}
@@ -166,16 +164,16 @@
                     {% if reward in active_rewards %}
                     <tr>
                     {% else %}
-                    <tr style="color: red;">
+                    <tr style="color: orange;">
                     {% endif %}
                         <td>{{ prefix }}{{ reward }}</td>
                         <td>{{ reward_info["price"] }}</td>
                         <td>{{ reward_info["info"] }}</td>
                         <td>{{ reward_info["categories"] | join(', ') }}</td>
                         <td>
-                            <a href="{{ url_for('web_panels.edit', reward_name=reward) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: green;">Edit</span></button></a>&nbsp
-                            <a href="{{ url_for('web_panels.delete', reward_name=reward) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: red;">Delete</span></button></a>&nbsp
-                            <a href="{{ url_for('web_panels.reset', reward_name=reward, reward_type=reward_info["type"]) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: orange;">Reset</span></button></a>
+                            <a href="{{ url_for('web_panels.edit', reward_name=reward) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: orange;">Edit</span></button></a>&nbsp
+                            <a href="{{ url_for('web_panels.delete', reward_name=reward) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: orange;">Delete</span></button></a>&nbsp
+                            <a href="{{ url_for('web_panels.reset', reward_name=reward, reward_type=reward_info["type"]) }}"><button class="button button2" onclick="openTab(event, 'managerewards')"><span style="color: #295e11;">Reset</span></button></a>
                     {% endif %}
                     </tr>
                 {% endfor %}
@@ -189,7 +187,17 @@
         <body>
             <h3>Manage Viewer Accounts</h3>
             {% if users %}
-            To merge an account with a temporary account: Copy the email address from the temporary account, click "edit" next to the account you want to merge, paste the email address into that account, click "save".<br><br>
+            <i>You may need to refresh this page to get updated account information.</i>
+            <br><br>
+            If a viewer makes a donation before registering their email address with OwnchatBot,<br>
+            a temporary account is created. Temporary accounts are in <span style="color: orange;">orange</span>.<br>
+            To merge a viewer account with a temporary account:
+            <ol>
+                <li>Copy the email address from the temporary account.</li>
+                <li>Click "edit" next to the viewer account you want to merge.</li>
+                <li>Paste the email address into that account.</li>
+                <li>Click "save".</li><br>
+            </ol>
             <table>
                 <thead>
                     <tr>
@@ -204,7 +212,7 @@
                 {% for user in users %}
                     <tr>
                         {% if not user[1] %}
-                            <td style="color: green;"> Temporary Account </td>
+                            <td style="color: orange;"> Temporary Account </td>
                         {% else %}
                             <td> {{ user[1] }} </td>
                         {% endif %}
@@ -213,7 +221,7 @@
                         <td>{{ user[2] }} {{ points_label }}</td>
                         {% if user[4] %}
                             {% if not user[1] %}
-                                <td style="color: green;">{{ user[4] }}</td>
+                                <td style="color: orange;">{{ user[4] }}</td>
                             {% else %}
                                 <td>{{ user[4] }}</td>
                             {% endif %}
@@ -225,7 +233,10 @@
                         {% else %}
                             <td>No</td>
                         {% endif %}
-                        <td> <a href="/mgmt/edit_account/{{ user[0] }}?name={{ user[1] }}&points={{ user[2] }}&email={{ user[4] }}"><button class="button button2" onclick="openTab(event, 'panel')">Edit</button></a> </td>
+                        <td> 
+                        {% if user[1] %}
+                        <a href="/mgmt/edit_account/{{ user[0] }}?name={{ user[1] }}&points={{ user[2] }}&email={{ user[4] }}"><button class="button button2" onclick="openTab(event, 'panel')">Edit</button></a> </td>
+                        {% endif %}
                     </tr>
                 {% endfor %}
                 </tbody>
@@ -250,12 +261,12 @@
                     <tr>
                         <td>
                             {% for cat in active_categories %}
-                            &nbsp;&nbsp;{{ cat }} - <a href="/mgmt/deactivate/{{ cat }}"><span style="color: green;">Deactivate</span></a>&nbsp;<a href="/mgmt/delcat/{{ cat }}/active"><span style="color: red;">Delete</span></a><br>
+                            &nbsp;&nbsp;{{ cat }} - <a href="/mgmt/deactivate/{{ cat }}"><span style="color: orange;">Deactivate</span></a>&nbsp;<a href="/mgmt/delcat/{{ cat }}/active"><span style="color: #295e11;">Delete</span></a><br>
                             {% endfor %}
                         </td>
                         <td>
                             {% for cat in inactive_categories %}
-                            &nbsp;&nbsp;{{ cat }} - <a href="/mgmt/activate/{{ cat }}"><span style="color: green;">Activate</span></a>&nbsp;<a href="/mgmt/delcat/{{ cat }}/inactive"><span style="color: red;">Delete</span></a><br>
+                            &nbsp;&nbsp;{{ cat }} - <a href="/mgmt/activate/{{ cat }}"><span style="color: orange;">Activate</span></a>&nbsp;<a href="/mgmt/delcat/{{ cat }}/inactive"><span style="color: #295e11;">Delete</span></a><br>
                             {% endfor %}
                         </td>
                     </tr>
@@ -267,7 +278,7 @@
 
     <div id='announcements' class="tabcontent">
         <body>
-            <form method="POST" action="/mgmt/announcements">
+            <form id="announcements form" method="POST" action="/mgmt/announcements">
                 <table>
                 <h3>Manage Announcements</h3>
                 <table>
@@ -280,27 +291,27 @@
                     </thead>
                     <tr>
                         <td> <label for="announce_enable">Enable:</label> </td>
-                        {% if settings_info[8] %}
-                        <td> <input type="checkbox" name="announce_enable" value="{{ settings_info[8] }}" checked> </td>
+                        {% if settings_info[6] %}
+                        <td> <input id="announce_enable" type="checkbox" name="announce_enable" value="{{ settings_info[6] }}" checked> </td>
                         {% else %}
-                        <td> <input type="checkbox" name="announce_enable" value="{{ settings_info[8] }}"> </td>
+                        <td> <input id="announce_enable" type="checkbox" name="announce_enable" value="{{ settings_info[6] }}"> </td>
                         {% endif %}
                         <td>Enable periodic announcements</td>
                     </tr>
                     <tr>
                         <td> <label for="announce_interval">Interval:</label> </td>
-                        <td> <input type="number" name="announce_interval" value="{{ settings_info[9] }}" size="2" required> minutes</td>
+                        <td> <input id="announce_interval" type="number" name="announce_interval" value="{{ settings_info[7] }}" size="2" required> minutes</td>
                         <td>How long between each announcement?</td>
                     </tr>
                     <tr>
                         <td> <label for="announcements">Announcements:</label> </td>
                         <td>
-                            <textarea name="announcements" rows="5" cols="50">{{ announcements | join('\n') }}</textarea>
+                            <textarea id="announcements" name="announcements" rows="5" cols="50">{{ announcements | join('\n') }}</textarea>
                         </td>
                         <td>Enter your announcements, one per line. May contain html <a href=https://www.w3schools.com/tags/tag_img.asp>IMG tags</a> to display images.</td>
                     </tr>
                 </table>
-                <br><button class="button button2" type="submit">Save Changes</button><br>
+                <br><button id="announcements form" class="button button2" type="submit">Save Changes</button><br>
             </form>
             <br><br>
         </body>
@@ -308,7 +319,7 @@
 
     <div id='settings' class="tabcontent">
         <body style="text-align: left;">
-            <form method="POST" action="/mgmt/settings">
+            <form id="settings form" method="POST" action="/mgmt/settings">
                 <table>
                 <h3>OwnchatBot Settings</h3>
                     <thead>
@@ -320,25 +331,24 @@
                     </thead>
                     <tr>
                         <td> <label for="points_interval">Points Interval:</label> </td>
-                        <td> <input type="number" name="points_interval" value="{{ settings_info[1] }}" size="5" required> minutes</td>
+                        <td> <input id="points_interval" type="number" name="points_interval" value="{{ settings_info[1] }}" size="5" required> minutes</td>
                         <td>How often do you want to award your viewers points?</td>
                     </tr>
                     <tr>
                         <td> <label for="points_award">Points Award:</label> </td>
-                        <td> <input type="number" name="points_award" value="{{ settings_info[2] }}" size="5"  required> points</td>
+                        <td> <input id="points_award" type="number" name="points_award" value="{{ settings_info[2] }}" size="5" required> points</td>
                         <td>How many points do you want to award them?</td>
                     </tr>
                     <tr>
                         <td> <label for="prefix">Chat Command Prefix:</label> </td>
-                        <td> <input type="text" name="prefix" maxlength="1" size="1" value="{{ settings_info[3] }}" required> </td>
+                        <td> <input id="prefix" type="text" name="prefix" maxlength="1" size="1" value="{{ settings_info[3] }}" required> </td>
                         <td>Character that preceeds chat commands, so OwnchatBot knows what to look for.<br>Example: "{{ settings_info[3] }}points"</td>
                     </tr>
                     <tr>
-                        <td> <label for=ocb_url>External URL:</label> </td>
-                        <td> <input type="text" name="ocb_url" size="40" value="{{ settings_info[10] }}" required> </td>
+                        <td> <label for="ocb_url">External URL:</label> </td>
+                        <td> <input id="ocb_url" type="text" name="ocb_url" size="40" value="{{ settings_info[8] }}" required> </td>
                         <td>What is the external URL of your OwnchatBot?</td>
                     </tr>
-                        
                 </table>
                 
                 <h3>Owncast Integration</h3>
@@ -352,58 +362,35 @@
                     </thead>
                     <tr>
                         <td> <label for="access_id">Access Token Name:</label> </td>
-                        <td style="padding: 5px;"> <input type="text" name="access_id" value="{{ settings_info[0] }}" size="40"> </td>
+                        <td style="padding: 5px;"> <input id="access_id" type="text" name="access_id" value="{{ settings_info[0] }}" size="40"> </td>
                         <td>Create in Owncast Admin panel. Integrations -> Access Tokens (check all three boxes)</td>
                     </tr>
                     <tr>
                         <td> <label for="access_token">Access Token:</label> </td>
-                        <td style="padding: 5px;"> <input type="password" name="access_token" value="{{ settings_info[4] }}" size="40"> </td>
+                        <td style="padding: 5px;"> <input id="access_tokens" type="password" name="access_token" value="{{ settings_info[4] }}" size="40"> </td>
                         <td>The token you created above.</td>
                     </tr>
                     <tr>
                         <td> <label for="owncast_url">Your Owncast URL:</label> </td>
-                        <td> <input type="text" name="owncast_url" value="{{ settings_info[5] }}" size="40"> </td>
+                        <td> <input type="text" id="owncast_url" name="owncast_url" value="{{ settings_info[5] }}" size="40"> </td>
                         <td>The external URL of your Owncast instance, with "http://" or "https://".</td>
                     </tr>
                 </table>
                 
-                <h3>Kofi Integration</h3>
-                <table>
-                    <thead>
-                        <tr style="border-bottom: none;">
-                            <th style="width: 20%;"></th>
-                            <th></th>
-                            <th style="width: 50%;"></th>
-                        </tr>
-                    </thead>
-                    <tr>
-                        <td> <label for="kofi_integration">Enable:</label> </td>
-                        {% if settings_info[7] %}
-                        <td> <input type="checkbox" name="kofi_integration" value="{{ settings_info[7] }}" checked> </td>
-                        <td>Enable Ko-fi integration.</td>
-                        {% else %}
-                        <td> <input type="checkbox" name="kofi_integration" value="{{ settings_info[7] }}"> </td>
-                        <td>Enable Ko-fi integration. ("Kofi Settings" button will appear in the navigation bar when enabled.)</td>
-                        {% endif %}
-                    </tr>
-                    <tr>
-                        <td> <label for="kofi_token">Verification Token:</label> </td>
-                        <td style="padding: 5px;"> <input type="password" name="kofi_token" value="{{ settings_info[6] }}" size="40"> </td>
-                        <td>Get from Kofi -> More -> API -> Webhooks -> Advanced -> Verification Token.</td>
-                    </tr>
-                </table>
-                
-                <br><button class="button button2" type="submit">Save Changes</button><br>
+                <br><button id="settings form" class="button button2" type="submit">Save Changes</button><br>
             </form>
             <br><br>
         </body>
     </div>
 
-    <div id='kofi-settings' class="tabcontent">
+    <div id="donations" class="tabcontent">
         <body style="text-align: left;">
-            <form method="POST" action="/mgmt/ksettings">
+        <h2>Donations</h2>
+        OwnchatBot can integrate with Ko-fi and GiveButter, awarding viewers points for donations and monthly subscriptions.<br>
+        For anonymous donations, no identifying information is logged or sent in chat announcements.
+            <form id='kofi-settings_form' method="POST" action="/mgmt/ksettings">
                 <table>
-                <h3>Kofi Settings</h3>
+                <h3><img style="height: 25px; width: auto; padding-right: 8px; !important" alt="Coffee Cup Logo" src="/static/img/kofi_symbol.png"> <u>Ko-fi</u></h3>
                 
                 <h4> Donations </h4>
                     <thead>
@@ -414,47 +401,138 @@
                         </tr>
                     </thead>
                     <tr>
-                        <td> <label for="enable_donations">Enable points for donations:</label> </td>
+                        <td> <label for="k_integration">Enable:</label> </td>
+                        {% if kofi_settings['integration'] %}
+                        <td> <input id="k_integration" type="checkbox" name="k_integration" value="{{ kofi_settings['integration'] }}" checked> </td>
+                        {% else %}
+                        <td> <input id="k_integration" type="checkbox" name="k_integration" value="{{ kofi_settings['integration'] }}"> </td>
+                        {% endif %}
+                        <td>Enable Ko-fi integration.</td>
+                    </tr>
+                    <tr>
+                        <td> Your Ko-fi webhook: </td>
+                        <td>{{ settings_info[8] }}/kofiHook</td>
+                        <td>Enter this in Ko-fi -> More -> API -> Webhooks.</td>
+                    </tr>
+                    <tr>
+                        <td> <label for="k_token">Verification Token:</label> </td>
+                        <td style="padding: 5px;"> <input id="k_token" type="password" name="k_token" value="{{ kofi_settings['token'] }}" size="40"> </td>
+                        <td>Get from Ko-fi -> More -> API -> Webhooks -> Advanced -> Verification Token.</td>
+                    </tr>
+                    <tr>
+                        <td> <label for="k_enable_donations">Enable points for donations:</label> </td>
                         {% if kofi_settings['donations'] %}
-                        <td> <input type="checkbox" name="enable_donations" value="{{ kofi_settings['donations'] }}" checked> </td>
+                        <td> <input id="k_enable_donations" type="checkbox" name="k_enable_donations" value="{{ kofi_settings['donations'] }}" checked> </td>
                         {% else %}
-                        <td> <input type="checkbox" name="enable_donations" value="{{ kofi_settings['donations'] }}"> </td>
+                        <td> <input id="k_enable_donations" type="checkbox" name="k_enable_donations" value="{{ kofi_settings['donations'] }}"> </td>
                         {% endif %}
                         <td>Enable awarding points for donations</td>
                     </tr>
                     <tr>
-                        <td> <label for="set_donation_points">Points per dollar:</label> </td>
-                        <td> <input type="number" name="set_donation_points" value="{{ kofi_settings['donation_points'] }}" size="5" required> points</td>
+                        <td> <label for="k_donation_points">Points per dollar:</label> </td>
+                        <td> <input id="k_donation_points" type="number" name="k_donation_points" value="{{ kofi_settings['donation_points'] }}" size="5" required> points</td>
                         <td>How many points should viewers recieve, for every dollar they donate?</td>
                     </tr>
                     <tr>
-                        <td> <label for="kofi_url">Kofi Page:</label> </td>
-                        <td style="padding: 5px;"> <input type="text" name="kofi_url" value="{{ kofi_settings['kofi_url'] }}" size="30"> </td>
-                        <td>What is your Kofi page URL?</td>
+                        <td> <label for="kofi_url">Ko-fi URL:</label> </td>
+                        <td style="padding: 5px;"> <input id="kofi_url" type="text" name="kofi_url" value="{{ kofi_settings['kofi_url'] }}" size="30"> </td>
+                        <td>What is your Ko-fi URL?</td>
                     </tr>
                     <tr>
                         <table>
                         <h4> Subscriptions </h4>
                             <tbody>
                                 <tr>
-                                    <td> <label for="enable_subs">Enable points for subscriptions:</label> </td>
+                                    <td> <label for="k_enable_subs">Enable points for subscriptions:</label> </td>
                                     {% if kofi_settings['subs'] %}
-                                    <td> <input type="checkbox" name="enable_subs" value="{{ kofi_settings['subs'] }}" checked> </td>
+                                    <td> <input id="k_enable_subs" type="checkbox" name="k_enable_subs" value="{{ kofi_settings['subs'] }}" checked> </td>
                                     {% else %}
-                                    <td> <input type="checkbox" name="enable_subs" value="{{ kofi_settings['subs'] }}"> </td>
+                                    <td> <input id="k_enable_subs" type="checkbox" name="k_enable_subs" value="{{ kofi_settings['subs'] }}"> </td>
                                     {% endif %}
                                     <td>Enable awarding points for monthly subscriptions</td>
                                 </tr>
                                 <tr>
-                                    <td> <label for="sub_points">Points per month:</label> </td>
-                                    <td> <input type="number" name="sub_points" value="{{ kofi_settings['sub_points'] }}" size="6" required> points</td>
+                                    <td> <label for="k_sub_points">Points per month:</label> </td>
+                                    <td> <input id="k_sub_points" type="number" name="k_sub_points" value="{{ kofi_settings['sub_points'] }}" size="6" required> points</td>
                                     <td>How many points should subscribers recieve every month?</td>
                                 </tr>
                                         </tbody>
-                                    </table>
-                                </tr>
+                            </table>
+                        </tr>
+                </table>
+                <br><button id="kofi-settings_form" class="button button2" type="submit">Save</button><br>
+            </form>
+            <br><br>
+            <form id='gb-settings_form' method="POST" action="/mgmt/gbsettings">
+                <table>
+                <h3><img style="height: 25px; width: auto; padding-right: 8px; !important" alt="Give Butter Logo" src="/static/img/givebutter.png"> <u>GiveButter</u></h3>
+                <i>Scroll to the bottom to see instructions for enabling anonymous donations.</i>
+                <h4> Donations </h4>
+                    <thead>
+                        <tr style="border-bottom: none;">
+                            <th style="width: 20%;"></th>
+                            <th></th>
+                            <th style="width: 50%;"></th>
+                        </tr>
+                    </thead>
+                    <tr>
+                        <td> <label for="gb_integration">Enable:</label> </td>
+                        {% if gb_settings['integration'] %}
+                        <td> <input id="gb_integration" type="checkbox" name="gb_integration" value="{{ gb_settings['integration'] }}" checked> </td>
+                        <td>Enable GiveButter integration.</td>
+                        {% else %}
+                        <td> <input id="gb_integration" type="checkbox" name="gb_integration" value="{{ gb_settings['integration'] }}"> </td>
+                        <td>Enable GiveButter integration.</td>
+                        {% endif %}
+                    </tr>
+                    <tr>
+                        <td> Your GiveButter webhook: </td>
+                        <td>{{ settings_info[8] }}/gbHook</td>
+                        <td>Enter this in GiveButter -> Settings -> Developers -> Webhooks -> +New Webhook.</td>
+                    </tr>
+                    <tr>
+                        <td> <label for="gb_secret">Signing Secret:</label> </td>
+                        <td style="padding: 5px;"> <input id="gb_secret" type="password" name="gb_secret" value="{{ gb_settings['secret'] }}" size="40"> </td>
+                        <td>In the GiveButter webhooks window, click on the eye icon (View) next to the webhook you set up for OwnchatBot.</td>
+                    </tr>
+                    <tr>
+                        <td> <label for="gb_enable_donations">Enable points for donations:</label> </td>
+                        {% if gb_settings['donations'] %}
+                        <td> <input id="gb_enable_donations" type="checkbox" name="gb_enable_donations" value="{{ gb_settings['donations'] }}" checked> </td>
+                        {% else %}
+                        <td> <input id="gb_enable_donations" type="checkbox" name="gb_enable_donations" value="{{ gb_settings['donations'] }}"> </td>
+                        {% endif %}
+                        <td>Enable awarding points for donations</td>
+                    </tr>
+                    <tr>
+                        <td> <label for="gb_donation_points">Points per dollar:</label> </td>
+                        <td> <input id="gb_donation_points" type="number" name="gb_donation_points" value="{{ gb_settings['donation_points'] }}" size="5" required> points</td>
+                        <td>How many points should viewers recieve, for every dollar they donate?</td>
+                    </tr>
+                    <tr>
+                        <td> <label for="gb_url">GiveButter URL:</label> </td>
+                        <td style="padding: 5px;"> <input id="gb_url" type="text" name="gb_url" value="{{ gb_settings['gb_url'] }}" size="30"> </td>
+                        <td>What is your GiveButter URL?</td>
+                    </tr>
+                    <tr>
+                        <td colspan="3">Monthy subscriptions are not yet supported for GiveButter. Coming soon.</td>
+                    </tr>
                 </table>
-                <br><button class="button button2" type="submit">Save Changes</button><br>
+                <br><button id="gb-settings_form" class="button button2" type="submit">Save</button><br><br>
+                GiveButter currently has no built-in function for anonymous donations. However, there is a work-around:<br>
+                In your GiveButter dashboard, go to Campaigns. Click the three dots next to your campaign -> Edit -> Tools -> Custom Fields -> New Custom Field -> Radio.<br>Enter the following information:<br>
+                <ul>
+                    <li>Title: Anonymous</li>
+                    <li>Description: Whatever you like</li>
+                    <li>Required?: Make required</li>
+                    <li>Answers:</li>
+                        <ul>
+                            <li>Yes</li>
+                            <li>No</li>
+                        </ul>
+                    <li>Click "Save"</li>
+                </ul>
+                <span style="color: orange;"><i>"Title" and "Answers" must be exacty as you see here, or OwnchatBot may not pick them up correctly.<br>If these are not set, or are set incorrectly, OwnchatBot will default to anonymous donation.</i></span>
             </form>
             <br><br>
         </body>
@@ -469,7 +547,7 @@
         <body onload="focusInput()">
         <h1>To-Do List</h1>
         <form id="todo-item-form" method="POST" onsubmit="focusInput()" action="/mgmt/addtodoitem">
-            <input type="text" id="itemInput" name="item" placeholder="Add a new item">
+            <input type="text" id="itemInput" name="item" aria-label="Add a new item" placeholder="Add a new item">
             <button id="todo-item-form" class="button button2" type="submit">Add</button>
         </form>
         <ul>
@@ -478,23 +556,22 @@
                     {% if item.crossed == 'no' %}
                         <li style="text-decoration:none;">
                             {{ item.name }}
-                            <a href="{{ url_for('web_panels.cross', item_id=loop.index0) }}">[Cross Off]</a>&nbsp;<span style="color: red;"><a href="{{ url_for('web_panels.rem_todo_item', item_id=loop.index0) }}">[Remove]</a></span>
+                            <a href="{{ url_for('web_panels.cross', item_id=loop.index0) }}">[Cross Off]</a>&nbsp;<a href="{{ url_for('web_panels.rem_todo_item', item_id=loop.index0) }}"><span style="color: yellow;">[Remove]</span></a>
                         </li>
                     {% else %}
                         <li> <span style="text-decoration:line-through;">
                             {{ item.name }}</span>
-                            <a href="{{ url_for('web_panels.uncross', item_id=loop.index0) }}">[Un-Cross]</a>&nbsp;<span style="color: red;"><a href="{{ url_for('web_panels.rem_todo_item', item_id=loop.index0) }}">[Remove]</a></span>
-                        </li>
+                            <a href="{{ url_for('web_panels.uncross', item_id=loop.index0) }}">[Un-Cross]</a>&nbsp;<a href="{{ url_for('web_panels.rem_todo_item', item_id=loop.index0) }}"><span style="color: yellow;">[Remove]</span></a>
                     {% endif %}
                 {% endfor %}
             {% endif %}
         </ul>
-        <form action="/mgmt/clearlist" method="get" style="display: inline;">
-            <button class="button button2" type="submit" class="button">Clear List</button>
+        <form id="clear_list_form" action="/mgmt/clearlist" method="get" style="display: inline;">
+            <button id="clear_list_form" class="button button2" type="submit" class="button">Clear List</button>
         </form>
         <hr>
         <h3>Stream Overlay</h3>
-        To place the to-do list overlay in your stream, create a browser source in your streaming software<br>using the following URL: {{ settings_info[10] }}/todo<br>
+        To place the to-do list overlay in your stream, create a browser source in your streaming software<br>using the following URL: {{ settings_info[8] }}/todo<br>
         &emsp;Recommended width: 420<br>
         &emsp;Recommended height: 500<br>
         &emsp;In OBS, check "Refresh browser source when scene becomes active"
@@ -536,59 +613,77 @@
                 </tr>
             </thead>
                 <tr>
-                    <form id="nf_upload" action="/mgmt/alertupload/FOLLOWER_ALERT" method="post" enctype="multipart/form-data">
+                    <form id="new_follow_image_upload" action="/mgmt/alertupload/FOLLOWER_ALERT" method="post" enctype="multipart/form-data">
                     <td>New Follower:</td>
                     {% set follower_alert = alerts_dict["FOLLOWER_ALERT"] %}
                     {% if follower_alert %}
                         {% if "webm" in follower_alert %}
                             <td><video height="100" autoplay loop><source src="{{ url_for('web_panels.assets', asset_name=follower_alert) }}" type="video/webm"></video></td>
                         {% else %}
-                            <td><img src="{{ url_for('web_panels.assets', asset_name=follower_alert) }}"></td>
+                            <td><img alt="User Configurable Image" src="{{ url_for('web_panels.assets', asset_name=follower_alert) }}"></td>
                         {% endif %}
                     {% else %}
                         <td>Empty</td>
                     {% endif %}
                     <td><input type="file" name="FOLLOWER_ALERT" accept=".gif, .jpg, .jpeg, .png, .webm" required></td>
-                    <td><button id="nf_upload" class="button button2" type="submit">Upload</button>
+                    <td><button id="new_follow_image_upload" class="button button2" type="submit">Upload</button>
                     </form>&nbsp
-                        <a href="{{ url_for('web_panels.del_alert', alert_type='FOLLOWER_ALERT') }}"><button class="button button2" onclick="openTab(event, 'alerts')"><span style="color: red;">Clear</span></button></a></td>
-                    <td>{{ settings_info[10] }}/alert/follower</td>
+                        <a href="{{ url_for('web_panels.del_alert', alert_type='FOLLOWER_ALERT') }}"><button id="new_follow_image_upload" class="button button2" onclick="openTab(event, 'alerts')"><span style="color: yellow;">Clear</span></button></a></td>
+                    <td>{{ settings_info[8] }}/alert/follower</td>
                 </tr>
                 <tr>
-                    <form id="nms_upload" action="/mgmt/alertupload/MILESTONE_ALERT" method="post" enctype="multipart/form-data">
+                    <form id="new_milestone_image_upload" action="/mgmt/alertupload/MILESTONE_ALERT" method="post" enctype="multipart/form-data">
                     <td>Milestone Reached:</td>
                     {% set milestone_alert = alerts_dict["MILESTONE_ALERT"] %}
                     {% if milestone_alert %}
                         {% if "webm" in milestone_alert %}
                             <td><video height="100" autoplay loop><source src="{{ url_for('web_panels.assets', asset_name=milestone_alert) }}" type="video/webm"></video></td>
                         {% else %}
-                            <td><img src="{{ url_for('web_panels.assets', asset_name=milestone_alert) }}"></td>
+                            <td><img alt="User Configurable Image" src="{{ url_for('web_panels.assets', asset_name=milestone_alert) }}"></td>
                         {% endif %}
                     {% else %}
                         <td>Empty</td>
                     {% endif %}
                     <td><input type="file" name="MILESTONE_ALERT" accept=".gif, .jpg, .jpeg, .png, .webm" required></td>
-                    <td><button id="nms_upload" class="button button2" type="submit">Upload</button></form>&nbsp
-                        <a href="{{ url_for('web_panels.del_alert', alert_type='MILESTONE_ALERT') }}"><button class="button button2" onclick="openTab(event, 'alerts')"><span style="color: red;">Clear</span></button></a></td>
-                    <td>{{ settings_info[10] }}/alert/milestone</td>
+                    <td><button id="new_milestone_image_upload" class="button button2" type="submit">Upload</button></form>&nbsp
+                        <a href="{{ url_for('web_panels.del_alert', alert_type='MILESTONE_ALERT') }}"><button id="new_milestone_image_upload" class="button button2" onclick="openTab(event, 'alerts')"><span style="color: yellow;">Clear</span></button></a></td>
+                    <td>{{ settings_info[8] }}/alert/milestone</td>
                 </tr>
                 <tr>
-                    <form id="ng_upload" action="/mgmt/alertupload/GOAL_ALERT" method="post" enctype="multipart/form-data">
+                    <form id="new_goal_image_upload" action="/mgmt/alertupload/GOAL_ALERT" method="post" enctype="multipart/form-data">
                     <td>Goal Reached:</td>
                     {% set goal_alert = alerts_dict["GOAL_ALERT"] %}
                     {% if goal_alert %}
                         {% if "webm" in goal_alert %}
                             <td><video height="100" autoplay loop><source src="{{ url_for('web_panels.assets', asset_name=goal_alert) }}" type="video/webm"></video></td>
                         {% else %}
-                            <td><img src="{{ url_for('web_panels.assets', asset_name=goal_alert) }}"></td>
+                            <td><img alt="User Configurable Image" src="{{ url_for('web_panels.assets', asset_name=goal_alert) }}"></td>
                         {% endif %}
                     {% else %}
                         <td>Empty</td>
                     {% endif %}
                     <td><input type="file" name="GOAL_ALERT" accept=".gif, .jpg, .jpeg, .png, .webm" required></td>
-                    <td><button id="ng_upload" class="button button2" type="submit">Upload</button></form>&nbsp
-                        <a href="{{ url_for('web_panels.del_alert', alert_type='GOAL_ALERT') }}"><button class="button button2" onclick="openTab(event, 'alerts')"><span style="color: red;">Clear</span></button></a></td>
-                    <td>{{ settings_info[10] }}/alert/goal</td>
+                    <td><button id="new_goal_image_upload" class="button button2" type="submit">Upload</button></form>&nbsp
+                        <a href="{{ url_for('web_panels.del_alert', alert_type='GOAL_ALERT') }}"><button id="new_goal_image_upload" class="button button2" onclick="openTab(event, 'alerts')"><span style="color: yellow;">Clear</span></button></a></td>
+                    <td>{{ settings_info[8] }}/alert/goal</td>
+                </tr>
+                <tr>
+                    <form id="new_givebutter_image_upload" action="/mgmt/alertupload/GIVEBUTTER_ALERT" method="post" enctype="multipart/form-data">
+                    <td>GiveButter Donation:</td>
+                    {% set givebutter_alert = alerts_dict["GIVEBUTTER_ALERT"] %}
+                    {% if givebutter_alert %}
+                        {% if "webm" in givebutter_alert %}
+                            <td><video height="100" autoplay loop><source src="{{ url_for('web_panels.assets', asset_name=givebutter_alert) }}" type="video/webm"></video></td>
+                        {% else %}
+                            <td><img alt="User Configurable Image" src="{{ url_for('web_panels.assets', asset_name=givebutter_alert) }}"></td>
+                        {% endif %}
+                    {% else %}
+                        <td>Empty</td>
+                    {% endif %}
+                    <td><input type="file" name="GIVEBUTTER_ALERT" accept=".gif, .jpg, .jpeg, .png, .webm" required></td>
+                    <td><button id="new_givebutter_image_upload" class="button button2" type="submit">Upload</button></form>&nbsp
+                        <a href="{{ url_for('web_panels.del_alert', alert_type='GIVEBUTTER_ALERT') }}"><button id="new_givebutter_image_upload" class="button button2" onclick="openTab(event, 'alerts')"><span style="color: yellow;">Clear</span></button></a></td>
+                    <td>{{ settings_info[8] }}/alert/givebutter</td>
                 </tr>
         </table>
         <h3>Stream Overlays</h3>
@@ -610,4 +705,8 @@
         </body>
         <br><br>
     </div>
+    <footer>
+        If you are thrilled to death with OwnchatBot, and want to throw a little monetary love DeadTOm's way, <a href=https://ko-fi.com/deadtom>he's on Ko-fi</a>.<br>
+        OwnchatBot © 2025 by <a href=https://www.deadtom.me>DeadTOm</a> is licensed under <a href=https://creativecommons.org/licenses/by-sa/4.0/>Creative Commons Attribution-ShareAlike 4.0 International</a>.
+    </footer>
 </html>

+ 2 - 2
ownchatbot/templates/queue.html

@@ -14,7 +14,7 @@
     <div class="panel">
     <div class="navbar">
         <p>Rewards Queue Management</p>
-        <img src="/static/img/ownchatbotwide.png">
+        <img alt="Ownchat Bot logo, whimsical robot" src="/static/img/ownchatbotwide.png">
     </div>
         <body>
             {% if votes %}
@@ -84,7 +84,7 @@
                         </td>
 
                         {% if goal[1] == goal[2] %}
-                        <td> {{ goal[1] }} / {{ goal[2] }} <img src=/static/img/tada.png style="width: 24px; height: 24px;"></td>
+                        <td> {{ goal[1] }} / {{ goal[2] }} <img alt="Party horn with confetti" src=/static/img/tada.png style="width: 24px; height: 24px;"></td>
                         {% else %}
                         <td> {{ goal[1] }} / {{ goal[2] }} </td>
                         {% endif %}

+ 55 - 0
ownchatbot/templates/rgbdonation.html

@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="stylesheet" href="{{ url_for('static', filename='alertstyle.css') }}">
+    <title>GiveButter Donation Alert</title>
+</head>
+<body>
+    {% if not alert_name %}
+        <div id="alertVid"></div>
+        <div id="alertImg"></div>
+    {% elif "webm" in alert_name %}
+        <video id="alertVid" autoplay>
+            <source src="{{ url_for('web_panels.assets', asset_name=alert_name) }}" type="video/webm">
+        </video>
+        <div id="alertImg"></div>
+    {% else %}
+        <div id="alertVid"></div>
+        <img id="alertImg" src="{{ url_for('web_panels.assets', asset_name=alert_name) }}">
+    {% endif %}
+
+    <div id="nameBox"></div>
+
+    <script>
+        setInterval(function() {
+            fetch('/checkGBs')
+                .then(response => response.json())
+                .then(data => {
+                    if (data) {
+                        const alertVid = document.getElementById('alertVid');
+                        const alertImg = document.getElementById('alertImg');
+                        const nameBox = document.getElementById('nameBox');
+
+                        const alertName = data.name;
+
+                        nameBox.innerHTML = `
+                            ${alertName}
+                        `;
+                        nameBox.style.display = 'block';
+
+                        alertVid.style.display = 'block';
+                        alertImg.style.display = 'block';
+
+                        setTimeout(() => {
+                            alertVid.style.display = 'none';
+                            alertImg.style.display = 'none';
+                            nameBox.style.display = 'none';
+                        }, 15000); // Visible for 15 seconds
+                    }
+                });
+        }, 3000); // Check for new donation every 3 seconds
+    </script>
+</body>
+</html>

+ 2 - 4
ownchatbot/templates/rgoal.html

@@ -1,7 +1,5 @@
 <!DOCTYPE html>
 <html lang="en">
-<!-- Polls the check_goals route every three seconds. When a goal is reached,
-the alert is displayed for ten seconds. -->
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -48,10 +46,10 @@ the alert is displayed for ten seconds. -->
                             alertVid.style.display = 'none';
                             alertImg.style.display = 'none';
                             nameBox.style.display = 'none';
-                        }, 10000); // Visible for 10 seconds
+                        }, 15000); // Visible for 15 seconds
                     }
                 });
-        }, 3000); // Check for new followers every 3 seconds
+        }, 3000); // Check for new goal every 3 seconds
     </script>
 </body>
 </html>

+ 2 - 4
ownchatbot/templates/rmilestone.html

@@ -1,7 +1,5 @@
 <!DOCTYPE html>
 <html lang="en">
-<!-- Polls the check_milestones route every three seconds. When a new milestone is reached,
-the alert is displayed for ten seconds. -->
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -48,10 +46,10 @@ the alert is displayed for ten seconds. -->
                             alertVid.style.display = 'none';
                             alertImg.style.display = 'none';
                             nameBox.style.display = 'none';
-                        }, 10000); // Visible for 10 seconds
+                        }, 15000); // Visible for 15 seconds
                     }
                 });
-        }, 3000); // Check for new followers every 3 seconds
+        }, 3000); // Check for new milestone every 3 seconds
     </script>
 </body>
 </html>

+ 69 - 85
ownchatbot/templates/userpanel.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-    <title>OCB - Stream Points and Rewards</title>
+    <title>OwnchatBot</title>
     <link rel="icon" type="image/x-icon" href="/static/img/favicon.ico">
     <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
 </head>
@@ -9,11 +9,11 @@
     
     <div class="navbar">
         <div class="tab">
-            <button class="tablinks" data-tab="ocbinfo" onclick="openTab(event, 'ocbinfo')">OwnchatBot Info</button>
-            <button class="tablinks" data-tab="rewards" onclick="openTab(event, 'rewards')">Points and Rewards</button>
-            <button class="tablinks" data-tab="queue" onclick="openTab(event, 'queue')">Rewards Queue</button>
+            <button tabindex="0" class="tablinks" data-tab="ocbinfo" onclick="openTab(event, 'ocbinfo')">OwnchatBot Info</button>
+            <button tabindex="1" class="tablinks" data-tab="rewards" onclick="openTab(event, 'rewards')">Points and Rewards</button>
+            <button tabindex="2" class="tablinks" data-tab="queue" onclick="window.location.href='{{ url_for('web_panels.user_queue', activeTab='queue') }}'">Rewards Queue</button>
         </div>
-        <img src="/static/img/ownchatbotwide.png">
+        <img alt="Ownchat Bot logo, whimsical robot" src="/static/img/ownchatbotwide.png">
     </div>
 
     <div id='ocbinfo' class="tabcontent">
@@ -35,60 +35,90 @@
         </ul>
         
         <hr>
-        {% if kofi_integration %}
-            <h4><a href="{{ kofi_settings['kofi_url'] }}/donate" target="new"><img src="/static/img/kofi_symbol.png"></a> Kofi Integration</h4>
+        <table>
+            <thead>
+                <tr style="border-bottom: none;">
+                    <th></th>
+                    <th></th>
+                </tr>
+            </thead>
+        {% if kofi_settings['donations'] %}
+            <tr style="border-bottom: none;">
+                <td rowspan="2"><h4><a href="{{ kofi_settings['kofi_url'] }}/donate" target="new"><img style="height: 25px; width: auto; padding-right: 8px; !important" alt="Coffee Cup Logo" src="/static/img/kofi_symbol.png"></a> Ko-fi</h4></td>
             
             {% if kofi_settings['donations'] %}
-                {% set d_points_label = 'point' if donation_points == 1 else 'points' %}
-                You are awarded {{ kofi_settings['donation_points'] }} {{ d_points_label }} for every dollar you donate on Kofi.<br>
-            {% endif %}
-            
-            {% if kofi_settings['subs'] %}
-                {% set s_points_label = 'point' if sub_points == 1 else 'points' %}
-                Kofi subscribers get {{ kofi_settings['sub_points'] }} {{ s_points_label }} every month.<br>
+                {% set d_points_label = 'point' if kofi_settings['donation_points'] == 1 else 'points' %}
+                <td>You are awarded {{ kofi_settings['donation_points'] }} {{ d_points_label }} for every dollar donated.<br></td>
             {% endif %}
-            <i>You must be authenticated with Owncast to earn points for Kofi donations.</i><br>
-            
+            </tr>
+        {% if gb_settings['integration'] %}
+            <tr>
+        {% else %}
+            <tr style="border-bottom: none;">
+        {% endif %}           
+        {% if kofi_settings['subs'] %}
+            {% set s_points_label = 'point' if kofi_settings['sub_points'] == 1 else 'points' %}
+            <td>Subscribers get {{ kofi_settings['sub_points'] }} {{ s_points_label }} every month.<br></td>
+        {% endif %}
+            </tr>
+        {% endif %}
+        
+        {% if gb_settings['integration'] %}
+        {% if gb_settings['donations'] %}
+            <tr style="border-bottom: none;">
+            <td><h4><a href="{{ gb_settings['gb_url'] }}" target="new"><img style="height: 25px; width: auto; padding-right: 8px; !important" alt="Give Butter Logo" src="/static/img/givebutter.png"></a> GiveButter</h4></td>
+            {% set d_points_label = 'point' if gb_settings['donation_points'] == 1 else 'points' %}
+            <td>You are awarded {{ gb_settings['donation_points'] }} {{ d_points_label }} for every dollar donated.<br></td>
+            </tr>
+        {% endif %}
+        {% endif %}
+        </table>
+            <br><span style="color: orange;"><i>You must be authenticated with Owncast to earn points for donations.</i></span><br>
             {% for user in users %}
             
             <div>
                 {% if user['user_authed'] %}
                     <br>
-                    OwnchatBot recognizes your Kofi account by your email address. In order for OwnchatBot to award your donation points, you must enter the email address associated with your Kofi account here.<br><br>
+                    OwnchatBot recognizes your Ko-fi and GiveButter accounts by your email address. In order for OwnchatBot to award your donation points, you must supply your email address.<br><br>
                     
                     {% with messages = get_flashed_messages(with_categories=true) %}
                         {% if messages %}
                             {% for category, message in messages %}
                                 {% if category == 'failure' %}
-                                    <span style="color: red;">{{ message }}</span>
+                                    <span style="color: orange;">{{ message }}</span>
                                 {% endif %}
                                 {% if category == 'success' %}
-                                    <span style="color: green;">{{ message }}</span>
+                                    <span style="color: yellow;">{{ message }}</span>
                                 {% endif %}
                             {% endfor %}
                         {% endif %}
                     {% endwith %}
                     
-                    <form method="POST" action="/set_viewer_email">
-                        <label for="code">Type !reg_mail into the chat, and enter the code it gives you here:</label>
-                        <input type="number" name="code" size="6" required><br>                        
-                        <label for="new_email">Enter the email address associated with your Kofi account:</label>
-                        <input type="text" name="new_email" value="{{ user[4] }}" size="40" required>
-                        <input type="hidden" name="instance" value="{{ instance }}">
-                        <input type="hidden" name="user_name" value="{{ username }}">
-                        <br>Email addresses are <b>ONLY</b> used for Kofi integration. They are not sent to any other individual or company, and will not be used to create or send mailing lists of any kind.<br>
-                        <input type="hidden" name="user_id" value="{{ user[0] }}"> <button class="button button2" type="submit">Save Email</button>
+                    <form id="code" method="POST" action="/set_viewer_email">
+                    <table>
+                        <tr style="border-bottom: none;">
+                            <td><label for="reg_code">Type !reg_mail into the chat, and enter the code it gives you here:</label></td>
+                            <td><input id="reg_code" type="number" name="code" size="6" aria-required="true" required></td>
+                        </tr>
+                        <tr style="border-bottom: none;">
+                            <td><label for="email">Enter your email address:</label></td>
+                            <td><input id="email" type="text" name="new_email" value="{{ user[4] }}" size="40" aria-required="true" aria-label="Enter email address" required></td>
+                        </tr>
+                    </table>
+                        <input id="code" type="hidden" name="instance" value="{{ instance }}">
+                        <input id="code" type="hidden" name="user_name" value="{{ username }}">
+                        <input id="code" type="hidden" name="user_id" value="{{ user[0] }}"> <button class="button button2" type="submit">Save Email</button>
                     </form><br>
+                        <i>Email addresses are ONLY used for Ko-fi and GiveButter integration. They are not sent to any other individual or company, and will not be used to create or send mailing lists of any kind. Ever.</i>
                 {% endif %}
                 </div>
             {% endfor %}
-        {% endif %}
 
         <hr>
         <h4>Other stuff</h4>
         <div>
             OwnchatBot can be downloaded from <a href=https://git.deadtom.me/deadtom/OwnchatBot>https://git.deadtom.me/deadtom/OwnchatBot</a>.<br>
-            If you are thrilled to death with OwnchatBot, and want to throw a little monetary love the developer's way, <a href=https://ko-fi.com/deadtom>he's on Kofi</a>.<br>
+            If you are thrilled to death with OwnchatBot, and want to throw a little monetary love the DeadTOm's way, <a href=https://ko-fi.com/deadtom>he's on Ko-fi</a>.<br>
             OwnchatBot © 2025 by <a href=https://www.deadtom.me>DeadTOm</a> is licensed under <a href=https://creativecommons.org/licenses/by-sa/4.0/>Creative Commons Attribution-ShareAlike 4.0 International</a>.
         </div>
         <br><br>
@@ -116,7 +146,7 @@
                     <br>&nbsp;&nbsp;You can also get {{ kofi_settings['donation_points'] }} {{ d_points_label }} for every dollar you donate on Kofi{% if kofi_settings['subs'] %}, and {{ kofi_settings['sub_points'] }} {{ s_points_label }} every month for subscribing to my Kofi page{% endif %}.
                 {% endif %}
             {% endif %}
-            <h3>Active Votes</h3>
+            <h3><u>Active Votes</u></h3>
             {% if votes %}
                 <table>
                     <thead>
@@ -144,10 +174,10 @@
                     </tbody>
                 </table>
             {% else %}
-                &nbsp;&nbsp;There are currently no active votes
+                &nbsp;&nbsp;<span style="color: yellow;">There are currently no active votes</span>
                 <br>
             {% endif %}
-            <h3>Active Goals</h3>
+            <h3><u>Active Goals</u></h3>
             {% if goals %}
             <table>
                 <thead>
@@ -174,7 +204,7 @@
                                         {% if milestones[milestone_key][1] < goal[2] %}
                                             {% set milestone_progress = milestones[milestone_key][1] / goal[2] * 100 %}
                                                 <div class="milestone-marker" style="position: absolute; left: {{ milestone_progress }}%; transform: translateX(-50%);">
-                                                    <img src="/static/img/milestone.png" style="width: 16px; height: 16px;" title="{{ milestones[milestone_key][1] }} points. {{ milestones[milestone_key][0] }}">
+                                                    <img alt="Red Flag" src="/static/img/milestone.png" style="width: 16px; height: 16px;" title="{{ milestones[milestone_key][1] }} points. {{ milestones[milestone_key][0] }}">
                                                 </div>
                                             {% endif %}
                                         {% endif %}
@@ -182,7 +212,7 @@
                             </div>
                         </td>
                         {% if goal[1] == goal[2] %}
-                        <td>   {{ goal[1] }} / {{ goal[2] }} <img src=/static/img/tada.png style="width: 24px; height: 24px;"></td>
+                        <td>   {{ goal[1] }} / {{ goal[2] }} <img alt="Party horn with confetti" src=/static/img/tada.png style="width: 24px; height: 24px;"></td>
                         {% else %}
                         <td>   {{ goal[1] }} / {{ goal[2] }} </td>
                         {% endif %}
@@ -198,12 +228,11 @@
                 </tbody>
             </table>
             {% else %}
-            &nbsp;&nbsp;There are currently no active goals
+            &nbsp;&nbsp;<span style="color: yellow;">There are currently no active goals</span>
             <br>
             {% endif %}
-        </body>
-        <body>
-            <h3>Active Rewards</h3>
+            
+            <h3><u>Active Rewards</u></h3>
             {% if rewards %}
             <table>
                 <thead>
@@ -240,55 +269,10 @@
                 </tbody>
             </table>
             {% else %}
-            &nbsp;&nbsp;There are currently no active redeems
+            &nbsp;&nbsp;<span style="color: yellow;">There are currently no active redeems</span>
             {% endif %}
             <br><br>
         </body>
     </div>
 
-    <div id='queue' class="tabcontent">
-
-        <body>
-            <h3>Queue</h3>
-            {% if queue %}
-            <table>
-                <thead>
-                    <tr>
-                        <th>Time</th>
-                        <th>Name</th>
-                        <th>Description</th>
-                        <th>User</th>
-                        <th></th>
-                    </tr>
-                </thead>
-                <tbody>
-                {% for row in queue %}
-                <tr>
-                    <td>{{ row[1].replace(tzinfo=utc_timezone).astimezone().strftime("%H:%M") }}</td>
-                    {% if row[4] %}
-                        <td><s>{{ prefix }}{{ row[2] }}</s></td>
-                    {% else %}
-                        <td>{{ prefix }}{{ row[2] }}</td>
-                    {% endif %}
-                    <td>{{ all_rewards[row[2]]["info"] }}</td>
-                    {% if row[6] %}
-                    <td>{{ row[6] }}</td>
-                    {% else %}
-                    <td></td>
-                    {% endif %}
-                </tr>
-                {% endfor %}
-                </tbody>
-            </table>
-            {% else %}
-            &nbsp;&nbsp;The queue is currently empty
-            {% endif %}
-        </body>
-        <br><br>
-    </div>
-    
-    <script>
-        setTimeout(refreshPage, 30 * 1000);
-    </script>
-
 </html>

+ 78 - 0
ownchatbot/templates/userqueue.html

@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <title>OwnchatBot</title>
+    <link rel="icon" type="image/x-icon" href="/static/img/favicon.ico">
+    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
+</head>
+    
+    <div class="navbar">
+        <div class="tab">
+            <button tabindex="0" class="tablinks" data-tab="ocbinfo" onclick="setActiveTabAndNavigate('ocbinfo', '{{ url_for('web_panels.user_panel', instance=instance, username=username) }}')">OwnchatBot Info</button>
+            <button tabindex="1" class="tablinks" data-tab="rewards" onclick="setActiveTabAndNavigate('rewards', '{{ url_for('web_panels.user_panel', instance=instance, username=username) }}')">Points and Rewards</button>
+            <button tabindex="2" style="background-color: #003399; color: white;" class="tablinks" data-tab="queue" onclick="window.location.href='{{ url_for('web_panels.user_queue', activeTab='queue') }}'">Rewards Queue</button>
+        </div>
+        <img alt="Ownchat Bot logo, whimsical robot" src="/static/img/ownchatbotwide.png">
+    </div>
+
+    <div id="queue" class="queuecontent">
+
+        <body>
+        <i>This page auto-refreshes every 30 seconds.</i>
+            <h3>Queue</h3>
+            {% if queue %}
+            <table>
+                <thead>
+                    <tr>
+                        <th>Time</th>
+                        <th>Name</th>
+                        <th>Description</th>
+                        <th>User</th>
+                        <th></th>
+                    </tr>
+                </thead>
+                <tbody>
+                {% for row in queue %}
+                <tr>
+                    <td>{{ row[1].replace(tzinfo=utc_timezone).astimezone().strftime("%H:%M") }}</td>
+                    {% if row[4] %}
+                        <td><s>{{ prefix }}{{ row[2] }}</s></td>
+                    {% else %}
+                        <td>{{ prefix }}{{ row[2] }}</td>
+                    {% endif %}
+                    <td>{{ all_rewards[row[2]]["info"] }}</td>
+                    {% if row[6] %}
+                    <td>{{ row[6] }}</td>
+                    {% else %}
+                    <td></td>
+                    {% endif %}
+                </tr>
+                {% endfor %}
+                </tbody>
+            </table>
+            {% else %}
+            &nbsp;&nbsp;<span style="color: yellow;">The queue is currently empty</span>
+            {% endif %}
+        </body>
+        <br><br>
+    </div>
+    
+    <script>
+        window.onload = function() {
+            var activeTab = 'queue';
+            localStorage.setItem('activeTab', activeTab);
+        }
+        
+        function setActiveTabAndNavigate(tabName, url) {
+            localStorage.setItem('activeTab', tabName);
+            window.location.href = url;
+        }
+
+        function refreshPage() {
+            window.location.reload();
+        }
+    
+        setTimeout(refreshPage, 30 * 1000);
+    </script>
+
+</html>

+ 38 - 38
ownchatbot/user_handlers.py

@@ -17,7 +17,7 @@ def save_todolist(list_items):  # Save todo list items
         current_app.config.from_pyfile('todo.py', silent=True)  # Reread the list into the app
         return True
     except Exception as stdlerror:
-        current_app.logger.error(f'Couldn\'t save todo.py: {stdlerror.args[0]}')
+        current_app.logger.error(f'Couldn\'t save todo.py: {stdlerror}')
         return False
 
 
@@ -28,8 +28,8 @@ def get_users_points(db, user_id):  # Look up one user's points by user id
             (user_id,)
         )
         return cursor.fetchone()[0]
-    except Error as guperror:
-        current_app.logger.error(f'Couldn\'t look up points for {user_id}: {guperror.args[0]}')
+    except Exception as guperror:
+        current_app.logger.error(f'Couldn\'t look up points for {user_id}: {guperror}')
 
 
 def get_email_code(db, user_id):  # Get user's verification code
@@ -39,8 +39,8 @@ def get_email_code(db, user_id):  # Get user's verification code
             (user_id,)
         )
         return cursor.fetchone()[0]
-    except Error as gecerror:
-        current_app.logger.error(f'Couldn\'t look up points for {user_id}: {gecerror.args[0]}')
+    except Exception as gecerror:
+        current_app.logger.error(f'Couldn\'t look up points for {user_id}: {gecerror}')
 
 
 def get_id_by_email(db, email):  # Look up all users' points by username
@@ -51,8 +51,8 @@ def get_id_by_email(db, email):  # Look up all users' points by username
         )
         users = cursor.fetchall()
         return users
-    except Error as gaubeerror:
-        current_app.logger.error(f'Couldn\'t look up user id for {email} by email: {gaubeerror.args[0]}')
+    except Exception as gaubeerror:
+        current_app.logger.error(f'Couldn\'t look up user id for {email} by email: {gaubeerror}')
 
 
 def get_all_users_by_name(db, username):  # Look up all users' points by username
@@ -63,8 +63,8 @@ def get_all_users_by_name(db, username):  # Look up all users' points by usernam
         )
         users = cursor.fetchall()
         return users
-    except Error as gaubnerror:
-        current_app.logger.error(f'Couldn\'t look up points for {username} by username: {gaubnerror.args[0]}')
+    except Exception as gaubnerror:
+        current_app.logger.error(f'Couldn\'t look up points for {username} by username: {gaubnerror}')
 
 
 def get_all_users_with_user_id(db, user_id):  # Look up all users' points by user id
@@ -75,8 +75,8 @@ def get_all_users_with_user_id(db, user_id):  # Look up all users' points by use
         )
         users = cursor.fetchall()
         return users
-    except Error as gauwuierror:
-        current_app.logger.error(f'Couldn\'t look up points for {user_id} by user_id: {gauwuierror.args[0]}')
+    except Exception as gauwuierror:
+        current_app.logger.error(f'Couldn\'t look up points for {user_id} by user_id: {gauwuierror}')
 
 
 def get_all_users(db):  # Get all users' details from points database
@@ -86,8 +86,8 @@ def get_all_users(db):  # Get all users' details from points database
         )
         users = cursor.fetchall()
         return users
-    except Error as gauerror:
-        current_app.logger.error(f'Couldn\'t get all users\' points: {gauerror.args[0]}')
+    except Exception as gauerror:
+        current_app.logger.error(f'Couldn\'t get all users\' points: {gauerror}')
 
 
 def award_chat_points(db, user_id, points):  # Award points to user by user id
@@ -98,8 +98,8 @@ def award_chat_points(db, user_id, points):  # Award points to user by user id
         )
         db.commit()
         return True
-    except Error as acperror:
-        current_app.logger.error(f'Couldn\'t give {points} points to {user_id}: {acperror.args[0]}')
+    except Exception as acperror:
+        current_app.logger.error(f'Couldn\'t give {points} points to {user_id}: {acperror}')
         return False
 
 
@@ -111,8 +111,8 @@ def set_email_code(db, user_id, reg_code):  # Set verification code
         )
         db.commit()
         return True
-    except Error as secerror:
-        current_app.logger.error(f'Couldn\'t set reg code \"{reg_code}\" for {user_id}: {secerror.args[0]}')
+    except Exception as secerror:
+        current_app.logger.error(f'Couldn\'t set reg code \"{reg_code}\" for {user_id}: {secerror}')
         return False
 
 
@@ -124,8 +124,8 @@ def del_email_code(db, user_id):  # Delete verification code
         )
         db.commit()
         return True
-    except Error as decerror:
-        current_app.logger.error(f'Couldn\'t remove reg code for {user_id}: {decerror.args[0]}')
+    except Exception as decerror:
+        current_app.logger.error(f'Couldn\'t remove reg code for {user_id}: {decerror}')
         return False
 
 
@@ -138,8 +138,8 @@ def adjust_points(db, user_id, points):  # For streamer to manually adjust a use
         db.commit()
         current_app.logger.info(f'Adjusted points for {user_id}')
         return True
-    except Error as aperror:
-        current_app.logger.error(f'Couldn\'t adjust points for {user_id}: {aperror.args[0]}')
+    except Exception as aperror:
+        current_app.logger.error(f'Couldn\'t adjust points for {user_id}: {aperror}')
         return False
 
 
@@ -151,8 +151,8 @@ def delete_user(db, user_id):
             )
         db.commit()
         return True
-    except Error as du_error:
-        current_app.logger.error(f'Couldn\'t change delete {user_id} from the database: {du_error.args[0]}')
+    except Exception as du_error:
+        current_app.logger.error(f'Couldn\'t change delete {user_id} from the database: {du_error}')
         return False
 
 
@@ -176,8 +176,8 @@ def change_email(db, user_id, new_email):  # For streamer to manually adjust a u
         )
         db.commit()
         return True
-    except Error as ce_error:
-        current_app.logger.error(f'Couldn\'t change email address for {user_id}: {ce_error.args[0]}')
+    except Exception as ce_error:
+        current_app.logger.error(f'Couldn\'t change email address for {user_id}: {ce_error}')
         return False
 
 
@@ -189,8 +189,8 @@ def spend_points(db, user_id, points):  # A user spends points on a redeem
         )
         db.commit()
         return True
-    except Error as sperror:
-        current_app.logger.error(f'Couldn\'t spend {user_id}\'s {points} points: {sperror.args[0]}')
+    except Exception as sperror:
+        current_app.logger.error(f'Couldn\'t spend {user_id}\'s {points} points: {sperror}')
         return False
 
 
@@ -204,7 +204,7 @@ def refund_points(db, user_id, points):  # Streamer refunds points for a redeem
         current_app.logger.info(f'Refunded {points} points to {user_id}.')
         return True
     except Exception as rerror:
-        current_app.logger.error(f'Couldn\'t refund {points} points for {user_id}: {rerror.args[0]}')
+        current_app.logger.error(f'Couldn\'t refund {points} points for {user_id}: {rerror}')
         return False
 
 
@@ -217,8 +217,8 @@ def user_in_points(db, user_id):  # Check if a user is in the points database
         if cursor.fetchone() is None:
             return False
         return True
-    except Error as uiperror:
-        current_app.logger.error(f'Couldn\'t for {user_id} in points database: {uiperror.args[0]}')
+    except Exception as uiperror:
+        current_app.logger.error(f'Couldn\'t for {user_id} in points database: {uiperror}')
 
 
 def add_email_to_points(db, email, points):  # Add an anonymous user and points to the database
@@ -230,8 +230,8 @@ def add_email_to_points(db, email, points):  # Add an anonymous user and points
         )
         db.commit()
         return True
-    except Error as aetperror:
-        current_app.logger.error(f'Couldn\'t add {email} to points database: {aetperror.args[0]}')
+    except Exception as aetperror:
+        current_app.logger.error(f'Couldn\'t add {email} to points database: {aetperror}')
         return False
 
 
@@ -259,8 +259,8 @@ def add_user_to_points(db, user_id, display_name, authed):  # Add a user to the
             )
         db.commit()
         return True
-    except Error as autperror:
-        current_app.logger.error(f'Couldn\'t add {user_id}/{display_name}/{authed} to database: {autperror.args[0]}')
+    except Exception as autperror:
+        current_app.logger.error(f'Couldn\'t add {user_id}/{display_name}/{authed} to database: {autperror}')
         return False
 
 
@@ -271,8 +271,8 @@ def change_name(db, user_id, new_name):  # Change a user name in the points data
             (new_name, user_id)
         )
         db.commit()
-    except Error as cnerror:
-        current_app.logger.error(f'Couldn\'t change name to {new_name} for {user_id}: {cnerror.args[0]}')
+    except Exception as cnerror:
+        current_app.logger.error(f'Couldn\'t change name to {new_name} for {user_id}: {cnerror}')
 
 
 def remove_duplicates(db, user_id, username):  # Remove duplicate usernames
@@ -282,5 +282,5 @@ def remove_duplicates(db, user_id, username):  # Remove duplicate usernames
             (username, user_id)
         )
         db.commit()
-    except Error as rderror:
-        current_app.logger.error(f'Couldn\'t remove duplicate username {username} for {user_id}: {rderror.args[0]}')
+    except Exception as rderror:
+        current_app.logger.error(f'Couldn\'t remove duplicate username {username} for {user_id}: {rderror}')

+ 54 - 20
ownchatbot/web_panels.py

@@ -7,7 +7,7 @@ from ownchatbot.bot_messages import save_announce, porps
 from ownchatbot.owncast_com import send_private_chat, send_chat
 import json
 import emoji
-from ownchatbot.kofi_handlers import save_kofi_settings, kofi_pngs
+from ownchatbot.donation_handlers import save_kofi_settings, save_gb_settings
 import random
 import pkce
 import requests
@@ -101,8 +101,6 @@ def mgmt():
     prefix = current_app.config['PREFIX']
     access_id = current_app.config['ACCESS_ID']
     access_token = current_app.config['ACCESS_TOKEN']
-    kofi_token = current_app.config['KOFI_TOKEN']
-    kofi_integration = current_app.config['KOFI_INTEGRATION']
     announce_enable = current_app.config['ANNOUNCE_ENABLE']
     announce_interval = current_app.config['ANNOUNCE_INTERVAL']
     announcements = current_app.config['ANNOUNCEMENTS']
@@ -117,8 +115,6 @@ def mgmt():
         prefix,
         access_token,
         owncast_url,
-        kofi_token,
-        kofi_integration,
         announce_enable,
         announce_interval,
         ocb_url
@@ -133,7 +129,7 @@ def mgmt():
                            active_rewards=active_rewards,
                            prefix=current_app.config['PREFIX'],
                            kofi_settings=current_app.config['KOFI_SETTINGS'],
-                           kofi_integration=kofi_integration,
+                           gb_settings=current_app.config['GB_SETTINGS'],
                            announcements=announcements,
                            users=users,
                            utc_timezone=utc_timezone,
@@ -180,14 +176,13 @@ def user_panel():
         users = []
     utc_timezone = timezone.utc
     return render_template('userpanel.html',
-                           queue=get_queue(db),
                            votes=all_active_votes(db),
                            goals=all_active_goals(db),
                            rewards=all_active_rewards(),
                            all_rewards=all_rewards,
                            prefix=current_app.config['PREFIX'],
                            kofi_settings=current_app.config['KOFI_SETTINGS'],
-                           kofi_integration=current_app.config['KOFI_INTEGRATION'],
+                           gb_settings=current_app.config['GB_SETTINGS'],
                            points_interval=points_interval,
                            points_award=points_award,
                            username=username,
@@ -196,6 +191,20 @@ def user_panel():
                            utc_timezone=utc_timezone)
 
 
+@ocb.route('/userqueue', methods=['GET'])  # The viewers panel
+def user_queue():
+    db = get_db()
+    instance = request.args.get('instance')
+    all_rewards = rewards = current_app.config['REWARDS']
+    utc_timezone = timezone.utc
+    return render_template('userqueue.html',
+                           queue=get_queue(db),
+                           all_rewards=all_rewards,
+                           prefix=current_app.config['PREFIX'],
+                           instance=instance,
+                           utc_timezone=utc_timezone)
+
+
 @ocb.route('/mgmt/fulfill', methods=['GET'])
 @requires_login
 def fulfilled():
@@ -341,8 +350,6 @@ def settings():
     access_token = request.form['access_token']
     owncast_url = request.form['owncast_url']
     ocb_url = request.form['ocb_url']
-    kofi_integration = 'kofi_integration' in request.form
-    kofi_token = request.form['kofi_token']
     config_dict = {
         'POINTS_INTERVAL': points_interval,
         'POINTS_AWARD': points_award,
@@ -350,9 +357,7 @@ def settings():
         'OCB_URL': ocb_url,
         'ACCESS_ID': access_id,
         'ACCESS_TOKEN': access_token,
-        'OWNCAST_URL': owncast_url,
-        'KOFI_TOKEN': kofi_token,
-        'KOFI_INTEGRATION': kofi_integration
+        'OWNCAST_URL': owncast_url
         }
     if save_config(config_dict):  # Save new config.py
         current_app.logger.info('Saved new config.')
@@ -378,24 +383,50 @@ def announcements():
     return redirect(url_for('web_panels.mgmt', activeTab='announcements'))
 
 
-@ocb.route('/mgmt/ksettings', methods=['GET', 'POST'])  # OwnchatBot settings panel
+@ocb.route('/mgmt/ksettings', methods=['GET', 'POST'])  # Ko-fi settings panel
 @requires_login
 def ksettings():
     kofi_settings_dict = current_app.config['KOFI_SETTINGS']
     if request.method == 'POST':
-        enable_donations = 'enable_donations' in request.form
-        set_donation_points = request.form['set_donation_points']
-        enable_subs = 'enable_subs' in request.form
-        sub_points = int(request.form['sub_points'])
+        kofi_integration = 'k_integration' in request.form
+        kofi_token = request.form['k_token']
+        enable_donations = 'k_enable_donations' in request.form
+        donation_points = int(request.form['k_donation_points'])
+        enable_subs = 'k_enable_subs' in request.form
+        sub_points = int(request.form['k_sub_points'])
         kofi_url = request.form['kofi_url']
+        kofi_settings_dict['integration'] = kofi_integration
+        kofi_settings_dict['token'] = kofi_token
         kofi_settings_dict['donations'] = enable_donations
+        kofi_settings_dict['donation_points'] = donation_points
         kofi_settings_dict['subs'] = enable_subs
         kofi_settings_dict['sub_points'] = sub_points
         kofi_settings_dict['kofi_url'] = kofi_url
         if save_kofi_settings(kofi_settings_dict):
-            current_app.logger.info(f'Saved Kofi settings')
+            current_app.logger.info(f'Saved Ko-fi settings')
+
+    return redirect(url_for('web_panels.mgmt', activeTab='donations'))
 
-    return redirect(url_for('web_panels.mgmt', activeTab='kofi-settings'))
+
+@ocb.route('/mgmt/gbsettings', methods=['GET', 'POST'])  # GiveButter settings panel
+@requires_login
+def gbsettings():
+    gb_settings_dict = current_app.config['GB_SETTINGS']
+    if request.method == 'POST':
+        gb_integration = 'gb_integration' in request.form
+        gb_secret = request.form['gb_secret']
+        enable_donations = 'gb_enable_donations' in request.form
+        donation_points = int(request.form['gb_donation_points'])
+        gb_url = request.form['gb_url']
+        gb_settings_dict['integration'] = gb_integration
+        gb_settings_dict['secret'] = gb_secret
+        gb_settings_dict['donations'] = enable_donations
+        gb_settings_dict['donation_points'] = donation_points
+        gb_settings_dict['gb_url'] = gb_url
+        if save_gb_settings(gb_settings_dict):
+            current_app.logger.info(f'Saved GiveButter settings')
+
+    return redirect(url_for('web_panels.mgmt', activeTab='donations'))
 
 
 @ocb.route('/mgmt/add/<reward_type>', methods=['GET', 'POST'])
@@ -687,6 +718,9 @@ def ocb_alert(alert_type):
     elif alert_type == 'GOAL_ALERT':
         return render_template('rgoal.html',
                                alert_name=alert_name)
+    elif alert_type == 'GIVEBUTTER_ALERT':
+        return render_template('rgbdonation.html',
+                               alert_name=alert_name)
 
 
 @ocb.route('/goals', methods=['GET'])  # Goals overlay

+ 168 - 61
ownchatbot/webhooks.py

@@ -4,9 +4,11 @@ from ownchatbot.owncast_com import send_chat, send_private_chat
 from ownchatbot.user_handlers import add_user_to_points, change_name, get_users_points, remove_duplicates, get_email_code, set_email_code, award_chat_points, user_in_points, get_all_users_with_user_id
 from ownchatbot.bot_messages import do_reward, help_message, porps
 from ownchatbot.reward_handlers import all_active_goals, all_active_votes, all_active_rewards, save_alerts
-from ownchatbot.kofi_handlers import accept_donation, accept_sub
+from ownchatbot.donation_handlers import accept_donation, accept_kofi_sub
 import json
 import random
+import hmac
+import hashlib
 
 
 ocb = Blueprint('webhooks', __name__)
@@ -68,16 +70,16 @@ def chat_hook():
                 send_private_chat(user_id, f'{display_name}, you have {porps(points)}.')
 
         elif lowercase_msg.startswith(f'{prefix}reg_mail'):  # Generate a code to verify users account for email registration
-            if current_app.config['KOFI_INTEGRATION']:
+            if current_app.config['KOFI_SETTINGS']['integration'] or current_app.config['GB_SETTINGS']['integration']:
                 mail_reg_code = get_email_code(db, user_id)
                 if mail_reg_code:  # If the viewer already has a code waiting
-                    send_private_chat(user_id, f'{display_name}, your code is {mail_reg_code}. Enter it into the form on the Stream Rewards Info page, with your email address, to enable Kofi perks!')
+                    send_private_chat(user_id, f'{display_name}, your code is {mail_reg_code}. Enter it into the form on the Stream Rewards Info page, with your email address, to enable donation perks!')
                 else:  # if not
                     mail_reg_code = random.randint(100000, 999999)
                     if set_email_code(db, user_id, mail_reg_code):
-                        send_private_chat(user_id, f'{display_name}, your code is {mail_reg_code}. Enter it into the form on the Stream Rewards Info page, with your email address, to enable Kofi perks!')
+                        send_private_chat(user_id, f'{display_name}, your code is {mail_reg_code}. Enter it into the form on the Stream Rewards Info page, with your email address, to enable donation perks!')
             else:
-                send_chat(f'{display_name}, Kofi integration is not enabled on this stream.')                
+                send_chat(f'{display_name}, donation integration is not enabled on this stream.')
 
         elif lowercase_msg.startswith(f'{prefix}rewards'):  # Send rewards list
             if current_app.config['REWARDS']:
@@ -111,68 +113,159 @@ def kofi_hook():
     current_app.logger.info(f'Kofi request')
     if request.content_type == 'application/x-www-form-urlencoded':
         raw_data = request.form.get('data')  # Get the kofi data
-        if raw_data:
-            raw_data = json.loads(raw_data)
-            is_authed = raw_data['verification_token']
-            if is_authed == current_app.config['KOFI_TOKEN']:
-                type = raw_data['type']
-                is_public = raw_data['is_public']
-                new_sub = raw_data['is_first_subscription_payment']
-                message = raw_data['message']
-                shop_items = raw_data['shop_items']
-                from_name = raw_data['from_name']
-                email = raw_data['email']
-                amount = raw_data['amount']
-                sub_payment = raw_data['is_subscription_payment']
-                first_sub = raw_data['is_first_subscription_payment']
-                tier_name = raw_data['tier_name']
-                if type == 'Shop Order':
-                    current_app.logger.info(f'{from_name} purchased {format(shop_items)}\nMessage: {message}\n')
-                if type == 'Donation':
-                    donation_info = [is_public, from_name, email, amount, message]
-                    donation_points = current_app.config['KOFI_SETTINGS']['donation_points']
-                    if accept_donation(donation_info, donation_points):
-                        if is_public:
-                            alert_info = {'name': from_name, 'amount': amount}
-                        else:
-                            alert_info = {'name': 'Anonymous Hero', 'amount': amount}
-                if type == 'Subscription':
-                    if current_app.config['KOFI_SETTINGS']['subs']:  # Check that subscriptions are enabled
-                        if first_sub:
-                            if tier_name:
-                                current_app.logger.info(f'{from_name} <{email}> subscribed as a {tier_name} tier member.')
-                            else:
-                                current_app.logger.info(f'{from_name} <{email}> subscribed.')
-                        else:
-                            if tier_name:
-                                current_app.logger.info(f'{from_name} <{email}> renewed their {tier_name} tier membership.')
+        if current_app.config['KOFI_SETTINGS']['integration']:
+            if raw_data:
+                raw_data = json.loads(raw_data)
+                is_authed = raw_data['verification_token']
+                if is_authed == current_app.config['KOFI_SETTINGS']['token']:
+                    type = raw_data['type']
+                    is_public = raw_data['is_public']
+                    new_sub = raw_data['is_first_subscription_payment']
+                    message = raw_data['message']
+                    shop_items = raw_data['shop_items']
+                    from_name = raw_data['from_name']
+                    email = raw_data['email']
+                    amount = raw_data['amount']
+                    sub_payment = raw_data['is_subscription_payment']
+                    first_sub = raw_data['is_first_subscription_payment']
+                    tier_name = raw_data['tier_name']
+                    if type == 'Shop Order':
+                        current_app.logger.info(f'{from_name} purchased {format(shop_items)}\nMessage: {message}\n')
+                    if type == 'Donation':
+                        donation_info = [is_public, from_name, email, amount, message]
+                        donation_points = current_app.config['KOFI_SETTINGS']['donation_points']
+                        if accept_donation(donation_info, donation_points, 'Kofi'):
+                            current_app.logger.info('Donation processed.')
+                    if type == 'Subscription':
+                        if current_app.config['KOFI_SETTINGS']['subs']:  # Check that subscriptions are enabled
+                            if first_sub:
+                                if tier_name:
+                                    current_app.logger.info(f'{from_name} <{email}> subscribed as a {tier_name} tier member.')
+                                else:
+                                    current_app.logger.info(f'{from_name} <{email}> subscribed.')
                             else:
-                                current_app.logger.info(f'{from_name} <{email}> renewed their membership.')
+                                if tier_name:
+                                    current_app.logger.info(f'{from_name} <{email}> renewed their {tier_name} tier membership.')
+                                else:
+                                    current_app.logger.info(f'{from_name} <{email}> renewed their membership.')
 
-                        sub_info = [is_public, from_name, email, amount, message, first_sub, tier_name]
-                        sub_points = current_app.config['KOFI_SETTINGS']['sub_points']
-                        if accept_sub(sub_info, sub_points):
-                            if is_public:
-                                alert_info = {'name': from_name, 'tiername': tier_name}
-                            else:
-                                alert_info = {'name': 'Anonymous Hero', 'teirname': tier_name}
-                    else:
-                        current_app.logger.info(f'Kofi membership received, but subscriptions are not enabled. Doing nothing.')
-                return jsonify({'status': 'success'}), 200
+                            sub_info = [is_public, from_name, email, amount, message, first_sub, tier_name]
+                            sub_points = current_app.config['KOFI_SETTINGS']['sub_points']
+                            if accept_kofi_sub(sub_info, sub_points):
+                                current_app.logger.info('Subscription processed.')
+                        else:
+                            current_app.logger.info(f'Kofi membership received, but subscriptions are not enabled. Doing nothing.')
+                    return jsonify({'status': 'success'}), 200
+                else:
+                    current_app.logger.info(f'Token invalid. Rejecting.')
+                    return jsonify({'status': 'unauthorized'}), 401
             else:
-                current_app.logger.info(f'Token invalid. Rejecting.')
-                return jsonify({'status': 'unauthorized'}), 401
+                return jsonify({'status': 'Failed. No data'}), 400
+            return jsonify({'status': 'success'}), 200
         else:
-            return jsonify({'status': 'Failed. No data'}), 400
-        return jsonify({'status': 'success'}), 200
+            current_app.logger.error(f'Kofi donation recieved, but Kofi integration is turned off. Doing nothing.')
+            return jsonify({'status': 'Failed. Not currently accepting Kofi donations.'}), 400
     else:
         return jsonify({'status': 'Failed. Invalid content type'}), 400
 
 
+def sign_payload(payload, secret):  # For TESTING purposes
+    test_payload = hmac.new(
+        secret.encode(),
+        payload.encode(),
+        hashlib.sha256
+    ).hexdigest()
+    current_app.logger.info(f'\n\nTest encoded payload output: {test_payload}\n\n')
+    return test_payload
+
+
+# Not currently in use by GB, but it was. Maybe in the future?
+# Leaving it here just in case.
+def verify_gbhook_signature(payload, signature, secret):
+    current_app.logger.info(f'\n\nRecieved Signature: {signature}\n\n')
+    expected_signature = hmac.new(
+        secret.encode(),
+        payload.encode(),
+        hashlib.sha256
+    ).hexdigest()
+    do_sig_check = hmac.compare_digest(signature, expected_signature)
+    current_app.logger.debug(f'\n\nExpected Signature: {expected_signature}\nResult: {do_sig_check}\n\n')
+    return do_sig_check
+
+
+@ocb.route('/gbHook', methods=['POST'])
+def gb_hook():
+    current_app.logger.info(f'----------------------------------------------------------------------------')
+    current_app.logger.info(f'GiveButter request')
+
+    headers = request.headers
+    current_app.logger.debug('Headers:')
+    for header, value in headers.items():
+        current_app.logger.debug(f'> {header}: {value}')
+
+    signature = request.headers.get('Signature')
+    gb_secret = current_app.config['GB_SETTINGS']['secret']
+
+    raw_data = request.get_data(as_text=True)
+
+    current_app.logger.debug(f'\n\n{raw_data}\n\n')
+
+    event = request.json
+
+    if signature == gb_secret:
+        if current_app.config['GB_SETTINGS']['integration']:
+            try:
+                event_type = event['event']
+                if event_type == 'transaction.succeeded':
+                    data = event['data']
+                    custom_fields = data["custom_fields"]
+                    if custom_fields:
+                        for field in custom_fields:  # Get anonymous donation custom field, if it's there'
+                            if field['title'].lower() == 'anonymous':
+                                if field['value'].lower() == "yes":
+                                    is_public = False
+                                    current_app.logger.info(f'Donation is anonymous.')
+                                else:
+                                    is_public = True
+                                    current_app.logger.info(f'Donation is not anonymous.')
+                            else:  # If it's not there, set to anonymous
+                                is_public = False
+                                current_app.logger.info(f'No anoymous donation custom field found. Assuming anonymous')
+                    else:  # No custom fields found, set to anonymous
+                        is_public = False
+                        current_app.logger.info(f'No custom fields found. Assuming anonymous')
+
+                    from_name = f'{data["first_name"]} {data["last_name"]}'
+                    email = data['email']
+                    amount = data['amount']
+                    current_app.logger.debug(f'\nFrom: {from_name}\nEmail: {email}\nAmount: {amount}\nAnonymous: {is_public}\n\n')
+                    donation_info = [is_public, from_name, email, amount, '']
+                    donation_points = current_app.config['GB_SETTINGS']['donation_points']
+                    points_for_donations = current_app.config['GB_SETTINGS']['donations']
+                    if points_for_donations:  # Are we giving points for donations?
+                        if accept_donation(donation_info, donation_points, 'GiveButter'):
+                            current_app.logger.info(f'Donation processed.')
+                    else:
+                        current_app.logger.info(f'Points for donations is disabled. Doing nothing.')
+                else:
+                    current_app.logger.info(f'Unhandled event type: {event_type}')
+            except Exception as pgberror:
+                current_app.logger.error(f'General exception processing gbhook: {pgberror}')
+
+        else:
+            current_app.logger.error(f'GiveButter donation recieved, but GiveButter integration is turned off. Doing nothing.')
+            return jsonify({'status': 'Failed. Not currently accepting GiveButter donations.'}), 400
+
+        return jsonify({'status': 'Success'}), 200  # If signature matched
+
+    else:
+        return jsonify({'status': 'Signature invalid'}), 401  # If signature didn't match
+
+
 @ocb.route('/checkFollows')  # Polled by follower.html template to check for new followers
 def check_follows():
     alerts_dict = current_app.config['ALERTS']
-    follower = {'name': alerts_dict['follower'], 'reward': 'New Follow!'}
+    follower = {'name': alerts_dict['follower']}
     if follower['name']:
         current_app.logger.info(f'New follower: \"{follower["name"]}\"')
         alerts_dict['follower'] = ''
@@ -184,12 +277,12 @@ def check_follows():
     return jsonify(None)
 
 
-@ocb.route('/checkGoals')  # Polled by ocbalert.html template to check for new followers
+@ocb.route('/checkGoals')  # Polled by rgoal.html template to check for if a goal has been passed
 def check_goals():
     alerts_dict = current_app.config['ALERTS']
-    rgoals = {'name': alerts_dict['g_name'], 'reward': 'GOAL!!'}
+    rgoals = {'name': alerts_dict['g_name']}
     if rgoals['name']:
-        current_app.logger.debug(rgoals)
+        current_app.logger.info(rgoals)
         alerts_dict['g_name'] = ''
         alerts_dict['g_reward'] = ''
         save_alerts(alerts_dict)
@@ -199,10 +292,10 @@ def check_goals():
     return jsonify(None)
 
 
-@ocb.route('/checkMilestones')  # Polled by ocbalert.html template to check for new followers
+@ocb.route('/checkMilestones')  # Polled by rmilestone.html template to check if a milestone has been passed
 def check_milestones():
     alerts_dict = current_app.config['ALERTS']
-    rmilestones = {'name': alerts_dict['m_name'], 'reward': 'Milestone!'}
+    rmilestones = {'name': alerts_dict['m_name']}
     if rmilestones['name']:
         current_app.logger.info(rmilestones)
         alerts_dict['m_name'] = ''
@@ -212,3 +305,17 @@ def check_milestones():
     else:
         current_app.logger.debug(f'No new milestones passed')
     return jsonify(None)
+
+
+@ocb.route('/checkGBs')  # Polled by rgbdonation.html template to check for new givebutter donations
+def check_gbs():
+    alerts_dict = current_app.config['ALERTS']
+    rgbs = {'name': alerts_dict['gb_name']}
+    if rgbs['name']:
+        current_app.logger.info(rgbs)
+        alerts_dict['gb_name'] = ''
+        save_alerts(alerts_dict)
+        return jsonify(rgbs)
+    else:
+        current_app.logger.debug(f'No GiveButter donation')
+    return jsonify(None)

+ 1 - 1
pyproject.toml

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "ownchatbot"
-version = "1.1.8"
+version = "1.1.9"
 authors = [
     {name = "DeadTOm", email = "deadtom@deadtom.me"},
 ]

BIN
screenshots/mgmtpanel03.png


BIN
screenshots/mgmtpanel04.png


BIN
screenshots/mgmtpanel05.png


BIN
screenshots/mgmtpanel07.png


+ 1 - 1
setup.py

@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
 
 setup(
     name='ownchatbot',
-    version='1.1.8',
+    version='1.1.9',
     packages=find_packages(),
     include_package_data=True,
     install_requires=[

+ 20 - 16
upgrade.sh

@@ -13,25 +13,29 @@ update_modules() {
 }
 
 update_config() {
-    local bak_file="instance/config.py.bak"
+    KOFI_TOKEN=$(grep "^KOFI_TOKEN" instance/config.py | cut -d'=' -f2 | cut -d'#' -f1 | tr -d '[:space:]')  # Get the value of KOFI_TOKEN
+    if [ "$KOFI_TOKEN" = "False" ]; then  # So bash doesn't blank the variable in kofi.py if it's False
+        KOFI_TOKEN="False" 
+    fi
+    if [ "$KOFI_INTEGRATION" = "False" ]; then  # Likewise
+        KOFI_INTEGRATION="False"
+    fi
+    if [ -z "$KOFI_INTEGRATION" ]; then  # Due to a bug I missed, the variable may be blank. If so, set to False
+        KOFI_INTEGRATION="False"
+    fi
+    KOFI_INTEGRATION=$(grep "^KOFI_INTEGRATION" instance/config.py | cut -d'=' -f2 | cut -d'#' -f1 | tr -d '[:space:]')  # Get the value of KOFI_INTEGRATION    
+    sed -i "s/}/, \'token\': ${KOFI_TOKEN}, \'integration\': ${KOFI_INTEGRATION}}/" instance/kofi.py  # Append those values to kofi.py 
+    sed -i "/^KOFI_TOKEN/d" instance/config.py  # Delete KOFI_TOKEN line from config.py
+    sed -i "/^KOFI_INTEGRATION/d" instance/config.py  # Delete KOFI_INTEGRATION line from config.py
 
-    read -p "Enter the external URL your OwnchatBot uses, with \"https://\": " OCB_URL
+    sed -i "s/}/,\"gb_name\"": \"\"}/" instance/alerts.py  # Append those values to kofi.py
     
-    if [[ -n "$OCB_URL" ]]; then  # Check if OCB_URL is not empty
-        echo "" >> instance/config.py  # Append an empty line so it formats correctly
-        echo "OCB_URL = '$OCB_URL'" >> instance/config.py  # Append the new line
-        echo "Set OCB_URL successfully."
-        cp instance/config.py "$bak_file"
-        rm "$bak_file"  # Remove the .bak file if the update was successful
-    else
-        echo "Failed to set OCB_URL. Please provide a valid URL."
-        exit 1  # Exit the script with a non-zero status
-    fi
+    cp ownchatbot/defaults/givebutter.py instance/  # Copy the default givebutter config file into instance/
 }
 
-# activate_venv
-# update_modules
+activate_venv
+update_modules
+deactivate
 update_config
-# deactivate
 
-echo -e "Your OwnchatBot configuration has been upgraded. Happy streaming!"
+echo -e "\nYour OwnchatBot configuration has been upgraded. Look for GiveButter integration settings in the OCB Management Panel.\nHappy streaming!"