Spaces:
Running
Running
/** | |
* Room Events Plugin | |
* Pokemon Showdown - http://pokemonshowdown.com/ | |
* | |
* This is a room-management system to keep track of upcoming room events. | |
* | |
* @license MIT license | |
*/ | |
import { Utils } from '../../lib'; | |
export interface RoomEvent { | |
eventName: string; | |
date: string; | |
desc: string; | |
started: boolean; | |
} | |
export interface RoomEventAlias { | |
eventID: ID; | |
} | |
export interface RoomEventCategory { | |
events: ID[]; | |
} | |
function convertAliasFormat(room: Room) { | |
if (!room.settings.events) return; | |
for (const event of Object.values(room.settings.events) as AnyObject[]) { | |
if (!event.aliases) continue; | |
for (const alias of event.aliases) { | |
room.settings.events[alias] = { eventID: toID(event.eventName) }; | |
} | |
delete event.aliases; | |
} | |
} | |
function formatEvent(room: Room, event: RoomEvent, showAliases?: boolean, showCategories?: boolean) { | |
const timeRemaining = new Date(event.date).getTime() - new Date().getTime(); | |
let explanation = timeRemaining.toString(); | |
if (!timeRemaining) explanation = "The time remaining for this event is not available"; | |
if (timeRemaining < 0) explanation = "This event will start soon"; | |
if (event.started) explanation = "This event has started"; | |
if (!isNaN(timeRemaining)) { | |
explanation = `This event will start in: ${Chat.toDurationString(timeRemaining, { precision: 2 })}`; | |
} | |
const eventID = toID(event.eventName); | |
const aliases = getAliases(room, eventID); | |
const categories = getAllCategories(room).filter( | |
category => (room.settings.events![category] as RoomEventCategory).events.includes(eventID) | |
); | |
let ret = `<tr title="${explanation}">`; | |
ret += Utils.html`<td>${event.eventName}</td>`; | |
if (showAliases) ret += Utils.html`<td>${aliases.join(", ")}</td>`; | |
if (showCategories) ret += Utils.html`<td>${categories.join(", ")}</td>`; | |
ret += `<td>${Chat.formatText(event.desc, true)}</td>`; | |
ret += Utils.html`<td><time>${event.date}</time></td></tr>`; | |
return ret; | |
} | |
function getAliases(room: Room, eventID?: ID) { | |
if (!room.settings.events) return []; | |
const aliases: string[] = []; | |
for (const aliasID in room.settings.events) { | |
if ( | |
'eventID' in room.settings.events[aliasID] && | |
(!eventID || room.settings.events[aliasID].eventID === eventID) | |
) aliases.push(aliasID); | |
} | |
return aliases; | |
} | |
function getAllCategories(room: Room) { | |
if (!room.settings.events) return []; | |
const categories: string[] = []; | |
for (const categoryID in room.settings.events) { | |
if ('events' in room.settings.events[categoryID]) categories.push(categoryID); | |
} | |
return categories; | |
} | |
function getAllEvents(room: Room) { | |
if (!room.settings.events) return []; | |
const events: RoomEvent[] = []; | |
for (const event of Object.values(room.settings.events)) { | |
if ('eventName' in event) events.push(event); | |
} | |
return events; | |
} | |
function getEventID(nameOrAlias: string, room: Room): ID { | |
let id = toID(nameOrAlias); | |
const event = room.settings.events?.[id]; | |
if (event && 'eventID' in event) { | |
id = event.eventID; | |
} | |
return id; | |
} | |
export const commands: Chat.ChatCommands = { | |
events: 'roomevents', | |
roomevent: 'roomevents', | |
roomevents: { | |
''(target, room, user) { | |
room = this.requireRoom(); | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
if (!room.settings.events || !Object.keys(room.settings.events).length) { | |
return this.errorReply("There are currently no planned upcoming events for this room."); | |
} | |
if (!this.runBroadcast()) return; | |
convertAliasFormat(room); | |
const hasAliases = getAliases(room).length > 0; | |
const hasCategories = getAllCategories(room).length > 0; | |
let buff = '<table border="1" cellspacing="0" cellpadding="3">'; | |
buff += '<th>Event Name:</th>'; | |
if (hasAliases) buff += '<th>Event Aliases:</th>'; | |
if (hasCategories) buff += '<th>Event Categories:</th>'; | |
buff += '<th>Event Description:</th><th>Event Date:</th>'; | |
for (const event of getAllEvents(room)) { | |
buff += formatEvent(room, event, hasAliases, hasCategories); | |
} | |
buff += '</table>'; | |
return this.sendReply(`|raw|<div class="infobox-limited">${buff}</div>`); | |
}, | |
new: 'add', | |
create: 'add', | |
edit: 'add', | |
add(target, room, user) { | |
room = this.requireRoom(); | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
this.checkCan('ban', null, room); | |
if (!room.settings.events) room.settings.events = Object.create(null); | |
convertAliasFormat(room); | |
const events = room.settings.events!; | |
const [eventName, date, ...desc] = target.split(target.includes('|') ? '|' : ','); | |
if (!(eventName && date && desc)) { | |
return this.errorReply("You're missing a command parameter - to see this command's syntax, use /help roomevents."); | |
} | |
const dateActual = date.trim(); | |
const descString = desc.join(target.includes('|') ? '|' : ',').trim(); | |
if (eventName.trim().length > 50) return this.errorReply("Event names should not exceed 50 characters."); | |
if (dateActual.length > 150) return this.errorReply("Event dates should not exceed 150 characters."); | |
if (descString.length > 1000) return this.errorReply("Event descriptions should not exceed 1000 characters."); | |
const eventId = getEventID(eventName, room); | |
if (!eventId) return this.errorReply("Event names must contain at least one alphanumerical character."); | |
const oldEvent = room.settings.events?.[eventId] as RoomEvent; | |
if (oldEvent && 'events' in oldEvent) return this.errorReply(`"${eventId}" is already the name of a category.`); | |
const eventNameActual = (oldEvent ? oldEvent.eventName : eventName.trim()); | |
this.privateModAction(`${user.name} ${oldEvent ? "edited the" : "added a"} roomevent titled "${eventNameActual}".`); | |
this.modlog('ROOMEVENT', null, `${oldEvent ? "edited" : "added"} "${eventNameActual}"`); | |
events[eventId] = { | |
eventName: eventNameActual, | |
date: dateActual, | |
desc: descString, | |
started: false, | |
}; | |
room.saveSettings(); | |
}, | |
rename(target, room, user) { | |
room = this.requireRoom(); | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
this.checkCan('ban', null, room); | |
let [oldName, newName] = target.split(target.includes('|') ? '|' : ','); | |
if (!(oldName && newName)) return this.errorReply("Usage: /roomevents rename [old name], [new name]"); | |
convertAliasFormat(room); | |
newName = newName.trim(); | |
const newID = toID(newName); | |
const oldID = (getAliases(room).includes(toID(oldName)) ? getEventID(oldName, room) : toID(oldName)); | |
if (newID === oldID) return this.errorReply("The new name must be different from the old one."); | |
if (!newID) return this.errorReply("Event names must contain at least one alphanumeric character."); | |
if (newName.length > 50) return this.errorReply("Event names should not exceed 50 characters."); | |
const events = room.settings.events!; | |
const eventData = events?.[oldID]; | |
if (!(eventData && 'eventName' in eventData)) return this.errorReply(`There is no event titled "${oldName}".`); | |
if (events?.[newID]) { | |
return this.errorReply(`"${newName}" is already an event, alias, or category.`); | |
} | |
const originalName = eventData.eventName; | |
eventData.eventName = newName; | |
events[newID] = eventData; | |
delete events[oldID]; | |
this.privateModAction(`${user.name} renamed the roomevent titled "${originalName}" to "${newName}".`); | |
this.modlog('ROOMEVENT', null, `renamed "${originalName}" to "${newName}"`); | |
room.saveSettings(); | |
}, | |
begin: 'start', | |
start(target, room, user) { | |
room = this.requireRoom(); | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
this.checkCan('ban', null, room); | |
if (!room.settings.events || !Object.keys(room.settings.events).length) { | |
return this.errorReply("There are currently no planned upcoming events for this room to start."); | |
} | |
if (!target) return this.errorReply("Usage: /roomevents start [event name]"); | |
convertAliasFormat(room); | |
target = toID(target); | |
const event = room.settings.events[getEventID(target, room)]; | |
if (!(event && 'eventName' in event)) return this.errorReply(`There is no event titled '${target}'. Check spelling?`); | |
if (event.started) { | |
return this.errorReply(`The event ${event.eventName} has already started.`); | |
} | |
for (const u in room.users) { | |
const activeUser = Users.get(u); | |
if (activeUser?.connected) { | |
activeUser.sendTo( | |
room, | |
Utils.html`|notify|A new roomevent in ${room.title} has started!|` + | |
`The "${event.eventName}" roomevent has started!` | |
); | |
} | |
} | |
this.add( | |
Utils.html`|raw|<div class="broadcast-blue"><b>The "${event.eventName}" roomevent has started!</b></div>` | |
); | |
this.modlog('ROOMEVENT', null, `started "${toID(event.eventName)}"`); | |
event.started = true; | |
room.saveSettings(); | |
}, | |
delete: 'remove', | |
remove(target, room, user) { | |
room = this.requireRoom(); | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
this.checkCan('ban', null, room); | |
if (!room.settings.events || Object.keys(room.settings.events).length === 0) { | |
return this.errorReply("There are currently no planned upcoming events for this room to remove."); | |
} | |
if (!target) return this.errorReply("Usage: /roomevents remove [event name]"); | |
const eventID = toID(target); | |
convertAliasFormat(room); | |
if (getAliases(room).includes(eventID)) return this.errorReply("To delete aliases, use /roomevents removealias."); | |
if (!(room.settings.events[eventID] && 'eventName' in room.settings.events[eventID])) { | |
return this.errorReply(`There is no event titled '${target}'. Check spelling?`); | |
} | |
delete room.settings.events[eventID]; | |
for (const alias of getAliases(room, eventID)) { | |
delete room.settings.events[alias]; | |
} | |
for (const category of getAllCategories(room).map(cat => room.settings.events?.[cat] as RoomEventCategory)) { | |
category.events = category.events.filter(event => event !== eventID); | |
} | |
this.privateModAction(`${user.name} removed a roomevent titled "${target}".`); | |
this.modlog('ROOMEVENT', null, `removed "${target}"`); | |
room.saveSettings(); | |
}, | |
view(target, room, user) { | |
room = this.requireRoom(); | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
if (!room.settings.events || !Object.keys(room.settings.events).length) { | |
return this.errorReply("There are currently no planned upcoming events for this room."); | |
} | |
if (!target) return this.errorReply("Usage: /roomevents view [event name, alias, or category]"); | |
convertAliasFormat(room); | |
target = getEventID(target, room); | |
let events: RoomEvent[] = []; | |
if (getAllCategories(room).includes(target)) { | |
for (const categoryID of Object.keys(room.settings.events)) { | |
const category = room.settings.events[categoryID]; | |
if ('events' in category && categoryID === target) { | |
events = category.events | |
.map(e => room.settings.events?.[e] as RoomEvent) | |
.filter(e => e); | |
break; | |
} | |
} | |
} else if (room.settings.events[target] && 'eventName' in room.settings.events[target]) { | |
events.push(room.settings.events[target] as RoomEvent); | |
} else { | |
return this.errorReply(`There is no event or category titled '${target}'. Check spelling?`); | |
} | |
if (!this.runBroadcast()) return; | |
let hasAliases = false; | |
let hasCategories = false; | |
for (const event of events) { | |
if (getAliases(room, toID(event.eventName)).length) hasAliases = true; | |
} | |
for (const potentialCategory of getAllCategories(room)) { | |
if ( | |
events.map(event => toID(event.eventName)) | |
.filter(id => (room.settings.events?.[potentialCategory] as RoomEventCategory).events.includes(id)).length | |
) hasCategories = true; break; | |
} | |
let buff = '<table border="1" cellspacing="0" cellpadding="3">'; | |
buff += '<th>Event Name:</th>'; | |
if (hasAliases) buff += '<th>Event Aliases:</th>'; | |
if (hasCategories) buff += '<th>Event Categories:</th>'; | |
buff += '<th>Event Description:</th><th>Event Date:</th>'; | |
for (const event of events) { | |
buff += formatEvent(room, event, hasAliases, hasCategories); | |
} | |
buff += '</table>'; | |
this.sendReply(`|raw|<div class="infobox-limited">${buff}</div>`); | |
if (!this.broadcasting && user.can('ban', null, room, 'roomevents add') && events.length === 1) { | |
const event = events[0]; | |
this.sendReplyBox(Utils.html`<details><summary>Source</summary><code style="white-space: pre-wrap; display: table; tab-size: 3">/roomevents add ${event.eventName} | ${event.date} | ${event.desc}</code></details>`.replace(/\n/g, '<br />')); | |
} | |
}, | |
alias: 'addalias', | |
addalias(target, room, user) { | |
room = this.requireRoom(); | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
this.checkCan('ban', null, room); | |
const [alias, eventId] = target.split(target.includes('|') ? '|' : ',').map(argument => toID(argument)); | |
if (!(alias && eventId)) { | |
return this.errorReply("Usage: /roomevents addalias [alias], [event name]. Aliases must contain at least one alphanumeric character."); | |
} | |
if (!room.settings.events || Object.keys(room.settings.events).length === 0) { | |
return this.errorReply(`There are currently no scheduled events.`); | |
} | |
convertAliasFormat(room); | |
const event = room.settings.events[eventId]; | |
if (!(event && 'eventName' in event)) return this.errorReply(`There is no event titled "${eventId}".`); | |
if (room.settings.events[alias]) return this.errorReply(`"${alias}" is already an event, alias, or category.`); | |
room.settings.events[alias] = { eventID: eventId }; | |
this.privateModAction(`${user.name} added an alias "${alias}" for the roomevent "${eventId}".`); | |
this.modlog('ROOMEVENT', null, `alias for "${eventId}": "${alias}"`); | |
room.saveSettings(); | |
}, | |
deletealias: 'removealias', | |
removealias(target, room, user) { | |
room = this.requireRoom(); | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
this.checkCan('ban', null, room); | |
target = toID(target); | |
if (!target) return this.errorReply("Usage: /roomevents removealias <alias>"); | |
if (!room.settings.events || Object.keys(room.settings.events).length === 0) { | |
return this.errorReply(`There are currently no scheduled events.`); | |
} | |
convertAliasFormat(room); | |
if (!(room.settings.events[target] && 'eventID' in room.settings.events[target])) { | |
return this.errorReply(`${target} isn't an alias.`); | |
} | |
delete room.settings.events[target]; | |
this.privateModAction(`${user.name} removed the alias "${target}"`); | |
this.modlog('ROOMEVENT', null, `removed the alias "${target}"`); | |
room.saveSettings(); | |
}, | |
addtocategory(target, room, user) { | |
room = this.requireRoom(); | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
this.checkCan('ban', null, room); | |
const [eventId, categoryId] = target.split(target.includes('|') ? '|' : ',').map(argument => toID(argument)); | |
if (!(eventId && categoryId)) return this.errorReply("Usage: /roomevents addtocategory [event name], [category]."); | |
if (!room.settings.events || Object.keys(room.settings.events).length === 0) { | |
return this.errorReply(`There are currently no scheduled events.`); | |
} | |
convertAliasFormat(room); | |
const event = room.settings.events[getEventID(eventId, room)]; | |
if (!(event && 'eventName' in event)) return this.errorReply(`There is no event or alias titled "${eventId}".`); | |
const category = room.settings.events[categoryId]; | |
if (category && !('events' in category)) { | |
return this.errorReply(`There is already an event or alias titled "${categoryId}".`); | |
} | |
if (!category) { | |
return this.errorReply(`There is no category titled "${categoryId}". To create it, use /roomevents addcategory ${categoryId}.`); | |
} | |
if (category.events.includes(toID(event.eventName))) { | |
return this.errorReply(`The event "${eventId}" is already in the "${categoryId}" category.`); | |
} | |
category.events.push(toID(event.eventName)); | |
room.settings.events[categoryId] = category; | |
this.privateModAction(`${user.name} added the roomevent "${eventId}" to the category "${categoryId}".`); | |
this.modlog('ROOMEVENT', null, `category for "${eventId}": "${categoryId}"`); | |
room.saveSettings(); | |
}, | |
removefromcategory(target, room, user) { | |
room = this.requireRoom(); | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
this.checkCan('ban', null, room); | |
const [eventId, categoryId] = target.split(target.includes('|') ? '|' : ',').map(argument => toID(argument)); | |
if (!(eventId && categoryId)) { | |
return this.errorReply("Usage: /roomevents removefromcategory [event name], [category]."); | |
} | |
if (!room.settings.events || Object.keys(room.settings.events).length === 0) { | |
return this.errorReply(`There are currently no scheduled events.`); | |
} | |
convertAliasFormat(room); | |
const event = room.settings.events[getEventID(eventId, room)]; | |
if (!(event && 'eventName' in event)) return this.errorReply(`There is no event or alias titled "${eventId}".`); | |
const category = room.settings.events[categoryId]; | |
if (category && !('events' in category)) { | |
return this.errorReply(`There is already an event or alias titled "${categoryId}".`); | |
} | |
if (!category) return this.errorReply(`There is no category titled "${categoryId}".`); | |
if (!category.events.includes(toID(event.eventName))) { | |
return this.errorReply(`The event "${eventId}" isn't in the "${categoryId}" category.`); | |
} | |
category.events = category.events.filter(e => e !== eventId); | |
room.settings.events[categoryId] = category; | |
this.privateModAction(`${user.name} removed the roomevent "${eventId}" from the category "${categoryId}".`); | |
this.modlog('ROOMEVENT', null, `category for "${eventId}": removed "${categoryId}"`); | |
room.saveSettings(); | |
}, | |
addcat: 'addcategory', | |
addcategory(target, room, user) { | |
room = this.requireRoom(); | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
this.checkCan('ban', null, room); | |
const categoryId = toID(target); | |
if (!target) { | |
return this.errorReply("Usage: /roomevents addcategory [category name]. Categories must contain at least one alphanumeric character."); | |
} | |
convertAliasFormat(room); | |
if (!room.settings.events) room.settings.events = Object.create(null); | |
if (room.settings.events?.[categoryId]) return this.errorReply(`The category "${target}" already exists.`); | |
room.settings.events![categoryId] = { events: [] }; | |
this.privateModAction(`${user.name} added the category "${categoryId}".`); | |
this.modlog('ROOMEVENT', null, `category: added "${categoryId}"`); | |
room.saveSettings(); | |
}, | |
deletecategory: 'removecategory', | |
deletecat: 'removecategory', | |
removecat: 'removecategory', | |
rmcat: 'removecategory', | |
removecategory(target, room, user) { | |
room = this.requireRoom(); | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
this.checkCan('ban', null, room); | |
const categoryId = toID(target); | |
if (!target) return this.errorReply("Usage: /roomevents removecategory [category name]."); | |
convertAliasFormat(room); | |
if (!room.settings.events) room.settings.events = Object.create(null); | |
if (!room.settings.events?.[categoryId]) return this.errorReply(`The category "${target}" doesn't exist.`); | |
delete room.settings.events?.[categoryId]; | |
this.privateModAction(`${user.name} removed the category "${categoryId}".`); | |
this.modlog('ROOMEVENT', null, `category: removed "${categoryId}"`); | |
room.saveSettings(); | |
}, | |
viewcategories: 'categories', | |
categories(target, room, user) { | |
room = this.requireRoom(); | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
this.runBroadcast(); | |
const categoryButtons = getAllCategories(room).map( | |
category => `<button class="button" name="send" value="/roomevents view ${category}">${category}</button>` | |
); | |
if (!categoryButtons.length) return this.errorReply(`There are no roomevent categories in ${room.title}.`); | |
this.sendReplyBox(`Roomevent categories in ${room.title}: ${categoryButtons.join(' ')}`); | |
}, | |
help(target, room, user) { | |
return this.parse('/help roomevents'); | |
}, | |
sortby(target, room, user) { | |
room = this.requireRoom(); | |
// preconditions | |
if (!room.persist) return this.errorReply("This command is unavailable in temporary rooms."); | |
if (!room.settings.events || !Object.keys(room.settings.events).length) { | |
return this.errorReply("There are currently no planned upcoming events for this room."); | |
} | |
this.checkCan('ban', null, room); | |
// declare variables | |
let multiplier = 1; | |
let columnName = ""; | |
const delimited = target.split(target.includes('|') ? '|' : ','); | |
const sortable = Object.values(room.settings.events) | |
.filter((event): event is RoomEvent => 'eventName' in event); | |
// id tokens | |
if (delimited.length === 1) { | |
columnName = target; | |
} else { | |
let order = ""; | |
[columnName, order] = delimited; | |
order = toID(order); | |
multiplier = (order === 'desc') ? -1 : 1; | |
} | |
// sort the array by the appropriate column name | |
columnName = toID(columnName); | |
switch (columnName) { | |
case "date": | |
case "eventdate": | |
sortable.sort( | |
(a, b) => | |
(toID(a.date) < toID(b.date)) ? -multiplier : | |
(toID(b.date) < toID(a.date)) ? multiplier : 0 | |
); | |
break; | |
case "desc": | |
case "description": | |
case "eventdescription": | |
sortable.sort( | |
(a, b) => | |
(toID(a.desc) < toID(b.desc)) ? -multiplier : | |
(toID(b.desc) < toID(a.desc)) ? multiplier : 0 | |
); | |
break; | |
case "eventname": | |
case "name": | |
sortable.sort( | |
(a, b) => | |
(toID(a.eventName) < toID(b.eventName)) ? -multiplier : | |
(toID(b.eventName) < toID(a.eventName)) ? multiplier : 0 | |
); | |
break; | |
default: | |
return this.errorReply(`Invalid column name "${columnName}". Please use one of: date, desc, name.`); | |
} | |
// rebuild the room.settings.events object | |
for (const sortedObj of sortable) { | |
const eventId = toID(sortedObj.eventName); | |
delete room.settings.events[eventId]; | |
room.settings.events[eventId] = sortedObj; | |
} | |
// build communication string | |
const resultString = `sorted by column: ${columnName}` + | |
` in ${multiplier === 1 ? "ascending" : "descending"} order` + | |
`${delimited.length === 1 ? " (by default)" : ""}`; | |
this.modlog('ROOMEVENT', null, resultString); | |
return this.sendReply(resultString); | |
}, | |
}, | |
roomeventshelp() { | |
this.sendReply( | |
`|html|<details class="readmore"><summary><code>/roomevents</code>: displays a list of upcoming room-specific events.<br />` + | |
`<code>/roomevents add [event name] | [event date/time] | [event description]</code>: adds a room event. A timestamp in event date/time field like YYYY-MM-DD HH:MM±hh:mm will be displayed in user's timezone. Requires: @ # ~<br />` + | |
`<code>/roomevents start [event name]</code>: declares to the room that the event has started. Requires: @ # ~<br />` + | |
`<code>/roomevents remove [event name]</code>: deletes an event. Requires: @ # ~</summary>` + | |
`<code>/roomevents rename [old event name] | [new name]</code>: renames an event. Requires: @ # ~<br />` + | |
`<code>/roomevents addalias [alias] | [event name]</code>: adds an alias for the event. Requires: @ # ~<br />` + | |
`<code>/roomevents removealias [alias]</code>: removes an event alias. Requires: @ # ~<br />` + | |
`<code>/roomevents addcategory [category]</code>: adds an event category. Requires: @ # ~<br />` + | |
`<code>/roomevents removecategory [category]</code>: removes an event category. Requires: @ # ~<br />` + | |
`<code>/roomevents addtocategory [event name] | [category]</code>: adds the event to a category. Requires: @ # ~<br />` + | |
`<code>/roomevents removefromcategory [event name] | [category]</code>: removes the event from a category. Requires: @ # ~<br />` + | |
`<code>/roomevents sortby [column name] | [asc/desc (optional)]</code> sorts events table by column name and an optional argument to ascending or descending order. Ascending order is default. Requires: @ # ~<br />` + | |
`<code>/roomevents view [event name or category]</code>: displays information about a specific event or category of events.<br />` + | |
`<code>/roomevents viewcategories</code>: displays a list of event categories for that room.` + | |
`</details>` | |
); | |
}, | |
}; | |