182 lines
8.2 KiB
TypeScript
182 lines
8.2 KiB
TypeScript
|
import {getByteLength, Message as MessageImpl} from '../chat/common';
|
||
|
import core from '../chat/core';
|
||
|
import {Conversation, Logs as Logging, Settings} from '../chat/interfaces';
|
||
|
|
||
|
declare global {
|
||
|
const NativeFile: {
|
||
|
readFile(name: string): Promise<string | undefined>
|
||
|
readFile(name: string, start: number, length: number): Promise<string | undefined>
|
||
|
writeFile(name: string, data: string): Promise<void>
|
||
|
listDirectories(name: string): Promise<string>
|
||
|
listFiles(name: string): Promise<string>
|
||
|
getSize(name: string): Promise<number>
|
||
|
append(name: string, data: string): Promise<void>
|
||
|
ensureDirectory(name: string): Promise<void>
|
||
|
};
|
||
|
}
|
||
|
|
||
|
const dayMs = 86400000;
|
||
|
|
||
|
export class GeneralSettings {
|
||
|
account = '';
|
||
|
password = '';
|
||
|
host = 'wss://chat.f-list.net:9799';
|
||
|
theme = 'default';
|
||
|
}
|
||
|
|
||
|
type Index = {[key: string]: {name: string, index: {[key: number]: number | undefined}} | undefined};
|
||
|
|
||
|
function serializeMessage(message: Conversation.Message): string {
|
||
|
const time = message.time.getTime() / 1000;
|
||
|
let str = String.fromCharCode((time >> 24) % 256) + String.fromCharCode((time >> 16) % 256)
|
||
|
+ String.fromCharCode((time >> 8) % 256) + String.fromCharCode(time % 256);
|
||
|
str += String.fromCharCode(message.type);
|
||
|
if(message.type !== Conversation.Message.Type.Event) {
|
||
|
str += String.fromCharCode(message.sender.name.length);
|
||
|
str += message.sender.name;
|
||
|
} else str += '\0';
|
||
|
const textLength = message.text.length;
|
||
|
str += String.fromCharCode((textLength >> 8) % 256) + String.fromCharCode(textLength % 256);
|
||
|
str += message.text;
|
||
|
const length = getByteLength(str);
|
||
|
str += String.fromCharCode((length >> 8) % 256) + String.fromCharCode(length % 256);
|
||
|
return str;
|
||
|
}
|
||
|
|
||
|
function deserializeMessage(str: string): {message: Conversation.Message, end: number} {
|
||
|
let index = 0;
|
||
|
const time = str.charCodeAt(index++) << 24 | str.charCodeAt(index++) << 16 | str.charCodeAt(index++) << 8 | str.charCodeAt(index++);
|
||
|
const type = str.charCodeAt(index++);
|
||
|
const senderLength = str.charCodeAt(index++);
|
||
|
const sender = str.substring(index, index += senderLength);
|
||
|
const messageLength = str.charCodeAt(index++) << 8 | str.charCodeAt(index++);
|
||
|
const text = str.substring(index, index += messageLength);
|
||
|
const end = str.charCodeAt(index++) << 8 | str.charCodeAt(index);
|
||
|
return {message: new MessageImpl(type, core.characters.get(sender), text, new Date(time * 1000)), end: end + 2};
|
||
|
}
|
||
|
|
||
|
export class Logs implements Logging.Persistent {
|
||
|
private index: Index = {};
|
||
|
private logDir: string;
|
||
|
|
||
|
constructor() {
|
||
|
core.connection.onEvent('connecting', async() => {
|
||
|
this.index = {};
|
||
|
this.logDir = `${core.connection.character}/logs`;
|
||
|
await NativeFile.ensureDirectory(this.logDir);
|
||
|
const entries = <string[]>JSON.parse(await NativeFile.listFiles(this.logDir));
|
||
|
for(const entry of entries)
|
||
|
if(entry.substr(-4) === '.idx') {
|
||
|
const str = (await NativeFile.readFile(`${this.logDir}/${entry}`))!;
|
||
|
let i = str.charCodeAt(0);
|
||
|
const name = str.substr(1, i++);
|
||
|
const index: {[key: number]: number} = {};
|
||
|
while(i < str.length) {
|
||
|
const key = str.charCodeAt(i++) << 8 | str.charCodeAt(i++);
|
||
|
index[key] = str.charCodeAt(i++) << 32 | str.charCodeAt(i++) << 24 | str.charCodeAt(i++) << 16 |
|
||
|
str.charCodeAt(i++) << 8 | str.charCodeAt(i++);
|
||
|
}
|
||
|
this.index[entry.slice(0, -4).toLowerCase()] = {name, index};
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
async logMessage(conversation: Conversation, message: Conversation.Message): Promise<void> {
|
||
|
const file = `${this.logDir}/${conversation.key}`;
|
||
|
const serialized = serializeMessage(message);
|
||
|
const date = Math.floor(message.time.getTime() / dayMs);
|
||
|
let indexBuffer: string | undefined;
|
||
|
let index = this.index[conversation.key];
|
||
|
if(index !== undefined) {
|
||
|
if(index.index[date] === undefined) indexBuffer = '';
|
||
|
} else {
|
||
|
index = this.index[conversation.key] = {name: conversation.name, index: {}};
|
||
|
const nameLength = getByteLength(conversation.name);
|
||
|
indexBuffer = String.fromCharCode(nameLength) + conversation.name;
|
||
|
}
|
||
|
if(indexBuffer !== undefined) {
|
||
|
const size = await NativeFile.getSize(file);
|
||
|
index.index[date] = size;
|
||
|
indexBuffer += String.fromCharCode((date >> 8) % 256) + String.fromCharCode(date % 256) +
|
||
|
String.fromCharCode((size >> 32) % 256) + String.fromCharCode((size >> 24) % 256) +
|
||
|
String.fromCharCode((size >> 16) % 256) + String.fromCharCode((size >> 8) % 256) + String.fromCharCode(size % 256);
|
||
|
await NativeFile.append(`${file}.idx`, indexBuffer);
|
||
|
}
|
||
|
await NativeFile.append(file, serialized);
|
||
|
}
|
||
|
|
||
|
async getBacklog(conversation: Conversation): Promise<Conversation.Message[]> {
|
||
|
const file = `${this.logDir}/${conversation.key}`;
|
||
|
let count = 20;
|
||
|
let messages = new Array<Conversation.Message>(count);
|
||
|
let pos = await NativeFile.getSize(file);
|
||
|
while(pos > 0 && count > 0) {
|
||
|
const l = (await NativeFile.readFile(file, pos - 2, pos))!;
|
||
|
const length = (l.charCodeAt(0) << 8 | l.charCodeAt(1));
|
||
|
pos = pos - length - 2;
|
||
|
messages[--count] = deserializeMessage((await NativeFile.readFile(file, pos, length))!).message;
|
||
|
}
|
||
|
if(count !== 0) messages = messages.slice(count);
|
||
|
return messages;
|
||
|
}
|
||
|
|
||
|
async getLogs(key: string, date: Date): Promise<Conversation.Message[]> {
|
||
|
const file = `${this.logDir}/${key}`;
|
||
|
const messages: Conversation.Message[] = [];
|
||
|
const day = date.getTime() / dayMs;
|
||
|
const index = this.index[key];
|
||
|
if(index === undefined) return [];
|
||
|
let pos = index.index[date.getTime() / dayMs];
|
||
|
if(pos === undefined) return [];
|
||
|
const size = await NativeFile.getSize(file);
|
||
|
while(pos < size) {
|
||
|
const deserialized = deserializeMessage((await NativeFile.readFile(file, pos, 51000))!);
|
||
|
if(Math.floor(deserialized.message.time.getTime() / dayMs) !== day) break;
|
||
|
messages.push(deserialized.message);
|
||
|
pos += deserialized.end;
|
||
|
}
|
||
|
return messages;
|
||
|
}
|
||
|
|
||
|
getLogDates(key: string): ReadonlyArray<Date> {
|
||
|
const entry = this.index[key];
|
||
|
if(entry === undefined) return [];
|
||
|
const dates = [];
|
||
|
for(const date in entry.index)
|
||
|
dates.push(new Date(parseInt(date, 10) * dayMs));
|
||
|
return dates;
|
||
|
}
|
||
|
|
||
|
get conversations(): ReadonlyArray<{id: string, name: string}> {
|
||
|
const conversations: {id: string, name: string}[] = [];
|
||
|
for(const key in this.index) conversations.push({id: key, name: this.index[key]!.name});
|
||
|
conversations.sort((x, y) => (x.name < y.name ? -1 : (x.name > y.name ? 1 : 0)));
|
||
|
return conversations;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export async function getGeneralSettings(): Promise<GeneralSettings | undefined> {
|
||
|
const file = await NativeFile.readFile('!settings');
|
||
|
if(file === undefined) return undefined;
|
||
|
return <GeneralSettings>JSON.parse(file);
|
||
|
}
|
||
|
|
||
|
export async function setGeneralSettings(value: GeneralSettings): Promise<void> {
|
||
|
return NativeFile.writeFile('!settings', JSON.stringify(value));
|
||
|
}
|
||
|
|
||
|
export class SettingsStore implements Settings.Store {
|
||
|
async get<K extends keyof Settings.Keys>(key: K, character: string = core.connection.character): Promise<Settings.Keys[K] | undefined> {
|
||
|
const file = await NativeFile.readFile(`${character}/${key}`);
|
||
|
if(file === undefined) return undefined;
|
||
|
return <Settings.Keys[K]>JSON.parse(file);
|
||
|
}
|
||
|
|
||
|
async set<K extends keyof Settings.Keys>(key: K, value: Settings.Keys[K]): Promise<void> {
|
||
|
return NativeFile.writeFile(`${core.connection.character}/${key}`, JSON.stringify(value));
|
||
|
}
|
||
|
|
||
|
async getAvailableCharacters(): Promise<string[]> {
|
||
|
return <string[]>JSON.parse(await NativeFile.listDirectories('/'));
|
||
|
}
|
||
|
}
|