557 lines
19 KiB
TypeScript
557 lines
19 KiB
TypeScript
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 crypto from 'crypto';
|
|
|
|
import ConcurrentQueue from "../../concurrent-queue/concurrent-queue";
|
|
|
|
import * as sqlite from 'sqlite';
|
|
import * as sqlite3 from 'sqlite3';
|
|
|
|
import { Channel, GuildMetadataWithIds, Member, Message, Resource, SocketConfig } from "./data-types";
|
|
|
|
export default class PersonalDB {
|
|
private transactions = new ConcurrentQueue(1);
|
|
|
|
private constructor(
|
|
private readonly db: sqlite.Database
|
|
) {}
|
|
|
|
public static async create(filePath: string): Promise<PersonalDB> {
|
|
return new PersonalDB(
|
|
await sqlite.open({ driver: sqlite3.Database, filename: filePath })
|
|
);
|
|
}
|
|
|
|
async open(): Promise<void> {
|
|
await this.db.open();
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
await this.db.close();
|
|
}
|
|
|
|
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.transactions.push(async () => {
|
|
try {
|
|
await this.beginTransaction();
|
|
await func();
|
|
await this.commitTransaction();
|
|
} catch (e) {
|
|
await this.rollbackTransaction();
|
|
throw e;
|
|
}
|
|
});
|
|
}
|
|
|
|
async init(): Promise<void> {
|
|
await this.queueTransaction(async () => {
|
|
// NOTE: Guild ID is not shared between instances of corDis
|
|
await this.db.run(`
|
|
CREATE TABLE IF NOT EXISTS guilds (
|
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
|
|
, name TEXT
|
|
, icon_resource_id TEXT
|
|
, member_id TEXT
|
|
)
|
|
`);
|
|
|
|
await this.db.run(`
|
|
CREATE TABLE IF NOT EXISTS guild_sockets (
|
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
|
|
, guild_id INTEGER NOT NULL
|
|
, url TEXT NOT NULL
|
|
, cert TEXT NOT NULL
|
|
, public_key TEXT NOT NULL
|
|
, private_key TEXT NOT NULL
|
|
, FOREIGN KEY (guild_id) REFERENCES guilds(id)
|
|
)
|
|
`);
|
|
|
|
await this.db.run(`
|
|
CREATE TABLE IF NOT EXISTS members (
|
|
id TEXT NOT NULL
|
|
, guild_id INTEGER NOT NULL REFERENCES guilds(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_guild_id_con UNIQUE (id, guild_id)
|
|
)
|
|
`);
|
|
|
|
await this.db.run(`
|
|
CREATE TABLE IF NOT EXISTS channels (
|
|
id TEXT NOT NULL
|
|
, guild_id INTEGER NOT NULL REFERENCES guilds(id)
|
|
, "index" INTEGER NOT NULL
|
|
, name TEXT NOT NULL
|
|
, flavor_text TEXT
|
|
, CONSTRAINT channels_id_guild_id_con UNIQUE (id, guild_id)
|
|
)
|
|
`);
|
|
|
|
await this.db.run(`
|
|
CREATE TABLE IF NOT EXISTS resources (
|
|
id TEXT NOT NULL
|
|
, guild_id INTEGER NOT NULL REFERENCES guilds(id)
|
|
, hash BLOB NOT NULL
|
|
, data BLOB NOT NULL
|
|
, data_size INTEGER NOT NULL
|
|
, last_used INTEGER NOT NULL
|
|
, CONSTRAINT resources_id_guild_id_con UNIQUE (id, guild_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
|
|
, guild_id INTEGER NOT NULL REFERENCES guilds(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
|
|
, "order" TEXT
|
|
, CONSTRAINT messages_id_guild_id_con UNIQUE (id, guild_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)');
|
|
});
|
|
}
|
|
|
|
// dangerous!
|
|
async reset(): Promise<void> {
|
|
await this.queueTransaction(async () => {
|
|
await this.db.run('DROP TABLE IF EXISTS guilds');
|
|
await this.db.run('DROP TABLE IF EXISTS guild_sockets');
|
|
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');
|
|
});
|
|
}
|
|
|
|
// Guilds
|
|
|
|
async addGuild(name: string, iconResourceId: string, memberId: string): Promise<number> {
|
|
let result = await this.db.run(
|
|
`INSERT INTO guilds (name, icon_resource_id, member_id) VALUES (:name, :icon_resource_id, :member_id)`,
|
|
{ ':name': name, ':icon_resource_id': iconResourceId, ':member_id': memberId }
|
|
);
|
|
if (result.changes !== 1) throw new Error('unable to add guild');
|
|
if (result.lastID === undefined) throw new Error('unable to get guild last id');
|
|
return result.lastID as number;
|
|
}
|
|
|
|
async removeGuild(guildId: number): Promise<void> {
|
|
let result = await this.db.run(
|
|
`DELETE FROM guilds WHERE id=:id`,
|
|
{ ':id': guildId }
|
|
);
|
|
if (result?.changes !== 1) throw new Error('unable to remove guild');
|
|
}
|
|
|
|
async updateGuildName(guildId: number, newName: string): Promise<void> {
|
|
let result = await this.db.run(
|
|
`UPDATE guilds SET name=:name WHERE id=:guild_id`,
|
|
{ ':guild_id': guildId, ':name': newName }
|
|
);
|
|
if (result?.changes !== 1) throw new Error('unable to update guild name');
|
|
}
|
|
|
|
async updateGuildIcon(guildId: number, newIconResourceId: string): Promise<void> {
|
|
let result = await this.db.run(
|
|
`UPDATE guilds SET icon_resource_id=:icon_resource_id WHERE id=:guild_id`,
|
|
{ ':guild_id': guildId, ':icon_resource_id': newIconResourceId }
|
|
);
|
|
if (result?.changes !== 1) throw new Error('unable to update guild icon');
|
|
}
|
|
|
|
async fetchGuild(guildId: number): Promise<GuildMetadataWithIds> {
|
|
let result = await this.db.get(
|
|
`SELECT * FROM guilds WHERE id=:id`,
|
|
{ ':id': guildId }
|
|
);
|
|
if (!result) throw new Error('unable to fetch guild');
|
|
return GuildMetadataWithIds.fromDBData(result);
|
|
}
|
|
|
|
async fetchGuilds(): Promise<GuildMetadataWithIds[]> {
|
|
let result = await this.db.all('SELECT * FROM guilds');
|
|
return result.map(dataGuild => GuildMetadataWithIds.fromDBData(dataGuild));
|
|
}
|
|
|
|
// Guild Sockets
|
|
|
|
async addGuildSocket(guildId: number, url: string, cert: string, publicKey: crypto.KeyObject, privateKey: crypto.KeyObject): Promise<number> {
|
|
let publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
|
|
let privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
let result = await this.db.run(
|
|
`INSERT INTO guild_sockets (guild_id, url, cert, public_key, private_key) VALUES (:guild_id, :url, :cert, :public_key, :private_key)`,
|
|
{ ':guild_id': guildId, ':url': url, ':cert': cert, ':public_key': publicKeyPem, ':private_key': privateKeyPem }
|
|
);
|
|
if (result.changes !== 1) throw new Error('unable to add guild');
|
|
return result.lastID as number;
|
|
}
|
|
|
|
async removeGuildSocket(guildId: number, guildSocketId: number): Promise<void> {
|
|
let result = await this.db.run(
|
|
`DELETE FROM guild_sockets WHERE id=:guild_socket_id AND guild_id=:guild_id`,
|
|
{ ':guild_id': guildId, ':guild_socket_id': guildSocketId }
|
|
);
|
|
if (result?.changes !== 1) throw new Error('unable to remove guild');
|
|
}
|
|
|
|
async removeGuildSockets(guildId: number): Promise<void> {
|
|
let result = await this.db.run(
|
|
`DELETE FROM guild_sockets WHERE guild_id=:guild_id`,
|
|
{ ':guild_id': guildId }
|
|
);
|
|
if (result?.changes !== 1) throw new Error('unable to remove guild');
|
|
}
|
|
|
|
async fetchGuildSockets(guildId: number): Promise<SocketConfig[]> {
|
|
let result = await this.db.all(
|
|
`SELECT * FROM guild_sockets WHERE guild_id=:guild_id`,
|
|
{ ':guild_id': guildId }
|
|
);
|
|
return result.map(dataGuildSocket => SocketConfig.fromDBData(dataGuildSocket));
|
|
}
|
|
|
|
async fetchGuildSocket(guildId: number, guildSocketId: number): Promise<SocketConfig> {
|
|
let result = await this.db.get(
|
|
`SELECT * FROM guild_sockets WHERE id=:guild_socket_id AND guild_id=:guild_id`,
|
|
{ ':guild_socket_id': guildSocketId, ':guild_id': guildId }
|
|
);
|
|
if (!result) throw new Error('unable to fetch specific guild socket');
|
|
return SocketConfig.fromDBData(result);
|
|
}
|
|
|
|
// Members
|
|
|
|
async addMembers(guildId: number, members: Member[]): Promise<void> {
|
|
await this.queueTransaction(async () => {
|
|
let stmt = await this.db.prepare(
|
|
`INSERT INTO members (
|
|
id, guild_id, display_name, status, avatar_resource_id
|
|
, role_name, role_color, role_priority, privileges
|
|
) VALUES (
|
|
:id, :guild_id, :display_name, :status, :avatar_resource_id
|
|
, :role_name, :role_color, :role_priority, :privileges
|
|
)`
|
|
);
|
|
for (let member of members) {
|
|
await stmt.run({
|
|
':id': member.id, ':guild_id': guildId, ':display_name': member.displayName, ':status': member.status, ':avatar_resource_id': member.avatarResourceId
|
|
, ':role_name': member.roleName, ':role_color': member.roleColor, ':role_priority': member.rolePriority, ':privileges': member.privileges.join(',')
|
|
});
|
|
}
|
|
await stmt.finalize();
|
|
});
|
|
}
|
|
|
|
async updateMembers(guildId: number, newMembers: Member[]): Promise<void> {
|
|
await this.queueTransaction(async () => {
|
|
let stmt = await this.db.prepare(
|
|
`UPDATE members SET
|
|
display_name=:display_name, status=:status, avatar_resource_id=:avatar_resource_id
|
|
, role_name=:role_name, role_color=:role_color, role_priority=:role_priority, privileges=:privileges
|
|
WHERE
|
|
id=:id AND guild_id=:guild_id`
|
|
);
|
|
for (let member of newMembers) {
|
|
await stmt.run({
|
|
':id': member.id, ':guild_id': guildId, ':display_name': member.displayName, ':status': member.status, ':avatar_resource_id': member.avatarResourceId
|
|
, ':role_name': member.roleName, ':role_color': member.roleColor, ':role_priority': member.rolePriority, ':privileges': member.privileges.join(',')
|
|
});
|
|
}
|
|
await stmt.finalize();
|
|
})
|
|
}
|
|
|
|
async deleteMembers(guildId: number, deletedMembers: Member[]): Promise<void> {
|
|
await this.queueTransaction(async () => {
|
|
let stmt = await this.db.prepare(
|
|
`DELETE FROM members WHERE id=:id AND guild_id=:guild_id`
|
|
);
|
|
for (let member of deletedMembers) {
|
|
await stmt.run({ ':id': member.id, ':guild_id': guildId });
|
|
}
|
|
await stmt.finalize();
|
|
})
|
|
}
|
|
|
|
async fetchMembers(guildId: number): Promise<Member[] | null> {
|
|
let result = await this.db.all('SELECT * FROM members WHERE guild_id=:guild_id', { ':guild_id': guildId });
|
|
if (result.length === 0) return null;
|
|
return result.map(dataMember => Member.fromDBData(dataMember));
|
|
}
|
|
|
|
async clearAllMembersStatus(guildId: number): Promise<void> {
|
|
await this.db.run(`UPDATE members SET status='unknown' WHERE guild_id=:guild_id`, { ':guild_id': guildId });
|
|
}
|
|
|
|
// Channels
|
|
|
|
async addChannels(guildId: number, channels: Channel[]) {
|
|
await this.queueTransaction(async () => {
|
|
let stmt = await this.db.prepare(
|
|
`INSERT INTO channels (
|
|
id, guild_id, "index", name, flavor_text
|
|
) VALUES (
|
|
:id, :guild_id, :index, :name, :flavor_text
|
|
)`
|
|
);
|
|
for (let channel of channels) {
|
|
await stmt.run({
|
|
':id': channel.id, ':guild_id': guildId, ':index': channel.index, ':name': channel.name, ':flavor_text': channel.flavorText
|
|
});
|
|
}
|
|
await stmt.finalize();
|
|
});
|
|
}
|
|
|
|
async updateChannels(guildId: number, newChannels: Channel[]): Promise<void> {
|
|
await this.queueTransaction(async () => {
|
|
let stmt = await this.db.prepare(
|
|
`UPDATE channels SET
|
|
"index"=:index, name=:name, flavor_text=:flavor_text
|
|
WHERE
|
|
id=:id AND guild_id=:guild_id`
|
|
);
|
|
for (let channel of newChannels) {
|
|
await stmt.run({
|
|
':id': channel.id, ':guild_id': guildId, ':index': channel.index, ':name': channel.name, ':flavor_text': channel.flavorText
|
|
});
|
|
}
|
|
await stmt.finalize();
|
|
})
|
|
}
|
|
|
|
async deleteChannels(guildId: number, deletedChannels: Channel[]): Promise<void> {
|
|
await this.queueTransaction(async () => {
|
|
let stmt = await this.db.prepare(
|
|
`DELETE FROM channels WHERE id=:id AND guild_id=:guild_id`
|
|
);
|
|
for (let channel of deletedChannels) {
|
|
await stmt.run({ ':id': channel.id, ':guild_id': guildId });
|
|
}
|
|
await stmt.finalize();
|
|
})
|
|
}
|
|
|
|
async fetchChannels(guildId: number): Promise<Channel[] | null> {
|
|
let result = await this.db.all('SELECT * FROM channels WHERE guild_id=:guild_id', { ':guild_id': guildId });
|
|
if (result.length === 0) return null;
|
|
return result.map(dataMember => Channel.fromDBData(dataMember));
|
|
}
|
|
|
|
// Resources
|
|
|
|
async addResources(guildId: number, resources: Resource[]) {
|
|
await this.queueTransaction(async () => {
|
|
let stmt = await this.db.prepare(
|
|
`INSERT INTO resources (
|
|
id, guild_id, hash, data, data_size, last_used
|
|
) VALUES (
|
|
:id, :guild_id, :hash, :data, :data_size, :last_used
|
|
)`
|
|
);
|
|
for (let resource of resources) {
|
|
await stmt.run({
|
|
':id': resource.id, ':guild_id': guildId, ':hash': resource.hash,
|
|
':data': resource.data, ':data_size': resource.data.length, ':last_used': Date.now()
|
|
});
|
|
}
|
|
await stmt.finalize();
|
|
});
|
|
}
|
|
|
|
async updateResources(guildId: number, newResources: Resource[]): Promise<void> {
|
|
await this.queueTransaction(async () => {
|
|
let stmt = await this.db.prepare(
|
|
`UPDATE resources SET
|
|
hash=:hash, data=:data, data_size=:data_size, last_used=:last_used
|
|
WHERE
|
|
id=:id AND guild_id=:guild_id`
|
|
);
|
|
for (let resource of newResources) {
|
|
await stmt.run({
|
|
':id': resource.id, ':guild_id': guildId, ':hash': resource.hash,
|
|
':data': resource.data, ':data_size': resource.data.length, ':last_used': Date.now()
|
|
});
|
|
}
|
|
await stmt.finalize();
|
|
})
|
|
}
|
|
|
|
async deleteResources(guildId: number, deletedResources: Resource[]): Promise<void> {
|
|
await this.queueTransaction(async () => {
|
|
let stmt = await this.db.prepare(
|
|
`DELETE FROM resources WHERE id=:id AND guild_id=:guild_id`
|
|
);
|
|
for (let resource of deletedResources) {
|
|
await stmt.run({ ':id': resource.id, ':guild_id': guildId });
|
|
}
|
|
await stmt.finalize();
|
|
})
|
|
}
|
|
|
|
async fetchResource(guildId: number, resourceId: string): Promise<Resource | null> {
|
|
let result = await this.db.get(
|
|
`SELECT * FROM resources WHERE id=:id AND guild_id=:guild_id`,
|
|
{ ':id': resourceId, ':guild_id': guildId }
|
|
);
|
|
if (!result) return null;
|
|
return Resource.fromDBData(result);
|
|
}
|
|
|
|
// Messages
|
|
|
|
async addMessages(guildId: number, messages: Message[]) {
|
|
await this.queueTransaction(async () => {
|
|
let stmt = await this.db.prepare(
|
|
`INSERT INTO messages (
|
|
id, guild_id, channel_id, member_id, sent_dtg, text
|
|
, resource_id, resource_name, resource_width, resource_height, resource_preview_id
|
|
, "order"
|
|
) VALUES (
|
|
:id, :guild_id, :channel_id, :member_id, :sent_dtg, :text
|
|
, :resource_id, :resource_name, :resource_width, :resource_height, :resource_preview_id
|
|
, :order
|
|
)
|
|
ON CONFLICT (id, guild_id) DO
|
|
UPDATE SET
|
|
id=:id, guild_id=:guild_id, channel_id=:channel_id, member_id=:member_id, sent_dtg=:sent_dtg,
|
|
text=:text, resource_id=:resource_id, resource_name=:resource_name,
|
|
resource_width=:resource_width, resource_height=:resource_height, resource_preview_id=:resource_preview_id,
|
|
"order"=:order
|
|
`
|
|
);
|
|
for (let message of messages) {
|
|
await stmt.run({
|
|
':id': message.id, ':guild_id': guildId,
|
|
':channel_id': message.channel.id, ':member_id': message.member.id,
|
|
':sent_dtg': message.sent.getTime(), ':text': message.text,
|
|
':resource_id': message.resourceId, ':resource_name': message.resourceName,
|
|
':resource_width': message.resourceWidth, ':resource_height': message.resourceHeight,
|
|
':resource_preview_id': message.resourcePreviewId,
|
|
':order': message._order
|
|
});
|
|
}
|
|
await stmt.finalize();
|
|
});
|
|
}
|
|
|
|
async updateMessages(guildId: number, newMessages: Message[]): Promise<void> {
|
|
await this.queueTransaction(async () => {
|
|
let stmt = await this.db.prepare(
|
|
`UPDATE resources SET
|
|
hash=:hash, data=:data, data_size=:data_size, last_used=:last_used
|
|
channel_id=:channel_id, member_id=:member_id, sent_id=:sent_dtg, text=:text
|
|
, resource_id=:resource_id, :resource_name=resource_name, resource_width=:resource_width
|
|
, resource_height=:resource_height, resource_preview_id=:resource_preview_id
|
|
, "order"=:order
|
|
WHERE
|
|
id=:id AND guild_id=:guild_id`
|
|
);
|
|
for (let message of newMessages) {
|
|
await stmt.run({
|
|
':id': message.id, ':guild_id': guildId,
|
|
':channel_id': message.channel.id, ':member_id': message.member.id,
|
|
':sent_dtg': message.sent.getTime(), ':text': message.text,
|
|
':resource_id': message.resourceId, ':resource_name': message.resourceName,
|
|
':resource_width': message.resourceWidth, ':resource_height': message.resourceHeight,
|
|
':resource_preview_id': message.resourcePreviewId,
|
|
':order': message._order
|
|
});
|
|
}
|
|
await stmt.finalize();
|
|
})
|
|
}
|
|
|
|
async deleteMessages(guildId: number, deletedMessages: Message[]): Promise<void> {
|
|
await this.queueTransaction(async () => {
|
|
let stmt = await this.db.prepare(
|
|
`DELETE FROM resources WHERE id=:id AND guild_id=:guild_id`
|
|
);
|
|
for (let message of deletedMessages) {
|
|
await stmt.run({ ':id': message.id, ':guild_id': guildId });
|
|
}
|
|
await stmt.finalize();
|
|
})
|
|
}
|
|
|
|
async fetchMessagesRecent(guildId: number, channelId: string, number: number): Promise<Message[] | null> {
|
|
let messages = await this.db.all(`
|
|
SELECT * FROM (
|
|
SELECT *
|
|
FROM "messages"
|
|
WHERE "guild_id"=:guild_id AND "channel_id"=:channel_id
|
|
ORDER BY "order" DESC
|
|
LIMIT :number
|
|
) AS "r" ORDER BY "r"."order"
|
|
`, { ':guild_id': guildId, ':channel_id': channelId, ':number': number });
|
|
if (messages.length === 0) return null;
|
|
LOG.debug(`return ${messages.length}/${number} recent messages`);
|
|
return messages.map(dataMessage => Message.fromDBData(dataMessage));
|
|
}
|
|
|
|
async fetchMessagesBefore(guildId: number, channelId: string, messageId: string, number: number): Promise<Message[] | null> {
|
|
let messages = await this.db.all(`
|
|
SELECT * FROM (
|
|
SELECT *
|
|
FROM "messages"
|
|
WHERE
|
|
"guild_id"=:guild_id
|
|
AND "channel_id"=:channel_id
|
|
AND "order" < (SELECT "order" FROM "messages" WHERE "id"=:message_id)
|
|
ORDER BY "order" DESC, "id" DESC
|
|
LIMIT :number
|
|
) AS "r" ORDER BY "r"."order" ASC
|
|
`, { ':guild_id': guildId, ':channel_id': channelId, ':message_id': messageId, ':number': number });
|
|
if (messages.length === 0) return null;
|
|
return messages.map(dataMessage => Message.fromDBData(dataMessage));
|
|
}
|
|
|
|
async fetchMessagesAfter(guildId: number, channelId: string, messageId: string, number: number): Promise<Message[] | null> {
|
|
let messages = await this.db.all(`
|
|
SELECT *
|
|
FROM "messages"
|
|
WHERE
|
|
"guild_id"=:guild_id
|
|
AND "channel_id"=:channel_id
|
|
AND "order" > (SELECT "order" FROM "messages" WHERE "id"=:message_id)
|
|
ORDER BY "order" ASC
|
|
LIMIT :number
|
|
`, { ':guild_id': guildId, ':channel_id': channelId, ':message_id': messageId, ':number': number });
|
|
if (messages.length === 0) return null;
|
|
return messages.map(dataMessage => Message.fromDBData(dataMessage));
|
|
}
|
|
}
|