Skip to content
Snippets Groups Projects
Commit 12adcd56 authored by Atanamo's avatar Atanamo
Browse files

Merge branch 'CustomChannels'

parents 0d5a9316 dfbc615a
No related branches found
No related tags found
No related merge requests found
Showing
with 1191 additions and 256 deletions
-- -----------------------------------------------------------------------------------
-- This example script sets up all database tables needed for the chat system itself,
-- but without tables for game world environment.
-- If you change table names, you have to modify the config.
-- If you change table structure or similar, you have to modify the database class.
-- -----------------------------------------------------------------------------------
-- MySQL server version: 5.6.12
-- -----------------------------------------------------------------------------------
--
-- Scheme for table `chat - channeljoins`
--
CREATE TABLE IF NOT EXISTS `chat - channeljoins` (
`ID` int(10) unsigned NOT NULL,
`UserID` int(10) unsigned NOT NULL,
`ChannelID` int(10) unsigned NOT NULL,
PRIMARY KEY (`UserID`,`ChannelID`),
UNIQUE KEY `ID` (`ID`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Current channel joins of clients/players';
-- --------------------------------------------------------
--
-- Scheme for table `chat - channellogs`
--
CREATE TABLE IF NOT EXISTS `chat - channellogs` (
`ChannelLogID` bigint(10) unsigned NOT NULL,
`ChannelBufferID` smallint(5) unsigned NOT NULL,
`ChannelTextID` varchar(50) NOT NULL,
`ChannelID` int(10) unsigned NOT NULL,
`EventTextID` varchar(25) NOT NULL,
`EventData` text NOT NULL,
`Timestamp` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`ChannelTextID`,`ChannelBufferID`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Current chat history of all channels';
-- --------------------------------------------------------
--
-- Scheme for table `chat - channels`
--
CREATE TABLE IF NOT EXISTS `chat - channels` (
`ID` int(10) unsigned NOT NULL AUTO_INCREMENT,
`GalaxyID` tinyint(5) unsigned NOT NULL,
`CreatorUserID` int(10) unsigned NOT NULL,
`Title` tinytext NOT NULL,
`Password` tinytext NOT NULL,
`IrcChannel` tinytext,
`IsPublic` tinyint(1) unsigned NOT NULL,
PRIMARY KEY (`ID`),
KEY `GalaxyID` (`GalaxyID`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Stored custom/non-default channels';
Loading
Loading
@@ -61,21 +61,34 @@
 
.tabsystemViewport {
border: 1px solid #aaa;
height: 200px;
height: 250px;
overflow: hidden;
background: #eee;
}
 
.tabsystemViewport > div {
display: flex;
flex-flow: column nowrap;
width: 100%;
height: 100%;
}
.chatOutputContainer {
display: flex;
flex: 1;
width: 100%;
min-height: 0; /* This fixes a bug for FF v34, v35 */
padding: 10px;
}
 
.chatFormContainer {
width: 100%;
padding: 10px;
border-top: solid 1px #aaa;
}
.chatMessagesContainer,
.chatUsersContainer {
height: 100%;
overflow-y: scroll;
}
 
Loading
Loading
@@ -100,15 +113,19 @@
div[data-channel] .chatMessages li.notice {
color: gray;
}
.chatMessages li.error {
color: red;
}
 
.chatUsersCount {
display: none;
.chatUsersCount,
.chatChannelName {
padding: 7px 7px;
border-bottom: solid 1px #aaa;
font-size: 0.9em
}
 
.chatUsersCount .title:after {
.chatUsersCount .title:after,
.chatChannelName .title:after {
content: ':';
}
 
Loading
Loading
@@ -121,14 +138,42 @@
list-style-type: none;
white-space: nowrap;
padding: 3px 7px;
font-size: 0.9em
font-size: 0.9em;
}
.chatUsers.players li:last-child {
border-bottom: solid 1px #aaa;
}
 
form {
width: 100%;
display: flex;
flex-flow: row wrap;
justify-content: space-between;
}
.formSection {
width: 50%;
}
.formSection:nth-child(n+3) {
margin-top: 7px;
}
form.inactive {
display: none;
.formSection.right {
text-align: right;
}
.formSection .sectionHeader {
font-size: 1em;
margin: 5px 0;
}
.formSection .inputContainer label {
display: inline-block;
min-width: 130px;
}
.formSection .inputContainer label:first-child:after {
content: ':';
}
 
</style>
Loading
Loading
@@ -142,39 +187,92 @@
 
<div id="tabsystem">
<ul class="tabsystemHeaders">
<li data-id="tabPageServer" class="active">Server</li>
<li data-id="tabPageServer" title="Server" class="active">
<span data-content="label.server_tab" class="title">Server<span>
</li>
</ul>
<div class="tabsystemViewport">
<div id="tabPageServer">
<div class="chatMessagesContainer">
<ul class="chatMessages">
<li>Loading...</li>
</ul>
<div id="tabPageServer" class="tabPage">
<div class="chatOutputContainer">
<div class="chatMessagesContainer">
<ul class="chatMessages">
<li data-content="manage_msg.loading_start">Loading...</li>
</ul>
</div>
</div>
<div class="chatFormContainer">
<form id="channelCreateForm" action="#">
<div class="formSection">
<h2 class="sectionHeader" data-content="label.channel_join_options"></h2>
<div class="inputContainer">
<label for="channelNameInput" data-content="label.channel_name">Channel name</label>
<input type="text" id="channelNameInput" required maxlength="30">
</div>
<div class="inputContainer">
<label for="channelPasswordInput" data-content="label.channel_password">Channel password</label>
<input type="text" id="channelPasswordInput" maxlength="20">
</div>
</div>
<div class="formSection">
<h2 class="sectionHeader" data-content="label.channel_creation_options"></h2>
<div class="inputContainer">
<input type="checkbox" id="channelFlagPublic">
<label for="channelFlagPublic" data-content="label.channel_flag_public">Hide joined users</label>
</div>
<div class="inputContainer">
<input type="checkbox" id="channelFlagIRC">
<label for="channelFlagIRC" data-content="label.channel_flag_irc">Mirror channel to IRC</label>
</div>
</div>
<div class="formSection">
<input type="submit" id="channelCreateSubmitButton">
</div>
<div class="formSection">
</div>
</form>
</div>
</div>
</div>
</div>
 
<form id="chat_form" action="#" class="inactive">
<input type="text" id="chat_input" />
<input type="submit" id="chat_submit_btn" />
</form>
 
<div id="tabPageSkeletonContainer" style="display:none;">
<div id="tabPageSkeleton">
<div class="chatMessagesContainer">
<ul class="chatMessages">
<div id="tabPageSkeleton" class="tabPage">
<div class="chatOutputContainer">
<div class="chatMessagesContainer">
<ul class="chatMessages">
 
</ul>
</div>
<div class="chatUsersContainer">
<div class="chatUsersCount">
<span class="title"></span>
<span class="value"></span>
</ul>
</div>
<ul class="chatUsers">
<div class="chatUsersContainer">
<div class="chatUsersCount">
<span class="title" data-content="label.current_number_of_players"></span>
<span class="value"></span>
</div>
<ul class="chatUsers players">
</ul>
<div class="chatChannelName">
<span class="title" data-content="label.irc_channel_name"></span>
<span class="value"></span>
</div>
<ul class="chatUsers irc">
 
</ul>
</ul>
</div>
</div>
<div class="chatFormContainer">
<form action="#" class="chatForm">
<div class="formSection">
<input type="text" name="chatInput" class="chatInput">
<input type="submit" class="chatSubmitButton">
</div>
<div class="formSection right">
<button class="channelDeleteButton" data-content="label.button.delete_channel">Delete channel</button>
<button class="channelLeaveButton" data-content="label.button.leave_channel">Leave channel</button>
<button class="channelCloseButton" data-content="label.button.close_channel">Close</button>
</div>
</form>
</div>
</div>
</div>
Loading
Loading
@@ -186,6 +284,7 @@
userID: 42, // The id of the player's account or game identity/character - to be passed to Database.getClientIdentityData()
gameID: 123, // The id of the player's game world - to be passed to Database.getClientIdentityData()
token: 'z4da9' // SecurityToken equal to the result of Database._get_security_token()
};
 
options = {
signalizeMessagesToWindow: true,
Loading
Loading
-- ------------------------------------------------------------------------------------------------
-- This example script sets up all database tables needed for the chat system itself,
-- but without tables for game world environment.
-- If you change table names, you have to modify the config (See SQL_TABLES).
-- If you change table structure or similar, you have to modify the queries of the database class.
-- ------------------------------------------------------------------------------------------------
-- MySQL server version: 5.6.12
-- ------------------------------------------------------------------------------------------------
--
-- Scheme for table `chat - channels`
--
CREATE TABLE IF NOT EXISTS `chat - channels` (
`ID` int(10) unsigned NOT NULL AUTO_INCREMENT,
`GalaxyID` tinyint(5) unsigned NOT NULL,
`CreatorUserID` int(10) unsigned NOT NULL,
`Title` tinytext NOT NULL,
`Password` tinytext NOT NULL,
`IrcChannel` tinytext,
`IsPublic` tinyint(1) unsigned NOT NULL,
PRIMARY KEY (`ID`),
KEY `GalaxyID` (`GalaxyID`),
KEY `CreatorUserID` (`CreatorUserID`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Stored custom/non-default channels';
-- --------------------------------------------------------
--
-- Scheme for table `chat - channeljoins`
--
CREATE TABLE IF NOT EXISTS `chat - channeljoins` (
`ID` int(10) unsigned NOT NULL AUTO_INCREMENT,
`UserID` int(10) unsigned NOT NULL,
`ChannelID` int(10) unsigned NOT NULL,
PRIMARY KEY (`UserID`,`ChannelID`),
UNIQUE KEY `ID` (`ID`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Current channel joins of clients/players';
-- --------------------------------------------------------
--
-- Scheme for table `chat - channellogs`
--
CREATE TABLE IF NOT EXISTS `chat - channellogs` (
`ChannelLogID` bigint(10) unsigned NOT NULL,
`ChannelBufferID` smallint(5) unsigned NOT NULL,
`ChannelTextID` varchar(50) NOT NULL,
`ChannelID` int(10) unsigned NOT NULL,
`EventTextID` varchar(25) NOT NULL,
`EventData` text NOT NULL,
`Timestamp` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`ChannelBufferID`,`ChannelTextID`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Current chat history of all channels';
Loading
Loading
@@ -53,7 +53,7 @@ class SchizoBot
debug: Config.DEBUG_IRC_COMM
showErrors: true
floodProtection: true # Protect the bot from beeing kicked, if users are flooding
floodProtectionDelay: 50 # Delay time for messages to avoid flooding
floodProtectionDelay: 100 # Delay time for messages to avoid flooding
retryDelay: Config.BOT_RECONNECT_DELAY # Delay time for reconnects
stripColors: true # Strip mirc colors
 
Loading
Loading
@@ -238,6 +238,7 @@ class SchizoBot
if channel?
respondFunc('Sry, what?')
else
message = message.substr(0, 20) + '...' if message.length > 25
respondFunc("Unknown command '#{message}'. --- #{defaultResponse}")
 
_handleIrcMessageToChannel: (senderNick, channel, message, rawData) =>
Loading
Loading
@@ -334,7 +335,7 @@ class SchizoBot
timespanDays = Math.floor(timespanHours / 24)
timespanFractHours = timespanHours - (timespanDays*24)
connectDateText = @connectDateTime.toUTCString()
respondFunc("Uptime = #{timespanDays} days, #{timespanFractHours} hours (Since #{connectDateText})")
respondFunc("Uptime = #{timespanDays} days, #{timespanFractHours.toFixed(2)} hours (Since #{connectDateText})")
return true
return false
 
Loading
Loading
@@ -378,6 +379,9 @@ class SchizoBot
_sendUserListRequestToIrcChannel: (channelName) ->
@client.send('names', channelName)
 
_sendModeSettingToIrcChannel: (channelName, modesExpression, additionalArgs...) ->
@client.send('MODE', channelName, modesExpression, additionalArgs...);
_respondToIrcViaCTCP: (receiverNick, ctcpCommand, responseText) ->
ctcpText = "#{ctcpCommand} #{responseText}"
@client.ctcp(receiverNick, 'notice', ctcpText) # Send notice for a reply to command
Loading
Loading
@@ -399,11 +403,32 @@ class SchizoBot
handleWebChannelJoin: (botChannel, isMasterBot) ->
joinDeferred = Q.defer()
ircChannelName = botChannel.getIrcChannelName()
ircChannelPassword = botChannel.getIrcChannelPassword() or null
# Store channel data
@botChannelList[ircChannelName] = botChannel
@masterChannelList[ircChannelName] = isMasterBot
log.info "Joining bot '#{@nickName}' to channel #{ircChannelName} (As master: #{isMasterBot})..."
@client.join ircChannelName, ->
joinDeferred.resolve()
# Join IRC channel (after connect)
promise = @getConnectionPromise()
promise = promise.then =>
log.info "Joining bot '#{@nickName}' to channel #{ircChannelName} " +
"(As master: #{isMasterBot}, password: #{(ircChannelPassword or '<none>')})..."
joinExpression = ircChannelName
joinExpression += ' ' + ircChannelPassword if ircChannelPassword?
@client.join joinExpression, =>
# Set channel modes
if ircChannelName.indexOf(Config.IRC_NONGAME_CHANNEL_PREFIX) is 0
@_sendModeSettingToIrcChannel(ircChannelName, '+s') # Set to secret
if ircChannelPassword?
@_sendModeSettingToIrcChannel(ircChannelName, '+k', ircChannelPassword) # Set channel password
# Resolve join
joinDeferred.resolve()
promise.done()
return joinDeferred.promise
 
handleWebChannelLeave: (botChannel) ->
Loading
Loading
@@ -411,9 +436,15 @@ class SchizoBot
ircChannelName = botChannel.getIrcChannelName()
delete @botChannelList[ircChannelName]
delete @masterChannelList[ircChannelName]
log.info "Removing bot '#{@nickName}' from channel #{ircChannelName}..."
@client.part ircChannelName, Config.BOT_LEAVE_MESSAGE, ->
partDeferred.resolve()
# Part from IRC channel (after connect)
promise = @getConnectionPromise()
promise = promise.then =>
log.info "Removing bot '#{@nickName}' from channel #{ircChannelName}..."
@client.part ircChannelName, Config.BOT_LEAVE_MESSAGE, ->
partDeferred.resolve()
promise.done()
return partDeferred.promise
 
handleWebChannelMasterNomination: (botChannel) ->
Loading
Loading
@@ -424,7 +455,10 @@ class SchizoBot
handleWebClientMessage: (channelName, senderIdentity, rawMessage) ->
clientNick = senderIdentity.getName()
messageText = "<#{clientNick}>: #{rawMessage}"
@client.say(channelName, messageText)
# Post to IRC, if client is connected
@client.say(channelName, messageText) unless @getConnectionPromise().isPending()
# Mirror to web channel, if no other bot (the master) is triggering the message though observing
@_sendMessageToWebChannel(channelName, @nickName, messageText) if @_isChannelMaster(channelName)
 
Loading
Loading
@@ -440,5 +474,3 @@ class SchizoBot
# Export class
module.exports = SchizoBot
 
Loading
Loading
@@ -7,6 +7,7 @@ ClientIdentity = require './clientidentity'
 
## Extended abstraction of a socket.io room, which is mirrored to an IRC channel.
## Communication to IRC is handled by multiple Bots, each representing a game instance.
## An instance of a BotChannel destroys itself after all clients and bots have left.
##
## When a socket client sends a message, it is routed to the bot, that represents the client's game instance.
## When a message is triggered on IRC, the respective bot sends it to the associated BotChannel.
Loading
Loading
@@ -14,18 +15,20 @@ ClientIdentity = require './clientidentity'
##
## @extends Channel
class BotChannel extends Channel
@_instances: {}
#@_instances: {} # Inherited
botList: null
 
gameID: 0
ircChannelName: ''
ircChannelTopic: null
ircChannelPassword: null
ircUserList: null
isPermanent: false
 
constructor: (data, @isPermanent) ->
super
@ircChannelName = data.irc_channel or @ircChannelName
@ircChannelPassword = data.password or @ircChannelPassword
@gameID = data.game_id or @gameID
@botList = {}
@ircUserList = {}
Loading
Loading
@@ -40,11 +43,12 @@ class BotChannel extends Channel
# @override
_checkForDestroy: ->
if @_getNumberOfBots() is 0
super unless @isPermanent
return super unless @isPermanent
return false
 
# @override
addClient: (clientSocket, isRejoin=false) ->
super(clientSocket, true) # true, because: dont do that: db.addClientToChannel(clientSocket, @name)
super(clientSocket, isRejoin)
@_sendChannelTopic(clientSocket, @ircChannelTopic) if @ircChannelTopic?
# Send list of irc users (if not already sent by super method)
if @isPublic
Loading
Loading
@@ -53,13 +57,14 @@ class BotChannel extends Channel
addBot: (bot) ->
# Store bot reference, addressable by game id
botID = bot.getID()
return Q(false) if @botList[botID]? # Cancel, if bot already added
@botList[botID] = bot
# Let bot join the irc channel
isMasterBot = Object.keys(@botList).length is 1 # First bot is master
joinPromise = bot.handleWebChannelJoin(this, isMasterBot)
return joinPromise
 
removeBot: (bot) ->
removeBot: (bot, checkForDestroy=true) ->
botID = bot.getID()
if @botList[botID]?
# Remove bot reference before nominating new master
Loading
Loading
@@ -73,13 +78,25 @@ class BotChannel extends Channel
partPromise = bot.handleWebChannelLeave(this)
 
# May destroy instance (if it was the last bot)
@_checkForDestroy()
@_checkForDestroy() if checkForDestroy
return partPromise
return Q(false)
 
# @override
_deleteByClient: (clientSocket) ->
customRoutine = =>
# Kick off bots
for botID, bot of @botList
@removeBot(bot, false)
return super(clientSocket, customRoutine)
getIrcChannelName: ->
return @ircChannelName
 
getIrcChannelPassword: ->
return @ircChannelPassword
getGameID: ->
return @gameID
 
Loading
Loading
Loading
Loading
@@ -5,7 +5,10 @@ BotChannel = require './botchannel'
Bot = require './bot'
 
 
## Main class
## Abstraction of a service for watching existence of games.
## It manages creation and destruction of bots based on the available games.
## To be used as singleton.
##
class BotManager
isManaging: false
watcherTimer: null
Loading
Loading
@@ -16,7 +19,7 @@ class BotManager
constructor: ->
@botList = {}
 
start: ->
start: =>
# Setup bot channels
globalChannelPromise = @_setupGlobalBotChannel()
singleChannelsPromise = @_setupSingleBotChannels()
Loading
Loading
@@ -43,6 +46,17 @@ class BotManager
 
return Q.all([globalChannelPromise, singleChannelsPromise])
 
# Used by SocketHandler as callback to add a bot to a new channel
# (Therefor must be bound to BotManager instance)
addGameBotToChannel: (gameID, channel) =>
bot = @botList[gameID]
return unless bot?
return @_addBotToChannel(bot, channel)
shutdown: =>
clearInterval(@watcherTimer) if @watcherTimer?
@_destroyBots(@botList)
 
#
# Initial setup routines
Loading
Loading
@@ -209,11 +223,6 @@ class BotManager
return Q.all(promises)
 
 
shutdown: ->
clearInterval(@watcherTimer) if @watcherTimer?
@_destroyBots(@botList)
 
## Export class
module.exports = BotManager
Loading
Loading
 
## Include app modules
#Config = require './config'
Config = require './config'
 
 
## Basic abstraction of a socket.io room.
## If channel is public, it doesn't inform connected clients by a current list of other socket clients.
## Instances have to be created by static method `getInstance()`:
## Each channel only is allowed to have one instance - meaning to have a singleton per channel name.
## An instance destroys itself after all clients have left.
##
## Messages to the channel (and other events) are broadcasted to all clients in the channel.
## Limitation on channels flagged public: Clients get not informed by a current list of other clients in the channel.
##
class Channel
@_instances: {}
Loading
Loading
@@ -12,26 +17,33 @@ class Channel
uniqueClientsMap: null
 
name: 'default'
isPublic: false
creatorID: 0
title: ''
isPublic: false
isCustom: false
 
eventNameMsg: 'message#unnamed'
eventNameLeave: 'leave#unnamed'
eventNameDelete: 'delete#unnamed'
eventNameHistory: 'history#unnamed'
listenerNameDisconnect: 'disconnectListener_unnamed'
 
constructor: (data) ->
@_updateUniqueClientsMap() # Initialize map of unique clients
 
@name = data.name or @name
@isPublic = data.is_public or @isPublic
@creatorID = data.creator_id or @creatorID
@title = data.title or @name
@isPublic = data.is_public or @isPublic
@isCustom = @name.indexOf(Config.INTERN_NONGAME_CHANNEL_PREFIX) is 0
 
log.debug "Creating new channel '#{@name}'"
 
@eventNameMsg = 'message#' + @name
@eventNameLeave = 'leave#' + @name
@eventNameDelete = 'delete#' + @name
@eventNameHistory = 'history#' + @name
@listenerNameDisconnect = 'disconnectListener_' + @name
 
@getInstance: (channelData) ->
name = channelData.name
Loading
Loading
@@ -49,6 +61,8 @@ class Channel
_checkForDestroy: ->
if @getNumberOfClients() is 0
@_destroy()
return true
return false
 
 
#
Loading
Loading
@@ -60,17 +74,38 @@ class Channel
return Object.keys(clientsMap).length
 
_registerListeners: (clientSocket) ->
# Store callback for channel-specific disconnect on socket
clientSocket[@listenerNameDisconnect] = disconnectCallback = =>
@_handleClientDisconnect(clientSocket)
# Register channel-specific client events
clientSocket.on @eventNameMsg, (messageText) => @_handleClientMessage(clientSocket, messageText)
clientSocket.on @eventNameLeave, => @_handleClientLeave(clientSocket)
clientSocket.on @eventNameLeave, (isClose) => @_handleClientLeave(clientSocket, isClose)
clientSocket.on @eventNameDelete, => @_handleClientDeleteRequest(clientSocket)
clientSocket.on @eventNameHistory, => @_handleClientHistoryRequest(clientSocket)
clientSocket.on 'disconnect', => @_handleClientLeave(clientSocket, true)
clientSocket.on 'disconnect', disconnectCallback
_unregisterListeners: (clientSocket) ->
# Deregister listeners
clientSocket.removeAllListeners @eventNameMsg
clientSocket.removeAllListeners @eventNameLeave
clientSocket.removeAllListeners @eventNameDelete
clientSocket.removeAllListeners @eventNameHistory
clientSocket.removeListener 'disconnect', clientSocket[@listenerNameDisconnect]
 
addClient: (clientSocket, isRejoin=false) ->
return false if clientSocket.rooms.indexOf(@name) >= 0 # Cancel, if socket is already joined to channel
log.debug "Adding client '#{clientSocket.identity.getName()}' to channel '#{@name}'"
isExistingIdentity = @_hasUniqueClient(clientSocket)
 
channelInfo =
title: @title
creatorID: @creatorID
isPublic: @isPublic
isCustom: @isCustom
ircChannelName: @ircChannelName # Only available, when called from sub class BotChannel
 
@_sendToSocket(clientSocket, 'joined', channelInfo) # Notice client for channel join
clientSocket.join(@name) # Join client to room of channel
Loading
Loading
@@ -92,7 +127,7 @@ class Channel
 
# Permanently register client for channel
unless isRejoin
db.addClientToChannel(clientSocket, @name)
db.addClientToChannel(clientSocket.identity, @name)
else
# Send initial user list to client
if @isPublic
Loading
Loading
@@ -100,16 +135,24 @@ class Channel
else
@_sendUserListToSocket(clientSocket)
 
removeClient: (clientSocket, isDisconnect=false) ->
return true
removeClient: (clientSocket, isClose=false, isDisconnect=false) ->
# Cancel, if socket is not joined to channel (but force on disconnect)
return false if not isDisconnect and clientSocket.rooms.indexOf(@name) < 0
log.debug "Removing client #{clientSocket.identity.getName()} from channel '#{@name}' (by close: #{isClose})"
# Unregister events for this channel
clientSocket.removeAllListeners @eventNameMsg
clientSocket.removeAllListeners @eventNameLeave
clientSocket.removeAllListeners @eventNameHistory
clientSocket.removeAllListeners 'disconnect'
@_unregisterListeners(clientSocket)
 
# Update client
clientSocket.leave(@name) # Remove client from room of channel
@_sendToSocket(clientSocket, 'left') # Notice client for channel leave
leaveInfo =
title: @title
isClose: isClose
clientSocket.leave(@name) # Remove client from room of channel
@_sendToSocket(clientSocket, 'left', leaveInfo) # Notice client for channel leave
 
# Update the list of unique identities (May removes client's identity)
@_updateUniqueClientsMap()
Loading
Loading
@@ -124,13 +167,54 @@ class Channel
@_sendUserChangeToRoom('remove', leaveAction, clientSocket.identity)
@_sendUserListToRoom()
 
# Permanently unregister client for channel
unless isDisconnect
db.removeClientFromChannel(clientSocket, @name)
# Permanently unregister client from channel
unless isClose
db.removeClientFromChannel(clientSocket.identity, @name)
 
# Remove and close instance, if last client left
@_checkForDestroy()
 
return true
# @protected
_deleteByClient: (clientSocket, customRoutine=null) ->
return false unless clientSocket.rooms.indexOf(@name) >= 0 # Cancel, if socket is not joined to channel
log.debug "Deleting channel '#{@name}' by client #{clientSocket.identity.getName()}"
# Unregister events for this channel
@_unregisterListeners(clientSocket)
# Inform clients (There may be multiple sockets, but only one identity)
deleteInfo =
title: @title
@_sendToRoom('deleted', deleteInfo, false)
# Kick all sockets out of room
clientMetaList = io.sockets.adapter.rooms[@name]
for clientID of clientMetaList
currClientSocket = io.sockets.connected[clientID] # This is the socket of each client in the room
if currClientSocket?
@_unregisterListeners(currClientSocket)
currClientSocket.leave(@name)
# Update the list of unique identities (Should now be empty)
@_updateUniqueClientsMap()
# Optionally run custom routine (For child classes)
customRoutine?()
# Remove and close instance, if last client left
is_destroyed = @_checkForDestroy()
# Delete channel from database (For security, only delete, if channel has been destroyed)
if is_destroyed
db.deleteChannel(@name)
return true
 
#
# Sending routines
Loading
Loading
@@ -147,11 +231,21 @@ class Channel
promise.then (logListData) =>
oldestTimestamp = logListData[0]?.timestamp or -1
newestTimestamp = logListData[logListData.length - 1]?.timestamp or -1
# Filter last entry, if it's the client's join (Cause in this case, the history is requested on joining)
if not @isPublic
lastEntry = logListData[logListData.length-1]
if lastEntry?.event_name is 'user_change'
eventData = JSON.parse(lastEntry.event_data) or {}
logListData.pop() if eventData.type is 'add' or eventData.action is 'join'
# Build marker data
markerData =
count: logListData.length
start: oldestTimestamp
end: newestTimestamp
 
# Send history
@_sendToSocket(clientSocket, 'history_start', markerData)
for logEntry in logListData
eventData = JSON.parse(logEntry.event_data)
Loading
Loading
@@ -209,20 +303,40 @@ class Channel
 
# May be overridden
# @protected
_handleClientMessage: (clientSocket, messageText) =>
_handleClientMessage: (clientSocket, messageText) ->
log.debug "Client message to '#{@name}':", messageText
messageText = messageText?.trim()
messageText = messageText?.trim() or ''
return if messageText is ''
@_sendMessageToRoom(clientSocket.identity, messageText)
 
_handleClientLeave: (clientSocket, isDisconnect=false) =>
log.debug "Removing client from channel '#{@name}' (by disconnect: #{isDisconnect}):", clientSocket.identity
@removeClient(clientSocket, isDisconnect)
_handleClientHistoryRequest: (clientSocket) =>
log.debug "Client requests chat history for '#{@name}'"
@_sendHistoryToSocket(clientSocket)
 
_handleClientLeave: (clientSocket, isClose=false) ->
if not isClose and clientSocket.identity.getUserID() is @creatorID
# Disallow permanent leaving on channels created by the client
@_sendToSocket(clientSocket, 'leave_fail', 'Cannot leave own channels')
else
@removeClient(clientSocket, isClose)
_handleClientDisconnect: (clientSocket) ->
# Immediately unregister listeners
@_unregisterListeners(clientSocket)
# Delay disconnect for configured time - This will allow to rejoin before disconnect is executed
delay_promise = Q.delay(Config.CLIENTS_DISCONNECT_DELAY)
delay_promise = delay_promise.then =>
@removeClient(clientSocket, true, true)
delay_promise.done()
_handleClientDeleteRequest: (clientSocket) ->
if clientSocket.identity.getUserID() isnt @creatorID
@_sendToSocket(clientSocket, 'delete_fail', 'Can only delete own channels')
else if @getNumberOfClients() > 1
@_sendToSocket(clientSocket, 'delete_fail', 'Can only delete empty channels')
else
@_deleteByClient(clientSocket)
 
#
# Helpers
Loading
Loading
@@ -252,7 +366,7 @@ class Channel
clientIdentity = clientSocket.identity
clientsMap[clientIdentity.getGlobalID()] = clientIdentity if clientIdentity?
else
clientsMap = @uniqueClientsMap
clientsMap = @uniqueClientsMap or {}
 
return clientsMap
 
Loading
Loading
@@ -271,7 +385,6 @@ class Channel
 
 
 
## Export class
module.exports = Channel
 
 
## Abstraction of a client's identity.
## Used to identify clients and to store information to be sent to other clients.
## Instances have to be created by appropriate factory methods of the class.
##
class ClientIdentity
id: 0
idGame: 0
Loading
Loading
##
## CONFIG ##
##
 
# Bot sub config
botNickPrefix = '_Galaxy' # Prefix for bot's nick name on IRC
botName = 'SGR GalaxyBot' # Bot's official name
botVersion = 'v1.1' # Bot's version number string
botLastUpdate = '2014-12-28' # Update info for bot version
# IRC sub config
ircServerIP = 'underworld1.no.quakenet.org'
ircServerPort = 6667
ircGlobalChannel = '#sgr2'
# MySQL sub config
# MySQL access config
mysqlServerIP = '127.0.0.1'
mysqlServerPort = 3306
mysqlUser = 'sgr'
mysqlUser = 'USERNAME'
mysqlPassword = 'SECRET'
mysqlCommonDatabase = 'irc_gateway' # The name of the database in which the app stores its own tables and/or core tables of the game
mysqlGameDatabasePrefix = 'game_world_' # The name prefix of the database in which tables of a game world can be found (To be appended with a database id)
 
# IRC access config
ircServerIP = 'underworld1.no.quakenet.org'
ircServerPort = 6667
ircGlobalChannel = '#irc_gateway_test'
# Bot sub config
botNickPrefix = '_Game' # Prefix for bot's nick name on IRC
botName = 'GameCommBot' # Bot's official name
botVersion = 'v1.2' # Bot's version number string
botLastUpdate = '2015-02-13' # Update info for bot version
 
# Main config
## Main config
module.exports =
 
DEBUG_ENABLED: true # Set to true, to enable some debug output
DEBUG_IRC_COMM: false # Set to true, to enable debug output from irc communication by bots
AUTH_ENABLED: false # Set to true, to enable client authentification by a security token (Otherwise all valid player IDs will be accepted)
REQUIRE_CHANNEL_PW: false # Set to true, to force clients to set a password when creating a channel (The password then must be at least 3 digits long)
 
WEB_SERVER_PORT: 8050 # The port of the webserver started by this app
 
CLIENT_AUTH_SECRET: 'SECRET_2' # A secret string to be used as part of the security token (The token needs to be sent from a client on login)
 
GAMES_LOOKUP_INTERVAL: 20 # Interval time in seconds, for looking up the games list in database and create/destroy appropriate bots accordingly
GAMES_LOOKUP_INTERVAL: 60 # Interval time in seconds, for looking up the games list in database and create/destroy appropriate bots accordingly
CLIENTS_DISCONNECT_DELAY: 2000 # Timeout in milliseconds, for waiting for reconnect of a client before broadcasting its disconnection (If it reconnects before timeout, nothing is broadcasted)
MAX_CHANNEL_LOGS: 100 # Maximum number of logs per channel in database - This controls the max size of the chat logs table
MAX_CHANNEL_LOGS_TO_CLIENT: 50 # Maximum number of channel logs for a client - This controls the max length of a channel's chat history a client can request
MAX_CHANNELS_PER_CLIENT: 3 # Maximum number of channels a client/user is allowed to create
MAX_BOTS: 5 # Maximum number of simultaneously existing bots - Use this, to obey connection limits to IRC server (Will also limit number of chats for game worlds)
 
BOT_RECONNECT_DELAY: 61000 # Delay time in milliseconds, for reconnecting to server, when connection has been refused
BOT_NICK_PATTERN: "#{botNickPrefix}<id>" # The nick name of the Bot on IRC, with <id> as a placeholder for the game ID
BOT_USERNAME_PATTERN: "Galaxy<id>Bot" # The user name of the Bot on IRC, with <id> as a placeholder for the game ID
BOT_USERNAME_PATTERN: "#{botName}_<id>" # The user name of the Bot on IRC, with <id> as a placeholder for the game ID
BOT_REALNAME_PATTERN: "<name> - #{botName} <id>" # The real name of the Bot on IRC, with <id> and <name> as placeholders for the game ID and name
BOT_VERSION_STRING: "#{botName}, #{botVersion} " + # The version string of the Bot, for requests on IRC
"(Last update: #{botLastUpdate}) " +
Loading
Loading
@@ -50,14 +54,12 @@ module.exports =
 
IRC_SERVER_IP: ircServerIP
IRC_SERVER_PORT: ircServerPort
IRC_GLOBAL_CHANNEL: ircGlobalChannel
#IRC_LOCAL_CHANNEL_PATTERN: '#sgr_ingame_galaxy_<id>'
#IRC_LOCAL_CHANNEL_PASSWORD: '!This1Is2The3Ultimate4PW5!'
IRC_NONGAME_CHANNEL_PREFIX: '#igw_ingame_'
 
INTERN_GLOBAL_CHANNEL_TITLE: "IRC (#{ircGlobalChannel})"
INTERN_GLOBAL_CHANNEL_NAME: 'irc_channel'
INTERN_GAME_CHANNEL_PREFIX: 'galaxy_'
INTERN_GLOBAL_CHANNEL_TITLE: 'Community IRC'
INTERN_GLOBAL_CHANNEL_NAME: 'community_channel'
INTERN_GAME_CHANNEL_PREFIX: 'game_'
INTERN_NONGAME_CHANNEL_PREFIX: 'channel_' # Must differ from INTERN_GAME_CHANNEL_PREFIX
 
SQL_HOST: mysqlServerIP
Loading
Loading
@@ -69,9 +71,11 @@ module.exports =
SQL_SOCKET_PATH: '/var/run/mysqld/mysqld.sock' # Define this path on unix systems. Find out the path using 'mysqladmin variables | grep sock'
 
SQL_TABLES:
GAMES_LIST: 'game_worlds' # The name of the table in common db, which contains the list of game worlds
PREFIX_GAME_TABLE: 'game_' # The name prefix of tables in game db, which contain data of a game world's contents
POSTFIX_GAME_PLAYERS: '_players' # The name postfix of the game table, which contains the list of a game world's players
CHANNEL_LIST: 'chat - channels'
CHANNEL_JOININGS: 'chat - channeljoins'
CHANNEL_LOGS: 'chat - channellogs'
GAMES_LIST: 'game_worlds' # The name of the table in common db, which contains the list of game worlds
PREFIX_GAME_TABLE: 'game_' # The name prefix of tables in game db, which contain data of a game world's contents
POSTFIX_GAME_PLAYERS: '_players' # The name postfix of the game table, which contains the list of a game world's players
CHANNEL_LIST: 'chat - channels' # The name of the table for storing non-game/custom channels (Must be created, see database_setup.sql)
CHANNEL_JOININGS: 'chat - channeljoins' # The name of the table for storing user joins to custom channels (Must be created, see database_setup.sql)
CHANNEL_LOGS: 'chat - channellogs' # The name of the table for storing chat histories of all channels (Must be created, see database_setup.sql)
This diff is collapsed.
Loading
Loading
@@ -11,22 +11,24 @@ getTimestamp = ->
return "[#{dateTimeString}]"
 
 
module.exports.info = (text...) ->
console.log '#', getTimestamp(), text...
#gateway_global.db.writeLog()
if Config.DEBUG_ENABLED
module.exports.debug = (text...) ->
console.log '=>', getTimestamp(), text...
else
module.exports.debug = ->
 
module.exports.info = (text...) ->
console.log '#', getTimestamp(), text...
module.exports.warn = (text, sender='General') ->
console.warn "! #{getTimestamp()} Warning by #{sender}:", text
 
module.exports.error = (textOrError, sender='General') ->
if textOrError instanceof Error
textOrError = if textOrError.message? then textOrError.message else textOrError.toString?() or textOrError
errObject = textOrError
textOrError = if errObject.message? then errObject.message else errObject.toString?() or errObject
console.error "!! #{getTimestamp()} ERROR by #{sender}:", textOrError
if errObject?
console.error ''
console.error(errObject.stack)
console.error ''
Loading
Loading
@@ -13,8 +13,8 @@ Database = require './database'
SocketHandler = require './sockethandler'
BotManager = require './botmanager'
 
BotChannel = require './botchannel'
Bot = require './bot'
## Configure global libraries
Q.longStackSupport = Config.DEBUG_ENABLED # On debug mode, enable better stack trace support for promises (Performance overhead)
 
## Create library API objects
app = express()
Loading
Loading
@@ -39,8 +39,8 @@ class Gateway
 
constructor: ->
@_bindServerEvents()
@socketHandler = new SocketHandler()
@botManager = new BotManager()
@socketHandler = new SocketHandler(@botManager.addGameBotToChannel)
 
_bindServerEvents: ->
## Register http server events
Loading
Loading
@@ -74,7 +74,7 @@ class Gateway
return @botManager.start()
 
# Start listening for socket.io emits and for HTTP requests
startupPromise.then =>
startupPromise = startupPromise.then =>
log.info 'Start listening...'
server.listen(Config.WEB_SERVER_PORT)
 
Loading
Loading
Loading
Loading
@@ -9,21 +9,30 @@ Channel = require './channel'
BotChannel = require './botchannel'
 
 
## Main class
## Abstraction of a handler for common client request on a socket.io socket.
## Handles client connects/disconnects, auth requests and channel join/creation requests.
## Additional channel-specific requests are handled by Channel instances.
## To be used as singleton.
##
class SocketHandler
 
constructor: ->
constructor: (addGameBotToChannelCallback) ->
@_addGameBotToChannel = addGameBotToChannelCallback
 
start: ->
@_bindSocketGlobalEvents()
 
_bindSocketGlobalEvents: ->
## Register common websocket events
# Register common websocket events
io.sockets.on 'connection', @_handleClientConnect # Build-in event
 
_bindSocketClientEvents: (clientSocket) ->
## Register client socket events
clientSocket.on 'disconnect', => @_handleClientDisconnect(clientSocket) # Build-in event
# Store callback for disconnect on socket
clientSocket.disconnectListener = =>
@_handleClientDisconnect(clientSocket)
# Register client socket events
clientSocket.on 'disconnect', clientSocket.disconnectListener # Build-in event
clientSocket.on 'auth', (authData) => @_handleClientAuthRequest(clientSocket, authData)
 
_bindSocketClientAuthorizedEvents: (clientSocket) ->
Loading
Loading
@@ -36,10 +45,11 @@ class SocketHandler
 
_handleClientDisconnect: (clientSocket) =>
log.debug 'Client disconnected...'
# Deregister listeners
clientSocket.isDisconnected = true
clientSocket.removeAllListeners()
#clientSocket.removeAllListeners 'disconnect'
# Deregister listeners
clientSocket.removeListener 'disconnect', clientSocket.disconnectListener
clientSocket.removeAllListeners 'auth'
clientSocket.removeAllListeners 'join'
 
_handleClientAuthRequest: (clientSocket, authData) =>
log.debug 'Client requests auth...'
Loading
Loading
@@ -49,20 +59,20 @@ class SocketHandler
gameID = authData.gameID
securityToken = authData.token
authPromise = Q.fcall =>
throw new Error('Invalid user data')
throw db.createValidationError('Invalid user data')
 
# Check auth data
if userID? and gameID?
authPromise = ClientIdentity.createFromDatabase(userID, gameID)
authPromise = authPromise.fail (err) =>
throw new Error('Unknown user') # Overwrite error
throw db.createValidationError('Unknown user') # Overwrite error
authPromise = authPromise.then (clientIdentity) =>
if Config.AUTH_ENABLED and securityToken isnt clientIdentity.securityToken
throw new Error('Invalid token')
throw db.createValidationError('Invalid token')
return clientIdentity
 
# Handle auth success/fail
authPromise.then (clientIdentity) =>
authPromise = authPromise.then (clientIdentity) =>
return if clientSocket.isDisconnected
log.debug 'Client auth granted:', clientIdentity
 
Loading
Loading
@@ -76,35 +86,111 @@ class SocketHandler
clientSocket.emit 'welcome', "Hello #{clientIdentity.getName()}, you are now online!"
 
# Add client to its channels
@_acceptNewClient(clientSocket)
return @_acceptNewClient(clientSocket)
 
authPromise.fail (err) =>
authPromise = authPromise.fail (err) =>
throw err unless err.isValidation
return if clientSocket.isDisconnected
log.debug 'Client auth rejected:', err.message
# Emit auth fail
clientSocket.emit 'auth_fail', err.message # TODO: Send notice based on auth error
clientSocket.emit 'auth_fail', err.message
 
_acceptNewClient: (clientSocket) ->
# Let client join default channel
#botChannel = BotChannel.getInstance(Config.INTERN_BOT_CHANNEL_NAME, Config.IRC_CHANNEL_GLOBAL) # TODO
#botChannel.addClient(clientSocket, true)
# End chain to observe errors (non-validation-errors)
authPromise.done()
 
# Let client join to saved channels
_acceptNewClient: (clientSocket) ->
# Let client join to saved channels (and default channels)
promise = db.getClientChannels(clientSocket.identity)
promise.then (channelList) =>
#console.log 'LIST', channelList
for channelData in channelList
channel = Channel.getInstance(channelData)
channel.addClient(clientSocket, true)
promise = promise.fail (err) =>
throw err unless err.isDatabaseResult
return promise
 
 
_handleClientChannelJoin: (clientSocket, channelData) =>
# TODO
# - Check for channel existence in DB
# - If it not exist, create it in DB
# - Create/Get Channel instance
# - Add client to channel instance
# - If channel is a bot Channel: Add the bot of client's game
return unless clientSocket.identity?
gameID = clientSocket.identity.getGameID()
requestedChannelTitle = channelData?.title or null
requestedChannelPassword = channelData?.password or null
return unless requestedChannelTitle?
log.debug 'Client requests join for channel, data:', channelData
# Check channel data
promise = Q.fcall =>
return if clientSocket.isDisconnected
checkData =
game_id: clientSocket.identity?.getGameID()
title: channelData.title
password: channelData.password
is_public: channelData.isPublic
is_for_irc: channelData.isForIrc
return db.getValidatedChannelDataForCreation(checkData)
# Check for existent channel
promise = promise.then (validChannelData) =>
channelData = validChannelData # Overwrite in outer scope (for passing to fail handler)
return db.getChannelDataByTitle(gameID, requestedChannelTitle)
# Handle existing/non-existing channel
promise = promise.fail (err) =>
# Cancel channel creation, if error is not from existence check
throw err unless err.isDatabaseResult
throw err if clientSocket.isDisconnected
# Channel does not exist yet, try creating it
return @_createNewChannel(clientSocket.identity, channelData)
promise = promise.then (existingChannelData) =>
return if clientSocket.isDisconnected
# Channel does (now) exist, try joining
@_joinClientToChannel(clientSocket, existingChannelData, requestedChannelPassword)
promise = promise.fail (err) =>
return if err.isDatabaseResult
throw err unless err.isValidation
return if clientSocket.isDisconnected
log.debug 'Client channel join rejected:', err.message
# Emit join fail
clientSocket.emit 'join_fail', err.message
# End chain to observe errors (non-validation-errors)
promise.done()
_createNewChannel: (clientIdentity, channelData) ->
# Check limit of created channels
countPromise = db.getClientCreatedChannelsCount(clientIdentity)
countPromise = countPromise.then (channelsCount) =>
if channelsCount >= Config.MAX_CHANNELS_PER_CLIENT
throw db.createValidationError('Reached channel limit')
return true
# If limit not reached, create the channel
createPromise = countPromise.then =>
log.debug 'Storing data for new channel:', channelData
return db.createChannelByData(clientIdentity, channelData)
return createPromise
_joinClientToChannel: (clientSocket, channelData, requestedChannelPassword) ->
# Check channel password
if (requestedChannelPassword or '') isnt (channelData.password or '')
throw db.createValidationError('Wrong password')
# Get/create channel instance
if channelData.irc_channel
channel = BotChannel.getInstance(channelData)
@_addGameBotToChannel(channelData.game_id, channel)
else
channel = Channel.getInstance(channelData)
# Let client join the channel
channel.addClient(clientSocket)
 
 
## Export class
Loading
Loading
Loading
Loading
@@ -4,6 +4,10 @@ class this.ChatController
 
socketHandler: null
 
CHANNEL_NAME_MIN_LENGTH = 4
CHANNEL_NAME_MAX_LENGTH = 20
CHANNEL_PASSWORD_MIN_LENGTH = 5
serverIP: ''
serverPort: 0
instanceData: {}
Loading
Loading
@@ -16,17 +20,28 @@ class this.ChatController
isSignalizingMessagesToWindow: false
 
gui:
chatForm: '#chat_form'
chatInput: '#chat_input'
multilangContents: '*[data-content]'
channelCreateForm: '#channelCreateForm'
channelNameInput: '#channelNameInput'
channelPasswordInput: '#channelPasswordInput'
channelFlagPublic: '#channelFlagPublic'
channelFlagIRC: '#channelFlagIRC'
channelCloseButton: '.channelCloseButton'
channelLeaveButton: '.channelLeaveButton'
channelDeleteButton: '.channelDeleteButton'
chatForm: '.chatForm'
chatInput: '.chatForm .chatInput'
tabsystemViewport: '#tabsystem .tabsystemViewport'
tabsystemHeaderList: '#tabsystem .tabsystemHeaders'
tabsystemHeaders: '.tabsystemHeaders li'
tabPagesMessagesPage: '.chatMessagesContainer'
tabPagesMessages: '.chatMessages'
tabPagesUsers: '.chatUsers'
tabPagesUsersIngame: '.chatUsers.players'
tabPagesUsersIrc: '.chatUsers.irc'
tabPagesUsersNumberBox: '.chatUsersCount'
tabPagesUsersNumberText: '.chatUsersCount .title'
tabPagesUsersNumberValue: '.chatUsersCount .value'
tabPagesChannelNameBox: '.chatChannelName'
tabPagesChannelNameValue: '.chatChannelName .value'
tabPagesOfChannels: '#tabsystem .tabsystemViewport > div[data-channel]'
tabPageServer: '#tabPageServer'
tabPageSkeleton: '#tabPageSkeleton'
Loading
Loading
@@ -35,8 +50,12 @@ class this.ChatController
addressTabMarker: '.addressed'
 
events:
'channelCreateForm submit': '_handleGuiChannelCreateSubmit'
'chatForm submit': '_handleGuiMessageSubmit'
'tabsystemHeaders click': '_handleGuiTabClick'
'channelCloseButton click': '_handleGuiChannelClose'
'channelLeaveButton click': '_handleGuiChannelLeave'
'channelDeleteButton click': '_handleGuiChannelDelete'
 
 
constructor: (@serverIP, @serverPort, @instanceData, options={}) ->
Loading
Loading
@@ -48,6 +67,8 @@ class this.ChatController
@windowTitleBackup = top.document.title
document.addEventListener('visibilitychange', => @_handleWindowVisibilityChange())
 
@_translateMultilangContents()
start: ->
@socketHandler = new SocketClient(this, @serverIP, @serverPort, @instanceData)
@socketHandler.start()
Loading
Loading
@@ -79,6 +100,12 @@ class this.ChatController
@_bindGuiElements()
@_bindGuiEvents()
 
_translateMultilangContents: ->
@ui.multilangContents.each (idx, element) =>
textElem = $(element)
translatedText = Translation.get(textElem.data('content'))
textElem.text(translatedText) if translatedText
 
#
# GUI event handling
Loading
Loading
@@ -89,11 +116,40 @@ class this.ChatController
top.document.title = @windowTitleBackup # Reset window title
@_resetNewEntryMarkOfTab(@activeTabPage) # Reset marker for unread messages
 
_handleGuiMessageSubmit: (event) =>
_handleGuiChannelCreateSubmit: (event) =>
event.preventDefault()
channelName = @ui.channelNameInput.val().trim()
channelPassword = @ui.channelPasswordInput.val().trim()
isPublic = @ui.channelFlagPublic.prop('checked') or false
isForIrc = @ui.channelFlagIRC.prop('checked') or false
return unless channelName
@socketHandler.sendChannelJoinRequest(channelName, channelPassword, isPublic, isForIrc)
_handleGuiChannelClose: (event) =>
event.preventDefault()
channel = @activeTabPage?.data('channel') or ''
@socketHandler.sendChannelLeaveRequest(channel, true)
_handleGuiChannelLeave: (event) =>
event.preventDefault()
messageText = @ui.chatInput.val().trim()
channel = @activeTabPage?.data('channel') or ''
 
if confirm(Translation.get('confirm_dialog.leave_channel'))
@socketHandler.sendChannelLeaveRequest(channel, false)
_handleGuiChannelDelete: (event) =>
event.preventDefault()
channel = @activeTabPage?.data('channel') or ''
if confirm(Translation.get('confirm_dialog.delete_channel'))
@socketHandler.sendChannelDeleteRequest(channel)
_handleGuiMessageSubmit: (event) =>
event.preventDefault()
channel = @activeTabPage?.data('channel') or ''
messageText = @activeTabPage?.find(@gui.chatInput).val().trim()
if messageText isnt '' and channel isnt ''
@socketHandler.sendMessage(channel, messageText)
 
Loading
Loading
@@ -114,8 +170,8 @@ class this.ChatController
# Show new active tab
@activeTabPage.show()
 
# Toggle usability of input form
@ui.chatForm.toggleClass('inactive', tabID is @ui.tabPageServer.attr('id'))
# Focus input field
@activeTabPage.find(@gui.chatInput).focus()
 
# Reset marker for unread messages
@_resetNewEntryMarkOfTab(@activeTabPage)
Loading
Loading
@@ -131,6 +187,10 @@ class this.ChatController
# Socket client handling
#
 
isHistoryReceivingChannel: (channel) ->
tabPage = @_getChannelTabPage(channel)
return @_isHistoryReceivingTab(tabPage)
handleServerDisconnect: ->
# Inform all channel tabs and clear user lists
informText = Translation.get('msg.server_connection_lost')
Loading
Loading
@@ -140,9 +200,11 @@ class this.ChatController
@_clearUserListOfTab(tabPage)
@_addNewEntryMarkToTab(tabPage, {force: true}, informText) if idx is 0
 
handleServerMessage: (msg) ->
handleServerMessage: (msg, isError=false) ->
messageType = 'log'
messageType = 'error' if isError
tabPage = @ui.tabPageServer
@_appendNoticeToTab(tabPage, null, 'log', msg)
@_appendNoticeToTab(tabPage, null, messageType, msg)
@_addNewEntryMarkToTab(tabPage)
 
handleChannelMessage: (channel, timestamp, data) ->
Loading
Loading
@@ -152,7 +214,7 @@ class this.ChatController
 
handleChannelNotice: (channel, timestamp, data) ->
tabPage = @_getChannelTabPage(channel)
@_appendNoticeToTab(tabPage, timestamp, 'notice', data.text, true)
@_appendNoticeToTab(tabPage, timestamp, 'notice', data.text, isSentByUser: true)
@_addNewEntryMarkToTab(tabPage, data, data.text)
 
handleChannelHistoryMark: (channel, timestamp, data) ->
Loading
Loading
@@ -173,43 +235,103 @@ class this.ChatController
tabID = @_getChannelTabID(channel)
tabPage = @_getChannelTabPage(channel)
channelTitle = data?.title or channel
ircChannelName = data.ircChannelName or null
isNewTab = (tabPage?.length is 0)
 
if isNewTab
# Set up tab parts
htmlTabHeader = "<li data-id=\"#{tabID}\">#{channelTitle}</li>"
# Build tab header
tabHeaderTitle = $("<span/>")
tabHeaderTitle.addClass('title')
tabHeaderTitle.text(channelTitle)
tabHeader = $("<li/>")
tabHeader.attr('data-id', tabID)
tabHeader.attr('title', channelTitle)
tabHeader.append(tabHeaderTitle)
# Build tab body
tabSkeleton = @ui.tabPageSkeleton.clone()
tabSkeleton.attr('id', tabID)
tabSkeleton.attr('data-channel', channel)
 
# Add tab to DOM
@ui.tabsystemViewport.append(tabSkeleton)
@ui.tabsystemHeaderList.append(htmlTabHeader)
@ui.tabsystemHeaderList.append(tabHeader)
@_updateGuiBindings()
 
# Get new tab
tabPage = @_getChannelTabPage(channel)
tabPage.hide()
 
# Print join message to tab
# Hide non-default boxes
tabPage.find(@gui.tabPagesUsersNumberBox).hide()
tabPage.find(@gui.tabPagesChannelNameBox).hide()
tabPage.find(@gui.tabPagesUsersIngame).hide()
# Remove invalid buttons
unless data.isCustom
tabPage.find(@gui.channelLeaveButton).remove()
if data.creatorID is @instanceData?.userID
tabPage.find(@gui.channelLeaveButton).remove()
else
tabPage.find(@gui.channelDeleteButton).remove()
# Print join message to new tab and server tab
noticeText = Translation.get('msg.channel_joined', channel: channelTitle)
@_appendNoticeToTab(tabPage, timestamp, 'initial_join', noticeText)
@handleServerMessage(noticeText)
# Set IRC channel name
@_setIrcChannelNameToTab(tabPage, ircChannelName) if ircChannelName?
# Reset the form for channel creation/joining
@ui.channelCreateForm[0]?.reset?()
 
return isNewTab
 
handleChannelLeft: (channel, timestamp) ->
handleChannelLeft: (channel, timestamp, {title, isClose}={}) ->
tabID = @_getChannelTabID(channel)
channelTitle = title or channel
 
# Remove tab from DOM
@ui.tabsystemViewport.find("##{tabID}").remove()
@ui.tabsystemHeaderList.find("[data-id=#{tabID}]").remove()
@_updateGuiBindings()
 
# Show server tab
@ui.tabsystemHeaders[0]?.click()
# Print leave message to server tab
unless isClose
noticeText = Translation.get('msg.channel_left', channel: channelTitle)
@handleServerMessage(noticeText)
handleChannelDeleted: (channel, timestamp, {title}={}) ->
channelTitle = title or channel
@handleChannelLeft(channel, timestamp, {title: channelTitle, isClose: true})
# Print delete message to server tab
noticeText = Translation.get('msg.channel_deleted', channel: channelTitle)
@handleServerMessage(noticeText)
handleChannelError: (channel, timestamp, errorMsg) ->
tabPage = @_getChannelTabPage(channel)
@_appendNoticeToTab(tabPage, timestamp, 'error', errorMsg)
@_addNewEntryMarkToTab(tabPage)
handleChannelUserList: (channel, clientList) ->
tabPage = @_getChannelTabPage(channel)
@_clearUserListOfTab(tabPage)
clientsNumber = 0
for identityData in clientList
@_appendUserEntryToTab(tabPage, identityData.name, identityData.title, identityData.isIrcClient)
clientsNumber++ unless identityData.isIrcClient
# Show list and number of players, if joined players are not hidden (number not zero)
if clientsNumber isnt 0
tabPage.find(@gui.tabPagesUsersIngame).show()
tabPage.find(@gui.tabPagesUsersIngame).removeClass('secret')
@_setUserNumberToTab(tabPage, clientsNumber)
else
tabPage.find(@gui.tabPagesUsersIngame).addClass('secret')
 
handleChannelUserNumber: (channel, clientsNumber) ->
tabPage = @_getChannelTabPage(channel)
Loading
Loading
@@ -269,7 +391,7 @@ class this.ChatController
userName = "-#{Translation.get('info.unknown')}-" unless userName?
noticeText = Translation.get('msg.user_list_changed', user: userName)
 
@_appendNoticeToTab(tabPage, timestamp, 'user_change', noticeText)
@_appendNoticeToTab(tabPage, timestamp, 'user_change', noticeText, data)
@_addNewEntryMarkToTab(tabPage, data, noticeText)
 
handleChannelModeChange: (channel, timestamp, {actor, mode, enabled, argument}) ->
Loading
Loading
@@ -297,25 +419,29 @@ class this.ChatController
_getTabPage: (tabID) ->
return $('#' + tabID)
 
_clearUserListOfTab: (tabPage) ->
tabPage.find(@gui.tabPagesUsers).empty()
_setUserNumberToTab: (tabPage, userNumber) ->
tabPage.find(@gui.tabPagesUsersNumberBox).show()
tabPage.find(@gui.tabPagesUsersNumberText).html(Translation.get('info.current_number_of_players'))
tabPage.find(@gui.tabPagesUsersNumberValue).html(userNumber)
 
_appendUserEntryToTab: (tabPage, shortName, fullName, isIrcUser) ->
itemText = shortName
itemText += ' [IRC]' if isIrcUser
_setIrcChannelNameToTab: (tabPage, ircChannelName) ->
tabPage.find(@gui.tabPagesChannelNameBox).show()
tabPage.find(@gui.tabPagesChannelNameValue).html(ircChannelName)
_clearUserListOfTab: (tabPage) ->
tabPage.find(@gui.tabPagesUsersIngame).empty()
tabPage.find(@gui.tabPagesUsersIrc).empty()
 
_appendUserEntryToTab: (tabPage, shortName, fullName, isIrcUser) ->
# Build new list item
itemElem = $('<li/>')
itemElem.attr('title', fullName)
itemElem.text(itemText)
itemElem.text(shortName)
 
# Append item to list
messagesElem = tabPage.find(@gui.tabPagesUsers)
if isIrcUser
messagesElem = tabPage.find(@gui.tabPagesUsersIrc)
else
messagesElem = tabPage.find(@gui.tabPagesUsersIngame)
messagesElem.append(itemElem)
 
_appendMessageToTab: (tabPage, timestamp, {text, sender, inlineAuthor, isOwn, isMentioningOwn, isAddressingOwn}) ->
Loading
Loading
@@ -323,7 +449,7 @@ class this.ChatController
 
if isOwn
styleClasses += ' own'
@ui.chatInput.val('')
tabPage.find(@gui.chatInput).val('')
else
styleClasses += ' external'
 
Loading
Loading
@@ -340,11 +466,13 @@ class this.ChatController
@_appendEntryToTab(tabPage, timestamp, 'message', text, options)
@_scrollToBottomOfTab(tabPage)
 
_appendNoticeToTab: (tabPage, timestamp, noticeType, noticeText, isSentByUser=false) ->
_appendNoticeToTab: (tabPage, timestamp, noticeType, noticeText, {isOwn, isSentByUser}={}) ->
noticeText = "** #{noticeText}" unless tabPage is @ui.tabPageServer # Prefix notices except for server tab
timestamp = (new Date()).getTime() unless timestamp?
styleClasses = 'notice'
styleClasses += ' own' if isOwn
styleClasses += ' fromUser' if isSentByUser
styleClasses += ' error' if noticeType is 'error'
 
@_appendEntryToTab(tabPage, timestamp, 'server', noticeText, styleClasses: styleClasses)
@_scrollToBottomOfTab(tabPage)
Loading
Loading
@@ -359,7 +487,7 @@ class this.ChatController
itemElem = $('<li/>')
itemElem.attr('data-item', entryType)
itemElem.addClass(options.styleClasses)
itemElem.addClass('historical') if entryType isnt 'marker' and tabPage.hasClass('receiving-history')
itemElem.addClass('historical') if entryType isnt 'marker' and @_isHistoryReceivingTab(tabPage)
 
if entryTimestamp?
spanElem = $('<span/>').addClass('time')
Loading
Loading
@@ -405,7 +533,7 @@ class this.ChatController
 
_addNewEntryMarkToTab: (tabPage, notifyData={}, notifyText=null) ->
tabID = tabPage.attr('id')
isReceivingHistory = tabPage.hasClass('receiving-history')
isReceivingHistory = @_isHistoryReceivingTab(tabPage)
 
# Ignore historical messages
return if isReceivingHistory
Loading
Loading
@@ -471,6 +599,8 @@ class this.ChatController
clearInterval(@windowSignalTimer) if @windowSignalTimer?
@windowSignalTimer = setInterval(blinkFunc, 800)
 
_isHistoryReceivingTab: (tabPage) ->
return tabPage.hasClass('receiving-history')
 
_scrollToBottomOfTab: (tabPage) ->
pageElem = tabPage.find(@gui.tabPagesMessagesPage)
Loading
Loading
Loading
Loading
@@ -9,6 +9,7 @@ class this.SocketClient
serverPort: 0
instanceData: null
identityData: null
lastMessageSentStamp: 0
 
constructor: (@chatController, @serverIP, @serverPort, @instanceData) ->
 
Loading
Loading
@@ -25,12 +26,17 @@ class this.SocketClient
@socket.on 'auth_fail', @_handleServerAuthFail
@socket.on 'welcome', @_handleServerWelcome
 
@socket.on 'join_fail', @_handleChannelJoinFail
@socket.on 'leave_fail', @_handleChannelLeaveFail
@socket.on 'delete_fail', @_handleChannelDeleteFail
@socket.on 'joined', @_handleChannelJoined
@socket.on 'left', @_handleChannelLeft
@socket.on 'deleted', @_handleChannelDeleted
@socket.on 'history_start', @_handleChannelHistoryStart
@socket.on 'history_end', @_handleChannelHistoryEnd
@socket.on 'message', @_handleChannelMessage
@socket.on 'notice', @_handleChannelNotice
@socket.on 'joined', @_handleChannelJoined
@socket.on 'left', @_handleChannelLeft
@socket.on 'channel_topic', @_handleChannelTopic
@socket.on 'channel_clients', @_handleChannelUserList
@socket.on 'channel_clients_count', @_handleChannelUserNumber
Loading
Loading
@@ -43,37 +49,59 @@ class this.SocketClient
#
 
_handleServerConnect: =>
@chatController.handleServerMessage('Connection established!')
@chatController.handleServerMessage('Authenticating...')
@chatController.handleServerMessage(Translation.get('manage_msg.connect_success'))
@chatController.handleServerMessage(Translation.get('manage_msg.auth_start'))
@_sendAuthRequest()
 
_handleServerDisconnect: (errorMsg) =>
@identityData = null
if errorMsg?
@chatController.handleServerMessage('Connection error: ' + errorMsg)
serverText = Translation.getForServerMessage(errorMsg)
text = Translation.get('manage_msg.connect_error', error: serverText)
@chatController.handleServerMessage(text, true)
console.error 'Connection error:', errorMsg
else
@chatController.handleServerMessage('Connection lost! Server may quit')
@chatController.handleServerMessage(Translation.get('manage_msg.connect_lost'), true)
@chatController.handleServerDisconnect()
 
_handleServerAuthAck: (identityData) =>
@identityData = identityData
@chatController.handleServerMessage('Authentication successful!')
@chatController.handleServerMessage(Translation.get('manage_msg.auth_success'))
 
_handleServerAuthFail: (errorMsg) =>
@chatController.handleServerMessage('Authentication failed!')
@chatController.handleServerMessage('Reason: ' + errorMsg)
serverText = Translation.getForServerMessage(errorMsg)
text = Translation.get('manage_msg.auth_failed', reason: serverText)
@chatController.handleServerMessage(text, true)
_handleServerWelcome: (welcomeMsg) =>
serverText = Translation.getForServerMessage(welcomeMsg)
text = Translation.get('manage_msg.welcome_message', message: serverText)
@chatController.handleServerMessage(text)
 
_handleServerWelcome: (text) =>
@chatController.handleServerMessage('Welcome message: ' + text)
_handleChannelJoinFail: (errorMsg) =>
serverText = Translation.getForServerMessage(errorMsg)
text = Translation.get('manage_msg.channel_join_failed', reason: serverText)
@chatController.handleServerMessage(text, true)
 
_handleChannelLeaveFail: (channel, timestamp, errorMsg) =>
serverText = Translation.getForServerMessage(errorMsg)
@chatController.handleChannelError(channel, timestamp, serverText)
_handleChannelDeleteFail: (channel, timestamp, errorMsg) =>
serverText = Translation.getForServerMessage(errorMsg)
@chatController.handleChannelError(channel, timestamp, serverText)
 
_handleChannelJoined: (channel, timestamp, data) =>
isOpeningJoin = @chatController.handleChannelJoined(channel, timestamp, data)
@_sendChannelHistoryRequest(channel) if isOpeningJoin # Only request history, if channel was not already opened
 
_handleChannelLeft: (channel, timestamp) =>
@chatController.handleChannelLeft(channel, timestamp)
_handleChannelLeft: (channel, timestamp, data) =>
@chatController.handleChannelLeft(channel, timestamp, data)
_handleChannelDeleted: (channel, timestamp, data) =>
@chatController.handleChannelDeleted(channel, timestamp, data)
 
_handleChannelHistoryStart: (channel, timestamp, data) =>
return if data.count is 0
Loading
Loading
@@ -100,7 +128,10 @@ class this.SocketClient
@chatController.handleChannelUserNumber(channel, clientsNumber)
 
_handleChannelUserChange: (channel, timestamp, data) =>
if not @_isOwnUser(data, 'user') or data.action in ['kick', 'kill'] # Ignore notices on own channel join/leave
isOwnHiddenChange = @_isOwnUser(data, 'user') and data.action in ['join', 'leave']
isHistoric = @chatController.isHistoryReceivingChannel(channel)
if not isOwnHiddenChange or isHistoric # Ignore live notices on own channel join/leave
@_simplifyUserIdentityData(data, 'user')
@_addContentMetaInfo(data, 'user')
@chatController.handleChannelUserChange(channel, timestamp, data)
Loading
Loading
@@ -114,6 +145,7 @@ class this.SocketClient
@_simplifyUserIdentityData(data, 'sender')
@_addContentMetaInfo(data, 'text')
@chatController.handleChannelMessage(channel, timestamp, data)
@lastMessageSentStamp = 0 if data.isOwn
 
_handleChannelNotice: (channel, timestamp, data) =>
@_simplifyUserIdentityData(data, 'sender')
Loading
Loading
@@ -136,8 +168,26 @@ class this.SocketClient
@socket.emit 'history#' + channel
 
sendMessage: (channel, messageText) ->
# Ignore, if last message had not been returned from server yet, but was sent since 10 seconds
return if @lastMessageSentStamp + 10000 > (new Date()).getTime()
# Send message to server
@lastMessageSentStamp = (new Date()).getTime()
@socket.emit 'message#' + channel, messageText
 
sendChannelLeaveRequest: (channel, isClose) ->
@socket.emit 'leave#' + channel, isClose
sendChannelDeleteRequest: (channel) ->
@socket.emit 'delete#' + channel
sendChannelJoinRequest: (channelName, channelPassword, isPublic, isForIrc) ->
channelData =
title: channelName or ''
password: channelPassword or ''
isPublic: isPublic or false
isForIrc: isForIrc or false
@socket.emit 'join', channelData
 
#
# Helper methods
Loading
Loading
@@ -163,8 +213,9 @@ class this.SocketClient
 
_addContentMetaInfo: (data, addressTextProperty='text') ->
unless data.isOwn
data.isMentioningOwn = @_isAddressedToOwnUser(data[addressTextProperty], false)
data.isAddressingOwn = @_isAddressedToOwnUser(data[addressTextProperty], true)
addressText = data[addressTextProperty] or ''
data.isMentioningOwn = @_isAddressedToOwnUser(addressText, false)
data.isAddressingOwn = @_isAddressedToOwnUser(addressText, true)
 
_isOwnUser: (data, nameProperty='sender') ->
ownIdentityData = @identityData or {}
Loading
Loading
Loading
Loading
@@ -3,8 +3,36 @@ class this.Translation
 
# English translated texts
TEXTS_EN = {
'server_msg.invalid_input': 'Invalid input!'
'server_msg.illegal_length_of_channel_name': 'The requested channel name is too short or too long!'
'server_msg.channel_password_too_short': 'The requested channel password is too short!'
'server_msg.channel_password_too_long': 'The requested channel password is too long!'
'server_msg.invalid_user_data': 'Invalid user data!'
'server_msg.unknown_user': 'Unknown user!'
'server_msg.invalid_token': 'Invalid token!'
'server_msg.reached_channel_limit': 'You reached the limit for self-created channels! Please delete other channels first.'
'server_msg.wrong_password': 'Wrong channel password!'
'server_msg.cannot_leave_own_channel': 'Self-created channels can not be leaved!'
'server_msg.can_only_delete_own_channels': 'A channel can only be deleted, if it has been created by yourself!'
'server_msg.can_only_delete_empty_channels': 'A channel can only be deleted, if no other users are joined to it (even if offline)!'
'manage_msg.loading_start': 'Loading...'
'manage_msg.connect_success': 'Connection established!'
'manage_msg.connect_error': 'Connection error: $error$'
'manage_msg.connect_lost': 'Connection lost! Server may quit'
'manage_msg.auth_start': 'Authenticating...'
'manage_msg.auth_success': 'Authentication successful!'
'manage_msg.auth_failed': 'Authentication failed! $reason$'
'manage_msg.welcome_message': 'Welcome message: $message$'
'manage_msg.channel_join_failed': 'Channel join failed! $reason$'
'confirm_dialog.leave_channel': 'Really want to leave the channel?'
'confirm_dialog.delete_channel': 'Really want to delete the channel?\nChat history would be lost.'
'msg.server_connection_lost': 'Error: Lost connection to server! Waiting for reconnect...'
'msg.channel_joined': 'Joined $channel$'
'msg.channel_joined': 'Joined \'$channel$\''
'msg.channel_left': 'Left \'$channel$\''
'msg.channel_deleted': 'Channel \'$channel$\' has been deleted'
'msg.initial_channel_topic': 'Channel topic: $topic$'
'msg.new_channel_topic.authorless': 'New channel topic: $topic$'
'msg.new_channel_topic.authored': '$author$ set new channel topic: $topic$'
Loading
Loading
@@ -18,16 +46,57 @@ class this.Translation
'msg.user_killed_from_server': '$user$ has been kicked from server by $actor$, reason: $reason$'
'msg.user_list_changed': 'The list of users has changed because of an unknown event for user $user$'
'msg.actor_changed_a_mode': '$actor$ set channel mode $mode_event$'
'info.current_number_of_players': 'Players online'
'info.start_of_chat_history': 'Start of chat history ($start$ - $end$)'
'info.end_of_chat_history': 'End of chat history ($start$ - $end$)'
'info.unknown': 'unknown'
'label.server_tab': 'Server'
'label.current_number_of_players': 'Players online'
'label.irc_channel_name': 'IRC'
'label.channel_join_options': 'Join or create channel'
'label.channel_creation_options': 'Further options for new channel'
'label.channel_name': 'Channel name'
'label.channel_password': 'Channel password'
'label.channel_flag_public': 'Hide joined users'
'label.channel_flag_irc': 'Mirror channel to IRC'
'label.button.close_channel': 'Close'
'label.button.leave_channel': 'Leave channel'
'label.button.delete_channel': 'Delete channel'
}
 
# German translated texts
TEXTS_DE = {
'msg.server_connection_lost': 'Fehler: Verbindung zum Server verloren! Warten auf Reconnect...'
'server_msg.invalid_input': 'Ung&uuml;ltige Eingaben!'
'server_msg.illegal_length_of_channel_name': 'Der angeforderte Channel-Name ist zu kurz oder zu lang!'
'server_msg.channel_password_too_short': 'Das angeforderte Channel-Passwort ist zu kurz!'
'server_msg.channel_password_too_long': 'Das angeforderte Channel-Passwort ist zu lang!'
'server_msg.invalid_user_data': 'Ung&uuml;ltige Benutzerdaten!'
'server_msg.unknown_user': 'Unbekannter Benutzer!'
'server_msg.invalid_token': 'Ung&uuml;ltiges Token!'
'server_msg.reached_channel_limit': 'Du hast das Limit f&uuml;r selbst erstellte Channels erreicht! L&ouml;sche bitte bestehende Channels vorher.'
'server_msg.wrong_password': 'Das Channel-Passwort ist falsch!'
'server_msg.cannot_leave_own_channels': 'Selbst erstellte Channels k&ouml;nnen nicht verlassen werden!'
'server_msg.can_only_delete_own_channels': 'Channels k&ouml;nnen nur gel&ouml;scht werden, wenn sie selbst erstellt wurden!'
'server_msg.can_only_delete_empty_channels': 'Channels k&ouml;nnen nur gel&ouml;scht werden, wenn keine anderen User beigetreten sind (selbst wenn offline)!'
'manage_msg.loading_start': 'Initialisierung l&auml;uft...'
'manage_msg.connect_success': 'Verbindung zum Server hergestellt!'
'manage_msg.connect_error': 'Verbindungsabbruch: $error$'
'manage_msg.connect_lost': 'Verbindung zum Server abgerissen! Server wurde eventuell beendet.'
'manage_msg.auth_start': 'Anmeldung l&auml;uft...'
'manage_msg.auth_success': 'Anmeldung erfolgreich!'
'manage_msg.auth_failed': 'Anmeldung fehlgeschlagen! $reason$'
'manage_msg.welcome_message': 'Willkommensnachricht: $message$'
'manage_msg.channel_join_failed': 'Channel-Beitritt fehlgeschlagen! $reason$'
'confirm_dialog.leave_channel': 'Soll der Channel wirklich verlassen werden?'
'confirm_dialog.delete_channel': 'Soll der Channel wirklich gel&ouml;scht werden?\nChatverl&auml;ufe w&uuml;rden verloren gehen.'
'msg.server_connection_lost': 'Fehler: Verbindung zum Server abgerissen! Warten auf Reconnect...'
'msg.channel_joined': 'Channel \'$channel$\' beigetreten'
'msg.channel_left': 'Channel \'$channel$\' verlassen'
'msg.channel_deleted': 'Channel \'$channel$\' wurde gel&ouml;scht'
'msg.initial_channel_topic': 'Channel-Thema: $topic$'
'msg.new_channel_topic.authorless': 'Ein neues Channel-Thema wurde gesetzt: $topic$'
'msg.new_channel_topic.authored': '$author$ hat ein neues Channel-Thema gesetzt: $topic$'
Loading
Loading
@@ -41,25 +110,51 @@ class this.Translation
'msg.user_killed_from_server': '$user$ wurde von $actor$ vom Server geworfen, Grund: $reason$'
'msg.user_list_changed': 'Die Userliste hat sich wegen einem Ereignis zu Benutzer $user$ aktualisiert'
'msg.actor_changed_a_mode': '$actor$ setzt Channel-Modus: $mode_event$'
'info.current_number_of_players': 'Spieler online'
'info.start_of_chat_history': 'Beginn des Chatverlaufs ($start$ - $end$)'
'info.end_of_chat_history': 'Ende des Chatverlaufs ($start$ - $end$)'
'info.unknown': 'Unbekannt'
'label.server_tab': 'Server'
'label.current_number_of_players': 'Spieler online'
'label.irc_channel_name': 'IRC'
'label.channel_join_options': 'Channel beitreten oder neu erstellen'
'label.channel_creation_options': 'Zusatzoptionen f&uuml;r neuen Channel'
'label.channel_name': 'Channel-Name'
'label.channel_password': 'Channel-Passwort'
'label.channel_flag_public': 'Beigetretene User verstecken'
'label.channel_flag_irc': 'Channel ins IRC spiegeln'
'label.button.close_channel': 'Schlie&szlig;en'
'label.button.leave_channel': 'Channel verlassen'
'label.button.delete_channel': 'Channel l&ouml;schen'
}
 
# Currently used translations
localTexts = TEXTS_EN
 
# Helper textarea, to decode html entities to result text
converterTextArea = $('<textarea/>')
# Returns the plaintext output for the given text, having all html entities to be resolved to real characters
@_toDecoded: (textWithEscapes) ->
converterTextArea.html(textWithEscapes) # Write/fill as html
return converterTextArea.val() # Read as value - this is the browser interpreted result of the html
 
# Returns the translations for a text with given key and replaces placeholders by given data
@get: (key, data) ->
text = localTexts[key] or ''
text = localTexts[key] or "{Missing text for key: #{key}}"
 
if data?
for name, val of data
text = text.replace("$#{name}$", val)
 
return text
return @_toDecoded(text)
# Returns the translations for the given server message
@getForServerMessage: (message) ->
key_part = message.toLowerCase().replace(/[ ]/g, '_')
text = localTexts["server_msg.#{key_part}"] or message
return @_toDecoded(text)
 
# Returns the browser language code
@getLangCode: ->
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment