mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-10 02:09:41 +00:00
[MIRROR] Chat history prototype (#10278)
Co-authored-by: Selis <12716288+ItsSelis@users.noreply.github.com> Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
36231619db
commit
09bb0e61b3
@@ -231,3 +231,22 @@ CREATE TABLE IF NOT EXISTS `round` (
|
||||
`station_name` VARCHAR(80) NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `chatlogs_ckeys` (
|
||||
`ckey` varchar(45) NOT NULL,
|
||||
`token` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`ckey`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `chatlogs_logs` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`round_id` int(11) NOT NULL DEFAULT -1,
|
||||
`target` varchar(45) NOT NULL,
|
||||
`text` mediumtext NOT NULL,
|
||||
`text_raw` mediumtext NOT NULL,
|
||||
`type` varchar(128) DEFAULT NULL,
|
||||
`created_at` bigint(20) unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `chatlogs_ckeys_FK` (`target`),
|
||||
CONSTRAINT `chatlogs_ckeys_FK` FOREIGN KEY (`target`) REFERENCES `chatlogs_ckeys` (`ckey`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
#define TOTAL_HUDS 14 // Total number of HUDs. Like body layers, and other things, it comes up sometimes.
|
||||
|
||||
#define CLIENT_FROM_VAR(I) (ismob(I) ? I:client : (isclient(I) ? I : null))
|
||||
#define CKEY_FROM_VAR(I) ((ismob(I) || isclient(I)) ? I:ckey : null)
|
||||
|
||||
// Shuttles.
|
||||
|
||||
|
||||
39
code/__defines/vchatlog.dm
Normal file
39
code/__defines/vchatlog.dm
Normal file
@@ -0,0 +1,39 @@
|
||||
/* This comment bypasses grep checks */ /var/__vchatlog
|
||||
|
||||
/proc/__detect_vchatlog()
|
||||
if (world.system_type == UNIX)
|
||||
return __vchatlog = (fexists("./libvchatlog.so") ? "./libvchatlog.so" : "libvchatlog")
|
||||
else
|
||||
return __vchatlog = "vchatlog"
|
||||
|
||||
#define VCHATLOG (__vchatlog || __detect_vchatlog())
|
||||
#define VCHATLOG_CALL(name, args...) call_ext(VCHATLOG, "byond:" + name)(args)
|
||||
|
||||
/**
|
||||
* Generates and returns a random access token, for external API communication.
|
||||
* The token is only valid for the current round.
|
||||
*
|
||||
* Arguments:
|
||||
* * ckey - Ckey of the message receiver
|
||||
* * token - Randomized token
|
||||
*/
|
||||
#define vchatlog_generate_token(ckey) VCHATLOG_CALL("generate_token", ckey)
|
||||
|
||||
/**
|
||||
* Writes a new chatlog entry to the database. This function does not return anything.
|
||||
*
|
||||
* Arguments:
|
||||
* * ckey - Ckey of the message receiver
|
||||
* * html - HTML of the received message
|
||||
* * round_id - Current ID of the round (library will resolve this to -1 if invalid or non-existant)
|
||||
*/
|
||||
#define vchatlog_write(ckey, html, round_id, type) VCHATLOG_CALL("write_chatlog", ckey, html, round_id, type)
|
||||
|
||||
/**
|
||||
* This function returns a list of the 10 most recent roundids that are available to be exported.
|
||||
* Note: -1 might appear. This id is used for internal library failures. Use with caution.
|
||||
*
|
||||
* Arguments:
|
||||
* * ckey - Ckey of the message receiver
|
||||
*/
|
||||
#define vchatlog_get_recent_roundids(ckey) VCHATLOG_CALL("get_recent_roundids", ckey)
|
||||
@@ -712,3 +712,12 @@
|
||||
/// Controls whether simple mobs may recolour themselves once/spawn by giving them the recolour verb
|
||||
/// Admins may manually give them the verb even if disabled
|
||||
/datum/config_entry/flag/allow_simple_mob_recolor
|
||||
|
||||
/// Chatlogs are now saved by calling the chatlogging library instead of letting the clients handle it
|
||||
/// REQUIRES an database
|
||||
/datum/config_entry/flag/chatlog_database_backend
|
||||
default = FALSE
|
||||
|
||||
/// The endpoint for the chat to fetch the chatlogs from (for example, the last 2500 messages on init for the history)
|
||||
/// REQUIRES chatlog_database_backend to be enabled
|
||||
/datum/config_entry/string/chatlog_database_api_endpoint
|
||||
|
||||
@@ -16,7 +16,7 @@ SUBSYSTEM_DEF(inactivity)
|
||||
var/client/C = client_list[client_list.len]
|
||||
client_list.len--
|
||||
if(C.is_afk(CONFIG_GET(number/kick_inactive) MINUTES) && can_kick(C))
|
||||
to_chat_immediate(C, world.time, span_warning("You have been inactive for more than [CONFIG_GET(number/kick_inactive)] minute\s and have been disconnected."))
|
||||
to_chat_immediate(C, span_warning("You have been inactive for more than [CONFIG_GET(number/kick_inactive)] minute\s and have been disconnected."))
|
||||
|
||||
var/information
|
||||
if(C.mob)
|
||||
|
||||
@@ -179,3 +179,6 @@
|
||||
|
||||
/// If this client has been fully initialized or not
|
||||
var/fully_created = FALSE
|
||||
|
||||
/// Token used for the external chatlog api. Only valid for the current round.
|
||||
var/chatlog_token
|
||||
|
||||
@@ -247,6 +247,9 @@
|
||||
GLOB.clients += src
|
||||
GLOB.directory[ckey] = src
|
||||
|
||||
if (CONFIG_GET(flag/chatlog_database_backend))
|
||||
chatlog_token = vchatlog_generate_token(ckey)
|
||||
|
||||
// Instantiate stat panel
|
||||
stat_panel = new(src, "statbrowser")
|
||||
stat_panel.subscribe(src, .proc/on_stat_panel_message)
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
if(target == world)
|
||||
target = GLOB.clients
|
||||
|
||||
if(islist(target) && !LAZYLEN(target))
|
||||
return
|
||||
|
||||
// Build a message
|
||||
var/message = list()
|
||||
if(type) message["type"] = type
|
||||
@@ -39,6 +42,18 @@
|
||||
// send it immediately
|
||||
SSchat.send_immediate(target, message)
|
||||
|
||||
if (CONFIG_GET(flag/chatlog_database_backend))
|
||||
if (islist(target))
|
||||
for(var/tgt in target)
|
||||
var/our_ckey = CKEY_FROM_VAR(tgt)
|
||||
if(isnull(our_ckey))
|
||||
continue
|
||||
vchatlog_write(our_ckey, html, GLOB.round_id, type)
|
||||
else
|
||||
var/our_ckey = CKEY_FROM_VAR(target)
|
||||
if(!isnull(our_ckey))
|
||||
vchatlog_write(our_ckey, html, GLOB.round_id, type)
|
||||
|
||||
/**
|
||||
* Sends the message to the recipient (target).
|
||||
*
|
||||
@@ -76,6 +91,9 @@
|
||||
if(target == world)
|
||||
target = GLOB.clients
|
||||
|
||||
if(islist(target) && !LAZYLEN(target))
|
||||
return
|
||||
|
||||
// Build a message
|
||||
var/message = list()
|
||||
if(type) message["type"] = type
|
||||
@@ -83,3 +101,15 @@
|
||||
if(html) message["html"] = html
|
||||
if(avoid_highlighting) message["avoidHighlighting"] = avoid_highlighting
|
||||
SSchat.queue(target, message)
|
||||
|
||||
if (CONFIG_GET(flag/chatlog_database_backend))
|
||||
if (islist(target))
|
||||
for(var/tgt in target)
|
||||
var/our_ckey = CKEY_FROM_VAR(tgt)
|
||||
if(isnull(our_ckey))
|
||||
continue
|
||||
vchatlog_write(our_ckey, html, GLOB.round_id, type)
|
||||
else
|
||||
var/our_ckey = CKEY_FROM_VAR(target)
|
||||
if(!isnull(our_ckey))
|
||||
vchatlog_write(our_ckey, html, GLOB.round_id, type)
|
||||
|
||||
@@ -73,11 +73,18 @@
|
||||
/datum/tgui_panel/proc/on_message(type, payload)
|
||||
if(type == "ready")
|
||||
broken = FALSE
|
||||
window.send_message("connected", list("round_id" = GLOB.round_id)) // Sends the round ID to the chat, requires round IDs
|
||||
var/list/stored_rounds = CONFIG_GET(flag/chatlog_database_backend) ? vchatlog_get_recent_roundids(client.ckey) : null
|
||||
window.send_message("connected", list(
|
||||
"round_id" = GLOB.round_id, // Sends the round ID to the chat, requires round IDs
|
||||
"chatlog_db_backend" = CONFIG_GET(flag/chatlog_database_backend),
|
||||
"chatlog_api_endpoint" = CONFIG_GET(string/chatlog_database_api_endpoint),
|
||||
"chatlog_stored_rounds" = islist(stored_rounds) ? list("0") + stored_rounds : list("0"),
|
||||
))
|
||||
window.send_message("update", list(
|
||||
"config" = list(
|
||||
"client" = list(
|
||||
"ckey" = client.ckey,
|
||||
"chatlog_token" = client.chatlog_token,
|
||||
"address" = client.address,
|
||||
"computer_id" = client.computer_id,
|
||||
),
|
||||
|
||||
@@ -587,3 +587,12 @@ JUKEBOX_TRACK_FILES config/jukebox.json
|
||||
# Admins may manually give them the verb even if disabled
|
||||
# Uncomment to enable
|
||||
#ALLOW_SIMPLE_MOB_RECOLOR
|
||||
|
||||
# Chatlogs are now saved by calling the chatlogging library instead of letting the clients handle it
|
||||
# REQUIRES an database
|
||||
# Uncomment to enable
|
||||
#CHATLOG_DATABASE_BACKEND
|
||||
|
||||
# The endpoint for the chat to fetch the chatlogs from (for example, the last 2500 messages on init for the history)
|
||||
# REQUIRES chatlog_database_backend to be enabled
|
||||
#CHATLOG_DATABASE_API_ENDPOINT https://example.com
|
||||
|
||||
BIN
libvchatlog.so
Executable file
BIN
libvchatlog.so
Executable file
Binary file not shown.
@@ -29,6 +29,7 @@
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/webpack-env": "^1.18.5",
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@typescript-eslint/parser": "^8.13.0",
|
||||
"@typescript-eslint/utils": "^8.13.0",
|
||||
"css-loader": "^7.1.2",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createAction } from 'common/redux';
|
||||
|
||||
import { createPage } from './model';
|
||||
|
||||
export const getChatData = createAction('chat/getChatData');
|
||||
export const loadChat = createAction('chat/load');
|
||||
export const rebuildChat = createAction('chat/rebuild');
|
||||
export const updateMessageCount = createAction('chat/updateMessageCount');
|
||||
|
||||
162
tgui/packages/tgui-panel/chat/chatExport.ts
Normal file
162
tgui/packages/tgui-panel/chat/chatExport.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { useGame } from '../game';
|
||||
import type { gameState } from '../game/types';
|
||||
import { useSettings } from '../settings';
|
||||
import { MESSAGE_TYPE_UNKNOWN, MESSAGE_TYPES } from './constants';
|
||||
import { canPageAcceptType } from './model';
|
||||
import { createMessageNode } from './renderer';
|
||||
import type { message, Page } from './types';
|
||||
|
||||
export function exportToDisk(
|
||||
cssText: string,
|
||||
startRound: number,
|
||||
endRound: number,
|
||||
hasTimestamps: boolean,
|
||||
page: Page | null,
|
||||
) {
|
||||
const game: gameState = useGame();
|
||||
|
||||
// Fetch from server database
|
||||
const opts: SaveFilePickerOptions = {
|
||||
id: `ss13-chatlog-${game.roundId}`,
|
||||
suggestedName: `ss13-chatlog-${game.roundId}.html`,
|
||||
types: [
|
||||
{
|
||||
description: 'SS13 file',
|
||||
accept: { 'text/plain': ['.html'] },
|
||||
},
|
||||
],
|
||||
};
|
||||
// We have to do it likes this, otherwise we get a security error
|
||||
// only 516 can do this btw
|
||||
if (startRound < endRound) {
|
||||
getRound(cssText, game, page, opts, hasTimestamps, startRound, endRound);
|
||||
} else if (startRound > 0) {
|
||||
getRound(cssText, game, page, opts, hasTimestamps, startRound);
|
||||
} else {
|
||||
getRound(cssText, game, page, opts, hasTimestamps);
|
||||
}
|
||||
}
|
||||
|
||||
async function getRound(
|
||||
cssText: string,
|
||||
game: gameState,
|
||||
page: Page | null,
|
||||
opts: SaveFilePickerOptions,
|
||||
hasTimestamps: boolean,
|
||||
startRound?: number,
|
||||
endRound?: number,
|
||||
) {
|
||||
const settings = useSettings();
|
||||
const { ckey, token } = game.userData;
|
||||
let messages: message[] = [];
|
||||
|
||||
const d = new Date();
|
||||
const utcOffset = d.getTimezoneOffset() / -60;
|
||||
|
||||
let roundToExport = startRound;
|
||||
if (!roundToExport) {
|
||||
roundToExport = game.roundId ? game.roundId : 0;
|
||||
}
|
||||
let requestString = `${game.chatlogApiEndpoint}/api/export/${ckey}/${settings.logLineCount}?start_id=${roundToExport}`;
|
||||
if (hasTimestamps) {
|
||||
requestString += `&timezone_offset=${utcOffset}`;
|
||||
}
|
||||
if (endRound) {
|
||||
requestString += `&end_id=${endRound}`;
|
||||
}
|
||||
window
|
||||
.showSaveFilePicker(opts)
|
||||
.then(async (fileHandle) => {
|
||||
await new Promise<void>((resolve) => {
|
||||
fetch(requestString, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
json.forEach(
|
||||
(obj: {
|
||||
msg_type: string | null;
|
||||
text_raw: string;
|
||||
created_at: number;
|
||||
round_id: number;
|
||||
}) => {
|
||||
const msg: message = {
|
||||
type: obj.msg_type ? obj.msg_type : '',
|
||||
html: obj.text_raw,
|
||||
createdAt: obj.created_at,
|
||||
roundId: obj.round_id,
|
||||
};
|
||||
const node = createMessageNode();
|
||||
node.innerHTML = msg.html ? msg.html : '';
|
||||
|
||||
if (!msg.type) {
|
||||
const typeDef = MESSAGE_TYPES.find(
|
||||
(typeDef) =>
|
||||
typeDef.selector && node.querySelector(typeDef.selector),
|
||||
);
|
||||
msg.type = typeDef?.type || MESSAGE_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
msg.html = node.outerHTML;
|
||||
|
||||
messages.push(msg);
|
||||
},
|
||||
);
|
||||
|
||||
let messagesHtml = '';
|
||||
if (messages) {
|
||||
for (let message of messages) {
|
||||
// Filter messages according to active tab for export
|
||||
if (page && canPageAcceptType(page, message.type)) {
|
||||
messagesHtml += message.html + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
const pageHtml =
|
||||
'<!doctype html>\n' +
|
||||
'<html>\n' +
|
||||
'<head>\n' +
|
||||
'<title>SS13 Chat Log - Round ' +
|
||||
game.roundId +
|
||||
'</title>\n' +
|
||||
'<style>\n' +
|
||||
cssText +
|
||||
'</style>\n' +
|
||||
'</head>\n' +
|
||||
'<body>\n' +
|
||||
'<div class="Chat">\n' +
|
||||
messagesHtml +
|
||||
'</div>\n' +
|
||||
'</body>\n' +
|
||||
'</html>\n';
|
||||
|
||||
try {
|
||||
fileHandle.createWritable().then((writableHandle) => {
|
||||
writableHandle.write(pageHtml);
|
||||
writableHandle.close();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
// Log the error if the error has nothing to do with the user aborting the download
|
||||
if (e.name !== 'AbortError') {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -16,11 +16,13 @@ import {
|
||||
updateHighlightSetting,
|
||||
updateSettings,
|
||||
} from '../settings/actions';
|
||||
import { blacklisted_tags } from '../settings/constants';
|
||||
import { selectSettings } from '../settings/selectors';
|
||||
import {
|
||||
addChatPage,
|
||||
changeChatPage,
|
||||
changeScrollTracking,
|
||||
getChatData,
|
||||
loadChat,
|
||||
moveChatPageLeft,
|
||||
moveChatPageRight,
|
||||
@@ -34,29 +36,31 @@ import {
|
||||
import { createMessage, serializeMessage } from './model';
|
||||
import { chatRenderer } from './renderer';
|
||||
import { selectChat, selectCurrentChatPage } from './selectors';
|
||||
import { message } from './types';
|
||||
import type { message } from './types';
|
||||
|
||||
// List of blacklisted tags
|
||||
const blacklisted_tags = ['a', 'iframe', 'link', 'video'];
|
||||
let storedRounds: number[] = [];
|
||||
let storedLines: number[] = [];
|
||||
|
||||
const saveChatToStorage = async (store: Store<number, Action<string>>) => {
|
||||
const game = selectGame(store.getState());
|
||||
const state = selectChat(store.getState());
|
||||
const settings = selectSettings(store.getState());
|
||||
const fromIndex = Math.max(
|
||||
0,
|
||||
chatRenderer.messages.length - settings.persistentMessageLimit,
|
||||
);
|
||||
const messages = chatRenderer.messages
|
||||
.slice(fromIndex)
|
||||
.map((message) => serializeMessage(message));
|
||||
storage.set('chat-state', state);
|
||||
storage.set('chat-messages', messages);
|
||||
storage.set(
|
||||
'chat-messages-archive',
|
||||
chatRenderer.archivedMessages.map((message) => serializeMessage(message)),
|
||||
); // FIXME: Better chat history
|
||||
if (!game.databaseBackendEnabled) {
|
||||
const fromIndex = Math.max(
|
||||
0,
|
||||
chatRenderer.messages.length - settings.persistentMessageLimit,
|
||||
);
|
||||
const messages = chatRenderer.messages
|
||||
.slice(fromIndex)
|
||||
.map((message) => serializeMessage(message));
|
||||
storage.set('chat-messages', messages);
|
||||
storage.set(
|
||||
'chat-messages-archive',
|
||||
chatRenderer.archivedMessages.map((message) => serializeMessage(message)),
|
||||
);
|
||||
} // FIXME: Better chat history
|
||||
};
|
||||
|
||||
const loadChatFromStorage = async (store: Store<number, Action<string>>) => {
|
||||
@@ -136,6 +140,83 @@ const loadChatFromStorage = async (store: Store<number, Action<string>>) => {
|
||||
store.dispatch(loadChat(state));
|
||||
};
|
||||
|
||||
const loadChatFromDBStorage = async (
|
||||
store: Store<number, Action<string>>,
|
||||
user_payload: { ckey: string; token: string },
|
||||
) => {
|
||||
const game = selectGame(store.getState());
|
||||
const settings = selectSettings(store.getState());
|
||||
const [state] = await Promise.all([storage.get('chat-state')]);
|
||||
// Discard incompatible versions
|
||||
if (state && state.version <= 4) {
|
||||
store.dispatch(loadChat());
|
||||
return;
|
||||
}
|
||||
|
||||
const messages: message[] = []; // FIX ME, load from DB, first load has errors => check console
|
||||
|
||||
// Thanks for inventing async/await
|
||||
await new Promise<void>((resolve) => {
|
||||
fetch(
|
||||
`${game.chatlogApiEndpoint}/api/logs/${user_payload.ckey}/${settings.persistentMessageLimit}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${user_payload.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
json.forEach(
|
||||
(obj: {
|
||||
msg_type: string | null;
|
||||
text_raw: string;
|
||||
created_at: number;
|
||||
round_id: number;
|
||||
}) => {
|
||||
const msg: message = {
|
||||
type: obj.msg_type ? obj.msg_type : '',
|
||||
html: obj.text_raw,
|
||||
createdAt: obj.created_at,
|
||||
roundId: obj.round_id,
|
||||
};
|
||||
|
||||
messages.push(msg);
|
||||
},
|
||||
);
|
||||
|
||||
if (messages) {
|
||||
for (let message of messages) {
|
||||
if (message.html) {
|
||||
message.html = DOMPurify.sanitize(message.html, {
|
||||
FORBID_TAGS: blacklisted_tags,
|
||||
});
|
||||
}
|
||||
}
|
||||
const batch = [
|
||||
...messages,
|
||||
createMessage({
|
||||
type: 'internal/reconnected',
|
||||
}),
|
||||
];
|
||||
chatRenderer.processBatch(batch, {
|
||||
prepend: true,
|
||||
});
|
||||
}
|
||||
|
||||
store.dispatch(loadChat(state));
|
||||
resolve();
|
||||
})
|
||||
.catch(() => {
|
||||
store.dispatch(loadChat(state));
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const chatMiddleware = (store) => {
|
||||
let initialized = false;
|
||||
let loaded = false;
|
||||
@@ -170,6 +251,7 @@ export const chatMiddleware = (store) => {
|
||||
settings.hideImportantInAdminTab,
|
||||
settings.interleave,
|
||||
settings.interleaveColor,
|
||||
game.databaseBackendEnabled,
|
||||
);
|
||||
// Load the chat once settings are loaded
|
||||
if (!initialized && (settings.initialized || settings.firstLoad)) {
|
||||
@@ -177,7 +259,7 @@ export const chatMiddleware = (store) => {
|
||||
setInterval(() => {
|
||||
saveChatToStorage(store);
|
||||
}, settings.saveInterval * 1000);
|
||||
loadChatFromStorage(store);
|
||||
// loadChatFromStorage(store);
|
||||
}
|
||||
if (type === 'chat/message') {
|
||||
let payload_obj;
|
||||
@@ -280,6 +362,8 @@ export const chatMiddleware = (store) => {
|
||||
settings.logLineCount,
|
||||
storedLines[storedLines.length - settings.exportEnd],
|
||||
storedLines[storedLines.length - settings.exportStart],
|
||||
settings.exportStart,
|
||||
settings.exportEnd,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -293,6 +377,19 @@ export const chatMiddleware = (store) => {
|
||||
settings.exportEnd = 0;
|
||||
return;
|
||||
}
|
||||
if (type === 'exportDownloadReady') {
|
||||
const event = new Event('chatexportplaced');
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
if (type === getChatData.type) {
|
||||
const user_payload = payload;
|
||||
if (payload.token) {
|
||||
loadChatFromDBStorage(store, user_payload);
|
||||
} else {
|
||||
loadChatFromStorage(store);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return next(action);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { createUuid } from 'tgui-core/uuid';
|
||||
|
||||
import { MESSAGE_TYPE_INTERNAL, MESSAGE_TYPES } from './constants';
|
||||
import { message, Page } from './types';
|
||||
import type { message, Page } from './types';
|
||||
|
||||
export const canPageAcceptType = (page: Page, type: string): string | boolean =>
|
||||
type.startsWith(MESSAGE_TYPE_INTERNAL) || page.acceptedTypes[type];
|
||||
@@ -41,8 +41,10 @@ export const adminPageOnly = (page: Page): boolean => {
|
||||
return checked > 0 && adminTab;
|
||||
};
|
||||
|
||||
export const canStoreType = (storedTypes: Object, type: string) =>
|
||||
storedTypes[type];
|
||||
export const canStoreType = (
|
||||
storedTypes: Record<string, string>,
|
||||
type: string,
|
||||
) => storedTypes[type];
|
||||
|
||||
export const createPage = (obj?: Object): Page => {
|
||||
let acceptedTypes = {};
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Tooltip } from 'tgui-core/components';
|
||||
import { EventEmitter } from 'tgui-core/events';
|
||||
import { classes } from 'tgui-core/react';
|
||||
|
||||
import { exportToDisk } from './chatExport';
|
||||
import {
|
||||
IMAGE_RETRY_DELAY,
|
||||
IMAGE_RETRY_LIMIT,
|
||||
@@ -29,7 +30,7 @@ import {
|
||||
typeIsImportant,
|
||||
} from './model';
|
||||
import { highlightNode, linkifyNode } from './replaceInTextNode';
|
||||
import type { message } from './types';
|
||||
import type { message, Page } from './types';
|
||||
|
||||
const logger = createLogger('chatRenderer');
|
||||
|
||||
@@ -72,7 +73,7 @@ const createHighlightNode = (text: string, color: string): HTMLElement => {
|
||||
return node;
|
||||
};
|
||||
|
||||
const createMessageNode = (): HTMLElement => {
|
||||
export const createMessageNode = (): HTMLElement => {
|
||||
const node = document.createElement('div');
|
||||
node.className = 'ChatMessage';
|
||||
return node;
|
||||
@@ -123,18 +124,19 @@ const handleImageError = (e: ErrorEvent) => {
|
||||
setTimeout(() => {
|
||||
/** @type {HTMLImageElement} */
|
||||
const node = e.target as HTMLImageElement;
|
||||
if (node) {
|
||||
const attempts =
|
||||
parseInt(node.getAttribute('data-reload-n') || '', 10) || 0;
|
||||
if (attempts >= IMAGE_RETRY_LIMIT) {
|
||||
logger.error(`failed to load an image after ${attempts} attempts`);
|
||||
return;
|
||||
}
|
||||
const src = node.src;
|
||||
node.src = '';
|
||||
node.src = src + '#' + attempts;
|
||||
node.setAttribute('data-reload-n', (attempts + 1).toString());
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const attempts =
|
||||
parseInt(node.getAttribute('data-reload-n') || '', 10) || 0;
|
||||
if (attempts >= IMAGE_RETRY_LIMIT) {
|
||||
// logger.error(`failed to load an image after ${attempts} attempts`);
|
||||
return;
|
||||
}
|
||||
const src = node.src;
|
||||
node.src = '';
|
||||
node.src = src + '#' + attempts;
|
||||
node.setAttribute('data-reload-n', (attempts + 1).toString());
|
||||
}, IMAGE_RETRY_DELAY);
|
||||
};
|
||||
|
||||
@@ -162,11 +164,11 @@ const updateMessageBadge = (message: message) => {
|
||||
class ChatRenderer {
|
||||
loaded: boolean;
|
||||
rootNode: HTMLElement | null;
|
||||
queue: string[];
|
||||
queue: message[];
|
||||
messages: message[];
|
||||
archivedMessages: message[];
|
||||
visibleMessages: message[];
|
||||
page: null;
|
||||
page: Page | null;
|
||||
events: EventEmitter;
|
||||
prependTimestamps: boolean;
|
||||
visibleMessageLimit: number;
|
||||
@@ -176,7 +178,7 @@ class ChatRenderer {
|
||||
logLimit: number;
|
||||
logEnable: boolean;
|
||||
roundId: null | number;
|
||||
storedTypes: {};
|
||||
storedTypes: Record<string, string>;
|
||||
interleave: boolean;
|
||||
interleaveEnabled: boolean;
|
||||
interleaveColor: string;
|
||||
@@ -195,6 +197,7 @@ class ChatRenderer {
|
||||
blacklistregex: RegExp;
|
||||
}[]
|
||||
| null;
|
||||
databaseBackendEnabled: boolean;
|
||||
constructor() {
|
||||
/** @type {HTMLElement} */
|
||||
this.loaded = false;
|
||||
@@ -219,6 +222,7 @@ class ChatRenderer {
|
||||
this.interleaveEnabled = false;
|
||||
this.interleaveColor = '#909090';
|
||||
this.hideImportantInAdminTab = false;
|
||||
this.databaseBackendEnabled = false;
|
||||
// Scroll handler
|
||||
/** @type {HTMLElement} */
|
||||
this.scrollNode = null;
|
||||
@@ -448,12 +452,13 @@ class ChatRenderer {
|
||||
combineIntervalLimit: number,
|
||||
logEnable: boolean,
|
||||
logLimit: number,
|
||||
storedTypes: {},
|
||||
storedTypes: Record<string, string>,
|
||||
roundId: number | null,
|
||||
prependTimestamps: boolean,
|
||||
hideImportantInAdminTab: boolean,
|
||||
interleaveEnabled: boolean,
|
||||
interleaveColor: string,
|
||||
databaseBackendEnabled: boolean,
|
||||
) {
|
||||
this.visibleMessageLimit = visibleMessageLimit;
|
||||
this.combineMessageLimit = combineMessageLimit;
|
||||
@@ -466,9 +471,10 @@ class ChatRenderer {
|
||||
this.hideImportantInAdminTab = hideImportantInAdminTab;
|
||||
this.interleaveEnabled = interleaveEnabled;
|
||||
this.interleaveColor = interleaveColor;
|
||||
this.databaseBackendEnabled = databaseBackendEnabled;
|
||||
}
|
||||
|
||||
changePage(page) {
|
||||
changePage(page: Page) {
|
||||
if (!this.isReady()) {
|
||||
this.page = page;
|
||||
this.tryFlushQueue();
|
||||
@@ -509,7 +515,7 @@ class ChatRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
getCombinableMessage(predicate) {
|
||||
getCombinableMessage(predicate: message) {
|
||||
const now = Date.now();
|
||||
const len = this.visibleMessages.length;
|
||||
const from = len - 1;
|
||||
@@ -532,7 +538,7 @@ class ChatRenderer {
|
||||
}
|
||||
|
||||
processBatch(
|
||||
batch,
|
||||
batch: message[],
|
||||
options: {
|
||||
prepend?: boolean;
|
||||
notifyListeners?: boolean;
|
||||
@@ -692,6 +698,7 @@ class ChatRenderer {
|
||||
if (
|
||||
doArchive &&
|
||||
this.logEnable &&
|
||||
!this.databaseBackendEnabled &&
|
||||
this.storedTypes &&
|
||||
canStoreType(this.storedTypes, message.type)
|
||||
) {
|
||||
@@ -792,7 +799,7 @@ class ChatRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
rebuildChat(rebuildLimit) {
|
||||
rebuildChat(rebuildLimit: number) {
|
||||
if (!this.isReady()) {
|
||||
return;
|
||||
}
|
||||
@@ -815,7 +822,13 @@ class ChatRenderer {
|
||||
});
|
||||
}
|
||||
|
||||
saveToDisk(logLineCount, startLine = 0, endLine = 0) {
|
||||
saveToDisk(
|
||||
logLineCount: number = 0,
|
||||
startLine: number = 0,
|
||||
endLine: number = 0,
|
||||
startRound: number = 0,
|
||||
endRound: number = 0,
|
||||
) {
|
||||
// Compile currently loaded stylesheets as CSS text
|
||||
let cssText = '';
|
||||
const styleSheets = document.styleSheets;
|
||||
@@ -830,59 +843,70 @@ class ChatRenderer {
|
||||
}
|
||||
cssText += 'body, html { background-color: #141414 }\n';
|
||||
// Compile chat log as HTML text
|
||||
let messagesHtml = '';
|
||||
|
||||
let tmpMsgArray: message[] = [];
|
||||
if (startLine || endLine) {
|
||||
if (!endLine) {
|
||||
tmpMsgArray = this.archivedMessages.slice(startLine);
|
||||
} else {
|
||||
tmpMsgArray = this.archivedMessages.slice(startLine, endLine);
|
||||
}
|
||||
if (logLineCount > 0) {
|
||||
tmpMsgArray = tmpMsgArray.slice(-logLineCount);
|
||||
}
|
||||
} else if (logLineCount > 0) {
|
||||
tmpMsgArray = this.archivedMessages.slice(-logLineCount);
|
||||
if (this.databaseBackendEnabled) {
|
||||
exportToDisk(
|
||||
cssText,
|
||||
startRound,
|
||||
endRound,
|
||||
this.prependTimestamps,
|
||||
this.page,
|
||||
);
|
||||
} else {
|
||||
tmpMsgArray = this.archivedMessages;
|
||||
}
|
||||
|
||||
// for (let message of this.visibleMessages) { // TODO: Actually having a better message archiving maybe for exports?
|
||||
for (let message of tmpMsgArray) {
|
||||
// Filter messages according to active tab for export
|
||||
if (this.page && canPageAcceptType(this.page, message.type)) {
|
||||
messagesHtml += message.html + '\n';
|
||||
// Fetch from chat storage
|
||||
let messagesHtml = '';
|
||||
let tmpMsgArray: message[] = [];
|
||||
if (startLine || endLine) {
|
||||
if (!endLine) {
|
||||
tmpMsgArray = this.archivedMessages.slice(startLine);
|
||||
} else {
|
||||
tmpMsgArray = this.archivedMessages.slice(startLine, endLine);
|
||||
}
|
||||
if (logLineCount > 0) {
|
||||
tmpMsgArray = tmpMsgArray.slice(-logLineCount);
|
||||
}
|
||||
} else if (logLineCount > 0) {
|
||||
tmpMsgArray = this.archivedMessages.slice(-logLineCount);
|
||||
} else {
|
||||
tmpMsgArray = this.archivedMessages;
|
||||
}
|
||||
// if (message.node) {
|
||||
// messagesHtml += message.node.outerHTML + '\n';
|
||||
// }
|
||||
}
|
||||
// Create a page
|
||||
|
||||
const pageHtml =
|
||||
'<!doctype html>\n' +
|
||||
'<html>\n' +
|
||||
'<head>\n' +
|
||||
'<title>SS13 Chat Log</title>\n' +
|
||||
'<style>\n' +
|
||||
cssText +
|
||||
'</style>\n' +
|
||||
'</head>\n' +
|
||||
'<body>\n' +
|
||||
'<div class="Chat">\n' +
|
||||
messagesHtml +
|
||||
'</div>\n' +
|
||||
'</body>\n' +
|
||||
'</html>\n';
|
||||
// Create and send a nice blob
|
||||
const blob = new Blob([pageHtml], { type: 'text/plain' });
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.substring(0, 19)
|
||||
.replace(/[-:]/g, '')
|
||||
.replace('T', '-');
|
||||
Byond.saveBlob(blob, `ss13-chatlog-${timestamp}.html`, '.html');
|
||||
// for (let message of this.visibleMessages) { // TODO: Actually having a better message archiving maybe for exports?
|
||||
for (let message of tmpMsgArray) {
|
||||
// Filter messages according to active tab for export
|
||||
if (this.page && canPageAcceptType(this.page, message.type)) {
|
||||
messagesHtml += message.html + '\n';
|
||||
}
|
||||
// if (message.node) {
|
||||
// messagesHtml += message.node.outerHTML + '\n';
|
||||
// }
|
||||
}
|
||||
|
||||
// Create a page
|
||||
const pageHtml =
|
||||
'<!doctype html>\n' +
|
||||
'<html>\n' +
|
||||
'<head>\n' +
|
||||
'<title>SS13 Chat Log</title>\n' +
|
||||
'<style>\n' +
|
||||
cssText +
|
||||
'</style>\n' +
|
||||
'</head>\n' +
|
||||
'<body>\n' +
|
||||
'<div class="Chat">\n' +
|
||||
messagesHtml +
|
||||
'</div>\n' +
|
||||
'</body>\n' +
|
||||
'</html>\n';
|
||||
// Create and send a nice blob
|
||||
const blob = new Blob([pageHtml], { type: 'text/plain' });
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.substring(0, 19)
|
||||
.replace(/[-:]/g, '')
|
||||
.replace('T', '-');
|
||||
Byond.saveBlob(blob, `ss13-chatlog-${timestamp}.html`, '.html');
|
||||
}
|
||||
}
|
||||
|
||||
purgeMessageArchive() {
|
||||
|
||||
@@ -10,3 +10,4 @@ export const roundRestarted = createAction('roundrestart');
|
||||
export const connectionLost = createAction('game/connectionLost');
|
||||
export const connectionRestored = createAction('game/connectionRestored');
|
||||
export const dismissWarning = createAction('game/dismissWarning');
|
||||
export const updateExportData = createAction('game/updateExportData');
|
||||
|
||||
@@ -4,16 +4,21 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { connectionLost } from './actions';
|
||||
import { connectionLost, updateExportData } from './actions';
|
||||
import { connectionRestored, dismissWarning } from './actions';
|
||||
import type { gameState } from './types';
|
||||
|
||||
const initialState = {
|
||||
const initialState: gameState = {
|
||||
// TODO: This is where round info should be.
|
||||
roundId: null,
|
||||
roundTime: null,
|
||||
roundRestartedAt: null,
|
||||
connectionLostAt: null,
|
||||
dismissedConnectionWarning: false,
|
||||
databaseBackendEnabled: false,
|
||||
chatlogApiEndpoint: '',
|
||||
databaseStoredRounds: [],
|
||||
userData: { ckey: '', token: '' },
|
||||
};
|
||||
|
||||
export const gameReducer = (state = initialState, action) => {
|
||||
@@ -29,6 +34,9 @@ export const gameReducer = (state = initialState, action) => {
|
||||
return {
|
||||
...state,
|
||||
roundId: payload.round_id,
|
||||
databaseBackendEnabled: payload.chatlog_db_backend,
|
||||
chatlogApiEndpoint: payload.chatlog_api_endpoint,
|
||||
databaseStoredRounds: payload.chatlog_stored_rounds,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -51,5 +59,11 @@ export const gameReducer = (state = initialState, action) => {
|
||||
dismissedConnectionWarning: true,
|
||||
};
|
||||
}
|
||||
if (type === updateExportData.type) {
|
||||
return {
|
||||
...state,
|
||||
userData: payload,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
11
tgui/packages/tgui-panel/game/types.ts
Normal file
11
tgui/packages/tgui-panel/game/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type gameState = {
|
||||
roundId: number | null;
|
||||
roundTime: null | number;
|
||||
roundRestartedAt: null | number;
|
||||
connectionLostAt: null | number;
|
||||
dismissedConnectionWarning: boolean;
|
||||
databaseBackendEnabled: boolean;
|
||||
chatlogApiEndpoint: string;
|
||||
databaseStoredRounds: string[];
|
||||
userData: { ckey: string; token: string };
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Button,
|
||||
Collapsible,
|
||||
Divider,
|
||||
Dropdown,
|
||||
LabeledList,
|
||||
NumberInput,
|
||||
Section,
|
||||
@@ -32,63 +33,56 @@ export const ExportTab = (props) => {
|
||||
totalStoredMessages,
|
||||
storedTypes,
|
||||
} = useSelector(selectSettings);
|
||||
const [purgeConfirm, setPurgeConfirm] = useState(0);
|
||||
const [logConfirm, setLogConfirm] = useState(false);
|
||||
const [purgeButtonText, setPurgeButtonText] = useState(
|
||||
'Purge message archive',
|
||||
);
|
||||
return (
|
||||
<Section>
|
||||
<Stack align="baseline">
|
||||
{logEnable ? (
|
||||
logConfirm ? (
|
||||
<Button
|
||||
{!game.databaseBackendEnabled &&
|
||||
(logEnable ? (
|
||||
<Button.Confirm
|
||||
icon="ban"
|
||||
color="red"
|
||||
confirmIcon="ban"
|
||||
confirmColor="red"
|
||||
confirmContent="Disable?"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
updateSettings({
|
||||
logEnable: false,
|
||||
}),
|
||||
);
|
||||
setLogConfirm(false);
|
||||
}}
|
||||
>
|
||||
Disable?
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
icon="ban"
|
||||
color="red"
|
||||
onClick={() => {
|
||||
setLogConfirm(true);
|
||||
setTimeout(() => {
|
||||
setLogConfirm(false);
|
||||
}, 5000);
|
||||
}}
|
||||
>
|
||||
Disable logging
|
||||
</Button.Confirm>
|
||||
) : (
|
||||
<Button
|
||||
icon="download"
|
||||
color="green"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
updateSettings({
|
||||
logEnable: true,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
Enable logging
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
<Button
|
||||
icon="download"
|
||||
color="green"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
updateSettings({
|
||||
logEnable: true,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
Enable logging
|
||||
</Button>
|
||||
)}
|
||||
))}
|
||||
<Stack.Item grow />
|
||||
<Stack.Item color="label">Round ID: </Stack.Item>
|
||||
<Stack.Item color={game.roundId ? '' : 'red'}>
|
||||
{game.roundId ? game.roundId : 'ERROR'}
|
||||
</Stack.Item>
|
||||
<Stack.Item color="label">DB Chatlogging: </Stack.Item>
|
||||
<Stack.Item color={game.databaseBackendEnabled ? 'green' : 'red'}>
|
||||
{game.databaseBackendEnabled ? 'Enabled' : 'Disabled'}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{logEnable ? (
|
||||
{logEnable && !game.databaseBackendEnabled && (
|
||||
<>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Amount of rounds to log (1 to 8)">
|
||||
@@ -109,12 +103,10 @@ export const ExportTab = (props) => {
|
||||
}
|
||||
/>
|
||||
|
||||
{logRetainRounds > 3 ? (
|
||||
{logRetainRounds > 3 && (
|
||||
<Box inline fontSize="0.9em" color="red">
|
||||
Warning, might crash!
|
||||
</Box>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Hardlimit for the log archive (0 = inf. to 50000)">
|
||||
@@ -135,7 +127,7 @@ export const ExportTab = (props) => {
|
||||
}
|
||||
/>
|
||||
|
||||
{logLimit > 0 ? (
|
||||
{logLimit > 0 && (
|
||||
<Box
|
||||
inline
|
||||
fontSize="0.9em"
|
||||
@@ -145,8 +137,6 @@ export const ExportTab = (props) => {
|
||||
? 'Warning, might crash! Takes priority above round retention.'
|
||||
: 'Takes priority above round retention.'}
|
||||
</Box>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
@@ -170,48 +160,92 @@ export const ExportTab = (props) => {
|
||||
</Collapsible>
|
||||
</Section>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Export round start (0 = curr.) / end (0 = dis.)">
|
||||
<NumberInput
|
||||
width="5em"
|
||||
step={1}
|
||||
stepPixelSize={10}
|
||||
minValue={0}
|
||||
maxValue={exportEnd === 0 ? 0 : exportEnd - 1}
|
||||
value={exportStart}
|
||||
format={(value) => toFixed(value)}
|
||||
onDrag={(value) =>
|
||||
dispatch(
|
||||
updateSettings({
|
||||
exportStart: value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<NumberInput
|
||||
width="5em"
|
||||
step={1}
|
||||
stepPixelSize={10}
|
||||
minValue={exportStart === 0 ? 0 : exportStart + 1}
|
||||
maxValue={storedRounds}
|
||||
value={exportEnd}
|
||||
format={(value) => toFixed(value)}
|
||||
onDrag={(value) =>
|
||||
dispatch(
|
||||
updateSettings({
|
||||
exportEnd: value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<Box inline fontSize="0.9em" color="label">
|
||||
Stored Rounds:
|
||||
</Box>
|
||||
<Box inline>{storedRounds}</Box>
|
||||
<Stack align="center">
|
||||
{game.databaseBackendEnabled ? (
|
||||
<>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
onSelected={(value) =>
|
||||
dispatch(
|
||||
updateSettings({
|
||||
exportStart: value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
options={game.databaseStoredRounds}
|
||||
selected={exportStart}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
onSelected={(value) =>
|
||||
dispatch(
|
||||
updateSettings({
|
||||
exportEnd: value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
options={game.databaseStoredRounds}
|
||||
selected={exportEnd}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Stack.Item>
|
||||
<NumberInput
|
||||
width="5em"
|
||||
step={1}
|
||||
stepPixelSize={10}
|
||||
minValue={0}
|
||||
maxValue={exportEnd === 0 ? 0 : exportEnd - 1}
|
||||
value={exportStart}
|
||||
format={(value) => toFixed(value)}
|
||||
onDrag={(value) =>
|
||||
dispatch(
|
||||
updateSettings({
|
||||
exportStart: value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<NumberInput
|
||||
width="5em"
|
||||
step={1}
|
||||
stepPixelSize={10}
|
||||
minValue={exportStart === 0 ? 0 : exportStart + 1}
|
||||
maxValue={storedRounds}
|
||||
value={exportEnd}
|
||||
format={(value) => toFixed(value)}
|
||||
onDrag={(value) =>
|
||||
dispatch(
|
||||
updateSettings({
|
||||
exportEnd: value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</>
|
||||
)}
|
||||
<Stack.Item>
|
||||
<Box fontSize="0.9em" color="label">
|
||||
Stored Rounds:
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box>
|
||||
{game.databaseBackendEnabled
|
||||
? game.databaseStoredRounds.length - 1
|
||||
: storedRounds}
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Amount of lines to export (0 = inf.)">
|
||||
<NumberInput
|
||||
@@ -231,38 +265,34 @@ export const ExportTab = (props) => {
|
||||
}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Totally stored messages">
|
||||
<Box>{totalStoredMessages}</Box>
|
||||
</LabeledList.Item>
|
||||
{!game.databaseBackendEnabled && (
|
||||
<LabeledList.Item label="Totally stored messages">
|
||||
<Box>{totalStoredMessages}</Box>
|
||||
</LabeledList.Item>
|
||||
)}
|
||||
</LabeledList>
|
||||
<Divider />
|
||||
<Button icon="save" onClick={() => dispatch(saveChatToDisk())}>
|
||||
Save chat log
|
||||
</Button>
|
||||
{purgeConfirm > 0 ? (
|
||||
<Button
|
||||
{!game.databaseBackendEnabled && (
|
||||
<Button.Confirm
|
||||
disabled={purgeButtonText === 'Purged!'}
|
||||
icon="trash"
|
||||
color="red"
|
||||
confirmIcon="trash"
|
||||
confirmColor="red"
|
||||
confirmContent="Are you sure?"
|
||||
onClick={() => {
|
||||
dispatch(purgeChatMessageArchive());
|
||||
setPurgeConfirm(2);
|
||||
}}
|
||||
>
|
||||
{purgeConfirm > 1 ? 'Purged!' : 'Are you sure?'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
icon="trash"
|
||||
color="red"
|
||||
onClick={() => {
|
||||
setPurgeConfirm(1);
|
||||
setPurgeButtonText('Purged!');
|
||||
setTimeout(() => {
|
||||
setPurgeConfirm(0);
|
||||
}, 5000);
|
||||
setPurgeButtonText('Purge message archive');
|
||||
}, 1000);
|
||||
}}
|
||||
>
|
||||
Purge message archive
|
||||
</Button>
|
||||
{purgeButtonText}
|
||||
</Button.Confirm>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -2,11 +2,13 @@ import { useDispatch, useSelector } from 'tgui/backend';
|
||||
import { Box, LabeledList, NumberInput, Section } from 'tgui-core/components';
|
||||
import { toFixed } from 'tgui-core/math';
|
||||
|
||||
import { useGame } from '../../game';
|
||||
import { updateSettings } from '../actions';
|
||||
import { selectSettings } from '../selectors';
|
||||
|
||||
export const MessageLimits = (props) => {
|
||||
const dispatch = useDispatch();
|
||||
const game = useGame();
|
||||
const {
|
||||
visibleMessageLimit,
|
||||
persistentMessageLimit,
|
||||
@@ -35,12 +37,10 @@ export const MessageLimits = (props) => {
|
||||
}
|
||||
/>
|
||||
|
||||
{visibleMessageLimit >= 5000 ? (
|
||||
{visibleMessageLimit >= 5000 && (
|
||||
<Box inline fontSize="0.9em" color="red">
|
||||
Impacts performance!
|
||||
</Box>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Amount of visually persistent lines 0-10000 (Default: 1000)">
|
||||
@@ -61,12 +61,10 @@ export const MessageLimits = (props) => {
|
||||
}
|
||||
/>
|
||||
|
||||
{persistentMessageLimit >= 2500 ? (
|
||||
{persistentMessageLimit >= 2500 && (
|
||||
<Box inline fontSize="0.9em" color="red">
|
||||
Delays initialization!
|
||||
</Box>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Amount of different lines in-between to combine 0-10 (Default: 5)">
|
||||
@@ -106,33 +104,33 @@ export const MessageLimits = (props) => {
|
||||
}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Message store interval 1-10 (Default: 10 Seconds) [Requires restart]">
|
||||
<NumberInput
|
||||
width="5em"
|
||||
step={1}
|
||||
stepPixelSize={5}
|
||||
minValue={1}
|
||||
maxValue={10}
|
||||
value={saveInterval}
|
||||
unit="s"
|
||||
format={(value) => toFixed(value)}
|
||||
onDrag={(value) =>
|
||||
dispatch(
|
||||
updateSettings({
|
||||
saveInterval: value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{saveInterval <= 3 ? (
|
||||
<Box inline fontSize="0.9em" color="red">
|
||||
Warning, experimental! Might crash!
|
||||
</Box>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
{!game.databaseBackendEnabled && (
|
||||
<LabeledList.Item label="Message store interval 1-10 (Default: 10 Seconds) [Requires restart]">
|
||||
<NumberInput
|
||||
width="5em"
|
||||
step={1}
|
||||
stepPixelSize={5}
|
||||
minValue={1}
|
||||
maxValue={10}
|
||||
value={saveInterval}
|
||||
unit="s"
|
||||
format={(value) => toFixed(value)}
|
||||
onDrag={(value) =>
|
||||
dispatch(
|
||||
updateSettings({
|
||||
saveInterval: value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{saveInterval <= 3 && (
|
||||
<Box inline fontSize="0.9em" color="red">
|
||||
Warning, experimental! Might crash!
|
||||
</Box>
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
)}
|
||||
</LabeledList>
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -193,7 +193,7 @@ const TextHighlightSetting = (props) => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
{highlightBlacklist ? (
|
||||
{!!highlightBlacklist && (
|
||||
<TextArea
|
||||
height="3em"
|
||||
value={blacklistText}
|
||||
@@ -207,8 +207,6 @@ const TextHighlightSetting = (props) => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Stack.Item>
|
||||
);
|
||||
|
||||
@@ -52,3 +52,5 @@ export const FONTS = [
|
||||
];
|
||||
|
||||
export const MAX_HIGHLIGHT_SETTINGS = 10;
|
||||
|
||||
export const blacklisted_tags = ['a', 'iframe', 'link', 'video'];
|
||||
|
||||
@@ -7,21 +7,31 @@
|
||||
import { storage } from 'common/storage';
|
||||
import { createLogger } from 'tgui/logging';
|
||||
|
||||
import { getChatData } from './chat/actions';
|
||||
import { updateExportData } from './game/actions';
|
||||
|
||||
const logger = createLogger('telemetry');
|
||||
|
||||
const MAX_CONNECTIONS_STORED = 10;
|
||||
|
||||
type Client = { ckey: string; address: string; computer_id: string };
|
||||
type Client = {
|
||||
ckey: string;
|
||||
chatlog_token: string;
|
||||
address: string;
|
||||
computer_id: string;
|
||||
};
|
||||
type Telemetry = { limits: { connections: number }[]; connections: Client[] };
|
||||
|
||||
const connectionsMatch = (a: Client, b: Client) =>
|
||||
a.ckey === b.ckey &&
|
||||
a.chatlog_token === b.chatlog_token &&
|
||||
a.address === b.address &&
|
||||
a.computer_id === b.computer_id;
|
||||
|
||||
export const telemetryMiddleware = (store) => {
|
||||
let telemetry: Telemetry;
|
||||
let wasRequestedWithPayload: Telemetry | null;
|
||||
let firstMutate: boolean = true;
|
||||
return (next) => (action) => {
|
||||
const { type, payload } = action;
|
||||
// Handle telemetry requests
|
||||
@@ -78,6 +88,18 @@ export const telemetryMiddleware = (store) => {
|
||||
telemetry.connections.pop();
|
||||
}
|
||||
}
|
||||
if (firstMutate) {
|
||||
firstMutate = false;
|
||||
store.dispatch(
|
||||
getChatData({ ckey: client.ckey, token: client.chatlog_token }),
|
||||
);
|
||||
store.dispatch(
|
||||
updateExportData({
|
||||
ckey: client.ckey,
|
||||
token: client.chatlog_token,
|
||||
}),
|
||||
);
|
||||
}
|
||||
// Save telemetry
|
||||
if (telemetryMutated) {
|
||||
logger.debug('saving telemetry to storage', telemetry);
|
||||
|
||||
@@ -369,10 +369,10 @@
|
||||
fileHandle.createWritable().then(function (writeableFileHandle) {
|
||||
writeableFileHandle.write(blob).then(function () {
|
||||
writeableFileHandle.close()
|
||||
}).catch(function () { });
|
||||
}).catch(function () { });
|
||||
}).catch(function () { });
|
||||
} catch (e) { }
|
||||
}).catch(function (e) { console.error(e) });
|
||||
}).catch(function (e) { console.error(e) });
|
||||
}).catch(function (e) { console.error(e) });
|
||||
} catch (e) { console.error(e) }
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3725,6 +3725,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/wicg-file-system-access@npm:^2023.10.5":
|
||||
version: 2023.10.5
|
||||
resolution: "@types/wicg-file-system-access@npm:2023.10.5"
|
||||
checksum: 10c0/3c6099320a2517ab405bd65712eb19ba2c1f7f0c30f952744102b76809ed9ad5205f881c79ebe6f92ac7af7a13667df8c23dd59b0a5182119c74afa17335bfd3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ws@npm:^8.5.13":
|
||||
version: 8.5.14
|
||||
resolution: "@types/ws@npm:8.5.14"
|
||||
@@ -19180,6 +19187,7 @@ __metadata:
|
||||
"@types/react": "npm:^18.3.12"
|
||||
"@types/react-dom": "npm:^18.3.1"
|
||||
"@types/webpack-env": "npm:^1.18.5"
|
||||
"@types/wicg-file-system-access": "npm:^2023.10.5"
|
||||
"@typescript-eslint/parser": "npm:^8.13.0"
|
||||
"@typescript-eslint/utils": "npm:^8.13.0"
|
||||
css-loader: "npm:^7.1.2"
|
||||
|
||||
7
tools/alwayson-backend/.env.example
Normal file
7
tools/alwayson-backend/.env.example
Normal file
@@ -0,0 +1,7 @@
|
||||
# PLEASE REFER TO THE SCHEMA IN THE MAIN REPOSITORY
|
||||
# TO KNOW WHERE THE TABLES ARE AT (chatlogs / ckeys)
|
||||
|
||||
API_HOST=0.0.0.0
|
||||
API_PORT=50000
|
||||
|
||||
DBCONFIG_PATH=/opt/myserver/config/dbconfig.txt
|
||||
12
tools/alwayson-backend/README
Normal file
12
tools/alwayson-backend/README
Normal file
@@ -0,0 +1,12 @@
|
||||
The libvchatlog.so/vchatlog.dll file will fetch the required database credentials automatically from the
|
||||
config/dbconfig.txt if the chatlogging database backend was enabled.
|
||||
|
||||
For the alwayson-backend you will have to create some kind of service to keep it running. With the supplied
|
||||
.env.example you are able to set the port of the API. The configured chatlogging API URL (where you also turn it on)
|
||||
you will have to supply the correct ip-address/domain and port on where this API is reachable.
|
||||
|
||||
For this you can set the DBCONFIG_PATH environment variable (or use the .env.example) to the exact path of the config/dbconfig.txt.
|
||||
|
||||
|
||||
|
||||
Do not forget to rename .env.example to .env of course, if you want to use a file.
|
||||
BIN
tools/alwayson-backend/alwayson-backend
Executable file
BIN
tools/alwayson-backend/alwayson-backend
Executable file
Binary file not shown.
BIN
tools/alwayson-backend/alwayson-backend.exe
Normal file
BIN
tools/alwayson-backend/alwayson-backend.exe
Normal file
Binary file not shown.
BIN
vchatlog.dll
Normal file
BIN
vchatlog.dll
Normal file
Binary file not shown.
@@ -170,6 +170,7 @@
|
||||
#include "code\__defines\typeids.dm"
|
||||
#include "code\__defines\unit_tests.dm"
|
||||
#include "code\__defines\update_icons.dm"
|
||||
#include "code\__defines\vchatlog.dm"
|
||||
#include "code\__defines\verb_manager.dm"
|
||||
#include "code\__defines\visualnet.dm"
|
||||
#include "code\__defines\vore.dm"
|
||||
|
||||
Reference in New Issue
Block a user