Merge branch 'typescript-interface-body-parser+autogenerate-unit-tests+documentation'
This commit is contained in:
		
						commit
						33533fc183
					
				
							
								
								
									
										7488
									
								
								api/assets/schemas.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7488
									
								
								api/assets/schemas.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										78
									
								
								api/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										78
									
								
								api/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -12,7 +12,7 @@ | ||||
| 			"dependencies": { | ||||
| 				"@fosscord/util": "file:../util", | ||||
| 				"ajv": "^8.4.0", | ||||
| 				"ajv-formats": "^2.1.0", | ||||
| 				"ajv-formats": "^2.1.1", | ||||
| 				"amqplib": "^0.8.0", | ||||
| 				"assert": "^1.5.0", | ||||
| 				"atomically": "^1.7.0", | ||||
| @ -38,6 +38,7 @@ | ||||
| 				"node-fetch": "^2.6.1", | ||||
| 				"patch-package": "^6.4.7", | ||||
| 				"supertest": "^6.1.6", | ||||
| 				"tsconfig-paths": "^3.11.0", | ||||
| 				"typeorm": "^0.2.37" | ||||
| 			}, | ||||
| 			"devDependencies": { | ||||
| @ -79,12 +80,13 @@ | ||||
| 				"env-paths": "^2.2.1", | ||||
| 				"jsonwebtoken": "^8.5.1", | ||||
| 				"lambert-server": "^1.2.10", | ||||
| 				"missing-native-js-functions": "^1.2.14", | ||||
| 				"missing-native-js-functions": "^1.2.15", | ||||
| 				"node-fetch": "^2.6.1", | ||||
| 				"patch-package": "^6.4.7", | ||||
| 				"pg": "^8.7.1", | ||||
| 				"reflect-metadata": "^0.1.13", | ||||
| 				"sqlite3": "^5.0.2", | ||||
| 				"tsconfig-paths": "^3.11.0", | ||||
| 				"typeorm": "^0.2.37", | ||||
| 				"typescript": "^4.4.2", | ||||
| 				"typescript-json-schema": "^0.50.1" | ||||
| @ -1396,6 +1398,11 @@ | ||||
| 			"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"node_modules/@types/json5": { | ||||
| 			"version": "0.0.29", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", | ||||
| 			"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" | ||||
| 		}, | ||||
| 		"node_modules/@types/jsonwebtoken": { | ||||
| 			"version": "8.5.4", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz", | ||||
| @ -11344,6 +11351,36 @@ | ||||
| 				"strip-json-comments": "^2.0.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/tsconfig-paths": { | ||||
| 			"version": "3.11.0", | ||||
| 			"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz", | ||||
| 			"integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==", | ||||
| 			"dependencies": { | ||||
| 				"@types/json5": "^0.0.29", | ||||
| 				"json5": "^1.0.1", | ||||
| 				"minimist": "^1.2.0", | ||||
| 				"strip-bom": "^3.0.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/tsconfig-paths/node_modules/json5": { | ||||
| 			"version": "1.0.1", | ||||
| 			"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", | ||||
| 			"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", | ||||
| 			"dependencies": { | ||||
| 				"minimist": "^1.2.0" | ||||
| 			}, | ||||
| 			"bin": { | ||||
| 				"json5": "lib/cli.js" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/tsconfig-paths/node_modules/strip-bom": { | ||||
| 			"version": "3.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", | ||||
| 			"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", | ||||
| 			"engines": { | ||||
| 				"node": ">=4" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/tsconfig/node_modules/strip-bom": { | ||||
| 			"version": "3.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", | ||||
| @ -12977,12 +13014,13 @@ | ||||
| 				"jest": "^27.0.6", | ||||
| 				"jsonwebtoken": "^8.5.1", | ||||
| 				"lambert-server": "^1.2.10", | ||||
| 				"missing-native-js-functions": "^1.2.14", | ||||
| 				"missing-native-js-functions": "^1.2.15", | ||||
| 				"node-fetch": "^2.6.1", | ||||
| 				"patch-package": "^6.4.7", | ||||
| 				"pg": "^8.7.1", | ||||
| 				"reflect-metadata": "^0.1.13", | ||||
| 				"sqlite3": "^5.0.2", | ||||
| 				"tsconfig-paths": "^3.11.0", | ||||
| 				"typeorm": "^0.2.37", | ||||
| 				"typescript": "^4.4.2", | ||||
| 				"typescript-json-schema": "^0.50.1" | ||||
| @ -13574,6 +13612,11 @@ | ||||
| 			"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"@types/json5": { | ||||
| 			"version": "0.0.29", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", | ||||
| 			"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" | ||||
| 		}, | ||||
| 		"@types/jsonwebtoken": { | ||||
| 			"version": "8.5.4", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz", | ||||
| @ -18703,8 +18746,7 @@ | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"mpath": { | ||||
| 			"version": "0.8.3", | ||||
| 			"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz", | ||||
| 			"version": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz", | ||||
| 			"integrity": "sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA==" | ||||
| 		}, | ||||
| 		"mquery": { | ||||
| @ -21726,6 +21768,32 @@ | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		"tsconfig-paths": { | ||||
| 			"version": "3.11.0", | ||||
| 			"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz", | ||||
| 			"integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==", | ||||
| 			"requires": { | ||||
| 				"@types/json5": "^0.0.29", | ||||
| 				"json5": "^1.0.1", | ||||
| 				"minimist": "^1.2.0", | ||||
| 				"strip-bom": "^3.0.0" | ||||
| 			}, | ||||
| 			"dependencies": { | ||||
| 				"json5": { | ||||
| 					"version": "1.0.1", | ||||
| 					"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", | ||||
| 					"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", | ||||
| 					"requires": { | ||||
| 						"minimist": "^1.2.0" | ||||
| 					} | ||||
| 				}, | ||||
| 				"strip-bom": { | ||||
| 					"version": "3.0.0", | ||||
| 					"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", | ||||
| 					"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		"tslib": { | ||||
| 			"version": "2.3.1", | ||||
| 			"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", | ||||
|  | ||||
| @ -7,12 +7,14 @@ | ||||
| 	"scripts": { | ||||
| 		"test": "npm run build && jest --coverage --verbose --forceExit ./tests", | ||||
| 		"test:watch": "jest --watch", | ||||
| 		"start": "npm run build && node dist/start", | ||||
| 		"start": "npm run build && node -r ./scripts/tsconfig-paths-bootstrap.js dist/start", | ||||
| 		"build": "npx tsc -b .", | ||||
| 		"build-docker": "tsc -p tsconfig-docker.json", | ||||
| 		"dev": "tsnd --respawn src/start.ts", | ||||
| 		"patch": "npx patch-package", | ||||
| 		"postinstall": "npm run patch" | ||||
| 		"postinstall": "npm run patch", | ||||
| 		"generate:docs": "ts-node scripts/generate_openapi_schema.ts", | ||||
| 		"generate:schema": "ts-node scripts/generate_body_schema.ts" | ||||
| 	}, | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
| @ -60,7 +62,7 @@ | ||||
| 	"dependencies": { | ||||
| 		"@fosscord/util": "file:../util", | ||||
| 		"ajv": "^8.4.0", | ||||
| 		"ajv-formats": "^2.1.0", | ||||
| 		"ajv-formats": "^2.1.1", | ||||
| 		"amqplib": "^0.8.0", | ||||
| 		"assert": "^1.5.0", | ||||
| 		"atomically": "^1.7.0", | ||||
| @ -86,6 +88,7 @@ | ||||
| 		"node-fetch": "^2.6.1", | ||||
| 		"patch-package": "^6.4.7", | ||||
| 		"supertest": "^6.1.6", | ||||
| 		"tsconfig-paths": "^3.11.0", | ||||
| 		"typeorm": "^0.2.37" | ||||
| 	}, | ||||
| 	"jest": { | ||||
|  | ||||
							
								
								
									
										249
									
								
								api/patches/ajv+8.6.2.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								api/patches/ajv+8.6.2.patch
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,249 @@ | ||||
| diff --git a/node_modules/ajv/dist/compile/jtd/parse.js b/node_modules/ajv/dist/compile/jtd/parse.js
 | ||||
| index 1eeb1be..7684121 100644
 | ||||
| --- a/node_modules/ajv/dist/compile/jtd/parse.js
 | ||||
| +++ b/node_modules/ajv/dist/compile/jtd/parse.js
 | ||||
| @@ -239,6 +239,9 @@ function parseType(cxt) {
 | ||||
|              gen.if(fail, () => parsingError(cxt, codegen_1.str `invalid timestamp`)); | ||||
|              break; | ||||
|          } | ||||
| +		case "bigint":
 | ||||
| +			parseBigInt(cxt);
 | ||||
| +			break
 | ||||
|          case "float32": | ||||
|          case "float64": | ||||
|              parseNumber(cxt); | ||||
| @@ -284,6 +287,15 @@ function parseNumber(cxt, maxDigits) {
 | ||||
|      skipWhitespace(cxt); | ||||
|      gen.if(codegen_1._ `"-0123456789".indexOf(${jsonSlice(1)}) < 0`, () => jsonSyntaxError(cxt), () => parseWith(cxt, parseJson_1.parseJsonNumber, maxDigits)); | ||||
|  } | ||||
| +function parseBigInt(cxt, maxDigits) {
 | ||||
| +  const {gen} = cxt
 | ||||
| +  skipWhitespace(cxt)
 | ||||
| +  gen.if(
 | ||||
| +    _`"-0123456789".indexOf(${jsonSlice(1)}) < 0`,
 | ||||
| +    () => jsonSyntaxError(cxt),
 | ||||
| +    () => parseWith(cxt, parseJson_1.parseJsonBigInt, maxDigits)
 | ||||
| +  )
 | ||||
| +}
 | ||||
|  function parseBooleanToken(bool, fail) { | ||||
|      return (cxt) => { | ||||
|          const { gen, data } = cxt; | ||||
| diff --git a/node_modules/ajv/dist/compile/rules.js b/node_modules/ajv/dist/compile/rules.js
 | ||||
| index 82a591f..1ebd8fe 100644
 | ||||
| --- a/node_modules/ajv/dist/compile/rules.js
 | ||||
| +++ b/node_modules/ajv/dist/compile/rules.js
 | ||||
| @@ -1,7 +1,7 @@
 | ||||
|  "use strict"; | ||||
|  Object.defineProperty(exports, "__esModule", { value: true }); | ||||
|  exports.getRules = exports.isJSONType = void 0; | ||||
| -const _jsonTypes = ["string", "number", "integer", "boolean", "null", "object", "array"];
 | ||||
| +const _jsonTypes = ["string", "number", "integer", "boolean", "null", "object", "array","bigint"];
 | ||||
|  const jsonTypes = new Set(_jsonTypes); | ||||
|  function isJSONType(x) { | ||||
|      return typeof x == "string" && jsonTypes.has(x); | ||||
| @@ -13,10 +13,11 @@ function getRules() {
 | ||||
|          string: { type: "string", rules: [] }, | ||||
|          array: { type: "array", rules: [] }, | ||||
|          object: { type: "object", rules: [] }, | ||||
| +		bigint: {type: "bigint", rules: []}
 | ||||
|      }; | ||||
|      return { | ||||
| -        types: { ...groups, integer: true, boolean: true, null: true },
 | ||||
| -        rules: [{ rules: [] }, groups.number, groups.string, groups.array, groups.object],
 | ||||
| +        types: { ...groups, integer: true, boolean: true, null: true, bigint: true },
 | ||||
| +        rules: [{ rules: [] }, groups.number, groups.string, groups.array, groups.object, groups.bigint],
 | ||||
|          post: { rules: [] }, | ||||
|          all: {}, | ||||
|          keywords: {}, | ||||
| diff --git a/node_modules/ajv/dist/compile/validate/dataType.js b/node_modules/ajv/dist/compile/validate/dataType.js
 | ||||
| index 6319e76..8b50b4c 100644
 | ||||
| --- a/node_modules/ajv/dist/compile/validate/dataType.js
 | ||||
| +++ b/node_modules/ajv/dist/compile/validate/dataType.js
 | ||||
| @@ -52,7 +52,7 @@ function coerceAndCheckDataType(it, types) {
 | ||||
|      return checkTypes; | ||||
|  } | ||||
|  exports.coerceAndCheckDataType = coerceAndCheckDataType; | ||||
| -const COERCIBLE = new Set(["string", "number", "integer", "boolean", "null"]);
 | ||||
| +const COERCIBLE = new Set(["string", "number", "integer", "boolean", "null","bigint"]);
 | ||||
|  function coerceToTypes(types, coerceTypes) { | ||||
|      return coerceTypes | ||||
|          ? types.filter((t) => COERCIBLE.has(t) || (coerceTypes === "array" && t === "array")) | ||||
| @@ -83,6 +83,14 @@ function coerceData(it, types, coerceTo) {
 | ||||
|      }); | ||||
|      function coerceSpecificType(t) { | ||||
|          switch (t) { | ||||
| +			case "bigint":
 | ||||
| +				gen
 | ||||
| +				.elseIf(
 | ||||
| +					codegen_1._`${dataType} == "boolean" || ${data} === null
 | ||||
| +					|| (${dataType} == "string" && ${data} && ${data} == BigInt(${data}))`
 | ||||
| +				)
 | ||||
| +				.assign(coerced, codegen_1._`BigInt(${data})`)
 | ||||
| +				return
 | ||||
|              case "string": | ||||
|                  gen | ||||
|                      .elseIf(codegen_1._ `${dataType} == "number" || ${dataType} == "boolean"`) | ||||
| @@ -143,6 +151,9 @@ function checkDataType(dataType, data, strictNums, correct = DataType.Correct) {
 | ||||
|          case "number": | ||||
|              cond = numCond(); | ||||
|              break; | ||||
| +		 case "bigint":
 | ||||
| +			cond = codegen_1._`typeof ${data} == "bigint" && isFinite(${data})`
 | ||||
| +			break
 | ||||
|          default: | ||||
|              return codegen_1._ `typeof ${data} ${EQ} ${dataType}`; | ||||
|      } | ||||
| diff --git a/node_modules/ajv/dist/refs/json-schema-2019-09/meta/validation.json b/node_modules/ajv/dist/refs/json-schema-2019-09/meta/validation.json
 | ||||
| index 7027a12..25679c8 100644
 | ||||
| --- a/node_modules/ajv/dist/refs/json-schema-2019-09/meta/validation.json
 | ||||
| +++ b/node_modules/ajv/dist/refs/json-schema-2019-09/meta/validation.json
 | ||||
| @@ -78,7 +78,7 @@
 | ||||
|        "default": 0 | ||||
|      }, | ||||
|      "simpleTypes": { | ||||
| -      "enum": ["array", "boolean", "integer", "null", "number", "object", "string"]
 | ||||
| +      "enum": ["array", "boolean", "integer", "null", "number", "object", "string","bigint"]
 | ||||
|      }, | ||||
|      "stringArray": { | ||||
|        "type": "array", | ||||
| diff --git a/node_modules/ajv/dist/refs/json-schema-2020-12/meta/validation.json b/node_modules/ajv/dist/refs/json-schema-2020-12/meta/validation.json
 | ||||
| index e0ae13d..57c9036 100644
 | ||||
| --- a/node_modules/ajv/dist/refs/json-schema-2020-12/meta/validation.json
 | ||||
| +++ b/node_modules/ajv/dist/refs/json-schema-2020-12/meta/validation.json
 | ||||
| @@ -78,7 +78,7 @@
 | ||||
|        "default": 0 | ||||
|      }, | ||||
|      "simpleTypes": { | ||||
| -      "enum": ["array", "boolean", "integer", "null", "number", "object", "string"]
 | ||||
| +      "enum": ["array", "boolean", "integer", "null", "number", "object", "string","bigint"]
 | ||||
|      }, | ||||
|      "stringArray": { | ||||
|        "type": "array", | ||||
| diff --git a/node_modules/ajv/dist/refs/json-schema-draft-06.json b/node_modules/ajv/dist/refs/json-schema-draft-06.json
 | ||||
| index 5410064..774435b 100644
 | ||||
| --- a/node_modules/ajv/dist/refs/json-schema-draft-06.json
 | ||||
| +++ b/node_modules/ajv/dist/refs/json-schema-draft-06.json
 | ||||
| @@ -16,7 +16,7 @@
 | ||||
|        "allOf": [{"$ref": "#/definitions/nonNegativeInteger"}, {"default": 0}] | ||||
|      }, | ||||
|      "simpleTypes": { | ||||
| -      "enum": ["array", "boolean", "integer", "null", "number", "object", "string"]
 | ||||
| +      "enum": ["array", "boolean", "integer", "null", "number", "object", "string","bigint"]
 | ||||
|      }, | ||||
|      "stringArray": { | ||||
|        "type": "array", | ||||
| diff --git a/node_modules/ajv/dist/refs/json-schema-draft-07.json b/node_modules/ajv/dist/refs/json-schema-draft-07.json
 | ||||
| index 6a74851..fc6dd7d 100644
 | ||||
| --- a/node_modules/ajv/dist/refs/json-schema-draft-07.json
 | ||||
| +++ b/node_modules/ajv/dist/refs/json-schema-draft-07.json
 | ||||
| @@ -16,7 +16,7 @@
 | ||||
|        "allOf": [{"$ref": "#/definitions/nonNegativeInteger"}, {"default": 0}] | ||||
|      }, | ||||
|      "simpleTypes": { | ||||
| -      "enum": ["array", "boolean", "integer", "null", "number", "object", "string"]
 | ||||
| +      "enum": ["array", "boolean", "integer", "null", "number", "object", "string","bigint"]
 | ||||
|      }, | ||||
|      "stringArray": { | ||||
|        "type": "array", | ||||
| diff --git a/node_modules/ajv/dist/refs/jtd-schema.js b/node_modules/ajv/dist/refs/jtd-schema.js
 | ||||
| index 1ee940a..1148887 100644
 | ||||
| --- a/node_modules/ajv/dist/refs/jtd-schema.js
 | ||||
| +++ b/node_modules/ajv/dist/refs/jtd-schema.js
 | ||||
| @@ -38,6 +38,7 @@ const typeForm = (root) => ({
 | ||||
|                  "uint16", | ||||
|                  "int32", | ||||
|                  "uint32", | ||||
| +                "bigint",
 | ||||
|              ], | ||||
|          }, | ||||
|      }, | ||||
| diff --git a/node_modules/ajv/dist/runtime/parseJson.js b/node_modules/ajv/dist/runtime/parseJson.js
 | ||||
| index 2576a6e..e7447b1 100644
 | ||||
| --- a/node_modules/ajv/dist/runtime/parseJson.js
 | ||||
| +++ b/node_modules/ajv/dist/runtime/parseJson.js
 | ||||
| @@ -97,6 +97,71 @@ exports.parseJsonNumber = parseJsonNumber;
 | ||||
|  parseJsonNumber.message = undefined; | ||||
|  parseJsonNumber.position = 0; | ||||
|  parseJsonNumber.code = 'require("ajv/dist/runtime/parseJson").parseJsonNumber'; | ||||
| +
 | ||||
| +function parseJsonBigInt(s, pos, maxDigits) {
 | ||||
| +    let numStr = "";
 | ||||
| +    let c;
 | ||||
| +    parseJsonBigInt.message = undefined;
 | ||||
| +    if (s[pos] === "-") {
 | ||||
| +        numStr += "-";
 | ||||
| +        pos++;
 | ||||
| +    }
 | ||||
| +    if (s[pos] === "0") {
 | ||||
| +        numStr += "0";
 | ||||
| +        pos++;
 | ||||
| +    }
 | ||||
| +    else {
 | ||||
| +        if (!parseDigits(maxDigits)) {
 | ||||
| +            errorMessage();
 | ||||
| +            return undefined;
 | ||||
| +        }
 | ||||
| +    }
 | ||||
| +    if (maxDigits) {
 | ||||
| +        parseJsonBigInt.position = pos;
 | ||||
| +        return BigInt(numStr);
 | ||||
| +    }
 | ||||
| +    if (s[pos] === ".") {
 | ||||
| +        numStr += ".";
 | ||||
| +        pos++;
 | ||||
| +        if (!parseDigits()) {
 | ||||
| +            errorMessage();
 | ||||
| +            return undefined;
 | ||||
| +        }
 | ||||
| +    }
 | ||||
| +    if (((c = s[pos]), c === "e" || c === "E")) {
 | ||||
| +        numStr += "e";
 | ||||
| +        pos++;
 | ||||
| +        if (((c = s[pos]), c === "+" || c === "-")) {
 | ||||
| +            numStr += c;
 | ||||
| +            pos++;
 | ||||
| +        }
 | ||||
| +        if (!parseDigits()) {
 | ||||
| +            errorMessage();
 | ||||
| +            return undefined;
 | ||||
| +        }
 | ||||
| +    }
 | ||||
| +    parseJsonBigInt.position = pos;
 | ||||
| +    return BigInt(numStr);
 | ||||
| +    function parseDigits(maxLen) {
 | ||||
| +        let digit = false;
 | ||||
| +        while (((c = s[pos]), c >= "0" && c <= "9" && (maxLen === undefined || maxLen-- > 0))) {
 | ||||
| +            digit = true;
 | ||||
| +            numStr += c;
 | ||||
| +            pos++;
 | ||||
| +        }
 | ||||
| +        return digit;
 | ||||
| +    }
 | ||||
| +    function errorMessage() {
 | ||||
| +        parseJsonBigInt.position = pos;
 | ||||
| +        parseJsonBigInt.message = pos < s.length ? `unexpected token ${s[pos]}` : "unexpected end";
 | ||||
| +    }
 | ||||
| +}
 | ||||
| +exports.parseJsonBigInt = parseJsonBigInt;
 | ||||
| +parseJsonBigInt.message = undefined;
 | ||||
| +parseJsonBigInt.position = 0;
 | ||||
| +parseJsonBigInt.code = 'require("ajv/dist/runtime/parseJson").parseJsonBigInt';
 | ||||
| +
 | ||||
| +
 | ||||
|  const escapedChars = { | ||||
|      b: "\b", | ||||
|      f: "\f", | ||||
| diff --git a/node_modules/ajv/dist/vocabularies/jtd/type.js b/node_modules/ajv/dist/vocabularies/jtd/type.js
 | ||||
| index 428bddb..fbc3070 100644
 | ||||
| --- a/node_modules/ajv/dist/vocabularies/jtd/type.js
 | ||||
| +++ b/node_modules/ajv/dist/vocabularies/jtd/type.js
 | ||||
| @@ -45,6 +45,9 @@ const def = {
 | ||||
|                  cond = timestampCode(cxt); | ||||
|                  break; | ||||
|              } | ||||
| +			case "bigint":
 | ||||
| +				cond = codegen_1._`typeof ${data} == "bigint" || typeof ${data} == "string"`
 | ||||
| +				break
 | ||||
|              case "float32": | ||||
|              case "float64": | ||||
|                  cond = codegen_1._ `typeof ${data} == "number"`; | ||||
							
								
								
									
										60
									
								
								api/scripts/generate_body_schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								api/scripts/generate_body_schema.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| // https://mermade.github.io/openapi-gui/#
 | ||||
| // https://editor.swagger.io/
 | ||||
| import path from "path"; | ||||
| import fs from "fs"; | ||||
| import * as TJS from "typescript-json-schema"; | ||||
| import "missing-native-js-functions"; | ||||
| const schemaPath = path.join(__dirname, "..", "assets", "schemas.json"); | ||||
| 
 | ||||
| const settings: TJS.PartialArgs = { | ||||
| 	required: true, | ||||
| 	ignoreErrors: true, | ||||
| 	excludePrivate: true, | ||||
| 	defaultNumberType: "integer", | ||||
| 	noExtraProps: true, | ||||
| 	defaultProps: false | ||||
| }; | ||||
| const compilerOptions: TJS.CompilerOptions = { | ||||
| 	strictNullChecks: false | ||||
| }; | ||||
| const ExcludedSchemas = ["DefaultSchema", "Schema", "EntitySchema"]; | ||||
| 
 | ||||
| function main() { | ||||
| 	const program = TJS.getProgramFromFiles(walk(path.join(__dirname, "..", "src", "routes")), compilerOptions); | ||||
| 	const generator = TJS.buildGenerator(program, settings); | ||||
| 	if (!generator || !program) return; | ||||
| 
 | ||||
| 	const schemas = generator.getUserSymbols().filter((x) => x.endsWith("Schema") && !ExcludedSchemas.includes(x)); | ||||
| 	console.log(schemas); | ||||
| 
 | ||||
| 	var definitions: any = {}; | ||||
| 
 | ||||
| 	for (const name of schemas) { | ||||
| 		const part = TJS.generateSchema(program, name, settings, [], generator as TJS.JsonSchemaGenerator); | ||||
| 		if (!part) continue; | ||||
| 
 | ||||
| 		definitions = { ...definitions, [name]: { ...part } }; | ||||
| 	} | ||||
| 
 | ||||
| 	fs.writeFileSync(schemaPath, JSON.stringify(definitions, null, 4)); | ||||
| } | ||||
| 
 | ||||
| // #/definitions/
 | ||||
| main(); | ||||
| 
 | ||||
| function walk(dir: string) { | ||||
| 	var results = [] as string[]; | ||||
| 	var list = fs.readdirSync(dir); | ||||
| 	list.forEach(function (file) { | ||||
| 		file = dir + "/" + file; | ||||
| 		var stat = fs.statSync(file); | ||||
| 		if (stat && stat.isDirectory()) { | ||||
| 			/* Recurse into a subdirectory */ | ||||
| 			results = results.concat(walk(file)); | ||||
| 		} else { | ||||
| 			if (!file.endsWith(".ts")) return; | ||||
| 			results.push(file); | ||||
| 		} | ||||
| 	}); | ||||
| 	return results; | ||||
| } | ||||
							
								
								
									
										10
									
								
								api/scripts/tsconfig-paths-bootstrap.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								api/scripts/tsconfig-paths-bootstrap.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| const tsConfigPaths = require("tsconfig-paths"); | ||||
| const path = require("path"); | ||||
| 
 | ||||
| const cleanup = tsConfigPaths.register({ | ||||
| 	baseUrl: path.join(__dirname, ".."), | ||||
| 	paths: { | ||||
| 		"@fosscord/api": ["dist/index.js"], | ||||
| 		"@fosscord/api/*": ["dist/*"] | ||||
| 	} | ||||
| }); | ||||
| @ -1,12 +1,3 @@ | ||||
| export * from "./Server"; | ||||
| export * from "./middlewares/"; | ||||
| export * from "./schema/Ban"; | ||||
| export * from "./schema/Channel"; | ||||
| export * from "./schema/Guild"; | ||||
| export * from "./schema/Invite"; | ||||
| export * from "./schema/Message"; | ||||
| export * from "./util/instanceOf"; | ||||
| export * from "./util/instanceOf"; | ||||
| export * from "./util/RandomInviteID"; | ||||
| export * from "./util/String"; | ||||
| export { check as checkPassword } from "./util/passwordStrength"; | ||||
| export * from "./util/"; | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { NextFunction, Request, Response } from "express"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { EntityNotFoundError } from "typeorm"; | ||||
| import { FieldError } from "../util/instanceOf"; | ||||
| import { FieldError } from "@fosscord/api"; | ||||
| import { ApiError } from "@fosscord/util"; | ||||
| 
 | ||||
| export function ErrorHandler(error: Error, req: Request, res: Response, next: NextFunction) { | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { Config, listenEvent } from "@fosscord/util"; | ||||
| import { NextFunction, Request, Response, Router } from "express"; | ||||
| import { getIpAdress } from "../util/ipAddress"; | ||||
| import { getIpAdress } from "@fosscord/api"; | ||||
| import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; | ||||
| 
 | ||||
| // Docs: https://discord.com/developers/docs/topics/rate-limits
 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { check, FieldErrors, Length } from "../../util/instanceOf"; | ||||
| import { FieldErrors, route } from "@fosscord/api"; | ||||
| import bcrypt from "bcrypt"; | ||||
| import jwt from "jsonwebtoken"; | ||||
| import { Config, User } from "@fosscord/util"; | ||||
| @ -8,67 +8,65 @@ import { adjustEmail } from "./register"; | ||||
| const router: Router = Router(); | ||||
| export default router; | ||||
| 
 | ||||
| router.post( | ||||
| 	"/", | ||||
| 	check({ | ||||
| 		login: new Length(String, 2, 100), // email or telephone
 | ||||
| 		password: new Length(String, 8, 72), | ||||
| 		$undelete: Boolean, | ||||
| 		$captcha_key: String, | ||||
| 		$login_source: String, | ||||
| 		$gift_code_sku_id: String | ||||
| 	}), | ||||
| 	async (req: Request, res: Response) => { | ||||
| 		const { login, password, captcha_key, undelete } = req.body; | ||||
| 		const email = adjustEmail(login); | ||||
| 		console.log("login", email); | ||||
| export interface LoginSchema { | ||||
| 	login: string; | ||||
| 	password: string; | ||||
| 	undelete?: boolean; | ||||
| 	captcha_key?: string; | ||||
| 	login_source?: string; | ||||
| 	gift_code_sku_id?: string; | ||||
| } | ||||
| 
 | ||||
| 		const config = Config.get(); | ||||
| router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Response) => { | ||||
| 	const { login, password, captcha_key, undelete } = req.body as LoginSchema; | ||||
| 	const email = adjustEmail(login); | ||||
| 	console.log("login", email); | ||||
| 
 | ||||
| 		if (config.login.requireCaptcha && config.security.captcha.enabled) { | ||||
| 			if (!captcha_key) { | ||||
| 				const { sitekey, service } = config.security.captcha; | ||||
| 				return res.status(400).json({ | ||||
| 					captcha_key: ["captcha-required"], | ||||
| 					captcha_sitekey: sitekey, | ||||
| 					captcha_service: service | ||||
| 				}); | ||||
| 			} | ||||
| 	const config = Config.get(); | ||||
| 
 | ||||
| 			// TODO: check captcha
 | ||||
| 	if (config.login.requireCaptcha && config.security.captcha.enabled) { | ||||
| 		if (!captcha_key) { | ||||
| 			const { sitekey, service } = config.security.captcha; | ||||
| 			return res.status(400).json({ | ||||
| 				captcha_key: ["captcha-required"], | ||||
| 				captcha_sitekey: sitekey, | ||||
| 				captcha_service: service | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		const user = await User.findOneOrFail({ | ||||
| 			where: [{ phone: login }, { email: login }], | ||||
| 			select: ["data", "id", "disabled", "deleted", "settings"] | ||||
| 		}).catch((e) => { | ||||
| 			throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } }); | ||||
| 		}); | ||||
| 
 | ||||
| 		if (undelete) { | ||||
| 			// undelete refers to un'disable' here
 | ||||
| 			if (user.disabled) await User.update({ id: user.id }, { disabled: false }); | ||||
| 			if (user.deleted) await User.update({ id: user.id }, { deleted: false }); | ||||
| 		} else { | ||||
| 			if (user.deleted) return res.status(400).json({ message: "This account is scheduled for deletion.", code: 20011 }); | ||||
| 			if (user.disabled) return res.status(400).json({ message: req.t("auth:login.ACCOUNT_DISABLED"), code: 20013 }); | ||||
| 		} | ||||
| 
 | ||||
| 		// the salt is saved in the password refer to bcrypt docs
 | ||||
| 		const same_password = await bcrypt.compare(password, user.data.hash || ""); | ||||
| 		if (!same_password) { | ||||
| 			throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); | ||||
| 		} | ||||
| 
 | ||||
| 		const token = await generateToken(user.id); | ||||
| 
 | ||||
| 		// Notice this will have a different token structure, than discord
 | ||||
| 		// Discord header is just the user id as string, which is not possible with npm-jsonwebtoken package
 | ||||
| 		// https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png
 | ||||
| 
 | ||||
| 		res.json({ token, settings: user.settings }); | ||||
| 		// TODO: check captcha
 | ||||
| 	} | ||||
| ); | ||||
| 
 | ||||
| 	const user = await User.findOneOrFail({ | ||||
| 		where: [{ phone: login }, { email: login }], | ||||
| 		select: ["data", "id", "disabled", "deleted", "settings"] | ||||
| 	}).catch((e) => { | ||||
| 		throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } }); | ||||
| 	}); | ||||
| 
 | ||||
| 	if (undelete) { | ||||
| 		// undelete refers to un'disable' here
 | ||||
| 		if (user.disabled) await User.update({ id: user.id }, { disabled: false }); | ||||
| 		if (user.deleted) await User.update({ id: user.id }, { deleted: false }); | ||||
| 	} else { | ||||
| 		if (user.deleted) return res.status(400).json({ message: "This account is scheduled for deletion.", code: 20011 }); | ||||
| 		if (user.disabled) return res.status(400).json({ message: req.t("auth:login.ACCOUNT_DISABLED"), code: 20013 }); | ||||
| 	} | ||||
| 
 | ||||
| 	// the salt is saved in the password refer to bcrypt docs
 | ||||
| 	const same_password = await bcrypt.compare(password, user.data.hash || ""); | ||||
| 	if (!same_password) { | ||||
| 		throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); | ||||
| 	} | ||||
| 
 | ||||
| 	const token = await generateToken(user.id); | ||||
| 
 | ||||
| 	// Notice this will have a different token structure, than discord
 | ||||
| 	// Discord header is just the user id as string, which is not possible with npm-jsonwebtoken package
 | ||||
| 	// https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png
 | ||||
| 
 | ||||
| 	res.json({ token, settings: user.settings }); | ||||
| }); | ||||
| 
 | ||||
| export async function generateToken(id: string) { | ||||
| 	const iat = Math.floor(Date.now() / 1000); | ||||
|  | ||||
| @ -1,217 +1,224 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { trimSpecial, User, Snowflake, Config, defaultSettings } from "@fosscord/util"; | ||||
| import bcrypt from "bcrypt"; | ||||
| import { check, Email, EMAIL_REGEX, FieldErrors, Length } from "../../util/instanceOf"; | ||||
| import { EMAIL_REGEX, FieldErrors, route } from "@fosscord/api"; | ||||
| import "missing-native-js-functions"; | ||||
| import { generateToken } from "./login"; | ||||
| import { getIpAdress, IPAnalysis, isProxy } from "../../util/ipAddress"; | ||||
| import { getIpAdress, IPAnalysis, isProxy } from "@fosscord/api"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { In } from "typeorm"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.post( | ||||
| 	"/", | ||||
| 	check({ | ||||
| 		username: new Length(String, 2, 32), | ||||
| 		// TODO: check min password length in config
 | ||||
| 		// prevent Denial of Service with max length of 72 chars
 | ||||
| 		password: new Length(String, 8, 72), | ||||
| 		consent: Boolean, | ||||
| 		$email: new Length(Email, 5, 100), | ||||
| 		$fingerprint: String, | ||||
| 		$invite: String, | ||||
| 		$date_of_birth: Date, // "2000-04-03"
 | ||||
| 		$gift_code_sku_id: String, | ||||
| 		$captcha_key: String | ||||
| 	}), | ||||
| 	async (req: Request, res: Response) => { | ||||
| 		const { | ||||
| 			email, | ||||
| 			username, | ||||
| 			password, | ||||
| 			consent, | ||||
| 			fingerprint, | ||||
| 			invite, | ||||
| 			date_of_birth, | ||||
| 			gift_code_sku_id, // ? what is this
 | ||||
| 			captcha_key | ||||
| 		} = req.body; | ||||
| export interface RegisterSchema { | ||||
| 	/** | ||||
| 	 * @minLength 2 | ||||
| 	 * @maxLength 32 | ||||
| 	 */ | ||||
| 	username: string; | ||||
| 	/** | ||||
| 	 * @minLength 1 | ||||
| 	 * @maxLength 72 | ||||
| 	 */ | ||||
| 	password: string; // TODO: use password strength of config
 | ||||
| 	consent: boolean; | ||||
| 	/** | ||||
| 	 * @TJS-format email | ||||
| 	 */ | ||||
| 	email?: string; | ||||
| 	fingerprint?: string; | ||||
| 	invite?: string; | ||||
| 	date_of_birth?: Date; // "2000-04-03"
 | ||||
| 	gift_code_sku_id?: string; | ||||
| 	captcha_key?: string; | ||||
| } | ||||
| 
 | ||||
| 		// get register Config
 | ||||
| 		const { register, security } = Config.get(); | ||||
| 		const ip = getIpAdress(req); | ||||
| router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => { | ||||
| 	const { | ||||
| 		email, | ||||
| 		username, | ||||
| 		password, | ||||
| 		consent, | ||||
| 		fingerprint, | ||||
| 		invite, | ||||
| 		date_of_birth, | ||||
| 		gift_code_sku_id, // ? what is this
 | ||||
| 		captcha_key | ||||
| 	} = req.body; | ||||
| 
 | ||||
| 		if (register.blockProxies) { | ||||
| 			if (isProxy(await IPAnalysis(ip))) { | ||||
| 				console.log(`proxy ${ip} blocked from registration`); | ||||
| 				throw new HTTPError("Your IP is blocked from registration"); | ||||
| 			} | ||||
| 	// get register Config
 | ||||
| 	const { register, security } = Config.get(); | ||||
| 	const ip = getIpAdress(req); | ||||
| 
 | ||||
| 	if (register.blockProxies) { | ||||
| 		if (isProxy(await IPAnalysis(ip))) { | ||||
| 			console.log(`proxy ${ip} blocked from registration`); | ||||
| 			throw new HTTPError("Your IP is blocked from registration"); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 		console.log("register", req.body.email, req.body.username, ip); | ||||
| 		// TODO: automatically join invite
 | ||||
| 		// TODO: gift_code_sku_id?
 | ||||
| 		// TODO: check password strength
 | ||||
| 	console.log("register", req.body.email, req.body.username, ip); | ||||
| 	// TODO: automatically join invite
 | ||||
| 	// TODO: gift_code_sku_id?
 | ||||
| 	// TODO: check password strength
 | ||||
| 
 | ||||
| 		// adjusted_email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick
 | ||||
| 		let adjusted_email = adjustEmail(email); | ||||
| 	// adjusted_email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick
 | ||||
| 	let adjusted_email = adjustEmail(email); | ||||
| 
 | ||||
| 		// adjusted_password will be the hash of the password
 | ||||
| 		let adjusted_password = ""; | ||||
| 	// adjusted_password will be the hash of the password
 | ||||
| 	let adjusted_password = ""; | ||||
| 
 | ||||
| 		// trim special uf8 control characters -> Backspace, Newline, ...
 | ||||
| 		let adjusted_username = trimSpecial(username); | ||||
| 	// trim special uf8 control characters -> Backspace, Newline, ...
 | ||||
| 	let adjusted_username = trimSpecial(username); | ||||
| 
 | ||||
| 		// discriminator will be randomly generated
 | ||||
| 		let discriminator = ""; | ||||
| 	// discriminator will be randomly generated
 | ||||
| 	let discriminator = ""; | ||||
| 
 | ||||
| 		// check if registration is allowed
 | ||||
| 		if (!register.allowNewRegistration) { | ||||
| 			throw FieldErrors({ | ||||
| 				email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") } | ||||
| 			}); | ||||
| 		} | ||||
| 	// check if registration is allowed
 | ||||
| 	if (!register.allowNewRegistration) { | ||||
| 		throw FieldErrors({ | ||||
| 			email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") } | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 		// check if the user agreed to the Terms of Service
 | ||||
| 		if (!consent) { | ||||
| 			throw FieldErrors({ | ||||
| 				consent: { code: "CONSENT_REQUIRED", message: req.t("auth:register.CONSENT_REQUIRED") } | ||||
| 			}); | ||||
| 		} | ||||
| 	// check if the user agreed to the Terms of Service
 | ||||
| 	if (!consent) { | ||||
| 		throw FieldErrors({ | ||||
| 			consent: { code: "CONSENT_REQUIRED", message: req.t("auth:register.CONSENT_REQUIRED") } | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 		// require invite to register -> e.g. for organizations to send invites to their employees
 | ||||
| 		if (register.requireInvite && !invite) { | ||||
| 			throw FieldErrors({ | ||||
| 				email: { code: "INVITE_ONLY", message: req.t("auth:register.INVITE_ONLY") } | ||||
| 			}); | ||||
| 		} | ||||
| 	// require invite to register -> e.g. for organizations to send invites to their employees
 | ||||
| 	if (register.requireInvite && !invite) { | ||||
| 		throw FieldErrors({ | ||||
| 			email: { code: "INVITE_ONLY", message: req.t("auth:register.INVITE_ONLY") } | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 		if (email) { | ||||
| 			// replace all dots and chars after +, if its a gmail.com email
 | ||||
| 			if (!adjusted_email) throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req.t("auth:register.INVALID_EMAIL") } }); | ||||
| 	if (email) { | ||||
| 		// replace all dots and chars after +, if its a gmail.com email
 | ||||
| 		if (!adjusted_email) throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req.t("auth:register.INVALID_EMAIL") } }); | ||||
| 
 | ||||
| 			// check if there is already an account with this email
 | ||||
| 			const exists = await User.findOneOrFail({ email: adjusted_email }).catch((e) => {}); | ||||
| 
 | ||||
| 			if (exists) { | ||||
| 				throw FieldErrors({ | ||||
| 					email: { | ||||
| 						code: "EMAIL_ALREADY_REGISTERED", | ||||
| 						message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		} else if (register.email.necessary) { | ||||
| 			throw FieldErrors({ | ||||
| 				email: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		if (register.dateOfBirth.necessary && !date_of_birth) { | ||||
| 			throw FieldErrors({ | ||||
| 				date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } | ||||
| 			}); | ||||
| 		} else if (register.dateOfBirth.minimum) { | ||||
| 			const minimum = new Date(); | ||||
| 			minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum); | ||||
| 
 | ||||
| 			// higher is younger
 | ||||
| 			if (date_of_birth > minimum) { | ||||
| 				throw FieldErrors({ | ||||
| 					date_of_birth: { | ||||
| 						code: "DATE_OF_BIRTH_UNDERAGE", | ||||
| 						message: req.t("auth:register.DATE_OF_BIRTH_UNDERAGE", { years: register.dateOfBirth.minimum }) | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (!register.allowMultipleAccounts) { | ||||
| 			// TODO: check if fingerprint was eligible generated
 | ||||
| 			const exists = await User.findOne({ where: { fingerprints: In(fingerprint) } }); | ||||
| 
 | ||||
| 			if (exists) { | ||||
| 				throw FieldErrors({ | ||||
| 					email: { | ||||
| 						code: "EMAIL_ALREADY_REGISTERED", | ||||
| 						message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (register.requireCaptcha && security.captcha.enabled) { | ||||
| 			if (!captcha_key) { | ||||
| 				const { sitekey, service } = security.captcha; | ||||
| 				return res.status(400).json({ | ||||
| 					captcha_key: ["captcha-required"], | ||||
| 					captcha_sitekey: sitekey, | ||||
| 					captcha_service: service | ||||
| 				}); | ||||
| 			} | ||||
| 
 | ||||
| 			// TODO: check captcha
 | ||||
| 		} | ||||
| 
 | ||||
| 		// the salt is saved in the password refer to bcrypt docs
 | ||||
| 		adjusted_password = await bcrypt.hash(password, 12); | ||||
| 
 | ||||
| 		let exists; | ||||
| 		// randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists
 | ||||
| 		// if it all five times already exists, abort with USERNAME_TOO_MANY_USERS error
 | ||||
| 		// else just continue
 | ||||
| 		// TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the mongodb database?
 | ||||
| 		for (let tries = 0; tries < 5; tries++) { | ||||
| 			discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0"); | ||||
| 			exists = await User.findOne({ where: { discriminator, username: adjusted_username }, select: ["id"] }); | ||||
| 			if (!exists) break; | ||||
| 		} | ||||
| 		// check if there is already an account with this email
 | ||||
| 		const exists = await User.findOneOrFail({ email: adjusted_email }).catch((e) => {}); | ||||
| 
 | ||||
| 		if (exists) { | ||||
| 			throw FieldErrors({ | ||||
| 				username: { | ||||
| 					code: "USERNAME_TOO_MANY_USERS", | ||||
| 					message: req.t("auth:register.USERNAME_TOO_MANY_USERS") | ||||
| 				email: { | ||||
| 					code: "EMAIL_ALREADY_REGISTERED", | ||||
| 					message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		// TODO: save date_of_birth
 | ||||
| 		// appearently discord doesn't save the date of birth and just calculate if nsfw is allowed
 | ||||
| 		// if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false
 | ||||
| 
 | ||||
| 		const user = await new User({ | ||||
| 			created_at: new Date(), | ||||
| 			username: adjusted_username, | ||||
| 			discriminator, | ||||
| 			id: Snowflake.generate(), | ||||
| 			bot: false, | ||||
| 			system: false, | ||||
| 			desktop: false, | ||||
| 			mobile: false, | ||||
| 			premium: true, | ||||
| 			premium_type: 2, | ||||
| 			bio: "", | ||||
| 			mfa_enabled: false, | ||||
| 			verified: false, | ||||
| 			disabled: false, | ||||
| 			deleted: false, | ||||
| 			email: adjusted_email, | ||||
| 			nsfw_allowed: true, // TODO: depending on age
 | ||||
| 			public_flags: "0", | ||||
| 			flags: "0", // TODO: generate
 | ||||
| 			data: { | ||||
| 				hash: adjusted_password, | ||||
| 				valid_tokens_since: new Date() | ||||
| 			}, | ||||
| 			settings: { ...defaultSettings, locale: req.language || "en-US" }, | ||||
| 			fingerprints: [] | ||||
| 		}).save(); | ||||
| 
 | ||||
| 		return res.json({ token: await generateToken(user.id) }); | ||||
| 	} else if (register.email.necessary) { | ||||
| 		throw FieldErrors({ | ||||
| 			email: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } | ||||
| 		}); | ||||
| 	} | ||||
| ); | ||||
| 
 | ||||
| 	if (register.dateOfBirth.necessary && !date_of_birth) { | ||||
| 		throw FieldErrors({ | ||||
| 			date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } | ||||
| 		}); | ||||
| 	} else if (register.dateOfBirth.minimum) { | ||||
| 		const minimum = new Date(); | ||||
| 		minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum); | ||||
| 
 | ||||
| 		// higher is younger
 | ||||
| 		if (date_of_birth > minimum) { | ||||
| 			throw FieldErrors({ | ||||
| 				date_of_birth: { | ||||
| 					code: "DATE_OF_BIRTH_UNDERAGE", | ||||
| 					message: req.t("auth:register.DATE_OF_BIRTH_UNDERAGE", { years: register.dateOfBirth.minimum }) | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if (!register.allowMultipleAccounts) { | ||||
| 		// TODO: check if fingerprint was eligible generated
 | ||||
| 		const exists = await User.findOne({ where: { fingerprints: In(fingerprint) } }); | ||||
| 
 | ||||
| 		if (exists) { | ||||
| 			throw FieldErrors({ | ||||
| 				email: { | ||||
| 					code: "EMAIL_ALREADY_REGISTERED", | ||||
| 					message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if (register.requireCaptcha && security.captcha.enabled) { | ||||
| 		if (!captcha_key) { | ||||
| 			const { sitekey, service } = security.captcha; | ||||
| 			return res.status(400).json({ | ||||
| 				captcha_key: ["captcha-required"], | ||||
| 				captcha_sitekey: sitekey, | ||||
| 				captcha_service: service | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		// TODO: check captcha
 | ||||
| 	} | ||||
| 
 | ||||
| 	// the salt is saved in the password refer to bcrypt docs
 | ||||
| 	adjusted_password = await bcrypt.hash(password, 12); | ||||
| 
 | ||||
| 	let exists; | ||||
| 	// randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists
 | ||||
| 	// if it all five times already exists, abort with USERNAME_TOO_MANY_USERS error
 | ||||
| 	// else just continue
 | ||||
| 	// TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the mongodb database?
 | ||||
| 	for (let tries = 0; tries < 5; tries++) { | ||||
| 		discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0"); | ||||
| 		exists = await User.findOne({ where: { discriminator, username: adjusted_username }, select: ["id"] }); | ||||
| 		if (!exists) break; | ||||
| 	} | ||||
| 
 | ||||
| 	if (exists) { | ||||
| 		throw FieldErrors({ | ||||
| 			username: { | ||||
| 				code: "USERNAME_TOO_MANY_USERS", | ||||
| 				message: req.t("auth:register.USERNAME_TOO_MANY_USERS") | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO: save date_of_birth
 | ||||
| 	// appearently discord doesn't save the date of birth and just calculate if nsfw is allowed
 | ||||
| 	// if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false
 | ||||
| 
 | ||||
| 	const user = await new User({ | ||||
| 		created_at: new Date(), | ||||
| 		username: adjusted_username, | ||||
| 		discriminator, | ||||
| 		id: Snowflake.generate(), | ||||
| 		bot: false, | ||||
| 		system: false, | ||||
| 		desktop: false, | ||||
| 		mobile: false, | ||||
| 		premium: true, | ||||
| 		premium_type: 2, | ||||
| 		bio: "", | ||||
| 		mfa_enabled: false, | ||||
| 		verified: false, | ||||
| 		disabled: false, | ||||
| 		deleted: false, | ||||
| 		email: adjusted_email, | ||||
| 		nsfw_allowed: true, // TODO: depending on age
 | ||||
| 		public_flags: "0", | ||||
| 		flags: "0", // TODO: generate
 | ||||
| 		data: { | ||||
| 			hash: adjusted_password, | ||||
| 			valid_tokens_since: new Date() | ||||
| 		}, | ||||
| 		settings: { ...defaultSettings, locale: req.language || "en-US" }, | ||||
| 		fingerprints: [] | ||||
| 	}).save(); | ||||
| 
 | ||||
| 	return res.json({ token: await generateToken(user.id) }); | ||||
| }); | ||||
| 
 | ||||
| export function adjustEmail(email: string): string | undefined { | ||||
| 	// body parser already checked if it is a valid email
 | ||||
|  | ||||
| @ -1,48 +1,59 @@ | ||||
| import { ChannelDeleteEvent, Channel, ChannelUpdateEvent, emitEvent, getPermission } from "@fosscord/util"; | ||||
| import { ChannelDeleteEvent, Channel, ChannelUpdateEvent, emitEvent, ChannelType, ChannelPermissionOverwriteType } from "@fosscord/util"; | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { ChannelModifySchema } from "../../../schema/Channel"; | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { route } from "@fosscord/api"; | ||||
| const router: Router = Router(); | ||||
| // TODO: delete channel
 | ||||
| // TODO: Get channel
 | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { | ||||
| 	const { channel_id } = req.params; | ||||
| 
 | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 
 | ||||
| 	const permission = await getPermission(req.user_id, channel.guild_id, channel_id); | ||||
| 	permission.hasThrow("VIEW_CHANNEL"); | ||||
| 
 | ||||
| 	return res.send(channel); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/", async (req: Request, res: Response) => { | ||||
| router.delete("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { | ||||
| 	const { channel_id } = req.params; | ||||
| 
 | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 
 | ||||
| 	const permission = await getPermission(req.user_id, channel?.guild_id, channel_id); | ||||
| 	permission.hasThrow("MANAGE_CHANNELS"); | ||||
| 
 | ||||
| 	// TODO: Dm channel "close" not delete
 | ||||
| 	const data = channel; | ||||
| 
 | ||||
| 	await emitEvent({ event: "CHANNEL_DELETE", data, channel_id } as ChannelDeleteEvent); | ||||
| 
 | ||||
| 	await Channel.delete({ id: channel_id }); | ||||
| 	await Promise.all([emitEvent({ event: "CHANNEL_DELETE", data, channel_id } as ChannelDeleteEvent), Channel.delete({ id: channel_id })]); | ||||
| 
 | ||||
| 	res.send(data); | ||||
| }); | ||||
| 
 | ||||
| router.patch("/", check(ChannelModifySchema), async (req: Request, res: Response) => { | ||||
| export interface ChannelModifySchema { | ||||
| 	/** | ||||
| 	 * @maxLength 100 | ||||
| 	 */ | ||||
| 	name: string; | ||||
| 	type: ChannelType; | ||||
| 	topic?: string; | ||||
| 	bitrate?: number; | ||||
| 	user_limit?: number; | ||||
| 	rate_limit_per_user?: number; | ||||
| 	position?: number; | ||||
| 	permission_overwrites?: { | ||||
| 		id: string; | ||||
| 		type: ChannelPermissionOverwriteType; | ||||
| 		allow: bigint; | ||||
| 		deny: bigint; | ||||
| 	}[]; | ||||
| 	parent_id?: string; | ||||
| 	id?: string; // is not used (only for guild create)
 | ||||
| 	nsfw?: boolean; | ||||
| 	rtc_region?: string; | ||||
| 	default_auto_archive_duration?: number; | ||||
| } | ||||
| 
 | ||||
| router.patch("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { | ||||
| 	var payload = req.body as ChannelModifySchema; | ||||
| 	const { channel_id } = req.params; | ||||
| 
 | ||||
| 	const permission = await getPermission(req.user_id, undefined, channel_id); | ||||
| 	permission.hasThrow("MANAGE_CHANNELS"); | ||||
| 
 | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 	channel.assign(payload); | ||||
| 
 | ||||
|  | ||||
| @ -1,14 +1,25 @@ | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { random } from "../../../util/RandomInviteID"; | ||||
| import { InviteCreateSchema } from "../../../schema/Invite"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { random } from "@fosscord/api"; | ||||
| import { getPermission, Channel, Invite, InviteCreateEvent, emitEvent, User, Guild, PublicInviteRelation } from "@fosscord/util"; | ||||
| import { isTextChannel } from "./messages"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) => { | ||||
| export interface InviteCreateSchema { | ||||
| 	target_user_id?: string; | ||||
| 	target_type?: string; | ||||
| 	validate?: string; //? wtf is this
 | ||||
| 	max_age?: number; | ||||
| 	max_uses?: number; | ||||
| 	temporary?: boolean; | ||||
| 	unique?: boolean; | ||||
| 	target_user?: string; | ||||
| 	target_user_type?: number; | ||||
| } | ||||
| 
 | ||||
| router.post("/", route({ body: "InviteCreateSchema", permission: "CREATE_INSTANT_INVITE" }), async (req: Request, res: Response) => { | ||||
| 	const { user_id } = req; | ||||
| 	const { channel_id } = req.params; | ||||
| 	const channel = await Channel.findOneOrFail({ where: { id: channel_id }, select: ["id", "name", "type", "guild_id"] }); | ||||
| @ -19,23 +30,6 @@ router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) | ||||
| 	} | ||||
| 	const { guild_id } = channel; | ||||
| 
 | ||||
| 	const permission = await getPermission(user_id, guild_id, undefined, { | ||||
| 		guild_select: [ | ||||
| 			"banner", | ||||
| 			"description", | ||||
| 			"features", | ||||
| 			"icon", | ||||
| 			"id", | ||||
| 			"name", | ||||
| 			"nsfw", | ||||
| 			"nsfw_level", | ||||
| 			"splash", | ||||
| 			"vanity_url_code", | ||||
| 			"verification_level" | ||||
| 		] as (keyof Guild)[] | ||||
| 	}); | ||||
| 	permission.hasThrow("CREATE_INSTANT_INVITE"); | ||||
| 
 | ||||
| 	const expires_at = new Date(req.body.max_age * 1000 + Date.now()); | ||||
| 
 | ||||
| 	const invite = await new Invite({ | ||||
| @ -52,14 +46,14 @@ router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) | ||||
| 	}).save(); | ||||
| 	const data = invite.toJSON(); | ||||
| 	data.inviter = await User.getPublicUser(req.user_id); | ||||
| 	data.guild = permission.cache.guild; | ||||
| 	data.guild = await Guild.findOne({ id: guild_id }); | ||||
| 	data.channel = channel; | ||||
| 
 | ||||
| 	await emitEvent({ event: "INVITE_CREATE", data, guild_id } as InviteCreateEvent); | ||||
| 	res.status(201).send(data); | ||||
| }); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { | ||||
| 	const { user_id } = req; | ||||
| 	const { channel_id } = req.params; | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| @ -68,8 +62,6 @@ router.get("/", async (req: Request, res: Response) => { | ||||
| 		throw new HTTPError("This channel doesn't exist", 404); | ||||
| 	} | ||||
| 	const { guild_id } = channel; | ||||
| 	const permission = await getPermission(user_id, guild_id); | ||||
| 	permission.hasThrow("MANAGE_CHANNELS"); | ||||
| 
 | ||||
| 	const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation }); | ||||
| 
 | ||||
|  | ||||
| @ -1,14 +1,18 @@ | ||||
| import { emitEvent, getPermission, MessageAckEvent, ReadState } from "@fosscord/util"; | ||||
| import { Request, Response, Router } from "express"; | ||||
| 
 | ||||
| import { check } from "../../../../../util/instanceOf"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| // TODO: check if message exists
 | ||||
| // TODO: send read state event to all channel members
 | ||||
| 
 | ||||
| router.post("/", check({ $manual: Boolean, $mention_count: Number }), async (req: Request, res: Response) => { | ||||
| export interface MessageAcknowledgeSchema { | ||||
| 	manual?: boolean; | ||||
| 	mention_count?: number; | ||||
| } | ||||
| 
 | ||||
| router.post("/", route({ body: "MessageAcknowledgeSchema" }), async (req: Request, res: Response) => { | ||||
| 	const { channel_id, message_id } = req.params; | ||||
| 
 | ||||
| 	const permission = await getPermission(req.user_id, undefined, channel_id); | ||||
|  | ||||
| @ -1,12 +1,13 @@ | ||||
| import { Channel, emitEvent, getPermission, MessageDeleteEvent, Message, MessageUpdateEvent } from "@fosscord/util"; | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { MessageCreateSchema } from "../../../../../schema/Message"; | ||||
| import { check } from "../../../../../util/instanceOf"; | ||||
| import { handleMessage, postHandleMessage } from "../../../../../util/Message"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { handleMessage, postHandleMessage } from "@fosscord/api"; | ||||
| import { MessageCreateSchema } from "../index"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| // TODO: message content/embed string length limit
 | ||||
| 
 | ||||
| router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response) => { | ||||
| router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES" }), async (req: Request, res: Response) => { | ||||
| 	const { message_id, channel_id } = req.params; | ||||
| 	var body = req.body as MessageCreateSchema; | ||||
| 
 | ||||
| @ -47,14 +48,17 @@ router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response | ||||
| 
 | ||||
| // TODO: delete attachments in message
 | ||||
| 
 | ||||
| router.delete("/", async (req: Request, res: Response) => { | ||||
| // permission check only if deletes messagr from other user
 | ||||
| router.delete("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const { message_id, channel_id } = req.params; | ||||
| 
 | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 	const message = await Message.findOneOrFail({ id: message_id }); | ||||
| 
 | ||||
| 	const permission = await getPermission(req.user_id, channel.guild_id, channel_id); | ||||
| 	if (message.author_id !== req.user_id) permission.hasThrow("MANAGE_MESSAGES"); | ||||
| 	if (message.author_id !== req.user_id) { | ||||
| 		const permission = await getPermission(req.user_id, channel.guild_id, channel_id); | ||||
| 		permission.hasThrow("MANAGE_MESSAGES"); | ||||
| 	} | ||||
| 
 | ||||
| 	await Message.delete({ id: message_id }); | ||||
| 
 | ||||
|  | ||||
| @ -13,6 +13,7 @@ import { | ||||
| 	PublicUserProjection, | ||||
| 	User | ||||
| } from "@fosscord/util"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { In } from "typeorm"; | ||||
| @ -35,14 +36,11 @@ function getEmoji(emoji: string): PartialEmoji { | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| router.delete("/", async (req: Request, res: Response) => { | ||||
| router.delete("/", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => { | ||||
| 	const { message_id, channel_id } = req.params; | ||||
| 
 | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 
 | ||||
| 	const permissions = await getPermission(req.user_id, undefined, channel_id); | ||||
| 	permissions.hasThrow("MANAGE_MESSAGES"); | ||||
| 
 | ||||
| 	await Message.update({ id: message_id, channel_id }, { reactions: [] }); | ||||
| 
 | ||||
| 	await emitEvent({ | ||||
| @ -58,13 +56,10 @@ router.delete("/", async (req: Request, res: Response) => { | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/:emoji", async (req: Request, res: Response) => { | ||||
| router.delete("/:emoji", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => { | ||||
| 	const { message_id, channel_id } = req.params; | ||||
| 	const emoji = getEmoji(req.params.emoji); | ||||
| 
 | ||||
| 	const permissions = await getPermission(req.user_id, undefined, channel_id); | ||||
| 	permissions.hasThrow("MANAGE_MESSAGES"); | ||||
| 
 | ||||
| 	const message = await Message.findOneOrFail({ id: message_id, channel_id }); | ||||
| 
 | ||||
| 	const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); | ||||
| @ -88,7 +83,7 @@ router.delete("/:emoji", async (req: Request, res: Response) => { | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| router.get("/:emoji", async (req: Request, res: Response) => { | ||||
| router.get("/:emoji", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { | ||||
| 	const { message_id, channel_id } = req.params; | ||||
| 	const emoji = getEmoji(req.params.emoji); | ||||
| 
 | ||||
| @ -96,9 +91,6 @@ router.get("/:emoji", async (req: Request, res: Response) => { | ||||
| 	const reaction = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); | ||||
| 	if (!reaction) throw new HTTPError("Reaction not found", 404); | ||||
| 
 | ||||
| 	const permissions = await getPermission(req.user_id, undefined, channel_id); | ||||
| 	permissions.hasThrow("VIEW_CHANNEL"); | ||||
| 
 | ||||
| 	const users = await User.find({ | ||||
| 		where: { | ||||
| 			id: In(reaction.user_ids) | ||||
| @ -109,7 +101,7 @@ router.get("/:emoji", async (req: Request, res: Response) => { | ||||
| 	res.json(users); | ||||
| }); | ||||
| 
 | ||||
| router.put("/:emoji/:user_id", async (req: Request, res: Response) => { | ||||
| router.put("/:emoji/:user_id", route({ permission: "READ_MESSAGE_HISTORY" }), async (req: Request, res: Response) => { | ||||
| 	const { message_id, channel_id, user_id } = req.params; | ||||
| 	if (user_id !== "@me") throw new HTTPError("Invalid user"); | ||||
| 	const emoji = getEmoji(req.params.emoji); | ||||
| @ -118,13 +110,11 @@ router.put("/:emoji/:user_id", async (req: Request, res: Response) => { | ||||
| 	const message = await Message.findOneOrFail({ id: message_id, channel_id }); | ||||
| 	const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); | ||||
| 
 | ||||
| 	const permissions = await getPermission(req.user_id, undefined, channel_id); | ||||
| 	permissions.hasThrow("READ_MESSAGE_HISTORY"); | ||||
| 	if (!already_added) permissions.hasThrow("ADD_REACTIONS"); | ||||
| 	if (!already_added) req.permission!.hasThrow("ADD_REACTIONS"); | ||||
| 
 | ||||
| 	if (emoji.id) { | ||||
| 		const external_emoji = await Emoji.findOneOrFail({ id: emoji.id }); | ||||
| 		if (!already_added) permissions.hasThrow("USE_EXTERNAL_EMOJIS"); | ||||
| 		if (!already_added) req.permission!.hasThrow("USE_EXTERNAL_EMOJIS"); | ||||
| 		emoji.animated = external_emoji.animated; | ||||
| 		emoji.name = external_emoji.name; | ||||
| 	} | ||||
| @ -154,7 +144,7 @@ router.put("/:emoji/:user_id", async (req: Request, res: Response) => { | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/:emoji/:user_id", async (req: Request, res: Response) => { | ||||
| router.delete("/:emoji/:user_id", route({}), async (req: Request, res: Response) => { | ||||
| 	var { message_id, channel_id, user_id } = req.params; | ||||
| 
 | ||||
| 	const emoji = getEmoji(req.params.emoji); | ||||
| @ -162,10 +152,11 @@ router.delete("/:emoji/:user_id", async (req: Request, res: Response) => { | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 	const message = await Message.findOneOrFail({ id: message_id, channel_id }); | ||||
| 
 | ||||
| 	const permissions = await getPermission(req.user_id, undefined, channel_id); | ||||
| 
 | ||||
| 	if (user_id === "@me") user_id = req.user_id; | ||||
| 	else permissions.hasThrow("MANAGE_MESSAGES"); | ||||
| 	else { | ||||
| 		const permissions = await getPermission(req.user_id, undefined, channel_id); | ||||
| 		permissions.hasThrow("MANAGE_MESSAGES"); | ||||
| 	} | ||||
| 
 | ||||
| 	const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); | ||||
| 	if (!already_added || !already_added.user_ids.includes(user_id)) throw new HTTPError("Reaction not found", 404); | ||||
|  | ||||
| @ -1,18 +1,21 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { Channel, Config, emitEvent, getPermission, MessageDeleteBulkEvent, Message } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| 
 | ||||
| import { check } from "../../../../util/instanceOf"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { In } from "typeorm"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| export default router; | ||||
| 
 | ||||
| export interface BulkDeleteSchema { | ||||
| 	messages: string[]; | ||||
| } | ||||
| 
 | ||||
| // TODO: should users be able to bulk delete messages or only bots?
 | ||||
| // TODO: should this request fail, if you provide messages older than 14 days/invalid ids?
 | ||||
| // https://discord.com/developers/docs/resources/channel#bulk-delete-messages
 | ||||
| router.post("/", check({ messages: [String] }), async (req: Request, res: Response) => { | ||||
| router.post("/", route({ body: "BulkDeleteSchema" }), async (req: Request, res: Response) => { | ||||
| 	const { channel_id } = req.params; | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 	if (!channel.guild_id) throw new HTTPError("Can't bulk delete dm channel messages", 400); | ||||
|  | ||||
| @ -1,12 +1,10 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { Attachment, Channel, ChannelType, getPermission, Message } from "@fosscord/util"; | ||||
| import { Attachment, Channel, ChannelType, Embed, getPermission, Message } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { MessageCreateSchema } from "../../../../schema/Message"; | ||||
| import { check, instanceOf, Length } from "../../../../util/instanceOf"; | ||||
| import { instanceOf, Length, route } from "@fosscord/api"; | ||||
| import multer from "multer"; | ||||
| import { Query } from "mongoose"; | ||||
| import { sendMessage } from "../../../../util/Message"; | ||||
| import { uploadFile } from "../../../../util/cdn"; | ||||
| import { sendMessage } from "@fosscord/api"; | ||||
| import { uploadFile } from "@fosscord/api"; | ||||
| import { FindManyOptions, LessThan, MoreThan } from "typeorm"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| @ -31,6 +29,30 @@ export function isTextChannel(type: ChannelType): boolean { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export interface MessageCreateSchema { | ||||
| 	content?: string; | ||||
| 	nonce?: string; | ||||
| 	tts?: boolean; | ||||
| 	flags?: string; | ||||
| 	embeds?: Embed[]; | ||||
| 	embed?: Embed; | ||||
| 	// TODO: ^ embed is deprecated in favor of embeds (https://discord.com/developers/docs/resources/channel#message-object)
 | ||||
| 	allowed_mentions?: { | ||||
| 		parse?: string[]; | ||||
| 		roles?: string[]; | ||||
| 		users?: string[]; | ||||
| 		replied_user?: boolean; | ||||
| 	}; | ||||
| 	message_reference?: { | ||||
| 		message_id: string; | ||||
| 		channel_id: string; | ||||
| 		guild_id?: string; | ||||
| 		fail_if_not_exists?: boolean; | ||||
| 	}; | ||||
| 	payload_json?: string; | ||||
| 	file?: any; | ||||
| } | ||||
| 
 | ||||
| // https://discord.com/developers/docs/resources/channel#create-message
 | ||||
| // get messages
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| @ -109,39 +131,44 @@ const messageUpload = multer({ | ||||
| // TODO: check allowed_mentions
 | ||||
| 
 | ||||
| // Send message
 | ||||
| router.post("/", messageUpload.single("file"), async (req: Request, res: Response) => { | ||||
| 	const { channel_id } = req.params; | ||||
| 	var body = req.body as MessageCreateSchema; | ||||
| 	const attachments: Attachment[] = []; | ||||
| 
 | ||||
| 	if (req.file) { | ||||
| 		try { | ||||
| 			const file = await uploadFile(`/attachments/${channel_id}`, req.file); | ||||
| 			attachments.push({ ...file, proxy_url: file.url }); | ||||
| 		} catch (error) { | ||||
| 			return res.status(400).json(error); | ||||
| router.post( | ||||
| 	"/", | ||||
| 	messageUpload.single("file"), | ||||
| 	async (req, res, next) => { | ||||
| 		if (req.body.payload_json) { | ||||
| 			req.body = JSON.parse(req.body.payload_json); | ||||
| 		} | ||||
| 
 | ||||
| 		next(); | ||||
| 	}, | ||||
| 	route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES" }), | ||||
| 	async (req: Request, res: Response) => { | ||||
| 		const { channel_id } = req.params; | ||||
| 		var body = req.body as MessageCreateSchema; | ||||
| 		const attachments: Attachment[] = []; | ||||
| 
 | ||||
| 		if (req.file) { | ||||
| 			try { | ||||
| 				const file = await uploadFile(`/attachments/${req.params.channel_id}`, req.file); | ||||
| 				attachments.push({ ...file, proxy_url: file.url }); | ||||
| 			} catch (error) { | ||||
| 				return res.status(400).json(error); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		const embeds = []; | ||||
| 		if (body.embed) embeds.push(body.embed); | ||||
| 		const data = await sendMessage({ | ||||
| 			...body, | ||||
| 			type: 0, | ||||
| 			pinned: false, | ||||
| 			author_id: req.user_id, | ||||
| 			embeds, | ||||
| 			channel_id, | ||||
| 			attachments, | ||||
| 			edited_timestamp: undefined | ||||
| 		}); | ||||
| 
 | ||||
| 		return res.json(data); | ||||
| 	} | ||||
| 
 | ||||
| 	if (body.payload_json) { | ||||
| 		body = JSON.parse(body.payload_json); | ||||
| 	} | ||||
| 
 | ||||
| 	const errors = instanceOf(MessageCreateSchema, body, { req }); | ||||
| 	if (errors !== true) throw errors; | ||||
| 
 | ||||
| 	const embeds = []; | ||||
| 	if (body.embed) embeds.push(body.embed); | ||||
| 	const data = await sendMessage({ | ||||
| 		...body, | ||||
| 		type: 0, | ||||
| 		pinned: false, | ||||
| 		author_id: req.user_id, | ||||
| 		embeds, | ||||
| 		channel_id, | ||||
| 		attachments, | ||||
| 		edited_timestamp: undefined | ||||
| 	}); | ||||
| 
 | ||||
| 	return res.json(data); | ||||
| }); | ||||
| ); | ||||
|  | ||||
| @ -2,61 +2,61 @@ import { Channel, ChannelPermissionOverwrite, ChannelUpdateEvent, emitEvent, get | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| 
 | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { check, route } from "@fosscord/api"; | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| // TODO: Only permissions your bot has in the guild or channel can be allowed/denied (unless your bot has a MANAGE_ROLES overwrite in the channel)
 | ||||
| 
 | ||||
| router.put("/:overwrite_id", check({ allow: String, deny: String, type: Number, id: String }), async (req: Request, res: Response) => { | ||||
| 	const { channel_id, overwrite_id } = req.params; | ||||
| 	const body = req.body as { allow: bigint; deny: bigint; type: number; id: string }; | ||||
| export interface ChannelPermissionOverwriteSchema extends ChannelPermissionOverwrite {} | ||||
| 
 | ||||
| 	var channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 	if (!channel.guild_id) throw new HTTPError("Channel not found", 404); | ||||
| router.put( | ||||
| 	"/:overwrite_id", | ||||
| 	route({ body: "ChannelPermissionOverwriteSchema", permission: "MANAGE_ROLES" }), | ||||
| 	async (req: Request, res: Response) => { | ||||
| 		const { channel_id, overwrite_id } = req.params; | ||||
| 		const body = req.body as { allow: bigint; deny: bigint; type: number; id: string }; | ||||
| 
 | ||||
| 	const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); | ||||
| 	permissions.hasThrow("MANAGE_ROLES"); | ||||
| 		var channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 		if (!channel.guild_id) throw new HTTPError("Channel not found", 404); | ||||
| 
 | ||||
| 	if (body.type === 0) { | ||||
| 		if (!(await Role.count({ id: overwrite_id }))) throw new HTTPError("role not found", 404); | ||||
| 	} else if (body.type === 1) { | ||||
| 		if (!(await Member.count({ id: overwrite_id }))) throw new HTTPError("user not found", 404); | ||||
| 	} else throw new HTTPError("type not supported", 501); | ||||
| 		if (body.type === 0) { | ||||
| 			if (!(await Role.count({ id: overwrite_id }))) throw new HTTPError("role not found", 404); | ||||
| 		} else if (body.type === 1) { | ||||
| 			if (!(await Member.count({ id: overwrite_id }))) throw new HTTPError("user not found", 404); | ||||
| 		} else throw new HTTPError("type not supported", 501); | ||||
| 
 | ||||
| 	// @ts-ignore
 | ||||
| 	var overwrite: ChannelPermissionOverwrite = channel.permission_overwrites.find((x) => x.id === overwrite_id); | ||||
| 	if (!overwrite) { | ||||
| 		// @ts-ignore
 | ||||
| 		overwrite = { | ||||
| 			id: overwrite_id, | ||||
| 			type: body.type, | ||||
| 			allow: body.allow, | ||||
| 			deny: body.deny | ||||
| 		}; | ||||
| 		channel.permission_overwrites.push(overwrite); | ||||
| 		var overwrite: ChannelPermissionOverwrite = channel.permission_overwrites.find((x) => x.id === overwrite_id); | ||||
| 		if (!overwrite) { | ||||
| 			// @ts-ignore
 | ||||
| 			overwrite = { | ||||
| 				id: overwrite_id, | ||||
| 				type: body.type, | ||||
| 				allow: body.allow, | ||||
| 				deny: body.deny | ||||
| 			}; | ||||
| 			channel.permission_overwrites.push(overwrite); | ||||
| 		} | ||||
| 		overwrite.allow = body.allow; | ||||
| 		overwrite.deny = body.deny; | ||||
| 
 | ||||
| 		await Promise.all([ | ||||
| 			channel.save(), | ||||
| 			emitEvent({ | ||||
| 				event: "CHANNEL_UPDATE", | ||||
| 				channel_id, | ||||
| 				data: channel | ||||
| 			} as ChannelUpdateEvent) | ||||
| 		]); | ||||
| 
 | ||||
| 		return res.sendStatus(204); | ||||
| 	} | ||||
| 	overwrite.allow = body.allow; | ||||
| 	overwrite.deny = body.deny; | ||||
| 
 | ||||
| 	// @ts-ignore
 | ||||
| 	channel = await Channel.findOneOrFailAndUpdate({ id: channel_id }, channel, { new: true }); | ||||
| 
 | ||||
| 	await emitEvent({ | ||||
| 		event: "CHANNEL_UPDATE", | ||||
| 		channel_id, | ||||
| 		data: channel | ||||
| 	} as ChannelUpdateEvent); | ||||
| 
 | ||||
| 	return res.sendStatus(204); | ||||
| }); | ||||
| ); | ||||
| 
 | ||||
| // TODO: check permission hierarchy
 | ||||
| router.delete("/:overwrite_id", async (req: Request, res: Response) => { | ||||
| router.delete("/:overwrite_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { | ||||
| 	const { channel_id, overwrite_id } = req.params; | ||||
| 
 | ||||
| 	const permissions = await getPermission(req.user_id, undefined, channel_id); | ||||
| 	permissions.hasThrow("MANAGE_ROLES"); | ||||
| 
 | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 	if (!channel.guild_id) throw new HTTPError("Channel not found", 404); | ||||
| 
 | ||||
|  | ||||
| @ -1,19 +1,26 @@ | ||||
| import { Channel, ChannelPinsUpdateEvent, Config, emitEvent, getPermission, Message, MessageUpdateEvent } from "@fosscord/util"; | ||||
| import { | ||||
| 	Channel, | ||||
| 	ChannelPinsUpdateEvent, | ||||
| 	Config, | ||||
| 	emitEvent, | ||||
| 	getPermission, | ||||
| 	Message, | ||||
| 	MessageUpdateEvent, | ||||
| 	DiscordApiErrors | ||||
| } from "@fosscord/util"; | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { DiscordApiErrors } from "@fosscord/util"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.put("/:message_id", async (req: Request, res: Response) => { | ||||
| router.put("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { | ||||
| 	const { channel_id, message_id } = req.params; | ||||
| 
 | ||||
| 	const message = await Message.findOneOrFail({ id: message_id }); | ||||
| 	const permission = await getPermission(req.user_id, message.guild_id, channel_id); | ||||
| 	permission.hasThrow("VIEW_CHANNEL"); | ||||
| 
 | ||||
| 	// * in dm channels anyone can pin messages -> only check for guilds
 | ||||
| 	if (message.guild_id) permission.hasThrow("MANAGE_MESSAGES"); | ||||
| 	if (message.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); | ||||
| 
 | ||||
| 	const pinned_count = await Message.count({ channel: { id: channel_id }, pinned: true }); | ||||
| 	const { maxPins } = Config.get().limits.channel; | ||||
| @ -26,7 +33,6 @@ router.put("/:message_id", async (req: Request, res: Response) => { | ||||
| 			channel_id, | ||||
| 			data: message | ||||
| 		} as MessageUpdateEvent), | ||||
| 
 | ||||
| 		emitEvent({ | ||||
| 			event: "CHANNEL_PINS_UPDATE", | ||||
| 			channel_id, | ||||
| @ -41,14 +47,11 @@ router.put("/:message_id", async (req: Request, res: Response) => { | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/:message_id", async (req: Request, res: Response) => { | ||||
| router.delete("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { | ||||
| 	const { channel_id, message_id } = req.params; | ||||
| 
 | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 
 | ||||
| 	const permission = await getPermission(req.user_id, channel.guild_id, channel_id); | ||||
| 	permission.hasThrow("VIEW_CHANNEL"); | ||||
| 	if (channel.guild_id) permission.hasThrow("MANAGE_MESSAGES"); | ||||
| 	if (channel.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); | ||||
| 
 | ||||
| 	const message = await Message.findOneOrFail({ id: message_id }); | ||||
| 	message.pinned = false; | ||||
| @ -76,13 +79,9 @@ router.delete("/:message_id", async (req: Request, res: Response) => { | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({ permission: ["READ_MESSAGE_HISTORY"] }), async (req: Request, res: Response) => { | ||||
| 	const { channel_id } = req.params; | ||||
| 
 | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 	const permission = await getPermission(req.user_id, channel.guild_id, channel_id); | ||||
| 	permission.hasThrow("VIEW_CHANNEL"); | ||||
| 
 | ||||
| 	let pins = await Message.find({ channel_id: channel_id, pinned: true }); | ||||
| 
 | ||||
| 	res.send(pins); | ||||
|  | ||||
| @ -1,11 +1,10 @@ | ||||
| import { Channel, emitEvent, Member, TypingStartEvent } from "@fosscord/util"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { Router, Request, Response } from "express"; | ||||
| 
 | ||||
| import { HTTPError } from "lambert-server"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.post("/", async (req: Request, res: Response) => { | ||||
| router.post("/", route({ permission: "SEND_MESSAGES" }), async (req: Request, res: Response) => { | ||||
| 	const { channel_id } = req.params; | ||||
| 	const user_id = req.user_id; | ||||
| 	const timestamp = Date.now(); | ||||
| @ -24,6 +23,7 @@ router.post("/", async (req: Request, res: Response) => { | ||||
| 			guild_id: channel.guild_id | ||||
| 		} | ||||
| 	} as TypingStartEvent); | ||||
| 
 | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { check, Length } from "../../../util/instanceOf"; | ||||
| import { check, Length, route } from "@fosscord/api"; | ||||
| import { Channel, Config, getPermission, trimSpecial, Webhook } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { isTextChannel } from "./messages/index"; | ||||
| @ -7,9 +7,16 @@ import { DiscordApiErrors } from "@fosscord/util"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| // TODO: webhooks
 | ||||
| export interface WebhookCreateSchema { | ||||
| 	/** | ||||
| 	 * @maxLength 80 | ||||
| 	 */ | ||||
| 	name: string; | ||||
| 	avatar: string; | ||||
| } | ||||
| 
 | ||||
| // TODO: use Image Data Type for avatar instead of String
 | ||||
| router.post("/", check({ name: new Length(String, 1, 80), $avatar: String }), async (req: Request, res: Response) => { | ||||
| router.post("/", route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }), async (req: Request, res: Response) => { | ||||
| 	const channel_id = req.params.channel_id; | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 
 | ||||
| @ -20,12 +27,11 @@ router.post("/", check({ name: new Length(String, 1, 80), $avatar: String }), as | ||||
| 	const { maxWebhooks } = Config.get().limits.channel; | ||||
| 	if (webhook_count > maxWebhooks) throw DiscordApiErrors.MAXIMUM_WEBHOOKS.withParams(maxWebhooks); | ||||
| 
 | ||||
| 	const permission = await getPermission(req.user_id, channel.guild_id); | ||||
| 	permission.hasThrow("MANAGE_WEBHOOKS"); | ||||
| 
 | ||||
| 	var { avatar, name } = req.body as { name: string; avatar?: string }; | ||||
| 	name = trimSpecial(name); | ||||
| 	if (name === "clyde") throw new HTTPError("Invalid name", 400); | ||||
| 
 | ||||
| 	// TODO: save webhook in database and send response
 | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| import { Guild } from "@fosscord/util"; | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { In } from "typeorm"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const { limit } = req.params; | ||||
| 
 | ||||
| 	// ! this only works using SQL querys
 | ||||
|  | ||||
| @ -1,8 +1,9 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", (req: Request, res: Response) => { | ||||
| router.get("/", route({}), (req: Request, res: Response) => { | ||||
| 	// TODO:
 | ||||
| 	res.send({ fingerprint: "", assignments: [] }); | ||||
| }); | ||||
|  | ||||
| @ -1,9 +1,10 @@ | ||||
| import { Config } from "@fosscord/util"; | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", (req: Request, res: Response) => { | ||||
| router.get("/", route({}), (req: Request, res: Response) => { | ||||
| 	const { endpoint } = Config.get().gateway; | ||||
| 	res.json({ url: endpoint || process.env.GATEWAY || "ws://localhost:3002" }); | ||||
| }); | ||||
|  | ||||
| @ -1,20 +1,22 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { emitEvent, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, Guild, Ban, User, Member } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { getIpAdress } from "../../../util/ipAddress"; | ||||
| import { BanCreateSchema } from "../../../schema/Ban"; | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { getIpAdress, check, route } from "@fosscord/api"; | ||||
| 
 | ||||
| export interface BanCreateSchema { | ||||
| 	delete_message_days?: string; | ||||
| 	reason?: string; | ||||
| } | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 
 | ||||
| 	var bans = await Ban.find({ guild_id: guild_id }); | ||||
| 	return res.json(bans); | ||||
| }); | ||||
| 
 | ||||
| router.get("/:user", async (req: Request, res: Response) => { | ||||
| router.get("/:user", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 	const user_id = req.params.ban; | ||||
| 
 | ||||
| @ -22,15 +24,14 @@ router.get("/:user", async (req: Request, res: Response) => { | ||||
| 	return res.json(ban); | ||||
| }); | ||||
| 
 | ||||
| router.put("/:user_id", check(BanCreateSchema), async (req: Request, res: Response) => { | ||||
| router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 	const banned_user_id = req.params.user_id; | ||||
| 
 | ||||
| 	const banned_user = await User.getPublicUser(banned_user_id); | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("BAN_MEMBERS"); | ||||
| 
 | ||||
| 	if (req.user_id === banned_user_id) throw new HTTPError("You can't ban yourself", 400); | ||||
| 	if (perms.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400); | ||||
| 	if (req.permission!.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400); | ||||
| 
 | ||||
| 	const ban = new Ban({ | ||||
| 		user_id: banned_user_id, | ||||
| @ -56,17 +57,14 @@ router.put("/:user_id", check(BanCreateSchema), async (req: Request, res: Respon | ||||
| 	return res.json(ban); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/:user_id", async (req: Request, res: Response) => { | ||||
| 	var { guild_id } = req.params; | ||||
| 	var banned_user_id = req.params.user_id; | ||||
| router.delete("/:user_id", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id, user_id } = req.params; | ||||
| 
 | ||||
| 	const banned_user = await User.getPublicUser(banned_user_id); | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("BAN_MEMBERS"); | ||||
| 	const banned_user = await User.getPublicUser(user_id); | ||||
| 
 | ||||
| 	await Promise.all([ | ||||
| 		Ban.delete({ | ||||
| 			user_id: banned_user_id, | ||||
| 			user_id: user_id, | ||||
| 			guild_id | ||||
| 		}), | ||||
| 
 | ||||
|  | ||||
| @ -1,9 +1,8 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { ChannelModifySchema } from "../../../schema/Channel"; | ||||
| 
 | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { check, route } from "@fosscord/api"; | ||||
| import { ChannelModifySchema } from "../../channels/#channel_id"; | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| @ -13,10 +12,7 @@ router.get("/", async (req: Request, res: Response) => { | ||||
| 	res.json(channels); | ||||
| }); | ||||
| 
 | ||||
| // TODO: check if channel type is permitted
 | ||||
| // TODO: check if parent_id exists
 | ||||
| 
 | ||||
| router.post("/", check(ChannelModifySchema), async (req: Request, res: Response) => { | ||||
| router.post("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { | ||||
| 	// creates a new guild channel https://discord.com/developers/docs/resources/guild#create-guild-channel
 | ||||
| 	const { guild_id } = req.params; | ||||
| 	const body = req.body as ChannelModifySchema; | ||||
| @ -26,45 +22,39 @@ router.post("/", check(ChannelModifySchema), async (req: Request, res: Response) | ||||
| 	res.status(201).json(channel); | ||||
| }); | ||||
| 
 | ||||
| // TODO: check if parent_id exists
 | ||||
| router.patch( | ||||
| 	"/", | ||||
| 	check([{ id: String, $position: Number, $lock_permissions: Boolean, $parent_id: String }]), | ||||
| 	async (req: Request, res: Response) => { | ||||
| 		// changes guild channel position
 | ||||
| 		const { guild_id } = req.params; | ||||
| 		const body = req.body as { id: string; position?: number; lock_permissions?: boolean; parent_id?: string }[]; | ||||
| export type ChannelReorderSchema = { id: string; position?: number; lock_permissions?: boolean; parent_id?: string }[]; | ||||
| 
 | ||||
| 		const permission = await getPermission(req.user_id, guild_id); | ||||
| 		permission.hasThrow("MANAGE_CHANNELS"); | ||||
| router.patch("/", route({ body: "ChannelReorderSchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { | ||||
| 	// changes guild channel position
 | ||||
| 	const { guild_id } = req.params; | ||||
| 	const body = req.body as ChannelReorderSchema; | ||||
| 
 | ||||
| 		await Promise.all([ | ||||
| 			body.map(async (x) => { | ||||
| 				if (!x.position && !x.parent_id) throw new HTTPError(`You need to at least specify position or parent_id`, 400); | ||||
| 	await Promise.all([ | ||||
| 		body.map(async (x) => { | ||||
| 			if (!x.position && !x.parent_id) throw new HTTPError(`You need to at least specify position or parent_id`, 400); | ||||
| 
 | ||||
| 				const opts: any = {}; | ||||
| 				if (x.position) opts.position = x.position; | ||||
| 			const opts: any = {}; | ||||
| 			if (x.position) opts.position = x.position; | ||||
| 
 | ||||
| 				if (x.parent_id) { | ||||
| 					opts.parent_id = x.parent_id; | ||||
| 					const parent_channel = await Channel.findOneOrFail({ | ||||
| 						where: { id: x.parent_id, guild_id }, | ||||
| 						select: ["permission_overwrites"] | ||||
| 					}); | ||||
| 					if (x.lock_permissions) { | ||||
| 						opts.permission_overwrites = parent_channel.permission_overwrites; | ||||
| 					} | ||||
| 			if (x.parent_id) { | ||||
| 				opts.parent_id = x.parent_id; | ||||
| 				const parent_channel = await Channel.findOneOrFail({ | ||||
| 					where: { id: x.parent_id, guild_id }, | ||||
| 					select: ["permission_overwrites"] | ||||
| 				}); | ||||
| 				if (x.lock_permissions) { | ||||
| 					opts.permission_overwrites = parent_channel.permission_overwrites; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 				await Channel.update({ guild_id, id: x.id }, opts); | ||||
| 				const channel = await Channel.findOneOrFail({ guild_id, id: x.id }); | ||||
| 			await Channel.update({ guild_id, id: x.id }, opts); | ||||
| 			const channel = await Channel.findOneOrFail({ guild_id, id: x.id }); | ||||
| 
 | ||||
| 				await emitEvent({ event: "CHANNEL_UPDATE", data: channel, channel_id: x.id, guild_id } as ChannelUpdateEvent); | ||||
| 			}) | ||||
| 		]); | ||||
| 			await emitEvent({ event: "CHANNEL_UPDATE", data: channel, channel_id: x.id, guild_id } as ChannelUpdateEvent); | ||||
| 		}) | ||||
| 	]); | ||||
| 
 | ||||
| 		res.sendStatus(204); | ||||
| 	} | ||||
| ); | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
|  | ||||
| @ -1,12 +1,13 @@ | ||||
| import { Channel, emitEvent, GuildDeleteEvent, Guild, Member, Message, Role, Invite, Emoji } from "@fosscord/util"; | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| // discord prefixes this route with /delete instead of using the delete method
 | ||||
| // docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild
 | ||||
| router.post("/", async (req: Request, res: Response) => { | ||||
| router.post("/", route({}), async (req: Request, res: Response) => { | ||||
| 	var { guild_id } = req.params; | ||||
| 
 | ||||
| 	const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: ["owner_id"] }); | ||||
|  | ||||
| @ -1,23 +1,36 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { emitEvent, getPermission, Guild, GuildUpdateEvent, Member } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { GuildUpdateSchema } from "../../../schema/Guild"; | ||||
| 
 | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { handleFile } from "../../../util/cdn"; | ||||
| import { check, route } from "@fosscord/api"; | ||||
| import { handleFile } from "@fosscord/api"; | ||||
| import "missing-native-js-functions"; | ||||
| import { GuildCreateSchema } from "../index"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> { | ||||
| 	banner?: string; | ||||
| 	splash?: string; | ||||
| 	description?: string; | ||||
| 	features?: string[]; | ||||
| 	verification_level?: number; | ||||
| 	default_message_notifications?: number; | ||||
| 	system_channel_flags?: number; | ||||
| 	explicit_content_filter?: number; | ||||
| 	public_updates_channel_id?: string; | ||||
| 	afk_timeout?: number; | ||||
| 	afk_channel_id?: string; | ||||
| 	preferred_locale?: string; | ||||
| } | ||||
| 
 | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 
 | ||||
| 	const [guild, member_count, member] = await Promise.all([ | ||||
| 	const [guild, member] = await Promise.all([ | ||||
| 		Guild.findOneOrFail({ id: guild_id }), | ||||
| 		Member.count({ guild_id: guild_id, id: req.user_id }), | ||||
| 		Member.findOneOrFail({ id: req.user_id }) | ||||
| 		Member.findOne({ guild_id: guild_id, id: req.user_id }) | ||||
| 	]); | ||||
| 	if (!member_count) throw new HTTPError("You are not a member of the guild you are trying to access", 401); | ||||
| 	if (!member) throw new HTTPError("You are not a member of the guild you are trying to access", 401); | ||||
| 
 | ||||
| 	// @ts-ignore
 | ||||
| 	guild.joined_at = member?.joined_at; | ||||
| @ -25,14 +38,11 @@ router.get("/", async (req: Request, res: Response) => { | ||||
| 	return res.json(guild); | ||||
| }); | ||||
| 
 | ||||
| router.patch("/", check(GuildUpdateSchema), async (req: Request, res: Response) => { | ||||
| router.patch("/", route({ body: "GuildUpdateSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { | ||||
| 	const body = req.body as GuildUpdateSchema; | ||||
| 	const { guild_id } = req.params; | ||||
| 	// TODO: guild update check image
 | ||||
| 
 | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("MANAGE_GUILD"); | ||||
| 
 | ||||
| 	if (body.icon) body.icon = await handleFile(`/icons/${guild_id}`, body.icon); | ||||
| 	if (body.banner) body.banner = await handleFile(`/banners/${guild_id}`, body.banner); | ||||
| 	if (body.splash) body.splash = await handleFile(`/splashes/${guild_id}`, body.splash); | ||||
|  | ||||
| @ -1,14 +1,12 @@ | ||||
| import { getPermission, Invite, PublicInviteRelation } from "@fosscord/util"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { Request, Response, Router } from "express"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 
 | ||||
| 	const permissions = await getPermission(req.user_id, guild_id); | ||||
| 	permissions.hasThrow("MANAGE_GUILD"); | ||||
| 
 | ||||
| 	const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation }); | ||||
| 
 | ||||
| 	return res.json(invites); | ||||
|  | ||||
| @ -1,23 +1,15 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { | ||||
| 	Guild, | ||||
| 	Member, | ||||
| 	User, | ||||
| 	GuildMemberAddEvent, | ||||
| 	getPermission, | ||||
| 	PermissionResolvable, | ||||
| 	Role, | ||||
| 	GuildMemberUpdateEvent, | ||||
| 	emitEvent | ||||
| } from "@fosscord/util"; | ||||
| import { Member, getPermission, Role, GuildMemberUpdateEvent, emitEvent } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { check } from "../../../../../util/instanceOf"; | ||||
| import { MemberChangeSchema } from "../../../../../schema/Member"; | ||||
| import { In } from "typeorm"; | ||||
| import { check, route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| export interface MemberChangeSchema { | ||||
| 	roles?: string[]; | ||||
| } | ||||
| 
 | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const { guild_id, member_id } = req.params; | ||||
| 	await Member.IsInGuildOrFail(req.user_id, guild_id); | ||||
| 
 | ||||
| @ -26,8 +18,9 @@ router.get("/", async (req: Request, res: Response) => { | ||||
| 	return res.json(member); | ||||
| }); | ||||
| 
 | ||||
| router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response) => { | ||||
| 	const { guild_id, member_id } = req.params; | ||||
| router.patch("/", route({ body: "MemberChangeSchema" }), async (req: Request, res: Response) => { | ||||
| 	let { guild_id, member_id } = req.params; | ||||
| 	if (member_id === "@me") member_id = req.user_id; | ||||
| 	const body = req.body as MemberChangeSchema; | ||||
| 
 | ||||
| 	const member = await Member.findOneOrFail({ where: { id: member_id, guild_id }, relations: ["roles", "user"] }); | ||||
| @ -39,7 +32,7 @@ router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response) | ||||
| 	} | ||||
| 
 | ||||
| 	await member.save(); | ||||
| 	// do not use promise.all as we have to first write to db before emitting the event
 | ||||
| 	// do not use promise.all as we have to first write to db before emitting the event to catch errors
 | ||||
| 	await emitEvent({ | ||||
| 		event: "GUILD_MEMBER_UPDATE", | ||||
| 		guild_id, | ||||
| @ -49,7 +42,7 @@ router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response) | ||||
| 	res.json(member); | ||||
| }); | ||||
| 
 | ||||
| router.put("/", async (req: Request, res: Response) => { | ||||
| router.put("/", route({}), async (req: Request, res: Response) => { | ||||
| 	let { guild_id, member_id } = req.params; | ||||
| 	if (member_id === "@me") member_id = req.user_id; | ||||
| 
 | ||||
| @ -59,12 +52,9 @@ router.put("/", async (req: Request, res: Response) => { | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/", async (req: Request, res: Response) => { | ||||
| router.delete("/", route({ permission: "KICK_MEMBERS" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id, member_id } = req.params; | ||||
| 
 | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("KICK_MEMBERS"); | ||||
| 
 | ||||
| 	await Member.removeFromGuild(member_id, guild_id); | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
|  | ||||
| @ -1,11 +1,14 @@ | ||||
| import { getPermission, Member, PermissionResolvable } from "@fosscord/util"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { check } from "lambert-server"; | ||||
| import { MemberNickChangeSchema } from "../../../../../schema/Member"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.patch("/", check(MemberNickChangeSchema), async (req: Request, res: Response) => { | ||||
| export interface MemberNickChangeSchema { | ||||
| 	nick: string; | ||||
| } | ||||
| 
 | ||||
| router.patch("/", route({ body: "MemberNickChangeSchema" }), async (req: Request, res: Response) => { | ||||
| 	var { guild_id, member_id } = req.params; | ||||
| 	var permissionString: PermissionResolvable = "MANAGE_NICKNAMES"; | ||||
| 	if (member_id === "@me") { | ||||
|  | ||||
| @ -1,24 +1,19 @@ | ||||
| import { getPermission, Member } from "@fosscord/util"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { Request, Response, Router } from "express"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.delete("/:member_id/roles/:role_id", async (req: Request, res: Response) => { | ||||
| router.delete("/:member_id/roles/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id, role_id, member_id } = req.params; | ||||
| 
 | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("MANAGE_ROLES"); | ||||
| 
 | ||||
| 	await Member.removeRole(member_id, guild_id, role_id); | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| router.put("/:member_id/roles/:role_id", async (req: Request, res: Response) => { | ||||
| router.put("/:member_id/roles/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id, role_id, member_id } = req.params; | ||||
| 
 | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("MANAGE_ROLES"); | ||||
| 
 | ||||
| 	await Member.addRole(member_id, guild_id, role_id); | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
|  | ||||
| @ -1,13 +1,15 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { Guild, Member, PublicMemberProjection } from "@fosscord/util"; | ||||
| import { instanceOf, Length } from "../../../../util/instanceOf"; | ||||
| import { instanceOf, Length, route } from "@fosscord/api"; | ||||
| import { MoreThan } from "typeorm"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| // TODO: not allowed for user -> only allowed for bots with privileged intents
 | ||||
| // TODO: send over websocket
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| // TODO: check for GUILD_MEMBERS intent
 | ||||
| 
 | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 	const guild = await Guild.findOneOrFail({ id: guild_id }); | ||||
| 	await Member.IsInGuildOrFail(req.user_id, guild_id); | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| import {Config, Guild, Member} from "@fosscord/util"; | ||||
| import { Config, Guild, Member } from "@fosscord/util"; | ||||
| import { Request, Response, Router } from "express"; | ||||
| import {getVoiceRegions} from "../../../util/Voice"; | ||||
| import {getIpAdress} from "../../../util/ipAddress"; | ||||
| import { getVoiceRegions, route } from "@fosscord/api"; | ||||
| import { getIpAdress } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 	const guild = await Guild.findOneOrFail({ id: guild_id }); | ||||
| 	//TODO we should use an enum for guild's features and not hardcoded strings
 | ||||
|  | ||||
| @ -2,23 +2,34 @@ import { Request, Response, Router } from "express"; | ||||
| import { | ||||
| 	Role, | ||||
| 	getPermission, | ||||
| 	Snowflake, | ||||
| 	Member, | ||||
| 	GuildRoleCreateEvent, | ||||
| 	GuildRoleUpdateEvent, | ||||
| 	GuildRoleDeleteEvent, | ||||
| 	emitEvent, | ||||
| 	Config | ||||
| 	Config, | ||||
| 	DiscordApiErrors | ||||
| } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| 
 | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { RoleModifySchema, RolePositionUpdateSchema } from "../../../schema/Roles"; | ||||
| import { DiscordApiErrors } from "@fosscord/util"; | ||||
| import { check, route } from "@fosscord/api"; | ||||
| import { In } from "typeorm"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| export interface RoleModifySchema { | ||||
| 	name?: string; | ||||
| 	permissions?: bigint; | ||||
| 	color?: number; | ||||
| 	hoist?: boolean; // whether the role should be displayed separately in the sidebar
 | ||||
| 	mentionable?: boolean; // whether the role should be mentionable
 | ||||
| 	position?: number; | ||||
| } | ||||
| 
 | ||||
| export type RolePositionUpdateSchema = { | ||||
| 	id: string; | ||||
| 	position: number; | ||||
| }[]; | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| 	const guild_id = req.params.guild_id; | ||||
| 
 | ||||
| @ -29,13 +40,10 @@ router.get("/", async (req: Request, res: Response) => { | ||||
| 	return res.json(roles); | ||||
| }); | ||||
| 
 | ||||
| router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => { | ||||
| router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { | ||||
| 	const guild_id = req.params.guild_id; | ||||
| 	const body = req.body as RoleModifySchema; | ||||
| 
 | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("MANAGE_ROLES"); | ||||
| 
 | ||||
| 	const role_count = await Role.count({ guild_id }); | ||||
| 	const { maxRoles } = Config.get().limits.guild; | ||||
| 
 | ||||
| @ -50,7 +58,7 @@ router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => | ||||
| 		...body, | ||||
| 		guild_id: guild_id, | ||||
| 		managed: false, | ||||
| 		permissions: String(perms.bitfield & (body.permissions || 0n)), | ||||
| 		permissions: String(req.permission!.bitfield & (body.permissions || 0n)), | ||||
| 		tags: undefined | ||||
| 	}); | ||||
| 
 | ||||
| @ -69,14 +77,11 @@ router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => | ||||
| 	res.json(role); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/:role_id", async (req: Request, res: Response) => { | ||||
| router.delete("/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { | ||||
| 	const guild_id = req.params.guild_id; | ||||
| 	const { role_id } = req.params; | ||||
| 	if (role_id === guild_id) throw new HTTPError("You can't delete the @everyone role"); | ||||
| 
 | ||||
| 	const permissions = await getPermission(req.user_id, guild_id); | ||||
| 	permissions.hasThrow("MANAGE_ROLES"); | ||||
| 
 | ||||
| 	await Promise.all([ | ||||
| 		Role.delete({ | ||||
| 			id: role_id, | ||||
| @ -97,14 +102,11 @@ router.delete("/:role_id", async (req: Request, res: Response) => { | ||||
| 
 | ||||
| // TODO: check role hierarchy
 | ||||
| 
 | ||||
| router.patch("/:role_id", check(RoleModifySchema), async (req: Request, res: Response) => { | ||||
| router.patch("/:role_id", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { | ||||
| 	const { role_id, guild_id } = req.params; | ||||
| 	const body = req.body as RoleModifySchema; | ||||
| 
 | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("MANAGE_ROLES"); | ||||
| 
 | ||||
| 	const role = new Role({ ...body, id: role_id, guild_id, permissions: String(perms.bitfield & (body.permissions || 0n)) }); | ||||
| 	const role = new Role({ ...body, id: role_id, guild_id, permissions: String(req.permission!.bitfield & (body.permissions || 0n)) }); | ||||
| 
 | ||||
| 	await Promise.all([ | ||||
| 		role.save(), | ||||
| @ -121,7 +123,7 @@ router.patch("/:role_id", check(RoleModifySchema), async (req: Request, res: Res | ||||
| 	res.json(role); | ||||
| }); | ||||
| 
 | ||||
| router.patch("/", check(RolePositionUpdateSchema), async (req: Request, res: Response) => { | ||||
| router.patch("/", route({ body: "RolePositionUpdateSchema" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 	const body = req.body as RolePositionUpdateSchema; | ||||
| 
 | ||||
|  | ||||
| @ -1,9 +1,8 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { Guild, getPermission, Template } from "@fosscord/util"; | ||||
| import { Guild, Template } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { TemplateCreateSchema, TemplateModifySchema } from "../../../schema/Template"; | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { generateCode } from "../../../util/String"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { generateCode } from "@fosscord/api"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| @ -24,7 +23,17 @@ const TemplateGuildProjection: (keyof Guild)[] = [ | ||||
| 	"icon" | ||||
| ]; | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| export interface TemplateCreateSchema { | ||||
| 	name: string; | ||||
| 	description?: string; | ||||
| } | ||||
| 
 | ||||
| export interface TemplateModifySchema { | ||||
| 	name: string; | ||||
| 	description?: string; | ||||
| } | ||||
| 
 | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 
 | ||||
| 	var templates = await Template.find({ source_guild_id: guild_id }); | ||||
| @ -32,12 +41,9 @@ router.get("/", async (req: Request, res: Response) => { | ||||
| 	return res.json(templates); | ||||
| }); | ||||
| 
 | ||||
| router.post("/", check(TemplateCreateSchema), async (req: Request, res: Response) => { | ||||
| router.post("/", route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 	const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection }); | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("MANAGE_GUILD"); | ||||
| 
 | ||||
| 	const exists = await Template.findOneOrFail({ id: guild_id }).catch((e) => {}); | ||||
| 	if (exists) throw new HTTPError("Template already exists", 400); | ||||
| 
 | ||||
| @ -54,44 +60,31 @@ router.post("/", check(TemplateCreateSchema), async (req: Request, res: Response | ||||
| 	res.json(template); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/:code", async (req: Request, res: Response) => { | ||||
| 	const guild_id = req.params.guild_id; | ||||
| 	const { code } = req.params; | ||||
| 
 | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("MANAGE_GUILD"); | ||||
| router.delete("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { | ||||
| 	const { code, guild_id } = req.params; | ||||
| 
 | ||||
| 	const template = await Template.delete({ | ||||
| 		code | ||||
| 		code, | ||||
| 		source_guild_id: guild_id | ||||
| 	}); | ||||
| 
 | ||||
| 	res.json(template); | ||||
| }); | ||||
| 
 | ||||
| router.put("/:code", async (req: Request, res: Response) => { | ||||
| 	// synchronizes the template
 | ||||
| router.put("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { | ||||
| 	const { code, guild_id } = req.params; | ||||
| 
 | ||||
| 	const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection }); | ||||
| 
 | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("MANAGE_GUILD"); | ||||
| 
 | ||||
| 	const template = await new Template({ code, serialized_source_guild: guild }).save(); | ||||
| 
 | ||||
| 	res.json(template); | ||||
| }); | ||||
| 
 | ||||
| router.patch("/:code", check(TemplateModifySchema), async (req: Request, res: Response) => { | ||||
| 	// updates the template description
 | ||||
| 	const { guild_id } = req.params; | ||||
| 	const { code } = req.params; | ||||
| router.patch("/:code", route({ body: "TemplateModifySchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { | ||||
| 	const { code, guild_id } = req.params; | ||||
| 	const { name, description } = req.body; | ||||
| 
 | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("MANAGE_GUILD"); | ||||
| 
 | ||||
| 	const template = await new Template({ code, name: name, description: description }).save(); | ||||
| 	const template = await new Template({ code, name: name, description: description, source_guild_id: guild_id }).save(); | ||||
| 
 | ||||
| 	res.json(template); | ||||
| }); | ||||
|  | ||||
| @ -1,35 +1,37 @@ | ||||
| import { Channel, ChannelType, getPermission, Guild, Invite, trimSpecial } from "@fosscord/util"; | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { check, Length } from "../../../util/instanceOf"; | ||||
| import { check, Length, route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| const InviteRegex = /\W/g; | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 
 | ||||
| 	const permission = await getPermission(req.user_id, guild_id); | ||||
| 	permission.hasThrow("MANAGE_GUILD"); | ||||
| 
 | ||||
| 	const guild = await Guild.findOneOrFail({ where: { id: guild_id }, relations: ["vanity_url"] }); | ||||
| 	if (!guild.vanity_url) return res.json({ code: null }); | ||||
| 
 | ||||
| 	return res.json({ code: guild.vanity_url_code, uses: guild.vanity_url.uses }); | ||||
| }); | ||||
| 
 | ||||
| export interface VanityUrlSchema { | ||||
| 	/** | ||||
| 	 * @minLength 1 | ||||
| 	 * @maxLength 20 | ||||
| 	 */ | ||||
| 	code?: string; | ||||
| } | ||||
| 
 | ||||
| // TODO: check if guild is elgible for vanity url
 | ||||
| router.patch("/", check({ code: new Length(String, 0, 20) }), async (req: Request, res: Response) => { | ||||
| router.patch("/", route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 	const code = req.body.code.replace(InviteRegex); | ||||
| 	const body = req.body as VanityUrlSchema; | ||||
| 	const code = body.code?.replace(InviteRegex, ""); | ||||
| 
 | ||||
| 	await Invite.findOneOrFail({ code }); | ||||
| 
 | ||||
| 	const guild = await Guild.findOneOrFail({ id: guild_id }); | ||||
| 	const permission = await getPermission(req.user_id, guild_id); | ||||
| 	permission.hasThrow("MANAGE_GUILD"); | ||||
| 
 | ||||
| 	const { id } = await Channel.findOneOrFail({ guild_id, type: ChannelType.GUILD_TEXT }); | ||||
| 	guild.vanity_url_code = code; | ||||
| 
 | ||||
|  | ||||
| @ -1,15 +1,60 @@ | ||||
| import { check } from "../../../../../util/instanceOf"; | ||||
| import { VoiceStateUpdateSchema } from "../../../../../schema"; | ||||
| import { Channel, ChannelType, DiscordApiErrors, emitEvent, getPermission, VoiceState, VoiceStateUpdateEvent } from "@fosscord/util"; | ||||
| import { check, route } from "@fosscord/api"; | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { updateVoiceState } from "../../../../../util/VoiceState"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| //TODO need more testing when community guild and voice stage channel are working
 | ||||
| 
 | ||||
| router.patch("/", check(VoiceStateUpdateSchema), async (req: Request, res: Response) => { | ||||
| export interface VoiceStateUpdateSchema { | ||||
| 	channel_id: string; | ||||
| 	guild_id?: string; | ||||
| 	suppress?: boolean; | ||||
| 	request_to_speak_timestamp?: Date; | ||||
| 	self_mute?: boolean; | ||||
| 	self_deaf?: boolean; | ||||
| 	self_video?: boolean; | ||||
| } | ||||
| 
 | ||||
| router.patch("/", route({ body: "VoiceStateUpdateSchema" }), async (req: Request, res: Response) => { | ||||
| 	const body = req.body as VoiceStateUpdateSchema; | ||||
| 	const { guild_id, user_id } = req.params; | ||||
| 	await updateVoiceState(body, guild_id, req.user_id, user_id) | ||||
| 	var { guild_id, user_id } = req.params; | ||||
| 	if (user_id === "@me") user_id = req.user_id; | ||||
| 
 | ||||
| 	const perms = await getPermission(req.user_id, guild_id, body.channel_id); | ||||
| 
 | ||||
| 	/* | ||||
| 	From https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state
 | ||||
| 	You must have the MUTE_MEMBERS permission to unsuppress others. You can always suppress yourself. | ||||
| 	You must have the REQUEST_TO_SPEAK permission to request to speak. You can always clear your own request to speak. | ||||
| 	 */ | ||||
| 	if (body.suppress && user_id !== req.user_id) { | ||||
| 		perms.hasThrow("MUTE_MEMBERS"); | ||||
| 	} | ||||
| 	if (!body.suppress) body.request_to_speak_timestamp = new Date(); | ||||
| 	if (body.request_to_speak_timestamp) perms.hasThrow("REQUEST_TO_SPEAK"); | ||||
| 
 | ||||
| 	const voice_state = await VoiceState.findOne({ | ||||
| 		guild_id, | ||||
| 		channel_id: body.channel_id, | ||||
| 		user_id | ||||
| 	}); | ||||
| 	if (!voice_state) throw DiscordApiErrors.UNKNOWN_VOICE_STATE; | ||||
| 
 | ||||
| 	voice_state.assign(body); | ||||
| 	const channel = await Channel.findOneOrFail({ guild_id, id: body.channel_id }); | ||||
| 	if (channel.type !== ChannelType.GUILD_STAGE_VOICE) { | ||||
| 		throw DiscordApiErrors.CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE; | ||||
| 	} | ||||
| 
 | ||||
| 	await Promise.all([ | ||||
| 		voice_state.save(), | ||||
| 		emitEvent({ | ||||
| 			event: "VOICE_STATE_UPDATE", | ||||
| 			data: voice_state, | ||||
| 			guild_id | ||||
| 		} as VoiceStateUpdateEvent) | ||||
| 	]); | ||||
| 	return res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
| export default router; | ||||
|  | ||||
| @ -1,15 +0,0 @@ | ||||
| import { check } from "../../../../../util/instanceOf"; | ||||
| import { VoiceStateUpdateSchema } from "../../../../../schema"; | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { updateVoiceState } from "../../../../../util/VoiceState"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.patch("/", check(VoiceStateUpdateSchema), async (req: Request, res: Response) => { | ||||
| 	const body = req.body as VoiceStateUpdateSchema; | ||||
| 	const { guild_id } = req.params; | ||||
| 	await updateVoiceState(body, guild_id, req.user_id) | ||||
| 	return res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
| @ -1,31 +1,36 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { Guild, getPermission, Snowflake, Member } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| 
 | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { GuildUpdateWelcomeScreenSchema } from "../../../schema/Guild"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| export interface GuildUpdateWelcomeScreenSchema { | ||||
| 	welcome_channels?: { | ||||
| 		channel_id: string; | ||||
| 		description: string; | ||||
| 		emoji_id?: string; | ||||
| 		emoji_name: string; | ||||
| 	}[]; | ||||
| 	enabled?: boolean; | ||||
| 	description?: string; | ||||
| } | ||||
| 
 | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const guild_id = req.params.guild_id; | ||||
| 
 | ||||
| 	const guild = await Guild.findOneOrFail({ id: guild_id }); | ||||
| 
 | ||||
| 	await Member.IsInGuildOrFail(req.user_id, guild_id); | ||||
| 
 | ||||
| 	res.json(guild.welcome_screen); | ||||
| }); | ||||
| 
 | ||||
| router.patch("/", check(GuildUpdateWelcomeScreenSchema), async (req: Request, res: Response) => { | ||||
| router.patch("/", route({ body: "GuildUpdateWelcomeScreenSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { | ||||
| 	const guild_id = req.params.guild_id; | ||||
| 	const body = req.body as GuildUpdateWelcomeScreenSchema; | ||||
| 
 | ||||
| 	const guild = await Guild.findOneOrFail({ id: guild_id }); | ||||
| 
 | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("MANAGE_GUILD"); | ||||
| 
 | ||||
| 	if (!guild.welcome_screen.enabled) throw new HTTPError("Welcome screen disabled", 400); | ||||
| 	if (body.welcome_channels) guild.welcome_screen.welcome_channels = body.welcome_channels; // TODO: check if they exist and are valid
 | ||||
| 	if (body.description) guild.welcome_screen.description = body.description; | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { Config, Permissions, Guild, Invite, Channel, Member } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { random } from "../../../util/RandomInviteID"; | ||||
| import { random, route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| @ -14,7 +14,7 @@ const router: Router = Router(); | ||||
| 
 | ||||
| // https://discord.com/developers/docs/resources/guild#get-guild-widget
 | ||||
| // TODO: Cache the response for a guild for 5 minutes regardless of response
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 
 | ||||
| 	const guild = await Guild.findOneOrFail({ id: guild_id }); | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { Guild } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import fs from "fs"; | ||||
| import path from "path"; | ||||
| 
 | ||||
| @ -10,7 +11,7 @@ const router: Router = Router(); | ||||
| 
 | ||||
| // https://discord.com/developers/docs/resources/guild#get-guild-widget-image
 | ||||
| // TODO: Cache the response
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 
 | ||||
| 	const guild = await Guild.findOneOrFail({ id: guild_id }); | ||||
|  | ||||
| @ -1,31 +1,29 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { getPermission, Guild } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { WidgetModifySchema } from "../../../schema/Widget"; | ||||
| import { check, route } from "@fosscord/api"; | ||||
| 
 | ||||
| export interface WidgetModifySchema { | ||||
| 	enabled: boolean; // whether the widget is enabled
 | ||||
| 	channel_id: string; // the widget channel id
 | ||||
| } | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| // https://discord.com/developers/docs/resources/guild#get-guild-widget-settings
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 
 | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("MANAGE_GUILD"); | ||||
| 
 | ||||
| 	const guild = await Guild.findOneOrFail({ id: guild_id }); | ||||
| 
 | ||||
| 	return res.json({ enabled: guild.widget_enabled || false, channel_id: guild.widget_channel_id || null }); | ||||
| }); | ||||
| 
 | ||||
| // https://discord.com/developers/docs/resources/guild#modify-guild-widget
 | ||||
| router.patch("/", check(WidgetModifySchema), async (req: Request, res: Response) => { | ||||
| router.patch("/", route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { | ||||
| 	const body = req.body as WidgetModifySchema; | ||||
| 	const { guild_id } = req.params; | ||||
| 
 | ||||
| 	const perms = await getPermission(req.user_id, guild_id); | ||||
| 	perms.hasThrow("MANAGE_GUILD"); | ||||
| 
 | ||||
| 	await Guild.update({ id: guild_id }, { widget_enabled: body.enabled, widget_channel_id: body.channel_id }); | ||||
| 	// Widget invite for the widget_channel_id gets created as part of the /guilds/{guild.id}/widget.json request
 | ||||
| 
 | ||||
|  | ||||
| @ -1,15 +1,28 @@ | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { Role, Guild, Snowflake, Config, User, Member, Channel } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { check } from "./../../util/instanceOf"; | ||||
| import { GuildCreateSchema } from "../../schema/Guild"; | ||||
| import { check, route } from "@fosscord/api"; | ||||
| import { DiscordApiErrors } from "@fosscord/util"; | ||||
| import { ChannelModifySchema } from "../channels/#channel_id"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| export interface GuildCreateSchema { | ||||
| 	/** | ||||
| 	 * @maxLength 100 | ||||
| 	 */ | ||||
| 	name: string; | ||||
| 	region?: string; | ||||
| 	icon?: string; | ||||
| 	channels?: ChannelModifySchema[]; | ||||
| 	guild_template_code?: string; | ||||
| 	system_channel_id?: string; | ||||
| 	rules_channel_id?: string; | ||||
| } | ||||
| 
 | ||||
| //TODO: create default channel
 | ||||
| 
 | ||||
| router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) => { | ||||
| router.post("/", route({ body: "GuildCreateSchema" }), async (req: Request, res: Response) => { | ||||
| 	const body = req.body as GuildCreateSchema; | ||||
| 
 | ||||
| 	const { maxGuilds } = Config.get().limits.user; | ||||
|  | ||||
| @ -2,11 +2,15 @@ import { Request, Response, Router } from "express"; | ||||
| const router: Router = Router(); | ||||
| import { Template, Guild, Role, Snowflake, Config, User, Member } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { GuildTemplateCreateSchema } from "../../../schema/Guild"; | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { check, route } from "@fosscord/api"; | ||||
| import { DiscordApiErrors } from "@fosscord/util"; | ||||
| 
 | ||||
| router.get("/:code", async (req: Request, res: Response) => { | ||||
| export interface GuildTemplateCreateSchema { | ||||
| 	name: string; | ||||
| 	avatar?: string; | ||||
| } | ||||
| 
 | ||||
| router.get("/:code", route({}), async (req: Request, res: Response) => { | ||||
| 	const { code } = req.params; | ||||
| 
 | ||||
| 	const template = await Template.findOneOrFail({ code: code }); | ||||
| @ -14,7 +18,7 @@ router.get("/:code", async (req: Request, res: Response) => { | ||||
| 	res.json(template); | ||||
| }); | ||||
| 
 | ||||
| router.post("/:code", check(GuildTemplateCreateSchema), async (req: Request, res: Response) => { | ||||
| router.post("/:code", route({ body: "GuildTemplateCreateSchema" }), async (req: Request, res: Response) => { | ||||
| 	const { code } = req.params; | ||||
| 	const body = req.body as GuildTemplateCreateSchema; | ||||
| 
 | ||||
|  | ||||
| @ -1,9 +1,10 @@ | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { getPermission, Guild, Invite, Member, PublicInviteRelation } from "@fosscord/util"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.get("/:code", async (req: Request, res: Response) => { | ||||
| router.get("/:code", route({}), async (req: Request, res: Response) => { | ||||
| 	const { code } = req.params; | ||||
| 
 | ||||
| 	const invite = await Invite.findOneOrFail({ where: { code }, relations: PublicInviteRelation }); | ||||
| @ -11,7 +12,7 @@ router.get("/:code", async (req: Request, res: Response) => { | ||||
| 	res.status(200).send(invite); | ||||
| }); | ||||
| 
 | ||||
| router.post("/:code", async (req: Request, res: Response) => { | ||||
| router.post("/:code", route({}), async (req: Request, res: Response) => { | ||||
| 	const { code } = req.params; | ||||
| 
 | ||||
| 	const invite = await Invite.findOneOrFail({ code }); | ||||
| @ -23,7 +24,8 @@ router.post("/:code", async (req: Request, res: Response) => { | ||||
| 	res.status(200).send(invite); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/:code", async (req: Request, res: Response) => { | ||||
| // * cant use permission of route() function because path doesn't have guild_id/channel_id
 | ||||
| router.delete("/:code", route({}), async (req: Request, res: Response) => { | ||||
| 	const { code } = req.params; | ||||
| 	const invite = await Invite.findOneOrFail({ code }); | ||||
| 	const { guild_id, channel_id } = invite; | ||||
|  | ||||
| @ -1,8 +1,9 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", (req: Request, res: Response) => { | ||||
| router.get("/", route({}), (req: Request, res: Response) => { | ||||
| 	res.send("pong"); | ||||
| }); | ||||
| 
 | ||||
|  | ||||
| @ -1,8 +1,9 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.post("/", (req: Request, res: Response) => { | ||||
| router.post("/", route({}), (req: Request, res: Response) => { | ||||
| 	// TODO:
 | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
|  | ||||
| @ -1,9 +1,10 @@ | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { User } from "../../../../../util/dist"; | ||||
| import { User } from "@fosscord/util"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const { id } = req.params; | ||||
| 
 | ||||
| 	res.json(await User.getPublicUser(id)); | ||||
|  | ||||
| @ -1,9 +1,17 @@ | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { PublicConnectedAccount, PublicUser, User, UserPublic } from "../../../../../util/dist"; | ||||
| import { PublicConnectedAccount, PublicUser, User, UserPublic } from "@fosscord/util"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| export interface UserProfileResponse { | ||||
| 	user: UserPublic; | ||||
| 	connected_accounts: PublicConnectedAccount; | ||||
| 	premium_guild_since?: Date; | ||||
| 	premium_since?: Date; | ||||
| } | ||||
| 
 | ||||
| router.get("/", route({ response: { body: "UserProfileResponse" } }), async (req: Request, res: Response) => { | ||||
| 	if (req.params.id === "@me") req.params.id = req.user_id; | ||||
| 	const user = await User.getPublicUser(req.params.id, { relations: ["connected_accounts"] }); | ||||
| 
 | ||||
| @ -25,11 +33,4 @@ router.get("/", async (req: Request, res: Response) => { | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| export interface UserProfileResponse { | ||||
| 	user: UserPublic; | ||||
| 	connected_accounts: PublicConnectedAccount; | ||||
| 	premium_guild_since?: Date; | ||||
| 	premium_since?: Date; | ||||
| } | ||||
| 
 | ||||
| export default router; | ||||
|  | ||||
| @ -1,8 +1,9 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", (req: Request, res: Response) => { | ||||
| router.get("/", route({}), (req: Request, res: Response) => { | ||||
| 	// TODO:
 | ||||
| 	res.status(200).send({ guild_affinities: [] }); | ||||
| }); | ||||
|  | ||||
| @ -1,9 +1,10 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", (req: Request, res: Response) => { | ||||
| 	//TODO
 | ||||
| router.get("/", route({}), (req: Request, res: Response) => { | ||||
| 	// TODO:
 | ||||
| 	res.status(200).send({ user_affinities: [], inverse_user_affinities: [] }); | ||||
| }); | ||||
| 
 | ||||
|  | ||||
| @ -1,21 +1,23 @@ | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { Channel, ChannelCreateEvent, ChannelType, Snowflake, trimSpecial, User, emitEvent } from "@fosscord/util"; | ||||
| import { Channel, ChannelCreateEvent, ChannelType, Snowflake, trimSpecial, User, emitEvent, Recipient } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| 
 | ||||
| import { DmChannelCreateSchema } from "../../../schema/Channel"; | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { In } from "typeorm"; | ||||
| import { Recipient } from "../../../../../util/dist/entities/Recipient"; | ||||
| 
 | ||||
| export interface DmChannelCreateSchema { | ||||
| 	name?: string; | ||||
| 	recipients: string[]; | ||||
| } | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const recipients = await Recipient.find({ where: { user_id: req.user_id }, relations: ["channel"] }); | ||||
| 
 | ||||
| 	res.json(recipients.map((x) => x.channel)); | ||||
| }); | ||||
| 
 | ||||
| router.post("/", check(DmChannelCreateSchema), async (req: Request, res: Response) => { | ||||
| router.post("/", route({ body: "DmChannelCreateSchema" }), async (req: Request, res: Response) => { | ||||
| 	const body = req.body as DmChannelCreateSchema; | ||||
| 
 | ||||
| 	body.recipients = body.recipients.filter((x) => x !== req.user_id).unique(); | ||||
|  | ||||
| @ -1,10 +1,12 @@ | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { Guild, Member, User } from "@fosscord/util"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import bcrypt from "bcrypt"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.post("/", async (req: Request, res: Response) => { | ||||
| router.post("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const user = await User.findOneOrFail({ id: req.user_id }); //User object
 | ||||
| 	let correctpass = true; | ||||
| 
 | ||||
|  | ||||
| @ -1,8 +1,9 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.post("/", (req: Request, res: Response) => { | ||||
| router.post("/", route({}), (req: Request, res: Response) => { | ||||
| 	// TODO:
 | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
|  | ||||
| @ -1,10 +1,11 @@ | ||||
| import { User } from "@fosscord/util"; | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import bcrypt from "bcrypt"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.post("/", async (req: Request, res: Response) => { | ||||
| router.post("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const user = await User.findOneOrFail({ id: req.user_id }); //User object
 | ||||
| 	let correctpass = true; | ||||
| 
 | ||||
|  | ||||
| @ -1,18 +1,18 @@ | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { Guild, Member, User, GuildDeleteEvent, GuildMemberRemoveEvent, emitEvent } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { In } from "typeorm"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const members = await Member.find({ relations: ["guild"], where: { id: req.user_id } }); | ||||
| 
 | ||||
| 	res.json(members.map((x) => x.guild)); | ||||
| }); | ||||
| 
 | ||||
| // user send to leave a certain guild
 | ||||
| router.delete("/:id", async (req: Request, res: Response) => { | ||||
| router.delete("/:id", route({}), async (req: Request, res: Response) => { | ||||
| 	const guild_id = req.params.id; | ||||
| 	const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: ["owner_id"] }); | ||||
| 
 | ||||
|  | ||||
| @ -1,16 +1,33 @@ | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { User, PrivateUserProjection } from "@fosscord/util"; | ||||
| import { UserModifySchema } from "../../../schema/User"; | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { handleFile } from "../../../util/cdn"; | ||||
| import { check, route } from "@fosscord/api"; | ||||
| import { handleFile } from "@fosscord/api"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| export interface UserModifySchema { | ||||
| 	/** | ||||
| 	 * @minLength 1 | ||||
| 	 * @maxLength 100 | ||||
| 	 */ | ||||
| 	username?: string; | ||||
| 	avatar?: string | null; | ||||
| 	/** | ||||
| 	 * @maxLength 1024 | ||||
| 	 */ | ||||
| 	bio?: string; | ||||
| 	accent_color?: number | null; | ||||
| 	banner?: string | null; | ||||
| 	password?: string; | ||||
| 	new_password?: string; | ||||
| 	code?: string; | ||||
| } | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| 	res.json(await User.getPublicUser(req.user_id, { select: PrivateUserProjection })); | ||||
| 	res.json(await User.findOne({ select: PrivateUserProjection, where: { id: req.user_id } })); | ||||
| }); | ||||
| 
 | ||||
| router.patch("/", check(UserModifySchema), async (req: Request, res: Response) => { | ||||
| router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: Response) => { | ||||
| 	const body = req.body as UserModifySchema; | ||||
| 
 | ||||
| 	if (body.avatar) body.avatar = await handleFile(`/avatars/${req.user_id}`, body.avatar as string); | ||||
|  | ||||
| @ -1,8 +1,9 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", (req: Request, res: Response) => { | ||||
| router.get("/", route({}), (req: Request, res: Response) => { | ||||
| 	// TODO:
 | ||||
| 	res.status(200).send([]); | ||||
| }); | ||||
|  | ||||
| @ -11,19 +11,95 @@ import { | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { DiscordApiErrors } from "@fosscord/util"; | ||||
| 
 | ||||
| import { check, Length } from "../../../util/instanceOf"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| const userProjection: (keyof User)[] = ["relationships", ...PublicUserProjection]; | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const user = await User.findOneOrFail({ where: { id: req.user_id }, relations: ["relationships"] }); | ||||
| 
 | ||||
| 	return res.json(user.relationships); | ||||
| }); | ||||
| 
 | ||||
| export interface RelationshipPutSchema { | ||||
| 	type: RelationshipType; | ||||
| } | ||||
| 
 | ||||
| router.put("/:id", route({ body: "RelationshipPutSchema" }), async (req: Request, res: Response) => { | ||||
| 	return await updateRelationship( | ||||
| 		req, | ||||
| 		res, | ||||
| 		await User.findOneOrFail({ id: req.params.id }, { relations: ["relationships"], select: userProjection }), | ||||
| 		req.body.type | ||||
| 	); | ||||
| }); | ||||
| 
 | ||||
| export interface RelationshipPostSchema { | ||||
| 	discriminator: string; | ||||
| 	username: string; | ||||
| } | ||||
| 
 | ||||
| router.post("/", route({ body: "RelationshipPostSchema" }), async (req: Request, res: Response) => { | ||||
| 	return await updateRelationship( | ||||
| 		req, | ||||
| 		res, | ||||
| 		await User.findOneOrFail({ | ||||
| 			relations: ["relationships"], | ||||
| 			select: userProjection, | ||||
| 			where: req.body as { discriminator: string; username: string } | ||||
| 		}), | ||||
| 		req.body.type | ||||
| 	); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/:id", route({}), async (req: Request, res: Response) => { | ||||
| 	const { id } = req.params; | ||||
| 	if (id === req.user_id) throw new HTTPError("You can't remove yourself as a friend"); | ||||
| 
 | ||||
| 	const user = await User.findOneOrFail({ id: req.user_id }, { select: userProjection, relations: ["relationships"] }); | ||||
| 	const friend = await User.findOneOrFail({ id: id }, { select: userProjection, relations: ["relationships"] }); | ||||
| 
 | ||||
| 	const relationship = user.relationships.find((x) => x.id === id); | ||||
| 	const friendRequest = friend.relationships.find((x) => x.id === req.user_id); | ||||
| 
 | ||||
| 	if (relationship?.type === RelationshipType.blocked) { | ||||
| 		// unblock user
 | ||||
| 		user.relationships.remove(relationship); | ||||
| 
 | ||||
| 		await Promise.all([ | ||||
| 			user.save(), | ||||
| 			emitEvent({ event: "RELATIONSHIP_REMOVE", user_id: req.user_id, data: relationship } as RelationshipRemoveEvent) | ||||
| 		]); | ||||
| 		return res.sendStatus(204); | ||||
| 	} | ||||
| 	if (!relationship || !friendRequest) throw new HTTPError("You are not friends with the user", 404); | ||||
| 	if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you"); | ||||
| 
 | ||||
| 	user.relationships.remove(relationship); | ||||
| 	friend.relationships.remove(friendRequest); | ||||
| 
 | ||||
| 	await Promise.all([ | ||||
| 		user.save(), | ||||
| 		friend.save(), | ||||
| 		emitEvent({ | ||||
| 			event: "RELATIONSHIP_REMOVE", | ||||
| 			data: relationship, | ||||
| 			user_id: req.user_id | ||||
| 		} as RelationshipRemoveEvent), | ||||
| 		emitEvent({ | ||||
| 			event: "RELATIONSHIP_REMOVE", | ||||
| 			data: friendRequest, | ||||
| 			user_id: id | ||||
| 		} as RelationshipRemoveEvent) | ||||
| 	]); | ||||
| 
 | ||||
| 	return res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
| 
 | ||||
| async function updateRelationship(req: Request, res: Response, friend: User, type: RelationshipType) { | ||||
| 	const id = friend.id; | ||||
| 	if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend"); | ||||
| @ -114,71 +190,3 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ | ||||
| 
 | ||||
| 	return res.sendStatus(204); | ||||
| } | ||||
| 
 | ||||
| router.put("/:id", check({ $type: new Length(Number, 1, 4) }), async (req: Request, res: Response) => { | ||||
| 	return await updateRelationship( | ||||
| 		req, | ||||
| 		res, | ||||
| 		await User.findOneOrFail({ id: req.params.id }, { relations: ["relationships"], select: userProjection }), | ||||
| 		req.body.type | ||||
| 	); | ||||
| }); | ||||
| 
 | ||||
| router.post("/", check({ discriminator: String, username: String }), async (req: Request, res: Response) => { | ||||
| 	return await updateRelationship( | ||||
| 		req, | ||||
| 		res, | ||||
| 		await User.findOneOrFail({ | ||||
| 			relations: ["relationships"], | ||||
| 			select: userProjection, | ||||
| 			where: req.body as { discriminator: string; username: string } | ||||
| 		}), | ||||
| 		req.body.type | ||||
| 	); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/:id", async (req: Request, res: Response) => { | ||||
| 	const { id } = req.params; | ||||
| 	if (id === req.user_id) throw new HTTPError("You can't remove yourself as a friend"); | ||||
| 
 | ||||
| 	const user = await User.findOneOrFail({ id: req.user_id }, { select: userProjection, relations: ["relationships"] }); | ||||
| 	const friend = await User.findOneOrFail({ id: id }, { select: userProjection, relations: ["relationships"] }); | ||||
| 
 | ||||
| 	const relationship = user.relationships.find((x) => x.id === id); | ||||
| 	const friendRequest = friend.relationships.find((x) => x.id === req.user_id); | ||||
| 
 | ||||
| 	if (relationship?.type === RelationshipType.blocked) { | ||||
| 		// unblock user
 | ||||
| 		user.relationships.remove(relationship); | ||||
| 
 | ||||
| 		await Promise.all([ | ||||
| 			user.save(), | ||||
| 			emitEvent({ event: "RELATIONSHIP_REMOVE", user_id: req.user_id, data: relationship } as RelationshipRemoveEvent) | ||||
| 		]); | ||||
| 		return res.sendStatus(204); | ||||
| 	} | ||||
| 	if (!relationship || !friendRequest) throw new HTTPError("You are not friends with the user", 404); | ||||
| 	if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you"); | ||||
| 
 | ||||
| 	user.relationships.remove(relationship); | ||||
| 	friend.relationships.remove(friendRequest); | ||||
| 
 | ||||
| 	await Promise.all([ | ||||
| 		user.save(), | ||||
| 		friend.save(), | ||||
| 		emitEvent({ | ||||
| 			event: "RELATIONSHIP_REMOVE", | ||||
| 			data: relationship, | ||||
| 			user_id: req.user_id | ||||
| 		} as RelationshipRemoveEvent), | ||||
| 		emitEvent({ | ||||
| 			event: "RELATIONSHIP_REMOVE", | ||||
| 			data: friendRequest, | ||||
| 			user_id: id | ||||
| 		} as RelationshipRemoveEvent) | ||||
| 	]); | ||||
| 
 | ||||
| 	return res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
|  | ||||
| @ -1,11 +1,12 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { User, UserSettings } from "@fosscord/util"; | ||||
| import { check } from "../../../util/instanceOf"; | ||||
| import { UserSettingsSchema } from "../../../schema/User"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.patch("/", check(UserSettingsSchema), async (req: Request, res: Response) => { | ||||
| export interface UserSettingsSchema extends UserSettings {} | ||||
| 
 | ||||
| router.patch("/", route({ body: "UserSettingsSchema" }), async (req: Request, res: Response) => { | ||||
| 	const body = req.body as UserSettings; | ||||
| 
 | ||||
| 	// only users can update user settings
 | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| import { Router, Request, Response } from "express"; | ||||
| import {getIpAdress} from "../../util/ipAddress"; | ||||
| import {getVoiceRegions} from "../../util/Voice"; | ||||
| import { getIpAdress, route } from "@fosscord/api"; | ||||
| import { getVoiceRegions } from "@fosscord/api"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
|     res.json(await getVoiceRegions(getIpAdress(req), true))//vip true?
 | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	res.json(await getVoiceRegions(getIpAdress(req), true)); //vip true?
 | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
|  | ||||
| @ -1,9 +0,0 @@ | ||||
| export const BanCreateSchema = { | ||||
| 	$delete_message_days: String, | ||||
| 	$reason: String, | ||||
| }; | ||||
| 
 | ||||
| export interface BanCreateSchema { | ||||
| 	delete_message_days?: string; | ||||
| 	reason?: string; | ||||
| } | ||||
| @ -1,68 +0,0 @@ | ||||
| import { ChannelType } from "@fosscord/util"; | ||||
| import { Length } from "../util/instanceOf"; | ||||
| 
 | ||||
| export const ChannelModifySchema = { | ||||
| 	name: new Length(String, 2, 100), | ||||
| 	type: new Length(Number, 0, 13), | ||||
| 	$topic: new Length(String, 0, 1024), | ||||
| 	$bitrate: Number, | ||||
| 	$user_limit: Number, | ||||
| 	$rate_limit_per_user: new Length(Number, 0, 21600), | ||||
| 	$position: Number, | ||||
| 	$permission_overwrites: [ | ||||
| 		{ | ||||
| 			id: String, | ||||
| 			type: new Length(Number, 0, 1), // either 0 (role) or 1 (member)
 | ||||
| 			allow: BigInt, | ||||
| 			deny: BigInt | ||||
| 		} | ||||
| 	], | ||||
| 	$parent_id: String, | ||||
| 	$rtc_region: String, | ||||
| 	$default_auto_archive_duration: Number, | ||||
| 	$id: String, // kept for backwards compatibility does nothing (need for guild create)
 | ||||
| 	$nsfw: Boolean | ||||
| }; | ||||
| 
 | ||||
| export const DmChannelCreateSchema = { | ||||
| 	$name: String, | ||||
| 	recipients: new Length([String], 1, 10) | ||||
| }; | ||||
| 
 | ||||
| export interface DmChannelCreateSchema { | ||||
| 	name?: string; | ||||
| 	recipients: string[]; | ||||
| } | ||||
| 
 | ||||
| export interface ChannelModifySchema { | ||||
| 	name: string; | ||||
| 	type: number; | ||||
| 	topic?: string; | ||||
| 	bitrate?: number; | ||||
| 	user_limit?: number; | ||||
| 	rate_limit_per_user?: number; | ||||
| 	position?: number; | ||||
| 	permission_overwrites?: { | ||||
| 		id: string; | ||||
| 		type: number; | ||||
| 		allow: bigint; | ||||
| 		deny: bigint; | ||||
| 	}[]; | ||||
| 	parent_id?: string; | ||||
| 	id?: string; // is not used (only for guild create)
 | ||||
| 	nsfw?: boolean; | ||||
| 	rtc_region?: string; | ||||
| 	default_auto_archive_duration?: number; | ||||
| } | ||||
| 
 | ||||
| export const ChannelGuildPositionUpdateSchema = [ | ||||
| 	{ | ||||
| 		id: String, | ||||
| 		$position: Number | ||||
| 	} | ||||
| ]; | ||||
| 
 | ||||
| export type ChannelGuildPositionUpdateSchema = { | ||||
| 	id: string; | ||||
| 	position?: number; | ||||
| }[]; | ||||
| @ -1,13 +0,0 @@ | ||||
| // https://discord.com/developers/docs/resources/emoji
 | ||||
| 
 | ||||
| export const EmojiCreateSchema = { | ||||
| 	name: String, //name of the emoji
 | ||||
| 	image: String, // image data the 128x128 emoji image uri
 | ||||
| 	$roles: Array //roles allowed to use this emoji
 | ||||
| }; | ||||
| 
 | ||||
| export interface EmojiCreateSchema { | ||||
| 	name: string; // name of the emoji
 | ||||
| 	image: string; // image data the 128x128 emoji image uri
 | ||||
| 	roles?: string[]; //roles allowed to use this emoji
 | ||||
| } | ||||
| @ -1,106 +0,0 @@ | ||||
| import { Channel } from "@fosscord/util"; | ||||
| import { Length } from "../util/instanceOf"; | ||||
| import { ChannelModifySchema } from "./Channel"; | ||||
| 
 | ||||
| export const GuildCreateSchema = { | ||||
| 	name: new Length(String, 2, 100), | ||||
| 	$region: String, // auto complete voice region of the user
 | ||||
| 	$icon: String, | ||||
| 	$channels: [ChannelModifySchema], | ||||
| 	$guild_template_code: String, | ||||
| 	$system_channel_id: String, | ||||
| 	$rules_channel_id: String | ||||
| }; | ||||
| 
 | ||||
| export interface GuildCreateSchema { | ||||
| 	name: string; | ||||
| 	region?: string; | ||||
| 	icon?: string; | ||||
| 	channels?: ChannelModifySchema[]; | ||||
| 	guild_template_code?: string; | ||||
| 	system_channel_id?: string; | ||||
| 	rules_channel_id?: string; | ||||
| } | ||||
| 
 | ||||
| export const GuildUpdateSchema = { | ||||
| 	...GuildCreateSchema, | ||||
| 	name: undefined, | ||||
| 	$name: new Length(String, 2, 100), | ||||
| 	$banner: String, | ||||
| 	$splash: String, | ||||
| 	$description: String, | ||||
| 	$features: [String], | ||||
| 	$icon: String, | ||||
| 	$verification_level: Number, | ||||
| 	$default_message_notifications: Number, | ||||
| 	$system_channel_flags: Number, | ||||
| 	$system_channel_id: String, | ||||
| 	$explicit_content_filter: Number, | ||||
| 	$public_updates_channel_id: String, | ||||
| 	$afk_timeout: Number, | ||||
| 	$afk_channel_id: String, | ||||
| 	$preferred_locale: String | ||||
| }; | ||||
| // @ts-ignore
 | ||||
| delete GuildUpdateSchema.$channels; | ||||
| 
 | ||||
| export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> { | ||||
| 	banner?: string; | ||||
| 	splash?: string; | ||||
| 	description?: string; | ||||
| 	features?: string[]; | ||||
| 	verification_level?: number; | ||||
| 	default_message_notifications?: number; | ||||
| 	system_channel_flags?: number; | ||||
| 	explicit_content_filter?: number; | ||||
| 	public_updates_channel_id?: string; | ||||
| 	afk_timeout?: number; | ||||
| 	afk_channel_id?: string; | ||||
| 	preferred_locale?: string; | ||||
| } | ||||
| 
 | ||||
| export const GuildTemplateCreateSchema = { | ||||
| 	name: String, | ||||
| 	$avatar: String | ||||
| }; | ||||
| 
 | ||||
| export interface GuildTemplateCreateSchema { | ||||
| 	name: string; | ||||
| 	avatar?: string; | ||||
| } | ||||
| 
 | ||||
| export const GuildUpdateWelcomeScreenSchema = { | ||||
| 	$welcome_channels: [ | ||||
| 		{ | ||||
| 			channel_id: String, | ||||
| 			description: String, | ||||
| 			$emoji_id: String, | ||||
| 			emoji_name: String | ||||
| 		} | ||||
| 	], | ||||
| 	$enabled: Boolean, | ||||
| 	$description: new Length(String, 0, 140) | ||||
| }; | ||||
| 
 | ||||
| export interface GuildUpdateWelcomeScreenSchema { | ||||
| 	welcome_channels?: { | ||||
| 		channel_id: string; | ||||
| 		description: string; | ||||
| 		emoji_id?: string; | ||||
| 		emoji_name: string; | ||||
| 	}[]; | ||||
| 	enabled?: boolean; | ||||
| 	description?: string; | ||||
| } | ||||
| 
 | ||||
| export const VoiceStateUpdateSchema = { | ||||
| 	channel_id: String, // Snowflake
 | ||||
| 	$suppress: Boolean, | ||||
| 	$request_to_speak_timestamp: String // ISO8601 timestamp
 | ||||
| }; | ||||
| 
 | ||||
| export interface VoiceStateUpdateSchema { | ||||
| 	channel_id: string; // Snowflake
 | ||||
| 	suppress?: boolean; | ||||
| 	request_to_speak_timestamp?: string // ISO8601 timestamp
 | ||||
| } | ||||
| @ -1,22 +0,0 @@ | ||||
| export const InviteCreateSchema = { | ||||
| 	$target_user_id: String, | ||||
| 	$target_type: String, | ||||
| 	$validate: String, //? wtf is this
 | ||||
| 	$max_age: Number, | ||||
| 	$max_uses: Number, | ||||
| 	$temporary: Boolean, | ||||
| 	$unique: Boolean, | ||||
| 	$target_user: String, | ||||
| 	$target_user_type: Number | ||||
| }; | ||||
| export interface InviteCreateSchema { | ||||
| 	target_user_id?: string; | ||||
| 	target_type?: string; | ||||
| 	validate?: string; //? wtf is this
 | ||||
| 	max_age?: number; | ||||
| 	max_uses?: number; | ||||
| 	temporary?: boolean; | ||||
| 	unique?: boolean; | ||||
| 	target_user?: string; | ||||
| 	target_user_type?: number; | ||||
| } | ||||
| @ -1,29 +0,0 @@ | ||||
| export const MemberCreateSchema = { | ||||
| 	id: String, | ||||
| 	nick: String, | ||||
| 	guild_id: String, | ||||
| 	joined_at: Date | ||||
| }; | ||||
| 
 | ||||
| export interface MemberCreateSchema { | ||||
| 	id: string; | ||||
| 	nick: string; | ||||
| 	guild_id: string; | ||||
| 	joined_at: Date; | ||||
| } | ||||
| 
 | ||||
| export const MemberNickChangeSchema = { | ||||
| 	nick: String | ||||
| }; | ||||
| 
 | ||||
| export interface MemberNickChangeSchema { | ||||
| 	nick: string; | ||||
| } | ||||
| 
 | ||||
| export const MemberChangeSchema = { | ||||
| 	$roles: [String] | ||||
| }; | ||||
| 
 | ||||
| export interface MemberChangeSchema { | ||||
| 	roles?: string[]; | ||||
| } | ||||
| @ -1,92 +0,0 @@ | ||||
| import { Embed } from "@fosscord/util"; | ||||
| import { Length } from "../util/instanceOf"; | ||||
| 
 | ||||
| export const EmbedImage = { | ||||
| 	$url: String, | ||||
| 	$width: Number, | ||||
| 	$height: Number | ||||
| }; | ||||
| 
 | ||||
| const embed = { | ||||
| 	$title: new Length(String, 0, 256), //title of embed
 | ||||
| 	$type: String, // type of embed (always "rich" for webhook embeds)
 | ||||
| 	$description: new Length(String, 0, 2048), // description of embed
 | ||||
| 	$url: String, // url of embed
 | ||||
| 	$timestamp: String, // ISO8601 timestamp
 | ||||
| 	$color: Number, // color code of the embed
 | ||||
| 	$footer: { | ||||
| 		text: new Length(String, 0, 2048), | ||||
| 		icon_url: String, | ||||
| 		proxy_icon_url: String | ||||
| 	}, // footer object	footer information
 | ||||
| 	$image: EmbedImage, // image object	image information
 | ||||
| 	$thumbnail: EmbedImage, // thumbnail object	thumbnail information
 | ||||
| 	$video: EmbedImage, // video object	video information
 | ||||
| 	$provider: { | ||||
| 		name: String, | ||||
| 		url: String | ||||
| 	}, // provider object	provider information
 | ||||
| 	$author: { | ||||
| 		name: new Length(String, 0, 256), | ||||
| 		url: String, | ||||
| 		icon_url: String, | ||||
| 		proxy_icon_url: String | ||||
| 	}, // author object	author information
 | ||||
| 	$fields: new Length( | ||||
| 		[ | ||||
| 			{ | ||||
| 				name: new Length(String, 0, 256), | ||||
| 				value: new Length(String, 0, 1024), | ||||
| 				$inline: Boolean | ||||
| 			} | ||||
| 		], | ||||
| 		0, | ||||
| 		25 | ||||
| 	) | ||||
| }; | ||||
| 
 | ||||
| export const MessageCreateSchema = { | ||||
| 	$content: new Length(String, 0, 2000), | ||||
| 	$nonce: String, | ||||
| 	$tts: Boolean, | ||||
| 	$flags: String, | ||||
| 	$embed: embed, | ||||
| 	// TODO: ^ embed is deprecated in favor of embeds (https://discord.com/developers/docs/resources/channel#message-object)
 | ||||
| 	// $embeds: [embed],
 | ||||
| 	$allowed_mentions: { | ||||
| 		$parse: [String], | ||||
| 		$roles: [String], | ||||
| 		$users: [String], | ||||
| 		$replied_user: Boolean | ||||
| 	}, | ||||
| 	$message_reference: { | ||||
| 		message_id: String, | ||||
| 		channel_id: String, | ||||
| 		$guild_id: String, | ||||
| 		$fail_if_not_exists: Boolean | ||||
| 	}, | ||||
| 	$payload_json: String, | ||||
| 	$file: Object | ||||
| }; | ||||
| 
 | ||||
| export interface MessageCreateSchema { | ||||
| 	content?: string; | ||||
| 	nonce?: string; | ||||
| 	tts?: boolean; | ||||
| 	flags?: string; | ||||
| 	embed?: Embed & { timestamp?: string }; | ||||
| 	allowed_mentions?: { | ||||
| 		parse?: string[]; | ||||
| 		roles?: string[]; | ||||
| 		users?: string[]; | ||||
| 		replied_user?: boolean; | ||||
| 	}; | ||||
| 	message_reference?: { | ||||
| 		message_id: string; | ||||
| 		channel_id: string; | ||||
| 		guild_id?: string; | ||||
| 		fail_if_not_exists?: boolean; | ||||
| 	}; | ||||
| 	payload_json?: string; | ||||
| 	file?: any; | ||||
| } | ||||
| @ -1,29 +0,0 @@ | ||||
| export const RoleModifySchema = { | ||||
| 	$name: String, | ||||
| 	$permissions: BigInt, | ||||
| 	$color: Number, | ||||
| 	$hoist: Boolean, // whether the role should be displayed separately in the sidebar
 | ||||
| 	$mentionable: Boolean, // whether the role should be mentionable
 | ||||
| 	$position: Number | ||||
| }; | ||||
| 
 | ||||
| export interface RoleModifySchema { | ||||
| 	name?: string; | ||||
| 	permissions?: bigint; | ||||
| 	color?: number; | ||||
| 	hoist?: boolean; // whether the role should be displayed separately in the sidebar
 | ||||
| 	mentionable?: boolean; // whether the role should be mentionable
 | ||||
| 	position?: number; | ||||
| } | ||||
| 
 | ||||
| export const RolePositionUpdateSchema = [ | ||||
| 	{ | ||||
| 		id: String, | ||||
| 		position: Number | ||||
| 	} | ||||
| ]; | ||||
| 
 | ||||
| export type RolePositionUpdateSchema = { | ||||
| 	id: string; | ||||
| 	position: number; | ||||
| }[]; | ||||
| @ -1,19 +0,0 @@ | ||||
| export const TemplateCreateSchema = { | ||||
| 	name: String, | ||||
| 	$description: String, | ||||
| }; | ||||
| 
 | ||||
| export interface TemplateCreateSchema { | ||||
| 	name: string; | ||||
| 	description?: string; | ||||
| } | ||||
| 
 | ||||
| export const TemplateModifySchema = { | ||||
| 	name: String, | ||||
| 	$description: String, | ||||
| }; | ||||
| 
 | ||||
| export interface TemplateModifySchema { | ||||
| 	name: string; | ||||
| 	description?: string; | ||||
| } | ||||
| @ -1,74 +0,0 @@ | ||||
| import { UserSettings } from "../../../util/dist"; | ||||
| import { Length } from "../util/instanceOf"; | ||||
| 
 | ||||
| export const UserModifySchema = { | ||||
| 	$username: new Length(String, 2, 32), | ||||
| 	$avatar: String, | ||||
| 	$bio: new Length(String, 0, 190), | ||||
| 	$accent_color: Number, | ||||
| 	$banner: String, | ||||
| 	$password: String, | ||||
| 	$new_password: String, | ||||
| 	$code: String // 2fa code
 | ||||
| }; | ||||
| 
 | ||||
| export interface UserModifySchema { | ||||
| 	username?: string; | ||||
| 	avatar?: string | null; | ||||
| 	bio?: string; | ||||
| 	accent_color?: number | null; | ||||
| 	banner?: string | null; | ||||
| 	password?: string; | ||||
| 	new_password?: string; | ||||
| 	code?: string; | ||||
| } | ||||
| 
 | ||||
| export const UserSettingsSchema = { | ||||
| 	$afk_timeout: Number, | ||||
| 	$allow_accessibility_detection: Boolean, | ||||
| 	$animate_emoji: Boolean, | ||||
| 	$animate_stickers: Number, | ||||
| 	$contact_sync_enabled: Boolean, | ||||
| 	$convert_emoticons: Boolean, | ||||
| 	$custom_status: { | ||||
| 		$emoji_id: String, | ||||
| 		$emoji_name: String, | ||||
| 		$expires_at: Number, | ||||
| 		$text: String | ||||
| 	}, | ||||
| 	$default_guilds_restricted: Boolean, | ||||
| 	$detect_platform_accounts: Boolean, | ||||
| 	$developer_mode: Boolean, | ||||
| 	$disable_games_tab: Boolean, | ||||
| 	$enable_tts_command: Boolean, | ||||
| 	$explicit_content_filter: Number, | ||||
| 	$friend_source_flags: { | ||||
| 		all: Boolean | ||||
| 	}, | ||||
| 	$gateway_connected: Boolean, | ||||
| 	$gif_auto_play: Boolean, | ||||
| 	$guild_folders: [ | ||||
| 		{ | ||||
| 			color: Number, | ||||
| 			guild_ids: [String], | ||||
| 			id: Number, | ||||
| 			name: String | ||||
| 		} | ||||
| 	], | ||||
| 	$guild_positions: [String], | ||||
| 	$inline_attachment_media: Boolean, | ||||
| 	$inline_embed_media: Boolean, | ||||
| 	$locale: String, | ||||
| 	$message_display_compact: Boolean, | ||||
| 	$native_phone_integration_enabled: Boolean, | ||||
| 	$render_embeds: Boolean, | ||||
| 	$render_reactions: Boolean, | ||||
| 	$restricted_guilds: [String], | ||||
| 	$show_current_game: Boolean, | ||||
| 	$status: String, | ||||
| 	$stream_notifications_enabled: Boolean, | ||||
| 	$theme: String, | ||||
| 	$timezone_offset: Number | ||||
| }; | ||||
| 
 | ||||
| export interface UserSettingsSchema extends UserSettings {} | ||||
| @ -1,10 +0,0 @@ | ||||
| // https://discord.com/developers/docs/resources/guild#guild-widget-object
 | ||||
| export const WidgetModifySchema = { | ||||
| 	enabled: Boolean, // whether the widget is enabled
 | ||||
| 	channel_id: String // the widget channel id
 | ||||
| }; | ||||
| 
 | ||||
| export interface WidgetModifySchema { | ||||
| 	enabled: boolean; // whether the widget is enabled
 | ||||
| 	channel_id: string; // the widget channel id
 | ||||
| } | ||||
| @ -1,11 +0,0 @@ | ||||
| export * from "./Ban"; | ||||
| export * from "./Channel"; | ||||
| export * from "./Emoji"; | ||||
| export * from "./Guild"; | ||||
| export * from "./Invite"; | ||||
| export * from "./Member"; | ||||
| export * from "./Message"; | ||||
| export * from "./Roles"; | ||||
| export * from "./Template"; | ||||
| export * from "./User"; | ||||
| export * from "./Widget"; | ||||
| @ -1,12 +1,12 @@ | ||||
| import { check } from "./../util/passwordStrength"; | ||||
| import { checkPassword } from "@fosscord/api"; | ||||
| 
 | ||||
| console.log(check("123456789012345")); | ||||
| console.log(checkPassword("123456789012345")); | ||||
| // -> 0.25
 | ||||
| console.log(check("ABCDEFGHIJKLMOPQ")); | ||||
| console.log(checkPassword("ABCDEFGHIJKLMOPQ")); | ||||
| // -> 0.25
 | ||||
| console.log(check("ABC123___...123")); | ||||
| console.log(checkPassword("ABC123___...123")); | ||||
| // ->
 | ||||
| console.log(check("")); | ||||
| console.log(checkPassword("")); | ||||
| // ->
 | ||||
| // console.log(check(""));
 | ||||
| // console.log(checkPassword(""));
 | ||||
| // // ->
 | ||||
|  | ||||
| @ -22,7 +22,7 @@ import { | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import fetch from "node-fetch"; | ||||
| import cheerio from "cheerio"; | ||||
| import { MessageCreateSchema } from "../schema/Message"; | ||||
| import { MessageCreateSchema } from "../routes/channels/#channel_id/messages"; | ||||
| 
 | ||||
| // TODO: check webhook, application, system author
 | ||||
| 
 | ||||
|  | ||||
| @ -1,32 +1,32 @@ | ||||
| import {Config} from "@fosscord/util"; | ||||
| import {distanceBetweenLocations, IPAnalysis} from "./ipAddress"; | ||||
| import { Config } from "@fosscord/util"; | ||||
| import { distanceBetweenLocations, IPAnalysis } from "./ipAddress"; | ||||
| 
 | ||||
| export async function getVoiceRegions(ipAddress: string, vip: boolean) { | ||||
|     const regions = Config.get().regions; | ||||
|     const availableRegions = regions.available.filter(ar => vip ? true : !ar.vip); | ||||
|     let optimalId = regions.default | ||||
| 	const regions = Config.get().regions; | ||||
| 	const availableRegions = regions.available.filter((ar) => (vip ? true : !ar.vip)); | ||||
| 	let optimalId = regions.default; | ||||
| 
 | ||||
|     if(!regions.useDefaultAsOptimal) { | ||||
|         const clientIpAnalysis = await IPAnalysis(ipAddress) | ||||
| 	if (!regions.useDefaultAsOptimal) { | ||||
| 		const clientIpAnalysis = await IPAnalysis(ipAddress); | ||||
| 
 | ||||
|         let min = Number.POSITIVE_INFINITY | ||||
| 		let min = Number.POSITIVE_INFINITY; | ||||
| 
 | ||||
|         for (let ar of availableRegions) { | ||||
|             //TODO the endpoint location should be saved in the database if not already present to prevent IPAnalysis call
 | ||||
|             const dist = distanceBetweenLocations(clientIpAnalysis, ar.location || (await IPAnalysis(ar.endpoint))) | ||||
| 		for (let ar of availableRegions) { | ||||
| 			//TODO the endpoint location should be saved in the database if not already present to prevent IPAnalysis call
 | ||||
| 			const dist = distanceBetweenLocations(clientIpAnalysis, ar.location || (await IPAnalysis(ar.endpoint))); | ||||
| 
 | ||||
|             if(dist < min) { | ||||
|                 min = dist | ||||
|                 optimalId = ar.id | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 			if (dist < min) { | ||||
| 				min = dist; | ||||
| 				optimalId = ar.id; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     return availableRegions.map(ar => ({ | ||||
|         id: ar.id, | ||||
|         name: ar.name, | ||||
|         custom: ar.custom, | ||||
|         deprecated: ar.deprecated, | ||||
|         optimal: ar.id === optimalId | ||||
|     })) | ||||
| } | ||||
| 	return availableRegions.map((ar) => ({ | ||||
| 		id: ar.id, | ||||
| 		name: ar.name, | ||||
| 		custom: ar.custom, | ||||
| 		deprecated: ar.deprecated, | ||||
| 		optimal: ar.id === optimalId | ||||
| 	})); | ||||
| } | ||||
|  | ||||
| @ -1,54 +0,0 @@ | ||||
| import { Channel, ChannelType, DiscordApiErrors, emitEvent, getPermission, VoiceState, VoiceStateUpdateEvent } from "@fosscord/util"; | ||||
| import { VoiceStateUpdateSchema } from "../schema"; | ||||
| 
 | ||||
| 
 | ||||
| //TODO need more testing when community guild and voice stage channel are working
 | ||||
| export async function updateVoiceState(vsuSchema: VoiceStateUpdateSchema, guildId: string, userId: string, targetUserId?: string) { | ||||
| 	const perms = await getPermission(userId, guildId, vsuSchema.channel_id); | ||||
| 
 | ||||
| 	/* | ||||
| 	From https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state
 | ||||
| 	You must have the MUTE_MEMBERS permission to unsuppress yourself. You can always suppress yourself. | ||||
| 	You must have the REQUEST_TO_SPEAK permission to request to speak. You can always clear your own request to speak. | ||||
| 	 */ | ||||
| 	if (targetUserId !== undefined || (vsuSchema.suppress !== undefined && !vsuSchema.suppress)) { | ||||
| 		perms.hasThrow("MUTE_MEMBERS"); | ||||
| 	} | ||||
| 	if (vsuSchema.request_to_speak_timestamp !== undefined && vsuSchema.request_to_speak_timestamp !== "") { | ||||
| 		perms.hasThrow("REQUEST_TO_SPEAK") | ||||
| 	} | ||||
| 
 | ||||
| 	if (!targetUserId) { | ||||
| 		targetUserId = userId; | ||||
| 	} else { | ||||
| 		if (vsuSchema.suppress !== undefined && vsuSchema.suppress) | ||||
| 			vsuSchema.request_to_speak_timestamp = "" //Need to check if empty string is the right value
 | ||||
| 	} | ||||
| 
 | ||||
| 	//TODO assumed that empty string means clean, need to test if it's right
 | ||||
| 	let voiceState | ||||
| 	try { | ||||
| 		voiceState = await VoiceState.findOneOrFail({ | ||||
| 			guild_id: guildId, | ||||
| 			channel_id: vsuSchema.channel_id, | ||||
| 			user_id: targetUserId | ||||
| 		}); | ||||
| 	} catch (error) { | ||||
| 		throw DiscordApiErrors.UNKNOWN_VOICE_STATE; | ||||
| 	} | ||||
| 
 | ||||
| 	voiceState.assign(vsuSchema); | ||||
| 	const channel = await Channel.findOneOrFail({ guild_id: guildId, id: vsuSchema.channel_id }) | ||||
| 	if (channel.type !== ChannelType.GUILD_STAGE_VOICE) { | ||||
| 		throw DiscordApiErrors.CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE; | ||||
| 	} | ||||
| 
 | ||||
| 	await Promise.all([ | ||||
| 		voiceState.save(), | ||||
| 		emitEvent({ | ||||
| 			event: "VOICE_STATE_UPDATE", | ||||
| 			data: voiceState, | ||||
| 			guild_id: guildId | ||||
| 		} as VoiceStateUpdateEvent)]); | ||||
| 	return; | ||||
| } | ||||
							
								
								
									
										10
									
								
								api/src/util/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								api/src/util/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| export * from "./Base64"; | ||||
| export * from "./cdn"; | ||||
| export * from "./instanceOf"; | ||||
| export * from "./ipAddress"; | ||||
| export * from "./Message"; | ||||
| export * from "./passwordStrength"; | ||||
| export * from "./RandomInviteID"; | ||||
| export * from "./route"; | ||||
| export * from "./String"; | ||||
| export * from "./Voice"; | ||||
| @ -16,7 +16,7 @@ const blocklist: string[] = []; // TODO: update ones passwordblocklist is stored | ||||
|  * | ||||
|  * Returns: 0 > pw > 1 | ||||
|  */ | ||||
| export function check(password: string): number { | ||||
| export function checkPassword(password: string): number { | ||||
| 	const { minLength, minNumbers, minUpperCase, minSymbols } = Config.get().register.password; | ||||
| 	var strength = 0; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										76
									
								
								api/src/util/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								api/src/util/route.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | ||||
| import { DiscordApiErrors, Event, EventData, getPermission, PermissionResolvable, Permissions } from "@fosscord/util"; | ||||
| import { NextFunction, Request, Response } from "express"; | ||||
| import fs from "fs"; | ||||
| import path from "path"; | ||||
| import Ajv from "ajv"; | ||||
| import { AnyValidateFunction } from "ajv/dist/core"; | ||||
| import { FieldErrors } from ".."; | ||||
| import addFormats from "ajv-formats"; | ||||
| 
 | ||||
| const SchemaPath = path.join(__dirname, "..", "..", "assets", "schemas.json"); | ||||
| const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" })); | ||||
| export const ajv = new Ajv({ | ||||
| 	allErrors: true, | ||||
| 	parseDate: true, | ||||
| 	allowDate: true, | ||||
| 	schemas, | ||||
| 	messages: true, | ||||
| 	strict: true, | ||||
| 	strictRequired: true | ||||
| }); | ||||
| addFormats(ajv); | ||||
| 
 | ||||
| declare global { | ||||
| 	namespace Express { | ||||
| 		interface Request { | ||||
| 			permission?: Permissions; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export type RouteSchema = string; // typescript interface name
 | ||||
| export type RouteResponse = { status?: number; body?: RouteSchema; headers?: Record<string, string> }; | ||||
| 
 | ||||
| export interface RouteOptions { | ||||
| 	permission?: PermissionResolvable; | ||||
| 	body?: RouteSchema; | ||||
| 	response?: RouteResponse; | ||||
| 	example?: { | ||||
| 		body?: any; | ||||
| 		path?: string; | ||||
| 		event?: EventData; | ||||
| 		headers?: Record<string, string>; | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| export function route(opts: RouteOptions) { | ||||
| 	var validate: AnyValidateFunction<any>; | ||||
| 	if (opts.body) { | ||||
| 		// @ts-ignore
 | ||||
| 		validate = ajv.getSchema(opts.body); | ||||
| 		if (!validate) throw new Error(`Body schema ${opts.body} not found`); | ||||
| 	} | ||||
| 
 | ||||
| 	return async (req: Request, res: Response, next: NextFunction) => { | ||||
| 		if (opts.permission) { | ||||
| 			const required = new Permissions(opts.permission); | ||||
| 			const permission = await getPermission(req.user_id, req.params.guild_id, req.params.channel_id); | ||||
| 			console.log(required.bitfield, permission.bitfield, permission.bitfield & required.bitfield); | ||||
| 
 | ||||
| 			// bitfield comparison: check if user lacks certain permission
 | ||||
| 			if (!permission.has(required)) { | ||||
| 				throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(opts.permission as string); | ||||
| 			} | ||||
| 
 | ||||
| 			if (validate) { | ||||
| 				const valid = validate(req.body); | ||||
| 				if (!valid) { | ||||
| 					const fields: Record<string, { code?: string; message: string }> = {}; | ||||
| 					validate.errors?.forEach((x) => (fields[x.instancePath] = { code: x.keyword, message: x.message || "" })); | ||||
| 					throw FieldErrors(fields); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		next(); | ||||
| 	}; | ||||
| } | ||||
							
								
								
									
										0
									
								
								api/tests/automatic.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								api/tests/automatic.test.js
									
									
									
									
									
										Normal file
									
								
							| @ -63,6 +63,11 @@ | ||||
| 
 | ||||
| 		/* Advanced Options */ | ||||
| 		"skipLibCheck": true /* Skip type checking of declaration files. */, | ||||
| 		"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ | ||||
| 		"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, | ||||
| 		"baseUrl": ".", | ||||
| 		"paths": { | ||||
| 			"@fosscord/api": ["src/index.ts"], | ||||
| 			"@fosscord/api/*": ["src/*"] | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										83
									
								
								bundle/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										83
									
								
								bundle/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -17,7 +17,8 @@ | ||||
| 				"async-exit-hook": "^2.0.1", | ||||
| 				"express": "^4.17.1", | ||||
| 				"missing-native-js-functions": "^1.2.15", | ||||
| 				"node-os-utils": "^1.3.5" | ||||
| 				"node-os-utils": "^1.3.5", | ||||
| 				"tsconfig-paths": "^3.11.0" | ||||
| 			}, | ||||
| 			"devDependencies": { | ||||
| 				"@types/amqplib": "^0.8.1", | ||||
| @ -73,6 +74,7 @@ | ||||
| 				"node-fetch": "^2.6.1", | ||||
| 				"patch-package": "^6.4.7", | ||||
| 				"supertest": "^6.1.6", | ||||
| 				"tsconfig-paths": "^3.11.0", | ||||
| 				"typeorm": "^0.2.37" | ||||
| 			}, | ||||
| 			"devDependencies": { | ||||
| @ -158,6 +160,7 @@ | ||||
| 				"missing-native-js-functions": "^1.2.15", | ||||
| 				"mongoose-autopopulate": "^0.12.3", | ||||
| 				"node-fetch": "^2.6.1", | ||||
| 				"typeorm": "^0.2.37", | ||||
| 				"uuid": "^8.3.2", | ||||
| 				"ws": "^7.4.2" | ||||
| 			}, | ||||
| @ -194,6 +197,7 @@ | ||||
| 				"pg": "^8.7.1", | ||||
| 				"reflect-metadata": "^0.1.13", | ||||
| 				"sqlite3": "^5.0.2", | ||||
| 				"tsconfig-paths": "^3.11.0", | ||||
| 				"typeorm": "^0.2.37", | ||||
| 				"typescript": "^4.4.2", | ||||
| 				"typescript-json-schema": "^0.50.1" | ||||
| @ -326,6 +330,11 @@ | ||||
| 				"i18next": ">=17.0.11" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@types/json5": { | ||||
| 			"version": "0.0.29", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", | ||||
| 			"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" | ||||
| 		}, | ||||
| 		"node_modules/@types/jsonwebtoken": { | ||||
| 			"version": "8.5.4", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz", | ||||
| @ -795,6 +804,17 @@ | ||||
| 				"url": "https://github.com/sponsors/ljharb" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/json5": { | ||||
| 			"version": "1.0.1", | ||||
| 			"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", | ||||
| 			"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", | ||||
| 			"dependencies": { | ||||
| 				"minimist": "^1.2.0" | ||||
| 			}, | ||||
| 			"bin": { | ||||
| 				"json5": "lib/cli.js" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/media-typer": { | ||||
| 			"version": "0.3.0", | ||||
| 			"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", | ||||
| @ -846,6 +866,11 @@ | ||||
| 				"node": ">= 0.6" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/minimist": { | ||||
| 			"version": "1.2.5", | ||||
| 			"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", | ||||
| 			"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" | ||||
| 		}, | ||||
| 		"node_modules/missing-native-js-functions": { | ||||
| 			"version": "1.2.15", | ||||
| 			"resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.15.tgz", | ||||
| @ -1025,6 +1050,14 @@ | ||||
| 				"node": ">= 0.6" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/strip-bom": { | ||||
| 			"version": "3.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", | ||||
| 			"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", | ||||
| 			"engines": { | ||||
| 				"node": ">=4" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/toidentifier": { | ||||
| 			"version": "1.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", | ||||
| @ -1033,6 +1066,17 @@ | ||||
| 				"node": ">=0.6" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/tsconfig-paths": { | ||||
| 			"version": "3.11.0", | ||||
| 			"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz", | ||||
| 			"integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==", | ||||
| 			"dependencies": { | ||||
| 				"@types/json5": "^0.0.29", | ||||
| 				"json5": "^1.0.1", | ||||
| 				"minimist": "^1.2.0", | ||||
| 				"strip-bom": "^3.0.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/type-is": { | ||||
| 			"version": "1.6.18", | ||||
| 			"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", | ||||
| @ -1145,6 +1189,7 @@ | ||||
| 				"supertest": "^6.1.6", | ||||
| 				"ts-node": "^9.1.1", | ||||
| 				"ts-node-dev": "^1.1.6", | ||||
| 				"tsconfig-paths": "^3.11.0", | ||||
| 				"typeorm": "^0.2.37", | ||||
| 				"typescript": "^4.4.2", | ||||
| 				"typescript-json-schema": "^0.50.1" | ||||
| @ -1211,6 +1256,7 @@ | ||||
| 				"mongoose-autopopulate": "^0.12.3", | ||||
| 				"node-fetch": "^2.6.1", | ||||
| 				"ts-node-dev": "^1.1.6", | ||||
| 				"typeorm": "^0.2.37", | ||||
| 				"typescript": "^4.2.3", | ||||
| 				"uuid": "^8.3.2", | ||||
| 				"ws": "^7.4.2" | ||||
| @ -1238,6 +1284,7 @@ | ||||
| 				"pg": "^8.7.1", | ||||
| 				"reflect-metadata": "^0.1.13", | ||||
| 				"sqlite3": "^5.0.2", | ||||
| 				"tsconfig-paths": "^3.11.0", | ||||
| 				"typeorm": "^0.2.37", | ||||
| 				"typescript": "^4.4.2", | ||||
| 				"typescript-json-schema": "^0.50.1" | ||||
| @ -1334,6 +1381,11 @@ | ||||
| 				"i18next": ">=17.0.11" | ||||
| 			} | ||||
| 		}, | ||||
| 		"@types/json5": { | ||||
| 			"version": "0.0.29", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", | ||||
| 			"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" | ||||
| 		}, | ||||
| 		"@types/jsonwebtoken": { | ||||
| 			"version": "8.5.4", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz", | ||||
| @ -1731,6 +1783,14 @@ | ||||
| 				"has": "^1.0.3" | ||||
| 			} | ||||
| 		}, | ||||
| 		"json5": { | ||||
| 			"version": "1.0.1", | ||||
| 			"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", | ||||
| 			"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", | ||||
| 			"requires": { | ||||
| 				"minimist": "^1.2.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"media-typer": { | ||||
| 			"version": "0.3.0", | ||||
| 			"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", | ||||
| @ -1764,6 +1824,11 @@ | ||||
| 				"mime-db": "1.49.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"minimist": { | ||||
| 			"version": "1.2.5", | ||||
| 			"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", | ||||
| 			"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" | ||||
| 		}, | ||||
| 		"missing-native-js-functions": { | ||||
| 			"version": "1.2.15", | ||||
| 			"resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.15.tgz", | ||||
| @ -1912,11 +1977,27 @@ | ||||
| 			"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", | ||||
| 			"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" | ||||
| 		}, | ||||
| 		"strip-bom": { | ||||
| 			"version": "3.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", | ||||
| 			"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" | ||||
| 		}, | ||||
| 		"toidentifier": { | ||||
| 			"version": "1.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", | ||||
| 			"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" | ||||
| 		}, | ||||
| 		"tsconfig-paths": { | ||||
| 			"version": "3.11.0", | ||||
| 			"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz", | ||||
| 			"integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==", | ||||
| 			"requires": { | ||||
| 				"@types/json5": "^0.0.29", | ||||
| 				"json5": "^1.0.1", | ||||
| 				"minimist": "^1.2.0", | ||||
| 				"strip-bom": "^3.0.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"type-is": { | ||||
| 			"version": "1.6.18", | ||||
| 			"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", | ||||
|  | ||||
| @ -11,7 +11,7 @@ | ||||
| 		"build:api": "cd ../api/ && npm run build", | ||||
| 		"build:cdn": "cd ../cdn/ && npm run build", | ||||
| 		"build:gateway": "cd ../gateway/ && npm run build", | ||||
| 		"start": "npm run build && node dist/start.js", | ||||
| 		"start": "npm run build && node -r ./tsconfig-paths-bootstrap.js dist/start.js", | ||||
| 		"test": "echo \"Error: no test specified\" && exit 1" | ||||
| 	}, | ||||
| 	"repository": { | ||||
| @ -52,6 +52,7 @@ | ||||
| 		"async-exit-hook": "^2.0.1", | ||||
| 		"express": "^4.17.1", | ||||
| 		"missing-native-js-functions": "^1.2.15", | ||||
| 		"node-os-utils": "^1.3.5" | ||||
| 		"node-os-utils": "^1.3.5", | ||||
| 		"tsconfig-paths": "^3.11.0" | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										14
									
								
								bundle/tsconfig-paths-bootstrap.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								bundle/tsconfig-paths-bootstrap.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| const tsConfigPaths = require("tsconfig-paths"); | ||||
| const path = require("path"); | ||||
| 
 | ||||
| const cleanup = tsConfigPaths.register({ | ||||
| 	baseUrl: path.join(__dirname, "node_modules", "@fosscord"), | ||||
| 	paths: { | ||||
| 		"@fosscord/api": ["api/dist/index.js"], | ||||
| 		"@fosscord/api/*": ["api/dist/*"], | ||||
| 		"@fosscord/gateway": ["gateway/dist/index.js"], | ||||
| 		"@fosscord/gateway/*": ["gateway/dist/*"], | ||||
| 		"@fosscord/cdn": ["cdn/dist/index.js"], | ||||
| 		"@fosscord/cdn/*": ["cdn/dist/*"], | ||||
| 	}, | ||||
| }); | ||||
| @ -7,7 +7,7 @@ | ||||
| 	"scripts": { | ||||
| 		"test": "npm run build && jest --coverage ./tests", | ||||
| 		"build": "npx tsc -b .", | ||||
| 		"start": "npm run build && node dist/start.js" | ||||
| 		"start": "npm run build && node -r ./scripts/tsconfig-paths-bootstrap.js dist/start.js" | ||||
| 	}, | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
|  | ||||
							
								
								
									
										10
									
								
								cdn/scripts/tsconfig-paths-bootstrap.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								cdn/scripts/tsconfig-paths-bootstrap.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| const tsConfigPaths = require("tsconfig-paths"); | ||||
| const path = require("path"); | ||||
| 
 | ||||
| const cleanup = tsConfigPaths.register({ | ||||
| 	baseUrl: path.join(__dirname, ".."), | ||||
| 	paths: { | ||||
| 		"@fosscord/cdn": ["dist/index.js"], | ||||
| 		"@fosscord/cdn/*": ["dist/*"], | ||||
| 	}, | ||||
| }); | ||||
| @ -1,73 +1,87 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { Config, Snowflake } from "@fosscord/util"; | ||||
| import { storage } from "../util/Storage"; | ||||
| import { storage } from "@fosscord/cdn/util/Storage"; | ||||
| import FileType from "file-type"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { multer } from "../util/multer"; | ||||
| import { multer } from "@fosscord/cdn/util/multer"; | ||||
| import imageSize from "image-size"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.post("/:channel_id", multer.single("file"), async (req: Request, res: Response) => { | ||||
| 	if (req.headers.signature !== Config.get().security.requestSignature) | ||||
| 		throw new HTTPError("Invalid request signature"); | ||||
| 	if (!req.file) throw new HTTPError("file missing"); | ||||
| router.post( | ||||
| 	"/:channel_id", | ||||
| 	multer.single("file"), | ||||
| 	async (req: Request, res: Response) => { | ||||
| 		if (req.headers.signature !== Config.get().security.requestSignature) | ||||
| 			throw new HTTPError("Invalid request signature"); | ||||
| 		if (!req.file) throw new HTTPError("file missing"); | ||||
| 
 | ||||
| 	const { buffer, mimetype, size, originalname, fieldname } = req.file; | ||||
| 	const { channel_id } = req.params; | ||||
| 	const filename = originalname.replaceAll(" ", "_").replace(/[^a-zA-Z0-9._]+/g, ""); | ||||
| 	const id = Snowflake.generate(); | ||||
| 	const path = `attachments/${channel_id}/${id}/${filename}`; | ||||
| 		const { buffer, mimetype, size, originalname, fieldname } = req.file; | ||||
| 		const { channel_id } = req.params; | ||||
| 		const filename = originalname | ||||
| 			.replaceAll(" ", "_") | ||||
| 			.replace(/[^a-zA-Z0-9._]+/g, ""); | ||||
| 		const id = Snowflake.generate(); | ||||
| 		const path = `attachments/${channel_id}/${id}/${filename}`; | ||||
| 
 | ||||
| 	const endpoint = Config.get()?.cdn.endpoint || "http://localhost:3003"; | ||||
| 		const endpoint = Config.get()?.cdn.endpoint || "http://localhost:3003"; | ||||
| 
 | ||||
| 	await storage.set(path, buffer); | ||||
| 	var width; | ||||
| 	var height; | ||||
| 	if (mimetype.includes("image")) { | ||||
| 		const dimensions = imageSize(buffer); | ||||
| 		if (dimensions) { | ||||
| 			width = dimensions.width; | ||||
| 			height = dimensions.height; | ||||
| 		await storage.set(path, buffer); | ||||
| 		var width; | ||||
| 		var height; | ||||
| 		if (mimetype.includes("image")) { | ||||
| 			const dimensions = imageSize(buffer); | ||||
| 			if (dimensions) { | ||||
| 				width = dimensions.width; | ||||
| 				height = dimensions.height; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		const file = { | ||||
| 			id, | ||||
| 			content_type: mimetype, | ||||
| 			filename: filename, | ||||
| 			size, | ||||
| 			url: `${endpoint}/${path}`, | ||||
| 			width, | ||||
| 			height, | ||||
| 		}; | ||||
| 
 | ||||
| 		return res.json(file); | ||||
| 	} | ||||
| ); | ||||
| 
 | ||||
| 	const file = { | ||||
| 		id, | ||||
| 		content_type: mimetype, | ||||
| 		filename: filename, | ||||
| 		size, | ||||
| 		url: `${endpoint}/${path}`, | ||||
| 		width, | ||||
| 		height, | ||||
| 	}; | ||||
| router.get( | ||||
| 	"/:channel_id/:id/:filename", | ||||
| 	async (req: Request, res: Response) => { | ||||
| 		const { channel_id, id, filename } = req.params; | ||||
| 
 | ||||
| 	return res.json(file); | ||||
| }); | ||||
| 		const file = await storage.get( | ||||
| 			`attachments/${channel_id}/${id}/${filename}` | ||||
| 		); | ||||
| 		if (!file) throw new HTTPError("File not found"); | ||||
| 		const type = await FileType.fromBuffer(file); | ||||
| 
 | ||||
| router.get("/:channel_id/:id/:filename", async (req: Request, res: Response) => { | ||||
| 	const { channel_id, id, filename } = req.params; | ||||
| 		res.set("Content-Type", type?.mime); | ||||
| 		res.set("Cache-Control", "public, max-age=31536000"); | ||||
| 
 | ||||
| 	const file = await storage.get(`attachments/${channel_id}/${id}/${filename}`); | ||||
| 	if (!file) throw new HTTPError("File not found"); | ||||
| 	const type = await FileType.fromBuffer(file); | ||||
| 		return res.send(file); | ||||
| 	} | ||||
| ); | ||||
| 
 | ||||
| 	res.set("Content-Type", type?.mime); | ||||
| 	res.set("Cache-Control", "public, max-age=31536000"); | ||||
| router.delete( | ||||
| 	"/:channel_id/:id/:filename", | ||||
| 	async (req: Request, res: Response) => { | ||||
| 		if (req.headers.signature !== Config.get().security.requestSignature) | ||||
| 			throw new HTTPError("Invalid request signature"); | ||||
| 
 | ||||
| 	return res.send(file); | ||||
| }); | ||||
| 		const { channel_id, id, filename } = req.params; | ||||
| 		const path = `attachments/${channel_id}/${id}/${filename}`; | ||||
| 
 | ||||
| router.delete("/:channel_id/:id/:filename", async (req: Request, res: Response) => { | ||||
| 	if (req.headers.signature !== Config.get().security.requestSignature) | ||||
| 		throw new HTTPError("Invalid request signature"); | ||||
| 		await storage.delete(path); | ||||
| 
 | ||||
| 	const { channel_id, id, filename } = req.params; | ||||
| 	const path = `attachments/${channel_id}/${id}/${filename}`; | ||||
| 
 | ||||
| 	await storage.delete(path); | ||||
| 
 | ||||
| 	return res.send({ success: true }); | ||||
| }); | ||||
| 		return res.send({ success: true }); | ||||
| 	} | ||||
| ); | ||||
| 
 | ||||
| export default router; | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { Config, Snowflake } from "@fosscord/util"; | ||||
| import { storage } from "../util/Storage"; | ||||
| import { storage } from "@fosscord/cdn/util/Storage"; | ||||
| import FileType from "file-type"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { multer } from "../util/multer"; | ||||
| import { multer } from "@fosscord/cdn/util/multer"; | ||||
| import crypto from "crypto"; | ||||
| 
 | ||||
| // TODO: check premium and animated pfp are allowed in the config
 | ||||
| @ -12,36 +12,50 @@ import crypto from "crypto"; | ||||
| // TODO: delete old icons
 | ||||
| 
 | ||||
| const ANIMATED_MIME_TYPES = ["image/apng", "image/gif", "image/gifv"]; | ||||
| const STATIC_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/svg+xml", "image/svg"]; | ||||
| const STATIC_MIME_TYPES = [ | ||||
| 	"image/png", | ||||
| 	"image/jpeg", | ||||
| 	"image/webp", | ||||
| 	"image/svg+xml", | ||||
| 	"image/svg", | ||||
| ]; | ||||
| const ALLOWED_MIME_TYPES = [...ANIMATED_MIME_TYPES, ...STATIC_MIME_TYPES]; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.post("/:user_id", multer.single("file"), async (req: Request, res: Response) => { | ||||
| 	if (req.headers.signature !== Config.get().security.requestSignature) | ||||
| 		throw new HTTPError("Invalid request signature"); | ||||
| 	if (!req.file) throw new HTTPError("Missing file"); | ||||
| 	const { buffer, mimetype, size, originalname, fieldname } = req.file; | ||||
| 	const { user_id } = req.params; | ||||
| router.post( | ||||
| 	"/:user_id", | ||||
| 	multer.single("file"), | ||||
| 	async (req: Request, res: Response) => { | ||||
| 		if (req.headers.signature !== Config.get().security.requestSignature) | ||||
| 			throw new HTTPError("Invalid request signature"); | ||||
| 		if (!req.file) throw new HTTPError("Missing file"); | ||||
| 		const { buffer, mimetype, size, originalname, fieldname } = req.file; | ||||
| 		const { user_id } = req.params; | ||||
| 
 | ||||
| 	var hash = crypto.createHash("md5").update(Snowflake.generate()).digest("hex"); | ||||
| 		var hash = crypto | ||||
| 			.createHash("md5") | ||||
| 			.update(Snowflake.generate()) | ||||
| 			.digest("hex"); | ||||
| 
 | ||||
| 	const type = await FileType.fromBuffer(buffer); | ||||
| 	if (!type || !ALLOWED_MIME_TYPES.includes(type.mime)) throw new HTTPError("Invalid file type"); | ||||
| 	if (ANIMATED_MIME_TYPES.includes(type.mime)) hash = `a_${hash}`; // animated icons have a_ infront of the hash
 | ||||
| 		const type = await FileType.fromBuffer(buffer); | ||||
| 		if (!type || !ALLOWED_MIME_TYPES.includes(type.mime)) | ||||
| 			throw new HTTPError("Invalid file type"); | ||||
| 		if (ANIMATED_MIME_TYPES.includes(type.mime)) hash = `a_${hash}`; // animated icons have a_ infront of the hash
 | ||||
| 
 | ||||
| 	const path = `avatars/${user_id}/${hash}`; | ||||
| 	const endpoint = Config.get().cdn.endpoint || "http://localhost:3003"; | ||||
| 		const path = `avatars/${user_id}/${hash}`; | ||||
| 		const endpoint = Config.get().cdn.endpoint || "http://localhost:3003"; | ||||
| 
 | ||||
| 	await storage.set(path, buffer); | ||||
| 		await storage.set(path, buffer); | ||||
| 
 | ||||
| 	return res.json({ | ||||
| 		id: hash, | ||||
| 		content_type: type.mime, | ||||
| 		size, | ||||
| 		url: `${endpoint}${req.baseUrl}/${user_id}/${hash}`, | ||||
| 	}); | ||||
| }); | ||||
| 		return res.json({ | ||||
| 			id: hash, | ||||
| 			content_type: type.mime, | ||||
| 			size, | ||||
| 			url: `${endpoint}${req.baseUrl}/${user_id}/${hash}`, | ||||
| 		}); | ||||
| 	} | ||||
| ); | ||||
| 
 | ||||
| router.get("/:user_id/:hash", async (req: Request, res: Response) => { | ||||
| 	var { user_id, hash } = req.params; | ||||
|  | ||||
| @ -2,7 +2,7 @@ import { Router, Response, Request } from "express"; | ||||
| import fetch from "node-fetch"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { Snowflake } from "@fosscord/util"; | ||||
| import { storage } from "../util/Storage"; | ||||
| import { storage } from "@fosscord/cdn/util/Storage"; | ||||
| import FileType from "file-type"; | ||||
| import { Config } from "@fosscord/util"; | ||||
| 
 | ||||
| @ -13,7 +13,8 @@ const DEFAULT_FETCH_OPTIONS: any = { | ||||
| 	redirect: "follow", | ||||
| 	follow: 1, | ||||
| 	headers: { | ||||
| 		"user-agent": "Mozilla/5.0 (compatible Fosscordbot/0.1; +https://fosscord.com)", | ||||
| 		"user-agent": | ||||
| 			"Mozilla/5.0 (compatible Fosscordbot/0.1; +https://fosscord.com)", | ||||
| 	}, | ||||
| 	size: 1024 * 1024 * 8, | ||||
| 	compress: true, | ||||
|  | ||||
| @ -65,6 +65,11 @@ | ||||
| 
 | ||||
| 		/* Advanced Options */ | ||||
| 		"skipLibCheck": true /* Skip type checking of declaration files. */, | ||||
| 		"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ | ||||
| 		"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, | ||||
| 		"baseUrl": ".", | ||||
| 		"paths": { | ||||
| 			"@fosscord/cdn/": ["src/index.ts"], | ||||
| 			"@fosscord/cdn/*": ["src/*"] | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										1056
									
								
								gateway/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1056
									
								
								gateway/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -5,7 +5,7 @@ | ||||
| 	"main": "dist/index.js", | ||||
| 	"scripts": { | ||||
| 		"test": "echo \"Error: no test specified\" && exit 1", | ||||
| 		"start": "npm run build && node dist/start.js", | ||||
| 		"start": "npm run build && node -r ./scripts/tsconfig-paths-bootstrap.js dist/start.js", | ||||
| 		"build": "npx tsc -b .", | ||||
| 		"dev": "tsnd --respawn src/start.ts" | ||||
| 	}, | ||||
| @ -35,6 +35,7 @@ | ||||
| 		"missing-native-js-functions": "^1.2.15", | ||||
| 		"mongoose-autopopulate": "^0.12.3", | ||||
| 		"node-fetch": "^2.6.1", | ||||
| 		"typeorm": "^0.2.37", | ||||
| 		"uuid": "^8.3.2", | ||||
| 		"ws": "^7.4.2" | ||||
| 	} | ||||
|  | ||||
							
								
								
									
										10
									
								
								gateway/scripts/tsconfig-paths-bootstrap.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								gateway/scripts/tsconfig-paths-bootstrap.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| const tsConfigPaths = require("tsconfig-paths"); | ||||
| const path = require("path"); | ||||
| 
 | ||||
| const cleanup = tsConfigPaths.register({ | ||||
| 	baseUrl: path.join(__dirname, ".."), | ||||
| 	paths: { | ||||
| 		"@fosscord/gateway": ["dist/index.js"], | ||||
| 		"@fosscord/gateway/*": ["dist/*"], | ||||
| 	}, | ||||
| }); | ||||
| @ -1,4 +1,4 @@ | ||||
| import WebSocket from "../util/WebSocket"; | ||||
| import WebSocket from "@fosscord/gateway/util/WebSocket"; | ||||
| import { Message } from "./Message"; | ||||
| import { Session } from "@fosscord/util"; | ||||
| 
 | ||||
|  | ||||
| @ -1,10 +1,11 @@ | ||||
| import WebSocket, { Server } from "../util/WebSocket"; | ||||
| import WS from "ws"; | ||||
| import WebSocket from "@fosscord/gateway/util/WebSocket"; | ||||
| import { IncomingMessage } from "http"; | ||||
| import { Close } from "./Close"; | ||||
| import { Message } from "./Message"; | ||||
| import { setHeartbeat } from "../util/setHeartbeat"; | ||||
| import { Send } from "../util/Send"; | ||||
| import { CLOSECODES, OPCODES } from "../util/Constants"; | ||||
| import { setHeartbeat } from "@fosscord/gateway/util/setHeartbeat"; | ||||
| import { Send } from "@fosscord/gateway/util/Send"; | ||||
| import { CLOSECODES, OPCODES } from "@fosscord/gateway/util/Constants"; | ||||
| import { createDeflate } from "zlib"; | ||||
| import { URL } from "url"; | ||||
| import { Session } from "@fosscord/util"; | ||||
| @ -17,7 +18,11 @@ try { | ||||
| // TODO: specify rate limit in config
 | ||||
| // TODO: check msg max size
 | ||||
| 
 | ||||
| export async function Connection(this: Server, socket: WebSocket, request: IncomingMessage) { | ||||
| export async function Connection( | ||||
| 	this: WS.Server, | ||||
| 	socket: WebSocket, | ||||
| 	request: IncomingMessage | ||||
| ) { | ||||
| 	try { | ||||
| 		socket.on("close", Close); | ||||
| 		// @ts-ignore
 | ||||
| @ -27,18 +32,21 @@ export async function Connection(this: Server, socket: WebSocket, request: Incom | ||||
| 		// @ts-ignore
 | ||||
| 		socket.encoding = searchParams.get("encoding") || "json"; | ||||
| 		if (!["json", "etf"].includes(socket.encoding)) { | ||||
| 			if (socket.encoding === "etf" && erlpack) throw new Error("Erlpack is not installed: 'npm i -D erlpack'"); | ||||
| 			if (socket.encoding === "etf" && erlpack) | ||||
| 				throw new Error("Erlpack is not installed: 'npm i -D erlpack'"); | ||||
| 			return socket.close(CLOSECODES.Decode_error); | ||||
| 		} | ||||
| 
 | ||||
| 		// @ts-ignore
 | ||||
| 		socket.version = Number(searchParams.get("version")) || 8; | ||||
| 		if (socket.version != 8) return socket.close(CLOSECODES.Invalid_API_version); | ||||
| 		if (socket.version != 8) | ||||
| 			return socket.close(CLOSECODES.Invalid_API_version); | ||||
| 
 | ||||
| 		// @ts-ignore
 | ||||
| 		socket.compress = searchParams.get("compress") || ""; | ||||
| 		if (socket.compress) { | ||||
| 			if (socket.compress !== "zlib-stream") return socket.close(CLOSECODES.Decode_error); | ||||
| 			if (socket.compress !== "zlib-stream") | ||||
| 				return socket.close(CLOSECODES.Decode_error); | ||||
| 			socket.deflate = createDeflate({ chunkSize: 65535 }); | ||||
| 			socket.deflate.on("data", (chunk) => socket.send(chunk)); | ||||
| 		} | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| import WebSocket, { Data } from "../util/WebSocket"; | ||||
| import WebSocket from "@fosscord/gateway/util/WebSocket"; | ||||
| var erlpack: any; | ||||
| try { | ||||
| 	erlpack = require("erlpack"); | ||||
| } catch (error) {} | ||||
| import OPCodeHandlers from "../opcodes"; | ||||
| import { Payload, CLOSECODES, OPCODES } from "../util/Constants"; | ||||
| import { Payload, CLOSECODES, OPCODES } from "@fosscord/gateway/util/Constants"; | ||||
| import { instanceOf, Tuple } from "lambert-server"; | ||||
| import { check } from "../opcodes/instanceOf"; | ||||
| import WS from "ws"; | ||||
| @ -20,8 +20,10 @@ export async function Message(this: WebSocket, buffer: WS.Data) { | ||||
| 	// TODO: compression
 | ||||
| 	var data: Payload; | ||||
| 
 | ||||
| 	if (this.encoding === "etf" && buffer instanceof Buffer) data = erlpack.unpack(buffer); | ||||
| 	else if (this.encoding === "json" && typeof buffer === "string") data = JSON.parse(buffer); | ||||
| 	if (this.encoding === "etf" && buffer instanceof Buffer) | ||||
| 		data = erlpack.unpack(buffer); | ||||
| 	else if (this.encoding === "json" && typeof buffer === "string") | ||||
| 		data = JSON.parse(buffer); | ||||
| 	else return; | ||||
| 
 | ||||
| 	check.call(this, PayloadSchema, data); | ||||
| @ -41,6 +43,7 @@ export async function Message(this: WebSocket, buffer: WS.Data) { | ||||
| 		return await OPCodeHandler.call(this, data); | ||||
| 	} catch (error) { | ||||
| 		console.error(error); | ||||
| 		if (!this.CLOSED && this.CLOSING) return this.close(CLOSECODES.Unknown_error); | ||||
| 		if (!this.CLOSED && this.CLOSING) | ||||
| 			return this.close(CLOSECODES.Unknown_error); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -1 +1,4 @@ | ||||
| export * from "./Server"; | ||||
| export * from "./util/"; | ||||
| export * from "./opcodes/"; | ||||
| export * from "./listener/listener"; | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Flam3rboy
						Flam3rboy