mirror of
				https://github.com/hrfee/jellyfin-accounts.git
				synced 2025-10-31 18:19:34 +00:00 
			
		
		
		
	Added per-invite notifications for expiry and user creation
Notifications must be enabled in settings; they can then be toggled in the dropdown menu of each invite.
This commit is contained in:
		
							parent
							
								
									e80b233af2
								
							
						
					
					
						commit
						b8fdb64f68
					
				| @ -290,13 +290,6 @@ def main(): | |||||||
|                 success = True |                 success = True | ||||||
| 
 | 
 | ||||||
|     else: |     else: | ||||||
| 
 |  | ||||||
|         def signal_handler(sig, frame): |  | ||||||
|             print("Quitting...") |  | ||||||
|             sys.exit(0) |  | ||||||
| 
 |  | ||||||
|         signal.signal(signal.SIGINT, signal_handler) |  | ||||||
|         signal.signal(signal.SIGTERM, signal_handler) |  | ||||||
|         global app |         global app | ||||||
|         app = Flask(__name__, root_path=str(local_dir)) |         app = Flask(__name__, root_path=str(local_dir)) | ||||||
|         app.config["DEBUG"] = config.getboolean("ui", "debug") |         app.config["DEBUG"] = config.getboolean("ui", "debug") | ||||||
| @ -306,6 +299,13 @@ def main(): | |||||||
|         from waitress import serve |         from waitress import serve | ||||||
| 
 | 
 | ||||||
|         if first_run: |         if first_run: | ||||||
|  | 
 | ||||||
|  |             def signal_handler(sig, frame): | ||||||
|  |                 print("Quitting...") | ||||||
|  |                 sys.exit(0) | ||||||
|  | 
 | ||||||
|  |             signal.signal(signal.SIGINT, signal_handler) | ||||||
|  |             signal.signal(signal.SIGTERM, signal_handler) | ||||||
|             import jellyfin_accounts.setup |             import jellyfin_accounts.setup | ||||||
| 
 | 
 | ||||||
|             host = config["ui"]["host"] |             host = config["ui"]["host"] | ||||||
| @ -315,6 +315,7 @@ def main(): | |||||||
|         else: |         else: | ||||||
|             import jellyfin_accounts.web_api |             import jellyfin_accounts.web_api | ||||||
|             import jellyfin_accounts.web |             import jellyfin_accounts.web | ||||||
|  |             import jellyfin_accounts.invite_daemon | ||||||
| 
 | 
 | ||||||
|             host = config["ui"]["host"] |             host = config["ui"]["host"] | ||||||
|             port = config["ui"]["port"] |             port = config["ui"]["port"] | ||||||
| @ -330,4 +331,12 @@ def main(): | |||||||
|                 log.info("Starting email thread") |                 log.info("Starting email thread") | ||||||
|                 pwr.start() |                 pwr.start() | ||||||
| 
 | 
 | ||||||
|  |             def signal_handler(sig, frame): | ||||||
|  |                 print("Quitting...") | ||||||
|  |                 if config.getboolean("notifications", "enabled"): | ||||||
|  |                     jellyfin_accounts.invite_daemon.inviteDaemon.stop() | ||||||
|  |                 sys.exit(0) | ||||||
|  | 
 | ||||||
|  |             signal.signal(signal.SIGINT, signal_handler) | ||||||
|  |             signal.signal(signal.SIGTERM, signal_handler) | ||||||
|             serve(app, host=host, port=int(port)) |             serve(app, host=host, port=int(port)) | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ class Config: | |||||||
| 
 | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def load_config(config_path, data_dir, local_dir, log): |     def load_config(config_path, data_dir, local_dir, log): | ||||||
|  |         # Lord forgive me for this mess | ||||||
|         config = configparser.RawConfigParser() |         config = configparser.RawConfigParser() | ||||||
|         config.read(config_path) |         config.read(config_path) | ||||||
|         for key in config["files"]: |         for key in config["files"]: | ||||||
| @ -64,6 +65,31 @@ class Config: | |||||||
|             config["jellyfin"]["public_server"] = config["jellyfin"]["server"] |             config["jellyfin"]["public_server"] = config["jellyfin"]["server"] | ||||||
|         if "bs5" not in config["ui"] or config["ui"]["bs5"] == "": |         if "bs5" not in config["ui"] or config["ui"]["bs5"] == "": | ||||||
|             config["ui"]["bs5"] = "false" |             config["ui"]["bs5"] = "false" | ||||||
|  |         if ( | ||||||
|  |             "expiry_html" not in config["notifications"] | ||||||
|  |             or config["notifications"]["expiry_html"] == "" | ||||||
|  |         ): | ||||||
|  |             log.debug("Using default expiry notification HTML template") | ||||||
|  |             config["notifications"]["expiry_html"] = str(local_dir / "expired.html") | ||||||
|  |         if ( | ||||||
|  |             "expiry_text" not in config["notifications"] | ||||||
|  |             or config["notifications"]["expiry_text"] == "" | ||||||
|  |         ): | ||||||
|  |             log.debug("Using default expiry notification plaintext template") | ||||||
|  |             config["notifications"]["expiry_text"] = str(local_dir / "expired.txt") | ||||||
|  |         if ( | ||||||
|  |             "created_html" not in config["notifications"] | ||||||
|  |             or config["notifications"]["created_html"] == "" | ||||||
|  |         ): | ||||||
|  |             log.debug("Using default user creation notification HTML template") | ||||||
|  |             config["notifications"]["created_html"] = str(local_dir / "created.html") | ||||||
|  |         if ( | ||||||
|  |             "created_text" not in config["notifications"] | ||||||
|  |             or config["notifications"]["created_text"] == "" | ||||||
|  |         ): | ||||||
|  |             log.debug("Using default user creation notification plaintext template") | ||||||
|  |             config["notifications"]["created_text"] = str(local_dir / "created.txt") | ||||||
|  | 
 | ||||||
|         return config |         return config | ||||||
| 
 | 
 | ||||||
|     def __init__(self, file, instance, data_dir, local_dir, log): |     def __init__(self, file, instance, data_dir, local_dir, log): | ||||||
|  | |||||||
| @ -133,6 +133,15 @@ | |||||||
|             "value": "your password", |             "value": "your password", | ||||||
|             "description": "Password for admin page (Leave blank if using jellyfin_login)" |             "description": "Password for admin page (Leave blank if using jellyfin_login)" | ||||||
|         }, |         }, | ||||||
|  |         "email": { | ||||||
|  |             "name": "Admin email address", | ||||||
|  |             "required": false, | ||||||
|  |             "requires_restart": false, | ||||||
|  |             "depends_false": "jellyfin_login", | ||||||
|  |             "type": "text", | ||||||
|  |             "value": "example@example.com", | ||||||
|  |             "description": "Address to send notifications to (Leave blank if using jellyfin_login)" | ||||||
|  |         }, | ||||||
|         "debug": { |         "debug": { | ||||||
|             "name": "Debug logging", |             "name": "Debug logging", | ||||||
|             "required": false, |             "required": false, | ||||||
| @ -391,6 +400,56 @@ | |||||||
|             "description": "Base URL for jf-accounts. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself." |             "description": "Base URL for jf-accounts. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself." | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|  |     "notifications": { | ||||||
|  |         "meta": { | ||||||
|  |             "name": "Notifications", | ||||||
|  |             "description": "Notification related settings." | ||||||
|  |         }, | ||||||
|  |         "enabled": { | ||||||
|  |             "name": "Enabled", | ||||||
|  |             "required": "false", | ||||||
|  |             "requires_restart": true, | ||||||
|  |             "type": "bool", | ||||||
|  |             "value": true, | ||||||
|  |             "description": "Enabling adds optional toggles to invites to notify on expiry and user creation." | ||||||
|  |         }, | ||||||
|  |         "expiry_html": { | ||||||
|  |             "name": "Expiry email (HTML)", | ||||||
|  |             "required": false, | ||||||
|  |             "requires_restart": false, | ||||||
|  |             "depends_true": "enabled", | ||||||
|  |             "type": "text", | ||||||
|  |             "value": "", | ||||||
|  |             "description": "Path to expiry notification email HTML." | ||||||
|  |         }, | ||||||
|  |         "expiry_text": { | ||||||
|  |             "name": "Expiry email (Plaintext)", | ||||||
|  |             "required": false, | ||||||
|  |             "requires_restart": "false", | ||||||
|  |             "depends_true": "enabled", | ||||||
|  |             "type": "text", | ||||||
|  |             "value": "", | ||||||
|  |             "description": "Path to expiry notification email in plaintext." | ||||||
|  |         }, | ||||||
|  |         "created_html": { | ||||||
|  |             "name": "User created email (HTML)", | ||||||
|  |             "required": false, | ||||||
|  |             "requires_restart": false, | ||||||
|  |             "depends_true": "enabled", | ||||||
|  |             "type": "text", | ||||||
|  |             "value": "", | ||||||
|  |             "description": "Path to user creation notification email HTML." | ||||||
|  |         }, | ||||||
|  |         "created_text": { | ||||||
|  |             "name": "User created email (Plaintext)", | ||||||
|  |             "required": false, | ||||||
|  |             "requires_restart": false, | ||||||
|  |             "depends_true": "enabled", | ||||||
|  |             "type": "text", | ||||||
|  |             "value": "", | ||||||
|  |             "description": "Path to user creation notification email in plaintext." | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|     "mailgun": { |     "mailgun": { | ||||||
|         "meta": { |         "meta": { | ||||||
|             "name": "Mailgun (Email)", |             "name": "Mailgun (Email)", | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								jellyfin_accounts/data/created.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								jellyfin_accounts/data/created.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | A user was created using code {{ code }}. | ||||||
|  | 
 | ||||||
|  | Name: {{ username }} | ||||||
|  | Address: {{ address }} | ||||||
|  | Time: {{ time }} | ||||||
|  | 
 | ||||||
|  | Note: Notification emails can be toggled on the admin dashboard. | ||||||
							
								
								
									
										10
									
								
								jellyfin_accounts/data/email.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								jellyfin_accounts/data/email.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | Hi {{ username }}, | ||||||
|  | 
 | ||||||
|  | Someone has recently requests a password reset on Jellyfin. | ||||||
|  | If this was you, enter the below pin into the prompt. | ||||||
|  | This code will expire on {{ expiry_date }}, at {{ expiry_time }} , which is in {{ expires_in }}. | ||||||
|  | If this wasn't you, please ignore this email. | ||||||
|  | 
 | ||||||
|  | PIN: {{ pin }} | ||||||
|  | 
 | ||||||
|  | {{ message }} | ||||||
							
								
								
									
										5
									
								
								jellyfin_accounts/data/expired.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								jellyfin_accounts/data/expired.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | Invite expired. | ||||||
|  | 
 | ||||||
|  | Code {{ code }} expired at {{ expiry }}. | ||||||
|  | 
 | ||||||
|  | Note: Notification emails can be toggled on the admin dashboard. | ||||||
							
								
								
									
										8
									
								
								jellyfin_accounts/data/invite-email.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								jellyfin_accounts/data/invite-email.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | Hi, | ||||||
|  | You've been invited to Jellyfin. | ||||||
|  | To join, follow the below link. | ||||||
|  | This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick. | ||||||
|  | 
 | ||||||
|  | {{ invite_link }} | ||||||
|  | 
 | ||||||
|  | {{ message }} | ||||||
| @ -158,7 +158,7 @@ var userDefaultsModal = createModal('userDefaults'); | |||||||
| var usersModal = createModal('users'); | var usersModal = createModal('users'); | ||||||
| var restartModal = createModal('restartModal'); | var restartModal = createModal('restartModal'); | ||||||
| 
 | 
 | ||||||
| // Parsed invite: [<code>, <expires in _>, <1: Empty invite (no delete/link), 0: Actual invite>, <email address>, <remaining uses>, [<used-by>], <date created>]
 | // Parsed invite: [<code>, <expires in _>, <1: Empty invite (no delete/link), 0: Actual invite>, <email address>, <remaining uses>, [<used-by>], <date created>, <notify on expiry>, <notify on creation>]
 | ||||||
| function parseInvite(invite, empty = false) { | function parseInvite(invite, empty = false) { | ||||||
|     if (empty) { |     if (empty) { | ||||||
|         return ["None", "", 1]; |         return ["None", "", 1]; | ||||||
| @ -185,6 +185,12 @@ function parseInvite(invite, empty = false) { | |||||||
|     if ('created' in invite) { |     if ('created' in invite) { | ||||||
|         i[6] = invite['created']; |         i[6] = invite['created']; | ||||||
|     } |     } | ||||||
|  |     if ('notify-expiry' in invite) { | ||||||
|  |         i[7] = invite['notify-expiry']; | ||||||
|  |     } | ||||||
|  |     if ('notify-creation' in invite) { | ||||||
|  |         i[8] = invite['notify-creation']; | ||||||
|  |     } | ||||||
|     return i; |     return i; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -292,6 +298,78 @@ function addItem(parsedInvite) { | |||||||
|         dropdownLeft.appendChild(leftList); |         dropdownLeft.appendChild(leftList); | ||||||
|         dropdownContent.appendChild(dropdownLeft); |         dropdownContent.appendChild(dropdownLeft); | ||||||
|          |          | ||||||
|  |         {% if notifications %} | ||||||
|  |         let dropdownMiddle = document.createElement('div'); | ||||||
|  |         dropdownMiddle.id = parsedInvite[0] + '_notifyButtons'; | ||||||
|  |         dropdownMiddle.classList.add('col'); | ||||||
|  | 
 | ||||||
|  |         let middleList = document.createElement('ul'); | ||||||
|  |         middleList.classList.add('list-group', 'list-group-flush'); | ||||||
|  |         middleList.textContent = 'Notify on:'; | ||||||
|  | 
 | ||||||
|  |         let notifyExpiry = document.createElement('li'); | ||||||
|  |         notifyExpiry.classList.add('list-group-item', 'py-1', 'form-check'); | ||||||
|  |         notifyExpiry.innerHTML = ` | ||||||
|  |         <input class="form-check-input" type="checkbox" value="" id="${parsedInvite[0]}_notifyExpiry"> | ||||||
|  |         <label class="form-check-label" for="${parsedInvite[0]}_notifyExpiry">Expiry</label> | ||||||
|  |         `;
 | ||||||
|  |         if (typeof(parsedInvite[7]) == 'boolean') { | ||||||
|  |             notifyExpiry.getElementsByTagName('input')[0].checked = parsedInvite[7]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         notifyExpiry.getElementsByTagName('input')[0].onclick = function() { | ||||||
|  |             let req = new XMLHttpRequest(); | ||||||
|  |             var thisEl = this; | ||||||
|  |             let send = {}; | ||||||
|  |             let code = thisEl.id.replace('_notifyExpiry', ''); | ||||||
|  |             send[code] = {}; | ||||||
|  |             send[code]['notify-expiry'] = thisEl.checked; | ||||||
|  |             req.open("POST", "/setNotify", true); | ||||||
|  |             req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); | ||||||
|  |             req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); | ||||||
|  |             req.onreadystatechange = function() { | ||||||
|  |                 if (this.readyState == 4 && this.status != 200) { | ||||||
|  |                     thisEl.checked = !thisEl.checked; | ||||||
|  |                 } | ||||||
|  |             }; | ||||||
|  |             req.send(JSON.stringify(send)); | ||||||
|  |         }; | ||||||
|  |         middleList.appendChild(notifyExpiry); | ||||||
|  | 
 | ||||||
|  |         let notifyCreation = document.createElement('li'); | ||||||
|  |         notifyCreation.classList.add('list-group-item', 'py-1', 'form-check'); | ||||||
|  |         notifyCreation.innerHTML = ` | ||||||
|  |         <input class="form-check-input" type="checkbox" value="" id="${parsedInvite[0]}_notifyCreation"> | ||||||
|  |         <label class="form-check-label" for="${parsedInvite[0]}_notifyCreation">User creation</label> | ||||||
|  |         `;
 | ||||||
|  |         if (typeof(parsedInvite[8]) == 'boolean') { | ||||||
|  |             notifyCreation.getElementsByTagName('input')[0].checked = parsedInvite[8]; | ||||||
|  |         } | ||||||
|  |         notifyCreation.getElementsByTagName('input')[0].onclick = function() { | ||||||
|  |             let req = new XMLHttpRequest(); | ||||||
|  |             var thisEl = this; | ||||||
|  |             let send = {}; | ||||||
|  |             let code = thisEl.id.replace('_notifyCreation', ''); | ||||||
|  |             send[code] = {}; | ||||||
|  |             send[code]['notify-creation'] = thisEl.checked; | ||||||
|  |             req.open("POST", "/setNotify", true); | ||||||
|  |             req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); | ||||||
|  |             req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); | ||||||
|  |             req.onreadystatechange = function() { | ||||||
|  |                 if (this.readyState == 4 && this.status != 200) { | ||||||
|  |                     thisEl.checked = !thisEl.checked; | ||||||
|  |                 } | ||||||
|  |             }; | ||||||
|  |             req.send(JSON.stringify(send)); | ||||||
|  |         }; | ||||||
|  |         middleList.appendChild(notifyCreation); | ||||||
|  | 
 | ||||||
|  |         dropdownMiddle.appendChild(middleList); | ||||||
|  |         dropdownContent.appendChild(dropdownMiddle); | ||||||
|  | 
 | ||||||
|  |         {% endif %} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|         let dropdownRight = document.createElement('div'); |         let dropdownRight = document.createElement('div'); | ||||||
|         dropdownRight.id = parsedInvite[0] + '_usersCreated'; |         dropdownRight.id = parsedInvite[0] + '_usersCreated'; | ||||||
|         dropdownRight.classList.add('col'); |         dropdownRight.classList.add('col'); | ||||||
|  | |||||||
| @ -43,7 +43,10 @@ class JSONFile(dict): | |||||||
|     def __delitem__(self, key): |     def __delitem__(self, key): | ||||||
|         data = self.readJSON(self.path) |         data = self.readJSON(self.path) | ||||||
|         super(JSONFile, self).__init__(data) |         super(JSONFile, self).__init__(data) | ||||||
|         del data[key] |         try: | ||||||
|  |             del data[key] | ||||||
|  |         except KeyError: | ||||||
|  |             pass | ||||||
|         self.writeJSON(self.path, data) |         self.writeJSON(self.path, data) | ||||||
|         super(JSONFile, self).__delitem__(key) |         super(JSONFile, self).__delitem__(key) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -13,6 +13,15 @@ from jellyfin_accounts import config | |||||||
| from jellyfin_accounts import email_log as log | from jellyfin_accounts import email_log as log | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def format_datetime(dt): | ||||||
|  |     result = dt.strftime(config["email"]["date_format"]) | ||||||
|  |     if config.getboolean("email", "use_24h"): | ||||||
|  |         result += f' {dt.strftime("%H:%M")}' | ||||||
|  |     else: | ||||||
|  |         result += f' {dt.strftime("%I:%M %p")}' | ||||||
|  |     return result | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class Email: | class Email: | ||||||
|     def __init__(self, address): |     def __init__(self, address): | ||||||
|         self.address = address |         self.address = address | ||||||
| @ -75,6 +84,47 @@ class Email: | |||||||
|             self.content[key] = c |             self.content[key] = c | ||||||
|             log.info(f"{self.address}: {key} constructed") |             log.info(f"{self.address}: {key} constructed") | ||||||
| 
 | 
 | ||||||
|  |     def construct_expiry(self, invite): | ||||||
|  |         self.subject = "Notice: Invite expired" | ||||||
|  |         log.debug(f'Constructing expiry notification for {invite["code"]}') | ||||||
|  |         expiry = format_datetime(invite["expiry"]) | ||||||
|  |         for key in ["text", "html"]: | ||||||
|  |             sp = Path(config["notifications"]["expiry_" + key]) / ".." | ||||||
|  |             sp = str(sp.resolve()) + "/" | ||||||
|  |             template_loader = FileSystemLoader(searchpath=sp) | ||||||
|  |             template_env = Environment(loader=template_loader) | ||||||
|  |             fname = Path(config["notifications"]["expiry_" + key]).name | ||||||
|  |             template = template_env.get_template(fname) | ||||||
|  |             c = template.render(code=invite["code"], expiry=expiry) | ||||||
|  |             self.content[key] = c | ||||||
|  |             log.info(f"{self.address}: {key} constructed") | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     def construct_created(self, invite): | ||||||
|  |         self.subject = "Notice: User created" | ||||||
|  |         log.debug(f'Constructing expiry notification for {invite["code"]}') | ||||||
|  |         created = format_datetime(invite["created"]) | ||||||
|  |         if config.getboolean("email", "no_username"): | ||||||
|  |             email = "n/a" | ||||||
|  |         else: | ||||||
|  |             email = invite["address"] | ||||||
|  |         for key in ["text", "html"]: | ||||||
|  |             sp = Path(config["notifications"]["created_" + key]) / ".." | ||||||
|  |             sp = str(sp.resolve()) + "/" | ||||||
|  |             template_loader = FileSystemLoader(searchpath=sp) | ||||||
|  |             template_env = Environment(loader=template_loader) | ||||||
|  |             fname = Path(config["notifications"]["created_" + key]).name | ||||||
|  |             template = template_env.get_template(fname) | ||||||
|  |             c = template.render( | ||||||
|  |                 code=invite["code"], | ||||||
|  |                 username=invite["username"], | ||||||
|  |                 address=email, | ||||||
|  |                 time=created, | ||||||
|  |             ) | ||||||
|  |             self.content[key] = c | ||||||
|  |             log.info(f"{self.address}: {key} constructed") | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|     def construct_reset(self, reset): |     def construct_reset(self, reset): | ||||||
|         self.subject = config["password_resets"]["subject"] |         self.subject = config["password_resets"]["subject"] | ||||||
|         log.debug(f"{self.address}: Using subject {self.subject}") |         log.debug(f"{self.address}: Using subject {self.subject}") | ||||||
|  | |||||||
							
								
								
									
										42
									
								
								jellyfin_accounts/invite_daemon.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								jellyfin_accounts/invite_daemon.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | |||||||
|  | from threading import Timer | ||||||
|  | import time | ||||||
|  | from jellyfin_accounts import config, data_store | ||||||
|  | from jellyfin_accounts.web_api import checkInvite | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Repeat: | ||||||
|  |     def __init__(self, interval, function, *args, **kwargs): | ||||||
|  |         self._timer = None | ||||||
|  |         self.interval = interval | ||||||
|  |         self.function = function | ||||||
|  |         self.args = args | ||||||
|  |         self.kwargs = kwargs | ||||||
|  |         self.is_running = False | ||||||
|  |         self.next_call = time.time() | ||||||
|  |         self.start() | ||||||
|  | 
 | ||||||
|  |     def _run(self): | ||||||
|  |         self.is_running = False | ||||||
|  |         self.start() | ||||||
|  |         self.function(*self.args, **self.kwargs) | ||||||
|  | 
 | ||||||
|  |     def start(self): | ||||||
|  |         if not self.is_running: | ||||||
|  |             self.next_call += self.interval | ||||||
|  |             self._timer = Timer(self.next_call - time.time(), self._run) | ||||||
|  |             self._timer.start() | ||||||
|  |             self.is_running = True | ||||||
|  | 
 | ||||||
|  |     def stop(self): | ||||||
|  |         self._timer.cancel() | ||||||
|  |         self.is_running = False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def checkInvites(): | ||||||
|  |     invites = dict(data_store.invites) | ||||||
|  |     # checkInvite already loops over everything, no point running it multiple times. | ||||||
|  |     checkInvite(list(invites.keys())[0]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if config.getboolean("notifications", "enabled"): | ||||||
|  |     inviteDaemon = Repeat(60, checkInvites) | ||||||
| @ -106,6 +106,8 @@ def verify_password(username, password): | |||||||
|             user = Account().verify_token(username, accounts) |             user = Account().verify_token(username, accounts) | ||||||
|             if user: |             if user: | ||||||
|                 verified = True |                 verified = True | ||||||
|  |                 if user in accounts: | ||||||
|  |                     user = accounts[user] | ||||||
|             if not user: |             if not user: | ||||||
|                 log.debug(f"User {username} not found on Jellyfin") |                 log.debug(f"User {username} not found on Jellyfin") | ||||||
|                 return False |                 return False | ||||||
| @ -116,10 +118,10 @@ def verify_password(username, password): | |||||||
|         if username == user.username and user.verify_password(password): |         if username == user.username and user.verify_password(password): | ||||||
|             g.user = user |             g.user = user | ||||||
|             log.debug("HTTPAuth Allowed") |             log.debug("HTTPAuth Allowed") | ||||||
|             return True |             return user | ||||||
|         else: |         else: | ||||||
|             log.debug("HTTPAuth Denied") |             log.debug("HTTPAuth Denied") | ||||||
|             return False |             return False | ||||||
|     g.user = user |     g.user = user | ||||||
|     log.debug("HTTPAuth Allowed") |     log.debug("HTTPAuth Allowed") | ||||||
|     return True |     return user | ||||||
|  | |||||||
| @ -43,7 +43,12 @@ def static_proxy(path): | |||||||
|     if "html" not in path: |     if "html" not in path: | ||||||
|         if "admin.js" in path: |         if "admin.js" in path: | ||||||
|             return ( |             return ( | ||||||
|                 render_template("admin.js", bsVersion=bsVersion(), css_file=css_file), |                 render_template( | ||||||
|  |                     "admin.js", | ||||||
|  |                     bsVersion=bsVersion(), | ||||||
|  |                     css_file=css_file, | ||||||
|  |                     notifications=config.getboolean("notifications", "enabled"), | ||||||
|  |                 ), | ||||||
|                 200, |                 200, | ||||||
|                 {"Content-Type": "text/javascript"}, |                 {"Content-Type": "text/javascript"}, | ||||||
|             ) |             ) | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ from jellyfin_accounts import ( | |||||||
|     configparser, |     configparser, | ||||||
|     config_base_path, |     config_base_path, | ||||||
| ) | ) | ||||||
|  | from jellyfin_accounts.email import Mailgun, Smtp | ||||||
| from jellyfin_accounts import web_log as log | from jellyfin_accounts import web_log as log | ||||||
| from jellyfin_accounts.validate_password import PasswordValidator | from jellyfin_accounts.validate_password import PasswordValidator | ||||||
| 
 | 
 | ||||||
| @ -45,6 +46,22 @@ def checkInvite(code, used=False, username=None): | |||||||
|             "no-limit" not in invites[invite] and invites[invite]["remaining-uses"] < 1 |             "no-limit" not in invites[invite] and invites[invite]["remaining-uses"] < 1 | ||||||
|         ): |         ): | ||||||
|             log.debug(f"Housekeeping: Deleting expired invite {invite}") |             log.debug(f"Housekeeping: Deleting expired invite {invite}") | ||||||
|  |             if ( | ||||||
|  |                 config.getboolean("notifications", "enabled") | ||||||
|  |                 and "notify" in invites[invite] | ||||||
|  |             ): | ||||||
|  |                 for address in invites[invite]["notify"]: | ||||||
|  |                     if "notify-expiry" in invites[invite]["notify"][address]: | ||||||
|  |                         if invites[invite]["notify"][address]["notify-expiry"]: | ||||||
|  |                             method = config["email"]["method"] | ||||||
|  |                             if method == "mailgun": | ||||||
|  |                                 email = Mailgun(address) | ||||||
|  |                             elif method == "smtp": | ||||||
|  |                                 email = Smtp(address) | ||||||
|  |                             if email.construct_expiry( | ||||||
|  |                                 {"code": invite, "expiry": expiry} | ||||||
|  |                             ): | ||||||
|  |                                 email.send() | ||||||
|             del data_store.invites[invite] |             del data_store.invites[invite] | ||||||
|         elif invite == code: |         elif invite == code: | ||||||
|             match = True |             match = True | ||||||
| @ -184,7 +201,28 @@ def newUser(): | |||||||
|                 return jsonify({"error": error}) |                 return jsonify({"error": error}) | ||||||
|             except: |             except: | ||||||
|                 return jsonify({"error": "Unknown error"}) |                 return jsonify({"error": "Unknown error"}) | ||||||
|  |             invites = dict(data_store.invites) | ||||||
|             checkInvite(data["code"], used=True, username=data["username"]) |             checkInvite(data["code"], used=True, username=data["username"]) | ||||||
|  |             if ( | ||||||
|  |                 config.getboolean("notifications", "enabled") | ||||||
|  |                 and "notify" in invites[data["code"]] | ||||||
|  |             ): | ||||||
|  |                 for address in invites[data["code"]]["notify"]: | ||||||
|  |                     if "notify-creation" in invites[data["code"]]["notify"][address]: | ||||||
|  |                         if invites[data["code"]]["notify"][address]["notify-creation"]: | ||||||
|  |                             method = config["email"]["method"] | ||||||
|  |                             if method == "mailgun": | ||||||
|  |                                 email = Mailgun(address) | ||||||
|  |                             elif method == "smtp": | ||||||
|  |                                 email = Smtp(address) | ||||||
|  |                             if email.construct_created( | ||||||
|  |                                 { | ||||||
|  |                                     "code": data["code"], | ||||||
|  |                                     "username": data["username"], | ||||||
|  |                                     "created": datetime.datetime.now(), | ||||||
|  |                                 } | ||||||
|  |                             ): | ||||||
|  |                                 email.send() | ||||||
|             if user.status_code == 200: |             if user.status_code == 200: | ||||||
|                 try: |                 try: | ||||||
|                     policy = data_store.user_template |                     policy = data_store.user_template | ||||||
| @ -258,6 +296,11 @@ def generateInvite(): | |||||||
|         response = email.send() |         response = email.send() | ||||||
|         if response is False or type(response) != bool: |         if response is False or type(response) != bool: | ||||||
|             invite["email"] = f"Failed to send to {address}" |             invite["email"] = f"Failed to send to {address}" | ||||||
|  |     if config.getboolean("notifications", "enabled"): | ||||||
|  |         if "notify-creation" in data: | ||||||
|  |             invite["notify-creation"] = data["notify-creation"] | ||||||
|  |         if "notify-expiry" in data: | ||||||
|  |             invite["notify-expiry"] = data["notify-expiry"] | ||||||
|     data_store.invites[invite_code] = invite |     data_store.invites[invite_code] = invite | ||||||
|     log.info(f"New invite created: {invite_code}") |     log.info(f"New invite created: {invite_code}") | ||||||
|     return resp() |     return resp() | ||||||
| @ -296,6 +339,20 @@ def getInvites(): | |||||||
|             invite["remaining-uses"] = 1 |             invite["remaining-uses"] = 1 | ||||||
|         if "email" in invites[code]: |         if "email" in invites[code]: | ||||||
|             invite["email"] = invites[code]["email"] |             invite["email"] = invites[code]["email"] | ||||||
|  |         if "notify" in invites[code]: | ||||||
|  |             if config.getboolean("ui", "jellyfin_login"): | ||||||
|  |                 address = data_store.emails[g.user.id] | ||||||
|  |             else: | ||||||
|  |                 address = config["ui"]["email"] | ||||||
|  |             if address in invites[code]["notify"]: | ||||||
|  |                 if "notify-expiry" in invites[code]["notify"][address]: | ||||||
|  |                     invite["notify-expiry"] = invites[code]["notify"][address][ | ||||||
|  |                         "notify-expiry" | ||||||
|  |                     ] | ||||||
|  |                 if "notify-creation" in invites[code]["notify"][address]: | ||||||
|  |                     invite["notify-creation"] = invites[code]["notify"][address][ | ||||||
|  |                         "notify-creation" | ||||||
|  |                     ] | ||||||
|         response["invites"].append(invite) |         response["invites"].append(invite) | ||||||
|     return jsonify(response) |     return jsonify(response) | ||||||
| 
 | 
 | ||||||
| @ -407,3 +464,29 @@ def getConfig(): | |||||||
|             if entry in config[section]: |             if entry in config[section]: | ||||||
|                 response_config[section][entry]["value"] = config[section][entry] |                 response_config[section][entry]["value"] = config[section][entry] | ||||||
|     return jsonify(response_config), 200 |     return jsonify(response_config), 200 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @app.route("/setNotify", methods=["POST"]) | ||||||
|  | @auth.login_required | ||||||
|  | def setNotify(): | ||||||
|  |     data = request.get_json() | ||||||
|  |     change = False | ||||||
|  |     for code in data: | ||||||
|  |         for key in data[code]: | ||||||
|  |             if key in ["notify-expiry", "notify-creation"]: | ||||||
|  |                 inv = data_store.invites[code] | ||||||
|  |                 if config.getboolean("ui", "jellyfin_login"): | ||||||
|  |                     address = data_store.emails[g.user.id] | ||||||
|  |                 else: | ||||||
|  |                     address = config["ui"]["email"] | ||||||
|  |                 if "notify" not in inv: | ||||||
|  |                     inv["notify"] = {} | ||||||
|  |                 if address not in inv["notify"]: | ||||||
|  |                     inv["notify"][address] = {} | ||||||
|  |                 inv["notify"][address][key] = data[code][key] | ||||||
|  |                 log.debug(f"{code}: Notification settings changed") | ||||||
|  |                 change = True | ||||||
|  |     if change: | ||||||
|  |         data_store.invites[code] = inv | ||||||
|  |         return resp() | ||||||
|  |     return resp(success=False) | ||||||
|  | |||||||
							
								
								
									
										47
									
								
								mail/created.mjml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								mail/created.mjml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | |||||||
|  | <mjml> | ||||||
|  |   <mj-head> | ||||||
|  |     <mj-attributes> | ||||||
|  |       <mj-class name="bg" background-color="#101010" /> | ||||||
|  |       <mj-class name="bg2" background-color="#242424" /> | ||||||
|  |       <mj-class name="text" color="rgba(255,255,255,0.8)" /> | ||||||
|  |       <mj-class name="bold" color="rgba(255,255,255,0.87)" /> | ||||||
|  |       <mj-class name="secondary" color="rgb(153,153,153)" /> | ||||||
|  |       <mj-class name="blue" background-color="rgb(0,164,220)" /> | ||||||
|  |     </mj-attributes> | ||||||
|  |     <mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" /> | ||||||
|  |     <mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" /> | ||||||
|  |   </mj-head> | ||||||
|  |   <mj-body> | ||||||
|  |     <mj-section mj-class="bg2"> | ||||||
|  |       <mj-column> | ||||||
|  |         <mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> jellyfin-accounts </mj-text> | ||||||
|  |       </mj-column> | ||||||
|  |     </mj-section> | ||||||
|  |     <mj-section mj-class="bg"> | ||||||
|  |       <mj-column> | ||||||
|  |         <mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif"> | ||||||
|  |           <h3>User Created</h3> | ||||||
|  |           <p>A user was created using code {{ code }}.</p> | ||||||
|  |         </mj-text> | ||||||
|  |         <mj-table mj-class="text" container-background-color="#242424"> | ||||||
|  |           <tr style="text-align: left;"> | ||||||
|  |             <th>Name</th> | ||||||
|  |             <th>Address</th> | ||||||
|  |             <th>Time</th> | ||||||
|  |           </tr> | ||||||
|  |           <tr style="font-style: italic; text-align: left; color: rgb(153,153,153);"> | ||||||
|  |             <th>{{ username }}</th> | ||||||
|  |             <th>{{ address }}</th> | ||||||
|  |             <th>{{ time }}</th> | ||||||
|  |         </mj-table> | ||||||
|  |       </mj-column> | ||||||
|  |     </mj-section> | ||||||
|  |     <mj-section mj-class="bg2"> | ||||||
|  |       <mj-column> | ||||||
|  |         <mj-text mj-class="secondary" font-style="italic" font-size="14px"> | ||||||
|  |           Notification emails can be toggled on the admin dashboard. | ||||||
|  |         </mj-text> | ||||||
|  |       </mj-column> | ||||||
|  |     </mj-section> | ||||||
|  |     </body> | ||||||
|  | </mjml> | ||||||
							
								
								
									
										7
									
								
								mail/created.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								mail/created.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | A user was created using code {{ code }}. | ||||||
|  | 
 | ||||||
|  | Name: {{ username }} | ||||||
|  | Address: {{ address }} | ||||||
|  | Time: {{ time }} | ||||||
|  | 
 | ||||||
|  | Note: Notification emails can be toggled on the admin dashboard. | ||||||
							
								
								
									
										36
									
								
								mail/expired.mjml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								mail/expired.mjml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | |||||||
|  | <mjml> | ||||||
|  |   <mj-head> | ||||||
|  |     <mj-attributes> | ||||||
|  |       <mj-class name="bg" background-color="#101010" /> | ||||||
|  |       <mj-class name="bg2" background-color="#242424" /> | ||||||
|  |       <mj-class name="text" color="rgba(255,255,255,0.8)" /> | ||||||
|  |       <mj-class name="bold" color="rgba(255,255,255,0.87)" /> | ||||||
|  |       <mj-class name="secondary" color="rgb(153,153,153)" /> | ||||||
|  |       <mj-class name="blue" background-color="rgb(0,164,220)" /> | ||||||
|  |     </mj-attributes> | ||||||
|  |     <mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" /> | ||||||
|  |     <mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" /> | ||||||
|  |   </mj-head> | ||||||
|  |   <mj-body> | ||||||
|  |     <mj-section mj-class="bg2"> | ||||||
|  |       <mj-column> | ||||||
|  |         <mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> jellyfin-accounts </mj-text> | ||||||
|  |       </mj-column> | ||||||
|  |     </mj-section> | ||||||
|  |     <mj-section mj-class="bg"> | ||||||
|  |       <mj-column> | ||||||
|  |         <mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif"> | ||||||
|  |           <h3>Invite Expired.</h3> | ||||||
|  |           <p>Code {{ code }} expired at {{ expiry }}.</p> | ||||||
|  |         </mj-text> | ||||||
|  |       </mj-column> | ||||||
|  |     </mj-section> | ||||||
|  |     <mj-section mj-class="bg2"> | ||||||
|  |       <mj-column> | ||||||
|  |         <mj-text mj-class="secondary" font-style="italic" font-size="14px"> | ||||||
|  |           Notification emails can be toggled on the admin dashboard. | ||||||
|  |         </mj-text> | ||||||
|  |       </mj-column> | ||||||
|  |     </mj-section> | ||||||
|  |     </body> | ||||||
|  | </mjml> | ||||||
							
								
								
									
										5
									
								
								mail/expired.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								mail/expired.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | Invite expired. | ||||||
|  | 
 | ||||||
|  | Code {{ code }} expired at {{ expiry }}. | ||||||
|  | 
 | ||||||
|  | Note: Notification emails can be toggled on the admin dashboard. | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user