2020-09-16 21:41:55 +00:00
|
|
|
from log import logger
|
|
|
|
import time
|
|
|
|
import secrets
|
2020-09-16 22:14:27 +00:00
|
|
|
import subprocess
|
2021-09-04 06:19:57 +00:00
|
|
|
import requests
|
|
|
|
from uuid import uuid4
|
2020-09-16 21:41:55 +00:00
|
|
|
|
|
|
|
from flask import abort
|
|
|
|
|
|
|
|
HTTP_NOTFOUND = 404
|
|
|
|
|
2021-09-04 06:19:57 +00:00
|
|
|
random_email = lambda: 'spaceport-' + str(uuid4()).split('-')[0] + '@protospace.ca'
|
|
|
|
|
2020-09-16 22:14:27 +00:00
|
|
|
def set_wiki_password(username, password):
|
|
|
|
# sets a user's wiki password
|
|
|
|
# creates the account if it doesn't exist
|
|
|
|
|
2021-09-04 06:19:57 +00:00
|
|
|
if not secrets.WIKI_MAINTENANCE:
|
|
|
|
logger.error('Wiki setting not configured, aborting')
|
|
|
|
abort(400)
|
|
|
|
|
2020-09-16 22:14:27 +00:00
|
|
|
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
|
2020-11-22 01:30:02 +00:00
|
|
|
output = output.strip()
|
2020-09-16 22:14:27 +00:00
|
|
|
|
|
|
|
logger.info('Output: ' + output)
|
|
|
|
|
|
|
|
if result.stderr:
|
|
|
|
abort(400)
|
2020-09-16 21:41:55 +00:00
|
|
|
|
2021-09-04 06:19:57 +00:00
|
|
|
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)
|
2021-09-25 03:12:11 +00:00
|
|
|
logger.debug('Response: %s %s', response.status_code, response.text)
|
2021-11-11 00:17:44 +00:00
|
|
|
response.raise_for_status()
|
2021-09-04 06:19:57 +00:00
|
|
|
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)
|
2021-09-25 03:12:11 +00:00
|
|
|
logger.debug('Response: %s %s', response.status_code, response.text)
|
2021-11-11 00:17:44 +00:00
|
|
|
response.raise_for_status()
|
2021-09-04 06:19:57 +00:00
|
|
|
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)
|
2021-09-25 03:12:11 +00:00
|
|
|
logger.debug('Response: %s %s', response.status_code, response.text)
|
2021-11-11 00:17:44 +00:00
|
|
|
response.raise_for_status()
|
2021-09-04 06:19:57 +00:00
|
|
|
return response
|
|
|
|
|
2021-09-24 23:00:39 +00:00
|
|
|
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)
|
2021-09-25 03:12:11 +00:00
|
|
|
logger.debug('Response: %s %s', response.status_code, response.text)
|
2021-11-11 00:17:44 +00:00
|
|
|
response.raise_for_status()
|
2021-09-24 23:00:39 +00:00
|
|
|
return response
|
|
|
|
|
2021-09-04 06:19:57 +00:00
|
|
|
def discourse_rails_script(script):
|
|
|
|
result = subprocess.run(['docker', 'exec', '-i', secrets.DISCOURSE_CONTAINER, 'rails', 'runner', script],
|
2022-07-05 03:47:06 +00:00
|
|
|
shell=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30)
|
2021-09-04 06:19:57 +00:00
|
|
|
output = result.stdout or result.stderr
|
|
|
|
output = output.strip() or 'No complaints'
|
|
|
|
return result, output
|
|
|
|
|
2021-09-25 03:12:11 +00:00
|
|
|
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 = []
|
|
|
|
page = 1
|
|
|
|
|
|
|
|
for _ in range(10):
|
|
|
|
params = {
|
|
|
|
'page': page,
|
|
|
|
}
|
|
|
|
response = discourse_api_get('https://forum.protospace.ca/admin/users/list/active.json', params)
|
|
|
|
response = response.json()
|
|
|
|
|
|
|
|
if not len(response):
|
|
|
|
break
|
|
|
|
|
|
|
|
for u in response:
|
|
|
|
usernames.append(u['username'])
|
|
|
|
|
|
|
|
page += 1
|
|
|
|
else: # for
|
|
|
|
logger.error('Too many user pages, aborting')
|
|
|
|
abort(400)
|
|
|
|
|
|
|
|
return usernames
|
|
|
|
|
2021-09-25 22:59:34 +00:00
|
|
|
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
|
|
|
|
|
2021-09-04 06:19:57 +00:00
|
|
|
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)
|
|
|
|
|
2021-09-25 22:59:34 +00:00
|
|
|
discourse_usernames = get_discourse_usernames()
|
|
|
|
username = translate_usernames([username], discourse_usernames)[0]
|
|
|
|
|
2021-09-04 06:19:57 +00:00
|
|
|
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:
|
2021-09-25 22:59:34 +00:00
|
|
|
if user['email'].lower() == email.lower():
|
2021-09-04 06:19:57 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2022-06-03 00:34:59 +00:00
|
|
|
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)
|
2021-09-04 06:19:57 +00:00
|
|
|
|
|
|
|
logger.info('Skipping set password')
|
|
|
|
return True
|
|
|
|
|
2022-06-03 00:34:59 +00:00
|
|
|
else:
|
|
|
|
logger.info('User exists, setting Discourse password for: ' + username)
|
2021-09-04 06:19:57 +00:00
|
|
|
|
2022-06-03 00:34:59 +00:00
|
|
|
script = 'User.find_by(username: "{}").update!(password: "{}")'.format(username, password)
|
|
|
|
result, output = discourse_rails_script(script)
|
2021-09-04 06:19:57 +00:00
|
|
|
|
2022-06-03 00:34:59 +00:00
|
|
|
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)
|
2021-09-04 06:19:57 +00:00
|
|
|
|
2022-06-03 00:34:59 +00:00
|
|
|
if result.stderr:
|
|
|
|
abort(400)
|
2021-09-04 06:19:57 +00:00
|
|
|
|
2021-09-24 23:00:39 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2021-09-25 22:59:34 +00:00
|
|
|
discourse_usernames = get_discourse_usernames()
|
|
|
|
usernames = translate_usernames(usernames, discourse_usernames)
|
|
|
|
usernames = set(usernames)
|
2021-09-25 03:12:11 +00:00
|
|
|
group_id = get_discourse_group_id(group_name)
|
2021-09-24 23:00:39 +00:00
|
|
|
|
2021-09-25 03:12:11 +00:00
|
|
|
logger.info('Filtering out usernames not on Discourse...')
|
2021-09-24 23:00:39 +00:00
|
|
|
|
2021-09-25 22:59:34 +00:00
|
|
|
discourse_usernames = set(discourse_usernames)
|
2021-09-25 03:12:11 +00:00
|
|
|
usernames = usernames & discourse_usernames
|
2021-09-24 23:00:39 +00:00
|
|
|
|
2021-09-25 03:12:11 +00:00
|
|
|
logger.info('Filtering out usernames that are already group members...')
|
2021-09-24 23:00:39 +00:00
|
|
|
|
2021-11-11 00:17:44 +00:00
|
|
|
url = 'https://forum.protospace.ca/groups/{}/members.json?limit=1000'.format(group_name)
|
2021-09-24 23:00:39 +00:00
|
|
|
response = discourse_api_get(url)
|
|
|
|
response = response.json()
|
|
|
|
|
|
|
|
member_usernames = set([m['username'] for m in response['members']])
|
2021-09-25 03:12:11 +00:00
|
|
|
usernames = usernames - member_usernames
|
|
|
|
usernames = list(usernames)
|
2021-09-24 23:00:39 +00:00
|
|
|
|
2021-09-25 03:12:11 +00:00
|
|
|
if not len(usernames):
|
2021-09-24 23:00:39 +00:00
|
|
|
logger.info('Skipping, no one left to add')
|
|
|
|
return True
|
|
|
|
|
2021-09-25 03:12:11 +00:00
|
|
|
logger.info('Adding %s remaining usernames to the group...', len(usernames))
|
2021-09-24 23:00:39 +00:00
|
|
|
|
|
|
|
url = 'https://forum.protospace.ca/groups/{}/members.json'.format(group_id)
|
|
|
|
data = {
|
2021-09-25 03:12:11 +00:00
|
|
|
'usernames': ','.join(usernames)
|
2021-09-24 23:00:39 +00:00
|
|
|
}
|
|
|
|
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)
|
|
|
|
|
2021-09-25 22:59:34 +00:00
|
|
|
discourse_usernames = get_discourse_usernames()
|
|
|
|
usernames = translate_usernames(usernames, discourse_usernames)
|
|
|
|
usernames = set(usernames)
|
2021-09-25 03:12:11 +00:00
|
|
|
group_id = get_discourse_group_id(group_name)
|
2021-09-24 23:00:39 +00:00
|
|
|
|
2021-09-25 03:12:11 +00:00
|
|
|
logger.info('Filtering out usernames not on Discourse...')
|
2021-09-24 23:00:39 +00:00
|
|
|
|
2021-09-25 22:59:34 +00:00
|
|
|
discourse_usernames = set(discourse_usernames)
|
2021-09-25 03:12:11 +00:00
|
|
|
usernames = usernames & discourse_usernames
|
|
|
|
usernames = list(usernames)
|
2021-09-24 23:00:39 +00:00
|
|
|
|
2021-09-25 22:59:34 +00:00
|
|
|
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))
|
2021-09-24 23:00:39 +00:00
|
|
|
|
|
|
|
url = 'https://forum.protospace.ca/groups/{}/members.json'.format(group_id)
|
|
|
|
data = {
|
2021-09-25 03:12:11 +00:00
|
|
|
'usernames': ','.join(usernames)
|
2021-09-24 23:00:39 +00:00
|
|
|
}
|
|
|
|
discourse_api_delete(url, data)
|
|
|
|
return True
|
|
|
|
|
2021-10-14 22:47:10 +00:00
|
|
|
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)
|
2021-09-24 23:00:39 +00:00
|
|
|
|
2021-10-14 22:47:10 +00:00
|
|
|
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
|
2021-09-24 23:00:39 +00:00
|
|
|
|
2020-09-16 21:41:55 +00:00
|
|
|
if __name__ == '__main__':
|
2021-09-04 06:19:57 +00:00
|
|
|
#set_wiki_password('tanner.collin', 'protospace1')
|
2021-10-14 22:47:10 +00:00
|
|
|
set_discourse_password('test8a', 'protospace1', 'testie', 'test8@example.com')
|
|
|
|
#for u in get_discourse_usernames():
|
|
|
|
# print(u)
|
|
|
|
#pass
|