mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-05-10 20:35:02 +00:00
Reorganize project. Add folder for shared code between backend/dashboard. Switch from jest to ava for tests.
This commit is contained in:
parent
80a82fe348
commit
16111bbe84
162 changed files with 11056 additions and 9900 deletions
45
backend/src/data/AllowedGuilds.ts
Normal file
45
backend/src/data/AllowedGuilds.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { AllowedGuild } from "./entities/AllowedGuild";
|
||||
import {
|
||||
getConnection,
|
||||
getRepository,
|
||||
Repository,
|
||||
Transaction,
|
||||
TransactionManager,
|
||||
TransactionRepository,
|
||||
} from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
|
||||
export class AllowedGuilds extends BaseRepository {
|
||||
private allowedGuilds: Repository<AllowedGuild>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.allowedGuilds = getRepository(AllowedGuild);
|
||||
}
|
||||
|
||||
async isAllowed(guildId) {
|
||||
const count = await this.allowedGuilds.count({
|
||||
where: {
|
||||
id: guildId,
|
||||
},
|
||||
});
|
||||
return count !== 0;
|
||||
}
|
||||
|
||||
getForApiUser(userId) {
|
||||
return this.allowedGuilds
|
||||
.createQueryBuilder("allowed_guilds")
|
||||
.innerJoin(
|
||||
"api_permissions",
|
||||
"api_permissions",
|
||||
"api_permissions.guild_id = allowed_guilds.id AND api_permissions.user_id = :userId",
|
||||
{ userId },
|
||||
)
|
||||
.getMany();
|
||||
}
|
||||
|
||||
updateInfo(id, name, icon, ownerId) {
|
||||
return this.allowedGuilds.update({ id }, { name, icon, owner_id: ownerId });
|
||||
}
|
||||
}
|
90
backend/src/data/ApiLogins.ts
Normal file
90
backend/src/data/ApiLogins.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { ApiLogin } from "./entities/ApiLogin";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import crypto from "crypto";
|
||||
import moment from "moment-timezone";
|
||||
|
||||
// tslint:disable-next-line:no-submodule-imports
|
||||
import uuidv4 from "uuid/v4";
|
||||
import { DBDateFormat } from "../utils";
|
||||
import { log } from "util";
|
||||
|
||||
export class ApiLogins extends BaseRepository {
|
||||
private apiLogins: Repository<ApiLogin>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.apiLogins = getRepository(ApiLogin);
|
||||
}
|
||||
|
||||
async getUserIdByApiKey(apiKey: string): Promise<string | null> {
|
||||
const [loginId, token] = apiKey.split(".");
|
||||
if (!loginId || !token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const login = await this.apiLogins
|
||||
.createQueryBuilder()
|
||||
.where("id = :id", { id: loginId })
|
||||
.andWhere("expires_at > NOW()")
|
||||
.getOne();
|
||||
|
||||
if (!login) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(loginId + token); // Remember to use loginId as the salt
|
||||
const hashedToken = hash.digest("hex");
|
||||
if (hashedToken !== login.token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return login.user_id;
|
||||
}
|
||||
|
||||
async addLogin(userId: string): Promise<string> {
|
||||
// Generate random login id
|
||||
let loginId;
|
||||
while (true) {
|
||||
loginId = uuidv4();
|
||||
const existing = await this.apiLogins.findOne({
|
||||
where: {
|
||||
id: loginId,
|
||||
},
|
||||
});
|
||||
if (!existing) break;
|
||||
}
|
||||
|
||||
// Generate token
|
||||
const token = uuidv4();
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(loginId + token); // Use loginId as a salt
|
||||
const hashedToken = hash.digest("hex");
|
||||
|
||||
// Save this to the DB
|
||||
await this.apiLogins.insert({
|
||||
id: loginId,
|
||||
token: hashedToken,
|
||||
user_id: userId,
|
||||
logged_in_at: moment().format(DBDateFormat),
|
||||
expires_at: moment()
|
||||
.add(1, "day")
|
||||
.format(DBDateFormat),
|
||||
});
|
||||
|
||||
return `${loginId}.${token}`;
|
||||
}
|
||||
|
||||
expireApiKey(apiKey) {
|
||||
const [loginId, token] = apiKey.split(".");
|
||||
if (!loginId || !token) return;
|
||||
|
||||
return this.apiLogins.update(
|
||||
{ id: loginId },
|
||||
{
|
||||
expires_at: moment().format(DBDateFormat),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
29
backend/src/data/ApiPermissions.ts
Normal file
29
backend/src/data/ApiPermissions.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { ApiPermission } from "./entities/ApiPermission";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
|
||||
export class ApiPermissions extends BaseRepository {
|
||||
private apiPermissions: Repository<ApiPermission>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.apiPermissions = getRepository(ApiPermission);
|
||||
}
|
||||
|
||||
getByUserId(userId) {
|
||||
return this.apiPermissions.find({
|
||||
where: {
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getByGuildAndUserId(guildId, userId) {
|
||||
return this.apiPermissions.findOne({
|
||||
where: {
|
||||
guild_id: guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
6
backend/src/data/ApiRoles.ts
Normal file
6
backend/src/data/ApiRoles.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export enum ApiRoles {
|
||||
Viewer = 1,
|
||||
Editor,
|
||||
Manager,
|
||||
ServerOwner,
|
||||
}
|
38
backend/src/data/ApiUserInfo.ts
Normal file
38
backend/src/data/ApiUserInfo.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { ApiUserInfo as ApiUserInfoEntity, ApiUserInfoData } from "./entities/ApiUserInfo";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { connection } from "./db";
|
||||
import moment from "moment-timezone";
|
||||
import { DBDateFormat } from "../utils";
|
||||
|
||||
export class ApiUserInfo extends BaseRepository {
|
||||
private apiUserInfo: Repository<ApiUserInfoEntity>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.apiUserInfo = getRepository(ApiUserInfoEntity);
|
||||
}
|
||||
|
||||
get(id) {
|
||||
return this.apiUserInfo.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
update(id, data: ApiUserInfoData) {
|
||||
return connection.transaction(async entityManager => {
|
||||
const repo = entityManager.getRepository(ApiUserInfoEntity);
|
||||
|
||||
const existingInfo = await repo.findOne({ where: { id } });
|
||||
const updatedAt = moment().format(DBDateFormat);
|
||||
|
||||
if (existingInfo) {
|
||||
await repo.update({ id }, { data, updated_at: updatedAt });
|
||||
} else {
|
||||
await repo.insert({ id, data, updated_at: updatedAt });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
28
backend/src/data/BaseGuildRepository.ts
Normal file
28
backend/src/data/BaseGuildRepository.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { BaseRepository } from "./BaseRepository";
|
||||
|
||||
export class BaseGuildRepository extends BaseRepository {
|
||||
private static guildInstances: Map<string, any>;
|
||||
|
||||
protected guildId: string;
|
||||
|
||||
constructor(guildId: string) {
|
||||
super();
|
||||
this.guildId = guildId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cached instance of the inheriting class for the specified guildId,
|
||||
* or creates a new instance if one doesn't exist yet
|
||||
*/
|
||||
public static getGuildInstance<T extends typeof BaseGuildRepository>(this: T, guildId: string): InstanceType<T> {
|
||||
if (!this.guildInstances) {
|
||||
this.guildInstances = new Map();
|
||||
}
|
||||
|
||||
if (!this.guildInstances.has(guildId)) {
|
||||
this.guildInstances.set(guildId, new this(guildId));
|
||||
}
|
||||
|
||||
return this.guildInstances.get(guildId) as InstanceType<T>;
|
||||
}
|
||||
}
|
30
backend/src/data/BaseRepository.ts
Normal file
30
backend/src/data/BaseRepository.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
export class BaseRepository {
|
||||
private nextRelations: string[];
|
||||
|
||||
constructor() {
|
||||
this.nextRelations = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Primes the specified relation(s) to be used in the next database operation.
|
||||
* Can be chained.
|
||||
*/
|
||||
public with(relations: string | string[]): this {
|
||||
if (Array.isArray(relations)) {
|
||||
this.nextRelations.push(...relations);
|
||||
} else {
|
||||
this.nextRelations.push(relations);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and resets the relations primed using with()
|
||||
*/
|
||||
protected getRelations(): string[] {
|
||||
const relations = this.nextRelations || [];
|
||||
this.nextRelations = [];
|
||||
return relations;
|
||||
}
|
||||
}
|
12
backend/src/data/CaseTypeColors.ts
Normal file
12
backend/src/data/CaseTypeColors.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { CaseTypes } from "./CaseTypes";
|
||||
|
||||
export const CaseTypeColors = {
|
||||
[CaseTypes.Note]: 0x3498db,
|
||||
[CaseTypes.Warn]: 0xdae622,
|
||||
[CaseTypes.Mute]: 0xe6b122,
|
||||
[CaseTypes.Unmute]: 0xa175b3,
|
||||
[CaseTypes.Kick]: 0xe67e22,
|
||||
[CaseTypes.Softban]: 0xe67e22,
|
||||
[CaseTypes.Ban]: 0xcb4314,
|
||||
[CaseTypes.Unban]: 0x9b59b6,
|
||||
};
|
11
backend/src/data/CaseTypes.ts
Normal file
11
backend/src/data/CaseTypes.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
export enum CaseTypes {
|
||||
Ban = 1,
|
||||
Unban,
|
||||
Note,
|
||||
Warn,
|
||||
Kick,
|
||||
Mute,
|
||||
Unmute,
|
||||
Expunged,
|
||||
Softban,
|
||||
}
|
74
backend/src/data/Configs.ts
Normal file
74
backend/src/data/Configs.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { Config } from "./entities/Config";
|
||||
import {
|
||||
getConnection,
|
||||
getRepository,
|
||||
Repository,
|
||||
Transaction,
|
||||
TransactionManager,
|
||||
TransactionRepository,
|
||||
} from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { connection } from "./db";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
|
||||
export class Configs extends BaseRepository {
|
||||
private configs: Repository<Config>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.configs = getRepository(Config);
|
||||
}
|
||||
|
||||
getActiveByKey(key) {
|
||||
return this.configs.findOne({
|
||||
where: {
|
||||
key,
|
||||
is_active: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getHighestId(): Promise<number> {
|
||||
const rows = await connection.query("SELECT MAX(id) AS highest_id FROM configs");
|
||||
return (rows.length && rows[0].highest_id) || 0;
|
||||
}
|
||||
|
||||
getActiveLargerThanId(id) {
|
||||
return this.configs
|
||||
.createQueryBuilder()
|
||||
.where("id > :id", { id })
|
||||
.andWhere("is_active = 1")
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async hasConfig(key) {
|
||||
return (await this.getActiveByKey(key)) != null;
|
||||
}
|
||||
|
||||
getRevisions(key, num = 10) {
|
||||
return this.configs.find({
|
||||
relations: this.getRelations(),
|
||||
where: { key },
|
||||
select: ["id", "key", "is_active", "edited_by", "edited_at"],
|
||||
order: {
|
||||
edited_at: "DESC",
|
||||
},
|
||||
take: num,
|
||||
});
|
||||
}
|
||||
|
||||
async saveNewRevision(key, config, editedBy) {
|
||||
return connection.transaction(async entityManager => {
|
||||
const repo = entityManager.getRepository(Config);
|
||||
// Mark all old revisions inactive
|
||||
await repo.update({ key }, { is_active: false });
|
||||
// Add new, active revision
|
||||
await repo.insert({
|
||||
key,
|
||||
config,
|
||||
is_active: true,
|
||||
edited_by: editedBy,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
62
backend/src/data/DefaultLogMessages.json
Normal file
62
backend/src/data/DefaultLogMessages.json
Normal file
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"MEMBER_WARN": "⚠️ {userMention(member)} was warned by {userMention(mod)}",
|
||||
"MEMBER_MUTE": "🔇 {userMention(user)} was muted indefinitely by {userMention(mod)}",
|
||||
"MEMBER_TIMED_MUTE": "🔇 {userMention(user)} was muted for **{time}** by {userMention(mod)}",
|
||||
"MEMBER_UNMUTE": "🔊 {userMention(user)} was unmuted by {userMention(mod)}",
|
||||
"MEMBER_TIMED_UNMUTE": "🔊 {userMention(user)} was scheduled to be unmuted in **{time}** by {userMention(mod)}",
|
||||
"MEMBER_MUTE_EXPIRED": "🔊 {userMention(member)}'s mute expired",
|
||||
"MEMBER_KICK": "👢 {userMention(user)} was kicked by {userMention(mod)}",
|
||||
"MEMBER_BAN": "🔨 {userMention(user)} was banned by {userMention(mod)}",
|
||||
"MEMBER_UNBAN": "🔓 User (`{userId}`) was unbanned by {userMention(mod)}",
|
||||
"MEMBER_FORCEBAN": "🔨 User (`{userId}`) was forcebanned by {userMention(mod)}",
|
||||
"MEMBER_SOFTBAN": "🔨 {userMention(member)} was softbanned by {userMention(mod)}",
|
||||
"MEMBER_JOIN": "📥 {userMention(member)} joined{new} (created {account_age} ago)",
|
||||
"MEMBER_LEAVE": "📤 {userMention(member)} left the server",
|
||||
"MEMBER_ROLE_ADD": "🔑 {userMention(member)}: role(s) **{roles}** added by {userMention(mod)}",
|
||||
"MEMBER_ROLE_REMOVE": "🔑 {userMention(member)}: role(s) **{roles}** removed by {userMention(mod)}",
|
||||
"MEMBER_ROLE_CHANGES": "🔑 {userMention(member)}: roles changed: added **{addedRoles}**, removed **{removedRoles}** by {userMention(mod)}",
|
||||
"MEMBER_NICK_CHANGE": "✏ {userMention(member)}: nickname changed from **{oldNick}** to **{newNick}**",
|
||||
"MEMBER_USERNAME_CHANGE": "✏ {userMention(user)}: username changed from **{oldName}** to **{newName}**",
|
||||
"MEMBER_RESTORE": "💿 Restored {restoredData} for {userMention(member)} on rejoin",
|
||||
|
||||
"CHANNEL_CREATE": "🖊 Channel {channelMention(channel)} was created",
|
||||
"CHANNEL_DELETE": "🗑 Channel {channelMention(channel)} was deleted",
|
||||
"CHANNEL_EDIT": "✏ Channel {channelMention(channel)} was edited",
|
||||
|
||||
"ROLE_CREATE": "🖊 Role **{role.name}** (`{role.id}`) was created",
|
||||
"ROLE_DELETE": "🖊 Role **{role.name}** (`{role.id}`) was deleted",
|
||||
"ROLE_EDIT": "🖊 Role **{role.name}** (`{role.id}`) was edited",
|
||||
|
||||
"MESSAGE_EDIT": "✏ {userMention(user)} edited their message (`{after.id}`) in {channelMention(channel)}:\n**Before:**{messageSummary(before)}**After:**{messageSummary(after)}",
|
||||
"MESSAGE_DELETE": "🗑 Message (`{message.id}`) from {userMention(user)} deleted in {channelMention(channel)} (originally posted at **{messageDate}**):{messageSummary(message)}",
|
||||
"MESSAGE_DELETE_BULK": "🗑 **{count}** messages deleted in {channelMention(channel)} ({archiveUrl})",
|
||||
"MESSAGE_DELETE_BARE": "🗑 Message (`{messageId}`) deleted in {channelMention(channel)} (no more info available)",
|
||||
|
||||
"VOICE_CHANNEL_JOIN": "🎙 🔵 {userMention(member)} joined **{channel.name}**",
|
||||
"VOICE_CHANNEL_MOVE": "🎙 ↔ {userMention(member)} moved from **{oldChannel.name}** to **{newChannel.name}**",
|
||||
"VOICE_CHANNEL_LEAVE": "🎙 🔴 {userMention(member)} left **{channel.name}**",
|
||||
"VOICE_CHANNEL_FORCE_MOVE": "\uD83C\uDF99 ✍ {userMention(member)} was moved from **{oldChannel.name}** to **{newChannel.name}** by {userMention(mod)}",
|
||||
|
||||
"COMMAND": "🤖 {userMention(member)} used command in {channelMention(channel)}:\n`{command}`",
|
||||
|
||||
"MESSAGE_SPAM_DETECTED": "🛑 {userMention(member)} spam detected in {channelMention(channel)}: {description} (more than {limit} in {interval}s)\n{archiveUrl}",
|
||||
"OTHER_SPAM_DETECTED": "🛑 {userMention(member)} spam detected: {description} (more than {limit} in {interval}s)",
|
||||
"CENSOR": "🛑 Censored message (`{message.id}`) from {userMention(user)} in {channelMention(channel)}: {reason}:\n```{messageText}```",
|
||||
"CLEAN": "🚿 {userMention(mod)} cleaned **{count}** message(s) in {channelMention(channel)}\n{archiveUrl}",
|
||||
|
||||
"CASE_CREATE": "✏ {userMention(mod)} manually created new **{caseType}** case (#{caseNum})",
|
||||
|
||||
"MASSBAN": "⚒ {userMention(mod)} massbanned {count} users",
|
||||
|
||||
"MEMBER_JOIN_WITH_PRIOR_RECORDS": "⚠ {userMention(member)} joined with prior records. Recent cases:\n{recentCaseSummary}",
|
||||
|
||||
"CASE_UPDATE": "✏ {userMention(mod)} updated case #{caseNumber} ({caseType}) with note:\n```{note}```",
|
||||
|
||||
"MEMBER_MUTE_REJOIN": "⚠ Reapplied active mute for {userMention(member)} on rejoin",
|
||||
|
||||
"SCHEDULED_MESSAGE": "⏰ {userMention(author)} scheduled a message to be posted to {channelMention(channel)} on {date} at {time} (UTC)",
|
||||
"POSTED_SCHEDULED_MESSAGE": "\uD83D\uDCE8 Posted scheduled message (`{messageId}`) to {channelMention(channel)} as scheduled by {userMention(author)}",
|
||||
|
||||
"BOT_ALERT": "⚠ {tmplEval(body)}",
|
||||
"AUTOMOD_ACTION": "\uD83E\uDD16 Automod rule **{rule}** triggered by {userMention(user)}. Actions taken: **{actionsTaken}**\n{matchSummary}"
|
||||
}
|
100
backend/src/data/GuildArchives.ts
Normal file
100
backend/src/data/GuildArchives.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
import uuid from "uuid/v4"; // tslint:disable-line
|
||||
import moment from "moment-timezone";
|
||||
import { ArchiveEntry } from "./entities/ArchiveEntry";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { trimLines } from "../utils";
|
||||
import { SavedMessage } from "./entities/SavedMessage";
|
||||
import { Channel, Guild, User } from "eris";
|
||||
import { renderTemplate } from "../templateFormatter";
|
||||
|
||||
const DEFAULT_EXPIRY_DAYS = 30;
|
||||
|
||||
const MESSAGE_ARCHIVE_HEADER_FORMAT = trimLines(`
|
||||
Server: {guild.name} ({guild.id})
|
||||
`);
|
||||
const MESSAGE_ARCHIVE_MESSAGE_FORMAT =
|
||||
"[#{channel.name}] [{user.id}] [{timestamp}] {user.username}#{user.discriminator}: {content}{attachments}";
|
||||
|
||||
export class GuildArchives extends BaseGuildRepository {
|
||||
protected archives: Repository<ArchiveEntry>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.archives = getRepository(ArchiveEntry);
|
||||
|
||||
// Clean expired archives at start and then every hour
|
||||
this.deleteExpiredArchives();
|
||||
setInterval(() => this.deleteExpiredArchives(), 1000 * 60 * 60);
|
||||
}
|
||||
|
||||
private deleteExpiredArchives() {
|
||||
this.archives
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere("expires_at IS NOT NULL")
|
||||
.andWhere("expires_at <= NOW()")
|
||||
.delete()
|
||||
.execute();
|
||||
}
|
||||
|
||||
async find(id: string): Promise<ArchiveEntry> {
|
||||
return this.archives.findOne({
|
||||
where: { id },
|
||||
relations: this.getRelations(),
|
||||
});
|
||||
}
|
||||
|
||||
async makePermanent(id: string): Promise<void> {
|
||||
await this.archives.update(
|
||||
{ id },
|
||||
{
|
||||
expires_at: null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns ID of the created entry
|
||||
*/
|
||||
async create(body: string, expiresAt: moment.Moment = null): Promise<string> {
|
||||
if (!expiresAt) {
|
||||
expiresAt = moment().add(DEFAULT_EXPIRY_DAYS, "days");
|
||||
}
|
||||
|
||||
const result = await this.archives.insert({
|
||||
guild_id: this.guildId,
|
||||
body,
|
||||
expires_at: expiresAt.format("YYYY-MM-DD HH:mm:ss"),
|
||||
});
|
||||
|
||||
return result.identifiers[0].id;
|
||||
}
|
||||
|
||||
async createFromSavedMessages(savedMessages: SavedMessage[], guild: Guild, expiresAt = null) {
|
||||
if (expiresAt == null) expiresAt = moment().add(DEFAULT_EXPIRY_DAYS, "days");
|
||||
|
||||
const headerStr = await renderTemplate(MESSAGE_ARCHIVE_HEADER_FORMAT, { guild });
|
||||
const msgLines = [];
|
||||
for (const msg of savedMessages) {
|
||||
const channel = guild.channels.get(msg.channel_id);
|
||||
const user = { ...msg.data.author, id: msg.user_id };
|
||||
|
||||
const line = await renderTemplate(MESSAGE_ARCHIVE_MESSAGE_FORMAT, {
|
||||
id: msg.id,
|
||||
timestamp: moment(msg.posted_at).format("YYYY-MM-DD HH:mm:ss"),
|
||||
content: msg.data.content,
|
||||
user,
|
||||
channel,
|
||||
});
|
||||
msgLines.push(line);
|
||||
}
|
||||
const messagesStr = msgLines.join("\n");
|
||||
|
||||
return this.create([headerStr, messagesStr].join("\n\n"), expiresAt);
|
||||
}
|
||||
|
||||
getUrl(baseUrl, archiveId) {
|
||||
return baseUrl ? `${baseUrl}/archives/${archiveId}` : `Archive ID: ${archiveId}`;
|
||||
}
|
||||
}
|
57
backend/src/data/GuildAutoReactions.ts
Normal file
57
backend/src/data/GuildAutoReactions.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { AutoReaction } from "./entities/AutoReaction";
|
||||
|
||||
export class GuildAutoReactions extends BaseGuildRepository {
|
||||
private autoReactions: Repository<AutoReaction>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.autoReactions = getRepository(AutoReaction);
|
||||
}
|
||||
|
||||
async all(): Promise<AutoReaction[]> {
|
||||
return this.autoReactions.find({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getForChannel(channelId: string): Promise<AutoReaction> {
|
||||
return this.autoReactions.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async removeFromChannel(channelId: string) {
|
||||
await this.autoReactions.delete({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
});
|
||||
}
|
||||
|
||||
async set(channelId: string, reactions: string[]) {
|
||||
const existingRecord = await this.getForChannel(channelId);
|
||||
if (existingRecord) {
|
||||
this.autoReactions.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
},
|
||||
{
|
||||
reactions,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await this.autoReactions.insert({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
reactions,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
155
backend/src/data/GuildCases.ts
Normal file
155
backend/src/data/GuildCases.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
import { Case } from "./entities/Case";
|
||||
import { CaseNote } from "./entities/CaseNote";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, In, Repository } from "typeorm";
|
||||
import { disableLinkPreviews } from "../utils";
|
||||
import { CaseTypes } from "./CaseTypes";
|
||||
import moment = require("moment-timezone");
|
||||
|
||||
const CASE_SUMMARY_REASON_MAX_LENGTH = 300;
|
||||
|
||||
export class GuildCases extends BaseGuildRepository {
|
||||
private cases: Repository<Case>;
|
||||
private caseNotes: Repository<CaseNote>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.cases = getRepository(Case);
|
||||
this.caseNotes = getRepository(CaseNote);
|
||||
}
|
||||
|
||||
async get(ids: number[]): Promise<Case[]> {
|
||||
return this.cases.find({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
id: In(ids),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async find(id: number): Promise<Case> {
|
||||
return this.cases.findOne({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findByCaseNumber(caseNumber: number): Promise<Case> {
|
||||
return this.cases.findOne({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
case_number: caseNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findLatestByModId(modId: string): Promise<Case> {
|
||||
return this.cases.findOne({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
mod_id: modId,
|
||||
},
|
||||
order: {
|
||||
case_number: "DESC",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findByAuditLogId(auditLogId: string): Promise<Case> {
|
||||
return this.cases.findOne({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
audit_log_id: auditLogId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByUserId(userId: string): Promise<Case[]> {
|
||||
return this.cases.find({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getRecentByModId(modId: string, count: number): Promise<Case[]> {
|
||||
return this.cases.find({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
mod_id: modId,
|
||||
is_hidden: 0,
|
||||
},
|
||||
take: count,
|
||||
order: {
|
||||
case_number: "DESC",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async setHidden(id: number, hidden: boolean): Promise<void> {
|
||||
await this.cases.update(
|
||||
{ id },
|
||||
{
|
||||
is_hidden: hidden,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async create(data): Promise<Case> {
|
||||
const result = await this.cases.insert({
|
||||
...data,
|
||||
guild_id: this.guildId,
|
||||
case_number: () => `(SELECT IFNULL(MAX(case_number)+1, 1) FROM cases AS ma2 WHERE guild_id = ${this.guildId})`,
|
||||
});
|
||||
|
||||
return this.find(result.identifiers[0].id);
|
||||
}
|
||||
|
||||
update(id, data) {
|
||||
return this.cases.update(id, data);
|
||||
}
|
||||
|
||||
async createNote(caseId: number, data: any): Promise<void> {
|
||||
await this.caseNotes.insert({
|
||||
...data,
|
||||
case_id: caseId,
|
||||
});
|
||||
}
|
||||
|
||||
getSummaryText(theCase: Case) {
|
||||
const firstNote = theCase.notes[0];
|
||||
let reason = firstNote ? firstNote.body : "";
|
||||
|
||||
if (reason.length > CASE_SUMMARY_REASON_MAX_LENGTH) {
|
||||
const match = reason.slice(CASE_SUMMARY_REASON_MAX_LENGTH, 100).match(/(?:[.,!?\s]|$)/);
|
||||
const nextWhitespaceIndex = match ? CASE_SUMMARY_REASON_MAX_LENGTH + match.index : CASE_SUMMARY_REASON_MAX_LENGTH;
|
||||
if (nextWhitespaceIndex < reason.length) {
|
||||
reason = reason.slice(0, nextWhitespaceIndex - 1) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
reason = disableLinkPreviews(reason);
|
||||
|
||||
const timestamp = moment(theCase.created_at).format("YYYY-MM-DD");
|
||||
let line = `\`[${timestamp}]\` \`Case #${theCase.case_number}\` __${CaseTypes[theCase.type]}__ ${reason}`;
|
||||
if (theCase.notes.length > 1) {
|
||||
line += ` *(+${theCase.notes.length - 1} ${theCase.notes.length === 2 ? "note" : "notes"})*`;
|
||||
}
|
||||
|
||||
if (theCase.is_hidden) {
|
||||
line += " *(hidden)*";
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
}
|
42
backend/src/data/GuildEvents.ts
Normal file
42
backend/src/data/GuildEvents.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { QueuedEventEmitter } from "../QueuedEventEmitter";
|
||||
|
||||
export class GuildEvents extends BaseGuildRepository {
|
||||
private queuedEventEmitter: QueuedEventEmitter;
|
||||
private pluginListeners: Map<string, Map<string, any[]>>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.queuedEventEmitter = new QueuedEventEmitter();
|
||||
}
|
||||
|
||||
public on(pluginName: string, eventName: string, fn) {
|
||||
this.queuedEventEmitter.on(eventName, fn);
|
||||
|
||||
if (!this.pluginListeners.has(pluginName)) {
|
||||
this.pluginListeners.set(pluginName, new Map());
|
||||
}
|
||||
|
||||
const pluginListeners = this.pluginListeners.get(pluginName);
|
||||
if (!pluginListeners.has(eventName)) {
|
||||
pluginListeners.set(eventName, []);
|
||||
}
|
||||
|
||||
const pluginEventListeners = pluginListeners.get(eventName);
|
||||
pluginEventListeners.push(fn);
|
||||
}
|
||||
|
||||
public offPlugin(pluginName: string) {
|
||||
const pluginListeners = this.pluginListeners.get(pluginName) || new Map();
|
||||
for (const [eventName, listeners] of Array.from(pluginListeners.entries())) {
|
||||
for (const listener of listeners) {
|
||||
this.queuedEventEmitter.off(eventName, listener);
|
||||
}
|
||||
}
|
||||
this.pluginListeners.delete(pluginName);
|
||||
}
|
||||
|
||||
public emit(eventName: string, args: any[] = []) {
|
||||
return this.queuedEventEmitter.emit(eventName, args);
|
||||
}
|
||||
}
|
55
backend/src/data/GuildLogs.ts
Normal file
55
backend/src/data/GuildLogs.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import EventEmitter from "events";
|
||||
import { LogType } from "./LogType";
|
||||
|
||||
// Use the same instance for the same guild, even if a new instance is created
|
||||
const guildInstances: Map<string, GuildLogs> = new Map();
|
||||
|
||||
interface IIgnoredLog {
|
||||
type: LogType;
|
||||
ignoreId: any;
|
||||
}
|
||||
|
||||
export class GuildLogs extends EventEmitter {
|
||||
protected guildId: string;
|
||||
protected ignoredLogs: IIgnoredLog[];
|
||||
|
||||
constructor(guildId) {
|
||||
if (guildInstances.has(guildId)) {
|
||||
// Return existing instance for this guild if one exists
|
||||
return guildInstances.get(guildId);
|
||||
}
|
||||
|
||||
super();
|
||||
this.guildId = guildId;
|
||||
this.ignoredLogs = [];
|
||||
|
||||
// Store the instance for this guild so it can be returned later if a new instance for this guild is requested
|
||||
guildInstances.set(guildId, this);
|
||||
}
|
||||
|
||||
log(type: LogType, data: any, ignoreId = null) {
|
||||
if (ignoreId && this.isLogIgnored(type, ignoreId)) {
|
||||
this.clearIgnoredLog(type, ignoreId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit("log", { type, data });
|
||||
}
|
||||
|
||||
ignoreLog(type: LogType, ignoreId: any, timeout: number = null) {
|
||||
this.ignoredLogs.push({ type, ignoreId });
|
||||
|
||||
// Clear after expiry (15sec by default)
|
||||
setTimeout(() => {
|
||||
this.clearIgnoredLog(type, ignoreId);
|
||||
}, timeout || 1000 * 15);
|
||||
}
|
||||
|
||||
isLogIgnored(type: LogType, ignoreId: any) {
|
||||
return this.ignoredLogs.some(info => type === info.type && ignoreId === info.ignoreId);
|
||||
}
|
||||
|
||||
clearIgnoredLog(type: LogType, ignoreId: any) {
|
||||
this.ignoredLogs.splice(this.ignoredLogs.findIndex(info => type === info.type && ignoreId === info.ignoreId), 1);
|
||||
}
|
||||
}
|
101
backend/src/data/GuildMutes.ts
Normal file
101
backend/src/data/GuildMutes.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import moment from "moment-timezone";
|
||||
import { Mute } from "./entities/Mute";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, Repository, Brackets } from "typeorm";
|
||||
|
||||
export class GuildMutes extends BaseGuildRepository {
|
||||
private mutes: Repository<Mute>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.mutes = getRepository(Mute);
|
||||
}
|
||||
|
||||
async getExpiredMutes(): Promise<Mute[]> {
|
||||
return this.mutes
|
||||
.createQueryBuilder("mutes")
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere("expires_at IS NOT NULL")
|
||||
.andWhere("expires_at <= NOW()")
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async findExistingMuteForUserId(userId: string): Promise<Mute> {
|
||||
return this.mutes.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async isMuted(userId: string): Promise<boolean> {
|
||||
const mute = await this.findExistingMuteForUserId(userId);
|
||||
return mute != null;
|
||||
}
|
||||
|
||||
async addMute(userId, expiryTime): Promise<Mute> {
|
||||
const expiresAt = expiryTime
|
||||
? moment()
|
||||
.add(expiryTime, "ms")
|
||||
.format("YYYY-MM-DD HH:mm:ss")
|
||||
: null;
|
||||
|
||||
const result = await this.mutes.insert({
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
expires_at: expiresAt,
|
||||
});
|
||||
|
||||
return this.mutes.findOne({ where: result.identifiers[0] });
|
||||
}
|
||||
|
||||
async updateExpiryTime(userId, newExpiryTime) {
|
||||
const expiresAt = newExpiryTime
|
||||
? moment()
|
||||
.add(newExpiryTime, "ms")
|
||||
.format("YYYY-MM-DD HH:mm:ss")
|
||||
: null;
|
||||
|
||||
return this.mutes.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
expires_at: expiresAt,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async getActiveMutes(): Promise<Mute[]> {
|
||||
return this.mutes
|
||||
.createQueryBuilder("mutes")
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere(
|
||||
new Brackets(qb => {
|
||||
qb.where("expires_at > NOW()").orWhere("expires_at IS NULL");
|
||||
}),
|
||||
)
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async setCaseId(userId: string, caseId: number) {
|
||||
await this.mutes.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
case_id: caseId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async clear(userId) {
|
||||
await this.mutes.delete({
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
});
|
||||
}
|
||||
}
|
68
backend/src/data/GuildNicknameHistory.ts
Normal file
68
backend/src/data/GuildNicknameHistory.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { NicknameHistoryEntry } from "./entities/NicknameHistoryEntry";
|
||||
import { sorter } from "../utils";
|
||||
|
||||
export const MAX_NICKNAME_ENTRIES_PER_USER = 10;
|
||||
|
||||
export class GuildNicknameHistory extends BaseGuildRepository {
|
||||
private nicknameHistory: Repository<NicknameHistoryEntry>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.nicknameHistory = getRepository(NicknameHistoryEntry);
|
||||
}
|
||||
|
||||
async getByUserId(userId): Promise<NicknameHistoryEntry[]> {
|
||||
return this.nicknameHistory.find({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
order: {
|
||||
id: "DESC",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getLastEntry(userId): Promise<NicknameHistoryEntry> {
|
||||
return this.nicknameHistory.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
order: {
|
||||
id: "DESC",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async addEntry(userId, nickname) {
|
||||
await this.nicknameHistory.insert({
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
nickname,
|
||||
});
|
||||
|
||||
// Cleanup (leave only the last MAX_NICKNAME_ENTRIES_PER_USER entries)
|
||||
const lastEntries = await this.getByUserId(userId);
|
||||
if (lastEntries.length > MAX_NICKNAME_ENTRIES_PER_USER) {
|
||||
const earliestEntry = lastEntries
|
||||
.sort(sorter("timestamp", "DESC"))
|
||||
.slice(0, 10)
|
||||
.reduce((earliest, entry) => {
|
||||
if (earliest == null) return entry;
|
||||
if (entry.id < earliest.id) return entry;
|
||||
return earliest;
|
||||
}, null);
|
||||
|
||||
this.nicknameHistory
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guildId", { guildId: this.guildId })
|
||||
.andWhere("user_id = :userId", { userId })
|
||||
.andWhere("id < :id", { id: earliestEntry.id })
|
||||
.delete()
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
}
|
58
backend/src/data/GuildPersistedData.ts
Normal file
58
backend/src/data/GuildPersistedData.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { PersistedData } from "./entities/PersistedData";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
|
||||
export interface IPartialPersistData {
|
||||
roles?: string[];
|
||||
nickname?: string;
|
||||
is_voice_muted?: boolean;
|
||||
}
|
||||
|
||||
export class GuildPersistedData extends BaseGuildRepository {
|
||||
private persistedData: Repository<PersistedData>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.persistedData = getRepository(PersistedData);
|
||||
}
|
||||
|
||||
async find(userId: string) {
|
||||
return this.persistedData.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async set(userId: string, data: IPartialPersistData = {}) {
|
||||
const finalData: any = {};
|
||||
if (data.roles) finalData.roles = data.roles.join(",");
|
||||
if (data.nickname) finalData.nickname = data.nickname;
|
||||
if (data.is_voice_muted != null) finalData.is_voice_muted = data.is_voice_muted ? 1 : 0;
|
||||
|
||||
const existing = await this.find(userId);
|
||||
if (existing) {
|
||||
await this.persistedData.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
finalData,
|
||||
);
|
||||
} else {
|
||||
await this.persistedData.insert({
|
||||
...finalData,
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async clear(userId: string) {
|
||||
await this.persistedData.delete({
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
});
|
||||
}
|
||||
}
|
55
backend/src/data/GuildPingableRoles.ts
Normal file
55
backend/src/data/GuildPingableRoles.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { PingableRole } from "./entities/PingableRole";
|
||||
|
||||
export class GuildPingableRoles extends BaseGuildRepository {
|
||||
private pingableRoles: Repository<PingableRole>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.pingableRoles = getRepository(PingableRole);
|
||||
}
|
||||
|
||||
async all(): Promise<PingableRole[]> {
|
||||
return this.pingableRoles.find({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getForChannel(channelId: string): Promise<PingableRole[]> {
|
||||
return this.pingableRoles.find({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByChannelAndRoleId(channelId: string, roleId: string): Promise<PingableRole> {
|
||||
return this.pingableRoles.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
role_id: roleId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async delete(channelId: string, roleId: string) {
|
||||
await this.pingableRoles.delete({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
role_id: roleId,
|
||||
});
|
||||
}
|
||||
|
||||
async add(channelId: string, roleId: string) {
|
||||
await this.pingableRoles.insert({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
role_id: roleId,
|
||||
});
|
||||
}
|
||||
}
|
62
backend/src/data/GuildReactionRoles.ts
Normal file
62
backend/src/data/GuildReactionRoles.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { ReactionRole } from "./entities/ReactionRole";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
|
||||
export class GuildReactionRoles extends BaseGuildRepository {
|
||||
private reactionRoles: Repository<ReactionRole>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.reactionRoles = getRepository(ReactionRole);
|
||||
}
|
||||
|
||||
async all(): Promise<ReactionRole[]> {
|
||||
return this.reactionRoles.find({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getForMessage(messageId: string): Promise<ReactionRole[]> {
|
||||
return this.reactionRoles.find({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
message_id: messageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByMessageAndEmoji(messageId: string, emoji: string): Promise<ReactionRole> {
|
||||
return this.reactionRoles.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
message_id: messageId,
|
||||
emoji,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async removeFromMessage(messageId: string, emoji: string = null) {
|
||||
const criteria: any = {
|
||||
guild_id: this.guildId,
|
||||
message_id: messageId,
|
||||
};
|
||||
|
||||
if (emoji) {
|
||||
criteria.emoji = emoji;
|
||||
}
|
||||
|
||||
await this.reactionRoles.delete(criteria);
|
||||
}
|
||||
|
||||
async add(channelId: string, messageId: string, emoji: string, roleId: string) {
|
||||
await this.reactionRoles.insert({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
message_id: messageId,
|
||||
emoji,
|
||||
role_id: roleId,
|
||||
});
|
||||
}
|
||||
}
|
46
backend/src/data/GuildReminders.ts
Normal file
46
backend/src/data/GuildReminders.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { Reminder } from "./entities/Reminder";
|
||||
|
||||
export class GuildReminders extends BaseGuildRepository {
|
||||
private reminders: Repository<Reminder>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.reminders = getRepository(Reminder);
|
||||
}
|
||||
|
||||
async getDueReminders(): Promise<Reminder[]> {
|
||||
return this.reminders
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guildId", { guildId: this.guildId })
|
||||
.andWhere("remind_at <= NOW()")
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async getRemindersByUserId(userId: string): Promise<Reminder[]> {
|
||||
return this.reminders.find({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
await this.reminders.delete({
|
||||
guild_id: this.guildId,
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
async add(userId: string, channelId: string, remindAt: string, body: string) {
|
||||
await this.reminders.insert({
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
channel_id: channelId,
|
||||
remind_at: remindAt,
|
||||
body,
|
||||
});
|
||||
}
|
||||
}
|
285
backend/src/data/GuildSavedMessages.ts
Normal file
285
backend/src/data/GuildSavedMessages.ts
Normal file
|
@ -0,0 +1,285 @@
|
|||
import { Brackets, getRepository, Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { ISavedMessageData, SavedMessage } from "./entities/SavedMessage";
|
||||
import { QueuedEventEmitter } from "../QueuedEventEmitter";
|
||||
import { GuildChannel, Message } from "eris";
|
||||
import moment from "moment-timezone";
|
||||
|
||||
const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 min
|
||||
|
||||
const RETENTION_PERIOD = 5 * 24 * 60 * 60 * 1000; // 5 days
|
||||
|
||||
async function cleanup() {
|
||||
const repository = getRepository(SavedMessage);
|
||||
await repository
|
||||
.createQueryBuilder("messages")
|
||||
.where(
|
||||
// Clear deleted messages
|
||||
new Brackets(qb => {
|
||||
qb.where("deleted_at IS NOT NULL");
|
||||
qb.andWhere(`deleted_at <= (NOW() - INTERVAL ${CLEANUP_INTERVAL}000 MICROSECOND)`);
|
||||
}),
|
||||
)
|
||||
.orWhere(
|
||||
// Clear old messages
|
||||
new Brackets(qb => {
|
||||
qb.where("is_permanent = 0");
|
||||
qb.andWhere(`posted_at <= (NOW() - INTERVAL ${RETENTION_PERIOD}000 MICROSECOND)`);
|
||||
}),
|
||||
)
|
||||
.delete()
|
||||
.execute();
|
||||
|
||||
setTimeout(cleanup, CLEANUP_INTERVAL);
|
||||
}
|
||||
|
||||
// Start first cleanup 30 seconds after startup
|
||||
setTimeout(cleanup, 30 * 1000);
|
||||
|
||||
export class GuildSavedMessages extends BaseGuildRepository {
|
||||
private messages: Repository<SavedMessage>;
|
||||
protected toBePermanent: Set<string>;
|
||||
|
||||
public events: QueuedEventEmitter;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.messages = getRepository(SavedMessage);
|
||||
this.events = new QueuedEventEmitter();
|
||||
|
||||
this.toBePermanent = new Set();
|
||||
}
|
||||
|
||||
public msgToSavedMessageData(msg: Message): ISavedMessageData {
|
||||
const data: ISavedMessageData = {
|
||||
author: {
|
||||
username: msg.author.username,
|
||||
discriminator: msg.author.discriminator,
|
||||
},
|
||||
content: msg.content,
|
||||
timestamp: msg.timestamp,
|
||||
};
|
||||
|
||||
if (msg.attachments.length) data.attachments = msg.attachments;
|
||||
if (msg.embeds.length) data.embeds = msg.embeds;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
find(id) {
|
||||
return this.messages
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere("id = :id", { id })
|
||||
.andWhere("deleted_at IS NULL")
|
||||
.getOne();
|
||||
}
|
||||
|
||||
getLatestBotMessagesByChannel(channelId, limit) {
|
||||
return this.messages
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere("channel_id = :channel_id", { channel_id: channelId })
|
||||
.andWhere("is_bot = 1")
|
||||
.andWhere("deleted_at IS NULL")
|
||||
.orderBy("id", "DESC")
|
||||
.limit(limit)
|
||||
.getMany();
|
||||
}
|
||||
|
||||
getLatestByChannelBeforeId(channelId, beforeId, limit) {
|
||||
return this.messages
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere("channel_id = :channel_id", { channel_id: channelId })
|
||||
.andWhere("id < :beforeId", { beforeId })
|
||||
.andWhere("deleted_at IS NULL")
|
||||
.orderBy("id", "DESC")
|
||||
.limit(limit)
|
||||
.getMany();
|
||||
}
|
||||
|
||||
getLatestByChannelAndUser(channelId, userId, limit) {
|
||||
return this.messages
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere("channel_id = :channel_id", { channel_id: channelId })
|
||||
.andWhere("user_id = :user_id", { user_id: userId })
|
||||
.andWhere("deleted_at IS NULL")
|
||||
.orderBy("id", "DESC")
|
||||
.limit(limit)
|
||||
.getMany();
|
||||
}
|
||||
|
||||
getUserMessagesByChannelAfterId(userId, channelId, afterId, limit = null) {
|
||||
let query = this.messages
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere("user_id = :user_id", { user_id: userId })
|
||||
.andWhere("channel_id = :channel_id", { channel_id: channelId })
|
||||
.andWhere("id > :afterId", { afterId })
|
||||
.andWhere("deleted_at IS NULL");
|
||||
|
||||
if (limit != null) {
|
||||
query = query.limit(limit);
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
getMultiple(messageIds: string[]): Promise<SavedMessage[]> {
|
||||
return this.messages
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere("id IN (:messageIds)", { messageIds })
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async create(data) {
|
||||
const isPermanent = this.toBePermanent.has(data.id);
|
||||
if (isPermanent) {
|
||||
data.is_permanent = true;
|
||||
this.toBePermanent.delete(data.id);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.messages.insert(data);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return;
|
||||
}
|
||||
|
||||
const inserted = await this.messages.findOne(data.id);
|
||||
this.events.emit("create", [inserted]);
|
||||
this.events.emit(`create:${data.id}`, [inserted]);
|
||||
}
|
||||
|
||||
async createFromMsg(msg: Message, overrides = {}) {
|
||||
const existingSavedMsg = await this.find(msg.id);
|
||||
if (existingSavedMsg) return;
|
||||
|
||||
const savedMessageData = this.msgToSavedMessageData(msg);
|
||||
const postedAt = moment.utc(msg.timestamp, "x").format("YYYY-MM-DD HH:mm:ss.SSS");
|
||||
|
||||
const data = {
|
||||
id: msg.id,
|
||||
guild_id: (msg.channel as GuildChannel).guild.id,
|
||||
channel_id: msg.channel.id,
|
||||
user_id: msg.author.id,
|
||||
is_bot: msg.author.bot,
|
||||
data: savedMessageData,
|
||||
posted_at: postedAt,
|
||||
};
|
||||
|
||||
return this.create({ ...data, ...overrides });
|
||||
}
|
||||
|
||||
async markAsDeleted(id) {
|
||||
await this.messages
|
||||
.createQueryBuilder("messages")
|
||||
.update()
|
||||
.set({
|
||||
deleted_at: () => "NOW(3)",
|
||||
})
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere("id = :id", { id })
|
||||
.execute();
|
||||
|
||||
const deleted = await this.messages.findOne(id);
|
||||
|
||||
if (deleted) {
|
||||
this.events.emit("delete", [deleted]);
|
||||
this.events.emit(`delete:${id}`, [deleted]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the specified messages as deleted in the database (if they weren't already marked before).
|
||||
* If any messages were marked as deleted, also emits the deleteBulk event.
|
||||
*/
|
||||
async markBulkAsDeleted(ids) {
|
||||
const deletedAt = moment().format("YYYY-MM-DD HH:mm:ss.SSS");
|
||||
|
||||
await this.messages
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.set({ deleted_at: deletedAt })
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere("id IN (:ids)", { ids })
|
||||
.andWhere("deleted_at IS NULL")
|
||||
.execute();
|
||||
|
||||
const deleted = await this.messages
|
||||
.createQueryBuilder()
|
||||
.where("id IN (:ids)", { ids })
|
||||
.andWhere("deleted_at = :deletedAt", { deletedAt })
|
||||
.getMany();
|
||||
|
||||
if (deleted.length) {
|
||||
this.events.emit("deleteBulk", [deleted]);
|
||||
}
|
||||
}
|
||||
|
||||
async saveEdit(id, newData: ISavedMessageData) {
|
||||
const oldMessage = await this.messages.findOne(id);
|
||||
if (!oldMessage) return;
|
||||
|
||||
const newMessage = { ...oldMessage, data: newData };
|
||||
|
||||
await this.messages.update(
|
||||
{ id },
|
||||
{
|
||||
data: newData,
|
||||
},
|
||||
);
|
||||
|
||||
this.events.emit("update", [newMessage, oldMessage]);
|
||||
this.events.emit(`update:${id}`, [newMessage, oldMessage]);
|
||||
}
|
||||
|
||||
async saveEditFromMsg(msg: Message) {
|
||||
const newData = this.msgToSavedMessageData(msg);
|
||||
return this.saveEdit(msg.id, newData);
|
||||
}
|
||||
|
||||
async setPermanent(id: string) {
|
||||
const savedMsg = await this.find(id);
|
||||
if (savedMsg) {
|
||||
await this.messages.update(
|
||||
{ id },
|
||||
{
|
||||
is_permanent: true,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this.toBePermanent.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
async onceMessageAvailable(id: string, handler: (msg: SavedMessage) => any, timeout: number = 60 * 1000) {
|
||||
let called = false;
|
||||
let onceEventListener;
|
||||
let timeoutFn;
|
||||
|
||||
const callHandler = async (msg: SavedMessage) => {
|
||||
this.events.off(`create:${id}`, onceEventListener);
|
||||
clearTimeout(timeoutFn);
|
||||
|
||||
if (called) return;
|
||||
called = true;
|
||||
|
||||
await handler(msg);
|
||||
};
|
||||
|
||||
onceEventListener = this.events.once(`create:${id}`, callHandler);
|
||||
timeoutFn = setTimeout(() => {
|
||||
called = true;
|
||||
callHandler(null);
|
||||
}, timeout);
|
||||
|
||||
const messageInDB = await this.find(id);
|
||||
if (messageInDB) {
|
||||
callHandler(messageInDB);
|
||||
}
|
||||
}
|
||||
}
|
41
backend/src/data/GuildScheduledPosts.ts
Normal file
41
backend/src/data/GuildScheduledPosts.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { ScheduledPost } from "./entities/ScheduledPost";
|
||||
|
||||
export class GuildScheduledPosts extends BaseGuildRepository {
|
||||
private scheduledPosts: Repository<ScheduledPost>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.scheduledPosts = getRepository(ScheduledPost);
|
||||
}
|
||||
|
||||
all(): Promise<ScheduledPost[]> {
|
||||
return this.scheduledPosts
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guildId", { guildId: this.guildId })
|
||||
.getMany();
|
||||
}
|
||||
|
||||
getDueScheduledPosts(): Promise<ScheduledPost[]> {
|
||||
return this.scheduledPosts
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guildId", { guildId: this.guildId })
|
||||
.andWhere("post_at <= NOW()")
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
await this.scheduledPosts.delete({
|
||||
guild_id: this.guildId,
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: Partial<ScheduledPost>) {
|
||||
await this.scheduledPosts.insert({
|
||||
...data,
|
||||
guild_id: this.guildId,
|
||||
});
|
||||
}
|
||||
}
|
38
backend/src/data/GuildSelfGrantableRoles.ts
Normal file
38
backend/src/data/GuildSelfGrantableRoles.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { SelfGrantableRole } from "./entities/SelfGrantableRole";
|
||||
|
||||
export class GuildSelfGrantableRoles extends BaseGuildRepository {
|
||||
private selfGrantableRoles: Repository<SelfGrantableRole>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.selfGrantableRoles = getRepository(SelfGrantableRole);
|
||||
}
|
||||
|
||||
async getForChannel(channelId: string): Promise<SelfGrantableRole[]> {
|
||||
return this.selfGrantableRoles.find({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async delete(channelId: string, roleId: string) {
|
||||
await this.selfGrantableRoles.delete({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
role_id: roleId,
|
||||
});
|
||||
}
|
||||
|
||||
async add(channelId: string, roleId: string, aliases: string[]) {
|
||||
await this.selfGrantableRoles.insert({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
role_id: roleId,
|
||||
aliases,
|
||||
});
|
||||
}
|
||||
}
|
121
backend/src/data/GuildSlowmodes.ts
Normal file
121
backend/src/data/GuildSlowmodes.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { SlowmodeChannel } from "./entities/SlowmodeChannel";
|
||||
import { SlowmodeUser } from "./entities/SlowmodeUser";
|
||||
import moment from "moment-timezone";
|
||||
|
||||
export class GuildSlowmodes extends BaseGuildRepository {
|
||||
private slowmodeChannels: Repository<SlowmodeChannel>;
|
||||
private slowmodeUsers: Repository<SlowmodeUser>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.slowmodeChannels = getRepository(SlowmodeChannel);
|
||||
this.slowmodeUsers = getRepository(SlowmodeUser);
|
||||
}
|
||||
|
||||
async getChannelSlowmode(channelId): Promise<SlowmodeChannel> {
|
||||
return this.slowmodeChannels.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async setChannelSlowmode(channelId, seconds): Promise<void> {
|
||||
const existingSlowmode = await this.getChannelSlowmode(channelId);
|
||||
if (existingSlowmode) {
|
||||
await this.slowmodeChannels.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
},
|
||||
{
|
||||
slowmode_seconds: seconds,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await this.slowmodeChannels.insert({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
slowmode_seconds: seconds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async deleteChannelSlowmode(channelId): Promise<void> {
|
||||
await this.slowmodeChannels.delete({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
});
|
||||
}
|
||||
|
||||
async getChannelSlowmodeUser(channelId, userId): Promise<SlowmodeUser> {
|
||||
return this.slowmodeUsers.findOne({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
user_id: userId,
|
||||
});
|
||||
}
|
||||
|
||||
async userHasSlowmode(channelId, userId): Promise<boolean> {
|
||||
return (await this.getChannelSlowmodeUser(channelId, userId)) != null;
|
||||
}
|
||||
|
||||
async addSlowmodeUser(channelId, userId): Promise<void> {
|
||||
const slowmode = await this.getChannelSlowmode(channelId);
|
||||
if (!slowmode) return;
|
||||
|
||||
const expiresAt = moment()
|
||||
.add(slowmode.slowmode_seconds, "seconds")
|
||||
.format("YYYY-MM-DD HH:mm:ss");
|
||||
|
||||
if (await this.userHasSlowmode(channelId, userId)) {
|
||||
// Update existing
|
||||
await this.slowmodeUsers.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
expires_at: expiresAt,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Add new
|
||||
await this.slowmodeUsers.insert({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
user_id: userId,
|
||||
expires_at: expiresAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async clearSlowmodeUser(channelId, userId): Promise<void> {
|
||||
await this.slowmodeUsers.delete({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
user_id: userId,
|
||||
});
|
||||
}
|
||||
|
||||
async getChannelSlowmodeUsers(channelId): Promise<SlowmodeUser[]> {
|
||||
return this.slowmodeUsers.find({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getExpiredSlowmodeUsers(): Promise<SlowmodeUser[]> {
|
||||
return this.slowmodeUsers
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guildId", { guildId: this.guildId })
|
||||
.andWhere("expires_at <= NOW()")
|
||||
.getMany();
|
||||
}
|
||||
}
|
84
backend/src/data/GuildStarboards.ts
Normal file
84
backend/src/data/GuildStarboards.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { Starboard } from "./entities/Starboard";
|
||||
import { StarboardMessage } from "./entities/StarboardMessage";
|
||||
|
||||
export class GuildStarboards extends BaseGuildRepository {
|
||||
private starboards: Repository<Starboard>;
|
||||
private starboardMessages: Repository<StarboardMessage>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.starboards = getRepository(Starboard);
|
||||
this.starboardMessages = getRepository(StarboardMessage);
|
||||
}
|
||||
|
||||
getStarboardByChannelId(channelId): Promise<Starboard> {
|
||||
return this.starboards.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getStarboardsByEmoji(emoji): Promise<Starboard[]> {
|
||||
return this.starboards.find({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
emoji,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getStarboardMessageByStarboardIdAndMessageId(starboardId, messageId): Promise<StarboardMessage> {
|
||||
return this.starboardMessages.findOne({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
starboard_id: starboardId,
|
||||
message_id: messageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getStarboardMessagesByMessageId(id): Promise<StarboardMessage[]> {
|
||||
return this.starboardMessages.find({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
message_id: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createStarboardMessage(starboardId, messageId, starboardMessageId): Promise<void> {
|
||||
await this.starboardMessages.insert({
|
||||
starboard_id: starboardId,
|
||||
message_id: messageId,
|
||||
starboard_message_id: starboardMessageId,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteStarboardMessage(starboardId, messageId): Promise<void> {
|
||||
await this.starboardMessages.delete({
|
||||
starboard_id: starboardId,
|
||||
message_id: messageId,
|
||||
});
|
||||
}
|
||||
|
||||
async create(channelId: string, channelWhitelist: string[], emoji: string, reactionsRequired: number): Promise<void> {
|
||||
await this.starboards.insert({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
channel_whitelist: channelWhitelist ? channelWhitelist.join(",") : null,
|
||||
emoji,
|
||||
reactions_required: reactionsRequired,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(channelId: string): Promise<void> {
|
||||
await this.starboards.delete({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
});
|
||||
}
|
||||
}
|
89
backend/src/data/GuildTags.ts
Normal file
89
backend/src/data/GuildTags.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { Tag } from "./entities/Tag";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { TagResponse } from "./entities/TagResponse";
|
||||
|
||||
export class GuildTags extends BaseGuildRepository {
|
||||
private tags: Repository<Tag>;
|
||||
private tagResponses: Repository<TagResponse>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.tags = getRepository(Tag);
|
||||
this.tagResponses = getRepository(TagResponse);
|
||||
}
|
||||
|
||||
async all(): Promise<Tag[]> {
|
||||
return this.tags.find({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async find(tag): Promise<Tag> {
|
||||
return this.tags.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
tag,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createOrUpdate(tag, body, userId) {
|
||||
const existingTag = await this.find(tag);
|
||||
if (existingTag) {
|
||||
await this.tags
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.set({
|
||||
body,
|
||||
user_id: userId,
|
||||
created_at: () => "NOW()",
|
||||
})
|
||||
.where("guild_id = :guildId", { guildId: this.guildId })
|
||||
.andWhere("tag = :tag", { tag })
|
||||
.execute();
|
||||
} else {
|
||||
await this.tags.insert({
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
tag,
|
||||
body,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async delete(tag) {
|
||||
await this.tags.delete({
|
||||
guild_id: this.guildId,
|
||||
tag,
|
||||
});
|
||||
}
|
||||
|
||||
async findResponseByCommandMessageId(messageId: string): Promise<TagResponse> {
|
||||
return this.tagResponses.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
command_message_id: messageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findResponseByResponseMessageId(messageId: string): Promise<TagResponse> {
|
||||
return this.tagResponses.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
response_message_id: messageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async addResponse(cmdMessageId, responseMessageId) {
|
||||
await this.tagResponses.insert({
|
||||
guild_id: this.guildId,
|
||||
command_message_id: cmdMessageId,
|
||||
response_message_id: responseMessageId,
|
||||
});
|
||||
}
|
||||
}
|
63
backend/src/data/GuildVCAlerts.ts
Normal file
63
backend/src/data/GuildVCAlerts.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { VCAlert } from "./entities/VCAlert";
|
||||
|
||||
export class GuildVCAlerts extends BaseGuildRepository {
|
||||
private allAlerts: Repository<VCAlert>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.allAlerts = getRepository(VCAlert);
|
||||
}
|
||||
|
||||
async getOutdatedAlerts(): Promise<VCAlert[]> {
|
||||
return this.allAlerts
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guildId", { guildId: this.guildId })
|
||||
.andWhere("expires_at <= NOW()")
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async getAllGuildAlerts(): Promise<VCAlert[]> {
|
||||
return this.allAlerts
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guildId", { guildId: this.guildId })
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async getAlertsByUserId(userId: string): Promise<VCAlert[]> {
|
||||
return this.allAlerts.find({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getAlertsByRequestorId(requestorId: string): Promise<VCAlert[]> {
|
||||
return this.allAlerts.find({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
requestor_id: requestorId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
await this.allAlerts.delete({
|
||||
guild_id: this.guildId,
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
async add(requestorId: string, userId: string, channelId: string, expiresAt: string, body: string) {
|
||||
await this.allAlerts.insert({
|
||||
guild_id: this.guildId,
|
||||
requestor_id: requestorId,
|
||||
user_id: userId,
|
||||
channel_id: channelId,
|
||||
expires_at: expiresAt,
|
||||
body,
|
||||
});
|
||||
}
|
||||
}
|
62
backend/src/data/LogType.ts
Normal file
62
backend/src/data/LogType.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
export enum LogType {
|
||||
MEMBER_WARN = 1,
|
||||
MEMBER_MUTE,
|
||||
MEMBER_UNMUTE,
|
||||
MEMBER_MUTE_EXPIRED,
|
||||
MEMBER_KICK,
|
||||
MEMBER_BAN,
|
||||
MEMBER_UNBAN,
|
||||
MEMBER_FORCEBAN,
|
||||
MEMBER_SOFTBAN,
|
||||
MEMBER_JOIN,
|
||||
MEMBER_LEAVE,
|
||||
MEMBER_ROLE_ADD,
|
||||
MEMBER_ROLE_REMOVE,
|
||||
MEMBER_NICK_CHANGE,
|
||||
MEMBER_USERNAME_CHANGE,
|
||||
MEMBER_RESTORE,
|
||||
|
||||
CHANNEL_CREATE,
|
||||
CHANNEL_DELETE,
|
||||
|
||||
ROLE_CREATE,
|
||||
ROLE_DELETE,
|
||||
|
||||
MESSAGE_EDIT,
|
||||
MESSAGE_DELETE,
|
||||
MESSAGE_DELETE_BULK,
|
||||
MESSAGE_DELETE_BARE,
|
||||
|
||||
VOICE_CHANNEL_JOIN,
|
||||
VOICE_CHANNEL_LEAVE,
|
||||
VOICE_CHANNEL_MOVE,
|
||||
|
||||
COMMAND,
|
||||
|
||||
MESSAGE_SPAM_DETECTED,
|
||||
CENSOR,
|
||||
CLEAN,
|
||||
|
||||
CASE_CREATE,
|
||||
|
||||
MASSBAN,
|
||||
|
||||
MEMBER_TIMED_MUTE,
|
||||
MEMBER_TIMED_UNMUTE,
|
||||
|
||||
MEMBER_JOIN_WITH_PRIOR_RECORDS,
|
||||
OTHER_SPAM_DETECTED,
|
||||
|
||||
MEMBER_ROLE_CHANGES,
|
||||
VOICE_CHANNEL_FORCE_MOVE,
|
||||
|
||||
CASE_UPDATE,
|
||||
|
||||
MEMBER_MUTE_REJOIN,
|
||||
|
||||
SCHEDULED_MESSAGE,
|
||||
POSTED_SCHEDULED_MESSAGE,
|
||||
|
||||
BOT_ALERT,
|
||||
AUTOMOD_ACTION,
|
||||
}
|
65
backend/src/data/UsernameHistory.ts
Normal file
65
backend/src/data/UsernameHistory.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { UsernameHistoryEntry } from "./entities/UsernameHistoryEntry";
|
||||
import { sorter } from "../utils";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
|
||||
export const MAX_USERNAME_ENTRIES_PER_USER = 10;
|
||||
|
||||
export class UsernameHistory extends BaseRepository {
|
||||
private usernameHistory: Repository<UsernameHistoryEntry>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.usernameHistory = getRepository(UsernameHistoryEntry);
|
||||
}
|
||||
|
||||
async getByUserId(userId): Promise<UsernameHistoryEntry[]> {
|
||||
return this.usernameHistory.find({
|
||||
where: {
|
||||
user_id: userId,
|
||||
},
|
||||
order: {
|
||||
id: "DESC",
|
||||
},
|
||||
take: MAX_USERNAME_ENTRIES_PER_USER,
|
||||
});
|
||||
}
|
||||
|
||||
getLastEntry(userId): Promise<UsernameHistoryEntry> {
|
||||
return this.usernameHistory.findOne({
|
||||
where: {
|
||||
user_id: userId,
|
||||
},
|
||||
order: {
|
||||
id: "DESC",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async addEntry(userId, username) {
|
||||
await this.usernameHistory.insert({
|
||||
user_id: userId,
|
||||
username,
|
||||
});
|
||||
|
||||
// Cleanup (leave only the last MAX_USERNAME_ENTRIES_PER_USER entries)
|
||||
const lastEntries = await this.getByUserId(userId);
|
||||
if (lastEntries.length > MAX_USERNAME_ENTRIES_PER_USER) {
|
||||
const earliestEntry = lastEntries
|
||||
.sort(sorter("timestamp", "DESC"))
|
||||
.slice(0, 10)
|
||||
.reduce((earliest, entry) => {
|
||||
if (earliest == null) return entry;
|
||||
if (entry.id < earliest.id) return entry;
|
||||
return earliest;
|
||||
}, null);
|
||||
|
||||
this.usernameHistory
|
||||
.createQueryBuilder()
|
||||
.andWhere("user_id = :userId", { userId })
|
||||
.andWhere("id < :id", { id: earliestEntry.id })
|
||||
.delete()
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
}
|
117
backend/src/data/Zalgo.ts
Normal file
117
backend/src/data/Zalgo.ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
// From https://github.com/b1naryth1ef/rowboat/blob/master/rowboat/util/zalgo.py
|
||||
const zalgoChars = [
|
||||
"\u030d",
|
||||
"\u030e",
|
||||
"\u0304",
|
||||
"\u0305",
|
||||
"\u033f",
|
||||
"\u0311",
|
||||
"\u0306",
|
||||
"\u0310",
|
||||
"\u0352",
|
||||
"\u0357",
|
||||
"\u0351",
|
||||
"\u0307",
|
||||
"\u0308",
|
||||
"\u030a",
|
||||
"\u0342",
|
||||
"\u0343",
|
||||
"\u0344",
|
||||
"\u034a",
|
||||
"\u034b",
|
||||
"\u034c",
|
||||
"\u0303",
|
||||
"\u0302",
|
||||
"\u030c",
|
||||
"\u0350",
|
||||
"\u0300",
|
||||
"\u030b",
|
||||
"\u030f",
|
||||
"\u0312",
|
||||
"\u0313",
|
||||
"\u0314",
|
||||
"\u033d",
|
||||
"\u0309",
|
||||
"\u0363",
|
||||
"\u0364",
|
||||
"\u0365",
|
||||
"\u0366",
|
||||
"\u0367",
|
||||
"\u0368",
|
||||
"\u0369",
|
||||
"\u036a",
|
||||
"\u036b",
|
||||
"\u036c",
|
||||
"\u036d",
|
||||
"\u036e",
|
||||
"\u036f",
|
||||
"\u033e",
|
||||
"\u035b",
|
||||
"\u0346",
|
||||
"\u031a",
|
||||
"\u0315",
|
||||
"\u031b",
|
||||
"\u0340",
|
||||
"\u0341",
|
||||
"\u0358",
|
||||
"\u0321",
|
||||
"\u0322",
|
||||
"\u0327",
|
||||
"\u0328",
|
||||
"\u0334",
|
||||
"\u0335",
|
||||
"\u0336",
|
||||
"\u034f",
|
||||
"\u035c",
|
||||
"\u035d",
|
||||
"\u035e",
|
||||
"\u035f",
|
||||
"\u0360",
|
||||
"\u0362",
|
||||
"\u0338",
|
||||
"\u0337",
|
||||
"\u0361",
|
||||
"\u0489",
|
||||
"\u0316",
|
||||
"\u0317",
|
||||
"\u0318",
|
||||
"\u0319",
|
||||
"\u031c",
|
||||
"\u031d",
|
||||
"\u031e",
|
||||
"\u031f",
|
||||
"\u0320",
|
||||
"\u0324",
|
||||
"\u0325",
|
||||
"\u0326",
|
||||
"\u0329",
|
||||
"\u032a",
|
||||
"\u032b",
|
||||
"\u032c",
|
||||
"\u032d",
|
||||
"\u032e",
|
||||
"\u032f",
|
||||
"\u0330",
|
||||
"\u0331",
|
||||
"\u0332",
|
||||
"\u0333",
|
||||
"\u0339",
|
||||
"\u033a",
|
||||
"\u033b",
|
||||
"\u033c",
|
||||
"\u0345",
|
||||
"\u0347",
|
||||
"\u0348",
|
||||
"\u0349",
|
||||
"\u034d",
|
||||
"\u034e",
|
||||
"\u0353",
|
||||
"\u0354",
|
||||
"\u0355",
|
||||
"\u0356",
|
||||
"\u0359",
|
||||
"\u035a",
|
||||
"\u0323",
|
||||
];
|
||||
|
||||
export const ZalgoRegex = new RegExp(zalgoChars.join("|"));
|
24
backend/src/data/db.ts
Normal file
24
backend/src/data/db.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { SimpleError } from "../SimpleError";
|
||||
import { Connection, createConnection } from "typeorm";
|
||||
|
||||
let connectionPromise: Promise<Connection>;
|
||||
|
||||
export let connection: Connection;
|
||||
|
||||
export function connect() {
|
||||
if (!connectionPromise) {
|
||||
connectionPromise = createConnection().then(newConnection => {
|
||||
// Verify the DB timezone is set to UTC
|
||||
return newConnection.query("SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP) AS tz").then(r => {
|
||||
if (r[0].tz !== "00:00:00") {
|
||||
throw new SimpleError(`Database timezone must be UTC (detected ${r[0].tz})`);
|
||||
}
|
||||
|
||||
connection = newConnection;
|
||||
return newConnection;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return connectionPromise;
|
||||
}
|
17
backend/src/data/entities/AllowedGuild.ts
Normal file
17
backend/src/data/entities/AllowedGuild.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { Entity, Column, PrimaryColumn, CreateDateColumn } from "typeorm";
|
||||
|
||||
@Entity("allowed_guilds")
|
||||
export class AllowedGuild {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column()
|
||||
icon: string;
|
||||
|
||||
@Column()
|
||||
owner_id: string;
|
||||
}
|
25
backend/src/data/entities/ApiLogin.ts
Normal file
25
backend/src/data/entities/ApiLogin.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Entity, Column, PrimaryColumn, OneToOne, ManyToOne, JoinColumn } from "typeorm";
|
||||
import { ApiUserInfo } from "./ApiUserInfo";
|
||||
|
||||
@Entity("api_logins")
|
||||
export class ApiLogin {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
token: string;
|
||||
|
||||
@Column()
|
||||
user_id: string;
|
||||
|
||||
@Column()
|
||||
logged_in_at: string;
|
||||
|
||||
@Column()
|
||||
expires_at: string;
|
||||
|
||||
@ManyToOne(type => ApiUserInfo, userInfo => userInfo.logins)
|
||||
@JoinColumn({ name: "user_id" })
|
||||
userInfo: ApiUserInfo;
|
||||
}
|
20
backend/src/data/entities/ApiPermission.ts
Normal file
20
backend/src/data/entities/ApiPermission.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from "typeorm";
|
||||
import { ApiUserInfo } from "./ApiUserInfo";
|
||||
|
||||
@Entity("api_permissions")
|
||||
export class ApiPermission {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
guild_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
user_id: string;
|
||||
|
||||
@Column()
|
||||
role: string;
|
||||
|
||||
@ManyToOne(type => ApiUserInfo, userInfo => userInfo.permissions)
|
||||
@JoinColumn({ name: "user_id" })
|
||||
userInfo: ApiUserInfo;
|
||||
}
|
28
backend/src/data/entities/ApiUserInfo.ts
Normal file
28
backend/src/data/entities/ApiUserInfo.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Entity, Column, PrimaryColumn, OneToMany } from "typeorm";
|
||||
import { ApiLogin } from "./ApiLogin";
|
||||
import { ApiPermission } from "./ApiPermission";
|
||||
|
||||
export interface ApiUserInfoData {
|
||||
username: string;
|
||||
discriminator: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
@Entity("api_user_info")
|
||||
export class ApiUserInfo {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: string;
|
||||
|
||||
@Column("simple-json")
|
||||
data: ApiUserInfoData;
|
||||
|
||||
@Column()
|
||||
updated_at: string;
|
||||
|
||||
@OneToMany(type => ApiLogin, login => login.userInfo)
|
||||
logins: ApiLogin[];
|
||||
|
||||
@OneToMany(type => ApiPermission, perm => perm.userInfo)
|
||||
permissions: ApiPermission[];
|
||||
}
|
16
backend/src/data/entities/ArchiveEntry.ts
Normal file
16
backend/src/data/entities/ArchiveEntry.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
@Entity("archives")
|
||||
export class ArchiveEntry {
|
||||
@Column()
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column() guild_id: string;
|
||||
|
||||
@Column() body: string;
|
||||
|
||||
@Column() created_at: string;
|
||||
|
||||
@Column() expires_at: string;
|
||||
}
|
15
backend/src/data/entities/AutoReaction.ts
Normal file
15
backend/src/data/entities/AutoReaction.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
import { ISavedMessageData } from "./SavedMessage";
|
||||
|
||||
@Entity("auto_reactions")
|
||||
export class AutoReaction {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
guild_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
channel_id: string;
|
||||
|
||||
@Column("simple-array") reactions: string[];
|
||||
}
|
34
backend/src/data/entities/Case.ts
Normal file
34
backend/src/data/entities/Case.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
|
||||
import { CaseNote } from "./CaseNote";
|
||||
|
||||
@Entity("cases")
|
||||
export class Case {
|
||||
@PrimaryGeneratedColumn() id: number;
|
||||
|
||||
@Column() guild_id: string;
|
||||
|
||||
@Column() case_number: number;
|
||||
|
||||
@Column() user_id: string;
|
||||
|
||||
@Column() user_name: string;
|
||||
|
||||
@Column() mod_id: string;
|
||||
|
||||
@Column() mod_name: string;
|
||||
|
||||
@Column() type: number;
|
||||
|
||||
@Column() audit_log_id: string;
|
||||
|
||||
@Column() created_at: string;
|
||||
|
||||
@Column() is_hidden: boolean;
|
||||
|
||||
@Column() pp_id: string;
|
||||
|
||||
@Column() pp_name: string;
|
||||
|
||||
@OneToMany(type => CaseNote, note => note.case)
|
||||
notes: CaseNote[];
|
||||
}
|
21
backend/src/data/entities/CaseNote.ts
Normal file
21
backend/src/data/entities/CaseNote.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from "typeorm";
|
||||
import { Case } from "./Case";
|
||||
|
||||
@Entity("case_notes")
|
||||
export class CaseNote {
|
||||
@PrimaryGeneratedColumn() id: number;
|
||||
|
||||
@Column() case_id: number;
|
||||
|
||||
@Column() mod_id: string;
|
||||
|
||||
@Column() mod_name: string;
|
||||
|
||||
@Column() body: string;
|
||||
|
||||
@Column() created_at: string;
|
||||
|
||||
@ManyToOne(type => Case, theCase => theCase.notes)
|
||||
@JoinColumn({ name: "case_id" })
|
||||
case: Case;
|
||||
}
|
28
backend/src/data/entities/Config.ts
Normal file
28
backend/src/data/entities/Config.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Entity, Column, PrimaryColumn, CreateDateColumn, ManyToOne, JoinColumn } from "typeorm";
|
||||
import { ApiUserInfo } from "./ApiUserInfo";
|
||||
|
||||
@Entity("configs")
|
||||
export class Config {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
key: string;
|
||||
|
||||
@Column()
|
||||
config: string;
|
||||
|
||||
@Column()
|
||||
is_active: boolean;
|
||||
|
||||
@Column()
|
||||
edited_by: string;
|
||||
|
||||
@Column()
|
||||
edited_at: string;
|
||||
|
||||
@ManyToOne(type => ApiUserInfo)
|
||||
@JoinColumn({ name: "edited_by" })
|
||||
userInfo: ApiUserInfo;
|
||||
}
|
18
backend/src/data/entities/Mute.ts
Normal file
18
backend/src/data/entities/Mute.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("mutes")
|
||||
export class Mute {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
guild_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
user_id: string;
|
||||
|
||||
@Column() created_at: string;
|
||||
|
||||
@Column() expires_at: string;
|
||||
|
||||
@Column() case_id: number;
|
||||
}
|
16
backend/src/data/entities/NicknameHistoryEntry.ts
Normal file
16
backend/src/data/entities/NicknameHistoryEntry.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("nickname_history")
|
||||
export class NicknameHistoryEntry {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: string;
|
||||
|
||||
@Column() guild_id: string;
|
||||
|
||||
@Column() user_id: string;
|
||||
|
||||
@Column() nickname: string;
|
||||
|
||||
@Column() timestamp: string;
|
||||
}
|
18
backend/src/data/entities/PersistedData.ts
Normal file
18
backend/src/data/entities/PersistedData.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("persisted_data")
|
||||
export class PersistedData {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
guild_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
user_id: string;
|
||||
|
||||
@Column("simple-array") roles: string[];
|
||||
|
||||
@Column() nickname: string;
|
||||
|
||||
@Column() is_voice_muted: number;
|
||||
}
|
15
backend/src/data/entities/PingableRole.ts
Normal file
15
backend/src/data/entities/PingableRole.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
import { ISavedMessageData } from "./SavedMessage";
|
||||
|
||||
@Entity("pingable_roles")
|
||||
export class PingableRole {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column() guild_id: string;
|
||||
|
||||
@Column() channel_id: string;
|
||||
|
||||
@Column() role_id: string;
|
||||
}
|
22
backend/src/data/entities/ReactionRole.ts
Normal file
22
backend/src/data/entities/ReactionRole.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("reaction_roles")
|
||||
export class ReactionRole {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
guild_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
channel_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
message_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
emoji: string;
|
||||
|
||||
@Column() role_id: string;
|
||||
}
|
18
backend/src/data/entities/Reminder.ts
Normal file
18
backend/src/data/entities/Reminder.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("reminders")
|
||||
export class Reminder {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column() guild_id: string;
|
||||
|
||||
@Column() user_id: string;
|
||||
|
||||
@Column() channel_id: string;
|
||||
|
||||
@Column() remind_at: string;
|
||||
|
||||
@Column() body: string;
|
||||
}
|
35
backend/src/data/entities/SavedMessage.ts
Normal file
35
backend/src/data/entities/SavedMessage.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
|
||||
export interface ISavedMessageData {
|
||||
attachments?: object[];
|
||||
author: {
|
||||
username: string;
|
||||
discriminator: string;
|
||||
};
|
||||
content: string;
|
||||
embeds?: object[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@Entity("messages")
|
||||
export class SavedMessage {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: string;
|
||||
|
||||
@Column() guild_id: string;
|
||||
|
||||
@Column() channel_id: string;
|
||||
|
||||
@Column() user_id: string;
|
||||
|
||||
@Column() is_bot: boolean;
|
||||
|
||||
@Column("simple-json") data: ISavedMessageData;
|
||||
|
||||
@Column() posted_at: string;
|
||||
|
||||
@Column() deleted_at: string;
|
||||
|
||||
@Column() is_permanent: boolean;
|
||||
}
|
26
backend/src/data/entities/ScheduledPost.ts
Normal file
26
backend/src/data/entities/ScheduledPost.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
import { Attachment } from "eris";
|
||||
import { StrictMessageContent } from "../../utils";
|
||||
|
||||
@Entity("scheduled_posts")
|
||||
export class ScheduledPost {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column() guild_id: string;
|
||||
|
||||
@Column() author_id: string;
|
||||
|
||||
@Column() author_name: string;
|
||||
|
||||
@Column() channel_id: string;
|
||||
|
||||
@Column("simple-json") content: StrictMessageContent;
|
||||
|
||||
@Column("simple-json") attachments: Attachment[];
|
||||
|
||||
@Column() post_at: string;
|
||||
|
||||
@Column() enable_mentions: boolean;
|
||||
}
|
16
backend/src/data/entities/SelfGrantableRole.ts
Normal file
16
backend/src/data/entities/SelfGrantableRole.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("self_grantable_roles")
|
||||
export class SelfGrantableRole {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column() guild_id: string;
|
||||
|
||||
@Column() channel_id: string;
|
||||
|
||||
@Column() role_id: string;
|
||||
|
||||
@Column("simple-array") aliases: string[];
|
||||
}
|
14
backend/src/data/entities/SlowmodeChannel.ts
Normal file
14
backend/src/data/entities/SlowmodeChannel.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("slowmode_channels")
|
||||
export class SlowmodeChannel {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
guild_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
channel_id: string;
|
||||
|
||||
@Column() slowmode_seconds: number;
|
||||
}
|
18
backend/src/data/entities/SlowmodeUser.ts
Normal file
18
backend/src/data/entities/SlowmodeUser.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("slowmode_users")
|
||||
export class SlowmodeUser {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
guild_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
channel_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
user_id: string;
|
||||
|
||||
@Column() expires_at: string;
|
||||
}
|
23
backend/src/data/entities/Starboard.ts
Normal file
23
backend/src/data/entities/Starboard.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Entity, Column, PrimaryColumn, OneToMany } from "typeorm";
|
||||
import { CaseNote } from "./CaseNote";
|
||||
import { StarboardMessage } from "./StarboardMessage";
|
||||
|
||||
@Entity("starboards")
|
||||
export class Starboard {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column() guild_id: string;
|
||||
|
||||
@Column() channel_id: string;
|
||||
|
||||
@Column() channel_whitelist: string;
|
||||
|
||||
@Column() emoji: string;
|
||||
|
||||
@Column() reactions_required: number;
|
||||
|
||||
@OneToMany(type => StarboardMessage, msg => msg.starboard)
|
||||
starboardMessages: StarboardMessage[];
|
||||
}
|
25
backend/src/data/entities/StarboardMessage.ts
Normal file
25
backend/src/data/entities/StarboardMessage.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Entity, Column, PrimaryColumn, OneToMany, ManyToOne, JoinColumn, OneToOne } from "typeorm";
|
||||
import { Starboard } from "./Starboard";
|
||||
import { Case } from "./Case";
|
||||
import { SavedMessage } from "./SavedMessage";
|
||||
|
||||
@Entity("starboard_messages")
|
||||
export class StarboardMessage {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
starboard_id: number;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
message_id: string;
|
||||
|
||||
@Column() starboard_message_id: string;
|
||||
|
||||
@ManyToOne(type => Starboard, sb => sb.starboardMessages)
|
||||
@JoinColumn({ name: "starboard_id" })
|
||||
starboard: Starboard;
|
||||
|
||||
@OneToOne(type => SavedMessage)
|
||||
@JoinColumn({ name: "message_id" })
|
||||
message: SavedMessage;
|
||||
}
|
18
backend/src/data/entities/Tag.ts
Normal file
18
backend/src/data/entities/Tag.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Entity, Column, PrimaryColumn, CreateDateColumn } from "typeorm";
|
||||
|
||||
@Entity("tags")
|
||||
export class Tag {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
guild_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
tag: string;
|
||||
|
||||
@Column() user_id: string;
|
||||
|
||||
@Column() body: string;
|
||||
|
||||
@Column() created_at: string;
|
||||
}
|
14
backend/src/data/entities/TagResponse.ts
Normal file
14
backend/src/data/entities/TagResponse.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Entity, Column, PrimaryColumn, CreateDateColumn } from "typeorm";
|
||||
|
||||
@Entity("tag_responses")
|
||||
export class TagResponse {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: string;
|
||||
|
||||
@Column() guild_id: string;
|
||||
|
||||
@Column() command_message_id: string;
|
||||
|
||||
@Column() response_message_id: string;
|
||||
}
|
14
backend/src/data/entities/UsernameHistoryEntry.ts
Normal file
14
backend/src/data/entities/UsernameHistoryEntry.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("username_history")
|
||||
export class UsernameHistoryEntry {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: string;
|
||||
|
||||
@Column() user_id: string;
|
||||
|
||||
@Column() username: string;
|
||||
|
||||
@Column() timestamp: string;
|
||||
}
|
20
backend/src/data/entities/VCAlert.ts
Normal file
20
backend/src/data/entities/VCAlert.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("vc_alerts")
|
||||
export class VCAlert {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column() guild_id: string;
|
||||
|
||||
@Column() requestor_id: string;
|
||||
|
||||
@Column() user_id: string;
|
||||
|
||||
@Column() channel_id: string;
|
||||
|
||||
@Column() expires_at: string;
|
||||
|
||||
@Column() body: string;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue