forked from GitHub/dbot
Modularised web routes and views
This commit is contained in:
parent
88d2abab58
commit
e544c909ec
@ -217,10 +217,36 @@ var poll = function(dbot) {
|
|||||||
commands['~pdesc'].regex = [/~pdesc ([^ ]+)/, 2];
|
commands['~pdesc'].regex = [/~pdesc ([^ ]+)/, 2];
|
||||||
commands['~count'].regex = [/~count ([^ ]+)/, 2];
|
commands['~count'].regex = [/~count ([^ ]+)/, 2];
|
||||||
|
|
||||||
|
var pages = {
|
||||||
|
// Shows the results of a poll
|
||||||
|
'/polls/:key': function(req, res) {
|
||||||
|
var key = req.params.key.toLowerCase();
|
||||||
|
if(dbot.db.polls.hasOwnProperty(key) && dbot.db.polls[key].hasOwnProperty('description')) {
|
||||||
|
// tally the votes
|
||||||
|
var totalVotes = 0;
|
||||||
|
for( var v in dbot.db.polls[key].votes ) {
|
||||||
|
var N = Number(dbot.db.polls[key].votes[v]);
|
||||||
|
if( !isNaN(N) ) {
|
||||||
|
totalVotes += N;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.render('polls', { 'name': dbot.config.name, 'description': dbot.db.polls[key].description, 'votees': Object.keys(dbot.db.polls[key].votees), 'options': dbot.db.polls[key].votes, locals: { 'totalVotes': totalVotes, 'url_regex': RegExp.prototype.url_regex() } });
|
||||||
|
} else {
|
||||||
|
res.render('error', { 'name': dbot.config.name, 'message': 'No polls under that key.' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Lists all of the polls
|
||||||
|
'/polls': function(req, res) {
|
||||||
|
res.render('polllist', { 'name': dbot.config.name, 'polllist': Object.keys(dbot.db.polls) });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'name': 'poll',
|
'name': 'poll',
|
||||||
'ignorable': true,
|
'ignorable': true,
|
||||||
'commands': commands
|
'commands': commands,
|
||||||
|
'pages': pages
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -267,10 +267,34 @@ var quotes = function(dbot) {
|
|||||||
commands['~rmlast'].regex = [/^~rmlast ([\d\w\s-]*)/, 2];
|
commands['~rmlast'].regex = [/^~rmlast ([\d\w\s-]*)/, 2];
|
||||||
commands['~qadd'].regex = [/^~qadd ([\d\w\s-]+?)[ ]?=[ ]?(.+)$/, 3];
|
commands['~qadd'].regex = [/^~qadd ([\d\w\s-]+?)[ ]?=[ ]?(.+)$/, 3];
|
||||||
|
|
||||||
|
var pages = {
|
||||||
|
// Lists quotes in a category
|
||||||
|
'/quotes/:key': function(req, res) {
|
||||||
|
var key = req.params.key.toLowerCase();
|
||||||
|
if(dbot.db.quoteArrs.hasOwnProperty(key)) {
|
||||||
|
res.render('quotes', { 'name': dbot.config.name, 'quotes': dbot.db.quoteArrs[key], locals: { 'url_regex': RegExp.prototype.url_regex() } });
|
||||||
|
} else {
|
||||||
|
res.render('error', { 'name': dbot.config.name, 'message': 'No quotes under that key.' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show quote list.
|
||||||
|
'/quotes': function(req, res) {
|
||||||
|
res.render('quotelist', { 'name': dbot.config.name, 'quotelist': Object.keys(dbot.db.quoteArrs) });
|
||||||
|
},
|
||||||
|
|
||||||
|
// Load random quote category page
|
||||||
|
'/rq': function(req, res) {
|
||||||
|
var rCategory = Object.keys(dbot.db.quoteArrs).random();
|
||||||
|
res.render('quotes', { 'name': dbot.config.name, 'quotes': dbot.db.quoteArrs[rCategory], locals: { 'url_regex': RegExp.prototype.url_regex() } });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'name': 'quotes',
|
'name': 'quotes',
|
||||||
'ignorable': true,
|
'ignorable': true,
|
||||||
'commands': commands,
|
'commands': commands,
|
||||||
|
'pages': pages,
|
||||||
|
|
||||||
'listener': function(event) {
|
'listener': function(event) {
|
||||||
// Reality Once listener
|
// Reality Once listener
|
||||||
|
@ -21,6 +21,65 @@ var users = function(dbot) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var pages = {
|
||||||
|
'/connections': function(req, res) {
|
||||||
|
var connections = Object.keys(dbot.instance.connections);
|
||||||
|
res.render('connections', { 'name': dbot.config.name, 'connections': connections });
|
||||||
|
},
|
||||||
|
|
||||||
|
'/channels/:connection': function(req, res) {
|
||||||
|
var connection = req.params.connection;
|
||||||
|
if(dbot.instance.connections.hasOwnProperty(connection)) {
|
||||||
|
var channels = Object.keys(dbot.instance.connections[connection].channels);
|
||||||
|
res.render('channels', { 'name': dbot.config.name, 'connection': connection, 'channels': channels});
|
||||||
|
} else {
|
||||||
|
res.render_core('error', { 'name': dbot.config.name, 'message': 'No such connection.' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'/users/:connection/:channel': function(req, res) {
|
||||||
|
var connection = req.params.connection;
|
||||||
|
var channel = '#' + req.params.channel;
|
||||||
|
var connections = dbot.instance.connections;
|
||||||
|
|
||||||
|
if(connections.hasOwnProperty(connection) &&
|
||||||
|
connections[connection].channels.hasOwnProperty(channel)) {
|
||||||
|
var nicks = Object.keys(connections[connection].channels[channel].nicks);
|
||||||
|
res.render('users', { 'name': dbot.config.name, 'connection': connection,
|
||||||
|
'channel': channel, 'nicks': nicks });
|
||||||
|
} else {
|
||||||
|
res.render_core('error', { 'name': dbot.config.name, 'message': 'No such connection or channel.' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'/user/:connection/:channel/:user': function(req, res) {
|
||||||
|
var connection = req.params.connection;
|
||||||
|
var channel = '#' + req.params.channel;
|
||||||
|
var user = dbot.cleanNick(req.params.user);
|
||||||
|
|
||||||
|
var quoteCount = 'no';
|
||||||
|
if(dbot.db.quoteArrs.hasOwnProperty(user)) {
|
||||||
|
var quoteCount = dbot.db.quoteArrs[user].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!dbot.db.kicks.hasOwnProperty(req.params.user)) {
|
||||||
|
var kicks = '0';
|
||||||
|
} else {
|
||||||
|
var kicks = dbot.db.kicks[req.params.user];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!dbot.db.kickers.hasOwnProperty(req.params.user)) {
|
||||||
|
var kicked = '0';
|
||||||
|
} else {
|
||||||
|
var kicked = dbot.db.kickers[req.params.user];
|
||||||
|
}
|
||||||
|
|
||||||
|
res.render('user', { 'name': dbot.config.name, 'user': req.params.user,
|
||||||
|
'channel': channel, 'connection': connection, 'cleanUser': user,
|
||||||
|
'quotecount': quoteCount, 'kicks': kicks, 'kicked': kicked });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'name': 'users',
|
'name': 'users',
|
||||||
'ignorable': false,
|
'ignorable': false,
|
||||||
@ -37,6 +96,8 @@ var users = function(dbot) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'pages': pages,
|
||||||
|
|
||||||
'listener': function(event) {
|
'listener': function(event) {
|
||||||
var knownUsers = getServerUsers(event);
|
var knownUsers = getServerUsers(event);
|
||||||
if(event.action == 'JOIN') {
|
if(event.action == 'JOIN') {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
var express = require('express');
|
var express = require('express'),
|
||||||
|
fs = require('fs');
|
||||||
|
|
||||||
var webInterface = function(dbot) {
|
var webInterface = function(dbot) {
|
||||||
var pub = 'public';
|
var pub = 'public';
|
||||||
@ -11,112 +12,32 @@ var webInterface = function(dbot) {
|
|||||||
res.render('index', { 'name': dbot.config.name });
|
res.render('index', { 'name': dbot.config.name });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/connections', function(req, res) {
|
|
||||||
var connections = Object.keys(dbot.instance.connections);
|
|
||||||
res.render('connections', { 'name': dbot.config.name, 'connections': connections });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/channels/:connection', function(req, res) {
|
|
||||||
var connection = req.params.connection;
|
|
||||||
if(dbot.instance.connections.hasOwnProperty(connection)) {
|
|
||||||
var channels = Object.keys(dbot.instance.connections[connection].channels);
|
|
||||||
res.render('channels', { 'name': dbot.config.name, 'connection': connection, 'channels': channels});
|
|
||||||
} else {
|
|
||||||
res.render('error', { 'name': dbot.config.name, 'message': 'No such connection.' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/users/:connection/:channel', function(req, res) {
|
|
||||||
var connection = req.params.connection;
|
|
||||||
var channel = '#' + req.params.channel;
|
|
||||||
var connections = dbot.instance.connections;
|
|
||||||
|
|
||||||
if(connections.hasOwnProperty(connection) &&
|
|
||||||
connections[connection].channels.hasOwnProperty(channel)) {
|
|
||||||
var nicks = Object.keys(connections[connection].channels[channel].nicks);
|
|
||||||
res.render('users', { 'name': dbot.config.name, 'connection': connection,
|
|
||||||
'channel': channel, 'nicks': nicks });
|
|
||||||
} else {
|
|
||||||
res.render('error', { 'name': dbot.config.name, 'message': 'No such connection or channel.' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/user/:connection/:channel/:user', function(req, res) {
|
|
||||||
var connection = req.params.connection;
|
|
||||||
var channel = '#' + req.params.channel;
|
|
||||||
var user = dbot.cleanNick(req.params.user);
|
|
||||||
|
|
||||||
var quoteCount = 'no';
|
|
||||||
if(dbot.db.quoteArrs.hasOwnProperty(user)) {
|
|
||||||
var quoteCount = dbot.db.quoteArrs[user].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!dbot.db.kicks.hasOwnProperty(req.params.user)) {
|
|
||||||
var kicks = '0';
|
|
||||||
} else {
|
|
||||||
var kicks = dbot.db.kicks[req.params.user];
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!dbot.db.kickers.hasOwnProperty(req.params.user)) {
|
|
||||||
var kicked = '0';
|
|
||||||
} else {
|
|
||||||
var kicked = dbot.db.kickers[req.params.user];
|
|
||||||
}
|
|
||||||
|
|
||||||
res.render('user', { 'name': dbot.config.name, 'user': req.params.user,
|
|
||||||
'channel': channel, 'connection': connection, 'cleanUser': user,
|
|
||||||
'quotecount': quoteCount, 'kicks': kicks, 'kicked': kicked });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Lists the quote categories
|
|
||||||
app.get('/quotes', function(req, res) {
|
|
||||||
res.render('quotelist', { 'name': dbot.config.name, 'quotelist': Object.keys(dbot.db.quoteArrs) });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Lists quotes in a category
|
|
||||||
app.get('/quotes/:key', function(req, res) {
|
|
||||||
var key = req.params.key.toLowerCase();
|
|
||||||
if(dbot.db.quoteArrs.hasOwnProperty(key)) {
|
|
||||||
res.render('quotes', { 'name': dbot.config.name, 'quotes': dbot.db.quoteArrs[key], locals: { 'url_regex': RegExp.prototype.url_regex() } });
|
|
||||||
} else {
|
|
||||||
res.render('error', { 'name': dbot.config.name, 'message': 'No quotes under that key.' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load random quote category page
|
|
||||||
app.get('/rq', function(req, res) {
|
|
||||||
var rCategory = Object.keys(dbot.db.quoteArrs).random();
|
|
||||||
res.render('quotes', { 'name': dbot.config.name, 'quotes': dbot.db.quoteArrs[rCategory], locals: { 'url_regex': RegExp.prototype.url_regex() } });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Lists all of the polls
|
|
||||||
app.get('/polls', function(req, res) {
|
|
||||||
res.render('polllist', { 'name': dbot.config.name, 'polllist': Object.keys(dbot.db.polls) });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Shows the results of a poll
|
|
||||||
app.get('/polls/:key', function(req, res) {
|
|
||||||
var key = req.params.key.toLowerCase();
|
|
||||||
if(dbot.db.polls.hasOwnProperty(key) && dbot.db.polls[key].hasOwnProperty('description')) {
|
|
||||||
// tally the votes
|
|
||||||
var totalVotes = 0;
|
|
||||||
for( var v in dbot.db.polls[key].votes ) {
|
|
||||||
var N = Number(dbot.db.polls[key].votes[v]);
|
|
||||||
if( !isNaN(N) ) {
|
|
||||||
totalVotes += N;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.render('polls', { 'name': dbot.config.name, 'description': dbot.db.polls[key].description, 'votees': Object.keys(dbot.db.polls[key].votees), 'options': dbot.db.polls[key].votes, locals: { 'totalVotes': totalVotes, 'url_regex': RegExp.prototype.url_regex() } });
|
|
||||||
} else {
|
|
||||||
res.render('error', { 'name': dbot.config.name, 'message': 'No polls under that key.' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(dbot.config.web.webPort);
|
app.listen(dbot.config.web.webPort);
|
||||||
|
|
||||||
|
var reloadPages = function(pages) {
|
||||||
|
for(var p in pages) {
|
||||||
|
if( pages.hasOwnProperty(p) ) {
|
||||||
|
var func = pages[p];
|
||||||
|
var mod = func.module;
|
||||||
|
app.get(p, (function(req, resp) {
|
||||||
|
// Crazy shim to seperate module views.
|
||||||
|
var shim = Object.create(resp);
|
||||||
|
shim.render = (function(view, one, two) {
|
||||||
|
// Render with express.js
|
||||||
|
resp.render(this.module.name + '/' + view, one, two);
|
||||||
|
}).bind(this);
|
||||||
|
shim.render_core = resp.render;
|
||||||
|
this.call(this.module, req, shim);
|
||||||
|
}).bind(func));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'name': 'web',
|
'name': 'web',
|
||||||
'ignorable': false,
|
'ignorable': false,
|
||||||
|
'reloadPages': reloadPages,
|
||||||
|
|
||||||
'onDestroy': function() {
|
'onDestroy': function() {
|
||||||
app.close();
|
app.close();
|
||||||
|
24
run.js
24
run.js
@ -118,6 +118,7 @@ DBot.prototype.reloadModules = function() {
|
|||||||
|
|
||||||
this.rawModules = [];
|
this.rawModules = [];
|
||||||
this.modules = [];
|
this.modules = [];
|
||||||
|
this.pages = {};
|
||||||
this.commands = {};
|
this.commands = {};
|
||||||
this.commandMap = {}; // Map of which commands belong to which modules
|
this.commandMap = {}; // Map of which commands belong to which modules
|
||||||
this.usage = {};
|
this.usage = {};
|
||||||
@ -168,6 +169,8 @@ DBot.prototype.reloadModules = function() {
|
|||||||
var module = rawModule.fetch(this);
|
var module = rawModule.fetch(this);
|
||||||
this.rawModules.push(rawModule);
|
this.rawModules.push(rawModule);
|
||||||
|
|
||||||
|
module.name = name;
|
||||||
|
|
||||||
if(module.listener) {
|
if(module.listener) {
|
||||||
var listenOn = module.on;
|
var listenOn = module.on;
|
||||||
if(!(listenOn instanceof Array)) {
|
if(!(listenOn instanceof Array)) {
|
||||||
@ -194,6 +197,18 @@ DBot.prototype.reloadModules = function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load module web bits
|
||||||
|
if(module.pages) {
|
||||||
|
var newpages = module.pages;
|
||||||
|
for(var key in newpages)
|
||||||
|
{
|
||||||
|
if(newpages.hasOwnProperty(key) && Object.prototype.isFunction(newpages[key])) {
|
||||||
|
this.pages[key] = newpages[key];
|
||||||
|
this.pages[key].module = module;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load the module usage data
|
// Load the module usage data
|
||||||
try {
|
try {
|
||||||
var usage = JSON.parse(fs.readFileSync(moduleDir + 'usage.json', 'utf-8'));
|
var usage = JSON.parse(fs.readFileSync(moduleDir + 'usage.json', 'utf-8'));
|
||||||
@ -237,9 +252,18 @@ DBot.prototype.reloadModules = function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
this.reloadPages();
|
||||||
this.save();
|
this.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DBot.prototype.reloadPages = function() {
|
||||||
|
for( var m in this.modules ) {
|
||||||
|
if( Object.prototype.isFunction(this.modules[m].reloadPages)) {
|
||||||
|
this.modules[m].reloadPages(this.pages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DBot.prototype.cleanNick = function(key) {
|
DBot.prototype.cleanNick = function(key) {
|
||||||
key = key.toLowerCase();
|
key = key.toLowerCase();
|
||||||
while(key.endsWith("_")) {
|
while(key.endsWith("_")) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
extends layout
|
extends ../layout
|
||||||
|
|
||||||
block content
|
block content
|
||||||
div#backlink
|
div#backlink
|
39
views/poll/polls.jade
Normal file
39
views/poll/polls.jade
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
extends ../layout
|
||||||
|
|
||||||
|
block content
|
||||||
|
div#backlink
|
||||||
|
a(href='/polls/') « Poll list
|
||||||
|
h2 #{description}
|
||||||
|
p Voters (#{locals.totalVotes}):
|
||||||
|
-each voter in votees
|
||||||
|
| #{voter}
|
||||||
|
ul#votelist
|
||||||
|
-var hasYouTubeVids=false
|
||||||
|
-each votes,option in options
|
||||||
|
-var percentage = votes/locals.totalVotes*100
|
||||||
|
-if(options.hasOwnProperty(option))
|
||||||
|
-if(option.match(locals.url_regex))
|
||||||
|
li.option
|
||||||
|
-if(option.match(/(jpg|png|gif|jpeg|tiff)$/))
|
||||||
|
a(href=option)
|
||||||
|
img(src=option)
|
||||||
|
-else if(option.match(/youtube.com\/watch/))
|
||||||
|
-hasYouTubeVids = true
|
||||||
|
span(class='ytplaceholder')
|
||||||
|
=option
|
||||||
|
-else
|
||||||
|
a(href=option)
|
||||||
|
=option
|
||||||
|
-else
|
||||||
|
li.option #{option}
|
||||||
|
li.option-votes
|
||||||
|
.vote-track
|
||||||
|
-if(!isNaN(percentage))
|
||||||
|
.vote-percentage(style="width: #{percentage}%")
|
||||||
|
case votes
|
||||||
|
when 1: #{votes} vote
|
||||||
|
default: #{votes} votes
|
||||||
|
-if(!isNaN(percentage))
|
||||||
|
|(#{percentage.toFixed(2)}%)
|
||||||
|
-if(hasYouTubeVids)
|
||||||
|
script(src='/ytembed.js')
|
@ -1,36 +0,0 @@
|
|||||||
div#backlink
|
|
||||||
a(href='/polls/') « Poll list
|
|
||||||
h2 #{description}
|
|
||||||
p Voters (#{locals.totalVotes}):
|
|
||||||
-each voter in votees
|
|
||||||
| #{voter}
|
|
||||||
ul#votelist
|
|
||||||
-var hasYouTubeVids=false
|
|
||||||
-each votes,option in options
|
|
||||||
-var percentage = votes/locals.totalVotes*100
|
|
||||||
-if(options.hasOwnProperty(option))
|
|
||||||
-if(option.match(locals.url_regex))
|
|
||||||
li.option
|
|
||||||
-if(option.match(/(jpg|png|gif|jpeg|tiff)$/))
|
|
||||||
a(href=option)
|
|
||||||
img(src=option)
|
|
||||||
-else if(option.match(/youtube.com\/watch/))
|
|
||||||
-hasYouTubeVids = true
|
|
||||||
span(class='ytplaceholder')
|
|
||||||
=option
|
|
||||||
-else
|
|
||||||
a(href=option)
|
|
||||||
=option
|
|
||||||
-else
|
|
||||||
li.option #{option}
|
|
||||||
li.option-votes
|
|
||||||
.vote-track
|
|
||||||
-if(!isNaN(percentage))
|
|
||||||
.vote-percentage(style="width: #{percentage}%")
|
|
||||||
case votes
|
|
||||||
when 1: #{votes} vote
|
|
||||||
default: #{votes} votes
|
|
||||||
-if(!isNaN(percentage))
|
|
||||||
|(#{percentage.toFixed(2)}%)
|
|
||||||
-if(hasYouTubeVids)
|
|
||||||
script(src='/ytembed.js')
|
|
@ -1,4 +1,4 @@
|
|||||||
extends layout
|
extends ../layout
|
||||||
|
|
||||||
block content
|
block content
|
||||||
div#backlink
|
div#backlink
|
@ -1,4 +1,4 @@
|
|||||||
extends layout
|
extends ../layout
|
||||||
|
|
||||||
block content
|
block content
|
||||||
div#backlink
|
div#backlink
|
@ -1,4 +1,4 @@
|
|||||||
extends layout
|
extends ../layout
|
||||||
|
|
||||||
block content
|
block content
|
||||||
h3 Channels on #{connection}
|
h3 Channels on #{connection}
|
@ -1,4 +1,4 @@
|
|||||||
extends layout
|
extends ../layout
|
||||||
|
|
||||||
block content
|
block content
|
||||||
h3 Current Connections
|
h3 Current Connections
|
@ -1,4 +1,4 @@
|
|||||||
extends layout
|
extends ../layout
|
||||||
|
|
||||||
block content
|
block content
|
||||||
h3 #{user}'s profile
|
h3 #{user}'s profile
|
@ -1,4 +1,4 @@
|
|||||||
extends layout
|
extends ../layout
|
||||||
|
|
||||||
block content
|
block content
|
||||||
h3 Users currently in #{channel} on #{connection}
|
h3 Users currently in #{channel} on #{connection}
|
Loading…
Reference in New Issue
Block a user