mirror of https://github.com/calzoneman/sync.git
I think it works
This commit is contained in:
parent
bf8fef29cf
commit
5a9b3128d1
26
acp.js
26
acp.js
|
@ -9,12 +9,11 @@ The above copyright notice and this permission notice shall be included in all c
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var Server = require("./server");
|
||||
var Auth = require("./auth");
|
||||
var Database = require("./database");
|
||||
var ActionLog = require("./actionlog");
|
||||
|
||||
module.exports = {
|
||||
module.exports = function (Server) {
|
||||
return {
|
||||
init: function(user) {
|
||||
ActionLog.record(user.ip, user.name, "acp-init");
|
||||
user.socket.on("acp-announce", function(data) {
|
||||
|
@ -30,25 +29,25 @@ module.exports = {
|
|||
|
||||
user.socket.on("acp-global-ban", function(data) {
|
||||
ActionLog.record(user.ip, user.name, "acp-global-ban", data.ip);
|
||||
Database.globalBanIP(data.ip, data.note);
|
||||
user.socket.emit("acp-global-banlist", Database.refreshGlobalBans());
|
||||
Server.db.globalBanIP(data.ip, data.note);
|
||||
user.socket.emit("acp-global-banlist", Server.db.refreshGlobalBans());
|
||||
});
|
||||
|
||||
user.socket.on("acp-global-unban", function(ip) {
|
||||
ActionLog.record(user.ip, user.name, "acp-global-unban", ip);
|
||||
Database.globalUnbanIP(ip);
|
||||
user.socket.emit("acp-global-banlist", Database.refreshGlobalBans());
|
||||
Server.db.globalUnbanIP(ip);
|
||||
user.socket.emit("acp-global-banlist", Server.db.refreshGlobalBans());
|
||||
});
|
||||
|
||||
user.socket.emit("acp-global-banlist", Database.refreshGlobalBans());
|
||||
user.socket.emit("acp-global-banlist", Server.db.refreshGlobalBans());
|
||||
|
||||
user.socket.on("acp-lookup-user", function(name) {
|
||||
var db = Database.getConnection();
|
||||
var db = Server.db.getConnection();
|
||||
if(!db) {
|
||||
return;
|
||||
}
|
||||
|
||||
var query = Database.createQuery(
|
||||
var query = Server.db.createQuery(
|
||||
"SELECT id,uname,global_rank,profile_image,profile_text,email FROM registrations WHERE uname LIKE ?",
|
||||
["%"+name+"%"]
|
||||
);
|
||||
|
@ -65,7 +64,7 @@ module.exports = {
|
|||
if(Auth.getGlobalRank(data.name) >= user.global_rank)
|
||||
return;
|
||||
try {
|
||||
var hash = Database.generatePasswordReset(user.ip, data.name, data.email);
|
||||
var hash = Server.db.generatePasswordReset(user.ip, data.name, data.email);
|
||||
ActionLog.record(user.ip, user.name, "acp-reset-password", data.name);
|
||||
}
|
||||
catch(e) {
|
||||
|
@ -97,12 +96,12 @@ module.exports = {
|
|||
if(Auth.getGlobalRank(data.name) >= user.global_rank)
|
||||
return;
|
||||
|
||||
var db = Database.getConnection();
|
||||
var db = Server.db.getConnection();
|
||||
if(!db)
|
||||
return;
|
||||
|
||||
ActionLog.record(user.ip, user.name, "acp-set-rank", data);
|
||||
var query = Database.createQuery(
|
||||
var query = Server.db.createQuery(
|
||||
"UPDATE registrations SET global_rank=? WHERE uname=?",
|
||||
[data.rank, data.name]
|
||||
);
|
||||
|
@ -164,4 +163,5 @@ module.exports = {
|
|||
ActionLog.record(user.ip, user.name, "acp-actionlog-clear-one", data);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
58
channel.js
58
channel.js
|
@ -11,15 +11,11 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
|||
*/
|
||||
|
||||
var fs = require("fs");
|
||||
var Database = require("./database.js");
|
||||
var Poll = require("./poll.js").Poll;
|
||||
var Media = require("./media.js").Media;
|
||||
var formatTime = require("./media.js").formatTime;
|
||||
var Logger = require("./logger.js");
|
||||
var InfoGetter = require("./get-info.js");
|
||||
var Server = require("./server.js");
|
||||
var io = Server.io;
|
||||
var NWS = require("./notwebsocket");
|
||||
var Rank = require("./rank.js");
|
||||
var Auth = require("./auth.js");
|
||||
var ChatCommand = require("./chatcommand.js");
|
||||
|
@ -28,9 +24,10 @@ var ActionLog = require("./actionlog");
|
|||
var Playlist = require("./playlist");
|
||||
var sanitize = require("validator").sanitize;
|
||||
|
||||
var Channel = function(name) {
|
||||
var Channel = function(name, Server) {
|
||||
Logger.syslog.log("Opening channel " + name);
|
||||
this.initialized = false;
|
||||
this.server = Server;
|
||||
|
||||
this.name = name;
|
||||
// Initialize defaults
|
||||
|
@ -118,7 +115,7 @@ var Channel = function(name) {
|
|||
this.ipkey += "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[parseInt(Math.random() * 65)]
|
||||
}
|
||||
|
||||
Database.loadChannel(this);
|
||||
Server.db.loadChannel(this);
|
||||
if(this.registered) {
|
||||
this.loadDump();
|
||||
}
|
||||
|
@ -303,7 +300,7 @@ Channel.prototype.tryRegister = function(user) {
|
|||
});
|
||||
}
|
||||
else {
|
||||
if(Database.registerChannel(this.name, user.name)) {
|
||||
if(this.server.db.registerChannel(this.name, user.name)) {
|
||||
ActionLog.record(user.ip, user.name, "channel-register-success", this.name);
|
||||
this.registered = true;
|
||||
this.initialized = true;
|
||||
|
@ -325,7 +322,7 @@ Channel.prototype.tryRegister = function(user) {
|
|||
}
|
||||
|
||||
Channel.prototype.unregister = function() {
|
||||
if(Database.deleteChannel(this.name)) {
|
||||
if(this.server.db.deleteChannel(this.name)) {
|
||||
this.registered = false;
|
||||
return true;
|
||||
}
|
||||
|
@ -337,23 +334,23 @@ Channel.prototype.getRank = function(name) {
|
|||
if(!this.registered) {
|
||||
return global;
|
||||
}
|
||||
var local = Database.getChannelRank(this.name, name);
|
||||
var local = this.server.db.getChannelRank(this.name, name);
|
||||
return local > global ? local : global;
|
||||
}
|
||||
|
||||
Channel.prototype.saveRank = function(user) {
|
||||
return Database.setChannelRank(this.name, user.name, user.rank);
|
||||
return this.server.db.setChannelRank(this.name, user.name, user.rank);
|
||||
}
|
||||
|
||||
Channel.prototype.getIPRank = function(ip) {
|
||||
var names = [];
|
||||
if(!(ip in this.ip_alias))
|
||||
this.ip_alias[ip] = Database.getAliases(ip);
|
||||
this.ip_alias[ip] = this.server.db.getAliases(ip);
|
||||
this.ip_alias[ip].forEach(function(name) {
|
||||
names.push(name);
|
||||
});
|
||||
|
||||
var ranks = Database.getChannelRank(this.name, names);
|
||||
var ranks = this.server.db.getChannelRank(this.name, names);
|
||||
var rank = 0;
|
||||
for(var i = 0; i < ranks.length; i++) {
|
||||
rank = (ranks[i] > rank) ? ranks[i] : rank;
|
||||
|
@ -369,7 +366,7 @@ Channel.prototype.cacheMedia = function(media) {
|
|||
}
|
||||
this.library[media.id] = media;
|
||||
if(this.registered) {
|
||||
return Database.addToLibrary(this.name, media);
|
||||
return this.server.db.addToLibrary(this.name, media);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -403,7 +400,7 @@ Channel.prototype.tryNameBan = function(actor, name) {
|
|||
return false;
|
||||
}
|
||||
|
||||
return Database.channelBan(this.name, "*", name, actor.name);
|
||||
return this.server.db.channelBan(this.name, "*", name, actor.name);
|
||||
}
|
||||
|
||||
Channel.prototype.unbanName = function(actor, name) {
|
||||
|
@ -419,7 +416,7 @@ Channel.prototype.unbanName = function(actor, name) {
|
|||
this.users.forEach(function(u) {
|
||||
chan.sendBanlist(u);
|
||||
});
|
||||
return Database.channelUnbanName(this.name, name);
|
||||
return this.server.db.channelUnbanName(this.name, name);
|
||||
}
|
||||
|
||||
Channel.prototype.tryIPBan = function(actor, name, range) {
|
||||
|
@ -429,7 +426,7 @@ Channel.prototype.tryIPBan = function(actor, name, range) {
|
|||
if(typeof name != "string") {
|
||||
return false;
|
||||
}
|
||||
var ips = Database.ipForName(name);
|
||||
var ips = this.server.db.ipForName(name);
|
||||
var chan = this;
|
||||
ips.forEach(function(ip) {
|
||||
if(chan.getIPRank(ip) >= actor.rank) {
|
||||
|
@ -461,7 +458,7 @@ Channel.prototype.tryIPBan = function(actor, name, range) {
|
|||
return false;
|
||||
|
||||
// Update database ban table
|
||||
return Database.channelBan(chan.name, ip, name, actor.name);
|
||||
return this.server.db.channelBan(chan.name, ip, name, actor.name);
|
||||
});
|
||||
|
||||
var chan = this;
|
||||
|
@ -488,7 +485,7 @@ Channel.prototype.banIP = function(actor, receiver) {
|
|||
return false;
|
||||
|
||||
// Update database ban table
|
||||
return Database.channelBanIP(this.name, receiver.ip, receiver.name, actor.name);
|
||||
return this.server.db.channelBanIP(this.name, receiver.ip, receiver.name, actor.name);
|
||||
}
|
||||
|
||||
Channel.prototype.unbanIP = function(actor, ip) {
|
||||
|
@ -506,7 +503,7 @@ Channel.prototype.unbanIP = function(actor, ip) {
|
|||
|
||||
//this.broadcastBanlist();
|
||||
// Update database ban table
|
||||
return Database.channelUnbanIP(this.name, ip);
|
||||
return this.server.db.channelUnbanIP(this.name, ip);
|
||||
}
|
||||
|
||||
Channel.prototype.tryUnban = function(actor, data) {
|
||||
|
@ -651,7 +648,7 @@ Channel.prototype.userLeave = function(user) {
|
|||
if(this.users.length == 0) {
|
||||
this.logger.log("*** Channel empty, unloading");
|
||||
var name = this.name;
|
||||
Server.unload(this);
|
||||
this.server.unload(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -733,7 +730,7 @@ Channel.prototype.sendRankStuff = function(user) {
|
|||
|
||||
Channel.prototype.sendChannelRanks = function(user) {
|
||||
if(Rank.hasPermission(user, "acl")) {
|
||||
user.socket.emit("channelRanks", Database.listChannelRanks(this.name));
|
||||
user.socket.emit("channelRanks", this.server.db.listChannelRanks(this.name));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -777,8 +774,7 @@ Channel.prototype.sendRecentChat = function(user) {
|
|||
/* REGION Broadcasts to all clients */
|
||||
|
||||
Channel.prototype.sendAll = function(message, data) {
|
||||
io.sockets.in(this.name).emit(message, data);
|
||||
NWS.inRoom(this.name).emit(message, data);
|
||||
this.server.io.sockets.in(this.name).emit(message, data);
|
||||
}
|
||||
|
||||
Channel.prototype.sendAllWithPermission = function(perm, msg, data) {
|
||||
|
@ -811,7 +807,7 @@ Channel.prototype.broadcastUsercount = function() {
|
|||
}
|
||||
|
||||
Channel.prototype.broadcastNewUser = function(user) {
|
||||
var aliases = Database.getAliases(user.ip);
|
||||
var aliases = this.server.db.getAliases(user.ip);
|
||||
var chan = this;
|
||||
this.ip_alias[user.ip] = aliases;
|
||||
aliases.forEach(function(alias) {
|
||||
|
@ -939,7 +935,7 @@ Channel.prototype.broadcastBanlist = function() {
|
|||
}
|
||||
|
||||
Channel.prototype.broadcastRankTable = function() {
|
||||
var ranks = Database.listChannelRanks(this.name);
|
||||
var ranks = this.server.db.listChannelRanks(this.name);
|
||||
for(var i = 0; i < this.users.length; i++) {
|
||||
this.sendACL(this.users[i]);
|
||||
}
|
||||
|
@ -1299,7 +1295,7 @@ Channel.prototype.tryQueuePlaylist = function(user, data) {
|
|||
return;
|
||||
}
|
||||
|
||||
var pl = Database.loadUserPlaylist(user.name, data.name);
|
||||
var pl = this.server.db.loadUserPlaylist(user.name, data.name);
|
||||
data.list = pl;
|
||||
data.queueby = user.name;
|
||||
this.addMediaList(data, user);
|
||||
|
@ -1362,7 +1358,7 @@ Channel.prototype.tryUncache = function(user, data) {
|
|||
if(typeof data.id != "string") {
|
||||
return;
|
||||
}
|
||||
if(Database.removeFromLibrary(this.name, data.id)) {
|
||||
if(this.server.db.removeFromLibrary(this.name, data.id)) {
|
||||
delete this.library[data.id];
|
||||
}
|
||||
}
|
||||
|
@ -1866,7 +1862,7 @@ Channel.prototype.trySetRank = function(user, data) {
|
|||
var rrank = this.getRank(data.user);
|
||||
if(rrank >= user.rank)
|
||||
return;
|
||||
Database.setChannelRank(this.name, data.user, data.rank);
|
||||
this.server.db.setChannelRank(this.name, data.user, data.rank);
|
||||
}
|
||||
|
||||
this.sendAllWithPermission("acl", "setChannelRank", data);
|
||||
|
@ -1903,7 +1899,7 @@ Channel.prototype.tryPromoteUser = function(actor, data) {
|
|||
this.broadcastUserUpdate(receiver);
|
||||
}
|
||||
else {
|
||||
Database.setChannelRank(this.name, data.name, rank);
|
||||
this.server.db.setChannelRank(this.name, data.name, rank);
|
||||
}
|
||||
this.logger.log("*** " + actor.name + " promoted " + data.name + " from " + (rank - 1) + " to " + rank);
|
||||
//this.broadcastRankTable();
|
||||
|
@ -1940,7 +1936,7 @@ Channel.prototype.tryDemoteUser = function(actor, data) {
|
|||
this.broadcastUserUpdate(receiver);
|
||||
}
|
||||
else {
|
||||
Database.setChannelRank(this.name, data.name, rank);
|
||||
this.server.db.setChannelRank(this.name, data.name, rank);
|
||||
}
|
||||
this.logger.log("*** " + actor.name + " demoted " + data.name + " from " + (rank + 1) + " to " + rank);
|
||||
//this.broadcastRankTable();
|
||||
|
@ -1995,4 +1991,4 @@ Channel.prototype.tryChangeLeader = function(user, data) {
|
|||
this.changeLeader(data.name);
|
||||
}
|
||||
|
||||
exports.Channel = Channel;
|
||||
module.exports = Channel;
|
||||
|
|
|
@ -68,6 +68,7 @@ var Server = {
|
|||
ioserv: null,
|
||||
db: null,
|
||||
ips: {},
|
||||
acp: null,
|
||||
init: function () {
|
||||
this.app = express();
|
||||
// channel path
|
||||
|
@ -137,12 +138,15 @@ var Server = {
|
|||
// finally a valid user
|
||||
Logger.syslog.log("Accepted socket from /" + socket._ip);
|
||||
new User(socket, this);
|
||||
});
|
||||
}.bind(this));
|
||||
|
||||
// init database
|
||||
this.db = require("./database");
|
||||
this.db.setup(Config);
|
||||
this.db.init();
|
||||
|
||||
// init ACP
|
||||
this.acp = require("./acp")(this);
|
||||
},
|
||||
shutdown: function () {
|
||||
Logger.syslog.log("Unloading channels");
|
||||
|
@ -165,5 +169,5 @@ if(!Config.DEBUG) {
|
|||
});
|
||||
|
||||
process.on("exit", Server.shutdown);
|
||||
process.on("SIGINT", Server.shutdown);
|
||||
process.on("SIGINT", function () { process.exit(0); });
|
||||
}
|
||||
|
|
67
user.js
67
user.js
|
@ -13,16 +13,14 @@ var Rank = require("./rank.js");
|
|||
var Auth = require("./auth.js");
|
||||
var Channel = require("./channel.js").Channel;
|
||||
var formatTime = require("./media.js").formatTime;
|
||||
var Server = require("./server.js");
|
||||
var Database = require("./database.js");
|
||||
var Logger = require("./logger.js");
|
||||
var Config = require("./config.js");
|
||||
var ACP = require("./acp");
|
||||
var ActionLog = require("./actionlog");
|
||||
|
||||
// Represents a client connected via socket.io
|
||||
var User = function(socket, ip) {
|
||||
this.ip = ip;
|
||||
var User = function(socket, Server) {
|
||||
this.ip = socket._ip;
|
||||
this.server = Server;
|
||||
this.socket = socket;
|
||||
this.loggedIn = false;
|
||||
this.rank = Rank.Anonymous;
|
||||
|
@ -92,12 +90,12 @@ User.prototype.initCallbacks = function() {
|
|||
return;
|
||||
if(typeof data.name != "string")
|
||||
return;
|
||||
if(!data.name.match(/^[a-zA-Z0-9-_]+$/))
|
||||
if(!data.name.match(/^[\w-_]+$/))
|
||||
return;
|
||||
if(data.name.length > 100)
|
||||
return;
|
||||
data.name = data.name.toLowerCase();
|
||||
this.channel = Server.getOrCreateChannel(data.name);
|
||||
this.channel = this.server.getChannel(data.name);
|
||||
if(this.loggedIn) {
|
||||
var chanrank = this.channel.getRank(this.name);
|
||||
if(chanrank > this.rank) {
|
||||
|
@ -117,14 +115,6 @@ User.prototype.initCallbacks = function() {
|
|||
this.login(name, pw, session);
|
||||
}.bind(this));
|
||||
|
||||
this.socket.on("register", function(data) {
|
||||
if(data.name == undefined || data.pw == undefined)
|
||||
return;
|
||||
if(data.pw.length > 100)
|
||||
data.pw = data.pw.substring(0, 100);
|
||||
this.register(data.name, data.pw);
|
||||
}.bind(this));
|
||||
|
||||
this.socket.on("assignLeader", function(data) {
|
||||
if(this.channel != null) {
|
||||
this.channel.tryChangeLeader(this, data);
|
||||
|
@ -328,18 +318,6 @@ User.prototype.initCallbacks = function() {
|
|||
}
|
||||
}.bind(this));
|
||||
|
||||
this.socket.on("announce", function(data) {
|
||||
if(Rank.hasPermission(this, "announce")) {
|
||||
if(data.clear) {
|
||||
Server.announcement = null;
|
||||
}
|
||||
else {
|
||||
Server.io.sockets.emit("announcement", data);
|
||||
Server.announcement = data;
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.socket.on("setOptions", function(data) {
|
||||
if(this.channel != null) {
|
||||
this.channel.tryUpdateOptions(this, data);
|
||||
|
@ -420,23 +398,6 @@ User.prototype.initCallbacks = function() {
|
|||
}
|
||||
}.bind(this));
|
||||
|
||||
this.socket.on("setProfile", function(data) {
|
||||
if(!this.name) {
|
||||
return;
|
||||
}
|
||||
data.image = data.image || "";
|
||||
data.text = data.text || "";
|
||||
if(data.text.length > 4000) {
|
||||
data.text = data.text.substring(0, 4000);
|
||||
}
|
||||
if(Database.setProfile(this.name, data)) {
|
||||
this.profile = data;
|
||||
if(this.channel != null) {
|
||||
this.channel.broadcastUserUpdate(this);
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.socket.on("listPlaylists", function(data) {
|
||||
if(this.name == "" || this.rank < 1) {
|
||||
this.socket.emit("listPlaylists", {
|
||||
|
@ -446,7 +407,7 @@ User.prototype.initCallbacks = function() {
|
|||
return;
|
||||
}
|
||||
|
||||
var list = Database.getUserPlaylists(this.name);
|
||||
var list = this.server.db.getUserPlaylists(this.name);
|
||||
for(var i = 0; i < list.length; i++) {
|
||||
list[i].time = formatTime(list[i].time);
|
||||
}
|
||||
|
@ -477,12 +438,12 @@ User.prototype.initCallbacks = function() {
|
|||
}
|
||||
|
||||
var pl = this.channel.playlist.items.toArray();
|
||||
var result = Database.saveUserPlaylist(pl, this.name, data.name);
|
||||
var result = this.server.db.saveUserPlaylist(pl, this.name, data.name);
|
||||
this.socket.emit("savePlaylist", {
|
||||
success: result,
|
||||
error: result ? false : "Unknown"
|
||||
});
|
||||
var list = Database.getUserPlaylists(this.name);
|
||||
var list = this.server.db.getUserPlaylists(this.name);
|
||||
for(var i = 0; i < list.length; i++) {
|
||||
list[i].time = formatTime(list[i].time);
|
||||
}
|
||||
|
@ -502,8 +463,8 @@ User.prototype.initCallbacks = function() {
|
|||
return;
|
||||
}
|
||||
|
||||
Database.deleteUserPlaylist(this.name, data.name);
|
||||
var list = Database.getUserPlaylists(this.name);
|
||||
this.server.db.deleteUserPlaylist(this.name, data.name);
|
||||
var list = this.server.db.getUserPlaylists(this.name);
|
||||
for(var i = 0; i < list.length; i++) {
|
||||
list[i].time = formatTime(list[i].time);
|
||||
}
|
||||
|
@ -514,7 +475,7 @@ User.prototype.initCallbacks = function() {
|
|||
|
||||
this.socket.on("acp-init", function() {
|
||||
if(this.global_rank >= Rank.Siteadmin)
|
||||
ACP.init(this);
|
||||
this.server.acp.init(this);
|
||||
}.bind(this));
|
||||
|
||||
this.socket.on("borrow-rank", function(rank) {
|
||||
|
@ -580,7 +541,7 @@ User.prototype.login = function(name, pw, session) {
|
|||
lastguestlogin[this.ip] = Date.now();
|
||||
this.rank = Rank.Guest;
|
||||
Logger.syslog.log(this.ip + " signed in as " + name);
|
||||
Database.recordVisit(this.ip, name);
|
||||
this.server.db.recordVisit(this.ip, name);
|
||||
this.name = name;
|
||||
this.loggedIn = false;
|
||||
this.socket.emit("login", {
|
||||
|
@ -620,7 +581,7 @@ User.prototype.login = function(name, pw, session) {
|
|||
name: name
|
||||
});
|
||||
Logger.syslog.log(this.ip + " logged in as " + name);
|
||||
Database.recordVisit(this.ip, name);
|
||||
this.server.db.recordVisit(this.ip, name);
|
||||
this.profile = {
|
||||
image: row.profile_image,
|
||||
text: row.profile_text
|
||||
|
@ -695,4 +656,4 @@ User.prototype.register = function(name, pw) {
|
|||
}
|
||||
}
|
||||
|
||||
exports.User = User;
|
||||
module.exports = User;
|
||||
|
|
Loading…
Reference in New Issue