Commit 456fb764 authored by nanahira's avatar nanahira

Merge branch 'master' into tcg_random

parents 168eac34 b8fa792d
.git*
.dockerignore
Dockerfile*
/docs
/README.md
/*.coffee
/LICENSE
# Dockerfile for SRVPro
FROM node:12-stretch
FROM node:12-stretch-slim
# apt
RUN apt update && \
env DEBIAN_FRONTEND=noninteractive apt install -y curl wget vim sudo git build-essential libssl1.0-dev libsqlite3-dev sqlite3 mono-complete p7zip-full redis-server
env DEBIAN_FRONTEND=noninteractive apt install -y wget git build-essential libssl1.0-dev libsqlite3-dev mono-complete p7zip-full redis-server
RUN npm install -g pm2
......@@ -16,17 +16,17 @@ RUN wget 'https://github.com/libevent/libevent/releases/download/release-2.0.22-
make && \
make install && \
cd .. && \
bash -c 'ln -s /usr/local/lib/libevent-2.0.so.5 /usr/lib/libevent-2.0.so.5;ln -s /usr/local/lib/libevent_pthreads-2.0.so.5 /usr/lib/libevent_pthreads-2.0.so.5;ln -s /usr/local/lib/libevent-2.0.so.5 /usr/lib64/libevent-2.0.so.5;ln -s /usr/local/lib/libevent_pthreads-2.0.so.5 /usr/lib64/libevent_pthreads-2.0.so.5;exit 0'
bash -c 'ln -s /usr/local/lib/libevent-2.0.so.5 /usr/lib/libevent-2.0.so.5;ln -s /usr/local/lib/libevent_pthreads-2.0.so.5 /usr/lib/libevent_pthreads-2.0.so.5;ln -s /usr/local/lib/libevent-2.0.so.5 /usr/lib64/libevent-2.0.so.5;ln -s /usr/local/lib/libevent_pthreads-2.0.so.5 /usr/lib64/libevent_pthreads-2.0.so.5;exit 0' && \
rm -rf /libevent-2.0.22-stable.tar.gz /libevent-2.0.22-stable
# srvpro
COPY . /ygopro-server
WORKDIR /ygopro-server
RUN npm ci && \
mkdir config decks replays logs && \
cp data/default_config.json config/config.json
mkdir config decks replays logs /redis
# ygopro
RUN git clone --branch=server --recursive https://github.com/purerosefallen/ygopro /ygopro-server/ygopro
RUN git clone --branch=server --recursive --depth=1 https://github.com/purerosefallen/ygopro /ygopro-server/ygopro
WORKDIR /ygopro-server/ygopro
RUN git submodule foreach git checkout master && \
wget -O - https://github.com/premake/premake-core/releases/download/v5.0.0-alpha13/premake-5.0.0-alpha13-linux.tar.gz | tar zfx - && \
......@@ -39,18 +39,16 @@ RUN git submodule foreach git checkout master && \
mkdir replay expansions
# windbot
RUN git clone https://github.com/purerosefallen/windbot /ygopro-server/windbot
RUN git clone --depth=1 https://github.com/purerosefallen/windbot /ygopro-server/windbot
WORKDIR /ygopro-server/windbot
RUN xbuild /property:Configuration=Release /property:TargetFrameworkVersion="v4.5" && \
ln -s ./bin/Release/WindBot.exe . && \
ln -s /ygopro-server/ygopro/cards.cdb .
# infos
WORKDIR /
RUN mkdir /redis
WORKDIR /ygopro-server
EXPOSE 7911
EXPOSE 7922
VOLUME /ygopro-server/config
VOLUME /ygopro-server/ygopro/expansions
VOLUME [ /ygopro-server/config, /ygopro-server/decks, /ygopro-server/replays, /redis ]
CMD [ "pm2-docker", "start", "/ygopro-server/data/pm2-docker.json" ]
# Dockerfile for SRVPro Lite
FROM node:12-stretch-slim
# apt
RUN apt update && \
env DEBIAN_FRONTEND=noninteractive apt install -y wget git build-essential libssl1.0-dev libsqlite3-dev p7zip-full
# libevent
WORKDIR /
RUN wget 'https://github.com/libevent/libevent/releases/download/release-2.0.22-stable/libevent-2.0.22-stable.tar.gz' -O libevent-2.0.22-stable.tar.gz --no-check-certificate && \
tar xf libevent-2.0.22-stable.tar.gz && \
cd libevent-2.0.22-stable/ && \
./configure && \
make && \
make install && \
cd .. && \
bash -c 'ln -s /usr/local/lib/libevent-2.0.so.5 /usr/lib/libevent-2.0.so.5;ln -s /usr/local/lib/libevent_pthreads-2.0.so.5 /usr/lib/libevent_pthreads-2.0.so.5;ln -s /usr/local/lib/libevent-2.0.so.5 /usr/lib64/libevent-2.0.so.5;ln -s /usr/local/lib/libevent_pthreads-2.0.so.5 /usr/lib64/libevent_pthreads-2.0.so.5;exit 0' && \
rm -rf /libevent-2.0.22-stable.tar.gz /libevent-2.0.22-stable
# srvpro
COPY . /ygopro-server
WORKDIR /ygopro-server
RUN npm ci && \
mkdir config decks replays logs
# ygopro
RUN git clone --branch=server --recursive --depth=1 https://github.com/purerosefallen/ygopro /ygopro-server/ygopro
WORKDIR /ygopro-server/ygopro
RUN git submodule foreach git checkout master && \
wget -O - https://github.com/premake/premake-core/releases/download/v5.0.0-alpha13/premake-5.0.0-alpha13-linux.tar.gz | tar zfx - && \
./premake5 gmake && \
cd build && \
make config=release && \
cd .. && \
ln -s ./bin/release/ygopro . && \
strip ygopro && \
mkdir replay expansions
# infos
WORKDIR /ygopro-server
EXPOSE 7911
EXPOSE 7922
VOLUME [ /ygopro-server/config, /ygopro-server/decks, /ygopro-server/replays ]
CMD [ "node", "ygopro-server.js" ]
......@@ -35,11 +35,23 @@
* 安装修改后的YGOPro服务端:https://github.com/moecube/ygopro/tree/server
* `node ygopro-server.js`即可运行
* 简易的控制台在 http://srvpro.ygo233.com/dashboard.html 或 http://srvpro-cn.ygo233.com/dashboard.html
* 使用本项目的Docker镜像: https://hub.docker.com/r/mycard/ygopro-server/
* `7911`: YGOPro端口
* `7922`: 管理后台端口
* `/ygopro-server/config`: SRVPro配置文件数据卷
* `/ygopro-server/ygopro/expansions`: YGOPro额外卡片数据卷
* 使用本项目的Docker镜像: https://hub.docker.com/r/nanahira/ygopro-server/
* 镜像标签
* `nanahira/ygopro-server:latest`: 完整镜像
* `nanahira/ygopro-server:lite`: 基本镜像,云录像和人机对战功能需要配合`redis``nanahira/windbot`这两个镜像使用。
* 端口
* `7911`: YGOPro端口
* `7922`: 管理后台端口
* 数据卷
* `/ygopro-server/config`: SRVPro配置文件数据卷
* `/ygopro-server/ygopro/expansions`: YGOPro额外卡片数据卷
* `/ygopro-server/decks`: 竞赛模式卡组数据卷
* `/ygopro-server/replays`: 竞赛模式录像数据卷
* 若使用竞赛模式启动服务器,建议把启动命令修改为`pm2-docker start /ygopro-server/data/pm2-docker-tournament.js`
### 高级功能
* 待补充说明
......
......@@ -194,7 +194,8 @@
"arena_wait_timeout": "Your opponent did not appear, you may quit without any penalty.",
"auto_death_part1": "This room is an auto-extra-duel room. The Extra Duel will begin after ",
"auto_death_part2": " minutes.",
"athletic_arena_tip": "During an athletic match, a game quit behavior is regarded as a surrender."
"athletic_arena_tip": "During an athletic match, a game quit behavior is regarded as a surrender.",
"windbot_disable_random_room": "By adding the AI, this random game won't get any new player unless they enter the room name:"
},
"es-es": {
"random_duel_enter_room_waiting": "Tu oponente esta listo, Iniciar!",
......@@ -535,7 +536,8 @@
"arena_wait_timeout": "由于对手未能在30秒内进入游戏,此时您退出游戏不会扣分。",
"auto_death_part1": "本房间为自动加时赛房间。比赛开始",
"auto_death_part2": "分钟后,将自动进入加时赛。",
"athletic_arena_tip": "在竞技匹配中,比赛开始前退出游戏也会视为投降。"
"athletic_arena_tip": "在竞技匹配中,比赛开始前退出游戏也会视为投降。",
"windbot_disable_random_room": "因为添加了AI,本随机对战房间将只能通过房间名加入:"
},
"ko-kr": {
"random_duel_enter_room_waiting": "땅콩: 게임을 진행하게 준비 또는 시작을 하십시오.",
......
......@@ -27,7 +27,7 @@ or as follows, to use a specific set of permissions.
fs = require 'fs'
loadJSON = require('load-json-file').sync
moment = require 'moment'
moment.locale('zh-cn', {
moment.updateLocale('zh-cn', {
relativeTime: {
future: '%s内',
past: '%s前',
......@@ -45,13 +45,16 @@ moment.locale('zh-cn', {
}
})
bunyan = require 'bunyan'
log = bunyan.createLogger name: "auth"
if not fs.existsSync('./logs')
fs.mkdirSync('./logs')
add_log = (message) ->
mt = moment()
log.info(message)
text = mt.format('YYYY-MM-DD HH:mm:ss') + " --> " + message + "\n"
console.log(text)
res = false
try
fs.appendFileSync("./logs/"+mt.format('YYYY-MM-DD')+".log", text)
......
......@@ -28,7 +28,7 @@ or as follows, to use a specific set of permissions.
*/
(function() {
var add_log, check_permission, default_data, fs, loadJSON, moment, reload, save, setting_save, users;
var add_log, bunyan, check_permission, default_data, fs, loadJSON, log, moment, reload, save, setting_save, users;
fs = require('fs');
......@@ -36,7 +36,7 @@ or as follows, to use a specific set of permissions.
moment = require('moment');
moment.locale('zh-cn', {
moment.updateLocale('zh-cn', {
relativeTime: {
future: '%s内',
past: '%s前',
......@@ -54,6 +54,12 @@ or as follows, to use a specific set of permissions.
}
});
bunyan = require('bunyan');
log = bunyan.createLogger({
name: "auth"
});
if (!fs.existsSync('./logs')) {
fs.mkdirSync('./logs');
}
......@@ -61,8 +67,8 @@ or as follows, to use a specific set of permissions.
add_log = function(message) {
var mt, res, text;
mt = moment();
log.info(message);
text = mt.format('YYYY-MM-DD HH:mm:ss') + " --> " + message + "\n";
console.log(text);
res = false;
try {
fs.appendFileSync("./logs/" + mt.format('YYYY-MM-DD') + ".log", text);
......
......@@ -22,7 +22,7 @@ bunyan = require 'bunyan'
log = bunyan.createLogger name: "mycard"
moment = require 'moment'
moment.locale('zh-cn', {
moment.updateLocale('zh-cn', {
relativeTime: {
future: '%s内',
past: '%s前',
......@@ -378,6 +378,14 @@ if settings.modules.cloud_replay.enabled
if settings.modules.windbot.enabled
windbots = loadJSON(settings.modules.windbot.botlist).windbots
real_windbot_server_ip = settings.modules.windbot.server_ip
if !settings.modules.windbot.server_ip.includes("127.0.0.1")
dns = require('dns')
dns.lookup(settings.modules.windbot.server_ip,(err,addr) ->
if(!err)
real_windbot_server_ip = addr
)
if settings.modules.heartbeat_detection.enabled
long_resolve_cards = loadJSON('./data/long_resolve_cards.json')
......@@ -664,7 +672,7 @@ ROOM_find_or_create_random = (type, player_ip)->
max_player = if type == 'T' then 4 else 2
playerbanned = (bannedplayer and bannedplayer.count > 3 and moment() < bannedplayer.time)
result = _.find ROOM_all, (room)->
return room and room.random_type != '' and !room.started and
return room and room.random_type != '' and !room.started and !room.windbot and
((type == '' and room.random_type != 'T' and ((!_.endsWith(room.random_type, "MR") and room.random_type != 'M') or (settings.modules.random_duel.blank_pass_match and room.random_type != 'T'))) or room.random_type == type) and
room.get_playing_player().length < max_player and
(settings.modules.random_duel.no_rematch_check or room.get_host() == null or
......@@ -1649,7 +1657,7 @@ class Room
# 网络连接
net.createServer (client) ->
client.ip = client.remoteAddress
client.is_local = client.ip and (client.ip.includes('127.0.0.1') or client.ip.includes(settings.modules.windbot.server_ip))
client.is_local = client.ip and (client.ip.includes('127.0.0.1') or client.ip.includes(real_windbot_server_ip))
connect_count = ROOM_connected_ip[client.ip] or 0
if !settings.modules.test_mode.no_connect_count_limit and !client.is_local
......@@ -3258,6 +3266,8 @@ ygopro.ctos_follow 'CHAT', true, (buffer, info, client, server, datas)->
return
else
windbot = _.sample windbots
if room.random_type
ygopro.stoc_send_chat(client, "${windbot_disable_random_room} " + room.name, ygopro.constants.COLORS.BABYBLUE)
room.add_windbot(windbot)
when '/roomname'
......@@ -3394,7 +3404,7 @@ ygopro.ctos_follow 'CHAT', true, (buffer, info, client, server, datas)->
return cancel
if client.abuse_count>=5 or CLIENT_is_banned_by_mc(client)
log.warn "BANNED CHAT", client.name, client.ip, msg
ygopro.stoc_send_chat(client, "${banned_chat_tip}", ygopro.constants.COLORS.RED)
ygopro.stoc_send_chat(client, "${banned_chat_tip}" + (if client.ban_mc and client.ban_mc.message then (": " + client.ban_mc.message) else ""), ygopro.constants.COLORS.RED)
return true
oldmsg = msg
if (_.any(badwords.level3, (badword) ->
......@@ -4056,7 +4066,7 @@ if settings.modules.http
return
else
getpath=u.pathname.split("/")
filename=decodeURIComponent(getpath.pop())
filename=path.basename(decodeURIComponent(getpath.pop()))
fs.readFile(settings.modules.tournament_mode.replay_path + filename, (error, buffer)->
if error
response.writeHead(404)
......
// Generated by CoffeeScript 1.12.7
(function() {
var CLIENT_check_vip, CLIENT_get_absolute_pos, CLIENT_get_authorize_key, CLIENT_get_kick_reconnect_target, CLIENT_get_partner, CLIENT_heartbeat_register, CLIENT_heartbeat_unregister, CLIENT_import_data, CLIENT_is_able_to_kick_reconnect, CLIENT_is_able_to_reconnect, CLIENT_is_banned_by_mc, CLIENT_is_player, CLIENT_kick, CLIENT_kick_reconnect, CLIENT_pre_reconnect, CLIENT_reconnect, CLIENT_reconnect_register, CLIENT_reconnect_unregister, CLIENT_send_pre_reconnect_info, CLIENT_send_reconnect_info, CLIENT_send_replays, CLIENT_send_vip_status, CLIENT_use_cdkey, Cloud_replay_ids, ROOM_all, ROOM_bad_ip, ROOM_ban_player, ROOM_clear_disconnect, ROOM_connected_ip, ROOM_find_by_name, ROOM_find_by_port, ROOM_find_by_title, ROOM_find_or_create_ai, ROOM_find_or_create_by_name, ROOM_find_or_create_random, ROOM_player_flee, ROOM_player_get_score, ROOM_player_lose, ROOM_player_win, ROOM_players_banned, ROOM_players_oppentlist, ROOM_players_scores, ROOM_unwelcome, ROOM_validate, Room, SERVER_clear_disconnect, SOCKET_flush_data, VIP_generate_cdkeys, _, addCallback, auth, badwords, ban_user, bunyan, challonge, challonge_cache, challonge_module_name, challonge_queue_callbacks, chat_color, concat_name, config, cppversion, crypto, date, default_config, default_data, dialogues, disconnect_list, duel_log, e, exec, execFile, fs, geoip, get_callback, get_memory_usage, http, http_server, https, https_server, import_datas, imported, is_requesting, j, k, l, len1, len2, lflists, list, loadJSON, load_dialogues, load_dialogues_custom, load_tips, load_tips_zh, load_words, log, long_resolve_cards, memory_usage, merge, moment, net, oldbadwords, oldconfig, olddialogues, oldduellog, oldtips, oldwords, options, os, path, pgClient, pg_client, pg_query, rebooted, redis, redisdb, ref, ref1, ref2, refresh_challonge_cache, release_disconnect, replace_buffer, report_to_big_brother, request, requestListener, roomlist, setting_change, setting_save, settings, spawn, spawnSync, spawn_windbot, sqlite3, tips, url, users_cache, v, vip_info, wait_room_start, wait_room_start_arena, windbot_looplimit, windbot_process, windbots, words, ygopro, zlib;
var CLIENT_check_vip, CLIENT_get_absolute_pos, CLIENT_get_authorize_key, CLIENT_get_kick_reconnect_target, CLIENT_get_partner, CLIENT_heartbeat_register, CLIENT_heartbeat_unregister, CLIENT_import_data, CLIENT_is_able_to_kick_reconnect, CLIENT_is_able_to_reconnect, CLIENT_is_banned_by_mc, CLIENT_is_player, CLIENT_kick, CLIENT_kick_reconnect, CLIENT_pre_reconnect, CLIENT_reconnect, CLIENT_reconnect_register, CLIENT_reconnect_unregister, CLIENT_send_pre_reconnect_info, CLIENT_send_reconnect_info, CLIENT_send_replays, CLIENT_send_vip_status, CLIENT_use_cdkey, Cloud_replay_ids, ROOM_all, ROOM_bad_ip, ROOM_ban_player, ROOM_clear_disconnect, ROOM_connected_ip, ROOM_find_by_name, ROOM_find_by_port, ROOM_find_by_title, ROOM_find_or_create_ai, ROOM_find_or_create_by_name, ROOM_find_or_create_random, ROOM_player_flee, ROOM_player_get_score, ROOM_player_lose, ROOM_player_win, ROOM_players_banned, ROOM_players_oppentlist, ROOM_players_scores, ROOM_unwelcome, ROOM_validate, Room, SERVER_clear_disconnect, SOCKET_flush_data, VIP_generate_cdkeys, _, addCallback, auth, badwords, ban_user, bunyan, challonge, challonge_cache, challonge_module_name, challonge_queue_callbacks, chat_color, concat_name, config, cppversion, crypto, date, default_config, default_data, dialogues, disconnect_list, dns, duel_log, e, exec, execFile, fs, geoip, get_callback, get_memory_usage, http, http_server, https, https_server, import_datas, imported, is_requesting, j, k, l, len1, len2, lflists, list, loadJSON, load_dialogues, load_dialogues_custom, load_tips, load_tips_zh, load_words, log, long_resolve_cards, memory_usage, merge, moment, net, oldbadwords, oldconfig, olddialogues, oldduellog, oldtips, oldwords, options, os, path, pgClient, pg_client, pg_query, real_windbot_server_ip, rebooted, redis, redisdb, ref, ref1, ref2, refresh_challonge_cache, release_disconnect, replace_buffer, report_to_big_brother, request, requestListener, roomlist, setting_change, setting_save, settings, spawn, spawnSync, spawn_windbot, sqlite3, tips, url, users_cache, v, vip_info, wait_room_start, wait_room_start_arena, windbot_looplimit, windbot_process, windbots, words, ygopro, zlib;
net = require('net');
......@@ -40,7 +40,7 @@
moment = require('moment');
moment.locale('zh-cn', {
moment.updateLocale('zh-cn', {
relativeTime: {
future: '%s内',
past: '%s前',
......@@ -467,6 +467,15 @@
if (settings.modules.windbot.enabled) {
windbots = loadJSON(settings.modules.windbot.botlist).windbots;
real_windbot_server_ip = settings.modules.windbot.server_ip;
if (!settings.modules.windbot.server_ip.includes("127.0.0.1")) {
dns = require('dns');
dns.lookup(settings.modules.windbot.server_ip, function(err, addr) {
if (!err) {
return real_windbot_server_ip = addr;
}
});
}
}
if (settings.modules.heartbeat_detection.enabled) {
......@@ -856,7 +865,7 @@
max_player = type === 'T' ? 4 : 2;
playerbanned = bannedplayer && bannedplayer.count > 3 && moment() < bannedplayer.time;
result = _.find(ROOM_all, function(room) {
return room && room.random_type !== '' && !room.started && ((type === '' && room.random_type !== 'T' && ((!_.endsWith(room.random_type, "MR") && room.random_type !== 'M') || (settings.modules.random_duel.blank_pass_match && room.random_type !== 'T'))) || room.random_type === type) && room.get_playing_player().length < max_player && (settings.modules.random_duel.no_rematch_check || room.get_host() === null || room.get_host().ip !== ROOM_players_oppentlist[player_ip]) && (playerbanned === room.deprecated || type === 'T');
return room && room.random_type !== '' && !room.started && !room.windbot && ((type === '' && room.random_type !== 'T' && ((!_.endsWith(room.random_type, "MR") && room.random_type !== 'M') || (settings.modules.random_duel.blank_pass_match && room.random_type !== 'T'))) || room.random_type === type) && room.get_playing_player().length < max_player && (settings.modules.random_duel.no_rematch_check || room.get_host() === null || room.get_host().ip !== ROOM_players_oppentlist[player_ip]) && (playerbanned === room.deprecated || type === 'T');
});
if (result) {
result.welcome = '${random_duel_enter_room_waiting}';
......@@ -2166,7 +2175,7 @@
net.createServer(function(client) {
var connect_count, server;
client.ip = client.remoteAddress;
client.is_local = client.ip && (client.ip.includes('127.0.0.1') || client.ip.includes(settings.modules.windbot.server_ip));
client.is_local = client.ip && (client.ip.includes('127.0.0.1') || client.ip.includes(real_windbot_server_ip));
connect_count = ROOM_connected_ip[client.ip] || 0;
if (!settings.modules.test_mode.no_connect_count_limit && !client.is_local) {
connect_count++;
......@@ -4192,6 +4201,9 @@
} else {
windbot = _.sample(windbots);
}
if (room.random_type) {
ygopro.stoc_send_chat(client, "${windbot_disable_random_room} " + room.name, ygopro.constants.COLORS.BABYBLUE);
}
room.add_windbot(windbot);
}
break;
......@@ -4367,7 +4379,7 @@
}
if (client.abuse_count >= 5 || CLIENT_is_banned_by_mc(client)) {
log.warn("BANNED CHAT", client.name, client.ip, msg);
ygopro.stoc_send_chat(client, "${banned_chat_tip}", ygopro.constants.COLORS.RED);
ygopro.stoc_send_chat(client, "${banned_chat_tip}" + (client.ban_mc && client.ban_mc.message ? ": " + client.ban_mc.message : ""), ygopro.constants.COLORS.RED);
return true;
}
oldmsg = msg;
......@@ -5279,7 +5291,7 @@
return;
} else {
getpath = u.pathname.split("/");
filename = decodeURIComponent(getpath.pop());
filename = path.basename(decodeURIComponent(getpath.pop()));
fs.readFile(settings.modules.tournament_mode.replay_path + filename, function(error, buffer) {
if (error) {
response.writeHead(404);
......
......@@ -10,6 +10,7 @@
*/
var http = require('http');
var https = require('https');
var fs = require('fs');
var execSync = require('child_process').execSync;
var spawn = require('child_process').spawn;
......@@ -19,10 +20,11 @@ var moment = require('moment');
moment.locale('zh-cn');
var loadJSON = require('load-json-file').sync;
var constants = loadJSON('./data/constants.json');
//var constants = loadJSON('./data/constants.json');
var settings = loadJSON('./config/config.json');
config=settings.modules.webhook;
config = settings.modules.webhook;
ssl_config = settings.modules.http.ssl;
var status = {};
......@@ -174,8 +176,7 @@ var return_success = function(res, msg) {
return;
}
//will create an http server to receive apis
http.createServer(function (req, res) {
var requestListener = function (req, res) {
var u = url.parse(req.url, true);
var data = u.pathname.split("/");
if (data[0] !== "" || data[1] !== "api") {
......@@ -219,5 +220,17 @@ http.createServer(function (req, res) {
}
});
return;
}).listen(config.port);
}
//will create an http server to receive apis
if (ssl_config.enabled) {
const ssl_cert = fs.readFileSync(ssl_config.cert);
const ssl_key = fs.readFileSync(ssl_config.key);
const options = {
cert: ssl_cert,
key: ssl_key
}
https.createServer(options, requestListener).listen(config.port);
} else {
http.createServer(requestListener).listen(config.port);
}
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