├── .gitignore
├── .idea
├── Nertivia-Server.iml
├── jsLibraryMappings.xml
├── misc.xml
├── modules.xml
├── vcs.xml
└── workspace.xml
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── example.env
├── nodemon.json
├── package-lock.json
├── package.json
├── src
├── API
│ └── GDrive.js
├── Log.ts
├── ServerEventNames.ts
├── app.ts
├── custom.d.ts
├── emailBlacklist.json
├── index.ts
├── middlewares
│ ├── ChannelVerification.js
│ ├── GDriveOauthClient.js
│ ├── URLEmbed.ts
│ ├── UserPresentVerification.js
│ ├── authenticate.js
│ ├── avatarCacheLegacy.js
│ ├── channelRateLimit.js
│ ├── checkGDriveAvatarLegacy.js
│ ├── checkRolePermissions.js
│ ├── cors.ts
│ ├── disAllowBlockedUser.js
│ ├── isAdmin.js
│ ├── loadMedia.ts
│ ├── rateLimit.js
│ ├── realIP.ts
│ ├── redisSession.ts
│ └── serverChannelPermissions.js
├── models
│ ├── AdminActions.ts
│ ├── BannedIPs.ts
│ ├── BlockedUsers.ts
│ ├── Channels.ts
│ ├── CustomEmojis.ts
│ ├── Devices.ts
│ ├── Friends.ts
│ ├── MessageQuotes.ts
│ ├── MessageReactions.ts
│ ├── Messages.ts
│ ├── Notifications.ts
│ ├── PublicServers.ts
│ ├── PublicThemes.ts
│ ├── ServerInvites.ts
│ ├── ServerMembers.ts
│ ├── ServerRoles.ts
│ ├── Servers.ts
│ ├── Themes.ts
│ └── Users.ts
├── newRedisWrapper.ts
├── policies
│ ├── RolesPolicies.js
│ ├── ServerPolicies.js
│ ├── ThemePolicies.js
│ ├── UserPolicies.js
│ ├── authenticationPolicies.js
│ ├── errorReportPolicies.js
│ ├── forceCaptcha.js
│ ├── messagePolicies.js
│ ├── policyHandler.js
│ ├── publicThemePolicies.js
│ ├── reCaptchaPolicie.js
│ ├── relationshipPolicies.js
│ ├── settingsPolicies.js
│ └── surveyPolicies.js
├── redis.js
├── redis
│ └── instance.ts
├── routes
│ ├── admin
│ │ ├── Stats.ts
│ │ ├── approveTheme.js
│ │ ├── deleteServer.js
│ │ ├── getTheme.js
│ │ ├── index.js
│ │ ├── onlineUsers.js
│ │ ├── recentAdminActions.ts
│ │ ├── recentServers.ts
│ │ ├── recentUsers.ts
│ │ ├── sameIPUsers.js
│ │ ├── searchServers.js
│ │ ├── searchUsers.js
│ │ ├── suspendUser.js
│ │ ├── unsuspendUser.js
│ │ └── waitingThemes.js
│ ├── api.js
│ ├── bots
│ │ ├── botJoin.ts
│ │ ├── createBot.ts
│ │ ├── deleteBot.ts
│ │ ├── getBot.ts
│ │ ├── getCommands.ts
│ │ ├── index.js
│ │ ├── myBots.ts
│ │ ├── resetBotToken.ts
│ │ └── updateBot.ts
│ ├── channels
│ │ ├── deleteChannel.js
│ │ ├── getChannel.js
│ │ ├── index.js
│ │ └── openChannel.js
│ ├── chat.js
│ ├── chatBeta.js
│ ├── devices
│ │ ├── index.js
│ │ └── registerDevice.js
│ ├── errorReport
│ │ ├── index.js
│ │ └── reportError.js
│ ├── explore
│ │ ├── index.js
│ │ ├── servers
│ │ │ ├── addPublicServersList.js
│ │ │ ├── deletePublicServersList.js
│ │ │ ├── getPublicServersList.js
│ │ │ ├── getServer.js
│ │ │ ├── index.js
│ │ │ └── updatePublicServersList.js
│ │ └── themes
│ │ │ ├── AddLike.js
│ │ │ ├── addTheme.js
│ │ │ ├── applyPublicTheme.js
│ │ │ ├── getAllThemes.js
│ │ │ ├── getPublicTheme.js
│ │ │ ├── index.js
│ │ │ ├── removeLike.js
│ │ │ └── updateTheme.js
│ ├── files.js
│ ├── messages
│ │ ├── addReaction.js
│ │ ├── deleteMessage.js
│ │ ├── deleteMessageBulk.ts
│ │ ├── fileMessage.ts
│ │ ├── getMessage.js
│ │ ├── getMessages.js
│ │ ├── getReactedUsers.js
│ │ ├── index.js
│ │ ├── messageButtonCallback.js
│ │ ├── messageButtonClick.js
│ │ ├── removeReaction.js
│ │ ├── sendOrUpdateMessage.ts
│ │ └── sendTypingIndicator.js
│ ├── servers
│ │ ├── banMember.js
│ │ ├── bannedMembers.js
│ │ ├── channels
│ │ │ ├── channelPositions.js
│ │ │ ├── createServerChannel.js
│ │ │ ├── deleteServerChannel.js
│ │ │ ├── getServerChannels.js
│ │ │ ├── index.js
│ │ │ ├── muteServerChannel.js
│ │ │ ├── unmuteServerChannel.js
│ │ │ └── updateServerChannel.js
│ │ ├── createServer.js
│ │ ├── deleteServer.js
│ │ ├── getServer.js
│ │ ├── index.js
│ │ ├── invites
│ │ │ ├── createCustomInvite.js
│ │ │ ├── createInvite.js
│ │ │ ├── deleteInvite.js
│ │ │ ├── getInvites.js
│ │ │ ├── index.js
│ │ │ ├── inviteDetails.js
│ │ │ └── joinServer.js
│ │ ├── kickMember.js
│ │ ├── leaveServer.js
│ │ ├── muteServer.js
│ │ ├── roles
│ │ │ ├── applyRoleToMember.js
│ │ │ ├── createRole.js
│ │ │ ├── deleteRole.js
│ │ │ ├── index.js
│ │ │ ├── removeRoleFromMember.js
│ │ │ ├── updateRole.js
│ │ │ └── updateRolePosition.js
│ │ ├── unBanMember.js
│ │ └── updateServer.js
│ ├── settings
│ │ ├── addCustomEmoji.js
│ │ ├── changeAppearance.js
│ │ ├── changeCustomStatus.js
│ │ ├── changeStatus.js
│ │ ├── deleteCustomEmoji.js
│ │ ├── index.js
│ │ ├── linkGoogleDrive
│ │ │ ├── auth.js
│ │ │ ├── index.js
│ │ │ └── url.js
│ │ ├── renameCustomEmoji.js
│ │ └── serverPosition.js
│ ├── tenor
│ │ ├── getCategories.ts
│ │ ├── getSearches.ts
│ │ └── index.ts
│ ├── themes
│ │ ├── deleteTheme.ts
│ │ ├── getTheme.ts
│ │ ├── getThemes.ts
│ │ ├── index.ts
│ │ ├── saveTheme.ts
│ │ └── updateTheme.ts
│ ├── users
│ │ ├── agreeingPolicies.js
│ │ ├── blockUser.js
│ │ ├── confirmEmail.js
│ │ ├── deleteAccount.js
│ │ ├── htmlProfile
│ │ │ ├── addHtmlProfile.ts
│ │ │ ├── deleteHtmlProfile.ts
│ │ │ ├── getHtmlProfile.ts
│ │ │ └── index.ts
│ │ ├── index.js
│ │ ├── login.js
│ │ ├── logout.js
│ │ ├── register.js
│ │ ├── relationship
│ │ │ ├── acceptFriend.js
│ │ │ ├── addFriend.js
│ │ │ ├── index.js
│ │ │ └── removeFriend.js
│ │ ├── reset.js
│ │ ├── resetRequest.js
│ │ ├── survey
│ │ │ ├── index.js
│ │ │ ├── surveyDetails.js
│ │ │ └── surveyUpdate.js
│ │ ├── unblockUser.js
│ │ ├── userDetails.js
│ │ ├── userUpdate.js
│ │ └── welcomeDone.js
│ └── voice
│ │ ├── index.ts
│ │ ├── join.ts
│ │ └── leave.ts
├── services
│ └── Themes.ts
├── socket
│ └── instance.ts
├── socketController
│ ├── emitToAll.js
│ └── emitUserStatus.js
├── socketEvents
│ ├── index.js
│ └── notificationDismiss.js
├── socketIO.js
└── utils
│ ├── SendMessageNotification.js
│ ├── compressImage.ts
│ ├── cropImage.ts
│ ├── deleteServer.ts
│ ├── genFlakeId.ts
│ ├── getUserDetails.ts
│ ├── joinServer.ts
│ ├── kickUser.ts
│ ├── rolePermConstants.js
│ ├── sendPushNotification.ts
│ ├── tempSaveImage.ts
│ ├── tenor.ts
│ ├── uploadBase64Image.js
│ ├── uploadCDN
│ ├── googleDrive.ts
│ └── nertiviaCDN.ts
│ └── zip.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | src/config.ts
3 | src/public/
4 | src/fb-fcm.json
5 | dist
6 | .env
7 | server.env
8 | local.env
--------------------------------------------------------------------------------
/.idea/Nertivia-Server.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.tabSize": 2
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Nertivia---Server
2 | # Screenshots
3 | 
4 |
5 | To run Nertivia Server, please create the config.js and fb-fcm.json files, then run `node index.js`
6 |
--------------------------------------------------------------------------------
/example.env:
--------------------------------------------------------------------------------
1 | MONGODB_ADDRESS="mongodb://localhost"
2 | JWT_SECRET="somesecret"
3 | SESSION_SECRET="anothersecret"
4 |
5 | REDIS_HOST="localhost"
6 | REDIS_PORT=6379
7 | REDIS_PASS=""
8 |
9 | # https://tenor.com/developer/dashboard
10 | TENOR_KEY=""
11 |
12 | CAPTCHA_KEY="0x0000000000000000000000000000000000000000"
13 | CAPTCHA_SITE_KEY=""
14 |
15 |
16 | DOMAIN="localhost"
17 | ALLOWED_ORIGINS=["http://localhost:8080"]
18 |
19 | SMTP_SERVICE="gmail"
20 | SMTP_USER=""
21 | SMTP_PASS=""
22 | SMTP_FROM=""
23 |
24 | FILE_CDN_SECRET="www"
25 |
26 | # Google Drive
27 | DRIVE_CLIENT_ID=""
28 | DRIVE_CLIENT_SECRET=""
29 | DRIVE_URL=""
30 | DRIVE_KEY=""
31 |
32 |
33 | DEV_MODE=false
34 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"],
3 | "exec": "node index.js"
4 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "testss",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "compile": "tsc && npm run copyEnv && node dist",
8 | "build": "tsc",
9 | "copyEnv": "copyfiles -f ./.env dist",
10 | "runCompiled": "node dist",
11 | "watch": "tsc-watch --onSuccess \"npm run runCompiled\"",
12 | "dev": "npm run watch"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC",
17 | "dependencies": {
18 | "@socket.io/redis-adapter": "^7.0.0",
19 | "abort-controller": "^3.0.0",
20 | "bcryptjs": "^2.4.3",
21 | "body-parser": "^1.19.0",
22 | "cheerio": "^1.0.0-rc.3",
23 | "connect-busboy": "0.0.2",
24 | "connect-history-api-fallback": "^1.6.0",
25 | "connect-redis": "^4.0.4",
26 | "cookie-parser": "^1.4.5",
27 | "cors": "^2.8.5",
28 | "deep-email-validator": "^0.1.18",
29 | "dotenv": "^8.2.0",
30 | "express": "^4.17.1",
31 | "express-session": "^1.17.1",
32 | "express-validator": "^6.5.0",
33 | "fcm-node": "^1.5.2",
34 | "firebase-admin": "^9.3.0",
35 | "flakeid": "^1.0.0",
36 | "form-data": "^4.0.0",
37 | "gm": "^1.23.1",
38 | "googleapis": "^52.1.0",
39 | "html-safe-checker": "^1.1.2",
40 | "ip-range-check": "^0.2.0",
41 | "jsdom": "^16.2.2",
42 | "jsonhtmlfyer": "^0.3.3",
43 | "jsonwebtoken": "^8.5.1",
44 | "match-all": "^1.2.5",
45 | "mongoose": "^5.12.14",
46 | "node-fetch": "^2.6.6",
47 | "nodemailer": "^6.4.8",
48 | "pako": "^1.0.11",
49 | "redis": "^3.1.2",
50 | "sharp": "^0.25.4",
51 | "socket.io": "^4.1.2",
52 | "vhost": "^3.0.2"
53 | },
54 | "devDependencies": {
55 | "@types/bcryptjs": "^2.4.2",
56 | "@types/cheerio": "^0.22.18",
57 | "@types/connect-busboy": "0.0.2",
58 | "@types/connect-redis": "0.0.14",
59 | "@types/cors": "^2.8.6",
60 | "@types/express": "^4.17.6",
61 | "@types/express-serve-static-core": "^4.17.7",
62 | "@types/express-session": "^1.17.0",
63 | "@types/gm": "^1.18.8",
64 | "@types/jsonwebtoken": "^8.5.0",
65 | "@types/node-fetch": "^2.5.7",
66 | "@types/nodemailer": "^6.4.0",
67 | "@types/pako": "^1.0.1",
68 | "@types/redis": "^2.8.29",
69 | "@types/sharp": "^0.25.0",
70 | "copyfiles": "^2.3.0",
71 | "tsc-watch": "^4.5.0",
72 | "typescript": "^4.4.4"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Log.ts:
--------------------------------------------------------------------------------
1 | enum color {
2 | Reset = "\x1b[0m",
3 | Bright = "\x1b[1m",
4 | Dim = "\x1b[2m",
5 | Underscore = "\x1b[4m",
6 | Blink = "\x1b[5m",
7 | Reverse = "\x1b[7m",
8 | Hidden = "\x1b[8m",
9 |
10 | FgBlack = "\x1b[30m",
11 | FgRed = "\x1b[31m",
12 | FgGreen = "\x1b[32m",
13 | FgYellow = "\x1b[33m",
14 | FgBlue = "\x1b[34m",
15 | FgMagenta = "\x1b[35m",
16 | FgCyan = "\x1b[36m",
17 | FgWhite = "\x1b[37m",
18 |
19 | BgBlack = "\x1b[40m",
20 | BgRed = "\x1b[41m",
21 | BgGreen = "\x1b[42m",
22 | BgYellow = "\x1b[43m",
23 | BgBlue = "\x1b[44m",
24 | BgMagenta = "\x1b[45m",
25 | BgCyan = "\x1b[46m",
26 | BgWhite = "\x1b[47m"
27 | }
28 |
29 | function getTime() {
30 | const date = new Date();
31 |
32 | const hours = numberHandler(date.getHours());
33 | const minutes = numberHandler(date.getMinutes());
34 | const seconds = numberHandler(date.getSeconds());
35 |
36 | return `${hours}:${minutes}:${seconds}`;
37 | }
38 | function numberHandler(value: number) {
39 | return value > 9 ? value : `0${value}`;
40 | }
41 |
42 |
43 | export const Log = {
44 | info(...args: any) {
45 | console.log(`${color.FgCyan}[${getTime()}] ${color.FgGreen}[INFO]:${color.Reset}`, ...args);
46 | },
47 | warn(...args: any) {
48 | console.log(`${color.FgCyan}[${getTime()}] ${color.FgYellow}[INFO]:${color.Reset}`, ...args);
49 |
50 | },
51 | error(...args: any) {
52 | console.log(`${color.FgCyan}[${getTime()}] ${color.FgRed}[INFO]:${color.Reset}`, ...args);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/app.ts:
--------------------------------------------------------------------------------
1 | import {getIOInstance} from './socket/instance'
2 |
3 | import express from "express";
4 | import http from "http";
5 | // middlewares
6 | import realIP from "./middlewares/realIP";
7 | import redisSession from './middlewares/redisSession'
8 | import cors from "./middlewares/cors";
9 | import bodyParser from 'body-parser';
10 |
11 |
12 | export default function initServer() {
13 | const app = express();
14 | app.disable('x-powered-by');
15 |
16 | const server = new http.Server(app);
17 |
18 | const io = getIOInstance(server);
19 |
20 | //redis://[USER]:[PASSWORD]@[SERVICE-IP]:[PORT]
21 | // io.adapter(redisAdapter({
22 | // host: process.env.REDIS_HOST,
23 | // port: process.env.REDIS_PORT,
24 | // auth_pass: process.env.REDIS_PASS
25 | // }));
26 |
27 | // middlewares
28 | app.use(bodyParser.json({limit: '10mb'}));
29 | app.use(realIP);
30 | app.use(cors);
31 | app.use(function(req, res, next){
32 | req.io = io;
33 | next();
34 | })
35 | app.use(redisSession);
36 |
37 |
38 | // routes
39 | app.use('/api', require('./routes/api'));
40 |
41 | return server;
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/src/custom.d.ts:
--------------------------------------------------------------------------------
1 | import socketio from "socket.io";
2 |
3 | declare global {
4 | namespace Express {
5 | export interface Request {
6 | io: socketio.Server,
7 | userIP?: string | string[] | undefined,
8 | user: User,
9 | uploadFile: uploadFile,
10 | message_id: string,
11 | channel: Channel,
12 | permErrorMessage?: string,
13 | server: Server
14 | oauth2Client: any
15 | }
16 | }
17 | namespace NodeJS {
18 | interface ProcessEnv {
19 | MONGODB_ADDRESS: string
20 | JWT_SECRET: string
21 | JWT_HEADER: string
22 | SESSION_SECRET: string
23 |
24 | REDIS_HOST: string
25 | REDIS_PORT: string
26 | REDIS_PASS: string
27 |
28 | CAPTCHA_KEY: string
29 |
30 | DOMAIN: string
31 | ALLOWED_ORIGINS: string
32 |
33 | SMTP_SERVICE: string
34 | SMTP_USER: string
35 | SMTP_PASS: string
36 | SMTP_FROM: string
37 |
38 | FILE_CDN_SECRET: string
39 |
40 | DRIVE_CLIENT_ID: string
41 | DRIVE_CLIENT_SECRET: string
42 | DRIVE_URL:string
43 | DRIVE_KEY: string
44 |
45 | DEV_MODE: string
46 | }
47 | }
48 | }
49 | interface User {
50 | id: string
51 | _id: string
52 | username: string
53 | tag: string
54 | avatar: string
55 | admin: string
56 | bot?: boolean,
57 | badges?: number
58 | }
59 |
60 | interface uploadFile {
61 | file: object
62 | message: string
63 | }
64 | interface Channel {
65 | _id: string,
66 | channelId: string,
67 | server: Server
68 | recipients: any[]
69 | rateLimit?: number
70 | }
71 | interface Server {
72 | server_id: string
73 | _id: string
74 | }
--------------------------------------------------------------------------------
/src/middlewares/GDriveOauthClient.js:
--------------------------------------------------------------------------------
1 | const {
2 | google
3 | } = require('googleapis');
4 | import { Users } from "../models/Users";
5 |
6 | module.exports = async (req, res, next) => {
7 | const oauth2Client = new google.auth.OAuth2(
8 | process.env.DRIVE_CLIENT_ID,
9 | process.env.DRIVE_CLIENT_SECRET,
10 | process.env.DRIVE_URL
11 | );
12 | req.oauth2Client = oauth2Client;
13 |
14 | if (!req.session || !req.session['user']) return next()
15 | // check if GDriveRefreshToken exists in db
16 | if (!req.session['user'].GDriveRefreshToken) {
17 | const user = await Users.findById(req.session['user']._id, {_id: 0}).select('GDriveRefreshToken');
18 | if (user && user.GDriveRefreshToken) {
19 | req.session['user'].GDriveRefreshToken = user.GDriveRefreshToken
20 | }
21 | }
22 |
23 | if (req.session['user'].GDriveRefreshToken && !req.session['GDriveTokens']) {
24 | try {
25 | oauth2Client.setCredentials({
26 | refresh_token: req.session['user'].GDriveRefreshToken
27 | })
28 | const request_access_token = await oauth2Client.getAccessToken();
29 | req.session['GDriveTokens'] = request_access_token.res.data;
30 | oauth2Client.setCredentials(request_access_token.res.data);
31 | } catch (e) {
32 | console.log(e)
33 | }
34 | } else if (req.session['GDriveTokens']){
35 | oauth2Client.setCredentials(req.session['GDriveTokens']);
36 | }
37 |
38 | next()
39 | }
40 |
--------------------------------------------------------------------------------
/src/middlewares/avatarCacheLegacy.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const sharp = require('sharp')
3 | const fs = require('fs');
4 | module.exports = async (req, res, next) => {
5 |
6 | const id = req.params["0"].split("/")[0];
7 | const type = req.query.type;
8 |
9 | // find file
10 | fs.readdir( "public/cache", (err, files) => {
11 | if (!files) return next();
12 | const filePath = files.find(f => {
13 | const name = path.parse(f).name;
14 | return name === id
15 | })
16 | if (!filePath) return next();
17 | fs.readFile(`public/cache/${filePath}`, (err, buffer) => {
18 | res.set('Cache-Control', 'public, max-age=31536000');
19 | if (type && type === 'webp') {
20 | res.type('image/webp')
21 | sharp(buffer)
22 | .webp()
23 | .toBuffer()
24 | .then( data => { res.end(data); })
25 | .catch( err => {next()});
26 | } else {
27 | res.end(buffer);
28 | }
29 | })
30 | });
31 |
32 |
33 | };
--------------------------------------------------------------------------------
/src/middlewares/channelRateLimit.js:
--------------------------------------------------------------------------------
1 | const checkRolePermissions = require("./checkRolePermissions");
2 | const rateLimit = require("./rateLimit");
3 | const permissions = require('../utils/rolePermConstants');
4 |
5 | module.exports = function (req, res, next) {
6 | if (!req.channel.rateLimit) {
7 | return next();
8 | }
9 | checkRolePermissions("channel rate limit", permissions.roles.ADMIN, false)(req, res, () => {
10 | if (!req.permErrorMessage) {
11 | return next();
12 | }
13 | rateLimit({name: 'message-' + req.channel.channelId, expire: req.channel.rateLimit, requestsLimit: 1})(req, res, next);
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/src/middlewares/checkGDriveAvatarLegacy.js:
--------------------------------------------------------------------------------
1 | const sharp = require("sharp");
2 | const fs = require("fs");
3 | const {google} = require('googleapis');
4 |
5 | module.exports = async (req, res, next) => {
6 | const id = req.params["0"].split("/")[0];
7 | if (id === "default.png") return next();
8 | const url = `https://drive.google.com/uc?export=view&id=${id}`;
9 | const type = req.query.type;
10 |
11 |
12 | google.drive("v3").files.get({
13 | fileId: id,
14 | key: process.env.DRIVE_KEY,
15 | alt: 'media',
16 | }, {
17 | responseType: 'arraybuffer'
18 | }, (err, resp) => {
19 | if (err) return next()
20 | let contentType = resp.headers["content-type"];
21 | if (!contentType.startsWith("image/")) {
22 | res.status(404).end();
23 | return;
24 | }
25 | res.set("Cache-Control", "public, max-age=31536000");
26 | var buffer = new Buffer.from(resp.data, 'base64');
27 | if (type && type === "webp") {
28 | res.type('image/webp')
29 | sharp(buffer)
30 | .webp()
31 | .toBuffer()
32 | .then(data => {
33 | res.end(data);
34 | })
35 | .catch(err => {
36 | return next();
37 | });
38 | } else {
39 | res.end(buffer);
40 | }
41 | // save image to cache
42 | fs.writeFile(
43 | `public/cache/${id}.${contentType.split("/")[1]}`,
44 | buffer,
45 | "binary",
46 | () => {}
47 | );
48 | })
49 | };
50 |
--------------------------------------------------------------------------------
/src/middlewares/checkRolePermissions.js:
--------------------------------------------------------------------------------
1 | const { containsPerm, roles: {ADMIN} } = require('../utils/rolePermConstants');
2 | module.exports = function (name, flag, sendError = true) {
3 | return async function (req, res, next) {
4 |
5 |
6 | const server = req.server || req.channel.server;
7 | if (!req.channel) {
8 | if (!req.server) {
9 | sendErrorMessage(req, res, `No server!`, sendError, next)
10 | return;
11 | }
12 | } else if (!server) {
13 | return next();
14 | }
15 | // owner always has the permission
16 | const isCreator = server.creator === req.user._id
17 | if (isCreator) {
18 | return next();
19 | }
20 |
21 | if (req.permissions === undefined) {
22 | sendErrorMessage(req, res, `Missing permission! (${name})`, sendError, next)
23 | return;
24 | }
25 | if (containsPerm(req.permissions, flag | ADMIN)) {
26 | return next();
27 | } else {
28 | sendErrorMessage(req, res, `Missing permission! (${name})`, sendError, next)
29 | return;
30 | }
31 | }
32 | }
33 |
34 | function sendErrorMessage(req, res, message, sendError, next) {
35 | if (sendError) {
36 | res.status(403).json({ message })
37 | return;
38 | }
39 | req.permErrorMessage = message;
40 | next();
41 | }
--------------------------------------------------------------------------------
/src/middlewares/cors.ts:
--------------------------------------------------------------------------------
1 | import cors from "cors";
2 |
3 | export default cors({
4 | origin: function(origin, callback) {
5 | if (!origin) return callback(null, true);
6 | if (JSON.parse(process.env.ALLOWED_ORIGINS).indexOf(origin) === -1) {
7 | return callback(null, false)
8 | }
9 | return callback(null, true)
10 |
11 | },
12 | credentials: true,
13 | });
14 |
--------------------------------------------------------------------------------
/src/middlewares/disAllowBlockedUser.js:
--------------------------------------------------------------------------------
1 | module.exports = async (req, res, next) => {
2 |
3 | if (req.channel.isBlocked) {
4 | return res.status(403).json({
5 | message: "The user is either blocked by you, or them."
6 | });
7 | }
8 | next()
9 | }
10 |
--------------------------------------------------------------------------------
/src/middlewares/isAdmin.js:
--------------------------------------------------------------------------------
1 | module.exports = async (req, res, next) => {
2 | if (req.user.type !== "CREATOR" && req.user.type !== "ADMIN" ) {
3 | return res.status(401).json({message: 'Admin only! Go away.'})
4 | }
5 | next();
6 | };
--------------------------------------------------------------------------------
/src/middlewares/loadMedia.ts:
--------------------------------------------------------------------------------
1 | import { NextFunction, Request, Response } from "express";
2 | import sharp from "sharp";
3 |
4 | import fetch from "node-fetch";
5 | export default async (req: Request, res: Response, next: NextFunction) => {
6 | const id = req.params["0"].split("/")[0];
7 |
8 | const encode = encodeURIComponent(
9 | `https://drive.google.com/uc?export=view&id=${id}`
10 | );
11 | const url = `https://proxi.bree.workers.dev/cdn/${encode}`;
12 | const type = req.query.type;
13 | fetch(url).then(async (fetchResponse) => {
14 | if (fetchResponse.status !== 200) return res.status(404).end();
15 | const buffer = await fetchResponse.buffer();
16 | res.set("Cache-Control", "public, max-age=31536000");
17 | if (type !== "webp") {
18 | res.set(
19 | "content-type",
20 | fetchResponse.headers.get("content-type") || "image/png"
21 | );
22 | res.end(buffer);
23 | return;
24 | }
25 | res.set("content-type", "image/webp");
26 | res.type("image/webp");
27 | await sharp(buffer)
28 | .webp()
29 | .toBuffer()
30 | .then((data) => {
31 | res.end(data);
32 | })
33 | }).catch(() => {
34 | res.status(404).end();
35 | });
36 | };
--------------------------------------------------------------------------------
/src/middlewares/rateLimit.js:
--------------------------------------------------------------------------------
1 | const { checkRateLimited } = require('../newRedisWrapper.js');
2 | module.exports = function (options) {
3 | return async function (req, res, next) {
4 | const {name, expire, requestsLimit, useIP, nextIfInvalid} = options;
5 |
6 | const ttl = await checkRateLimited({
7 | id: req.userIP,
8 | userId: req.user?.id,
9 | expire,
10 | name,
11 | requestsLimit: requestsLimit
12 | })
13 |
14 | if (ttl && !nextIfInvalid) {
15 | res.status(429).json({
16 | message: 'Slow down!',
17 | ttl,
18 | })
19 | return;
20 | }
21 | if (ttl && nextIfInvalid) {
22 | req.rateLimited = true;
23 | }
24 | next();
25 |
26 | }
27 | }
--------------------------------------------------------------------------------
/src/middlewares/realIP.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import ipRangeCheck from "ip-range-check";
3 |
4 |
5 | export default (req: Request, res: Response, next: NextFunction) => {
6 | req.userIP = (req.headers["cf-connecting-ip"] ||
7 | req.headers["x-forwarded-for"] ||
8 | req.connection.remoteAddress)?.toString();
9 | next();
10 |
11 | };
12 |
--------------------------------------------------------------------------------
/src/middlewares/redisSession.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import connectRedis from "connect-redis";
3 | import session from "express-session";
4 | import JWT from 'jsonwebtoken';
5 | import {getRedisInstance} from '../redis/instance'
6 | import { RequestHandler } from "express-serve-static-core";
7 |
8 | const RedisStore = connectRedis(session);
9 |
10 |
11 | process.on('warning', (warning) => {
12 | console.warn(warning.name); // Print the warning name
13 | console.warn(warning.message); // Print the warning message
14 | console.warn(warning.stack); // Print the stack trace
15 | });
16 |
17 | let sessionInstance:RequestHandler | null = null;
18 |
19 | function getSessionInstance() {
20 | if (sessionInstance !== null) return sessionInstance;
21 | sessionInstance = session({
22 | secret: process.env.SESSION_SECRET,
23 | store: new RedisStore({
24 | client: getRedisInstance(),
25 | ttl: 600
26 | }),
27 | genid: req => {
28 | const token = process.env.JWT_HEADER + req.headers.authorization;
29 | try {
30 | // will contain user id
31 | const decryptedToken = JWT.verify(token, process.env.JWT_SECRET);
32 | return decryptedToken.toString().split("-")[0];
33 | } catch (err) {
34 | return "notLoggedIn";
35 | }
36 | },
37 | saveUninitialized: false,
38 | resave: false,
39 | })
40 | return sessionInstance;
41 | }
42 |
43 | export default (req: Request, res: Response, next: NextFunction) => {
44 | getSessionInstance()(req, res, next)
45 |
46 | };
47 |
--------------------------------------------------------------------------------
/src/middlewares/serverChannelPermissions.js:
--------------------------------------------------------------------------------
1 | const { containsPerm, roles: {ADMIN} } = require("../utils/rolePermConstants");
2 |
3 | function Permission(permission, defaultAllowed) {
4 | return Permission[permission, defaultAllowed] || (Permission[permission, defaultAllowed] = function(req, res, next) {
5 | if (!req.channel.server) return next();
6 | const permissions = req.channel.permissions;
7 |
8 | if (req.channel.server.creator === req.user._id) return next()
9 | // if user has admin role.
10 | if (containsPerm(req.permissions, ADMIN)) return next()
11 |
12 | if (defaultAllowed === false) {
13 | if (!permissions) {
14 | return res.status(403).json({
15 | status: false,
16 | message: "Missing permission: " + permission
17 | });
18 | }
19 | if (!permissions[permission]) {
20 | return res.status(403).json({
21 | status: false,
22 | message: "Missing permission: " + permission
23 | });
24 | }
25 | return next();
26 | }
27 |
28 |
29 | if (!permissions) {
30 | return next()
31 | }
32 |
33 | if (permissions[permission] === false) {
34 | return res.status(403).json({
35 | status: false,
36 | message: "Missing permission: " + permission
37 | });
38 | }
39 | if (permissions[permission] === true) {
40 | next()
41 | }
42 | })
43 | }
44 |
45 | module.exports = Permission
--------------------------------------------------------------------------------
/src/models/AdminActions.ts:
--------------------------------------------------------------------------------
1 | import {model, Schema} from 'mongoose';
2 |
3 |
4 | export interface AdminAction {
5 | action: string
6 | bannedIP: string,
7 | reason: string,
8 | admin: any,
9 | user: any,
10 | date: number,
11 | expireAt: number
12 | }
13 |
14 | const schema = new Schema({
15 | action: {type: String, enum: [
16 | "SUSPEND_USER",
17 | "UNSUSPEND_USER",
18 | "BAN_IP",
19 | "UNBAN_IP",
20 | "APPROVE_THEME"
21 | ]},
22 | bannedIP: String,
23 | reason: String,
24 | admin: { type: Schema.Types.ObjectId, ref: 'users'},
25 | user: { type: Schema.Types.ObjectId, ref: 'users'},
26 | date: Number,
27 | expireAt: {
28 | type: Date,
29 | default: Date.now,
30 | index: {expires: 398999 } // 4.6 days
31 | },
32 | });
33 |
34 |
35 | export const AdminActions = model('admin_actions', schema);
36 |
--------------------------------------------------------------------------------
/src/models/BannedIPs.ts:
--------------------------------------------------------------------------------
1 | import {Schema, model} from 'mongoose';
2 |
3 | export interface BannedIP {
4 | ip: string
5 | expireAt: number
6 | }
7 |
8 | const schema = new Schema({
9 | ip: {type: String, unique: true},
10 | expireAt: {
11 | type: Date,
12 | default: Date.now,
13 | index: {expires: 398999 } // 4.6 days
14 | },
15 | });
16 |
17 |
18 | export const BannedIPs = model('banned_ips', schema);
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/models/BlockedUsers.ts:
--------------------------------------------------------------------------------
1 | import {Schema, model} from 'mongoose';
2 |
3 | interface BlockedUser {
4 | requester: any;
5 | recipient: any;
6 | }
7 |
8 | const schema = new Schema({
9 | requester: { type: Schema.Types.ObjectId, ref: 'users'},
10 | recipient: { type: Schema.Types.ObjectId, ref: 'users'},
11 | })
12 |
13 |
14 | export const BlockedUsers = model('blocked_users', schema);
--------------------------------------------------------------------------------
/src/models/Channels.ts:
--------------------------------------------------------------------------------
1 |
2 | import {Schema, model} from 'mongoose';
3 |
4 |
5 | // TODO: replace this with bitwise permissions
6 | interface Permissions {
7 | send_message: boolean
8 | }
9 |
10 | export enum ChannelType {
11 | DM_CHANNEL = 0,
12 | SERVER_CHANNEL = 1,
13 | SERVER_CATEGORY = 2,
14 | }
15 | interface Channel {
16 | name: string,
17 | type: ChannelType,
18 | channelId: string
19 | categoryId: string;
20 | visibility: boolean
21 | creator: any
22 | recipients: any[]
23 | hide: boolean
24 | server: any
25 | server_id: any
26 | icon: string,
27 | lastMessaged: number
28 | rateLimit: number,
29 | status: number,
30 | permissions: Permissions
31 | }
32 | const permissionsSchema = new Schema({
33 | send_message: Boolean
34 | })
35 |
36 | const schema = new Schema({
37 | name: {type: String},
38 | channelId: { type: String, required: true },
39 | categoryId: {type: String, required: false},
40 | type: { type: Number, required: true, enums: [
41 | 0, // DM Channel
42 | 1, // Server Channel
43 | 2 // Server Category
44 | ]},
45 | visibility: {type: Boolean},
46 | creator: { type: Schema.Types.ObjectId, ref: 'users'},
47 | recipients: [{type: Schema.Types.ObjectId, ref: 'users'}],
48 | hide: {type: Boolean, select: false, required: false}, // only used for recent dms.
49 | server: {type: Schema.Types.ObjectId, ref: 'servers'},
50 | server_id: {type: String, required: false},
51 | icon: {type: String, required: false},
52 | lastMessaged: {type: Number, required: false},
53 | // in seconds
54 | rateLimit: {type: Number, required: false},
55 | status: {
56 | type: Number,
57 | default: 0,
58 | enums: [
59 | 0, //'not blocked',
60 | 1, //'blocked',
61 | ]
62 | },
63 | permissions: {type: permissionsSchema, select: true,}
64 | })
65 |
66 |
67 | export const Channels = model('channels', schema);
--------------------------------------------------------------------------------
/src/models/CustomEmojis.ts:
--------------------------------------------------------------------------------
1 | import {model, Schema} from 'mongoose';
2 |
3 | interface CustomEmoji {
4 | id: string
5 | user: any
6 | name: string
7 | gif: boolean
8 | }
9 |
10 | const schema = new Schema({
11 | id: {type: String, required: true},
12 | user: { type: Schema.Types.ObjectId, ref: 'users', required: true},
13 | name: { type: String, required: true},
14 | gif: {type: Boolean}
15 | })
16 |
17 |
18 | export const CustomEmojis = model('custom_emojis', schema);
--------------------------------------------------------------------------------
/src/models/Devices.ts:
--------------------------------------------------------------------------------
1 | import {model, Schema} from 'mongoose';
2 |
3 | interface Device {
4 | user: any
5 | userId: string,
6 | token: string,
7 | platform: string,
8 | }
9 |
10 |
11 | const schema = new Schema({
12 | user: { type: Schema.Types.ObjectId, ref: "users" },
13 | userId: {type: String},
14 | token: { type: String, unique: true },
15 | platform: { type: String }
16 | });
17 |
18 | export const Devices = model("devices", schema);
19 |
--------------------------------------------------------------------------------
/src/models/Friends.ts:
--------------------------------------------------------------------------------
1 | import {Schema, model} from 'mongoose';
2 |
3 | interface Friend {
4 | requester: any;
5 | recipient: any;
6 | status: number
7 | }
8 |
9 | const schema = new Schema({
10 | requester: { type: Schema.Types.ObjectId, ref: 'users'},
11 | recipient: { type: Schema.Types.ObjectId, ref: 'users'},
12 | status: {
13 | type: Number,
14 | enums: [
15 | 0, //'requested',
16 | 1, //'pending',
17 | 2, //'friends',
18 | ]
19 | }
20 | })
21 |
22 |
23 | export const Friends = model('friends', schema);
--------------------------------------------------------------------------------
/src/models/MessageQuotes.ts:
--------------------------------------------------------------------------------
1 | import {Schema, model} from 'mongoose'
2 |
3 |
4 |
5 | export interface MessageQuote {
6 | message: string,
7 | messageID: string,
8 | quotedChannel: any
9 | creator: any
10 | }
11 |
12 |
13 | const schema = new Schema({
14 | message: String,
15 | messageID: String,
16 | quotedChannel: { type : Schema.Types.ObjectId, ref: 'channels' },
17 | creator: { type : Schema.Types.ObjectId, ref: 'users' }
18 | });
19 |
20 |
21 | export const MessageQuotes = model('message_quotes', schema);
22 |
23 |
--------------------------------------------------------------------------------
/src/models/MessageReactions.ts:
--------------------------------------------------------------------------------
1 | import {model, Schema} from 'mongoose';
2 |
3 | interface MessageReaction {
4 | messageID: string,
5 | emojiID: string,
6 | unicode: string,
7 | gif: boolean,
8 | reactedBy: any[]
9 | }
10 |
11 |
12 | const schema = new Schema({
13 | messageID: String,
14 | emojiID: String,
15 | unicode: String,
16 | gif: Boolean,
17 | reactedBy: [{ type : Schema.Types.ObjectId, ref: 'users' }],
18 | })
19 |
20 |
21 |
22 |
23 |
24 | export const MessageReactions = model('message_reactions', schema);
--------------------------------------------------------------------------------
/src/models/Messages.ts:
--------------------------------------------------------------------------------
1 | import {Schema, Document, model} from 'mongoose'
2 | import flake from '../utils/genFlakeId'
3 | import { MessageQuote } from './MessageQuotes'
4 |
5 |
6 | interface Embed {
7 | title: string,
8 | type: string,
9 | url: string,
10 | image: any,
11 | site_name: string,
12 | description: string,
13 | }
14 | export interface Message {
15 | channelId: string
16 | messageID: string
17 | files?: any[]
18 | message: string
19 | creator: any
20 | created: number
21 | embed?: Embed
22 | buttons?: any[]
23 | htmlEmbed?: string
24 | mentions?: any[]
25 | quotes?: (MessageQuote & Document)['_id']
26 | timeEdited?: number
27 | color?: string
28 | type: MessageType
29 | }
30 |
31 | enum MessageType {
32 | MESSAGE = 0,
33 | JOIN_MESSAGE = 1,
34 | LEAVE_MESSAGE = 2,
35 | KICK_MESSAGE = 3,
36 | BAN_MESSAGE = 4
37 | }
38 |
39 | const embedSchema = new Schema