from log import logger import time import secrets import subprocess import requests from uuid import uuid4 from flask import abort HTTP_NOTFOUND = 404 random_email = lambda: 'spaceport-' + str(uuid4()).split('-')[0] + '@protospace.ca' def set_wiki_password(username, password): # sets a user's wiki password # creates the account if it doesn't exist if not secrets.WIKI_MAINTENANCE: logger.error('Wiki setting not configured, aborting') abort(400) if not username: logger.error('Empty username, aborting') abort(400) logger.info('Setting wiki password for: ' + username) if not password: logger.error('Empty password, aborting') abort(400) script = secrets.WIKI_MAINTENANCE + '/createAndPromote.php' result = subprocess.run(['php', script, '--force', username, password], shell=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output = result.stdout or result.stderr output = output.strip() logger.info('Output: ' + output) if result.stderr: abort(400) def discourse_api_get(url, params={}): headers = { 'Api-Key': secrets.DISCOURSE_API_KEY, 'Api-Username': secrets.DISCOURSE_API_USER, } response = requests.get(url, headers=headers, params=params, timeout=10) logger.debug('Response: %s %s', response.status_code, response.text) response.raise_for_status() return response def discourse_api_put(url, data={}): headers = { 'Api-Key': secrets.DISCOURSE_API_KEY, 'Api-Username': secrets.DISCOURSE_API_USER, } response = requests.put(url, headers=headers, data=data, timeout=10) logger.debug('Response: %s %s', response.status_code, response.text) response.raise_for_status() return response def discourse_api_post(url, data={}): headers = { 'Api-Key': secrets.DISCOURSE_API_KEY, 'Api-Username': secrets.DISCOURSE_API_USER, } response = requests.post(url, headers=headers, data=data, timeout=10) logger.debug('Response: %s %s', response.status_code, response.text) response.raise_for_status() return response def discourse_api_delete(url, data={}): headers = { 'Api-Key': secrets.DISCOURSE_API_KEY, 'Api-Username': secrets.DISCOURSE_API_USER, } response = requests.delete(url, headers=headers, data=data, timeout=10) logger.debug('Response: %s %s', response.status_code, response.text) response.raise_for_status() return response def discourse_rails_script(script): result = subprocess.run(['docker', 'exec', '-i', secrets.DISCOURSE_CONTAINER, 'rails', 'runner', script], shell=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=60) output = result.stdout or result.stderr output = output.strip() or 'No complaints' return result, output def get_discourse_group_id(group_name): logger.info('Getting the ID of group %s', group_name) url = 'https://forum.protospace.ca/groups/{}.json'.format(group_name) response = discourse_api_get(url) response = response.json() return response['group']['id'] def get_discourse_usernames(): usernames = [] response = discourse_api_get('https://forum.protospace.ca/groups/trust_level_0/members.json?limit=1000') response = response.json() for user in response['members']: usernames.append(user['username']) if len(usernames) == 1000: logger.error('Hit username limit, aborting!') abort(400) return usernames def translate_usernames(portal_usernames, discourse_usernames): # the case of portal and discourse usernames might not match # this causes a problem if someone creates a discourse user # as John.Smith and later sets up a portal account as john.smith # # solution: look for usernames in discourse with the same letters, # and then convert to the discourse version when using the API result = [] for pu in portal_usernames: for du in discourse_usernames: if pu.lower() == du.lower(): result.append(du) break else: # for result.append(pu) return result def set_discourse_password(username, password, first_name, email): # sets a user's discourse password # creates the account if it doesn't exist # things to test: # - user changes Spaceport password # - user changes Spaceport password to same # - new Spaceport signup # - existing Discourse user Spaceport signup # - existing Discourse user Spaceport signup with same email # note: Spaceport emails are unconfirmed!! if not secrets.DISCOURSE_CONTAINER or not secrets.DISCOURSE_API_KEY or not secrets.DISCOURSE_API_USER: logger.error('Discourse setting not configured, aborting') abort(400) if not username: logger.error('Empty username, aborting') abort(400) if not password: logger.error('Empty password, aborting') abort(400) if not first_name: logger.error('Empty first_name, aborting') abort(400) if not email: logger.error('Empty email, aborting') abort(400) discourse_usernames = get_discourse_usernames() username = translate_usernames([username], discourse_usernames)[0] logger.info('Checking Discourse for existing email: ' + email) params = { 'filter': email, 'show_emails': 'true', } response = discourse_api_get('https://forum.protospace.ca/admin/users/list/active.json', params) response = response.json() for user in response: if user['email'].lower() == email.lower(): if user['username'] == username: logger.info('Username match, skipping') continue new_email = random_email() logger.info('Email found on different user %s, changing to: %s', user['username'], new_email) script = 'UserEmail.find_by(email: "{}").update!(email: "{}")'.format(email, new_email) result, output = discourse_rails_script(script) logger.info('Confirming email change...') response = discourse_api_get('https://forum.protospace.ca/admin/users/list/active.json', params) if len(response.json()): logger.error('Email change failed, aborting') abort(400) user_exists = username in discourse_usernames if not user_exists: logger.info('Creating Discourse user for: ' + username) data = { 'name': first_name, 'username': username, 'password': password, 'email': email, 'active': True, 'approved': True, 'user_fields[10]': 'Spaceport auth', 'user_fields[11]': 'other', } response = discourse_api_post('https://forum.protospace.ca/users.json', data) response = response.json() logger.info('Response: %s', response) logger.info('Skipping set password') return True else: logger.info('User exists, setting Discourse password for: ' + username) script = 'User.find_by(username: "{}").update!(password: "{}")'.format(username, password) result, output = discourse_rails_script(script) if 'Password is the same' in result.stderr: logger.info('Output: Password is the same as your current password. (ActiveRecord::RecordInvalid)') return True else: logger.info('Output: ' + output) if result.stderr: abort(400) def add_discourse_group_members(group_name, usernames): if not group_name: logger.error('Empty group_name, aborting') abort(400) if not usernames: logger.error('Empty usernames, aborting') abort(400) discourse_usernames = get_discourse_usernames() usernames = translate_usernames(usernames, discourse_usernames) usernames = set(usernames) group_id = get_discourse_group_id(group_name) logger.info('Filtering out usernames not on Discourse...') discourse_usernames = set(discourse_usernames) usernames = usernames & discourse_usernames logger.info('Filtering out usernames that are already group members...') url = 'https://forum.protospace.ca/groups/{}/members.json?limit=1000'.format(group_name) response = discourse_api_get(url) response = response.json() member_usernames = set([m['username'] for m in response['members']]) usernames = usernames - member_usernames usernames = list(usernames) if not len(usernames): logger.info('Skipping, no one left to add') return True logger.info('Adding %s remaining usernames to the group...', len(usernames)) url = 'https://forum.protospace.ca/groups/{}/members.json'.format(group_id) data = { 'usernames': ','.join(usernames) } discourse_api_put(url, data) return True def remove_discourse_group_members(group_name, usernames): if not group_name: logger.error('Empty group_name, aborting') abort(400) if not usernames: logger.error('Empty usernames, aborting') abort(400) discourse_usernames = get_discourse_usernames() usernames = translate_usernames(usernames, discourse_usernames) usernames = set(usernames) group_id = get_discourse_group_id(group_name) logger.info('Filtering out usernames not on Discourse...') discourse_usernames = set(discourse_usernames) usernames = usernames & discourse_usernames usernames = list(usernames) if not len(usernames): logger.info('Skipping, no one left to remove') return True logger.info('Removing %s remaining usernames from the group...', len(usernames)) url = 'https://forum.protospace.ca/groups/{}/members.json'.format(group_id) data = { 'usernames': ','.join(usernames) } discourse_api_delete(url, data) return True def change_discourse_username(username, new_username): if not username: logger.error('Empty username, aborting') abort(400) if not new_username: logger.error('Empty new_username, aborting') abort(400) logger.info('Changing username %s to %s...', username, new_username) url = 'https://forum.protospace.ca/users/{}/preferences/username'.format(username) data = { 'new_username': new_username, } discourse_api_put(url, data) return True if __name__ == '__main__': #set_wiki_password('tanner.collin', 'protospace1') set_discourse_password('test8a', 'protospace1', 'testie', 'test8@example.com') #for u in get_discourse_usernames(): # print(u) #pass