Commit 5a711f4b authored by Geovanny's avatar Geovanny

Added session to keep user logged in. Added functionality to assign a unit to a user

parent 2141fad0
server/database/config.json server/database/config.json
server/node_modules/ server/node_modules/
server/.env
discord/.env discord/.env
discord/__pycache__ discord/__pycache__
*.pyc *.pyc
__pycache__ __pycache__
.vscode
\ No newline at end of file
import asyncio import asyncio
import discord import discord
import discord
from requests import Session
from discord.ext import commands from discord.ext import commands
from unit.manager import UnitManager from unit.manager import UnitManager
from user.manager import UserManager
from util.command_error_handler import CommandErrorHandler from util.command_error_handler import CommandErrorHandler
from util.api_requests import Api
from settings import DISCORD_TOKEN from settings import DISCORD_TOKEN
class ConqBot(commands.Bot):
def __init__(self, command_prefix='>'):
super().__init__(command_prefix)
self.id_to_session = {}
async def getUserSession(self, user:discord.User):
if user.id in self.id_to_session:
return self.id_to_session[user.id]
new_session = Session()
await Api.postSession('/user/d-login', {"discordId": user.id}, new_session);
self.id_to_session[user.id] = new_session
return new_session
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
bot = commands.Bot(command_prefix='>') bot = ConqBot(command_prefix='>')
bot.add_cog(CommandErrorHandler(bot)) bot.add_cog(CommandErrorHandler(bot))
bot.add_cog(UserManager(bot))
bot.add_cog(UnitManager(bot)) bot.add_cog(UnitManager(bot))
@bot.event @bot.event
......
...@@ -7,6 +7,7 @@ from unit.model import Unit ...@@ -7,6 +7,7 @@ from unit.model import Unit
from util.api_requests import Api, ApiError from util.api_requests import Api, ApiError
from .unit_parameters import * from .unit_parameters import *
from discord_argparse import ArgumentConverter from discord_argparse import ArgumentConverter
from util.embed_style import EmbedStyle
class UnitManager(commands.Cog): class UnitManager(commands.Cog):
...@@ -111,8 +112,6 @@ class UnitManager(commands.Cog): ...@@ -111,8 +112,6 @@ class UnitManager(commands.Cog):
except ApiError as error: except ApiError as error:
return await self.handleApiError(ctx, error) return await self.handleApiError(ctx, error)
if data==None:
return await ctx.send('No unit found for: ' + unit)
unit_data = Unit(**data) unit_data = Unit(**data)
embed = discord.Embed(); embed = discord.Embed();
...@@ -159,63 +158,7 @@ class UnitManager(commands.Cog): ...@@ -159,63 +158,7 @@ class UnitManager(commands.Cog):
await self.createUnitTable(ctx, units_data) await self.createUnitTable(ctx, units_data)
async def createUnitTable(self, ctx, data): async def createUnitTable(self, ctx, data):
max_rows = 5 return await EmbedStyle.createPages(self.bot, ctx, data, 5, self.createUnitPage)
max_pages = len(data) // max_rows
rem = len(data) % max_rows
cur_page = 0;
first_run = True
if rem!=0:
max_pages+=1
while True:
if first_run:
embed = self.createUnitPage(data, max_rows*cur_page, max_rows*cur_page + max_rows)
first_run = False
msg = await ctx.send(embed=embed)
reactmoji = []
if max_pages == 1 and cur_page == 0:
pass
elif cur_page == 0:
reactmoji.append('⏩')
elif cur_page == max_pages-1:
reactmoji.append('⏪')
elif cur_page > 0 and cur_page < max_pages-1:
reactmoji.extend(['⏪', '⏩'])
for react in reactmoji:
await msg.add_reaction(react)
def check_react(reaction, user):
if reaction.message.id != msg.id:
return False
if user != ctx.message.author:
return False
if str(reaction.emoji) not in reactmoji:
return False
return True
try:
res, user = await self.bot.wait_for('reaction_add', timeout=30.0, check=check_react)
except asyncio.TimeoutError:
return await msg.clear_reactions()
if '⏪' in str(res.emoji):
cur_page-= 1
embed = self.createUnitPage(data, max_rows*cur_page, max_rows*cur_page + max_rows)
await msg.clear_reactions()
await msg.edit(embed=embed)
if '⏩' in str(res.emoji):
cur_page+= 1
embed = self.createUnitPage(data, max_rows*cur_page, max_rows*cur_page + max_rows)
await msg.clear_reactions()
await msg.edit(embed=embed)
def createUnitPage(self, data, minI, maxI): def createUnitPage(self, data, minI, maxI):
embed = discord.Embed(color=0x19212d) embed = discord.Embed(color=0x19212d)
......
...@@ -2,7 +2,9 @@ class Unit(object): ...@@ -2,7 +2,9 @@ class Unit(object):
def __init__(self, id, name, type, stars=0, hp=0, pap=0, pd=0, sap=0, sd=0, bap=0, bd=0, pdf=0, sdf=0, bdf=0, leadership=0, troop_count=0, hero_level=0, speed=0, range=0, ammo=0, labour=0, def __init__(self, id, name, type, stars=0, hp=0, pap=0, pd=0, sap=0, sd=0, bap=0, bd=0, pdf=0, sdf=0, bdf=0, leadership=0, troop_count=0, hero_level=0, speed=0, range=0, ammo=0, labour=0,
img=None, img=None,
vet_img=None): vet_img=None,
unit_level=0,
elite_flg=None):
self.id = id self.id = id
self.name = name self.name = name
self.type = type self.type = type
...@@ -31,6 +33,9 @@ class Unit(object): ...@@ -31,6 +33,9 @@ class Unit(object):
if vet_img == None: if vet_img == None:
self.vet_img = 'https://www.conquerorsblade.com/static/img/Conqueror.cd5398b.png' self.vet_img = 'https://www.conquerorsblade.com/static/img/Conqueror.cd5398b.png'
self.unit_level = unit_level
self.elite_flg = elite_flg
@classmethod @classmethod
def from_dict(cls, **data): def from_dict(cls, **data):
return cls(**data) return cls(**data)
......
import asyncio
import discord
import json
from discord.ext import commands
from discord.user import User
from unit.model import Unit
from util.api_requests import Api, ApiError
from util.embed_style import EmbedStyle
from .uu_parameters import *
class UserManager(commands.Cog):
def __init__(self, bot, loop=None):
self.bot = bot
self.loop = loop or asyncio.get_event_loop()
async def handleApiError(self, ctx, error):
return await ctx.send(error.message)
@commands.command()
async def registerUser(self, ctx):
req_body = {'discordId': ctx.message.author.id}
try:
await Api.post('/user/discord-register', req_body)
await ctx.send('Register succesful')
except ApiError as error:
return await self.handleApiError(ctx, error)
@commands.command()
async def removeAssign(self, ctx, term:str):
data = None
try:
session = await self.bot.getUserSession(ctx.message.author)
data = await Api.getSession('/user/unit/{0}'.format(term), session)
except ApiError as error:
return await self.handleApiError(ctx, error)
await ctx.send('Do you want to remove \'{0}\'? Yes to confirm'.format(data['name']))
def check(m):
return m.channel == ctx.channel and m.author == ctx.message.author
msg = ''
try:
msg = await self.bot.wait_for('message', timeout=30.0, check=check)
except asyncio.TimeoutError:
return await ctx.send('Operation aborted')
try:
session = await self.bot.getUserSession(ctx.message.author)
await Api.deleteSession('/user/unit/{0}'.format(data['id']), session)
return await ctx.send('Unit Assignment Removed')
except ApiError as error:
return await self.handleApiError(ctx, error);
@commands.command()
async def modUnit(self, ctx, term:str, *, params:param_converter=param_converter.defaults()):
data = None
try:
session = await self.bot.getUserSession(ctx.message.author)
data = await Api.getSession('/user/unit/{0}'.format(term), session)
except ApiError as error:
return await self.handleApiError(ctx, error)
await ctx.send('Do you want to modify \'{0}\'? Yes to confirm'.format(data['name']))
def check(m):
return m.channel == ctx.channel and m.author == ctx.message.author
msg = ''
try:
msg = await self.bot.wait_for('message', timeout=30.0, check=check)
except asyncio.TimeoutError:
return await ctx.send('Operation aborted')
try:
session = await self.bot.getUserSession(ctx.message.author)
body = params
await Api.putSession('/user/unit/{0}'.format(data['id']), body, session)
return await ctx.send('Unit Modified Successfully')
except ApiError as error:
return await self.handleApiError(ctx, error);
@commands.command()
async def assignUnit(self, ctx, term:str, *, params:param_converter=param_converter.defaults()):
data = None
try:
data = await Api.get('/unit/{0}'.format(term))
except ApiError as error:
return await self.handleApiError(ctx, error)
await ctx.send('Do you want to assign \'{0}\'? Yes to confirm'.format(data['name']))
def check(m):
return m.channel == ctx.channel and m.author == ctx.message.author
msg = ''
try:
msg = await self.bot.wait_for('message', timeout=30.0, check=check)
except asyncio.TimeoutError:
return await ctx.send('Operation aborted')
try:
session = await self.bot.getUserSession(ctx.message.author)
body = params
body['unit_id'] = data['id']
print(body)
await Api.postSession('/user/unit', body, session)
return await ctx.send('Unit Assigned Successfully')
except ApiError as error:
return await self.handleApiError(ctx, error);
@commands.command()
async def myUnits(self, ctx):
try:
session = await self.bot.getUserSession(ctx.message.author)
data = await Api.getSession('/user/units', session)
units_data = []
for item in data['units']:
units_data.append(Unit(**item))
return await EmbedStyle.createPages(self.bot, ctx, units_data, 5, self.createUnitPage)
except ApiError as error:
return await self.handleApiError(ctx, error)
def createUnitPage(self, data, minI, maxI):
embed = discord.Embed(color=0x19212d)
embed.set_author(name='TestBot')
units_str = '';
for i in range(minI, maxI):
if i < len(data):
unit = data[i]
units_str+= '[{0}] {1}\t| {2}\t| {3}\t| {4}\n'.format(unit.id, unit.name, unit.type, unit.stars, unit.unit_level)
embed.add_field(name='id\t| name\t| type\t| stars\t| level', value=units_str)
return embed
from discord_argparse import *
param_converter = ArgumentConverter(
unit_level = OptionalArgument(
str,
doc='Level of Unit',
),
eltie_flg = OptionalArgument(
bool,
doc='Is Unit Elite',
)
)
\ No newline at end of file
...@@ -6,6 +6,7 @@ from settings import API_URL_BASE ...@@ -6,6 +6,7 @@ from settings import API_URL_BASE
class ApiError(Exception): class ApiError(Exception):
def __init__(self, error_code:int, message:str=None): def __init__(self, error_code:int, message:str=None):
self.error_code = error_code self.error_code = error_code
self.message = message self.message = message
...@@ -38,27 +39,38 @@ class Api: ...@@ -38,27 +39,38 @@ class Api:
return Api.status_code_handling(response) return Api.status_code_handling(response)
@staticmethod
async def delete(url, cookies=None):
req_url = API_URL_BASE + url
response = requests.delete(req_url, cookies=cookies)
return Api.status_code_handling(response)
@staticmethod @staticmethod
async def getWithUser(url, uid): async def getSession(url, session):
enc_id = Crypto.encrypt(str(uid)) req_url = API_URL_BASE + url
cookies = {'discord_id': enc_id} response = session.get(req_url)
print('ENC: ' + enc_id)
return await Api.get(url, cookies) return Api.status_code_handling(response)
@staticmethod @staticmethod
async def postWithUser(url, data, uid): async def postSession(url, data, session):
enc_id = Crypto.encrypt(str(uid)) req_url = API_URL_BASE + url
cookies = {'discord_id': enc_id} response = session.post(req_url, json=data)
print('ENC: ' + enc_id) return Api.status_code_handling(response)
return await Api.post(url, data, cookies)
@staticmethod
async def putSession(url, data, session):
req_url = API_URL_BASE + url
response = session.put(req_url, json=data)
return Api.status_code_handling(response)
@staticmethod @staticmethod
async def putWithUser(url, data, uid): async def deleteSession(url, session):
enc_id = Crypto.encrypt(str(uid)) req_url = API_URL_BASE + url
cookies = {'discord_id': enc_id} response = session.delete(req_url)
print('ENC: ' + enc_id)
return await Api.put(url, data, cookies) return Api.status_code_handling(response)
@staticmethod @staticmethod
...@@ -79,7 +91,6 @@ class Api: ...@@ -79,7 +91,6 @@ class Api:
elif response.status_code >= 400: elif response.status_code >= 400:
error_str = '[!] [{0}] Bad Request'.format(response.status_code); error_str = '[!] [{0}] Bad Request'.format(response.status_code);
print(error_str) print(error_str)
print(content)
raise ApiError(response.status_code, content) raise ApiError(response.status_code, content)
elif response.status_code >= 300: elif response.status_code >= 300:
error_str = '[!] [{0}] Unexpected redirect.'.format(response.status_code) error_str = '[!] [{0}] Unexpected redirect.'.format(response.status_code)
......
import traceback
import sys
from discord.ext import commands from discord.ext import commands
import discord_argparse.errors as da_errors import discord_argparse.errors as da_errors
...@@ -43,4 +45,8 @@ class CommandErrorHandler(commands.Cog): ...@@ -43,4 +45,8 @@ class CommandErrorHandler(commands.Cog):
if ctx.command.qualified_name == 'tag list': # Check if the command being invoked is 'tag list' if ctx.command.qualified_name == 'tag list': # Check if the command being invoked is 'tag list'
return await ctx.send('I could not find that member. Please try again.') return await ctx.send('I could not find that member. Please try again.')
# All other Errors not returned come here... And we can just print the default TraceBack.
print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr)
traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)
...@@ -7,33 +7,40 @@ from settings import ENC_DEC_MEDTHOD ...@@ -7,33 +7,40 @@ from settings import ENC_DEC_MEDTHOD
class Crypto: class Crypto:
"""Someday, I will implement encryption and decryption. Today is not that day"""
@staticmethod @staticmethod
def encrypt(str_to_enc): def encrypt(str_to_enc):
try:
aes_obj = AES.new(CRYPT_KEY, AES.MODE_CFB, CRYPT_SALT) # try:
hx_enc = aes_obj.encrypt(str_to_enc.encode('utf8')) # aes_obj = AES.new(CRYPT_KEY, AES.MODE_CFB, CRYPT_SALT)
mret = b64encode(hx_enc).decode(ENC_DEC_MEDTHOD) # hx_enc = aes_obj.encrypt(str_to_enc.encode('utf8'))
return mret # mret = b64encode(hx_enc).decode(ENC_DEC_MEDTHOD)
except ValueError as value_error: # return mret
if value_error.args[0] == 'IV must be 16 bytes long': # except ValueError as value_error:
raise ValueError('Encryption Error: SALT must be 16 characters long') # if value_error.args[0] == 'IV must be 16 bytes long':
elif value_error.args[0] == 'AES key must be either 16, 24, or 32 bytes long': # raise ValueError('Encryption Error: SALT must be 16 characters long')
raise ValueError('Encryption Error: Encryption key must be either 16, 24, or 32 characters long') # elif value_error.args[0] == 'AES key must be either 16, 24, or 32 bytes long':
else: # raise ValueError('Encryption Error: Encryption key must be either 16, 24, or 32 characters long')
raise ValueError(value_error) # else:
# raise ValueError(value_error)
return base64encode(str_to_enc.encode('utf8')).decode('utf-8')
@staticmethod @staticmethod
def decrypt(enc_str): def decrypt(enc_str):
try: # try:
aes_obj = AES.new(CRYPT_KEY.encode('utf8'), AES.MODE_CFB, CRYPT_SALT) # aes_obj = AES.new(CRYPT_KEY.encode('utf8'), AES.MODE_CFB, CRYPT_SALT)
str_tmp = b64decode(enc_str.encode(ENC_DEC_MEDTHOD)) # str_tmp = b64decode(enc_str.encode(ENC_DEC_MEDTHOD))
str_dec = aes_obj.decrypt(str_tmp) # str_dec = aes_obj.decrypt(str_tmp)
mret = str_dec.decode(ENC_DEC_MEDTHOD) # mret = str_dec.decode(ENC_DEC_MEDTHOD)
return mret # return mret
except ValueError as value_error: # except ValueError as value_error:
if value_error.args[0] == 'IV must be 16 bytes long': # if value_error.args[0] == 'IV must be 16 bytes long':
raise ValueError('Decryption Error: SALT must be 16 characters long') # raise ValueError('Decryption Error: SALT must be 16 characters long')
elif value_error.args[0] == 'AES key must be either 16, 24, or 32 bytes long': # elif value_error.args[0] == 'AES key must be either 16, 24, or 32 bytes long':
raise ValueError('Decryption Error: Encryption key must be either 16, 24, or 32 characters long') # raise ValueError('Decryption Error: Encryption key must be either 16, 24, or 32 characters long')
else: # else:
raise ValueError(value_error) # raise ValueError(value_error)
\ No newline at end of file
return base64decode(enc_str.encode('utf-8')).decode('utf8')
import discord
from asyncio import TimeoutError
class EmbedStyle:
@staticmethod
async def createPages(bot, ctx, data, max_rows, createRowsFunc):
max_pages = len(data) // max_rows
rem = len(data) % max_rows
cur_page = 0;
first_run = True
if rem!=0:
max_pages+=1
while True:
if first_run:
embed = createRowsFunc(data, max_rows*cur_page, max_rows*cur_page + max_rows)
first_run = False
msg = await ctx.send(embed=embed)
reactmoji = []
if max_pages == 1 and cur_page == 0:
pass
elif cur_page == 0:
reactmoji.append('⏩')
elif cur_page == max_pages-1:
reactmoji.append('⏪')
elif cur_page > 0 and cur_page < max_pages-1:
reactmoji.extend(['⏪', '⏩'])
for react in reactmoji:
await msg.add_reaction(react)
def check_react(reaction, user):
if reaction.message.id != msg.id:
return False
if user != ctx.message.author:
return False
if str(reaction.emoji) not in reactmoji:
return False
return True
try:
res, user = await bot.wait_for('reaction_add', timeout=30.0, check=check_react)
except TimeoutError:
return await msg.clear_reactions()
if '⏪' in str(res.emoji):
cur_page-= 1
embed = createRowsFunc(data, max_rows*cur_page, max_rows*cur_page + max_rows)
await msg.clear_reactions()
await msg.edit(embed=embed)
if '⏩' in str(res.emoji):
cur_page+= 1
embed = createRowsFunc(data, max_rows*cur_page, max_rows*cur_page + max_rows)
await msg.clear_reactions()
await msg.edit(embed=embed)
\ No newline at end of file
'use strict'; 'use strict';
const fs = require('fs');
const MySQL = require('promise-mysql'); const MySQL = require('promise-mysql');
const ENV = require('../settings');
let config_file = fs.readFileSync('./database/config.json');
let db_config = JSON.parse(config_file);
const db = {} const db = {}
let connection = MySQL.createConnection(db_config); let connection = MySQL.createConnection({
host: ENV.DB_HOST,
port: ENV.DB_PORT,
user: ENV.DB_USER,
password: ENV.DB_PASS,
database: ENV.DB_NAME
});
connection.then((con) =>{ connection.then((con) =>{
console.log('Database Connected'); console.log('Database Connected');
......
...@@ -3,15 +3,22 @@ ...@@ -3,15 +3,22 @@
const Koa = require('koa'); const Koa = require('koa');
const Router = require('@koa/router'); const Router = require('@koa/router');
const bodyParser = require('koa-body'); const bodyParser = require('koa-body');
const session = require('koa-session')
const auth = require('./util/auth'); const userRouter = require('./user/route');
const unitsRouter = require('./unit/route'); const unitRouter = require('./unit/route');
const SESS_CONFIG = require('./session_config');
const ENV = require('./settings')
const app = new Koa(); const app = new Koa();
const router = new Router(); const router = new Router();
router.use('/unit', unitsRouter.routes(), unitsRouter.allowedMethods()); app.keys = [ENV.SESS_KEY]
app.use(bodyParser()); app.use(bodyParser());
app.use(session(SESS_CONFIG, app));
router.use('/unit', unitRouter.routes(), unitRouter.allowedMethods());
router.use('/user', userRouter.routes(), userRouter.allowedMethods());
//app.use(auth()); //app.use(auth());
app.use(router.routes()).use(router.allowedMethods()); app.use(router.routes()).use(router.allowedMethods());
......
...@@ -62,6 +62,11 @@ ...@@ -62,6 +62,11 @@
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
}, },
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
},
"bignumber.js": { "bignumber.js": {
"version": "9.0.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
...@@ -72,6 +77,15 @@ ...@@ -72,6 +77,15 @@
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
}, },
"buffer": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
"integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
}
},
"bytes": { "bytes": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
...@@ -136,6 +150,14 @@ ...@@ -136,6 +150,14 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
}, },
"crc": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
"integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
"requires": {
"buffer": "^5.1.0"
}
},
"debug": { "debug": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
...@@ -233,6 +255,11 @@ ...@@ -233,6 +255,11 @@
"safer-buffer": ">= 2.1.2 < 3" "safer-buffer": ">= 2.1.2 < 3"
} }
}, },
"ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
},
"inflation": { "inflation": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz",
...@@ -243,16 +270,36 @@ ...@@ -243,16 +270,36 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"is-class-hotfix": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/is-class-hotfix/-/is-class-hotfix-0.0.6.tgz",
"integrity": "sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ=="
},
"is-generator-function": { "is-generator-function": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz",
"integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw=="
}, },
"is-type-of": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-type-of/-/is-type-of-1.2.1.tgz",
"integrity": "sha512-uK0kyX9LZYhSDS7H2sVJQJop1UnWPWmo5RvR3q2kFH6AUHYs7sOrVg0b4nyBHw29kRRNFofYN/JbHZDlHiItTA==",
"requires": {
"core-util-is": "^1.0.2",
"is-class-hotfix": "~0.0.6",
"isstream": "~0.1.2"
}
},
"isarray": { "isarray": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
}, },
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
"keygrip": { "keygrip": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
...@@ -362,6 +409,27 @@ ...@@ -362,6 +409,27 @@
} }
} }
}, },
"koa-session": {
"version": "5.13.1",
"resolved": "https://registry.npmjs.org/koa-session/-/koa-session-5.13.1.tgz",
"integrity": "sha512-TfYiun6xiFosyfIJKnEw0aoG5XmLIwM+K3OVWfkz84qY0NP2gbk0F/olRn0/Hrxq0f14s8amHVXeWyKYH3Cx3Q==",
"requires": {
"crc": "^3.4.4",
"debug": "^3.1.0",
"is-type-of": "^1.0.0",
"uuid": "^3.3.2"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
}
}
},
"koa-static": { "koa-static": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz",
...@@ -639,6 +707,11 @@ ...@@ -639,6 +707,11 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
}, },
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"vary": { "vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
"fs": "0.0.1-security", "fs": "0.0.1-security",
"koa": "^2.11.0", "koa": "^2.11.0",
"koa-body": "^4.1.1", "koa-body": "^4.1.1",
"koa-session": "^5.13.1",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"promise-mysql": "^4.1.3" "promise-mysql": "^4.1.3"
} }
......
const CONFIG = {
key: 'conqbl', /** (string) cookie key (default is koa:sess) */
/** (number || 'session') maxAge in ms (default is 1 days) */
/** 'session' will result in a cookie that expires when session/browser is closed */
/** Warning: If a session cookie is stolen, this cookie will never expire */
maxAge: 86400000,
autoCommit: true, /** (boolean) automatically commit headers (default true) */
overwrite: true, /** (boolean) can overwrite or not (default true) */
httpOnly: false, /** (boolean) httpOnly or not (default true) */
signed: true, /** (boolean) signed or not (default true) */
rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. (default is false) */
renew: false, /** (boolean) renew session when session is nearly expired, so we can always keep user logged in. (default is false)*/
sameSite: null, /** (string) session cookie sameSite options (default null, don't set it) */
};
module.exports = CONFIG;
\ No newline at end of file
require('dotenv').config()
SETTINGS = process.env;
module.exports = SETTINGS;
\ No newline at end of file
...@@ -2,57 +2,43 @@ ...@@ -2,57 +2,43 @@
const db = require('../database/database'); const db = require('../database/database');
const units = {}; const unitModel = {};
const unit_columns = ['name', 'type', 'stars', 'hp', 'pap', 'pd', 'sap', 'sd', 'bap', 'bp', 'pdf', 'sdf', 'bdf', 'leadership', 'troop_count', 'hero_level']; const unit_columns = ['name', 'type', 'stars', 'hp', 'pap', 'pd', 'sap', 'sd', 'bap', 'bp', 'pdf', 'sdf', 'bdf', 'leadership', 'troop_count', 'hero_level', 'speed', 'range', 'ammo', 'labour', 'img', 'vet_img'];
units.getAll = async (context, next) =>{ unitModel.getAll = async () =>{
const sql_text = 'SELECT * FROM units ORDER BY name ASC;';
const sql_text = 'SELECT * FROM units ORDER BY name ASC';
try{
const data = await db.con.query(sql_text); const data = await db.con.query(sql_text);
context.response.body = {units: data}; return data
}catch(error){
console.log(error);
context.throw(400, 'Invalid Data');
}
}
units.getUnit = async (context, next) =>{
try{
const units_data = await getUnit(context.params.term);
if(units_data.length===0){
context.throw(400, 'No Unit Found')
}
context.response.body = units_data[0];
context.status = 200;
}
catch(error){
console.log(error)
context.throw(400, 'No Unit Found')
}
} }
async function getUnit(term) {
unitModel.getUnit = async (term) =>{
const unit_id = parseInt(term, 10); const unit_id = parseInt(term, 10);
if(isNaN(unit_id)){ if(isNaN(unit_id)){
return await getUnitByName(term); return await unitModel.getUnitByName(term);
}else{ }else{
return await getUnitById(unit_id); return await unitModel.getUnitById(unit_id);
} }
} }
async function getUnitById(id){ unitModel.getUnitById = async (id) =>{
const sql_text = 'SELECT * FROM units WHERE id= ?'; const sql_text = 'SELECT * FROM units WHERE id= ?;';
const data = await db.con.query(sql_text, [id]); const data = await db.con.query(sql_text, [id]);
return data; if(!data[0]){
throw Error('Unit Not Found')
}
return data[0];
} }
async function getUnitByName(name){ unitModel.getUnitByName = async(name) =>{
const sql_text = 'SELECT * FROM units WHERE name LIKE ?'; const sql_text = 'SELECT * FROM units WHERE name LIKE ?;';
const data = await db.con.query(sql_text, [`%${context.params.name}%`]); const data = await db.con.query(sql_text, [`%${name}%`]);
return data; if(!data[0]){
throw Error('Unit Not Found')
}
return data[0];
} }
units.insertUnit = async (context, next) =>{ unitModel.insertUnit = async (body) =>{
const body = context.request.body;
let column_text = 'name'; let column_text = 'name';
let value_text = `${db.con.escape(body.name)}`; let value_text = `${db.con.escape(body.name)}`;
for (let i = 1; i < unit_columns.length; i++) { for (let i = 1; i < unit_columns.length; i++) {
...@@ -62,32 +48,13 @@ units.insertUnit = async (context, next) =>{ ...@@ -62,32 +48,13 @@ units.insertUnit = async (context, next) =>{
value_text += ', ' + db.con.escape(body[element]); value_text += ', ' + db.con.escape(body[element]);
} }
} }
const sql_query = 'INSERT INTO units (' + column_text + ') VALUES (' + value_text + ');'; const sql_query = `INSERT INTO units (${column_text}) VALUES (${value_text});`;
try{
const data = await db.con.query(sql_query); const data = await db.con.query(sql_query);
context.response.status = 204 return data;
}catch(error){
console.log(error);
context.throw(400, 'INVALID_DATA');
}
} }
units.modifyUnit = async (context, next) => { unitModel.modifyUnit = async (id, body) => {
const body = context.request.body;
if(!body){
context.throw(400, 'No parameters')
}
try{
const data = getUnitById(context.params.id)
if(data.length===0){
context.throw(400, 'No Unit exists')
}
}catch(error){
console.log(error);
context.throw(400, 'Invalid unit id')
}
let set_text = ''; let set_text = '';
for (let i = 0; i < unit_columns.length; i++) { for (let i = 0; i < unit_columns.length; i++) {
...@@ -100,16 +67,13 @@ units.modifyUnit = async (context, next) => { ...@@ -100,16 +67,13 @@ units.modifyUnit = async (context, next) => {
} }
} }
} }
const sql_query = `UPDATE units SET ${set_text} WHERE id = ?`; if(set_text===''){
throw Execption('No Update Arguments');
try{
const data = await db.con.query(sql_query, [context.params.id]);
context.response.status = 204
}catch(error){
console.log(error);
context.throw(400, 'Invalid Data');
} }
const sql_query = `UPDATE units SET ${set_text} WHERE id = ?;`;
const data = await db.con.query(sql_query, [id]);
return data;
} }
module.exports = units; module.exports = unitModel;
\ No newline at end of file \ No newline at end of file
...@@ -4,14 +4,68 @@ const Koa = require('koa'); ...@@ -4,14 +4,68 @@ const Koa = require('koa');
const Router = require('@koa/router'); const Router = require('@koa/router');
const router = new Router(); const router = new Router();
const unitsModel = require('./model'); const unitModel = require('./model');
router.get('/all', unitsModel.getAll); router.get('/all', async (context, next) =>{
const sql_text = 'SELECT * FROM units ORDER BY name ASC;';
try{
const data = await unitModel.getAll();
context.response.body = {units: data};
}catch(error){
console.log(error);
context.throw(500, 'Server Error');
}
});
router.get('/:term', unitsModel.getUnit); router.get('/:term', async (context, next) => {
try{
const unit_data = await unitModel.getUnit(context.params.term);
if(!unit_data){
context.throw(422, 'No Unit Found')
}
context.response.body = unit_data;
context.status = 200;
}
catch(error){
console.log(error)
context.throw(422, 'No Unit Found')
}
});
router.post('/', unitsModel.insertUnit); router.post('/', async (context, next) =>{
try{
const data = unitModel.insertUnit(context.request.body);
context.response.status = 204
}catch(error){
console.log(error);
context.throw(422, 'Invalid Data');
}
router.put('/:id', unitsModel.modifyUnit); });
router.put('/:id', async (context, next) => {
const body = context.request.body;
const unit_id = context.params.id;
if(!body){
context.throw(422, 'No Parameters')
}
try{
const data = unitModel.getUnitById(unit_id)
if(data.length===0){
context.throw(422, 'Invalid Unit Id')
}
}catch(error){
console.log(error);
context.throw(422, 'Invalid Unit Id')
}
try{
await unitModel.modifyUnit(unit_id, body);
context.status = 204
}catch(error){
console.log(error);
context.throw(422, 'Update Failed')
}
});
module.exports = router; module.exports = router;
\ No newline at end of file
'use strict' 'use strict'
const db = require('../database/database'); const db = require('../database/database');
const unitModel = require('../unit/model');
const crypto = require('../util/crypto');
const users = {}; const userModel = {};
user.getUserFromId = async (idText, id) => { const uu_columns = ['unit_level', 'elite_flg'];
let sql_text = `SELECT TOP 1 id, discord_id, house_id, leadership FROM users WHERE ${idText} = ?`;
try{ userModel.getUserFromId = async (id) => {
let data = db.con.query(sql_text,[id]); const sql_text = 'SELECT TOP 1 id, discord_id, house_id, leadership FROM users WHERE id = ?;';
return data; const data = await db.con.query(sql_text,[id]);
}catch(error){ return data[0];
return undefined; }
} userModel.getUserFromDiscord = async (discordId) =>{
const sql_text = 'SELECT id, discord_id, house_id, leadership FROM users WHERE discord_id = ?;';
const data = await db.con.query(sql_text, [discordId]);
return data[0];
} }
users.getUser = async (context, next) => { userModel.getUser = async (context, next) => {
let user = getUserFromId(context.params.id); let user = getUserFromId(context.params.id);
if(user){ if(user){
context.response.body = {user: user}; context.response.body = {user: user};
}else{ }else{
context.throw(400, 'INVALID_ID') context.throw(400, 'INVALID_ID');
} }
}; };
module.exports = users; userModel.getUserUnits = async(id) => {
\ No newline at end of file const sql_txt = `SELECT u.*, uu.unit_level, uu.elite_flg
FROM users as us
LEFT JOIN users_units as uu ON us.id = uu.user_id
LEFT JOIN units as u ON uu.unit_id = u.id
WHERE us.id = ? ORDER BY u.name ASC;`
const data = await db.con.query(sql_txt, [id]);
return data;
}
userModel.getUserUnit = async(id, term) =>{
const unit_id = parseInt(term, 10);
if(isNaN(unit_id)){
return await userModel.getUserUnitByName(id, term);
}else{
return await userModel.getUserUnitById(id, unit_id);
}
}
userModel.getUserUnitById = async (id, unit_id) =>{
const sql_text = `SELECT u.*, uu.unit_level, uu.elite_flg
FROM users as us
LEFT JOIN users_units as uu ON us.id = uu.user_id
LEFT JOIN units as u ON uu.unit_id = u.id
WHERE us.id = ? AND uu.unit_id = ? ORDER BY u.name ASC;`;
const data = await db.con.query(sql_text, [id, unit_id]);
if(!data[0]){
throw Error('Unit Not Found')
}
return data[0];
}
userModel.getUserUnitByName = async(id, name) =>{
const sql_text = `SELECT u.*, uu.unit_level, uu.elite_flg
FROM users as us
LEFT JOIN users_units as uu ON us.id = uu.user_id
LEFT JOIN units as u ON uu.unit_id = u.id
WHERE us.id = ? AND u.name LIKE ? ORDER BY u.name ASC;`
const data = await db.con.query(sql_text, [id, `%${name}%`]);
if(!data[0]){
throw Error('Unit Not Found')
}
return data[0];
}
userModel.assignUserUnit = async(id, unit_id, body) =>{
const unit = await unitModel.getUnitById(unit_id);
let column_text = 'user_id, unit_id';
let value_text = `${db.con.escape(id)}, ${db.con.escape(unit_id)}`
if(body){
for (let i = 0; i < uu_columns.length; i++) {
const element = uu_columns[i];
if(body[element]!==undefined){
column_text += ', ' + element;
value_text += ', ' + db.con.escape(body[element]);
}
}
}
const sql_text = `INSERT INTO users_units (${column_text}) VALUES (${value_text});`
const data = await db.con.query(sql_text);
}
userModel.modifyUserUnit = async(id, unit_id, body) =>{
const unit = await unitModel.getUnitById(unit_id);
let set_text = '';
for (let i = 0; i < uu_columns.length; i++) {
const element = uu_columns[i];
if(body[element]!==undefined){
if(set_text===''){
set_text += `${element} = ${db.con.escape(body[element])}`;
}else{
set_text += `, ${element} = ${db.con.escape(body[element])}`;
}
}
}
if(set_text===''){
throw Error('No Params to Update')
}
const sql_text = `UPDATE users_units SET ${set_text} WHERE user_id = ? AND unit_id = ?`
const data = await db.con.query(sql_text, [id, unit_id]);
}
userModel.deleteUserUnit = async (id, unit_id) => {
const unit = await unitModel.getUnitById(unit_id);
const sql_text = `DELETE FROM users_units WHERE user_id = ? AND unit_id = ?;`
const data = await db.con.query(sql_text, [id, unit_id]);
}
userModel.addDiscordIdToUser = async (user_id, discord_id) =>{
const sql_text = 'UPDATE users SET discord_id = ? WHERE id = ?;';
const data = await db.con.query(sql_text, [discordId, user_id])
}
userModel.createUserWithDiscord = async (discord_id) =>{
const sql_text = 'INSERT INTO users (discord_id) VALUES (?);';
await db.con.query(sql_text, [discord_id]);
}
module.exports = userModel;
\ No newline at end of file
...@@ -4,8 +4,137 @@ const Koa = require('koa'); ...@@ -4,8 +4,137 @@ const Koa = require('koa');
const Router = require('@koa/router'); const Router = require('@koa/router');
const router = new Router(); const router = new Router();
const usersModel = require('./model'); const userModel = require('./model');
router.get('/user/:id', usersModel.getUser); function checkUser(context){
if(!context.session.user_id){
context.throw(401, 'No User Logged')
}
}
router.post('/d-login', async (context, next) =>{
if(context.session.user_id && userModel.getUserFromId(context.session.user_id)){
context.throw(400, 'User is Already Logged In')
}
const body = context.request.body;
if(!body || !body.discordId){
context.throw(422, 'No Discord Id');
}
try{
const user = await userModel.getUserFromDiscord(body.discordId);
if(!user){
throw Error('No user found')
}
context.session.user_id = user.id;
context.status = 204;
}catch(error){
console.log(error);
context.throw(422, 'Login Failed')
}
});
router.get('/units', async (context, next) => {
checkUser(context);
try{
const data = await userModel.getUserUnits(context.session.user_id);
context.response.body = {units: data};
context.status = 200;
}catch(error){
console.log(error)
context.throw(400, 'No Units Found');
}
});
router.get('/unit/:term', async (context, next) =>{
checkUser(context);
try{
const data = await userModel.getUserUnit(context.session.user_id, context.params.term)
context.response.body = data;
context.status = 200;
}catch(error){
console.log(error)
context.throw(422, 'No Unit Found')
}
})
router.post('/unit', async (context, next) => {
checkUser(context);
try{
const body = context.request.body;
if(!body.unit_id){
throw Error('No Unit Id To assign')
}
await userModel.assignUserUnit(context.session.user_id, body.unit_id, body.unit_level);
context.status = 204;
}catch(error){
console.log(error);
if(error.errno===1062){
context.throw(422, 'Unit is already assigned')
}
context.throw(400, 'Could Not Assign Unit')
}
})
router.put('/unit/:unit_id', async (context, next) =>{
checkUser(context);
try{
const body = context.request.body;
if(!body){
throw Error('No Data to Modify')
}
await userModel.modifyUserUnit(context.session.user_id, context.params.unit_id, body);
context.status = 204;
}catch(error){
console.log(error);
context.throw(400, 'Could Not Modify Unit')
}
})
router.delete('/unit/:unit_id', async (context, next) =>{
checkUser(context);
try{
await userModel.deleteUserUnit(context.session.user_id, context.params.unit_id);
context.status = 204;
}catch(error){
console.log(error);
context.throw(400, 'Could Not Unassign Unit')
}
})
router.post('/discord-register', async(context, next) =>{
const body = context.request.body;
if(!body || !body.discordId){
context.throw(422, 'No Discord Id')
}
const discord_id = body.discordId;
if(context.user && !context.user.discord_id){
try{
await userModel.addDiscordIdToUser(context.user.id, discord_id);
context.status = 204;
}catch(error){
console.log(error);
context.throw(422, 'Invalid Discord Id')
}
}
let user = undefined;
try{
data = await userModel.getUserFromDiscord(discord_id);
}catch(error){
console.log(error);
context.throw(422, 'Invalid Discord Id')
}
if(!user){
try{
await userModel.createUserWithDiscord(discord_id);
context.status = 204;
}catch(error){
context.throw(422, 'Failed to create user with Discord Id')
}
}else{
context.throw(422, 'User already exists')
}
});
module.exports = router; module.exports = router;
\ No newline at end of file
'use strict'; 'use strict';
const db = require('../database/database'); const db = require('../database/database');
const crypto = require('./crypto');
async function discordAuth(context, id){ async function discordAuth(context, id){
let sql_text = `SELECT TOP 1 * FROM users WHERE discordId='${id}'`; let sql_text = `SELECT TOP 1 * FROM users WHERE discordId='${id}'`;
...@@ -22,9 +23,11 @@ module.exports = function(opts){ ...@@ -22,9 +23,11 @@ module.exports = function(opts){
const discordId = context.cookies.get('discordId'); const discordId = context.cookies.get('discordId');
const sessionId = context.cookies.get('sessionId'); const sessionId = context.cookies.get('sessionId');
if(discordId!==undefined){ if(discordId!==undefined){
dec_id = crypto.decode(discordId);
discordAuth(context, discordId); discordAuth(context, discordId);
}else if(sessionId!==undefined){ }else if(sessionId!==undefined){
sessionAuth(context, context.cookies.get('sessionId')); dec_id = crypto.decode(dec_id);
sessionAuth(context, dec_id);
}else{ }else{
context.throw(400, 'NO ID FOUND'); context.throw(400, 'NO ID FOUND');
} }
......
'use strict'
const crypto = {};
// """Someday, I will implement encryption and decryption. Today is not that day"""
crypto.encode = (data) =>{
const buff = new Buffer(data);
return buff.toString('base64');
}
crypto.decode = (enc_data) =>{
const buff = new Buffer(enc_data, 'base64');
return buff.toString('utf-8');
}
module.exports = crypto;
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment