replaced improper references to 'server' with 'guild' in client
This commit is contained in:
parent
87a4d8584f
commit
ad346fc37d
@ -1,944 +0,0 @@
|
||||
import * as electronRemote from '@electron/remote';
|
||||
const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import * as StackTrace from '../../stack-trace/stack-trace';
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import * as socketio from 'socket.io-client';
|
||||
|
||||
import ConcurrentQueue from '../../concurrent-queue/concurrent-queue';
|
||||
|
||||
import { Message, Member, Channel, Changes, ConnectionInfo, CacheServerData, ServerMetaData, ServerConfig, ShouldNeverHappenError, Token } from './data-types';
|
||||
import DBCache from './db-cache';
|
||||
import ResourceRAMCache from './resource-ram-cache';
|
||||
import RecentMessageRAMCache from './message-ram-cache';
|
||||
|
||||
// Events:
|
||||
// 'connected' function() called when connected to the guild
|
||||
// 'disconnected' function() called when connection to the guild is lost
|
||||
// 'verified' function() called when verification handshake is completed
|
||||
|
||||
// 'update-guild' function(guildMetadata) called when the guild metadata updates
|
||||
|
||||
// 'deleted-members' function(members) called when members were deleted on the server-side
|
||||
// 'updated-members' function(data: Array [ { oldMember, newMember } ]) called when a member was updated on the server-side
|
||||
// 'added-members' function(members) called when members were added on the server-side
|
||||
|
||||
// 'deleted-channels' function(channels) called when channels were deleted on the server-side
|
||||
// 'updated-channels' function(data: Array [ { oldChannel, newChannel } ]) called when a channel was updated on the server-side
|
||||
// 'added-channels' function(channels) called when channels are added on the server-side
|
||||
|
||||
// 'new-message' function(message) called when a message is received in a channel
|
||||
|
||||
// 'deleted-messages' function(messages) called when messages were deleted on the server-side
|
||||
// 'updated-messages' function(data: Array [ { oldMessage, newMessage } ]) called when messages were updated on the server-side
|
||||
// 'added-messages' function(addedAfter : Map<messageId -> addedMessage>, addedBefore : <messageId -> addedMessage>) called when messages were added on the server-side (that were not in our client-side db).
|
||||
|
||||
// Functions:
|
||||
// these three have grab counterparts
|
||||
// async fetchMetadata() Fetches the guild information as { name, icon_resource_id }
|
||||
// async fetchMembers() Fetches the guild members as [ { id, display_name, avatar_resource_id }, ... ]
|
||||
// async fetchChannels() Fetches the available channels as [ { id, name }, ... ]
|
||||
|
||||
// async fetchMessagesRecent(channelId, number) Fetches the most recent number messages in a channel [ Message, ... ]
|
||||
// async fetchMessagesBefore(channelId, messageId, number) Fetches number messages before a specified message [ Message, ... ]
|
||||
// async fetchMessagesAfter(channelId, messageId, number) Fetches number messages after a specified message [ Message, ... ]
|
||||
|
||||
// TODO: change this to grab
|
||||
// async fetchResource(resourceId) Fetches the resource associated with a specified resrouceId
|
||||
|
||||
// async sendMessage(channelId, text) Sends a text message to a channel, returning the sent Message
|
||||
// async sendMessageWithResource(channelId, text, resource, resourceName) Sends a text message with a resource to a channel, returning the sent Message. text is optional
|
||||
|
||||
// async setStatus(status) Set the current logged in user's status
|
||||
// async setDisplayName(displayName) Sets the current logged in user's display name
|
||||
// async setAvatar(avatarBuff)
|
||||
// async updateChannel(channelId, name, flavorText) Updates a channel's name and flavor text
|
||||
|
||||
// async queryTokens() Queries for the login tokens as [ { token, member_id, created, expires }, ... ]
|
||||
// async revokeToken(token) Revokes an outstanding token
|
||||
|
||||
interface FetchCachedAndVerifyProps<ClientType, ServerType> {
|
||||
lock?: ConcurrentQueue<void>,
|
||||
serverFunc: (() => Promise<ServerType>)
|
||||
cacheFunc: (() => Promise<ClientType | null>),
|
||||
cacheUpdateFunc: ((cacheData: ClientType | null, serverData: ServerType) => Promise<boolean>), // returns true if changes were made to the cache
|
||||
updateEventName?: string | null,
|
||||
}
|
||||
|
||||
export default class ClientController extends EventEmitter {
|
||||
private dbCache: DBCache;
|
||||
|
||||
public id: string;
|
||||
public memberId: string;
|
||||
public url: string;
|
||||
public publicKey: crypto.KeyObject;
|
||||
public privateKey: crypto.KeyObject;
|
||||
public serverCert: string
|
||||
|
||||
public socket: socketio.Socket;
|
||||
|
||||
private _metadata: ServerMetaData | CacheServerData | null;
|
||||
private _recentMessages: RecentMessageRAMCache;
|
||||
|
||||
public channels: Map<string, Channel>;
|
||||
public members: Map<string, Member>;
|
||||
|
||||
public channelsLock: ConcurrentQueue<void>;
|
||||
public membersLock: ConcurrentQueue<void>;
|
||||
|
||||
public isVerified: boolean;
|
||||
|
||||
public resourceCallbacks: Map<string, ((err: any, resourceBuff: Buffer | null) => Promise<void> | void)[]>;
|
||||
public dedupedCallbacks: Map<string, (() => Promise<void> | void)[]>;
|
||||
|
||||
constructor(dbCache: DBCache, config: ServerConfig) {
|
||||
super();
|
||||
|
||||
this.dbCache = dbCache;
|
||||
|
||||
// TODO: fetch these from the cache when they are needed rather than storing them in memory (especially private key)
|
||||
let publicKey = typeof config.publicKey === 'string' ? crypto.createPublicKey(config.publicKey) : config.publicKey;
|
||||
let privateKey = typeof config.privateKey === 'string' ? crypto.createPrivateKey(config.privateKey) : config.privateKey;
|
||||
|
||||
this.id = config.guildId + ''; // this is the client-side server id (from Cache)
|
||||
this.memberId = config.memberId;
|
||||
this.url = config.url;
|
||||
this.publicKey = publicKey;
|
||||
this.privateKey = privateKey;
|
||||
this.serverCert = config.serverCert;
|
||||
|
||||
this.socket = socketio.connect(this.url, {
|
||||
forceNew: true,
|
||||
ca: this.serverCert, // this provides identity verification for the server
|
||||
});
|
||||
|
||||
this._metadata = null; // use grabMetadata();
|
||||
this._recentMessages = new RecentMessageRAMCache(); // use grabRecentMessages();
|
||||
|
||||
// These are used to make message objects more useful
|
||||
// TODO: make these private
|
||||
this.channels = new Map<string, Channel>(); // use grabChannels();
|
||||
this.members = new Map<string, Member>(); // use grabMembers();
|
||||
|
||||
this.channelsLock = new ConcurrentQueue<void>(1);
|
||||
this.membersLock = new ConcurrentQueue<void>(1);
|
||||
|
||||
this.isVerified = false;
|
||||
|
||||
this.resourceCallbacks = new Map<string, ((err: any, resourceBuff: Buffer | null) => Promise<void> | void)[]>(); // resourceId -> [ callbackFunc, ... ];
|
||||
this.dedupedCallbacks = new Map<string, (() => Promise<void> | void)[]>(); // dedupeId -> [ callbackFunc, ... ];
|
||||
|
||||
this._bindSocket();
|
||||
this._bindInternalEvents();
|
||||
}
|
||||
|
||||
_bindSocket(): void {
|
||||
this.socket.on('connect', async () => {
|
||||
LOG.info('connected to server#' + this.id);
|
||||
this.emit('connected');
|
||||
// TODO: re-verify on verification failure?
|
||||
await this.verify();
|
||||
});
|
||||
this.socket.on('disconnect', () => {
|
||||
LOG.info('disconnected from server#' + this.id);
|
||||
this.isVerified = false;
|
||||
|
||||
this._metadata = null;
|
||||
this._recentMessages.clear();
|
||||
this.channels.clear();
|
||||
this.members.clear();
|
||||
|
||||
this.emit('disconnected');
|
||||
});
|
||||
this.socket.on('new-message', async (dataMessage) => {
|
||||
await this.ensureMembers();
|
||||
await this.ensureChannels();
|
||||
let message = Message.fromDBData(dataMessage, this.members, this.channels);
|
||||
await this.dbCache.upsertServerMessages(this.id, message.channel.id, [ message ]);
|
||||
LOG.info(message.toString());
|
||||
this._recentMessages.addNewMessage(this.id, message);
|
||||
this.emit('new-message', message);
|
||||
});
|
||||
this.socket.on('update-member', async (member) => {
|
||||
await this.ensureMembers();
|
||||
let oldMember = this.members.get(member.id);
|
||||
if (oldMember) {
|
||||
this.emit('updated-members', [ { oldMember: oldMember, newMember: member } ]);
|
||||
} else {
|
||||
this.emit('added-members', [ member ]);
|
||||
}
|
||||
});
|
||||
this.socket.on('update-channel', async (channel) => {
|
||||
await this.ensureChannels();
|
||||
let oldChannel = this.channels.get(channel.id);
|
||||
if (oldChannel) {
|
||||
this.emit('updated-channels', [ { oldChannel: oldChannel, newChannel: channel } ]);
|
||||
} else {
|
||||
this.emit('added-channels', [ channel ]);
|
||||
}
|
||||
});
|
||||
this.socket.on('new-channel', async (channel) => {
|
||||
await this.ensureChannels();
|
||||
this.emit('added-channels', [ channel ]);
|
||||
});
|
||||
this.socket.on('update-server', async (serverMeta) => {
|
||||
await this.dbCache.updateServer(this.id, serverMeta);
|
||||
this.emit('update-server', serverMeta);
|
||||
});
|
||||
}
|
||||
|
||||
_bindInternalEvents(): void {
|
||||
this.on('added-members', async (members: Member[]) => {
|
||||
for (let member of members) {
|
||||
this.members.set(member.id, member);
|
||||
}
|
||||
await this.dbCache.updateServerMembers(this.id, Array.from(this.members.values()));
|
||||
});
|
||||
this.on('updated-members', async (data: { oldMember: Member, newMember: Member }[]) => {
|
||||
for (const { oldMember, newMember } of data) {
|
||||
this.members.set(newMember.id, newMember);
|
||||
}
|
||||
await this.dbCache.updateServerMembers(this.id, Array.from(this.members.values()));
|
||||
});
|
||||
this.on('deleted-members', async (members: Member[]) => {
|
||||
for (let member of members) {
|
||||
this.members.delete(member.id);
|
||||
}
|
||||
await this.dbCache.updateServerMembers(this.id, Array.from(this.members.values()));
|
||||
});
|
||||
|
||||
this.on('added-channels', async (channels: Channel[]) => {
|
||||
for (let channel of channels) {
|
||||
this.channels.set(channel.id, channel);
|
||||
}
|
||||
await this.dbCache.updateServerChannels(this.id, Array.from(this.channels.values()));
|
||||
});
|
||||
this.on('updated-channels', async (data: { oldChannel: Channel, newChannel: Channel }[]) => {
|
||||
for (const { oldChannel, newChannel } of data) {
|
||||
this.channels.set(newChannel.id, newChannel);
|
||||
}
|
||||
await this.dbCache.updateServerChannels(this.id, Array.from(this.channels.values()));
|
||||
});
|
||||
this.on('deleted-channels', async (channels: Channel[]) => {
|
||||
for (let channel of channels) {
|
||||
this.channels.delete(channel.id);
|
||||
}
|
||||
await this.dbCache.updateServerChannels(this.id, Array.from(this.channels.values()));
|
||||
});
|
||||
|
||||
this.on('added-messages', async (channel: Channel, addedAfter: Map<string, Message>, addedBefore: Map<string, Message>) => {
|
||||
// Adding messages is surprisingly complicated (see script.js's added-messages function)
|
||||
// so we can just drop the channel and refresh it once if any messages got added while
|
||||
// we were gone. Further, it is probably best to make 100% sure that the script.js
|
||||
// implementation is correct before copying it elsewhere. (I'm 90% sure it is correct)
|
||||
// Getting this correct and actually implementing the added-messages feature in the
|
||||
// RAM cache would be useful for first-time joiners to a server, getting the channel
|
||||
// messages for the first time
|
||||
// Alternatively, just store the date in the message and use order-by
|
||||
this._recentMessages.dropChannel(this.id, channel.id);
|
||||
await this.dbCache.upsertServerMessages(this.id, channel.id, Array.from(addedAfter.values()));
|
||||
});
|
||||
this.on('updated-messages', async (channel: Channel, data: { oldMessage: Message, newMessage: Message }[]) => {
|
||||
for (let { oldMessage, newMessage } of data) {
|
||||
this._recentMessages.updateMessage(this.id, oldMessage, newMessage);
|
||||
}
|
||||
await this.dbCache.upsertServerMessages(this.id, channel.id, data.map(change => change.newMessage));
|
||||
});
|
||||
this.on('deleted-messages', async (_channel: Channel, messages: Message[]) => {
|
||||
for (let message of messages) {
|
||||
this._recentMessages.deleteMessage(this.id, message);
|
||||
}
|
||||
await this.dbCache.deleteServerMessages(this.id, messages.map(message => message.id));
|
||||
});
|
||||
}
|
||||
|
||||
_updateCachedMembers(members: Member[]): void {
|
||||
this.members.clear();
|
||||
for (let member of members) {
|
||||
this.members.set(member.id, member);
|
||||
}
|
||||
}
|
||||
|
||||
_updateCachedChannels(channels: Channel[]): void {
|
||||
this.channels.clear();
|
||||
for (let channel of channels) {
|
||||
this.channels.set(channel.id, channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data from the server but returns cached data if it already exists. If the server data comes back different
|
||||
* from the cached data, emits an event with the server data
|
||||
* @param lock A locking enqueue function to make sure that the cache data does not change while waiting for the server data to be requested.
|
||||
* @param serverFunc A function() that returns the data from the server
|
||||
* @param cacheFunc A function() that returns the data from the cache (returns null if no data)
|
||||
* @param cacheUpdateFunc A function(cacheData, serverData) is called after the server data is fetched. It should be used to update the data in the cache
|
||||
* @param updateEventName The name of the event to emit when the server data is different from the cache data (and the cache data is not null), null for no event
|
||||
*/
|
||||
async _fetchCachedAndVerify<T, U>(props: FetchCachedAndVerifyProps<T, U>): Promise<T | U> {
|
||||
const { lock, serverFunc, cacheFunc, cacheUpdateFunc, updateEventName } = props;
|
||||
return await new Promise(async (resolve, reject) => {
|
||||
let func = async () => {
|
||||
try {
|
||||
let serverPromise = serverFunc();
|
||||
|
||||
let cacheData = await cacheFunc();
|
||||
if (cacheData !== null) {
|
||||
resolve(cacheData);
|
||||
}
|
||||
|
||||
let serverData: U;
|
||||
try {
|
||||
serverData = await serverPromise;
|
||||
} catch (e) {
|
||||
if (cacheData !== null) {
|
||||
// Print an error if this was already resolved
|
||||
LOG.warn('Error fetching server data:', e);
|
||||
return;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure the 'added/deleted/updated' events get run before returning the server data
|
||||
try {
|
||||
let changesMade = await cacheUpdateFunc(cacheData, serverData);
|
||||
|
||||
if (updateEventName && cacheData != null && changesMade) {
|
||||
this.emit(updateEventName, serverData);
|
||||
}
|
||||
} catch (e) {
|
||||
LOG.error('error handling cache update', e);
|
||||
}
|
||||
|
||||
if (cacheData == null) {
|
||||
resolve(serverData);
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
if (lock) {
|
||||
await lock.push(func);
|
||||
} else {
|
||||
await func();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// This function is a promise for error stack tracking purposes.
|
||||
async _socketEmitTimeout(ms: number, name: string, ...args: any[]): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
let socketArgs = args.slice(0, args.length - 1);
|
||||
let respond = args[args.length - 1];
|
||||
|
||||
let cutoff = false;
|
||||
let timeout = setTimeout(() => {
|
||||
cutoff = true;
|
||||
respond('emit timeout');
|
||||
resolve();
|
||||
}, ms);
|
||||
|
||||
this.socket.emit(name, ...socketArgs, (...respondArgs: any[]) => {
|
||||
if (cutoff) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
respond(...respondArgs);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries data from the server
|
||||
* @param endpoint The server-side socket endpoint
|
||||
* @param args The server socket arguments
|
||||
*/
|
||||
async _queryServer(endpoint: string, ...args: any[]): Promise<any> {
|
||||
// NOTE: socket.io may cause client-side memory leaks if the ack function is never called
|
||||
await this.ensureVerified(5000);
|
||||
let message = `querying s#${this.id} @${endpoint}(${args.map(arg => LOG.inspect(arg)).join(', ')})`;
|
||||
LOG.silly(message);
|
||||
//if (endpoint === 'fetch-messages-recent') LOG.silly(null, new Error('call stack'));
|
||||
return await new Promise((resolve, reject) => {
|
||||
this._socketEmitTimeout(5000, endpoint, ...args, async (errMsg: string, serverData: any) => {
|
||||
if (errMsg) {
|
||||
reject(new Error('error fetching server data @' + endpoint + ' / [' + args.map(arg => LOG.inspect(arg)).join(', ') + ']: ' + errMsg));
|
||||
} else {
|
||||
resolve(serverData);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Make this "T extends something with an id"
|
||||
static _getChanges<T>(cacheData: T[] | null, serverData: T[], equal: ((a: T, b: T) => boolean)): Changes<T> {
|
||||
if (cacheData === null) {
|
||||
return { updated: [], added: serverData, deleted: [] };
|
||||
}
|
||||
let updated: { oldDataPoint: T, newDataPoint: T }[] = [];
|
||||
let added: T[] = [];
|
||||
let deleted: T[] = [];
|
||||
for (let serverDataPoint of serverData) {
|
||||
let cacheDataPoint = cacheData.find((m: T) => (m as any).id == (serverDataPoint as any).id);
|
||||
if (cacheDataPoint) {
|
||||
if (!equal(cacheDataPoint, serverDataPoint)) {
|
||||
updated.push({ oldDataPoint: cacheDataPoint, newDataPoint: serverDataPoint });
|
||||
}
|
||||
} else {
|
||||
added.push(serverDataPoint);
|
||||
}
|
||||
}
|
||||
for (let cacheDataPoint of cacheData) {
|
||||
let serverDataPoint = serverData.find((s: T) => (s as any).id == (cacheDataPoint as any).id);
|
||||
if (serverDataPoint == null) {
|
||||
deleted.push(cacheDataPoint);
|
||||
}
|
||||
}
|
||||
|
||||
return { updated, added, deleted };
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.socket.disconnect();
|
||||
}
|
||||
|
||||
async verify(): Promise<void> {
|
||||
await new Promise<void>(async (resolve, reject) => {
|
||||
// Solve the server's challenge
|
||||
let publicKeyBuff = this.publicKey.export({ type: 'spki', format: 'der' });
|
||||
this._socketEmitTimeout(5000, 'challenge', publicKeyBuff, (errMsg, algo, type, challenge) => {
|
||||
if (errMsg) {
|
||||
reject(new Error('challenge request failed: ' + errMsg));
|
||||
return;
|
||||
}
|
||||
const sign = crypto.createSign(algo);
|
||||
sign.write(challenge);
|
||||
sign.end();
|
||||
|
||||
let signature = sign.sign(this.privateKey, type);
|
||||
this._socketEmitTimeout(5000, 'verify', signature, (errMsg, memberId) => {
|
||||
if (errMsg) {
|
||||
reject(new Error('verification request failed: ' + errMsg));
|
||||
return;
|
||||
}
|
||||
this.memberId = memberId;
|
||||
this.dbCache.updateServerMemberId(this.id, this.memberId);
|
||||
this.isVerified = true;
|
||||
LOG.info(`verified at server#${this.id} as u#${this.memberId}`);
|
||||
resolve();
|
||||
this.emit('verified');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// timeout is in ms
|
||||
async ensureVerified(timeout: number): Promise<void> {
|
||||
if (this.isVerified) {
|
||||
return;
|
||||
}
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let timeoutId: any = null;
|
||||
let listener = () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve();
|
||||
}
|
||||
this.once('verified', listener);
|
||||
timeoutId = setTimeout(() => {
|
||||
this.off('verified', listener);
|
||||
reject(new Error('verification timeout'));
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
async ensureMetadata(): Promise<void> {
|
||||
if (this._metadata !== null) return;
|
||||
await this.fetchMetadata();
|
||||
}
|
||||
|
||||
async ensureMembers(): Promise<void> {
|
||||
if (this.members.size > 0) return;
|
||||
await this.fetchMembers();
|
||||
}
|
||||
|
||||
async ensureChannels(): Promise<void> {
|
||||
if (this.channels.size > 0) return;
|
||||
await this.fetchChannels();
|
||||
}
|
||||
|
||||
async getMyMember(): Promise<Member> {
|
||||
await this.ensureMembers();
|
||||
return this.members.get(this.memberId) as Member;
|
||||
}
|
||||
|
||||
async grabMetadata(): Promise<ServerMetaData | CacheServerData> {
|
||||
await this.ensureMetadata();
|
||||
return this._metadata as ServerMetaData | CacheServerData;
|
||||
}
|
||||
|
||||
async grabMembers(): Promise<Member[]> {
|
||||
await this.ensureMembers();
|
||||
return Array.from(this.members.values());
|
||||
}
|
||||
|
||||
async grabChannels(): Promise<Channel[]> {
|
||||
await this.ensureChannels();
|
||||
return Array.from(this.channels.values());
|
||||
}
|
||||
|
||||
async grabRecentMessages(channelId: string, number: number): Promise<Message[]> {
|
||||
let cached = this._recentMessages.getRecentMessages(this.id, channelId, number);
|
||||
if (cached !== null) return cached;
|
||||
return await this.fetchMessagesRecent(channelId, number);
|
||||
}
|
||||
|
||||
async fetchMetadata(): Promise<ServerMetaData | CacheServerData> {
|
||||
function isDifferent(cacheData: CacheServerData | null, serverData: ServerMetaData) {
|
||||
if (cacheData === null) return true;
|
||||
return !!(cacheData.name != serverData.name || cacheData.iconResourceId != serverData.iconResourceId)
|
||||
}
|
||||
let metadata = await this._fetchCachedAndVerify<CacheServerData, ServerMetaData>({
|
||||
serverFunc: async () => { return ServerMetaData.fromServerDBData(await this._queryServer('fetch-server')); },
|
||||
cacheFunc: async () => { return await this.dbCache.getServer(this.id); },
|
||||
cacheUpdateFunc: async (cacheData: CacheServerData | null, serverData: ServerMetaData) => {
|
||||
if (!isDifferent(cacheData, serverData)) return false;
|
||||
await this.dbCache.updateServer(this.id, serverData);
|
||||
return true;
|
||||
},
|
||||
updateEventName: 'update-server',
|
||||
});
|
||||
this._metadata = metadata as ServerMetaData | CacheServerData;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
// if not verified, will attempt to load from cache rather than waiting for verification
|
||||
// returns { avatar_resource_id, display_name, status }
|
||||
async fetchConnectionInfo(): Promise<ConnectionInfo> {
|
||||
let connection: ConnectionInfo = {
|
||||
id: null,
|
||||
avatarResourceId: null,
|
||||
displayName: 'Connecting...',
|
||||
status: '',
|
||||
privileges: [],
|
||||
roleName: null,
|
||||
roleColor: null,
|
||||
rolePriority: null
|
||||
}
|
||||
if (this.isVerified) {
|
||||
await this.ensureMembers();
|
||||
let member = this.members.get(this.memberId);
|
||||
if (member) {
|
||||
connection.id = member.id;
|
||||
connection.avatarResourceId = member.avatarResourceId;
|
||||
connection.displayName = member.displayName;
|
||||
connection.status = member.status;
|
||||
connection.roleName = member.roleName;
|
||||
connection.roleColor = member.roleColor;
|
||||
connection.rolePriority = member.rolePriority;
|
||||
connection.privileges = member.privileges;
|
||||
} else {
|
||||
LOG.warn('Unable to find self in members', this.members);
|
||||
}
|
||||
} else {
|
||||
let cacheMembers = await this.dbCache.getMembers(this.id);
|
||||
if (cacheMembers) {
|
||||
let member = cacheMembers.find(m => m.id == this.memberId);
|
||||
if (member) {
|
||||
connection.id = member.id;
|
||||
connection.avatarResourceId = member.avatarResourceId;
|
||||
connection.displayName = member.displayName;
|
||||
connection.status = 'connecting';
|
||||
connection.privileges = [];
|
||||
} else {
|
||||
LOG.warn('Unable to find self in cached members');
|
||||
}
|
||||
}
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
async fetchMembers(): Promise<Member[]> {
|
||||
let members = await this._fetchCachedAndVerify<Member[], Member[]>({
|
||||
lock: this.membersLock,
|
||||
serverFunc: async () => {
|
||||
let dataMembers = (await this._queryServer('fetch-members')) as any[];
|
||||
return dataMembers.map((dataMember: any) => Member.fromDBData(dataMember));
|
||||
},
|
||||
cacheFunc: async () => { return await this.dbCache.getMembers(this.id); },
|
||||
cacheUpdateFunc: async (cacheData: Member[] | null, serverData: Member[]) => {
|
||||
function equal(cacheMember: Member, serverMember: Member) {
|
||||
return (
|
||||
cacheMember.id === serverMember.id &&
|
||||
cacheMember.displayName === serverMember.displayName &&
|
||||
cacheMember.status === serverMember.status &&
|
||||
cacheMember.avatarResourceId === serverMember.avatarResourceId &&
|
||||
cacheMember.roleName === serverMember.roleName &&
|
||||
cacheMember.roleColor === serverMember.roleColor &&
|
||||
cacheMember.rolePriority === serverMember.rolePriority &&
|
||||
cacheMember.privileges.join(',') === serverMember.privileges.join(',')
|
||||
);
|
||||
}
|
||||
let changes = ClientController._getChanges<Member>(cacheData, serverData, equal);
|
||||
|
||||
if (changes.updated.length == 0 && changes.added.length == 0 && changes.deleted.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.dbCache.updateServerMembers(this.id, serverData);
|
||||
this._updateCachedMembers(serverData);
|
||||
|
||||
if (changes.deleted.length > 0) {
|
||||
this.emit('deleted-members', changes.deleted);
|
||||
}
|
||||
if (changes.added.length > 0) {
|
||||
this.emit('added-members', changes.added);
|
||||
}
|
||||
if (changes.updated.length > 0) {
|
||||
this.emit('updated-members', changes.updated.map(change => ({ oldMember: change.oldDataPoint, newMember: change.newDataPoint })));
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
updateEventName: null,
|
||||
});
|
||||
this._updateCachedMembers(members);
|
||||
return members;
|
||||
}
|
||||
|
||||
async fetchChannels(): Promise<Channel[]> {
|
||||
let changes: Changes<Channel> | null = null;
|
||||
let channels = await this._fetchCachedAndVerify<Channel[], Channel[]>({
|
||||
lock: this.channelsLock,
|
||||
serverFunc: async () => {
|
||||
let dataChannels = (await this._queryServer('fetch-channels')) as any[];
|
||||
return dataChannels.map((dataChannel: any) => Channel.fromDBData(dataChannel));
|
||||
},
|
||||
cacheFunc: async () => { return await this.dbCache.getChannels(this.id); },
|
||||
cacheUpdateFunc: async (cacheData: Channel[] | null, serverData: Channel[]) => {
|
||||
function equal(cacheChannel: Channel, serverChannel: Channel) {
|
||||
return cacheChannel.id == serverChannel.id &&
|
||||
cacheChannel.index == serverChannel.index &&
|
||||
cacheChannel.name == serverChannel.name &&
|
||||
cacheChannel.flavorText == serverChannel.flavorText;
|
||||
}
|
||||
|
||||
let changes = ClientController._getChanges(cacheData, serverData, equal);
|
||||
if (changes.updated.length == 0 && changes.added.length == 0 && changes.deleted.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All cache updates are handled by internal event handlers
|
||||
if (changes.deleted.length > 0) {
|
||||
this.emit('deleted-channels', changes.deleted);
|
||||
}
|
||||
if (changes.added.length > 0) {
|
||||
this.emit('added-channels', changes.added);
|
||||
}
|
||||
if (changes.updated.length > 0) {
|
||||
this.emit('updated-channels', changes.updated.map(change => ({ oldChannel: change.oldDataPoint, newChannel: change.newDataPoint })));
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
updateEventName: null,
|
||||
});
|
||||
this._updateCachedChannels(channels);
|
||||
return channels;
|
||||
}
|
||||
|
||||
// channelId: the id of the channel the messages were fetched from (used in the events)
|
||||
// firstMessageId: the first message in the current list of messages
|
||||
// lastMessageId: the last element in the current list of messages
|
||||
private async updateMessageCache(
|
||||
channelId: string,
|
||||
firstMessageId: string | null,
|
||||
lastMessageId: string | null,
|
||||
cacheMessages: Message[] | null,
|
||||
serverMessages: Message[]
|
||||
): Promise<boolean> {
|
||||
function equal(cacheMessage: Message, serverMessage: Message) {
|
||||
return cacheMessage.id == serverMessage.id &&
|
||||
cacheMessage.channel.id == serverMessage.channel.id &&
|
||||
cacheMessage.member.id == serverMessage.member.id &&
|
||||
cacheMessage.sent.getTime() == serverMessage.sent.getTime() &&
|
||||
cacheMessage.text == serverMessage.text &&
|
||||
cacheMessage.resourceId == serverMessage.resourceId &&
|
||||
cacheMessage.resourceName == serverMessage.resourceName &&
|
||||
cacheMessage.resourceWidth == serverMessage.resourceWidth &&
|
||||
cacheMessage.resourceHeight == serverMessage.resourceHeight &&
|
||||
cacheMessage.resourcePreviewId == serverMessage.resourcePreviewId
|
||||
}
|
||||
|
||||
let diffFound = false;
|
||||
|
||||
let updatedMessages: { oldMessage: Message, newMessage: Message }[] = [];
|
||||
let addedAfter = new Map<string, Message>(); // messageId -> message added after this message
|
||||
let addedBefore = new Map<string, Message>(); // messageId -> message added before this message
|
||||
for (let i = 0; i < serverMessages.length; ++i) {
|
||||
let serverMessage = serverMessages[i];
|
||||
let cacheMessage = cacheMessages?.find(m => serverMessage.id == m.id);
|
||||
if (cacheMessage) {
|
||||
if (!equal(cacheMessage, serverMessage)) {
|
||||
diffFound = true;
|
||||
updatedMessages.push({
|
||||
oldMessage: cacheMessage,
|
||||
newMessage: serverMessage,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// items in server not in cache are added
|
||||
diffFound = true;
|
||||
let comesAfter = serverMessages[i - 1] || { id: lastMessageId }; // this message comesAfter comesAfter
|
||||
let comesBefore = serverMessages[i + 1] || { id: firstMessageId }; // this message comesBefore comesBefore
|
||||
addedAfter.set(comesAfter.id, serverMessage);
|
||||
addedBefore.set(comesBefore.id, serverMessage);
|
||||
}
|
||||
}
|
||||
|
||||
let deletedMessages: Message[] = [];
|
||||
if (cacheMessages !== null) {
|
||||
for (let cacheMessage of cacheMessages) {
|
||||
let serverMessage = serverMessages.find(m => cacheMessage.id == m.id);
|
||||
if (serverMessage == null) {
|
||||
// items in cache not in server are deleted
|
||||
diffFound = true;
|
||||
deletedMessages.push(cacheMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send out the events and update the cache
|
||||
|
||||
if (cacheMessages !== null) {
|
||||
if (cacheMessages.length > 0 && deletedMessages.length === cacheMessages.length) {
|
||||
// if all of the cache data was invalid, it is likely that it needs to be cleared
|
||||
// this typically happens when the server got a lot of new messages since the cache
|
||||
// was last updated
|
||||
await this.dbCache.clearServerMessages(this.id, deletedMessages[0].channel.id);
|
||||
} else if (deletedMessages.length > 0) {
|
||||
// Messages from the cache that come on the far side of the request are marked as deleted
|
||||
// so they are deleted from the UI. However, they should not be removed from the cache
|
||||
// yet since they most likely have not been pulled from the server yet. (and will be
|
||||
// very likely pulled from the server on the next fetch).
|
||||
let cacheDeletedMessages = deletedMessages.slice();
|
||||
let i: number | null = null;
|
||||
if (firstMessageId || (firstMessageId == null && lastMessageId == null)) { // before & recent
|
||||
i = 0;
|
||||
while (cacheDeletedMessages.length > 0 && cacheDeletedMessages[0].id == cacheMessages[i].id) {
|
||||
cacheDeletedMessages.shift();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (lastMessageId) { // after
|
||||
i = 0;
|
||||
while (cacheDeletedMessages.length > 0 && cacheDeletedMessages[cacheDeletedMessages.length - 1].id == cacheMessages[cacheMessages.length - 1 - i].id) {
|
||||
cacheDeletedMessages.pop();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
//LOG.debug('skipping ' + i + ' deleted messages on the cache side -> deleting ' + cacheDeletedMessages.length + ' cache messages instead of ' + deletedMessages.length);
|
||||
if (cacheDeletedMessages.length > 0) {
|
||||
await this.dbCache.deleteServerMessages(this.id, cacheDeletedMessages.map(m => m.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedMessages.length > 0) { // make sure to do deleted before added
|
||||
this.emit('deleted-messages', this.channels.get(channelId), deletedMessages);
|
||||
}
|
||||
if (updatedMessages.length > 0) {
|
||||
this.emit('updated-messages', this.channels.get(channelId), updatedMessages);
|
||||
}
|
||||
if (addedAfter.size > 0 || addedBefore.size > 0) {
|
||||
this.emit('added-messages', this.channels.get(channelId), addedAfter, addedBefore);
|
||||
}
|
||||
|
||||
return diffFound;
|
||||
}
|
||||
|
||||
async fetchMessagesRecent(channelId: string, number: number): Promise<Message[]> {
|
||||
await this.ensureMembers();
|
||||
await this.ensureChannels();
|
||||
let messages = await this._fetchCachedAndVerify<Message[], Message[]>({
|
||||
serverFunc: async () => {
|
||||
let dataMessages = await this._queryServer('fetch-messages-recent', channelId, number) as any[];
|
||||
return dataMessages.map((dataMessage: any) => Message.fromDBData(dataMessage, this.members, this.channels));
|
||||
},
|
||||
cacheFunc: async () => {
|
||||
return await this.dbCache.getMessagesRecent(this.id, channelId, number, this.members, this.channels);
|
||||
},
|
||||
cacheUpdateFunc: async (cacheData: Message[] | null, serverData: Message[]) => {
|
||||
return await this.updateMessageCache(channelId, null, null, cacheData, serverData);
|
||||
},
|
||||
updateEventName: null // all events are handled in diffFunc
|
||||
});
|
||||
this._recentMessages.putRecentMessages(this.id, channelId, messages);
|
||||
return messages;
|
||||
}
|
||||
|
||||
async fetchMessagesBefore(channelId: string, messageId: string, number: number): Promise<Message[] | null> {
|
||||
await this.ensureMembers();
|
||||
await this.ensureChannels();
|
||||
let messages = await this._fetchCachedAndVerify<Message[], Message[] | null>({
|
||||
serverFunc: async () => {
|
||||
let dataMessages = await this._queryServer('fetch-messages-before', channelId, messageId, number) as any[];
|
||||
return dataMessages.map((dataMessage: any) => Message.fromDBData(dataMessage, this.members, this.channels));
|
||||
},
|
||||
cacheFunc: async () => {
|
||||
return await this.dbCache.getMessagesBefore(this.id, channelId, messageId, number, this.members, this.channels);
|
||||
},
|
||||
cacheUpdateFunc: async (cacheData: Message[] | null, serverData: Message[]) => {
|
||||
return await this.updateMessageCache(channelId, messageId, null, cacheData, serverData);
|
||||
},
|
||||
updateEventName: null // all events are handled in diffFunc
|
||||
});
|
||||
return messages;
|
||||
}
|
||||
|
||||
async fetchMessagesAfter(channelId: string, messageId: string, number: number): Promise<Message[] | null> {
|
||||
await this.ensureMembers();
|
||||
await this.ensureChannels();
|
||||
let messages = await this._fetchCachedAndVerify<Message[], Message[] | null>({
|
||||
serverFunc: async () => {
|
||||
let dataMessages = await this._queryServer('fetch-messages-after', channelId, messageId, number) as any[];
|
||||
return dataMessages.map((dataMessage: any) => Message.fromDBData(dataMessage, this.members, this.channels));
|
||||
},
|
||||
cacheFunc: async () => { return await this.dbCache.getMessagesAfter(this.id, channelId, messageId, number, this.members, this.channels); },
|
||||
cacheUpdateFunc: async (cacheData: Message[] | null, serverData: Message[]) => {
|
||||
return await this.updateMessageCache(channelId, null, messageId, cacheData, serverData);
|
||||
},
|
||||
updateEventName: null // all events are handled in diffFunc
|
||||
});
|
||||
return messages;
|
||||
}
|
||||
|
||||
async _fetchResourceInternal(resourceId: string): Promise<Buffer> {
|
||||
// not using standard _fetchCached here because server-side resources never change.
|
||||
// rather, the resource_id would be updated if it changes for a message, server icon, avatar, etc.
|
||||
// this provides for a much simpler cache system (3 stages, client-memory, client-db, server-side)
|
||||
// since all guildId / resourceId pairs will never update their resource buffers, this cache becomes exceedingly simple
|
||||
|
||||
let resourceCacheDataBuff = ResourceRAMCache.getResource(this.id, resourceId);
|
||||
if (resourceCacheDataBuff != null) {
|
||||
return resourceCacheDataBuff;
|
||||
}
|
||||
|
||||
let cacheData = await this.dbCache.getResource(this.id, resourceId);
|
||||
if (cacheData !== null) {
|
||||
ResourceRAMCache.putResource(this.id, resourceId, cacheData.data);
|
||||
return cacheData.data;
|
||||
}
|
||||
|
||||
// Note: Not pre-requesting from server asynchronously to reduce fetch-resource requests
|
||||
let serverData = await this._queryServer('fetch-resource', resourceId);
|
||||
|
||||
ResourceRAMCache.putResource(this.id, resourceId, serverData.data);
|
||||
await this.dbCache.upsertServerResources(this.id, [ serverData ]);
|
||||
|
||||
return serverData.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduplicates client-side resource requests. Useful for when the client wants to fetch an avatar
|
||||
* multiple times for the channel feed. Or if there is a duplicate image in the feed.
|
||||
* @param resourceId
|
||||
*/
|
||||
async fetchResource(resourceId: string): Promise<Buffer> {
|
||||
return await new Promise(async (resolve, reject) => {
|
||||
let resultFunc = (err: any, resourceBuff: Buffer | null) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (resourceBuff) {
|
||||
resolve(resourceBuff);
|
||||
} else {
|
||||
reject(new ShouldNeverHappenError('no buffer or error!'));
|
||||
}
|
||||
}
|
||||
if (this.resourceCallbacks.has(resourceId)) {
|
||||
this.resourceCallbacks.get(resourceId)?.push(resultFunc);
|
||||
} else {
|
||||
this.resourceCallbacks.set(resourceId, [ resultFunc ]);
|
||||
let result: Buffer | null = null;
|
||||
let err: any = null;
|
||||
try {
|
||||
result = await this._fetchResourceInternal(resourceId);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
for (let callbackFunc of this.resourceCallbacks.get(resourceId) ?? []) {
|
||||
callbackFunc(err, result);
|
||||
}
|
||||
this.resourceCallbacks.delete(resourceId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async sendMessage(channelId: string, text: string) {
|
||||
let dataMessage = await this._queryServer('send-message', channelId, text);
|
||||
await this.ensureMembers();
|
||||
await this.ensureChannels();
|
||||
return Message.fromDBData(dataMessage, this.members, this.channels);
|
||||
}
|
||||
|
||||
async sendMessageWithResource(channelId: string, text: string | null, resourceBuff: Buffer, resourceName: string) {
|
||||
let dataMessage = await this._queryServer('send-message-with-resource', channelId, text, resourceBuff, resourceName);
|
||||
await this.ensureMembers();
|
||||
await this.ensureChannels();
|
||||
return Message.fromDBData(dataMessage, this.members, this.channels);
|
||||
}
|
||||
|
||||
async setStatus(status: string): Promise<void> {
|
||||
// wow, that's simple for a change
|
||||
await this._queryServer('set-status', status);
|
||||
}
|
||||
|
||||
async setDisplayName(displayName: string): Promise<void> {
|
||||
await this._queryServer('set-display-name', displayName);
|
||||
}
|
||||
|
||||
async setAvatar(avatarBuff: Buffer): Promise<void> {
|
||||
await this._queryServer('set-avatar', avatarBuff);
|
||||
}
|
||||
|
||||
async setName(name: string): Promise<ServerMetaData> {
|
||||
return ServerMetaData.fromServerDBData(await this._queryServer('set-name', name));
|
||||
}
|
||||
|
||||
async setIcon(iconBuff: Buffer): Promise<ServerMetaData> {
|
||||
return ServerMetaData.fromServerDBData(await this._queryServer('set-icon', iconBuff));
|
||||
}
|
||||
|
||||
async updateChannel(channelId: string, name: string, flavorText: string | null): Promise<Channel> {
|
||||
return Channel.fromDBData(await this._queryServer('update-channel', channelId, name, flavorText));
|
||||
}
|
||||
|
||||
async createChannel(name: string, flavorText: string | null): Promise<Channel> {
|
||||
return Channel.fromDBData(await this._queryServer('create-text-channel', name, flavorText));
|
||||
}
|
||||
|
||||
async queryTokens(): Promise<Token[]> {
|
||||
// No cacheing for now, this is a relatively small request and comes
|
||||
// after a context-menu click so it's not as important to cache as the
|
||||
// channels, members, and messages
|
||||
let dataTokens = await this._queryServer('fetch-tokens');
|
||||
await this.ensureMembers();
|
||||
return dataTokens.map(dataToken => {
|
||||
return Token.fromDBData(dataToken, this.members);
|
||||
});
|
||||
}
|
||||
|
||||
async revokeToken(token: string): Promise<void> {
|
||||
return await this._queryServer('revoke-token', token);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.socket.close();
|
||||
}
|
||||
}
|
@ -1,522 +0,0 @@
|
||||
import * as electronRemote from '@electron/remote';
|
||||
const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import ConcurrentQueue from '../../concurrent-queue/concurrent-queue';
|
||||
|
||||
import Globals from './globals';
|
||||
|
||||
import * as sqlite from 'sqlite';
|
||||
import * as sqlite3 from 'sqlite3';
|
||||
import { Message, Member, Channel, Resource, ServerMetaData, ServerConfig, CacheServerData } from './data-types';
|
||||
|
||||
// A cache implemented using an sqlite database
|
||||
// Also stores configuration for server connections
|
||||
export default class DBCache {
|
||||
private TRANSACTION_QUEUE = new ConcurrentQueue(1);
|
||||
|
||||
private constructor(
|
||||
private readonly db: sqlite.Database
|
||||
) {}
|
||||
|
||||
async beginTransaction(): Promise<void> {
|
||||
await this.db.run('BEGIN TRANSACTION');
|
||||
}
|
||||
|
||||
async rollbackTransaction(): Promise<void> {
|
||||
await this.db.run('ROLLBACK');
|
||||
}
|
||||
|
||||
async commitTransaction(): Promise<void> {
|
||||
await this.db.run('COMMIT');
|
||||
}
|
||||
|
||||
async queueTransaction(func: (() => Promise<void>)): Promise<void> {
|
||||
await this.TRANSACTION_QUEUE.push(async () => {
|
||||
try {
|
||||
await this.beginTransaction();
|
||||
await func();
|
||||
await this.commitTransaction();
|
||||
} catch (e) {
|
||||
await this.rollbackTransaction();
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async connect(): Promise<DBCache> {
|
||||
try {
|
||||
await fs.access('./db');
|
||||
} catch (e) {
|
||||
await fs.mkdir('./db');
|
||||
}
|
||||
return new DBCache(await sqlite.open({
|
||||
driver: sqlite3.Database,
|
||||
filename: './db/cache.db'
|
||||
}));
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.queueTransaction(async () => {
|
||||
await this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS identities (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
|
||||
, public_key TEXT NOT NULL
|
||||
, private_key TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
await this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
|
||||
, url TEXT NOT NULL
|
||||
, cert TEXT NOT NULL
|
||||
, name TEXT
|
||||
, icon_resource_id TEXT
|
||||
, member_id TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
await this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS server_identities (
|
||||
server_id INTEGER NOT NULL
|
||||
, identity_id INTEGER NOT NULL
|
||||
, FOREIGN KEY (server_id) REFERENCES servers(id)
|
||||
, FOREIGN KEY (identity_id) REFERENCES identities(id)
|
||||
)
|
||||
`);
|
||||
|
||||
await this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS members (
|
||||
id TEXT NOT NULL
|
||||
, server_id INTEGER NOT NULL REFERENCES servers(id)
|
||||
, display_name TEXT NOT NULL
|
||||
, status TEXT NOT NULL
|
||||
, avatar_resource_id TEXT NOT NULL
|
||||
, role_name TEXT
|
||||
, role_color TEXT
|
||||
, role_priority INTEGER
|
||||
, privileges TEXT
|
||||
, CONSTRAINT members_id_server_id_con UNIQUE (id, server_id)
|
||||
)
|
||||
`);
|
||||
|
||||
await this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS channels (
|
||||
id TEXT NOT NULL
|
||||
, server_id INTEGER NOT NULL REFERENCES servers(id)
|
||||
, "index" INTEGER NOT NULL
|
||||
, name TEXT NOT NULL
|
||||
, flavor_text TEXT
|
||||
, CONSTRAINT channels_id_server_id_con UNIQUE (id, server_id)
|
||||
)
|
||||
`);
|
||||
|
||||
await this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS resources (
|
||||
id TEXT NOT NULL
|
||||
, server_id INTEGER NOT NULL REFERENCES servers(id)
|
||||
, hash BLOB NOT NULL
|
||||
, data BLOB NOT NULL
|
||||
, data_size INTEGER NOT NULL
|
||||
, last_used INTEGER NOT NULL
|
||||
, CONSTRAINT resources_id_server_id_con UNIQUE (id, server_id)
|
||||
)
|
||||
`);
|
||||
await this.db.run('CREATE INDEX IF NOT EXISTS resources_data_size_idx ON resources (data_size)');
|
||||
|
||||
// note: no foreign key on resource_id since we may not have cached the resource yet
|
||||
await this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT NOT NULL
|
||||
, server_id INTEGER NOT NULL REFERENCES servers(id)
|
||||
, channel_id TEXT NOT NULL REFERENCES channels(id)
|
||||
, member_id TEXT NOT NULL REFERENCES members(id)
|
||||
, sent_dtg INTEGER NOT NULL
|
||||
, text TEXT
|
||||
, resource_id TEXT
|
||||
, resource_name TEXT
|
||||
, resource_width INTEGER
|
||||
, resource_height INTEGER
|
||||
, resource_preview_id TEXT
|
||||
, CONSTRAINT messages_id_server_id_con UNIQUE (id, server_id)
|
||||
)
|
||||
`);
|
||||
await this.db.run('CREATE INDEX IF NOT EXISTS messages_id_idx ON messages (id)');
|
||||
await this.db.run('CREATE INDEX IF NOT EXISTS messages_sent_dtg_idx ON messages (sent_dtg)');
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.db.close();
|
||||
}
|
||||
|
||||
// dangerous!
|
||||
async reset(): Promise<void> {
|
||||
await this.queueTransaction(async () => {
|
||||
await this.db.run('DROP TABLE IF EXISTS identities');
|
||||
await this.db.run('DROP TABLE IF EXISTS servers');
|
||||
await this.db.run('DROP TABLE IF EXISTS server_identities');
|
||||
await this.db.run('DROP TABLE IF EXISTS members');
|
||||
await this.db.run('DROP TABLE IF EXISTS channels');
|
||||
await this.db.run('DROP TABLE IF EXISTS resources');
|
||||
await this.db.run('DROP TABLE IF EXISTS messages');
|
||||
});
|
||||
}
|
||||
|
||||
// returns the id of the identity inserted
|
||||
async addIdentity(publicKeyPem: string, privateKeyPem: string): Promise<number> {
|
||||
let result = await this.db.run(`
|
||||
INSERT INTO identities (public_key, private_key) VALUES (?, ?)
|
||||
`, [ publicKeyPem, privateKeyPem ]);
|
||||
if (!result || result.changes !== 1) {
|
||||
throw new Error('unable to insert identity');
|
||||
}
|
||||
return result.lastID as number;
|
||||
}
|
||||
|
||||
// returns the id (client-side) of the server inserted
|
||||
async addServer(url: string, cert?: string, name?: string): Promise<number> {
|
||||
let result = await this.db.run(`
|
||||
INSERT INTO servers (url, cert, name, icon_resource_id) VALUES (?, ?, ?, NULL)
|
||||
`, [ url, cert, name ]);
|
||||
if (!result || result.changes !== 1) {
|
||||
throw new Error('unable to insert server');
|
||||
}
|
||||
return result.lastID as number;
|
||||
}
|
||||
|
||||
async removeServer(serverId: string): Promise<void> {
|
||||
let result = await this.db.run('DELETE FROM servers WHERE id=?', [ serverId ]);
|
||||
if (result.changes != 1) {
|
||||
throw new Error('unable to remove server');
|
||||
}
|
||||
}
|
||||
|
||||
async addServerIdentity(serverId: number, identityId: number): Promise<void> {
|
||||
let result = await this.db.run(`
|
||||
INSERT INTO server_identities (server_id, identity_id) VALUES (?, ?)
|
||||
`, [ serverId, identityId ]);
|
||||
if (result.changes != 1) {
|
||||
throw new Error('unable to insert server identity');
|
||||
}
|
||||
}
|
||||
|
||||
async updateServer(serverId: string, serverMeta: ServerMetaData): Promise<void> {
|
||||
let result = await this.db.run('UPDATE servers SET name=?, icon_resource_id=? WHERE id=?', [ serverMeta.name, serverMeta.iconResourceId, serverId ]);
|
||||
if (result.changes != 1) {
|
||||
throw new Error('unable to update server');
|
||||
}
|
||||
}
|
||||
|
||||
async updateServerMemberId(serverId: string, memberId: string): Promise<void> {
|
||||
let result = await this.db.run('UPDATE servers SET member_id=? WHERE id=?', [ memberId, serverId ]);
|
||||
if (result.changes != 1) {
|
||||
throw new Error(`unable to update member id, s#${serverId}, mem#${memberId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async updateServerMembers(serverId: string, members: Member[]): Promise<void> {
|
||||
await this.queueTransaction(async () => {
|
||||
await this.db.run('DELETE FROM members WHERE server_id=?', [ serverId ]);
|
||||
let stmt = await this.db.prepare('INSERT INTO members (id, server_id, display_name, status, avatar_resource_id, role_name, role_color, role_priority, privileges) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||
for (let member of members) {
|
||||
let result = await stmt.run([ member.id, serverId, member.displayName, member.status, member.avatarResourceId, member.roleName, member.roleColor, member.rolePriority, member.privileges?.join(',') ]);
|
||||
if (result.changes != 1) {
|
||||
// note: probably want to warn and continue
|
||||
throw new Error('failed to insert member');
|
||||
}
|
||||
}
|
||||
await stmt.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
async clearAllMemberStatus(serverId: string): Promise<void> {
|
||||
await this.db.run(`UPDATE members SET status='unknown' WHERE server_id=?`, [ serverId ]);
|
||||
}
|
||||
|
||||
async updateServerChannels(serverId: string, channels: Channel[]): Promise<void> {
|
||||
console.log('setting to ' + channels.length + ' channels');
|
||||
await this.queueTransaction(async () => {
|
||||
await this.db.run('DELETE FROM channels WHERE server_id=?', [ serverId ]);
|
||||
let stmt = await this.db.prepare('INSERT INTO channels (id, server_id, "index", name, flavor_text) VALUES (?, ?, ?, ?, ?)');
|
||||
for (let channel of channels) {
|
||||
let result = await stmt.run([ channel.id, serverId, channel.index, channel.name, channel.flavorText ]);
|
||||
if (result.changes != 1) {
|
||||
// note: probably want to warn and continue
|
||||
throw new Error('failed to insert channel');
|
||||
}
|
||||
}
|
||||
await stmt.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: make this singular and a non-transaction based function?
|
||||
async upsertServerResources(serverId: string, resources: Resource[]): Promise<void> {
|
||||
await this.queueTransaction(async () => {
|
||||
let currentSizeResult = await this.db.get('SELECT SUM(data_size) AS current_size FROM resources WHERE server_id=?', [ serverId ]);
|
||||
let currentSize = parseInt(currentSizeResult.current_size || 0);
|
||||
let stmt = await this.db.prepare(`
|
||||
INSERT INTO resources (id, server_id, hash, data, data_size, last_used) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
ON CONFLICT (id, server_id) DO UPDATE SET hash=?3, data=?4, last_used=?6
|
||||
`);
|
||||
for (let resource of resources) {
|
||||
if (resource.data.length > Globals.MAX_CACHED_RESOURCE_SIZE) {
|
||||
continue;
|
||||
}
|
||||
while (resource.data.length + currentSize > Globals.MAX_SERVER_RESOURCE_CACHE_SIZE) {
|
||||
let targetResult = await this.db.get('SELECT id, data_size FROM resources ORDER BY last_used ASC LIMIT 1');
|
||||
let deleteResult = await this.db.run('DELETE FROM resources WHERE id=?', [ targetResult.id ]);
|
||||
if (deleteResult.changes != 1) {
|
||||
throw new Error('failed to delete excess resource');
|
||||
}
|
||||
currentSize -= targetResult.data_size;
|
||||
}
|
||||
let result = await stmt.run([ resource.id, serverId, resource.hash, resource.data, resource.data.length, new Date().getTime() ]);
|
||||
if (result.changes != 1) {
|
||||
throw new Error('failed to insert resource');
|
||||
}
|
||||
}
|
||||
await stmt.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
async upsertServerMessages(serverId: string, channelId: string, messages: Message[]): Promise<void> {
|
||||
await this.queueTransaction(async () => {
|
||||
let stmt = await this.db.prepare(`
|
||||
INSERT INTO messages (
|
||||
id, server_id, channel_id, member_id, sent_dtg, text
|
||||
, resource_id, resource_name, resource_width, resource_height, resource_preview_id
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
|
||||
ON CONFLICT (id, server_id)
|
||||
DO UPDATE SET
|
||||
server_id=?2, channel_id=?3, member_id=?4, sent_dtg=?5, text=?6
|
||||
, resource_id=?7, resource_name=?8, resource_width=?9, resource_height=?10, resource_preview_id=?11
|
||||
`);
|
||||
for (let message of messages) {
|
||||
let result = await stmt.run([
|
||||
message.id, serverId, message.channel.id,
|
||||
message.member.id, message.sent.getTime(), message.text,
|
||||
message.resourceId, message.resourceName,
|
||||
message.resourceWidth, message.resourceHeight,
|
||||
message.resourcePreviewId
|
||||
]);
|
||||
if (result.changes != 1) {
|
||||
// note: probably want to warn and continue
|
||||
throw new Error('failed to insert message');
|
||||
}
|
||||
}
|
||||
await stmt.finalize();
|
||||
// delete the oldest messages if the cache is too big
|
||||
await this.db.run(`
|
||||
DELETE FROM messages WHERE id IN (
|
||||
SELECT id FROM messages WHERE server_id=?1 AND channel_id=?2 ORDER BY sent_dtg
|
||||
LIMIT max(0, (SELECT COUNT(*) FROM messages WHERE server_id=?1 AND channel_id=?2) - ?3)
|
||||
)
|
||||
`, [ serverId, channelId, Globals.MAX_CACHED_CHANNEL_MESSAGES ]);
|
||||
});
|
||||
}
|
||||
|
||||
async clearServerMessages(serverId: string, channelId: string): Promise<void> {
|
||||
await this.db.run('DELETE FROM messages WHERE server_id=? AND channel_id=?', [ serverId, channelId ]);
|
||||
}
|
||||
|
||||
async deleteServerMessages(serverId: string, messageIds: string[]): Promise<void> {
|
||||
await this.queueTransaction(async () => {
|
||||
let stmt = await this.db.prepare('DELETE FROM messages WHERE id=? AND server_id=?'); // include server_id for security purposes
|
||||
for (let messageId of messageIds) {
|
||||
let result = await stmt.run([ messageId, serverId ]);
|
||||
if (result.changes != 1) {
|
||||
// note: probably want to warn and continue
|
||||
throw new Error('failed to delete message');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getServerConfigs(): Promise<ServerConfig[]> {
|
||||
let result = await this.db.all(`
|
||||
SELECT
|
||||
servers.id AS server_id
|
||||
, servers.url AS url
|
||||
, servers.cert AS server_cert
|
||||
, servers.member_id AS member_id
|
||||
, identities.public_key AS public_key
|
||||
, identities.private_key AS private_key
|
||||
FROM
|
||||
server_identities
|
||||
, servers
|
||||
, identities
|
||||
WHERE
|
||||
server_identities.identity_id = identities.id
|
||||
AND server_identities.server_id = servers.id
|
||||
`);
|
||||
return result.map((dataServerConfig: any) => ServerConfig.fromDBData(dataServerConfig));
|
||||
}
|
||||
|
||||
async getServerConfig(serverId: number, identityId: number): Promise<ServerConfig> {
|
||||
let result = await this.db.get(`
|
||||
SELECT
|
||||
servers.id AS server_id
|
||||
, servers.url AS url
|
||||
, servers.cert AS server_cert
|
||||
, servers.member_id AS member_id
|
||||
, identities.public_key AS public_key
|
||||
, identities.private_key AS private_key
|
||||
FROM
|
||||
server_identities
|
||||
, servers
|
||||
, identities
|
||||
WHERE
|
||||
server_identities.identity_id = identities.id
|
||||
AND server_identities.server_id = servers.id
|
||||
AND servers.id=?
|
||||
AND identities.id=?
|
||||
`, [ serverId, identityId ]);
|
||||
return ServerConfig.fromDBData(result);
|
||||
}
|
||||
|
||||
/*
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
|
||||
, url TEXT NOT NULL
|
||||
, cert TEXT NOT NULL
|
||||
, name TEXT
|
||||
, icon_resource_id TEXT
|
||||
, member_id TEXT
|
||||
)
|
||||
*/
|
||||
|
||||
async getServer(serverId: string): Promise<CacheServerData | null> {
|
||||
let result = await this.db.get(`
|
||||
SELECT
|
||||
id, url, cert, name,
|
||||
icon_resource_id, member_id
|
||||
FROM servers
|
||||
WHERE id=?
|
||||
`, [ serverId ]);
|
||||
if (result.name == null || result.icon_resource_id == null) {
|
||||
// server is not set up yet.
|
||||
return null;
|
||||
}
|
||||
return CacheServerData.fromDBData(result);
|
||||
}
|
||||
|
||||
async getServerMemberId(serverId: string): Promise<string> {
|
||||
let server = await this.db.get('SELECT member_id FROM servers WHERE id=?', [ serverId ]);
|
||||
return server.member_id;
|
||||
}
|
||||
|
||||
// returns null if no members
|
||||
async getMembers(serverId: string): Promise<Member[] | null> {
|
||||
let members = await this.db.all('SELECT * FROM members WHERE server_id=?', [ serverId ]);
|
||||
if (members.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return members.map((dataMember: any) => Member.fromDBData(dataMember));
|
||||
}
|
||||
|
||||
// returns null if no channels
|
||||
async getChannels(serverId: string): Promise<Channel[] | null> {
|
||||
let channels = await this.db.all('SELECT * FROM channels WHERE server_id=?', [ serverId ]);
|
||||
if (channels.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return channels.map((dataChannel: any) => Channel.fromDBData(dataChannel));
|
||||
}
|
||||
|
||||
// returns [] if no messages found
|
||||
async getMessagesRecent(
|
||||
serverId: string, channelId: string, number: number,
|
||||
members: Map<string, Member>, channels: Map<string, Channel>
|
||||
): Promise<Message[]> {
|
||||
let messages = await this.db.all(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
"id", "channel_id", "member_id"
|
||||
,"sent_dtg", "text"
|
||||
, "resource_id"
|
||||
, "resource_name"
|
||||
, "resource_width"
|
||||
, "resource_height"
|
||||
, "resource_preview_id"
|
||||
FROM "messages"
|
||||
WHERE "server_id"=? AND "channel_id"=?
|
||||
ORDER BY "sent_dtg" DESC
|
||||
LIMIT ?
|
||||
) AS "r" ORDER BY "r"."sent_dtg" ASC
|
||||
`, [ serverId, channelId, number ]);
|
||||
return messages.map((dataMessage: any) => Message.fromDBData(dataMessage, members, channels));
|
||||
}
|
||||
|
||||
// returns null if no messages found
|
||||
async getMessagesBefore(
|
||||
serverId: string, channelId: string, messageId: string, number: number,
|
||||
members: Map<string, Member>, channels: Map<string, Channel>
|
||||
): Promise<Message[] | null> {
|
||||
// Note: this query succeeds returning no results if the message with specified id is not found
|
||||
let messages = await this.db.all(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
"id", "channel_id", "member_id"
|
||||
, "sent_dtg", "text"
|
||||
, "resource_id"
|
||||
, "resource_name"
|
||||
, "resource_width"
|
||||
, "resource_height"
|
||||
, "resource_preview_id"
|
||||
FROM "messages"
|
||||
WHERE
|
||||
"server_id"=?
|
||||
AND "channel_id"=?
|
||||
AND "sent_dtg" < (SELECT "sent_dtg" FROM "messages" WHERE "id"=?)
|
||||
ORDER BY "sent_dtg" DESC
|
||||
LIMIT ?
|
||||
) AS "r" ORDER BY "r"."sent_dtg" ASC
|
||||
`, [ serverId, channelId, messageId, number ]);
|
||||
if (messages.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return messages.map((messageData: any) => Message.fromDBData(messageData, members, channels));
|
||||
}
|
||||
|
||||
// returns null if no messages found
|
||||
async getMessagesAfter(
|
||||
serverId: string, channelId: string, messageId: string, number: number,
|
||||
members: Map<string, Member>, channels: Map<string, Channel>
|
||||
): Promise<Message[] | null> {
|
||||
// Note: this query succeeds returning no results if the message with specified id is not found
|
||||
let messages = await this.db.all(`
|
||||
SELECT
|
||||
"id", "channel_id", "member_id"
|
||||
, "sent_dtg", "text"
|
||||
, "resource_id"
|
||||
, "resource_name"
|
||||
, "resource_width"
|
||||
, "resource_height"
|
||||
, "resource_preview_id"
|
||||
FROM "messages"
|
||||
WHERE
|
||||
"server_id"=?
|
||||
AND "channel_id"=?
|
||||
AND "sent_dtg" > (SELECT "sent_dtg" FROM "messages" WHERE "id"=?)
|
||||
ORDER BY "sent_dtg" ASC
|
||||
LIMIT ?
|
||||
`, [ serverId, channelId, messageId, number ]);
|
||||
if (messages.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return messages.map((messageData: any) => Message.fromDBData(messageData, members, channels));
|
||||
}
|
||||
|
||||
async getResource(serverId: string, resourceId: string): Promise<Resource | null> {
|
||||
let row = await this.db.get('SELECT id, data, hash FROM resources WHERE server_id=? AND id=?', [ serverId, resourceId ]);
|
||||
await this.db.run('UPDATE resources SET last_used=?1 WHERE server_id=?2 AND id=?3', [ new Date().getTime(), serverId, resourceId ]);
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
return Resource.fromDBData(row);
|
||||
}
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
import Globals from './globals';
|
||||
|
||||
import { Message, ShouldNeverHappenError } from './data-types';
|
||||
|
||||
// TODO: this class is junk now :(
|
||||
|
||||
// TODO: make this non-static
|
||||
export default class RecentMessageRAMCache {
|
||||
private data = new Map<string, { messages: Message[], size: number, hasFirstMessage: boolean, lastUsed: Date }>(); // (guildId, channelId) -> { messages, size, hasFirstMessage, lastUsed }
|
||||
private size = 0;
|
||||
|
||||
clear(): void {
|
||||
this.data.clear();
|
||||
this.size = 0;
|
||||
}
|
||||
|
||||
_cullIfNeeded(): void { // TODO: Test this
|
||||
if (this.size > Globals.MAX_RAM_CACHED_MESSAGES_TOTAL_SIZE) {
|
||||
let entries = Array.from(this.data.entries())
|
||||
.map(([ key, value ]) => { return { id: key, value: value }})
|
||||
.sort((a, b) => b.value.lastUsed.getTime() - a.value.lastUsed.getTime());
|
||||
while (this.size > Globals.MAX_RAM_CACHED_MESSAGES_TOTAL_SIZE) {
|
||||
let entry = entries.pop();
|
||||
if (entry === undefined) throw new ShouldNeverHappenError('No entry in the array but the message cache still has a size...');
|
||||
this.data.delete(entry.id);
|
||||
this.size -= entry.value.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_cullChannelIfNeeded(guildId: string, channelId: string): void { // TODO: test this
|
||||
let id = `s#${guildId}/c#${channelId}`;
|
||||
let value = this.data.get(id);
|
||||
if (!value) return;
|
||||
|
||||
while (value.size > Globals.MAX_RAM_CACHED_MESSAGES_CHANNEL_SIZE) {
|
||||
value.hasFirstMessage = false;
|
||||
let message = value.messages.shift();
|
||||
if (!message) return;
|
||||
value.size -= message.text?.length ?? 0;
|
||||
this.size -= message.text?.length ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
// @param messages may be modified in addNewMessage due to pass-by-reference fun
|
||||
putRecentMessages(guildId: string, channelId: string, messages: Message[]): void {
|
||||
let size = 0;
|
||||
for (let message of messages) {
|
||||
size += message.text?.length ?? 0;
|
||||
}
|
||||
if (size > Globals.MAX_RAM_CACHED_MESSAGES_CHANNEL_SIZE) return;
|
||||
let id = `s#${guildId}/c#${channelId}`;
|
||||
this.data.set(id, {
|
||||
messages: messages,
|
||||
size: size,
|
||||
hasFirstMessage: true, // will be false if this was ever channel-culled
|
||||
lastUsed: new Date()
|
||||
});
|
||||
this.size += size;
|
||||
this._cullIfNeeded();
|
||||
}
|
||||
|
||||
addNewMessage(guildId: string, message: Message): void {
|
||||
let channelId = message.channel.id;
|
||||
let id = `s#${guildId}/c#${channelId}`;
|
||||
|
||||
let value = this.data.get(id);
|
||||
if (!value) return;
|
||||
|
||||
value.messages.push(message);
|
||||
value.size += message.text?.length ?? 0;
|
||||
this.size += message.text?.length ?? 0;
|
||||
|
||||
this._cullChannelIfNeeded(guildId, channelId);
|
||||
this._cullIfNeeded();
|
||||
}
|
||||
|
||||
updateMessage(guildId: string, oldMessage: Message, newMessage: Message): void {
|
||||
let channelId = oldMessage.channel.id;
|
||||
let id = `s#${guildId}/c#${channelId}`;
|
||||
|
||||
let value = this.data.get(id);
|
||||
if (!value) return;
|
||||
|
||||
let taggedIndex = value.messages.findIndex(cachedMessage => cachedMessage.id == oldMessage.id);
|
||||
|
||||
let [ oldCachedMessage ] = value.messages.splice(taggedIndex, 1, newMessage);
|
||||
value.size -= oldCachedMessage.text?.length ?? 0;
|
||||
value.size += newMessage.text?.length ?? 0;
|
||||
this.size -= oldCachedMessage.text?.length ?? 0;
|
||||
this.size += newMessage.text?.length ?? 0;
|
||||
|
||||
this._cullChannelIfNeeded(guildId, channelId);
|
||||
this._cullIfNeeded();
|
||||
}
|
||||
|
||||
deleteMessage(guildId: string, message: Message): void {
|
||||
let channelId = message.channel.id;
|
||||
let id = `s#${guildId}/c#${channelId}`;
|
||||
|
||||
let value = this.data.get(id);
|
||||
if (!value) return;
|
||||
|
||||
let taggedIndex = value.messages.findIndex(cachedMessage => cachedMessage.id == message.id);
|
||||
|
||||
let [ oldCachedMessage ] = value.messages.splice(taggedIndex, 1);
|
||||
value.size -= oldCachedMessage.text?.length ?? 0;
|
||||
this.size -= oldCachedMessage.text?.length ?? 0;
|
||||
|
||||
// No need to cull since we are always shrinking
|
||||
}
|
||||
|
||||
dropChannel(guildId: string, channelId: string): void {
|
||||
let id = `s#${guildId}/c#${channelId}`;
|
||||
let value = this.data.get(id);
|
||||
if (!value) return;
|
||||
this.size -= value.size;
|
||||
this.data.delete(id);
|
||||
}
|
||||
|
||||
getRecentMessages(guildId: string, channelId: string, number: number): Message[] | null {
|
||||
let id = `s#${guildId}/c#${channelId}`;
|
||||
if (!this.data.has(id)) {
|
||||
//LOG.silly(`recent message cache miss on ${id} requesting ${number}`);
|
||||
return null; // not in the cache
|
||||
}
|
||||
let value = this.data.get(id);
|
||||
if (!value) throw new ShouldNeverHappenError('javascript is not on a single thread >:|');
|
||||
if (!value.hasFirstMessage && value.messages.length < number) {
|
||||
//LOG.silly(`recent message cache incomplete on ${id} requesting ${number}`, { value });
|
||||
return null; // requesting older messages than we have
|
||||
}
|
||||
value.lastUsed = new Date();
|
||||
return value.messages.slice(-number);
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ export default class Actions {
|
||||
errorIndicatorAddFunc: async (errorIndicatorElement) => {
|
||||
await ui.setMembersErrorIndicator(guild, errorIndicatorElement);
|
||||
},
|
||||
errorContainer: q.$('#server-members'),
|
||||
errorContainer: q.$('#guild-members'),
|
||||
errorMessage: 'Error loading members'
|
||||
});
|
||||
}
|
||||
|
@ -373,72 +373,6 @@ export interface Changes<T> {
|
||||
deleted: T[]
|
||||
}
|
||||
|
||||
|
||||
// Old Garbage
|
||||
export class ServerConfig {
|
||||
private constructor(
|
||||
public readonly guildId: string,
|
||||
public readonly url: string,
|
||||
public readonly serverCert: string,
|
||||
public readonly memberId: string,
|
||||
public readonly publicKey: string,
|
||||
public readonly privateKey: string,
|
||||
public readonly source?: any
|
||||
) {}
|
||||
|
||||
public static fromDBData(dataServerConfig: any): ServerConfig {
|
||||
return new ServerConfig(
|
||||
dataServerConfig.server_id,
|
||||
dataServerConfig.url,
|
||||
dataServerConfig.server_cert,
|
||||
dataServerConfig.member_id,
|
||||
dataServerConfig.public_key,
|
||||
dataServerConfig.private_key,
|
||||
dataServerConfig
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerMetaData {
|
||||
private constructor(
|
||||
public readonly name: string,
|
||||
public readonly iconResourceId: string,
|
||||
public readonly source?: any
|
||||
) {}
|
||||
|
||||
public static fromServerDBData(data: any): ServerMetaData {
|
||||
return new ServerMetaData(
|
||||
data.name,
|
||||
data.icon_resource_id,
|
||||
data
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class CacheServerData {
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly url: string,
|
||||
public readonly cert: string,
|
||||
public readonly name: string,
|
||||
public readonly iconResourceId: string | null,
|
||||
public readonly memberId: string,
|
||||
public readonly source?: any
|
||||
) {}
|
||||
|
||||
public static fromDBData(data: any): CacheServerData {
|
||||
return new CacheServerData(
|
||||
data.id,
|
||||
data.url,
|
||||
data.cert,
|
||||
data.name,
|
||||
data.icon_resource_id,
|
||||
data.member_id,
|
||||
data
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConnectionInfo {
|
||||
id: string | null;
|
||||
avatarResourceId: string | null;
|
||||
|
@ -8,7 +8,7 @@ import Q from '../q-module';
|
||||
import CombinedGuild from '../guild-combined';
|
||||
|
||||
export default function createChannel(document: Document, q: Q, ui: UI, guild: CombinedGuild, channel: Channel) {
|
||||
let element = q.create({ class: 'channel text', 'meta-id': channel.id, 'meta-server-id': guild.id, content: [
|
||||
let element = q.create({ class: 'channel text', 'meta-id': channel.id, 'meta-guild-id': guild.id, content: [
|
||||
// Scraped directly from discord (#)
|
||||
{ class: 'icon', content: BaseElements.TEXT_CHANNEL_ICON },
|
||||
{ class: 'name', content: channel.name },
|
||||
|
@ -10,23 +10,23 @@ import UI from '../ui';
|
||||
import GuildsManager from '../guilds-manager';
|
||||
import CombinedGuild from '../guild-combined';
|
||||
|
||||
export default function createServerContextMenu(document: Document, q: Q, ui: UI, guildsManager: GuildsManager, guild: CombinedGuild) {
|
||||
export default function createGuildContextMenu(document: Document, q: Q, ui: UI, guildsManager: GuildsManager, guild: CombinedGuild) {
|
||||
let element = BaseElements.createContextMenu(document, {
|
||||
class: 'server-context', content: [
|
||||
{ class: 'item red leave-server', content: 'Leave Server' }
|
||||
class: 'guild-context', content: [
|
||||
{ class: 'item red leave-guild', content: 'Leave Guild' }
|
||||
]
|
||||
});
|
||||
|
||||
q.$$$(element, '.leave-server').addEventListener('click', async () => {
|
||||
q.$$$(element, '.leave-guild').addEventListener('click', async () => {
|
||||
element.removeSelf();
|
||||
guild.disconnect();
|
||||
await guildsManager.removeServer(guild);
|
||||
await guildsManager.removeGuild(guild);
|
||||
await ui.removeGuild(guild);
|
||||
let firstServerElement = q.$_('#server-list .server');
|
||||
if (firstServerElement) {
|
||||
firstServerElement.click();
|
||||
let firstGuildElement = q.$_('#guild-list .guild');
|
||||
if (firstGuildElement) {
|
||||
firstGuildElement.click();
|
||||
} else {
|
||||
LOG.warn('no first server element to click on');
|
||||
LOG.warn('no first guild element to click on');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -28,7 +28,7 @@ export default function createImageContextMenu(
|
||||
q.$$$(contextMenu, '.copy-image').innerText = 'Copying...';
|
||||
let nativeImage: electron.NativeImage;
|
||||
if (mime != 'image/png' && mime != 'image/jpeg' && mime != 'image/jpg') {
|
||||
// use sharp to convserver: serverrt to png since nativeImage only supports jpeg/png
|
||||
// use sharp to convert to png since nativeImage only supports jpeg/png
|
||||
nativeImage = electron.nativeImage.createFromBuffer(await sharp(buffer).png().toBuffer());
|
||||
} else {
|
||||
nativeImage = electron.nativeImage.createFromBuffer(buffer);
|
||||
|
@ -1,10 +1,10 @@
|
||||
import Q from '../q-module';
|
||||
import UI from '../ui';
|
||||
import createServerTitleContextMenu from './context-menu-guild-title';
|
||||
import createGuildTitleContextMenu from './context-menu-guild-title';
|
||||
import ElementsUtil from './require/elements-util';
|
||||
|
||||
export default function bindAddServerTitleEvents(document: Document, q: Q, ui: UI) {
|
||||
q.$('#server-name-container').addEventListener('click', () => {
|
||||
export default function bindAddGuildTitleEvents(document: Document, q: Q, ui: UI) {
|
||||
q.$('#guild-name-container').addEventListener('click', () => {
|
||||
if (ui.activeConnection === null) return;
|
||||
if (ui.activeGuild === null) return;
|
||||
if (!ui.activeGuild.isSocketVerified()) return;
|
||||
@ -13,8 +13,8 @@ export default function bindAddServerTitleEvents(document: Document, q: Q, ui: U
|
||||
!ui.activeConnection.privileges.includes('modify_members')
|
||||
) return;
|
||||
|
||||
let contextMenu = createServerTitleContextMenu(document, q, ui, ui.activeGuild);
|
||||
let contextMenu = createGuildTitleContextMenu(document, q, ui, ui.activeGuild);
|
||||
document.body.appendChild(contextMenu);
|
||||
ElementsUtil.alignContextElement(contextMenu, q.$('#server-name-container'), { top: 'bottom', centerX: 'centerX' });
|
||||
ElementsUtil.alignContextElement(contextMenu, q.$('#guild-name-container'), { top: 'bottom', centerX: 'centerX' });
|
||||
});
|
||||
}
|
||||
|
@ -27,10 +27,10 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
|
||||
|
||||
sendingMessage = true;
|
||||
|
||||
let server = ui.activeGuild as CombinedGuild;
|
||||
let guild = ui.activeGuild as CombinedGuild;
|
||||
let channel = ui.activeChannel as Channel;
|
||||
|
||||
if (!server.isSocketVerified()) {
|
||||
if (!guild.isSocketVerified()) {
|
||||
LOG.warn('client attempted to send message while not verified');
|
||||
q.$('#send-error').innerText = 'Not Connected to Server';
|
||||
await ElementsUtil.shakeElement(q.$('#send-error'), 400);
|
||||
@ -46,11 +46,11 @@ export default function bindTextInputEvents(document: Document, q: Q, ui: UI): v
|
||||
return;
|
||||
}
|
||||
|
||||
await ui.lockMessages(server, channel, async () => {
|
||||
await ui.lockMessages(guild, channel, async () => {
|
||||
q.$('#text-input').removeAttribute('contenteditable');
|
||||
q.$('#text-input').classList.add('sending');
|
||||
try {
|
||||
await server.requestSendMessage(channel.id, text);
|
||||
await guild.requestSendMessage(channel.id, text);
|
||||
|
||||
q.$('#send-error').innerText = '';
|
||||
q.$('#text-input').innerText = '';
|
||||
|
@ -98,7 +98,7 @@ export default function createGuildSettingsOverlay(document: Document, q: Q, gui
|
||||
// Set Name
|
||||
if (newName != guildMeta.name) {
|
||||
try {
|
||||
await guild.requestSetServerName(newName);
|
||||
await guild.requestSetGuildName(newName);
|
||||
guildMeta = await guild.fetchMetadata();
|
||||
} catch (e) {
|
||||
LOG.error('error setting new guild name', e);
|
||||
@ -112,7 +112,7 @@ export default function createGuildSettingsOverlay(document: Document, q: Q, gui
|
||||
// Set Icon
|
||||
if (!failed && newIconBuff != null) {
|
||||
try {
|
||||
await guild.requestSetServerIcon(newIconBuff);
|
||||
await guild.requestSetGuildIcon(newIconBuff);
|
||||
newIconBuff = null; // prevent resubmit
|
||||
} catch (e) {
|
||||
LOG.error('error setting new guild icon', e);
|
||||
|
@ -21,7 +21,7 @@ export default class Globals {
|
||||
|
||||
static MAX_CACHED_CHANNEL_MESSAGES = 1000; // the 1000 most recent messages in each text channel are cached (in the sqlite db)
|
||||
|
||||
static MAX_SERVER_RESOURCE_CACHE_SIZE = 1024 * 1024 * 1024; // 1 GB max resource cache per guild
|
||||
static MAX_GUILD_RESOURCE_CACHE_SIZE = 1024 * 1024 * 1024; // 1 GB max resource cache per guild
|
||||
static MAX_CACHED_RESOURCE_SIZE = 1024 * 1024 * 4; // 4 MB is the biggest resource that will be cached
|
||||
|
||||
static MAX_RAM_CACHED_MESSAGES_CHANNEL_CHARACTERS = 1024 * 1024 * 64; // at most, 64 MB of channel
|
||||
|
@ -7,7 +7,7 @@ import * as socketio from 'socket.io-client';
|
||||
import PersonalDBGuild from './guild-personal-db';
|
||||
import RAMGuild from './guild-ram';
|
||||
import SocketGuild from './guild-socket';
|
||||
import { Changes, Channel, ConnectionInfo, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, ServerMetaData, SocketConfig, Token } from './data-types';
|
||||
import { Changes, Channel, ConnectionInfo, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, SocketConfig, Token } from './data-types';
|
||||
|
||||
import MessageRAMCache from "./message-ram-cache";
|
||||
import PersonalDB from "./personal-db";
|
||||
@ -299,11 +299,11 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
await this.socketGuild.requestSetAvatar(avatar);
|
||||
}
|
||||
// TODO: Rename Server -> Guild
|
||||
async requestSetServerName(serverName: string): Promise<void> {
|
||||
await this.socketGuild.requestSetServerName(serverName);
|
||||
async requestSetGuildName(guildName: string): Promise<void> {
|
||||
await this.socketGuild.requestSetGuildName(guildName);
|
||||
}
|
||||
async requestSetServerIcon(serverIcon: Buffer): Promise<void> {
|
||||
await this.socketGuild.requestSetServerIcon(serverIcon);
|
||||
async requestSetGuildIcon(guildIcon: Buffer): Promise<void> {
|
||||
await this.socketGuild.requestSetGuildIcon(guildIcon);
|
||||
}
|
||||
async requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise<void> {
|
||||
await this.socketGuild.requestDoUpdateChannel(channelId, name, flavorText);
|
||||
|
@ -4,7 +4,7 @@ import Logger from '../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import * as socketio from "socket.io-client";
|
||||
import { Channel, GuildMetadata, Member, Message, Resource, ServerMetaData, Token } from "./data-types";
|
||||
import { Channel, GuildMetadata, Member, Message, Resource, Token } from "./data-types";
|
||||
import Globals from './globals';
|
||||
import { Connectable, AsyncRequestable, AsyncGuaranteedFetchable } from './guild-types';
|
||||
import DedupAwaiter from './dedup-awaiter';
|
||||
@ -145,11 +145,11 @@ export default class SocketGuild extends EventEmitter<Connectable> implements As
|
||||
async requestSetAvatar(avatar: Buffer): Promise<void> {
|
||||
await this.query(Globals.DEFAULT_SOCKET_TIMEOUT, 'set-avatar', avatar);
|
||||
}
|
||||
async requestSetServerName(serverName: string): Promise<void> {
|
||||
await this.query(Globals.DEFAULT_SOCKET_TIMEOUT, 'set-name', serverName);
|
||||
async requestSetGuildName(guildName: string): Promise<void> {
|
||||
await this.query(Globals.DEFAULT_SOCKET_TIMEOUT, 'set-name', guildName);
|
||||
}
|
||||
async requestSetServerIcon(serverIcon: Buffer): Promise<void> {
|
||||
await this.query(Globals.DEFAULT_SOCKET_TIMEOUT, 'set-icon', serverIcon);
|
||||
async requestSetGuildIcon(guildIcon: Buffer): Promise<void> {
|
||||
await this.query(Globals.DEFAULT_SOCKET_TIMEOUT, 'set-icon', guildIcon);
|
||||
}
|
||||
async requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise<void> {
|
||||
let _changedChannel = await this.query(Globals.DEFAULT_SOCKET_TIMEOUT, 'update-channel', channelId, name, flavorText);
|
||||
|
@ -45,8 +45,8 @@ export interface AsyncRequestable {
|
||||
requestSetStatus(status: string): Promise<void>;
|
||||
requestSetDisplayName(displayName: string): Promise<void>;
|
||||
requestSetAvatar(avatar: Buffer): Promise<void>;
|
||||
requestSetServerName(serverName: string): Promise<void>;
|
||||
requestSetServerIcon(serverIcon: Buffer): Promise<void>;
|
||||
requestSetGuildName(guildName: string): Promise<void>;
|
||||
requestSetGuildIcon(guildIcon: Buffer): Promise<void>;
|
||||
requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise<void>;
|
||||
requestDoCreateChannel(name: string, flavorText: string | null): Promise<void>;
|
||||
requestDoRevokeToken(token: string): Promise<void>;
|
||||
|
@ -9,8 +9,8 @@ import * as socketio from 'socket.io-client';
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, ServerConfig, SocketConfig, Token } from './data-types';
|
||||
import { IAddServerData } from './elements/overlay-add-server';
|
||||
import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, SocketConfig, Token } from './data-types';
|
||||
import { IAddGuildData } from './elements/overlay-add-guild';
|
||||
import { EventEmitter } from 'tsee';
|
||||
import CombinedGuild from './guild-combined';
|
||||
import PersonalDB from './personal-db';
|
||||
@ -54,7 +54,7 @@ export default class GuildsManager extends EventEmitter<{
|
||||
}
|
||||
|
||||
async _connectFromConfig(guildMetadata: GuildMetadataWithIds, socketConfig: SocketConfig): Promise<CombinedGuild> {
|
||||
LOG.debug(`connecting to server#${guildMetadata.id} at ${socketConfig.url}`);
|
||||
LOG.debug(`connecting to g#${guildMetadata.id} at ${socketConfig.url}`);
|
||||
|
||||
let guild = await CombinedGuild.create(
|
||||
guildMetadata,
|
||||
@ -112,8 +112,8 @@ export default class GuildsManager extends EventEmitter<{
|
||||
});
|
||||
}
|
||||
|
||||
async addNewGuild(serverConfig: IAddServerData, displayName: string, avatarBuff: Buffer): Promise<CombinedGuild> {
|
||||
const { name, url, cert, token } = serverConfig;
|
||||
async addNewGuild(guildConfig: IAddGuildData, displayName: string, avatarBuff: Buffer): Promise<CombinedGuild> {
|
||||
const { name, url, cert, token } = guildConfig;
|
||||
LOG.debug('Adding new server', { name, url, cert, token, displayName, avatarBuff });
|
||||
|
||||
|
||||
@ -132,7 +132,7 @@ export default class GuildsManager extends EventEmitter<{
|
||||
});
|
||||
});
|
||||
try {
|
||||
// Create a new Public/Private key pair to identify ourselves with this server
|
||||
// Create a new Public/Private key pair to identify ourselves with this guild
|
||||
let { publicKey, privateKey } = await new Promise((resolve, reject) => {
|
||||
crypto.generateKeyPair('rsa', { modulusLength: 4096 }, (err, publicKey, privateKey) => {
|
||||
if (err) {
|
||||
@ -178,7 +178,7 @@ export default class GuildsManager extends EventEmitter<{
|
||||
}
|
||||
}
|
||||
|
||||
async removeServer(guild: CombinedGuild): Promise<void> {
|
||||
async removeGuild(guild: CombinedGuild): Promise<void> {
|
||||
await this.personalDB.queueTransaction(async () => {
|
||||
await this.personalDB.removeGuildSockets(guild.id);
|
||||
await this.personalDB.removeGuild(guild.id);
|
||||
|
@ -35,16 +35,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="content">
|
||||
<div id="server-list-container">
|
||||
<div id="server-list"></div>
|
||||
<div id="add-server">
|
||||
<div id="guild-list-container">
|
||||
<div id="guild-list"></div>
|
||||
<div id="add-guild">
|
||||
<div class="pill"></div>
|
||||
<img src="./img/add-server-icon.png">
|
||||
<img src="./img/add-guild-icon.png">
|
||||
</div>
|
||||
</div>
|
||||
<div id="server">
|
||||
<div id="server-sidebar">
|
||||
<div id="server-name-container"><span id="server-name"></span></div>
|
||||
<div id="guild">
|
||||
<div id="guild-sidebar">
|
||||
<div id="guild-name-container"><span id="guild-name"></span></div>
|
||||
<div id="channel-list"></div>
|
||||
<div id="connection" class="hidden member">
|
||||
<div class="icon">
|
||||
@ -87,7 +87,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="server-members"></div>
|
||||
<div id="guild-members"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,11 +13,11 @@ export default class ResourceRAMCache {
|
||||
let id = `s#${guildId}/r#${resource.id}`;
|
||||
this.data.set(id, { resource: resource, lastUsed: new Date() });
|
||||
this.size += resource.data.length;
|
||||
if (this.size > Globals.MAX_SERVER_RESOURCE_CACHE_SIZE) { // TODO: this feature needs to be tested
|
||||
if (this.size > Globals.MAX_GUILD_RESOURCE_CACHE_SIZE) { // TODO: this feature needs to be tested
|
||||
let entries = Array.from(this.data.entries())
|
||||
.map(([ key, value ]) => { return { id: key, value: value }; })
|
||||
.sort((a, b) => b.value.lastUsed.getTime() - a.value.lastUsed.getTime()); // oldest last (for pop)
|
||||
while (this.size > Globals.MAX_SERVER_RESOURCE_CACHE_SIZE) {
|
||||
while (this.size > Globals.MAX_GUILD_RESOURCE_CACHE_SIZE) {
|
||||
let entry = entries.pop();
|
||||
if (entry === undefined) throw new ShouldNeverHappenError('No entry in the array but the ram cache still has a size...');
|
||||
this.data.delete(entry.id);
|
||||
|
@ -50,7 +50,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
#server.privilege-modify_channels .channel:hover .modify,
|
||||
#server.privilege-modify_channels .channel.active .modify {
|
||||
#guild.privilege-modify_channels .channel:hover .modify,
|
||||
#guild.privilege-modify_channels .channel.active .modify {
|
||||
display: unset;
|
||||
}
|
||||
|
@ -47,8 +47,8 @@
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.server-title-context .item .icon img,
|
||||
.server-title-context .item .icon svg {
|
||||
.guild-title-context .item .icon img,
|
||||
.guild-title-context .item .icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
@ -105,7 +105,7 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.content.server {
|
||||
.content.guild {
|
||||
line-height: 1;
|
||||
|
||||
.name:not(:last-child) {
|
||||
|
@ -201,9 +201,9 @@ body > .overlay {
|
||||
}
|
||||
}
|
||||
|
||||
/* Server Settings Overlay */
|
||||
/* guild Settings Overlay */
|
||||
|
||||
.content.server-settings {
|
||||
.content.guild-settings {
|
||||
min-width: 350px;
|
||||
|
||||
.preview {
|
||||
@ -228,9 +228,9 @@ body > .overlay {
|
||||
}
|
||||
}
|
||||
|
||||
/* Add Server Overlay */
|
||||
/* Add guild Overlay */
|
||||
|
||||
.content.add-server {
|
||||
.content.add-guild {
|
||||
min-width: 350px;
|
||||
background-color: $background-secondary;
|
||||
border-radius: 12px;
|
||||
|
@ -1,6 +1,6 @@
|
||||
@import "theme.scss";
|
||||
|
||||
#server-list-container {
|
||||
#guild-list-container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
overflow-y: scroll;
|
||||
@ -12,25 +12,25 @@
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#server-list {
|
||||
#guild-list {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
#server-list::-webkit-scrollbar {
|
||||
#guild-list::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#add-server,
|
||||
#server-list .server {
|
||||
#add-guild,
|
||||
#guild-list .guild {
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#add-server .pill,
|
||||
#server-list .server .pill {
|
||||
#add-guild .pill,
|
||||
#guild-list .guild .pill {
|
||||
background-color: $header-primary;
|
||||
width: 8px;
|
||||
height: 0;
|
||||
@ -40,29 +40,29 @@
|
||||
transition: height .1s ease-in-out;
|
||||
}
|
||||
|
||||
#server-list .server.active .pill {
|
||||
#guild-list .guild.active .pill {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
#server-list .server.unread:not(.active) .pill {
|
||||
#guild-list .guild.unread:not(.active) .pill {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
#add-server:hover .pill,
|
||||
#server-list .server:not(.active):hover .pill {
|
||||
#add-guild:hover .pill,
|
||||
#guild-list .guild:not(.active):hover .pill {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
#add-server img,
|
||||
#server-list .server img {
|
||||
#add-guild img,
|
||||
#guild-list .guild img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
transition: border-radius .1s ease-in-out;
|
||||
}
|
||||
|
||||
#add-server:hover img,
|
||||
#server-list .server:hover img,
|
||||
#server-list .server.active img {
|
||||
#add-guild:hover img,
|
||||
#guild-list .guild:hover img,
|
||||
#guild-list .guild.active img {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
@import "theme.scss";
|
||||
|
||||
#server-members {
|
||||
#guild-members {
|
||||
box-sizing: border-box;
|
||||
flex: none; /* >:| NOT GONNA SHINK BOI */
|
||||
background-color: $background-secondary;
|
||||
@ -10,22 +10,22 @@
|
||||
padding: 8px 0 8px 8px;
|
||||
}
|
||||
|
||||
#server-members .member {
|
||||
#guild-members .member {
|
||||
background-color: $background-secondary;
|
||||
padding: 4px 8px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#server-members .member .name {
|
||||
#guild-members .member .name {
|
||||
width: calc(208px - 40px);
|
||||
}
|
||||
|
||||
#server-members .member .status-circle {
|
||||
#guild-members .member .status-circle {
|
||||
border-color: $background-secondary;
|
||||
}
|
||||
|
||||
#server-members .member:hover {
|
||||
#guild-members .member:hover {
|
||||
background-color: $background-modifier-hover;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
@import "theme.scss";
|
||||
|
||||
#server {
|
||||
#guild {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#server-sidebar {
|
||||
#guild-sidebar {
|
||||
flex: none; /* >:| NOT GONNA SHINK BOI */
|
||||
width: 240px;
|
||||
display: flex;
|
||||
@ -14,7 +14,7 @@
|
||||
border-top-left-radius: 8px;
|
||||
}
|
||||
|
||||
#server-name-container {
|
||||
#guild-name-container {
|
||||
padding: 0 16px;
|
||||
height: 48px;
|
||||
font-weight: 600;
|
||||
@ -25,14 +25,14 @@
|
||||
border-bottom: 1px solid $background-secondary-alt;
|
||||
}
|
||||
|
||||
#server-name {
|
||||
#guild-name {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#server.privilege-modify_profile #server-name,
|
||||
#server.privilege-modify_members #server-name {
|
||||
#guild.privilege-modify_profile #guild-name,
|
||||
#guild.privilege-modify_members #guild-name {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -15,9 +15,9 @@
|
||||
@import "overlays.scss";
|
||||
@import "scrollbars.scss";
|
||||
@import "scrollbars.scss";
|
||||
@import "server-list.scss";
|
||||
@import "server-members.scss";
|
||||
@import "server.scss";
|
||||
@import "guild-list.scss";
|
||||
@import "guild-members.scss";
|
||||
@import "guild.scss";
|
||||
@import "shake.scss";
|
||||
@import "status-circles.scss";
|
||||
@import "title-bar.scss";
|
||||
|
Loading…
Reference in New Issue
Block a user