Compare commits

...

59 Commits

Author SHA1 Message Date
dependabot[bot]
3688ca8ec7 Bump decode-uri-component in /JavaScript/SimpleRESTapi
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-02 05:13:26 +00:00
Ben
4dbc459ada hm 2019-11-14 20:04:04 +00:00
Ben
c007002ec5 Simple Movie Server 2019-07-13 20:38:41 +01:00
Ben
65f45992ec bad challenge submission 2019-07-11 21:57:56 +01:00
Benjamin Kyd
9b7d7ca43b Blindfold 2019-05-21 17:44:50 +01:00
Benjamin Kyd
5ced778700 Command Parser 2019-05-18 17:28:56 +01:00
Benjamin Kyd
f99826571a FIXED bus system working OwO 2019-05-13 00:34:58 +01:00
Benjamin Kyd
6cb5cbef32 BROKEN bus system with basic functionality - 2 commits missed somehow 2019-05-13 00:20:50 +01:00
Benjamin Kyd
b9cda04514 Bus System configuration loading working! next to actually make it 2019-05-12 00:11:48 +01:00
Benjamin Kyd
0c811d174d Removed helipad 2019-05-11 00:19:26 +01:00
Benjamin Kyd
8583319470 Removed testing helipad 2019-05-11 00:08:38 +01:00
Benjamin Kyd
8f74765114 Finished sling loaded delivery (not much error checking nthat) 2019-05-11 00:04:15 +01:00
Benjamin Kyd
ac8af0fc77 Sling loaded delivery 2019-05-09 01:11:48 +01:00
Benjamin Kyd
dbedfcfe82 SQF Admin Event functions 2019-05-06 01:05:24 +01:00
Benjamin Kyd
911879d542 Renamed SQF folder to prevent brackets in the URL 2019-04-28 22:44:38 +01:00
Benjamin Kyd
81bc15c100 SQF And some other uncommited changed added 2019-04-28 22:42:37 +01:00
Benjamin Kyd
6761ddab08 smh 2019-04-09 20:55:44 +01:00
Ben
7dce6cab72 Verlet Cloth CMAKE 2019-04-04 01:44:17 +00:00
Ben
adae1dafa9 lol 2019-03-29 23:05:55 +00:00
Ben
0e6ad651cc Properly implimented verlet intergration 2019-03-27 16:20:17 +00:00
Ben
02a7059a7c Verlet intergrated cloth simulation 2019-03-27 11:55:43 +00:00
Ben
504228cfe6 thing 2019-03-01 16:56:44 +00:00
Ben
abf4225d5d Raytracer copied from the TS homepage 2019-02-26 10:30:25 +00:00
Ben
cd476567d9 Simple snake movement mechanic demonstration for a friend 2019-02-25 12:12:52 +00:00
Benjamin Kyd
84d7bb4b7e Epic 2019-02-20 18:34:36 +00:00
Benjamin Kyd
b36dd004dc removed shadowmapping 2019-02-20 17:55:36 +00:00
Ben
7e3e7324ad kinda shadowmapping£ 2019-02-20 17:06:17 +00:00
Ben
c8df32082c Lucy, and added a little bit of ambient lighting 2019-02-20 15:07:15 +00:00
Ben
e7efde12b9 Camera class (it's not working though 2019-02-19 15:25:33 +00:00
Ben
000c7e6842 lol 2019-02-18 21:58:37 +00:00
Ben
c859007d0a Merge branch 'master' of https://github.com/plane000/examples 2019-02-18 17:40:27 +00:00
Ben
c55d112f17 Code change 2019-02-18 16:15:54 +00:00
Ben
08aad581ff Minor additions 2019-02-18 16:09:19 +00:00
Ben
a2d4b33793 Specular lighting 2019-02-18 15:52:57 +00:00
Ben
078d4e52bf Diffuse lighting and sending the normals to shaders 2019-02-18 14:27:35 +00:00
Ben
cc734cce70 fixed more 2019-02-17 21:54:21 +00:00
Ben
3eb7691619 fixed dithering 2019-02-17 21:49:44 +00:00
Benjamin Kyd
d2775830d5 Object better 2019-02-17 19:28:13 +00:00
Benjamin Kyd
a3207edee8 The dragon renders yay 2019-02-17 16:34:56 +00:00
Ben
d39f5a4884 Shaders 2019-02-17 02:06:47 +00:00
Ben
06019cc13e removed CMakeFiles 2019-02-17 01:09:19 +00:00
Ben
8ff9122b23 OBJ Loader 2019-02-17 01:08:41 +00:00
Benjamin Kyd
fd20b12351 Some logic errors 2019-02-16 18:17:26 +00:00
Benjamin Kyd
95fcf6419c Removed useless shit 2019-02-16 18:06:40 +00:00
Benjamin Kyd
6bb71312ca Logger class and boilerplate out of the way 2019-02-16 18:03:29 +00:00
Benjamin Kyd
7fe8f5d3a4 GLAD loading and SDL initialization
Next: logger
2019-02-16 16:51:21 +00:00
Benjamin Kyd
bc6fb3d0de OpenGL playground started - goal - rendering shit with lighting and all that jazz 2019-02-16 16:43:25 +00:00
Benjamin Kyd
6f1bad1eca Merge branch 'master' of https://github.com/plane000/Examples 2019-02-12 13:25:31 +00:00
Benjamin Kyd
29de71a827 Added windows support for the OpenGL example cube 2019-02-12 13:25:14 +00:00
Benjamin Kyd
ee17c5a714 Update todo.txt 2019-02-08 11:17:30 +00:00
Benjamin Kyd
975a99f7f1 Create README.md 2019-02-08 11:16:48 +00:00
Benjamin Kyd
06a928c4e8 Update README.md 2019-02-08 11:13:31 +00:00
Ben
145854d945 MAZE 2019-02-05 16:02:30 +00:00
Benjamin Kyd
3a6a7bb0c7 More ideas 2019-02-04 20:27:31 +00:00
Benjamin Kyd
434888a661 DAmn 2019-02-04 19:07:48 +00:00
Ben
826fffbbc9 Merge branch 'master' of https://github.com/plane000/examples 2019-02-04 18:10:53 +00:00
Ben
5b284d07bd grayscale 2019-02-04 17:01:34 +00:00
Ben
afa83ebd72 Floyd-Strinberg diffusion 2019-02-04 16:13:57 +00:00
Benjamin Kyd
7d8345baae Merge pull request #3 from plane000/add-license-1
Create LICENSE
2019-02-03 20:17:11 +00:00
228 changed files with 57868 additions and 28 deletions

Binary file not shown.

View File

@@ -0,0 +1,105 @@
#include <iostream>
#include <cstdlib>
#define OLC_PGE_APPLICATION
#include "olcPixelGameEngine.h"
int WINDOW_HEIGHT = 720 / 2;
int WINDOW_WIDTH = 1280 / 2;
int* firePixelsArray = new int[(WINDOW_WIDTH) * (WINDOW_HEIGHT)];
int numberOfPixels = (WINDOW_WIDTH) * (WINDOW_HEIGHT);
int fireColoursPalette[37][3] = {{7, 7, 7}, {31, 7, 7}, {47, 15, 7}, {71, 15, 7}, {87, 23, 7}, {103, 31, 7}, {119, 31, 7}, {143, 39, 7}, {159, 47, 7}, {175, 63, 7}, {191, 71, 7}, {199, 71, 7}, {223, 79, 7}, {223, 87, 7}, {223, 87, 7}, {215, 95, 7}, {215, 95, 7}, {215, 103, 15}, {207, 111, 15}, {207, 119, 15}, {207, 127, 15}, {207, 135, 23}, {199, 135, 23}, {199, 143, 23}, {199, 151, 31}, {191, 159, 31}, {191, 159, 31}, {191, 167, 39}, {191, 167, 39}, {191, 175, 47}, {183, 175, 47}, {183, 183, 47}, {183, 183, 55}, {207, 207, 111}, {223, 223, 159}, {239, 239, 199}, {255, 255, 255}};
class FireSim : public olc::PixelGameEngine {
public:
FireSim() {
sAppName = "Doom Fire Simulator";
}
bool OnUserCreate() override {
for (int i = 0; i < numberOfPixels; i++) {
firePixelsArray[i] = 36;
}
return true;
};
bool OnUserUpdate(float fElapsedTime) override {
m_timeAccumilator += fElapsedTime;
if (m_timeAccumilator >= 0.023) {
m_timeAccumilator = 0.0f;
#pragma omp parallel for schedule(dynamic)
for (int i = 0; i < numberOfPixels; i++) {
UpdateFireIntensity(i);
}
}
Render();
if (GetMouse(0).bHeld) {
Vec2<int> m = {GetMouseX(), GetMouseY()};
auto fillCircle = [&](int x, int y, int radius, int val) {
int x0 = 0;
int y0 = radius;
int d = 3 - 2 * radius;
if (!radius) return;
auto drawline = [&](int sx, int ex, int ny) {
for (int i = sx; i <= ex; i++)
firePixelsArray[ny * WINDOW_WIDTH + i] = val;
};
while (y0 >= x0) {
drawline(x - x0, x + x0, y - y0);
drawline(x - y0, x + y0, y - x0);
drawline(x - x0, x + x0, y + y0);
drawline(x - y0, x + y0, y + x0);
if (d < 0) d += 4 * x0++ + 6;
else d += 4 * (x0++ - y0--) + 10;
}
};
fillCircle(m.x, m.y, 2, 36);
}
return true;
}
void UpdateFireIntensity(int pixel) {
int pixelBelowIndex = pixel + WINDOW_WIDTH;
if (pixelBelowIndex < numberOfPixels) {
int decay = (int)floor(rand() % 3) & 3;
int pixelBelowIntensity = firePixelsArray[pixelBelowIndex];
int intensity = pixelBelowIntensity - decay >= 0 ? pixelBelowIntensity - (decay & 1) : 0;
int position = (pixel - decay >= 0) ? pixel - (decay & 1) : 0;
firePixelsArray[position] = intensity;
}
}
void Render() {
Clear(olc::BLACK);
for (int x = 0; x < WINDOW_WIDTH; x++) {
for (int y = 0; y < WINDOW_HEIGHT; y++) {
int pixel = x + (WINDOW_WIDTH * y);
int fireIntensity = firePixelsArray[pixel];
uint8_t r = fireColoursPalette[fireIntensity][0];
uint8_t g = fireColoursPalette[fireIntensity][1];
uint8_t b = fireColoursPalette[fireIntensity][2];
olc::Pixel col = {r, g, b, 255};
FillRect(x, y, 1, 1, col);
}
}
}
bool OnUserDestroy() override {
delete[] firePixelsArray;
return true;
}
private:
float m_timeAccumilator = 0.0f;
};
int main(int argc, char** argv) {
FireSim app;
if (app.Construct(WINDOW_WIDTH, WINDOW_HEIGHT, 2, 2))
app.Start();
return 0;
}

BIN
C++/Doom Fire Algorithm/output.o Normal file → Executable file

Binary file not shown.

View File

@@ -0,0 +1,27 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/output.o",
"args": ["image.jpg"],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": true,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}

View File

@@ -0,0 +1,28 @@
{
"files.associations": {
"*.tcc": "cpp",
"cctype": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"cstdarg": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"exception": "cpp",
"initializer_list": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"limits": "cpp",
"new": "cpp",
"ostream": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"type_traits": "cpp",
"typeinfo": "cpp"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -0,0 +1,93 @@
// fixed by CobaltXII cuz Ben is a furry
#include <iostream>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
struct Pixel {
unsigned char r, g, b, a;
};
int index(int x, int y, int w) {
return x+y*w;
}
int main(int argc, char** argv) {
if (argc < 2) {
std::cout << "Incorrect usage, use like ./output.o <imagepath>" << std::endl;
return 0;
}
int w, h, c;
Pixel* image = (Pixel*)stbi_load(*(argv + 1), &w, &h, &c, 4);
if (image == NULL){
std::cout << "Invalid image: " << stbi_failure_reason() << std::endl;
return 0;
}
Pixel* newImage = (Pixel*)malloc(sizeof(Pixel) * w * h);
for (int y = 1; y < h - 1; y++) {
for (int x = 1; x < w - 1; x++) {
// Convert image to black and white
int i = index(x, y, w);
int gray = round((image[i].r + 0.2126f) + (image[i].b + 0.7152) + (image[i].g + 0.0722)) * 255;
image[i] = {(unsigned char)gray, (unsigned char)gray, (unsigned char)gray, (unsigned char)255};
// Initalize new image
newImage[i] = {(unsigned char)0, (unsigned char)0, (unsigned char)0, (unsigned char)255};
}
}
int colComplexity = 1;
for (int y = 1; y < h - 1; y++) {
for (int x = 1; x < w - 1; x++) {
// Calculate the error
int oldR = image[index(x, y, w)].r;
int oldG = image[index(x, y, w)].g;
int oldB = image[index(x, y, w)].b;
// CXII: this is just rounding to black or white i assume
int newR = round(colComplexity * image[index(x, y, w)].r / 255) * (255 / colComplexity);
int newG = round(colComplexity * image[index(x, y, w)].g / 255) * (255 / colComplexity);
int newB = round(colComplexity * image[index(x, y, w)].b / 255) * (255 / colComplexity);
float errorR = oldR - newR; //image[index(x, y, w)].r - image[index(x, y, w)].r;
float errorG = oldG - newG; //image[index(x, y, w)].g - image[index(x, y, w)].g;
float errorB = oldB - newB; //image[index(x, y, w)].b - image[index(x, y, w)].b;
// Perform the diffusion
int i = index(x+1, y, w);
image[i].r = (float)image[i].r + errorR * (7.0f / 16.0f);
image[i].g = (float)image[i].g + errorG * (7.0f / 16.0f);
image[i].b = (float)image[i].b + errorB * (7.0f / 16.0f);
i = index(x-1, y+1, w);
image[i].r = (float)image[i].r + errorR * (3.0f / 16.0f);
image[i].g = (float)image[i].g + errorG * (3.0f / 16.0f);
image[i].b = (float)image[i].b + errorB * (3.0f / 16.0f);
i = index(x, y+1, w);
image[i].r = (float)image[i].r + errorR * (5.0f / 16.0f);
image[i].g = (float)image[i].g + errorG * (5.0f / 16.0f);
image[i].b = (float)image[i].b + errorB * (5.0f / 16.0f);
i = index(x+1, y+1, w);
image[i].r = (float)image[i].r + errorR * (1.0f / 16.0f);
image[i].g = (float)image[i].g + errorG * (1.0f / 16.0f);
image[i].b = (float)image[i].b + errorB * (1.0f / 16.0f);
// CXII: now this is where u went wrong buddy
newImage[index(x, y, w)].r = 255;
newImage[index(x, y, w)].g = 0;
newImage[index(x, y, w)].b = 0;
// pixel[x + 1][y ] := pixel[x + 1][y ] + quant_error * 7 / 16
// pixel[x - 1][y + 1] := pixel[x - 1][y + 1] + quant_error * 3 / 16
// pixel[x ][y + 1] := pixel[x ][y + 1] + quant_error * 5 / 16
// pixel[x + 1][y + 1] := pixel[x + 1][y + 1] + quant_error * 1 / 16
}
}
stbi_write_png("output.png", w, h, 4, (unsigned char*)newImage, 0);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

View File

@@ -0,0 +1,99 @@
// fixed by CobaltXII cuz Ben is a furry
#include <iostream>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
struct Pixel {
unsigned char r, g, b, a;
};
int index(int x, int y, int w) {
return x+y*w;
}
int floorColour(float col) {
//yeah do NOT diss me FUCK OFF
#include <cmath>
return std::max(0.0f,std::min(255.0f,col));
}
int main(int argc, char** argv) {
if (argc < 2) {
std::cout << "Incorrect usage, use like ./output.o <imagepath>" << std::endl;
return 0;
}
int w, h, c;
Pixel* image = (Pixel*)stbi_load(*(argv + 1), &w, &h, &c, 4);
if (image == NULL){
std::cout << "Invalid image: " << stbi_failure_reason() << std::endl;
return 0;
}
Pixel* newImage = (Pixel*)malloc(sizeof(Pixel) * w * h);
for (int y = 1; y < h - 1; y++) {
for (int x = 1; x < w - 1; x++) {
// Convert image to black and white
int i = index(x, y, w);
int gray = round((image[i].r + 0.2126f) + (image[i].b + 0.7152) + (image[i].g + 0.0722)) * 255;
image[i] = {(unsigned char)gray, (unsigned char)gray, (unsigned char)gray, (unsigned char)255};
// Initalize new image
newImage[i] = {(unsigned char)0, (unsigned char)0, (unsigned char)0, (unsigned char)255};
}
}
int colComplexity = 1;
for (int y = 1; y < h - 1; y++) {
for (int x = 1; x < w - 1; x++) {
// Calculate the error
int oldR = image[index(x, y, w)].r;
int oldG = image[index(x, y, w)].g;
int oldB = image[index(x, y, w)].b;
// CXII: this is just rounding to black or white i assume
int newR = image[index(x, y, w)].r < 127?0:255;
int newG = image[index(x, y, w)].g < 127?0:255;
int newB = image[index(x, y, w)].b < 127?0:255;
float errorR = oldR - newR; //image[index(x, y, w)].r - image[index(x, y, w)].r;
float errorG = oldG - newG; //image[index(x, y, w)].g - image[index(x, y, w)].g;
float errorB = oldB - newB; //image[index(x, y, w)].b - image[index(x, y, w)].b;
// Perform the diffusion
int i = index(x+1, y, w);
image[i].r = floorColour((float)image[i].r + errorR * (7.0f / 16.0f));
image[i].g = floorColour((float)image[i].g + errorG * (7.0f / 16.0f));
image[i].b = floorColour((float)image[i].b + errorB * (7.0f / 16.0f));
i = index(x-1, y+1, w);
image[i].r = floorColour((float)image[i].r + errorR * (3.0f / 16.0f));
image[i].g = floorColour((float)image[i].g + errorG * (3.0f / 16.0f));
image[i].b = floorColour((float)image[i].b + errorB * (3.0f / 16.0f));
i = index(x, y+1, w);
image[i].r = floorColour((float)image[i].r + errorR * (5.0f / 16.0f));
image[i].g = floorColour((float)image[i].g + errorG * (5.0f / 16.0f));
image[i].b = floorColour((float)image[i].b + errorB * (5.0f / 16.0f));
i = index(x+1, y+1, w);
image[i].r = floorColour((float)image[i].r + errorR * (1.0f / 16.0f));
image[i].g = floorColour((float)image[i].g + errorG * (1.0f / 16.0f));
image[i].b = floorColour((float)image[i].b + errorB * (1.0f / 16.0f));
// CXII: now this is where u went wrong buddy
newImage[index(x, y, w)].r = newR;
newImage[index(x, y, w)].g = newG;
newImage[index(x, y, w)].b = newB;
// pixel[x + 1][y ] := pixel[x + 1][y ] + quant_error * 7 / 16
// pixel[x - 1][y + 1] := pixel[x - 1][y + 1] + quant_error * 3 / 16
// pixel[x ][y + 1] := pixel[x ][y + 1] + quant_error * 5 / 16
// pixel[x + 1][y + 1] := pixel[x + 1][y + 1] + quant_error * 1 / 16
}
}
stbi_write_png("output.png", w, h, 4, (unsigned char*)newImage, 0);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
#include <iostream>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
struct Pixel {
unsigned char r, g, b, a;
};
int main(int argc, char** argv) {
if (argc < 2) {
std::err << "Invalid usage" << std::endl;
return 0;
}
Pixel* image =
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

68
C++/Maze/main.cpp Normal file
View File

@@ -0,0 +1,68 @@
#define OLC_PGE_APPLICATION
#include "olcPixelGameEngine.h"
#include <iostream>
#include <stack>
typedef enum class Direction {
NORTH,
SOUTH,
EAST,
WEST
} Direction;
struct Cell {
bool visited = false;
int width = 3;
};
class Maze : public olc::PixelGameEngine {
public:
Maze()
: m_mazeDimensions(50, 50) {
sAppName = "Maze";
}
bool OnUserCreate() {
m_maze = new Cell[m_mazeDimensions.x * m_mazeDimensions.y];
m_maze[0].visited = true;
m_stack.push({0, 0});
return true;
}
bool OnUserUpdate(float fElapsedTime) {
draw();
return true;
}
void draw() {
for (int x = 0; x < m_mazeDimensions.x; x++) {
for (int y = 0; y < m_mazeDimensions.y; y++) {
int index = x + y * m_mazeDimensions.x;
int width = m_maze[index].width;
if (m_maze[index].visited) {
DrawRect(x + m_mazeDimensions.x * width, y + m_mazeDimensions.y * width, width, width, olc::BLACK);
} else {
DrawRect(x + m_mazeDimensions.x * width, y + m_mazeDimensions.y * width, width, width, olc::RED);
}
}
}
}
private:
std::stack<Vec2<int>> m_stack;
Vec2<int> m_mazeDimensions;
Cell* m_maze;
};
int main(int argc, char** argv) {
Maze maze;
maze.Construct(500,500,2,2);
maze.Start();
// return 0;
}

File diff suppressed because it is too large Load Diff

BIN
C++/Maze/output.o Executable file

Binary file not shown.

View File

@@ -0,0 +1,31 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28922.388
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "29-Ben", "29-Ben\29-Ben.vcxproj", "{29524107-1C9D-4049-A791-254DEB6AE45F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{29524107-1C9D-4049-A791-254DEB6AE45F}.Debug|x64.ActiveCfg = Debug|x64
{29524107-1C9D-4049-A791-254DEB6AE45F}.Debug|x64.Build.0 = Debug|x64
{29524107-1C9D-4049-A791-254DEB6AE45F}.Debug|x86.ActiveCfg = Debug|Win32
{29524107-1C9D-4049-A791-254DEB6AE45F}.Debug|x86.Build.0 = Debug|Win32
{29524107-1C9D-4049-A791-254DEB6AE45F}.Release|x64.ActiveCfg = Release|x64
{29524107-1C9D-4049-A791-254DEB6AE45F}.Release|x64.Build.0 = Release|x64
{29524107-1C9D-4049-A791-254DEB6AE45F}.Release|x86.ActiveCfg = Release|Win32
{29524107-1C9D-4049-A791-254DEB6AE45F}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {09519332-8EB4-46DA-91B0-23C67A47CF88}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,105 @@
#include <iostream>
#include <cstdlib> // malloc
#include <string>
// TO CLARIFY FOR ANYONE READING THIS
// THIS CODE ISNT WRITTEN SERIOUSLY AND
// IS INTENTIONALLY AWFUL, THAT BEING
// SAID, ENJOY :)
// Returns ammount of double matches, if there's 3 in a row it will return -1
int matchesInRowOfThree(int first, int second, int third) {
// Check if all of the numbers are the same, if so then it will return
// -1, indicating that all of the numbers were the same
if (first == second && second == third)
return -1;
if (first == second || second == third)
return 1;
// If all else fails, there was no matches, so it will return 0
return 0;
}
// Returns ammount of double matches in a row of 2
int matchesInRowOfTwo(int first, int second) {
// Check if all of the numbers are the same, if so then it will return
// 1, indicating that all of the numbers were the same and there was 1 match
if (first == second)
return 1;
// If all else fails, there was no matches, so it will return 0
return 0;
}
int main(int theAmmountOfArgumentsThatWillBePassedInTheArgumentVectorThatProcedesThis, char** argumentVectorThatIsInputtedIntoTheProgramFromTheOSsCommandLineArguments) {
// Firstly announce to the user that numbers are expected to be inputted and provide them a format to input said numbers in
std::cout << "Enter 8 numbers such like '10 12 1 3 4 4 4 1': ";
// Allocate the array that the numbers will be stored in
// TODO: Find a more efficient way to allocate an array
int* inputArray = (int*)malloc(sizeof(int) * 9);
// Wait for the users formatted input and then store in the inputArray
std::cin >> inputArray[0] >> inputArray[1] >> inputArray[2] >> inputArray[3] >> inputArray[4]
>> inputArray[5] >> inputArray[6] >> inputArray[7] >> inputArray[8];
// End the line for consistant formatting
std::cout << std::endl << std::endl;
int numberOfTriplets = 0;
int numberOfDoubles = 0;
// Check the horizontal rows for matches by using the 2 functions defined above
int matchesOnTheFirstHorizontalRow = matchesInRowOfThree(inputArray[0], inputArray[1], inputArray[2]);
int matchesOnTheSecondHorizontalRow = matchesInRowOfThree(inputArray[3], inputArray[4], inputArray[5]);
int matchesOnTheThirdHorizontalRow = matchesInRowOfThree(inputArray[6], inputArray[7], inputArray[8]);
if (matchesOnTheFirstHorizontalRow == -1) numberOfTriplets = numberOfTriplets + 1;
if (matchesOnTheSecondHorizontalRow == -1) numberOfTriplets = numberOfTriplets + 1;
if (matchesOnTheThirdHorizontalRow == -1) numberOfTriplets = numberOfTriplets + 1;
if (matchesOnTheFirstHorizontalRow == 1) numberOfDoubles = numberOfDoubles + 1;
if (matchesOnTheSecondHorizontalRow == 1) numberOfDoubles = numberOfDoubles + 1;
if (matchesOnTheThirdHorizontalRow == 1) numberOfDoubles = numberOfDoubles + 1;
// Check the vertical columns (but to keep consistency, rows)
int matchesOnTheFirstVerticalRow = matchesInRowOfThree(inputArray[0], inputArray[3], inputArray[6]);
int matchesOnTheSecondVerticalRow = matchesInRowOfThree(inputArray[1], inputArray[4], inputArray[7]);
int matchesOnTheThirdVerticalRow = matchesInRowOfThree(inputArray[2], inputArray[5], inputArray[8]);
if (matchesOnTheFirstVerticalRow == -1) numberOfTriplets = numberOfTriplets + 1;
if (matchesOnTheSecondVerticalRow == -1) numberOfTriplets = numberOfTriplets + 1;
if (matchesOnTheThirdVerticalRow == -1) numberOfTriplets = numberOfTriplets + 1;
if (matchesOnTheFirstVerticalRow == 1) numberOfDoubles = numberOfDoubles + 1;
if (matchesOnTheSecondVerticalRow == 1) numberOfDoubles = numberOfDoubles + 1;
if (matchesOnTheThirdVerticalRow == 1) numberOfDoubles = numberOfDoubles + 1;
// Check the big 3 long diagonal rows
int matchesOnRightHandUpLongDiagonal = matchesInRowOfThree(inputArray[6], inputArray[4], inputArray[2]);
int matchesOnLeftHandUpLongDiagonal = matchesInRowOfThree(inputArray[8], inputArray[4], inputArray[0]);
if (matchesOnRightHandUpLongDiagonal == -1) numberOfTriplets = numberOfTriplets + 1;
if (matchesOnLeftHandUpLongDiagonal == -1) numberOfTriplets = numberOfTriplets + 1;
if (matchesOnRightHandUpLongDiagonal == 1) numberOfDoubles = numberOfDoubles + 1;
if (matchesOnLeftHandUpLongDiagonal == 1) numberOfDoubles = numberOfDoubles + 1;
// Check the 4 2 long diagonal rows
int matchesOnRightHandBottomUpDiagonal = matchesInRowOfTwo(inputArray[7], inputArray[5]);
int matchesOnLeftHandBottomUpDiagonal = matchesInRowOfTwo(inputArray[7], inputArray[3]);
int matchesOnRightHandTopDownDiagonal = matchesInRowOfTwo(inputArray[1], inputArray[5]);
int matchesOnLeftHandTopDownDiagonal = matchesInRowOfTwo(inputArray[1], inputArray[3]);
numberOfDoubles = numberOfDoubles + matchesOnRightHandBottomUpDiagonal;
numberOfDoubles = numberOfDoubles + matchesOnLeftHandBottomUpDiagonal;
numberOfDoubles = numberOfDoubles + matchesOnRightHandTopDownDiagonal;
numberOfDoubles = numberOfDoubles + matchesOnLeftHandTopDownDiagonal;
// Output the correct calculations to the user
std::cout << "Triplets=" << numberOfTriplets << " Doubles=" << numberOfDoubles << std::endl;
}

View File

@@ -0,0 +1,159 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{29524107-1C9D-4049-A791-254DEB6AE45F}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>My29Ben</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<PrecompiledHeader>
</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<PrecompiledHeader>
</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<PrecompiledHeader>
</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<PrecompiledHeader>
</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="29-Ben.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="29-Ben.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

1
C++/Soph/README.md Normal file
View File

@@ -0,0 +1 @@
These files were made by william maltby-wheiner (https://github.com/b-boy-ww) im not taking credit for this awful code

5
C++/Verlet Cloth/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.vscode/
CMakeFiles/
CMakeCache.txt
Makefile
cmake_install.cmake

56
C++/Verlet Cloth/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,56 @@
{
"files.associations": {
"atomic": "cpp",
"chrono": "cpp",
"cmath": "cpp",
"condition_variable": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"cwchar": "cpp",
"exception": "cpp",
"fstream": "cpp",
"functional": "cpp",
"initializer_list": "cpp",
"ios": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"limits": "cpp",
"list": "cpp",
"map": "cpp",
"memory": "cpp",
"mutex": "cpp",
"new": "cpp",
"numeric": "cpp",
"ostream": "cpp",
"ratio": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"string": "cpp",
"system_error": "cpp",
"xthread": "cpp",
"thread": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"typeinfo": "cpp",
"unordered_map": "cpp",
"utility": "cpp",
"vector": "cpp",
"xfacet": "cpp",
"xhash": "cpp",
"xiosbase": "cpp",
"xlocale": "cpp",
"xlocinfo": "cpp",
"xlocnum": "cpp",
"xmemory": "cpp",
"xmemory0": "cpp",
"xstddef": "cpp",
"xstring": "cpp",
"xtr1common": "cpp",
"xtree": "cpp",
"xutility": "cpp"
}
}

View File

@@ -0,0 +1,53 @@
cmake_minimum_required(VERSION 3.7)
project(VerletCloth)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} CMakeFiles/)
cmake_policy(SET CMP0037 OLD)
set(Executable output)
set(SourceDir src)
# set(IncludeDir include/) # Change include/ to your include directory
# include_directories(${IncludeDir})
file(GLOB_RECURSE SourceFiles
${SourceDir}/*.cpp
)
set(THREADS_PREFER_PTHREAD_FLAD ON)
find_package(Threads REQUIRED)
find_package(OpenGL REQUIRED)
if (UNIX)
find_package(X11 REQUIRED)
find_package(PNG REQUIRED)
include_directories(${PNG_INCLUDE_DIR})
endif (UNIX)
if (WIN32)
target_include_directories(${WinSDK})
endif (WIN32)
add_executable(${Executable}
${SourceFiles}
)
target_link_libraries(${Executable}
Threads::Threads
OpenGL::OpenGL
OpenGL::GL
OpenGL::GLX
)
if (UNIX)
target_link_libraries(${Executable}
${X11_LIBRARIES}
PNG::PNG
)
endif (UNIX)
if (WIN32)
target_link_libraries(${Executable}
${WinSDK}
)
endif (WIN32)

BIN
C++/Verlet Cloth/output Executable file

Binary file not shown.

View File

@@ -0,0 +1,148 @@
#include <iostream>
#include <sstream>
#include <vector>
#define OLC_PGE_APPLICATION
#include "olcPixelGameEngine.h"
const float GRAVITY = 5.81f;
const float DRAG = 0.99f;
struct Link;
struct Vec2f {
float x, y;
};
struct MassPoint {
Vec2f sPosition;
bool bLocked = false; // If true, the point will not be able to move
float fMass;
std::vector<Link*> sLinks;
MassPoint(Vec2f sPos, float fMass)
: sPosition(sPos),
sLastPosition(sPos),
fMass(fMass) { }
void step() {
if (bLocked) return;
// Inertia
Vec2f sVelocity;
sVelocity.x = sPosition.x - sLastPosition.x * DRAG;
sVelocity.y = sPosition.y - sLastPosition.y * DRAG;
sPosition.x += sVelocity.x;
sPosition.y += sVelocity.y + GRAVITY;
sLastPosition.x = sPosition.x;
sLastPosition.y = sPosition.y;
}
void solve() {
}
private:
Vec2f sLastPosition;
};
struct Link {
float fRestingDistance;
float fStiffness;
float fTear;
MassPoint* p0;
MassPoint* p1;
void solve() {
float diffX = p0->sPosition.x - p1->sPosition.x;
float diffY = p0->sPosition.y - p1->sPosition.y;
float d = sqrt(diffX * diffX + diffY * diffY);
float difference = (fRestingDistance - d) / d;
float im1 = 1 / p0->fMass;
float im2 = 1 / p1->fMass;
float scalarP1 = (im1 / (im1 + im2)) * fStiffness;
float scalarP2 = fStiffness - scalarP1;
p0->sPosition.x += diffX * scalarP1 * difference;
p0->sPosition.y += diffY * scalarP1 * difference;
p1->sPosition.x -= diffX * scalarP2 * difference;
p1->sPosition.y -= diffY * scalarP2 * difference;
}
};
class VerletCloth : public olc::PixelGameEngine {
public:
std::vector<MassPoint> sPoints;
int iConstraintAccuracy;
VerletCloth() {
sAppName = "Verlet Cloth Simulation";
}
bool OnUserCreate() override {
sPoints.push_back({{ 1.0f, 1.0f }, 1.0f});
iConstraintAccuracy = 5;
return true;
}
bool OnUserUpdate(float fElapsedTime) override {
m_fTimeCounter += fElapsedTime;
if (m_fTimeCounter >= 0.016f) {
Clear(olc::WHITE);
m_fTimeCounter = 0.0f;
for (int x = 0; x < iConstraintAccuracy; x++) {
for (int i = 0; i < sPoints.size(); i++) {
MassPoint pointmass = sPoints[i];
pointmass.solve();
}
}
for (auto& sPoint : sPoints){
std::stringstream str;
str << "Pos: X:" << sPoint.sPosition.x << ":Y:" << sPoint.sPosition.y;
DrawString(0, 0, str.str(), olc::BLACK);
sPoint.step();
FillRect(sPoint.sPosition.x, sPoint.sPosition.y, 4, 4, olc::BLACK);
}
}
if (GetKey(olc::ESCAPE).bPressed)
exit(0);
return true;
}
void DrawLink(Link& link) {
DrawLine(link.p0->sPosition.x, link.p0->sPosition.y,
link.p1->sPosition.x, link.p1->sPosition.y,
olc::RED);
}
private:
float m_fTimeCounter = 0.0f;
};
int main(int argc, char** argv) {
VerletCloth app;
app.Construct(1000, 600, 1, 1);
app.Start();
return 0;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
#define OLC_PGE_APPLICATION
#include "olcPixelGameEngine.h"
#include <iostream>
#include <vector>
const int MAP_WIDTH = 40;
const int MAP_HEIGHT = 40;
struct Point {
int x, y;
bool isSnake;
};
typedef enum {
DIRECTION_UP,
DIRECTION_RIGHT,
DIRECTION_DOWN,
DIRECTION_LEFT
} SnakeDir;
int index(int x, int y) {
return y * MAP_HEIGHT + x;
}
class Snake : public olc::PixelGameEngine {
public:
std::vector<Point> map;
std::vector<Point> snake_stack;
SnakeDir dir = DIRECTION_RIGHT;
bool OnUserCreate() {
// Loop over every point in the map column
// by column and initialize them as points
// in the 1d map vector
for (int i = 0; i < MAP_WIDTH; i++) {
for (int j = 0; j < MAP_HEIGHT; j++) {
map.push_back({i, j, false});
}
}
// Settup snake stack
snake_stack.push_back({2, 3, true});
snake_stack.push_back({3, 3, true});
snake_stack.push_back({4, 3, true});
snake_stack.push_back({5, 3, true});
dir = DIRECTION_RIGHT;
return true;
}
bool OnUserUpdate(float fElapsedTime) {
// Take input and change direction
if (GetKey(olc::Key::W).bPressed)
dir = DIRECTION_UP;
if (GetKey(olc::Key::A).bPressed)
dir = DIRECTION_LEFT;
if (GetKey(olc::Key::S).bPressed)
dir = DIRECTION_DOWN;
if (GetKey(olc::Key::D).bPressed)
dir = DIRECTION_RIGHT;
// Push an element for the head
// dependant on the direction
if (dir == DIRECTION_UP) {
snake_stack.push_back({snake_stack.back().x, snake_stack.back().y - 1, true});
}
if (dir == DIRECTION_RIGHT) {
snake_stack.push_back({snake_stack.back().x + 1, snake_stack.back().y, true});
}
if (dir == DIRECTION_DOWN) {
snake_stack.push_back({snake_stack.back().x, snake_stack.back().y + 1, true});
}
if (dir == DIRECTION_LEFT) {
snake_stack.push_back({snake_stack.back().x - 1, snake_stack.back().y, true});
}
// Pop last element of the tail
snake_stack.erase(snake_stack.begin());
updateSnake();
draw();
return true;
}
void updateSnake() {
// Set every map point to no snake
for (int i = 0; i < map.size(); i++) {
map[i].isSnake = false;
}
// Set the points that the snake are in in the map
// to have a snake in
for (int i = 0; i < snake_stack.size(); i++) {
map[index(snake_stack[i].x, snake_stack[i].y)].isSnake = true;
}
}
void draw() {
// Loop over every element in the map
// and draw them on the map in their respective
// screen position
for (int i = 0; i < MAP_WIDTH; i++) {
for (int j = 0; j < MAP_HEIGHT; j++) {
if (map[index(i, j)].isSnake) {
DrawRect(i, j, 1, 1, olc::RED);
} else {
DrawRect(i, j, 1, 1, olc::BLUE);
}
}
}
}
};
int main(int argc, char** argv) {
Snake app;
app.Construct(MAP_WIDTH, MAP_HEIGHT, 20, 20);
app.Start();
return 0;
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

155
C++/todo.txt Normal file
View File

@@ -0,0 +1,155 @@
Totally not stollen ToDo list
- [ ] Forward kinematics
- [ ] Inverse kinematics
- [ ] Maze generation
- [ ] Flood fill search algorithm
- [ ] A* Search
- [x] Simple pendulum
- [ ] Double pendulum
- [x] Doom fire algorithm
- [ ] Quad tree compression
- [ ] GLSL smallpt
- [ ] CPU smallpt
- [ ] Dithering library
- [ ] All RGB
- [ ] Chip-8 emulator
- [ ] Chip-8 assembler
- [ ] Chip-8 disassembler
- [ ] Normal mapping on images
- [ ] Tesseract
- [ ] Lissajous table
- [ ] Navier-Stokes fluid simulation
- [x] Floyd-Steinberg dithering
- [ ] Terminal text extension
- [ ] SDL2 audio extension
- [ ] Other error diffusion methods
- [ ] Elementary cellular automata
- [ ] Two-dimensional cellular automata
- [ ] Software rasterizer
- [ ] Fire automata
- [ ] Plasma effect
- [ ] Perlin noise terrain
- [ ] Refracting rays
- [ ] Sierpinski triangle
- [ ] Untextured ray caster
- [ ] Textured ray caster
- [ ] Verlet integration rag doll physics
- [ ] Truetype font rasterizing
- [ ] Quaternion raymarching
- [ ] Newton fractal
- [ ] Complex function visualization
- [ ] Smoothed particle hydrodynamics
- [ ] Porting minecraft4k
- [ ] Physics sandbox (Verlet)
- [ ] Barycentric triangle coordinates
- [ ] Pac-Man simulation
- [ ] Dynamic lighting (line of sight)
- [ ] Tetris simulation
- [ ] Falling text (Matrix simulation)
- [ ] Snake simulation
- [ ] Ball/ball collisions and response
- [ ] The Powder Toy
- [ ] Burning ship fractal
- [ ] NES emulator
- [ ] MNIST neural network
- [ ] Simple XOR neural network
- [ ] Genetic algorithms
- [ ] Ear-clipping triangulation
- [ ] Fireworks particle system
- [ ] Newtonian gravity particle system
- [ ] Load models (SGL)
- [ ] Projection (SGL)
- [ ] Transformation (SGL)
- [ ] Culling (SGL)
- [ ] Clipping (SGL)
- [ ] Lighting (SGL)
- [x] Mandelbrot explorer
- [ ] Black/white dithering
- [ ] 3-bit color dithering
- [ ] N-body simulation
- [ ] Barnes-Hut n-body simulation
- [ ] Cloth simulation (Verlet)
- [ ] Procedural texture generation
- [ ] Hilbert curve
- [ ] Turtle graphics engine
- [ ] OpenGL procedural terrain
- [ ] Julia set explorer
- [ ] OpenGL model viewer
- [ ] GPU acceleration framework
- [ ] Raymarching silhouettes
- [ ] Raymarching Phong illumination
- [ ] Domain repetition and primitive operators (raymarching)
- [ ] OpenCV with OpenGL video processing
- [ ] Ray marched terrain
- [ ] GPU accelerated Mandelbrot explorer
- [ ] GPU accelerated Julia explorer
- [ ] GPU accelerated Burning Ship explorer
- [ ] GPU accelerated Newton explorer
- [ ] Physically correct asteroids
- [ ] Fractal generator (high-precision)
- [ ] Affine transformations (identity)
- [ ] Affine transformations (rotation)
- [ ] Affine transformations (translation)
- [ ] Affine transformations (scalar)
- [ ] Affine transformations (shear)
- [ ] Fractal Brownian motion simulation
- [ ] Phong lighting with normal interpolation
- [ ] Audio processing with stb_vorbis
- [ ] Voronoi diagrams
- [ ] Raymarched die
- [ ] Raymarched pawn
- [ ] Cellular noise terrain
- [ ] Voronoi based terrain generation
- [ ] Naive metaballs
- [ ] Naive Voronoi metaballs
- [ ] Naive blended metaballs
- [ ] Metaballs with marching squares
- [ ] Perlin noise flow field
- [ ] Pressure and heat simulations
- [ ] Marching squares isolines
- [ ] 15 seconds of RAM on Chip-8
- [ ] K-means clustering
- [ ] Additive blending
- [ ] RGB 3D visualization
- [ ] RGB-HSL and vice versa
- [ ] Grapher
- [ ] Flocking (boids)
- [ ] Image quantization (median-cut)
- [ ] Image quantization (k-means)
- [ ] Double-precision GLSL fractals
- [ ] Bézier curve
- [ ] Refraction (optics)
- [ ] Mode 7 racing
- [ ] Spiral raster
- [ ] Plot function
- [ ] Rope simulation (Verlet)
- [ ] Hair simulation (Verlet)
- [ ] Koch snowflake
- [ ] Newtons cradle simulation (Verlet)
- [ ] Anti-aliased line rasterization (Xiaolin Wu)
- [ ] Anti-aliased thick line rasterization (Xiaolin Wu)
- [ ] Spirograph
- [ ] Circle-line intersection
- [ ] Fourier series visualization (square)
- [ ] Fourier series visualization (saw)
- [ ] Discrete Fourier transform
- [ ] Fourier transform based epicycles
- [ ] Fast Fourier transform
- [ ] 2D nearest-neighbor interpolation
- [ ] Bilinear interpolation
- [ ] Bicubic interpolation
- [ ] Edge detection
- [ ] Catmull-Rom splines
- [ ] Sepia filter
- [ ] Various black/white filters
- [ ] Guassian blur
- [ ] Box blur
- [ ] Image convolution using kernels
- [ ] Function approximation using neural networks
- [ ] Checkers/chess game in OpenGL
- [ ] Rubiks cube solver and scrambler
- [ ] Rubiks cube renderer in OpenGL
- [ ] Connect 4 in OpenGL
- [ ] Fast Fourier transform for audio visualization
- [ ] Text editor using Terminal extension

View File

@@ -0,0 +1,2 @@
node_modules/
movies/

View File

@@ -0,0 +1,34 @@
const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const app = express();
// Done with HTTP so that it's easier to use websockets in the future
const server = require('http').createServer(app);
console.log('Server Settup');
// Server homepage
app.use(express.static('./static'));
if (!fs.existsSync('./movies/')) {
fs.mkdirSync('./movies');
console.log('Please provide a movies folder and put movies in it');
process.exit(1);
}
app.use(express.static('./movies'));
app.listen(80);
console.log('App listening on port 80');
app.get('/movies', async (req, res) => {
let response = [];
let movies = fs.readdirSync('./movies');
for (movie of movies) {
response.push(movie);
}
res.send(JSON.stringify(response));
});

View File

@@ -0,0 +1,374 @@
{
"name": "simple-movie-server",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
"integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA=="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"requires": {
"mime-db": "1.40.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"proxy-addr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
"integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.0"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
}
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "simple-movie-server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Ben (plane000)",
"license": "MIT",
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1"
}
}

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Movies</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway">
<style>
body {
font-family: 'Raleway', sans-serif;
}
</style>
</head>
<body>
<div id="Loading">Loading Movies...</div>
<div id="movieContainer"></div>
<script>
let movieContainer = document.getElementById('movieContainer');
let loading = document.getElementById('Loading');
async function loadMovies() {
let res = await fetch('/movies', {
method: 'GET'
});
res = await res.json();
for (i of res) {
movieContainer.innerHTML += `<div id="movieItem"><button onclick="window.location.href='${i}'">${i}</button></div><br>\n`;
}
loading.style.display = "none";
}
loadMovies();
</script>
</body>
</html>

View File

@@ -487,7 +487,7 @@
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
"dev": true
},
"deep-extend": {
@@ -904,7 +904,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@@ -925,12 +926,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
@@ -945,17 +948,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -1072,7 +1078,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@@ -1084,6 +1091,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "1.0.1"
}
@@ -1098,6 +1106,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "1.1.11"
}
@@ -1105,12 +1114,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "5.1.1",
"yallist": "3.0.2"
@@ -1129,6 +1140,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -1209,7 +1221,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -1221,6 +1234,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1.0.2"
}
@@ -1306,7 +1320,8 @@
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -1342,6 +1357,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "1.1.0",
"is-fullwidth-code-point": "1.0.0",
@@ -1361,6 +1377,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "2.1.1"
}
@@ -1404,12 +1421,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},

BIN
Lua/rpbot/a.exe Normal file

Binary file not shown.

BIN
Lua/rpbot/a.out Normal file

Binary file not shown.

114
Lua/rpbot/deps/base64.lua Normal file
View File

@@ -0,0 +1,114 @@
--[[lit-meta
name = "creationix/base64"
description = "A pure lua implemention of base64 using bitop"
tags = {"crypto", "base64", "bitop"}
version = "2.0.0"
license = "MIT"
author = { name = "Tim Caswell" }
]]
local bit = require 'bit'
local rshift = bit.rshift
local lshift = bit.lshift
local bor = bit.bor
local band = bit.band
local char = string.char
local byte = string.byte
local concat = table.concat
local codes = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
-- Loop over input 3 bytes at a time
-- a,b,c are 3 x 8-bit numbers
-- they are encoded into groups of 4 x 6-bit numbers
-- aaaaaa aabbbb bbbbcc cccccc
-- if there is no c, then pad the 4th with =
-- if there is also no b then pad the 3rd with =
local function base64Encode(str)
local parts = {}
local j = 1
for i = 1, #str, 3 do
local a, b, c = byte(str, i, i + 2)
parts[j] = char(
-- Higher 6 bits of a
byte(codes, rshift(a, 2) + 1),
-- Lower 2 bits of a + high 4 bits of b
byte(codes, bor(
lshift(band(a, 3), 4),
b and rshift(b, 4) or 0
) + 1),
-- Low 4 bits of b + High 2 bits of c
b and byte(codes, bor(
lshift(band(b, 15), 2),
c and rshift(c, 6) or 0
) + 1) or 61, -- 61 is '='
-- Lower 6 bits of c
c and byte(codes, band(c, 63) + 1) or 61 -- 61 is '='
)
j = j + 1
end
return concat(parts)
end
-- Reverse map from character code to 6-bit integer
local map = {}
for i = 1, #codes do
map[byte(codes, i)] = i - 1
end
-- loop over input 4 characters at a time
-- The characters are mapped to 4 x 6-bit integers a,b,c,d
-- They need to be reassalbled into 3 x 8-bit bytes
-- aaaaaabb bbbbcccc ccdddddd
-- if d is padding then there is no 3rd byte
-- if c is padding then there is no 2nd byte
local function base64Decode(data)
local bytes = {}
local j = 1
for i = 1, #data, 4 do
local a = map[byte(data, i)]
local b = map[byte(data, i + 1)]
local c = map[byte(data, i + 2)]
local d = map[byte(data, i + 3)]
-- higher 6 bits are the first char
-- lower 2 bits are upper 2 bits of second char
bytes[j] = char(bor(lshift(a, 2), rshift(b, 4)))
-- if the third char is not padding, we have a second byte
if c < 64 then
-- high 4 bits come from lower 4 bits in b
-- low 4 bits come from high 4 bits in c
bytes[j + 1] = char(bor(lshift(band(b, 0xf), 4), rshift(c, 2)))
-- if the fourth char is not padding, we have a third byte
if d < 64 then
-- Upper 2 bits come from Lower 2 bits of c
-- Lower 6 bits come from d
bytes[j + 2] = char(bor(lshift(band(c, 3), 6), d))
end
end
j = j + 3
end
return concat(bytes)
end
assert(base64Encode("") == "")
assert(base64Encode("f") == "Zg==")
assert(base64Encode("fo") == "Zm8=")
assert(base64Encode("foo") == "Zm9v")
assert(base64Encode("foob") == "Zm9vYg==")
assert(base64Encode("fooba") == "Zm9vYmE=")
assert(base64Encode("foobar") == "Zm9vYmFy")
assert(base64Decode("") == "")
assert(base64Decode("Zg==") == "f")
assert(base64Decode("Zm8=") == "fo")
assert(base64Decode("Zm9v") == "foo")
assert(base64Decode("Zm9vYg==") == "foob")
assert(base64Decode("Zm9vYmE=") == "fooba")
assert(base64Decode("Zm9vYmFy") == "foobar")
return {
encode = base64Encode,
decode = base64Decode,
}

View File

@@ -0,0 +1,183 @@
--[[lit-meta
name = "creationix/coro-channel"
version = "3.0.1"
homepage = "https://github.com/luvit/lit/blob/master/deps/coro-channel.lua"
description = "An adapter for wrapping uv streams as coro-streams."
tags = {"coro", "adapter"}
license = "MIT"
author = { name = "Tim Caswell" }
]]
-- local p = require('pretty-print').prettyPrint
local function makeCloser(socket)
local closer = {
read = false,
written = false,
errored = false,
}
local closed = false
local function close()
if closed then return end
closed = true
if not closer.readClosed then
closer.readClosed = true
if closer.onClose then
closer.onClose()
end
end
if not socket:is_closing() then
socket:close()
end
end
closer.close = close
function closer.check()
if closer.errored or (closer.read and closer.written) then
return close()
end
end
return closer
end
local function makeRead(socket, closer)
local paused = true
local queue = {}
local tindex = 0
local dindex = 0
local function dispatch(data)
-- p("<-", data[1])
if tindex > dindex then
local thread = queue[dindex]
queue[dindex] = nil
dindex = dindex + 1
assert(coroutine.resume(thread, unpack(data)))
else
queue[dindex] = data
dindex = dindex + 1
if not paused then
paused = true
assert(socket:read_stop())
end
end
end
closer.onClose = function ()
if not closer.read then
closer.read = true
return dispatch {nil, closer.errored}
end
end
local function onRead(err, chunk)
if err then
closer.errored = err
return closer.check()
end
if not chunk then
if closer.read then return end
closer.read = true
dispatch {}
return closer.check()
end
return dispatch {chunk}
end
local function read()
if dindex > tindex then
local data = queue[tindex]
queue[tindex] = nil
tindex = tindex + 1
return unpack(data)
end
if paused then
paused = false
assert(socket:read_start(onRead))
end
queue[tindex] = coroutine.running()
tindex = tindex + 1
return coroutine.yield()
end
-- Auto use wrapper library for backwards compat
return read
end
local function makeWrite(socket, closer)
local function wait()
local thread = coroutine.running()
return function (err)
assert(coroutine.resume(thread, err))
end
end
local function write(chunk)
if closer.written then
return nil, "already shutdown"
end
-- p("->", chunk)
if chunk == nil then
closer.written = true
closer.check()
local success, err = socket:shutdown(wait())
if not success then
return nil, err
end
err = coroutine.yield()
return not err, err
end
local success, err = socket:write(chunk, wait())
if not success then
closer.errored = err
closer.check()
return nil, err
end
err = coroutine.yield()
return not err, err
end
return write
end
local function wrapRead(socket)
local closer = makeCloser(socket)
closer.written = true
return makeRead(socket, closer), closer.close
end
local function wrapWrite(socket)
local closer = makeCloser(socket)
closer.read = true
return makeWrite(socket, closer), closer.close
end
local function wrapStream(socket)
assert(socket
and socket.write
and socket.shutdown
and socket.read_start
and socket.read_stop
and socket.is_closing
and socket.close, "socket does not appear to be a socket/uv_stream_t")
local closer = makeCloser(socket)
return makeRead(socket, closer), makeWrite(socket, closer), closer.close
end
return {
wrapRead = wrapRead,
wrapWrite = wrapWrite,
wrapStream = wrapStream,
}

View File

@@ -0,0 +1,195 @@
--[[lit-meta
name = "creationix/coro-http"
version = "3.1.0"
dependencies = {
"creationix/coro-net@3.0.0",
"luvit/http-codec@3.0.0"
}
homepage = "https://github.com/luvit/lit/blob/master/deps/coro-http.lua"
description = "An coro style http(s) client and server helper."
tags = {"coro", "http"}
license = "MIT"
author = { name = "Tim Caswell" }
]]
local httpCodec = require('http-codec')
local net = require('coro-net')
local function createServer(host, port, onConnect)
net.createServer({
host = host,
port = port,
encode = httpCodec.encoder(),
decode = httpCodec.decoder(),
}, function (read, write, socket)
for head in read do
local parts = {}
for part in read do
if #part > 0 then
parts[#parts + 1] = part
else
break
end
end
local body = table.concat(parts)
head, body = onConnect(head, body, socket)
write(head)
if body then write(body) end
write("")
if not head.keepAlive then break end
end
end)
end
local function parseUrl(url)
local protocol, host, hostname, port, path = url:match("^(https?:)//(([^/:]+):?([0-9]*))(/?.*)$")
if not protocol then error("Not a valid http url: " .. url) end
local tls = protocol == "https:"
port = port and tonumber(port) or (tls and 443 or 80)
if path == "" then path = "/" end
return {
tls = tls,
host = host,
hostname = hostname,
port = port,
path = path
}
end
local connections = {}
local function getConnection(host, port, tls, timeout)
for i = #connections, 1, -1 do
local connection = connections[i]
if connection.host == host and connection.port == port and connection.tls == tls then
table.remove(connections, i)
-- Make sure the connection is still alive before reusing it.
if not connection.socket:is_closing() then
connection.reused = true
connection.socket:ref()
return connection
end
end
end
local read, write, socket, updateDecoder, updateEncoder = assert(net.connect {
host = host,
port = port,
tls = tls,
timeout = timeout,
encode = httpCodec.encoder(),
decode = httpCodec.decoder()
})
return {
socket = socket,
host = host,
port = port,
tls = tls,
read = read,
write = write,
updateEncoder = updateEncoder,
updateDecoder = updateDecoder,
reset = function ()
-- This is called after parsing the response head from a HEAD request.
-- If you forget, the codec might hang waiting for a body that doesn't exist.
updateDecoder(httpCodec.decoder())
end
}
end
local function saveConnection(connection)
if connection.socket:is_closing() then return end
connections[#connections + 1] = connection
connection.socket:unref()
end
local function request(method, url, headers, body, timeout)
local uri = parseUrl(url)
local connection = getConnection(uri.hostname, uri.port, uri.tls, timeout)
local read = connection.read
local write = connection.write
local req = {
method = method,
path = uri.path,
{"Host", uri.host}
}
local contentLength
local chunked
if headers then
for i = 1, #headers do
local key, value = unpack(headers[i])
key = key:lower()
if key == "content-length" then
contentLength = value
elseif key == "content-encoding" and value:lower() == "chunked" then
chunked = true
end
req[#req + 1] = headers[i]
end
end
if type(body) == "string" then
if not chunked and not contentLength then
req[#req + 1] = {"Content-Length", #body}
end
end
write(req)
if body then write(body) end
local res = read()
if not res then
if not connection.socket:is_closing() then
connection.socket:close()
end
-- If we get an immediate close on a reused socket, try again with a new socket.
-- TODO: think about if this could resend requests with side effects and cause
-- them to double execute in the remote server.
if connection.reused then
return request(method, url, headers, body)
end
error("Connection closed")
end
body = {}
if req.method == "HEAD" then
connection.reset()
else
while true do
local item = read()
if not item then
res.keepAlive = false
break
end
if #item == 0 then
break
end
body[#body + 1] = item
end
end
if res.keepAlive then
saveConnection(connection)
else
write()
end
-- Follow redirects
if method == "GET" and (res.code == 302 or res.code == 307) then
for i = 1, #res do
local key, location = unpack(res[i])
if key:lower() == "location" then
return request(method, location, headers)
end
end
end
return res, table.concat(body)
end
return {
createServer = createServer,
parseUrl = parseUrl,
getConnection = getConnection,
saveConnection = saveConnection,
request = request,
}

181
Lua/rpbot/deps/coro-net.lua Normal file
View File

@@ -0,0 +1,181 @@
--[[lit-meta
name = "creationix/coro-net"
version = "3.2.0"
dependencies = {
"creationix/coro-channel@3.0.0",
"creationix/coro-wrapper@3.0.0",
}
optionalDependencies = {
"luvit/secure-socket@1.0.0"
}
homepage = "https://github.com/luvit/lit/blob/master/deps/coro-net.lua"
description = "An coro style client and server helper for tcp and pipes."
tags = {"coro", "tcp", "pipe", "net"}
license = "MIT"
author = { name = "Tim Caswell" }
]]
local uv = require('uv')
local wrapStream = require('coro-channel').wrapStream
local wrapper = require('coro-wrapper')
local merger = wrapper.merger
local decoder = wrapper.decoder
local encoder = wrapper.encoder
local secureSocket -- Lazy required from "secure-socket" on first use.
local function makeCallback(timeout)
local thread = coroutine.running()
local timer, done
if timeout then
timer = uv.new_timer()
timer:start(timeout, 0, function ()
if done then return end
done = true
timer:close()
return assert(coroutine.resume(thread, nil, "timeout"))
end)
end
return function (err, data)
if done then return end
done = true
if timer then timer:close() end
if err then
return assert(coroutine.resume(thread, nil, err))
end
return assert(coroutine.resume(thread, data or true))
end
end
local function normalize(options, server)
local t = type(options)
if t == "string" then
options = {path=options}
elseif t == "number" then
options = {port=options}
elseif t ~= "table" then
assert("Net options must be table, string, or number")
end
if options.port or options.host then
options.isTcp = true
options.host = options.host or "127.0.0.1"
assert(options.port, "options.port is required for tcp connections")
elseif options.path then
options.isTcp = false
else
error("Must set either options.path or options.port")
end
if options.tls == true then
options.tls = {}
end
if options.tls then
if server then
options.tls.server = true
assert(options.tls.cert, "TLS servers require a certificate")
assert(options.tls.key, "TLS servers require a key")
else
options.tls.server = false
options.tls.servername = options.host
end
end
return options
end
local function connect(options)
local socket, success, err
options = normalize(options)
if options.isTcp then
success, err = uv.getaddrinfo(options.host, options.port, {
socktype = options.socktype or "stream",
family = options.family or "inet",
}, makeCallback(options.timeout))
if not success then return nil, err end
local res
res, err = coroutine.yield()
if not res then return nil, err end
socket = uv.new_tcp()
socket:connect(res[1].addr, res[1].port, makeCallback(options.timeout))
else
socket = uv.new_pipe(false)
socket:connect(options.path, makeCallback(options.timeout))
end
success, err = coroutine.yield()
if not success then return nil, err end
local dsocket
if options.tls then
if not secureSocket then secureSocket = require('secure-socket') end
dsocket, err = secureSocket(socket, options.tls)
if not dsocket then
return nil, err
end
else
dsocket = socket
end
local read, write, close = wrapStream(dsocket)
local updateDecoder, updateEncoder
if options.scan then
-- TODO: Should we expose updateScan somehow?
read = merger(read, options.scan)
end
if options.decode then
read, updateDecoder = decoder(read, options.decode)
end
if options.encode then
write, updateEncoder = encoder(write, options.encode)
end
return read, write, dsocket, updateDecoder, updateEncoder, close
end
local function createServer(options, onConnect)
local server
options = normalize(options, true)
if options.isTcp then
server = uv.new_tcp()
assert(server:bind(options.host, options.port))
else
server = uv.new_pipe(false)
assert(server:bind(options.path))
end
assert(server:listen(256, function (err)
assert(not err, err)
local socket = options.isTcp and uv.new_tcp() or uv.new_pipe(false)
server:accept(socket)
coroutine.wrap(function ()
local success, failure = xpcall(function ()
local dsocket
if options.tls then
if not secureSocket then secureSocket = require('secure-socket') end
dsocket = assert(secureSocket(socket, options.tls))
dsocket.socket = socket
else
dsocket = socket
end
local read, write = wrapStream(dsocket)
local updateDecoder, updateEncoder
if options.scan then
-- TODO: should we expose updateScan somehow?
read = merger(read, options.scan)
end
if options.decode then
read, updateDecoder = decoder(read, options.decode)
end
if options.encode then
write, updateEncoder = encoder(write, options.encode)
end
return onConnect(read, write, dsocket, updateDecoder, updateEncoder)
end, debug.traceback)
if not success then
print(failure)
end
end)()
end))
return server
end
return {
makeCallback = makeCallback,
connect = connect,
createServer = createServer,
}

View File

@@ -0,0 +1,196 @@
--[[lit-meta
name = "creationix/coro-websocket"
version = "3.1.0"
dependencies = {
"luvit/http-codec@3.0.0",
"creationix/websocket-codec@3.0.0",
"creationix/coro-net@3.0.0",
}
homepage = "https://github.com/luvit/lit/blob/master/deps/coro-websocket.lua"
description = "Websocket helpers assuming coro style I/O."
tags = {"coro", "websocket"}
license = "MIT"
author = { name = "Tim Caswell" }
]]
local uv = require('uv')
local httpCodec = require('http-codec')
local websocketCodec = require('websocket-codec')
local net = require('coro-net')
local function parseUrl(url)
local protocol, host, port, pathname = string.match(url, "^(wss?)://([^:/]+):?(%d*)(/?[^#?]*)")
local tls
if protocol == "ws" then
port = tonumber(port) or 80
tls = false
elseif protocol == "wss" then
port = tonumber(port) or 443
tls = true
else
return nil, "Sorry, only ws:// or wss:// protocols supported"
end
return {
host = host,
port = port,
tls = tls,
pathname = pathname
}
end
local function wrapIo(rawRead, rawWrite, options)
local closeSent = false
local timer
local function cleanup()
if timer then
if not timer:is_closing() then
timer:close()
end
timer = nil
end
end
local function write(message)
if message then
message.mask = options.mask
if message.opcode == 8 then
closeSent = true
rawWrite(message)
cleanup()
return rawWrite()
end
else
if not closeSent then
return write({
opcode = 8,
payload = ""
})
end
end
return rawWrite(message)
end
local function read()
while true do
local message = rawRead()
if not message then
return cleanup()
end
if message.opcode < 8 then
return message
end
if not closeSent then
if message.opcode == 8 then
write {
opcode = 8,
payload = message.payload
}
elseif message.opcode == 9 then
write {
opcode = 10,
payload = message.payload
}
end
return message
end
end
end
if options.heartbeat then
local interval = options.heartbeat
timer = uv.new_timer()
timer:unref()
timer:start(interval, interval, function ()
coroutine.wrap(function ()
local success, err = write {
opcode = 10,
payload = ""
}
if not success then
timer:close()
print(err)
end
end)()
end)
end
return read, write
end
-- options table to configure connection
-- options.path
-- options.host
-- options.port
-- options.tls
-- options.pathname
-- options.subprotocol
-- options.headers (as list of header/value pairs)
-- options.timeout
-- options.heartbeat
-- returns res, read, write (res.socket has socket)
local function connect(options)
options = options or {}
local config = {
path = options.path,
host = options.host,
port = options.port,
tls = options.tls,
encode = httpCodec.encoder(),
decode = httpCodec.decoder(),
}
local read, write, socket, updateDecoder, updateEncoder
= net.connect(config, options.timeout or 10000)
if not read then
return nil, write
end
local res
local success, err = websocketCodec.handshake({
host = options.host,
path = options.pathname,
protocol = options.subprotocol
}, function (req)
local headers = options.headers
if headers then
for i = 1, #headers do
req[#req + 1] = headers[i]
end
end
write(req)
res = read()
if not res then error("Missing server response") end
if res.code == 400 then
-- p { req = req, res = res }
local reason = read() or res.reason
error("Invalid request: " .. reason)
end
return res
end)
if not success then
return nil, err
end
-- Upgrade the protocol to websocket
updateDecoder(websocketCodec.decode)
updateEncoder(websocketCodec.encode)
read, write = wrapIo(read, write, {
mask = true,
heartbeat = options.heartbeat
})
res.socket = socket
return res, read, write
end
return {
parseUrl = parseUrl,
wrapIo = wrapIo,
connect = connect,
}

View File

@@ -0,0 +1,151 @@
--[[lit-meta
name = "creationix/coro-wrapper"
version = "3.1.0"
homepage = "https://github.com/luvit/lit/blob/master/deps/coro-wrapper.lua"
description = "An adapter for applying decoders to coro-streams."
tags = {"coro", "decoder", "adapter"}
license = "MIT"
author = { name = "Tim Caswell" }
]]
local concat = table.concat
local sub = string.sub
-- Merger allows for effecient merging of many chunks.
-- The scan function returns truthy when the chunk contains a useful delimeter
-- Or in other words, when there is enough data to flush to the decoder.
-- merger(read, scan) -> read, updateScan
-- read() -> chunk or nil
-- scan(chunk) -> should_flush
-- updateScan(scan)
local function merger(read, scan)
local parts = {}
-- Return a new read function that combines chunks smartly
return function ()
while true do
-- Read the next event from upstream.
local chunk = read()
-- We got an EOS (end of stream)
if not chunk then
-- If there is nothing left to flush, emit EOS here.
if #parts == 0 then return end
-- Flush the buffer
chunk = concat(parts)
parts = {}
return chunk
end
-- Accumulate the chunk
parts[#parts + 1] = chunk
-- Flush the buffer if scan tells us to.
if scan(chunk) then
chunk = concat(parts)
parts = {}
return chunk
end
end
end,
-- This is used to update or disable the scan function. It's useful for
-- protocols that change mid-stream (like HTTP upgrades in websockets)
function (newScan)
scan = newScan
end
end
-- Decoder takes in a read function and a decode function and returns a new
-- read function that emits decoded events. When decode returns `nil` it means
-- that it needs more data before it can parse. The index output in decode is
-- the index to start the next decode. If output index if nil it means nothing
-- is leftover and next decode starts fresh.
-- decoder(read, decode) -> read, updateDecode
-- read() -> chunk or nil
-- decode(chunk, index) -> nil or (data, index)
-- updateDecode(Decode)
local function decoder(read, decode)
local buffer, index
local want = true
return function ()
while true do
-- If there isn't enough data to decode then get more data.
if want then
local chunk = read()
if buffer then
-- If we had leftover data in the old buffer, trim it down.
if index > 1 then
buffer = sub(buffer, index)
index = 1
end
if chunk then
-- Concatenate the chunk with the old data
buffer = buffer .. chunk
end
else
-- If there was no leftover data, set new data in the buffer
if chunk then
buffer = chunk
index = 1
else
buffer = nil
index = nil
end
end
end
-- Return nil if the buffer is empty
if buffer == '' or buffer == nil then
return nil
end
-- If we have data, lets try to decode it
local item, newIndex = decode(buffer, index)
want = not newIndex
if item or newIndex then
-- There was enough data to emit an event!
if newIndex then
assert(type(newIndex) == "number", "index must be a number if set")
-- There was leftover data
index = newIndex
else
want = true
-- There was no leftover data
buffer = nil
index = nil
end
-- Emit the event
return item
end
end
end,
function (newDecode)
decode = newDecode
end
end
local function encoder(write, encode)
return function (item)
if not item then
return write()
end
return write(encode(item))
end,
function (newEncode)
encode = newEncode
end
end
return {
merger = merger,
decoder = decoder,
encoder = encoder,
}

View File

@@ -0,0 +1,236 @@
local fs = require('fs')
local pathjoin = require('pathjoin')
local insert, sort, concat = table.insert, table.sort, table.concat
local format = string.format
local pathJoin = pathjoin.pathJoin
local function scan(dir)
for fileName, fileType in fs.scandirSync(dir) do
local path = pathJoin(dir, fileName)
if fileType == 'file' then
coroutine.yield(path)
else
scan(path)
end
end
end
local function checkType(docstring, token)
return docstring:find(token) == 1
end
local function match(s, pattern) -- only useful for one return value
return assert(s:match(pattern), s)
end
local docs = {}
for f in coroutine.wrap(function() scan('./libs') end) do
local d = assert(fs.readFileSync(f))
local class = {
methods = {},
statics = {},
properties = {},
parents = {},
}
for s in d:gmatch('--%[=%[%s*(.-)%s*%]=%]') do
if checkType(s, '@i?c') then
class.name = match(s, '@i?c (%w+)')
class.userInitialized = checkType(s, '@ic')
for parent in s:gmatch('x (%w+)') do
insert(class.parents, parent)
end
class.desc = match(s, '@d (.+)'):gsub('\r?\n', ' ')
class.parameters = {}
for optional, paramName, paramType in s:gmatch('@(o?)p ([%w%p]+)%s+([%w%p]+)') do
insert(class.parameters, {paramName, paramType, optional == 'o'})
end
elseif checkType(s, '@s?m') then
local method = {parameters = {}}
method.name = match(s, '@s?m ([%w%p]+)')
for optional, paramName, paramType in s:gmatch('@(o?)p ([%w%p]+)%s+([%w%p]+)') do
insert(method.parameters, {paramName, paramType, optional == 'o'})
end
method.returnType = match(s, '@r ([%w%p]+)')
method.desc = match(s, '@d (.+)'):gsub('\r?\n', ' ')
insert(checkType(s, '@sm') and class.statics or class.methods, method)
elseif checkType(s, '@p') then
local propertyName, propertyType, propertyDesc = s:match('@p (%w+)%s+([%w%p]+)%s+(.+)')
assert(propertyName, s); assert(propertyType, s); assert(propertyDesc, s)
propertyDesc = propertyDesc:gsub('\r?\n', ' ')
insert(class.properties, {
name = propertyName,
type = propertyType,
desc = propertyDesc,
})
end
end
if class.name then
docs[class.name] = class
end
end
local function link(str)
local ret = {}
for t in str:gmatch('[^/]+') do
insert(ret, docs[t] and format('[[%s]]', t) or t)
end
return concat(ret, '/')
end
local function sorter(a, b)
return a.name < b.name
end
local function writeProperties(f, properties)
sort(properties, sorter)
f:write('| Name | Type | Description |\n')
f:write('|-|-|-|\n')
for _, v in ipairs(properties) do
f:write('| ', v.name, ' | ', link(v.type), ' | ', v.desc, ' |\n')
end
end
local function writeParameters(f, parameters)
f:write('(')
local optional
if parameters[1] then
for i, param in ipairs(parameters) do
f:write(param[1])
if i < #parameters then
f:write(', ')
end
if param[3] then
optional = true
end
end
f:write(')\n')
if optional then
f:write('>| Parameter | Type | Optional |\n')
f:write('>|-|-|:-:|\n')
for _, param in ipairs(parameters) do
local o = param[3] and '' or ''
f:write('>| ', param[1], ' | ', param[2], ' | ', o, ' |\n')
end
else
f:write('>| Parameter | Type |\n')
f:write('>|-|-|\n')
for _, param in ipairs(parameters) do
f:write('>| ', param[1], ' | ', link(param[2]), '|\n')
end
end
else
f:write(')\n')
end
end
local function writeMethods(f, methods)
sort(methods, sorter)
for _, method in ipairs(methods) do
f:write('### ', method.name)
writeParameters(f, method.parameters)
f:write('>\n>', method.desc, '\n>\n')
f:write('>Returns: ', link(method.returnType), '\n\n')
end
end
if not fs.existsSync('docs') then
fs.mkdirSync('docs')
end
local function clean(input, seen)
local fields = {}
for _, field in ipairs(input) do
if not seen[field.name] then
insert(fields, field)
end
end
return fields
end
for _, class in pairs(docs) do
local seen = {}
for _, v in pairs(class.properties) do seen[v.name] = true end
for _, v in pairs(class.statics) do seen[v.name] = true end
for _, v in pairs(class.methods) do seen[v.name] = true end
local f = io.open(pathJoin('docs', class.name .. '.md'), 'w')
if next(class.parents) then
f:write('#### *extends ', '[[', concat(class.parents, ']], [['), ']]*\n\n')
end
f:write(class.desc, '\n\n')
if class.userInitialized then
f:write('## Constructor\n\n')
f:write('### ', class.name)
writeParameters(f, class.parameters)
f:write('\n')
else
f:write('*Instances of this class should not be constructed by users.*\n\n')
end
for _, parent in ipairs(class.parents) do
if docs[parent] and next(docs[parent].properties) then
local properties = docs[parent].properties
if next(properties) then
f:write('## Properties Inherited From ', link(parent), '\n\n')
writeProperties(f, clean(properties, seen))
end
end
end
if next(class.properties) then
f:write('## Properties\n\n')
writeProperties(f, class.properties)
end
for _, parent in ipairs(class.parents) do
if docs[parent] and next(docs[parent].statics) then
local statics = docs[parent].statics
if next(statics) then
f:write('## Static Methods Inherited From ', link(parent), '\n\n')
writeMethods(f, clean(statics, seen))
end
end
end
for _, parent in ipairs(class.parents) do
if docs[parent] and next(docs[parent].methods) then
local methods = docs[parent].methods
if next(methods) then
f:write('## Methods Inherited From ', link(parent), '\n\n')
writeMethods(f, clean(methods, seen))
end
end
end
if next(class.statics) then
f:write('## Static Methods\n\n')
writeMethods(f, class.statics)
end
if next(class.methods) then
f:write('## Methods\n\n')
writeMethods(f, class.methods)
end
f:close()
end

View File

@@ -0,0 +1,28 @@
local discordia = require("discordia")
local client = discordia.Client()
local lines = {} -- blank table of messages
client:on("ready", function() -- bot is ready
print("Logged in as " .. client.user.username)
end)
client:on("messageCreate", function(message)
local content = message.content
local author = message.author
if author == client.user then return end -- the bot should not append its own messages
if content == "!lines" then -- if the lines command is activated
message.channel:send {
file = {"lines.txt", table.concat(lines, "\n")} -- concatenate and send the collected lines in a file
}
lines = {} -- empty the lines table
else -- if the lines command is NOT activated
table.insert(lines, content) -- append the message as a new line
end
end)
client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token

View File

@@ -0,0 +1,25 @@
local discordia = require("discordia")
local client = discordia.Client()
discordia.extensions() -- load all helpful extensions
client:on("ready", function() -- bot is ready
print("Logged in as " .. client.user.username)
end)
client:on("messageCreate", function(message)
local content = message.content
local args = content:split(" ") -- split all arguments into a table
if args[1] == "!ping" then
message:reply("Pong!")
elseif args[1] == "!echo" then
table.remove(args, 1) -- remove the first argument (!echo) from the table
message:reply(table.concat(args, " ")) -- concatenate the arguments into a string, then reply with it
end
end)
client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token

View File

@@ -0,0 +1,45 @@
local discordia = require("discordia")
local client = discordia.Client()
client:on("ready", function() -- bot is ready
print("Logged in as " .. client.user.username)
end)
client:on("messageCreate", function(message)
local content = message.content
local author = message.author
if content == "!embed" then
message:reply {
embed = {
title = "Embed Title",
description = "Here is my fancy description!",
author = {
name = author.username,
icon_url = author.avatarURL
},
fields = { -- array of fields
{
name = "Field 1",
value = "This is some information",
inline = true
},
{
name = "Field 2",
value = "This is some more information",
inline = false
}
},
footer = {
text = "Created with Discordia"
},
color = 0x000000 -- hex color code
}
}
end
end)
client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token

View File

@@ -0,0 +1,44 @@
local discordia = require('discordia')
local client = discordia.Client()
discordia.extensions() -- load all helpful extensions
local prefix = "."
local commands = {
[prefix .. "ping"] = {
description = "Answers with pong.",
exec = function(message)
message.channel:send("Pong!")
end
},
[prefix .. "hello"] = {
description = "Answers with world.",
exec = function(message)
message.channel:send("world!")
end
}
}
client:on('ready', function()
p(string.format('Logged in as %s', client.user.username))
end)
client:on("messageCreate", function(message)
local args = message.content:split(" ") -- split all arguments into a table
local command = commands[prefix..args[1]]
if command then -- ping or hello
command.exec(message) -- execute the command
end
if args[1] == prefix.."help" then -- display all the commands
local output = {}
for word, tbl in pairs(commands) do
table.insert(output, "Command: " .. word .. "\nDescription: " .. tbl.description)
end
message:reply(table.concat(output, "\n\n"))
end
end)
client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token

View File

@@ -0,0 +1,20 @@
local discordia = require("discordia")
local client = discordia.Client()
client:on("ready", function() -- bot is ready
print("Logged in as " .. client.user.username)
end)
client:on("messageCreate", function(message)
local content = message.content
if content == "!ping" then
message:reply("Pong!")
elseif content == "!pong" then
message:reply("Ping!")
end
end)
client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token

View File

@@ -0,0 +1,18 @@
return {
class = require('class'),
enums = require('enums'),
extensions = require('extensions'),
package = require('./package.lua'),
Client = require('client/Client'),
Clock = require('utils/Clock'),
Color = require('utils/Color'),
Date = require('utils/Date'),
Deque = require('utils/Deque'),
Emitter = require('utils/Emitter'),
Logger = require('utils/Logger'),
Mutex = require('utils/Mutex'),
Permissions = require('utils/Permissions'),
Stopwatch = require('utils/Stopwatch'),
Time = require('utils/Time'),
storage = {},
}

View File

@@ -0,0 +1,165 @@
local format = string.format
local meta = {}
local names = {}
local classes = {}
local objects = setmetatable({}, {__mode = 'k'})
function meta:__call(...)
local obj = setmetatable({}, self)
objects[obj] = true
obj:__init(...)
return obj
end
function meta:__tostring()
return 'class ' .. self.__name
end
local default = {}
function default:__tostring()
return self.__name
end
function default:__hash()
return self
end
local function isClass(cls)
return classes[cls]
end
local function isObject(obj)
return objects[obj]
end
local function isSubclass(sub, cls)
if isClass(sub) and isClass(cls) then
if sub == cls then
return true
else
for _, base in ipairs(sub.__bases) do
if isSubclass(base, cls) then
return true
end
end
end
end
return false
end
local function isInstance(obj, cls)
return isObject(obj) and isSubclass(obj.__class, cls)
end
local function profile()
local ret = setmetatable({}, {__index = function() return 0 end})
for obj in pairs(objects) do
local name = obj.__name
ret[name] = ret[name] + 1
end
return ret
end
local types = {['string'] = true, ['number'] = true, ['boolean'] = true}
local function _getPrimitive(v)
return types[type(v)] and v or v ~= nil and tostring(v) or nil
end
local function serialize(obj)
if isObject(obj) then
local ret = {}
for k, v in pairs(obj.__getters) do
ret[k] = _getPrimitive(v(obj))
end
return ret
else
return _getPrimitive(obj)
end
end
local rawtype = type
local function type(obj)
return isObject(obj) and obj.__name or rawtype(obj)
end
return setmetatable({
classes = names,
isClass = isClass,
isObject = isObject,
isSubclass = isSubclass,
isInstance = isInstance,
type = type,
profile = profile,
serialize = serialize,
}, {__call = function(_, name, ...)
if names[name] then return error(format('Class %q already defined', name)) end
local class = setmetatable({}, meta)
classes[class] = true
for k, v in pairs(default) do
class[k] = v
end
local bases = {...}
local getters = {}
local setters = {}
for _, base in ipairs(bases) do
for k1, v1 in pairs(base) do
class[k1] = v1
for k2, v2 in pairs(base.__getters) do
getters[k2] = v2
end
for k2, v2 in pairs(base.__setters) do
setters[k2] = v2
end
end
end
class.__name = name
class.__class = class
class.__bases = bases
class.__getters = getters
class.__setters = setters
local pool = {}
local n = #pool
function class:__index(k)
if getters[k] then
return getters[k](self)
elseif pool[k] then
return rawget(self, pool[k])
else
return class[k]
end
end
function class:__newindex(k, v)
if setters[k] then
return setters[k](self, v)
elseif class[k] or getters[k] then
return error(format('Cannot overwrite protected property: %s.%s', name, k))
elseif k:find('_', 1, true) ~= 1 then
return error(format('Cannot write property to object without leading underscore: %s.%s', name, k))
else
if not pool[k] then
n = n + 1
pool[k] = n
end
return rawset(self, pool[k], v)
end
end
names[name] = class
return class, getters, setters
end})

View File

@@ -0,0 +1,717 @@
local json = require('json')
local timer = require('timer')
local http = require('coro-http')
local package = require('../../package.lua')
local Date = require('utils/Date')
local Mutex = require('utils/Mutex')
local endpoints = require('endpoints')
local request = http.request
local f, gsub, byte = string.format, string.gsub, string.byte
local max, random = math.max, math.random
local encode, decode, null = json.encode, json.decode, json.null
local insert, concat = table.insert, table.concat
local difftime = os.difftime
local sleep = timer.sleep
local running = coroutine.running
local BASE_URL = "https://discordapp.com/api/v7"
local BOUNDARY1 = 'Discordia' .. os.time()
local BOUNDARY2 = '--' .. BOUNDARY1
local BOUNDARY3 = BOUNDARY2 .. '--'
local JSON = 'application/json'
local MULTIPART = f('multipart/form-data;boundary=%s', BOUNDARY1)
local USER_AGENT = f('DiscordBot (%s, %s)', package.homepage, package.version)
local majorRoutes = {guilds = true, channels = true, webhooks = true}
local payloadRequired = {PUT = true, PATCH = true, POST = true}
local parseDate = Date.parseHeader
local function parseErrors(ret, errors, key)
for k, v in pairs(errors) do
if k == '_errors' then
for _, err in ipairs(v) do
insert(ret, f('%s in %s : %s', err.code, key or 'payload', err.message))
end
else
if key then
parseErrors(ret, v, f(k:find("^[%a_][%a%d_]*$") and '%s.%s' or tonumber(k) and '%s[%d]' or '%s[%q]', key, k))
else
parseErrors(ret, v, k)
end
end
end
return concat(ret, '\n\t')
end
local function sub(path)
return not majorRoutes[path] and path .. '/:id'
end
local function route(method, endpoint)
-- special case for reactions
if endpoint:find('reactions') then
endpoint = endpoint:match('.*/reactions')
end
-- remove the ID from minor routes
endpoint = endpoint:gsub('(%a+)/%d+', sub)
-- special case for message deletions
if method == 'DELETE' then
local i, j = endpoint:find('/channels/%d+/messages')
if i == 1 and j == #endpoint then
endpoint = method .. endpoint
end
end
return endpoint
end
local function attachFiles(payload, files)
local ret = {
BOUNDARY2,
'Content-Disposition:form-data;name="payload_json"',
'Content-Type:application/json\r\n',
payload,
}
for i, v in ipairs(files) do
insert(ret, BOUNDARY2)
insert(ret, f('Content-Disposition:form-data;name="file%i";filename=%q', i, v[1]))
insert(ret, 'Content-Type:application/octet-stream\r\n')
insert(ret, v[2])
end
insert(ret, BOUNDARY3)
return concat(ret, '\r\n')
end
local mutexMeta = {
__mode = 'v',
__index = function(self, k)
self[k] = Mutex()
return self[k]
end
}
local function tohex(char)
return f('%%%02X', byte(char))
end
local function urlencode(obj)
return (gsub(tostring(obj), '%W', tohex))
end
local API = require('class')('API')
function API:__init(client)
self._client = client
self._headers = {
{'User-Agent', USER_AGENT}
}
self._mutexes = setmetatable({}, mutexMeta)
end
function API:authenticate(token)
self._headers = {
{'Authorization', token},
{'User-Agent', USER_AGENT},
}
return self:getCurrentUser()
end
function API:request(method, endpoint, payload, query, files)
local _, main = running()
if main then
return error('Cannot make HTTP request outside of a coroutine', 2)
end
local url = BASE_URL .. endpoint
if query and next(query) then
url = {url}
for k, v in pairs(query) do
insert(url, #url == 1 and '?' or '&')
insert(url, urlencode(k))
insert(url, '=')
insert(url, urlencode(v))
end
url = concat(url)
end
local req
if payloadRequired[method] then
payload = payload and encode(payload) or '{}'
req = {}
for i, v in ipairs(self._headers) do
req[i] = v
end
if files and next(files) then
payload = attachFiles(payload, files)
insert(req, {'Content-Type', MULTIPART})
else
insert(req, {'Content-Type', JSON})
end
insert(req, {'Content-Length', #payload})
else
req = self._headers
end
local mutex = self._mutexes[route(method, endpoint)]
mutex:lock()
local data, err, delay = self:commit(method, url, req, payload, 0)
mutex:unlockAfter(delay)
if data then
return data
else
return nil, err
end
end
function API:commit(method, url, req, payload, retries)
local client = self._client
local options = client._options
local delay = options.routeDelay
local success, res, msg = pcall(request, method, url, req, payload)
if not success then
return nil, res, delay
end
for i, v in ipairs(res) do
res[v[1]] = v[2]
res[i] = nil
end
local reset = res['X-RateLimit-Reset']
local remaining = res['X-RateLimit-Remaining']
if reset and remaining == '0' then
local dt = difftime(reset, parseDate(res['Date']))
delay = max(1000 * dt, delay)
end
local data = res['Content-Type'] == JSON and decode(msg, 1, null) or msg
if res.code < 300 then
client:debug('%i - %s : %s %s', res.code, res.reason, method, url)
return data, nil, delay
else
if type(data) == 'table' then
local retry
if res.code == 429 then -- TODO: global ratelimiting
delay = data.retry_after
retry = retries < options.maxRetries
elseif res.code == 502 then
delay = delay + random(2000)
retry = retries < options.maxRetries
end
if retry then
client:warning('%i - %s : retrying after %i ms : %s %s', res.code, res.reason, delay, method, url)
sleep(delay)
return self:commit(method, url, req, payload, retries + 1)
end
if data.code and data.message then
msg = f('HTTP Error %i : %s', data.code, data.message)
else
msg = 'HTTP Error'
end
if data.errors then
msg = parseErrors({msg}, data.errors)
end
end
client:error('%i - %s : %s %s', res.code, res.reason, method, url)
return nil, msg, delay
end
end
-- start of auto-generated methods --
function API:getGuildAuditLog(guild_id, query)
local endpoint = f(endpoints.GUILD_AUDIT_LOGS, guild_id)
return self:request("GET", endpoint, nil, query)
end
function API:getChannel(channel_id) -- not exposed, use cache
local endpoint = f(endpoints.CHANNEL, channel_id)
return self:request("GET", endpoint)
end
function API:modifyChannel(channel_id, payload) -- Channel:_modify
local endpoint = f(endpoints.CHANNEL, channel_id)
return self:request("PATCH", endpoint, payload)
end
function API:deleteChannel(channel_id) -- Channel:delete
local endpoint = f(endpoints.CHANNEL, channel_id)
return self:request("DELETE", endpoint)
end
function API:getChannelMessages(channel_id, query) -- TextChannel:get[First|Last]Message, TextChannel:getMessages
local endpoint = f(endpoints.CHANNEL_MESSAGES, channel_id)
return self:request("GET", endpoint, nil, query)
end
function API:getChannelMessage(channel_id, message_id) -- TextChannel:getMessage fallback
local endpoint = f(endpoints.CHANNEL_MESSAGE, channel_id, message_id)
return self:request("GET", endpoint)
end
function API:createMessage(channel_id, payload, files) -- TextChannel:send
local endpoint = f(endpoints.CHANNEL_MESSAGES, channel_id)
return self:request("POST", endpoint, payload, nil, files)
end
function API:createReaction(channel_id, message_id, emoji, payload) -- Message:addReaction
local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTION_ME, channel_id, message_id, emoji)
return self:request("PUT", endpoint, payload)
end
function API:deleteOwnReaction(channel_id, message_id, emoji) -- Message:removeReaction
local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTION_ME, channel_id, message_id, emoji)
return self:request("DELETE", endpoint)
end
function API:deleteUserReaction(channel_id, message_id, emoji, user_id) -- Message:removeReaction
local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTION_USER, channel_id, message_id, emoji, user_id)
return self:request("DELETE", endpoint)
end
function API:getReactions(channel_id, message_id, emoji, query) -- Reaction:getUsers
local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTION, channel_id, message_id, emoji)
return self:request("GET", endpoint, nil, query)
end
function API:deleteAllReactions(channel_id, message_id) -- Message:clearReactions
local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTIONS, channel_id, message_id)
return self:request("DELETE", endpoint)
end
function API:editMessage(channel_id, message_id, payload) -- Message:_modify
local endpoint = f(endpoints.CHANNEL_MESSAGE, channel_id, message_id)
return self:request("PATCH", endpoint, payload)
end
function API:deleteMessage(channel_id, message_id) -- Message:delete
local endpoint = f(endpoints.CHANNEL_MESSAGE, channel_id, message_id)
return self:request("DELETE", endpoint)
end
function API:bulkDeleteMessages(channel_id, payload) -- GuildTextChannel:bulkDelete
local endpoint = f(endpoints.CHANNEL_MESSAGES_BULK_DELETE, channel_id)
return self:request("POST", endpoint, payload)
end
function API:editChannelPermissions(channel_id, overwrite_id, payload) -- various PermissionOverwrite methods
local endpoint = f(endpoints.CHANNEL_PERMISSION, channel_id, overwrite_id)
return self:request("PUT", endpoint, payload)
end
function API:getChannelInvites(channel_id) -- GuildChannel:getInvites
local endpoint = f(endpoints.CHANNEL_INVITES, channel_id)
return self:request("GET", endpoint)
end
function API:createChannelInvite(channel_id, payload) -- GuildChannel:createInvite
local endpoint = f(endpoints.CHANNEL_INVITES, channel_id)
return self:request("POST", endpoint, payload)
end
function API:deleteChannelPermission(channel_id, overwrite_id) -- PermissionOverwrite:delete
local endpoint = f(endpoints.CHANNEL_PERMISSION, channel_id, overwrite_id)
return self:request("DELETE", endpoint)
end
function API:triggerTypingIndicator(channel_id, payload) -- TextChannel:broadcastTyping
local endpoint = f(endpoints.CHANNEL_TYPING, channel_id)
return self:request("POST", endpoint, payload)
end
function API:getPinnedMessages(channel_id) -- TextChannel:getPinnedMessages
local endpoint = f(endpoints.CHANNEL_PINS, channel_id)
return self:request("GET", endpoint)
end
function API:addPinnedChannelMessage(channel_id, message_id, payload) -- Message:pin
local endpoint = f(endpoints.CHANNEL_PIN, channel_id, message_id)
return self:request("PUT", endpoint, payload)
end
function API:deletePinnedChannelMessage(channel_id, message_id) -- Message:unpin
local endpoint = f(endpoints.CHANNEL_PIN, channel_id, message_id)
return self:request("DELETE", endpoint)
end
function API:groupDMAddRecipient(channel_id, user_id, payload) -- GroupChannel:addRecipient
local endpoint = f(endpoints.CHANNEL_RECIPIENT, channel_id, user_id)
return self:request("PUT", endpoint, payload)
end
function API:groupDMRemoveRecipient(channel_id, user_id) -- GroupChannel:removeRecipient
local endpoint = f(endpoints.CHANNEL_RECIPIENT, channel_id, user_id)
return self:request("DELETE", endpoint)
end
function API:listGuildEmojis(guild_id) -- not exposed, use cache
local endpoint = f(endpoints.GUILD_EMOJIS, guild_id)
return self:request("GET", endpoint)
end
function API:getGuildEmoji(guild_id, emoji_id) -- not exposed, use cache
local endpoint = f(endpoints.GUILD_EMOJI, guild_id, emoji_id)
return self:request("GET", endpoint)
end
function API:createGuildEmoji(guild_id, payload) -- Guild:createEmoji
local endpoint = f(endpoints.GUILD_EMOJIS, guild_id)
return self:request("POST", endpoint, payload)
end
function API:modifyGuildEmoji(guild_id, emoji_id, payload) -- Emoji:_modify
local endpoint = f(endpoints.GUILD_EMOJI, guild_id, emoji_id)
return self:request("PATCH", endpoint, payload)
end
function API:deleteGuildEmoji(guild_id, emoji_id) -- Emoji:delete
local endpoint = f(endpoints.GUILD_EMOJI, guild_id, emoji_id)
return self:request("DELETE", endpoint)
end
function API:createGuild(payload) -- Client:createGuild
local endpoint = endpoints.GUILDS
return self:request("POST", endpoint, payload)
end
function API:getGuild(guild_id) -- not exposed, use cache
local endpoint = f(endpoints.GUILD, guild_id)
return self:request("GET", endpoint)
end
function API:modifyGuild(guild_id, payload) -- Guild:_modify
local endpoint = f(endpoints.GUILD, guild_id)
return self:request("PATCH", endpoint, payload)
end
function API:deleteGuild(guild_id) -- Guild:delete
local endpoint = f(endpoints.GUILD, guild_id)
return self:request("DELETE", endpoint)
end
function API:getGuildChannels(guild_id) -- not exposed, use cache
local endpoint = f(endpoints.GUILD_CHANNELS, guild_id)
return self:request("GET", endpoint)
end
function API:createGuildChannel(guild_id, payload) -- Guild:create[Text|Voice]Channel
local endpoint = f(endpoints.GUILD_CHANNELS, guild_id)
return self:request("POST", endpoint, payload)
end
function API:modifyGuildChannelPositions(guild_id, payload) -- GuildChannel:move[Up|Down]
local endpoint = f(endpoints.GUILD_CHANNELS, guild_id)
return self:request("PATCH", endpoint, payload)
end
function API:getGuildMember(guild_id, user_id) -- Guild:getMember fallback
local endpoint = f(endpoints.GUILD_MEMBER, guild_id, user_id)
return self:request("GET", endpoint)
end
function API:listGuildMembers(guild_id) -- not exposed, use cache
local endpoint = f(endpoints.GUILD_MEMBERS, guild_id)
return self:request("GET", endpoint)
end
function API:addGuildMember(guild_id, user_id, payload) -- not exposed, limited use
local endpoint = f(endpoints.GUILD_MEMBER, guild_id, user_id)
return self:request("PUT", endpoint, payload)
end
function API:modifyGuildMember(guild_id, user_id, payload) -- various Member methods
local endpoint = f(endpoints.GUILD_MEMBER, guild_id, user_id)
return self:request("PATCH", endpoint, payload)
end
function API:modifyCurrentUsersNick(guild_id, payload) -- Member:setNickname
local endpoint = f(endpoints.GUILD_MEMBER_ME_NICK, guild_id)
return self:request("PATCH", endpoint, payload)
end
function API:addGuildMemberRole(guild_id, user_id, role_id, payload) -- Member:addrole
local endpoint = f(endpoints.GUILD_MEMBER_ROLE, guild_id, user_id, role_id)
return self:request("PUT", endpoint, payload)
end
function API:removeGuildMemberRole(guild_id, user_id, role_id) -- Member:removeRole
local endpoint = f(endpoints.GUILD_MEMBER_ROLE, guild_id, user_id, role_id)
return self:request("DELETE", endpoint)
end
function API:removeGuildMember(guild_id, user_id, query) -- Guild:kickUser
local endpoint = f(endpoints.GUILD_MEMBER, guild_id, user_id)
return self:request("DELETE", endpoint, nil, query)
end
function API:getGuildBans(guild_id) -- Guild:getBans
local endpoint = f(endpoints.GUILD_BANS, guild_id)
return self:request("GET", endpoint)
end
function API:getGuildBan(guild_id, user_id) -- Guild:getBan
local endpoint = f(endpoints.GUILD_BAN, guild_id, user_id)
return self:request("GET", endpoint)
end
function API:createGuildBan(guild_id, user_id, query) -- Guild:banUser
local endpoint = f(endpoints.GUILD_BAN, guild_id, user_id)
return self:request("PUT", endpoint, nil, query)
end
function API:removeGuildBan(guild_id, user_id, query) -- Guild:unbanUser / Ban:delete
local endpoint = f(endpoints.GUILD_BAN, guild_id, user_id)
return self:request("DELETE", endpoint, nil, query)
end
function API:getGuildRoles(guild_id) -- not exposed, use cache
local endpoint = f(endpoints.GUILD_ROLES, guild_id)
return self:request("GET", endpoint)
end
function API:createGuildRole(guild_id, payload) -- Guild:createRole
local endpoint = f(endpoints.GUILD_ROLES, guild_id)
return self:request("POST", endpoint, payload)
end
function API:modifyGuildRolePositions(guild_id, payload) -- Role:move[Up|Down]
local endpoint = f(endpoints.GUILD_ROLES, guild_id)
return self:request("PATCH", endpoint, payload)
end
function API:modifyGuildRole(guild_id, role_id, payload) -- Role:_modify
local endpoint = f(endpoints.GUILD_ROLE, guild_id, role_id)
return self:request("PATCH", endpoint, payload)
end
function API:deleteGuildRole(guild_id, role_id) -- Role:delete
local endpoint = f(endpoints.GUILD_ROLE, guild_id, role_id)
return self:request("DELETE", endpoint)
end
function API:getGuildPruneCount(guild_id, query) -- Guild:getPruneCount
local endpoint = f(endpoints.GUILD_PRUNE, guild_id)
return self:request("GET", endpoint, nil, query)
end
function API:beginGuildPrune(guild_id, payload, query) -- Guild:pruneMembers
local endpoint = f(endpoints.GUILD_PRUNE, guild_id)
return self:request("POST", endpoint, payload, query)
end
function API:getGuildVoiceRegions(guild_id) -- Guild:listVoiceRegions
local endpoint = f(endpoints.GUILD_REGIONS, guild_id)
return self:request("GET", endpoint)
end
function API:getGuildInvites(guild_id) -- Guild:getInvites
local endpoint = f(endpoints.GUILD_INVITES, guild_id)
return self:request("GET", endpoint)
end
function API:getGuildIntegrations(guild_id) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_INTEGRATIONS, guild_id)
return self:request("GET", endpoint)
end
function API:createGuildIntegration(guild_id, payload) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_INTEGRATIONS, guild_id)
return self:request("POST", endpoint, payload)
end
function API:modifyGuildIntegration(guild_id, integration_id, payload) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_INTEGRATION, guild_id, integration_id)
return self:request("PATCH", endpoint, payload)
end
function API:deleteGuildIntegration(guild_id, integration_id) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_INTEGRATION, guild_id, integration_id)
return self:request("DELETE", endpoint)
end
function API:syncGuildIntegration(guild_id, integration_id, payload) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_INTEGRATION_SYNC, guild_id, integration_id)
return self:request("POST", endpoint, payload)
end
function API:getGuildEmbed(guild_id) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_EMBED, guild_id)
return self:request("GET", endpoint)
end
function API:modifyGuildEmbed(guild_id, payload) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_EMBED, guild_id)
return self:request("PATCH", endpoint, payload)
end
function API:getInvite(invite_code, query) -- Client:getInvite
local endpoint = f(endpoints.INVITE, invite_code)
return self:request("GET", endpoint, nil, query)
end
function API:deleteInvite(invite_code) -- Invite:delete
local endpoint = f(endpoints.INVITE, invite_code)
return self:request("DELETE", endpoint)
end
function API:acceptInvite(invite_code, payload) -- not exposed, invalidates tokens
local endpoint = f(endpoints.INVITE, invite_code)
return self:request("POST", endpoint, payload)
end
function API:getCurrentUser() -- API:authenticate
local endpoint = endpoints.USER_ME
return self:request("GET", endpoint)
end
function API:getUser(user_id) -- Client:getUser
local endpoint = f(endpoints.USER, user_id)
return self:request("GET", endpoint)
end
function API:modifyCurrentUser(payload) -- Client:_modify
local endpoint = endpoints.USER_ME
return self:request("PATCH", endpoint, payload)
end
function API:getCurrentUserGuilds() -- not exposed, use cache
local endpoint = endpoints.USER_ME_GUILDS
return self:request("GET", endpoint)
end
function API:leaveGuild(guild_id) -- Guild:leave
local endpoint = f(endpoints.USER_ME_GUILD, guild_id)
return self:request("DELETE", endpoint)
end
function API:getUserDMs() -- not exposed, use cache
local endpoint = endpoints.USER_ME_CHANNELS
return self:request("GET", endpoint)
end
function API:createDM(payload) -- User:getPrivateChannel fallback
local endpoint = endpoints.USER_ME_CHANNELS
return self:request("POST", endpoint, payload)
end
function API:createGroupDM(payload) -- Client:createGroupChannel
local endpoint = endpoints.USER_ME_CHANNELS
return self:request("POST", endpoint, payload)
end
function API:getUsersConnections() -- Client:getConnections
local endpoint = endpoints.USER_ME_CONNECTIONS
return self:request("GET", endpoint)
end
function API:listVoiceRegions() -- Client:listVoiceRegions
local endpoint = endpoints.VOICE_REGIONS
return self:request("GET", endpoint)
end
function API:createWebhook(channel_id, payload) -- GuildTextChannel:createWebhook
local endpoint = f(endpoints.CHANNEL_WEBHOOKS, channel_id)
return self:request("POST", endpoint, payload)
end
function API:getChannelWebhooks(channel_id) -- GuildTextChannel:getWebhooks
local endpoint = f(endpoints.CHANNEL_WEBHOOKS, channel_id)
return self:request("GET", endpoint)
end
function API:getGuildWebhooks(guild_id) -- Guild:getWebhooks
local endpoint = f(endpoints.GUILD_WEBHOOKS, guild_id)
return self:request("GET", endpoint)
end
function API:getWebhook(webhook_id) -- Client:getWebhook
local endpoint = f(endpoints.WEBHOOK, webhook_id)
return self:request("GET", endpoint)
end
function API:getWebhookWithToken(webhook_id, webhook_token) -- not exposed, needs webhook client
local endpoint = f(endpoints.WEBHOOK_TOKEN, webhook_id, webhook_token)
return self:request("GET", endpoint)
end
function API:modifyWebhook(webhook_id, payload) -- Webhook:_modify
local endpoint = f(endpoints.WEBHOOK, webhook_id)
return self:request("PATCH", endpoint, payload)
end
function API:modifyWebhookWithToken(webhook_id, webhook_token, payload) -- not exposed, needs webhook client
local endpoint = f(endpoints.WEBHOOK_TOKEN, webhook_id, webhook_token)
return self:request("PATCH", endpoint, payload)
end
function API:deleteWebhook(webhook_id) -- Webhook:delete
local endpoint = f(endpoints.WEBHOOK, webhook_id)
return self:request("DELETE", endpoint)
end
function API:deleteWebhookWithToken(webhook_id, webhook_token) -- not exposed, needs webhook client
local endpoint = f(endpoints.WEBHOOK_TOKEN, webhook_id, webhook_token)
return self:request("DELETE", endpoint)
end
function API:executeWebhook(webhook_id, webhook_token, payload) -- not exposed, needs webhook client
local endpoint = f(endpoints.WEBHOOK_TOKEN, webhook_id, webhook_token)
return self:request("POST", endpoint, payload)
end
function API:executeSlackCompatibleWebhook(webhook_id, webhook_token, payload) -- not exposed, needs webhook client
local endpoint = f(endpoints.WEBHOOK_TOKEN_SLACK, webhook_id, webhook_token)
return self:request("POST", endpoint, payload)
end
function API:executeGitHubCompatibleWebhook(webhook_id, webhook_token, payload) -- not exposed, needs webhook client
local endpoint = f(endpoints.WEBHOOK_TOKEN_GITHUB, webhook_id, webhook_token)
return self:request("POST", endpoint, payload)
end
function API:getGateway() -- Client:run
local endpoint = endpoints.GATEWAY
return self:request("GET", endpoint)
end
function API:getGatewayBot() -- Client:run
local endpoint = endpoints.GATEWAY_BOT
return self:request("GET", endpoint)
end
function API:getCurrentApplicationInformation() -- Client:run
local endpoint = endpoints.OAUTH2_APPLICATION_ME
return self:request("GET", endpoint)
end
-- end of auto-generated methods --
return API

View File

@@ -0,0 +1,646 @@
--[=[
@ic Client x Emitter
@op options table
@d The main point of entry into a Discordia application. All data relevant to
Discord are accessible through a client instance or its child objects after a
connection to Discord is established with the `run` method. In other words,
client data should not be expected and most client methods should not be called
until after the `ready` event is received. Base emitter methods may be called
at any time. See [[client options]].
]=]
local fs = require('fs')
local json = require('json')
local constants = require('constants')
local enums = require('enums')
local package = require('../../package.lua')
local API = require('client/API')
local Shard = require('client/Shard')
local Resolver = require('client/Resolver')
local GroupChannel = require('containers/GroupChannel')
local Guild = require('containers/Guild')
local PrivateChannel = require('containers/PrivateChannel')
local User = require('containers/User')
local Invite = require('containers/Invite')
local Webhook = require('containers/Webhook')
local Relationship = require('containers/Relationship')
local Cache = require('iterables/Cache')
local WeakCache = require('iterables/WeakCache')
local Emitter = require('utils/Emitter')
local Logger = require('utils/Logger')
local Mutex = require('utils/Mutex')
local VoiceManager = require('voice/VoiceManager')
local encode, decode, null = json.encode, json.decode, json.null
local readFileSync, writeFileSync = fs.readFileSync, fs.writeFileSync
local logLevel = enums.logLevel
local gameType = enums.gameType
local wrap = coroutine.wrap
local time, difftime = os.time, os.difftime
local format = string.format
local CACHE_AGE = constants.CACHE_AGE
local GATEWAY_VERSION = constants.GATEWAY_VERSION
-- do not change these options here
-- pass a custom table on client initialization instead
local defaultOptions = {
routeDelay = 300,
maxRetries = 5,
shardCount = 0,
firstShard = 0,
lastShard = -1,
largeThreshold = 100,
cacheAllMembers = false,
autoReconnect = true,
compress = true,
bitrate = 64000,
logFile = 'discordia.log',
logLevel = logLevel.info,
dateTime = '%F %T',
syncGuilds = false,
}
local function parseOptions(customOptions)
if type(customOptions) == 'table' then
local options = {}
for k, default in pairs(defaultOptions) do -- load options
local custom = customOptions[k]
if custom ~= nil then
options[k] = custom
else
options[k] = default
end
end
for k, v in pairs(customOptions) do -- validate options
local default = type(defaultOptions[k])
local custom = type(v)
if default ~= custom then
return error(format('invalid client option %q (%s expected, got %s)', k, default, custom), 3)
end
if custom == 'number' and (v < 0 or v % 1 ~= 0) then
return error(format('invalid client option %q (number must be a positive integer)', k), 3)
end
end
return options
else
return defaultOptions
end
end
local Client, get = require('class')('Client', Emitter)
function Client:__init(options)
Emitter.__init(self)
options = parseOptions(options)
self._options = options
self._shards = {}
self._api = API(self)
self._mutex = Mutex()
self._users = Cache({}, User, self)
self._guilds = Cache({}, Guild, self)
self._group_channels = Cache({}, GroupChannel, self)
self._private_channels = Cache({}, PrivateChannel, self)
self._relationships = Cache({}, Relationship, self)
self._webhooks = WeakCache({}, Webhook, self) -- used for audit logs
self._logger = Logger(options.logLevel, options.dateTime, options.logFile)
self._voice = VoiceManager(self)
self._role_map = {}
self._emoji_map = {}
self._channel_map = {}
self._events = require('client/EventHandler')
end
for name, level in pairs(logLevel) do
Client[name] = function(self, fmt, ...)
local msg = self._logger:log(level, fmt, ...)
return self:emit(name, msg or format(fmt, ...))
end
end
function Client:_deprecated(clsName, before, after)
local info = debug.getinfo(3)
return self:warning(
'%s:%s: %s.%s is deprecated; use %s.%s instead',
info.short_src,
info.currentline,
clsName,
before,
clsName,
after
)
end
local function run(self, token)
self:info('Discordia %s', package.version)
self:info('Connecting to Discord...')
local api = self._api
local users = self._users
local options = self._options
local user, err1 = api:authenticate(token)
if not user then
return self:error('Could not authenticate, check token: ' .. err1)
end
self._user = users:_insert(user)
self._token = token
self:info('Authenticated as %s#%s', user.username, user.discriminator)
local now = time()
local url, count, owner
local cache = readFileSync('gateway.json')
cache = cache and decode(cache)
if cache then
local d = cache[user.id]
if d and difftime(now, d.timestamp) < CACHE_AGE then
url = cache.url
if user.bot then
count = d.shards
owner = d.owner
else
count = 1
owner = user
end
end
else
cache = {}
end
if not url or not owner then
if user.bot then
local gateway, err2 = api:getGatewayBot()
if not gateway then
return self:error('Could not get gateway: ' .. err2)
end
local app, err3 = api:getCurrentApplicationInformation()
if not app then
return self:error('Could not get application information: ' .. err3)
end
url = gateway.url
count = gateway.shards
owner = app.owner
cache[user.id] = {owner = owner, shards = count, timestamp = now}
else
local gateway, err2 = api:getGateway()
if not gateway then
return self:error('Could not get gateway: ' .. err2)
end
url = gateway.url
count = 1
owner = user
cache[user.id] = {timestamp = now}
end
cache.url = url
writeFileSync('gateway.json', encode(cache))
end
self._owner = users:_insert(owner)
if options.shardCount > 0 then
if count ~= options.shardCount then
self:warning('Requested shard count (%i) is different from recommended count (%i)', options.shardCount, count)
end
count = options.shardCount
end
local first, last = options.firstShard, options.lastShard
if last < 0 then
last = count - 1
end
if last < first then
return self:error('First shard ID (%i) is greater than last shard ID (%i)', first, last)
end
local d = last - first + 1
if d > count then
return self:error('Shard count (%i) is less than target shard range (%i)', count, d)
end
if first == last then
self:info('Launching shard %i (%i out of %i)...', first, d, count)
else
self:info('Launching shards %i through %i (%i out of %i)...', first, last, d, count)
end
self._total_shard_count = count
self._shard_count = d
for id = first, last do
self._shards[id] = Shard(id, self)
end
local path = format('/?v=%i&encoding=json', GATEWAY_VERSION)
for _, shard in pairs(self._shards) do
wrap(shard.connect)(shard, url, path)
shard:identifyWait()
end
end
--[=[
@m run
@p token string
@op presence table
@r nil
@d Authenticates the current user via HTTPS and launches as many WSS gateway
shards as are required or requested. By using coroutines that are automatically
managed by Luvit libraries and a libuv event loop, multiple clients per process
and multiple shards per client can operate concurrently. This should be the last
method called after all other code and event handlers have been initialized.
]=]
function Client:run(token, presence)
self._presence = presence or {}
return wrap(run)(self, token)
end
--[=[
@m stop
@r nil
@d Disconnects all shards and effectively stop their loops. This does not
empty any data that the client may have cached.
]=]
function Client:stop()
for _, shard in pairs(self._shards) do
shard:disconnect()
end
end
function Client:_modify(payload)
local data, err = self._api:modifyCurrentUser(payload)
if data then
data.token = nil
self._user:_load(data)
return true
else
return false, err
end
end
--[=[
@m setUsername
@p username string
@r boolean
@d Sets the client's username. This must be between 2 and 32 characters in
length. This does not change the application name.
]=]
function Client:setUsername(username)
return self:_modify({username = username or null})
end
--[=[
@m setAvatar
@p avatar Base64-Resolveable
@r boolean
@d Sets the client's avatar. To remove the avatar, pass an empty string or nil.
This does not change the application image.
]=]
function Client:setAvatar(avatar)
avatar = avatar and Resolver.base64(avatar)
return self:_modify({avatar = avatar or null})
end
--[=[
@m createGuild
@p name string
@r boolean
@d Creates a new guild. The name must be between 2 and 100 characters in length.
This method may not work if the current user is in too many guilds. Note that
this does not return the created guild object; wait for the corresponding
`guildCreate` event if you need the object.
]=]
function Client:createGuild(name)
local data, err = self._api:createGuild({name = name})
if data then
return true
else
return false, err
end
end
--[=[
@m createGroupChannel
@r GroupChannel
@d Creates a new group channel. This method is only available for user accounts.
]=]
function Client:createGroupChannel()
local data, err = self._api:createGroupDM()
if data then
return self._group_channels:_insert(data)
else
return nil, err
end
end
--[=[
@m getWebhook
@p id string
@r Webhook
@d Gets a webhook object by ID. This always makes an HTTP request to obtain a
static object that is not cached and is not updated by gateway events.
]=]
function Client:getWebhook(id)
local data, err = self._api:getWebhook(id)
if data then
return Webhook(data, self)
else
return nil, err
end
end
--[=[
@m getInvite
@p code string
@op counts boolean
@r Invite
@d Gets an invite object by code. This always makes an HTTP request to obtain a
static object that is not cached and is not updated by gateway events.
]=]
function Client:getInvite(code, counts)
local data, err = self._api:getInvite(code, counts and {with_counts = true})
if data then
return Invite(data, self)
else
return nil, err
end
end
--[=[
@m getUser
@p id User-ID-Resolvable
@r User
@d Gets a user object by ID. If the object is already cached, then the cached
object will be returned; otherwise, an HTTP request is made. Under circumstances
which should be rare, the user object may be an old version, not updated by
gateway events.
]=]
function Client:getUser(id)
id = Resolver.userId(id)
local user = self._users:get(id)
if user then
return user
else
local data, err = self._api:getUser(id)
if data then
return self._users:_insert(data)
else
return nil, err
end
end
end
--[=[
@m getGuild
@p id Guild-ID-Resolvable
@r Guild
@d Gets a guild object by ID. The current user must be in the guild and the client
must be running the appropriate shard that serves this guild. This method never
makes an HTTP request to obtain a guild.
]=]
function Client:getGuild(id)
id = Resolver.guildId(id)
return self._guilds:get(id)
end
--[=[
@m getChannel
@p id Channel-ID-Resolvable
@r Channel
@d Gets a channel object by ID. For guild channels, the current user must be in
the channel's guild and the client must be running the appropriate shard that
serves the channel's guild.
For private channels, the channel must have been previously opened and cached.
If the channel is not cached, `User:getPrivateChannel` should be used instead.
]=]
function Client:getChannel(id)
id = Resolver.channelId(id)
local guild = self._channel_map[id]
if guild then
return guild._text_channels:get(id) or guild._voice_channels:get(id) or guild._categories:get(id)
else
return self._private_channels:get(id) or self._group_channels:get(id)
end
end
--[=[
@m getRole
@p id Role-ID-Resolvable
@r Role
@d Gets a role object by ID. The current user must be in the role's guild and
the client must be running the appropriate shard that serves the role's guild.
]=]
function Client:getRole(id)
id = Resolver.roleId(id)
local guild = self._role_map[id]
return guild and guild._roles:get(id)
end
--[=[
@m getEmoji
@p id Emoji-ID-Resolvable
@r Emoji
@d Gets an emoji object by ID. The current user must be in the emoji's guild and
the client must be running the appropriate shard that serves the emoji's guild.
]=]
function Client:getEmoji(id)
id = Resolver.emojiId(id)
local guild = self._emoji_map[id]
return guild and guild._emojis:get(id)
end
--[=[
@m listVoiceRegions
@r table
@d Returns a raw data table that contains a list of voice regions as provided by
Discord, with no additional parsing.
]=]
function Client:listVoiceRegions()
return self._api:listVoiceRegions()
end
--[=[
@m getConnections
@r table
@d Returns a raw data table that contains a list of connections as provided by
Discord, with no additional parsing. This is unrelated to voice connections.
]=]
function Client:getConnections()
return self._api:getUsersConnections()
end
local function updateStatus(self)
local presence = self._presence
presence.afk = presence.afk or null
presence.game = presence.game or null
presence.since = presence.since or null
presence.status = presence.status or null
for _, shard in pairs(self._shards) do
shard:updateStatus(presence)
end
end
--[=[
@m setStatus
@p status string
@r nil
@d Sets the current users's status on all shards that are managed by this client.
See the `status` enumeration for acceptable status values.
]=]
function Client:setStatus(status)
if type(status) == 'string' then
self._presence.status = status
if status == 'idle' then
self._presence.since = 1000 * time()
else
self._presence.since = null
end
else
self._presence.status = null
self._presence.since = null
end
return updateStatus(self)
end
--[=[
@m setGame
@p game string/table
@r nil
@d Sets the current users's game on all shards that are managed by this client.
If a string is passed, it is treated as the game name. If a table is passed, it
must have a `name` field and may optionally have a `url` field. Pass `nil` to
remove the game status.
]=]
function Client:setGame(game)
if type(game) == 'string' then
game = {name = game, type = gameType.default}
elseif type(game) == 'table' then
if type(game.name) == 'string' then
if type(game.type) ~= 'number' then
if type(game.url) == 'string' then
game.type = gameType.streaming
else
game.type = gameType.default
end
end
else
game = null
end
else
game = null
end
self._presence.game = game
return updateStatus(self)
end
--[=[
@m setAFK
@p afk boolean
@r nil
@d Set the current user's AFK status on all shards that are managed by this client.
This generally applies to user accounts and their push notifications.
]=]
function Client:setAFK(afk)
if type(afk) == 'boolean' then
self._presence.afk = afk
else
self._presence.afk = null
end
return updateStatus(self)
end
--[=[@p shardCount number/nil The number of shards that this client is managing.]=]
function get.shardCount(self)
return self._shard_count
end
--[=[@p totalShardCount number/nil The total number of shards that the current user is on.]=]
function get.totalShardCount(self)
return self._total_shard_count
end
--[=[@p user User/nil User object representing the current user.]=]
function get.user(self)
return self._user
end
--[=[@p owner User/nil User object representing the current user's owner.]=]
function get.owner(self)
return self._owner
end
--[=[@p verified boolean/nil Whether the current user's owner's account is verified.]=]
function get.verified(self)
return self._user and self._user._verified
end
--[=[@p mfaEnabled boolean/nil Whether the current user's owner's account has multi-factor (or two-factor)
authentication enabled.]=]
function get.mfaEnabled(self)
return self._user and self._user._verified
end
--[=[@p email string/nil The current user's owner's account's email address (user-accounts only).]=]
function get.email(self)
return self._user and self._user._email
end
--[=[@p guilds Cache An iterable cache of all guilds that are visible to the client. Note that the
guilds present here correspond to which shards the client is managing. If all
shards are managed by one client, then all guilds will be present.]=]
function get.guilds(self)
return self._guilds
end
--[=[@p users Cache An iterable cache of all users that are visible to the client.
To access a user that may exist but is not cached, use `Client:getUser`.]=]
function get.users(self)
return self._users
end
--[=[@p privateChannels Cache An iterable cache of all private channels that are visible to the client. The
channel must exist and must be open for it to be cached here. To access a
private channel that may exist but is not cached, `User:getPrivateChannel`.]=]
function get.privateChannels(self)
return self._private_channels
end
--[=[@p groupChannels Cache An iterable cache of all group channels that are visible to the client. Only
user-accounts should have these.]=]
function get.groupChannels(self)
return self._group_channels
end
--[=[@p relationships Cache An iterable cache of all relationships that are visible to the client. Only
user-accounts should have these.]=]
function get.relationships(self)
return self._relationships
end
return Client

View File

@@ -0,0 +1,540 @@
local enums = require('enums')
local json = require('json')
local channelType = enums.channelType
local concat, insert = table.concat, table.insert
local null = json.null
local function warning(client, object, id, event)
return client:warning('Uncached %s (%s) on %s', object, id, event)
end
local function checkReady(shard)
for _, v in pairs(shard._loading) do
if next(v) then return end
end
shard._ready = true
shard._loading = nil
collectgarbage()
local client = shard._client
client:emit('shardReady', shard._id)
for _, other in pairs(client._shards) do
if not other._ready then return end
end
return client:emit('ready')
end
local function getChannel(client, id)
local guild = client._channel_map[id]
if guild then
return guild._text_channels:get(id)
else
return client._private_channels:get(id) or client._group_channels:get(id)
end
end
local EventHandler = setmetatable({}, {__index = function(self, k)
self[k] = function(_, _, shard)
return shard:warning('Unhandled gateway event: %s', k)
end
return self[k]
end})
function EventHandler.READY(d, client, shard)
shard:info('Received READY (%s)', concat(d._trace, ', '))
shard:emit('READY')
shard._session_id = d.session_id
client._user = client._users:_insert(d.user)
local guilds = client._guilds
local group_channels = client._group_channels
local private_channels = client._private_channels
local relationships = client._relationships
for _, channel in ipairs(d.private_channels) do
if channel.type == channelType.private then
private_channels:_insert(channel)
elseif channel.type == channelType.group then
group_channels:_insert(channel)
end
end
local loading = shard._loading
if d.user.bot then
for _, guild in ipairs(d.guilds) do
guilds:_insert(guild)
loading.guilds[guild.id] = true
end
else
if client._options.syncGuilds then
local ids = {}
for _, guild in ipairs(d.guilds) do
guilds:_insert(guild)
if not guild.unavailable then
loading.syncs[guild.id] = true
insert(ids, guild.id)
end
end
shard:syncGuilds(ids)
else
guilds:_load(d.guilds)
end
end
relationships:_load(d.relationships)
for _, presence in ipairs(d.presences) do
local relationship = relationships:get(presence.user.id)
if relationship then
relationship:_loadPresence(presence)
end
end
return checkReady(shard)
end
function EventHandler.RESUMED(d, client, shard)
shard:info('Received RESUMED (%s)', concat(d._trace, ', '))
return client:emit('shardResumed', shard._id)
end
function EventHandler.GUILD_MEMBERS_CHUNK(d, client, shard)
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBERS_CHUNK') end
guild._members:_load(d.members)
if shard._loading and guild._member_count == #guild._members then
shard._loading.chunks[d.guild_id] = nil
return checkReady(shard)
end
end
function EventHandler.GUILD_SYNC(d, client, shard)
local guild = client._guilds:get(d.id)
if not guild then return warning(client, 'Guild', d.id, 'GUILD_SYNC') end
guild._large = d.large
guild:_loadMembers(d, shard)
if shard._loading then
shard._loading.syncs[d.id] = nil
return checkReady(shard)
end
end
function EventHandler.CHANNEL_CREATE(d, client)
local channel
local t = d.type
if t == channelType.text then
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_CREATE') end
channel = guild._text_channels:_insert(d)
elseif t == channelType.voice then
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_CREATE') end
channel = guild._voice_channels:_insert(d)
elseif t == channelType.private then
channel = client._private_channels:_insert(d)
elseif t == channelType.group then
channel = client._group_channels:_insert(d)
elseif t == channelType.category then
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_CREATE') end
channel = guild._categories:_insert(d)
else
return client:warning('Unhandled CHANNEL_CREATE (type %s)', d.type)
end
return client:emit('channelCreate', channel)
end
function EventHandler.CHANNEL_UPDATE(d, client)
local channel
local t = d.type
if t == channelType.text then
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_UPDATE') end
channel = guild._text_channels:_insert(d)
elseif t == channelType.voice then
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_UPDATE') end
channel = guild._voice_channels:_insert(d)
elseif t == channelType.private then -- private channels should never update
channel = client._private_channels:_insert(d)
elseif t == channelType.group then
channel = client._group_channels:_insert(d)
elseif t == channelType.category then
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_UPDATE') end
channel = guild._categories:_insert(d)
else
return client:warning('Unhandled CHANNEL_UPDATE (type %s)', d.type)
end
return client:emit('channelUpdate', channel)
end
function EventHandler.CHANNEL_DELETE(d, client)
local channel
local t = d.type
if t == channelType.text then
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_DELETE') end
channel = guild._text_channels:_remove(d)
elseif t == channelType.voice then
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_DELETE') end
channel = guild._voice_channels:_remove(d)
elseif t == channelType.private then
channel = client._private_channels:_remove(d)
elseif t == channelType.group then
channel = client._group_channels:_remove(d)
elseif t == channelType.category then
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_DELETE') end
channel = guild._categories:_remove(d)
else
return client:warning('Unhandled CHANNEL_DELETE (type %s)', d.type)
end
return client:emit('channelDelete', channel)
end
function EventHandler.CHANNEL_RECIPIENT_ADD(d, client)
local channel = client._group_channels:get(d.channel_id)
if not channel then return warning(client, 'GroupChannel', d.channel_id, 'CHANNEL_RECIPIENT_ADD') end
local user = channel._recipients:_insert(d.user)
return client:emit('recipientAdd', channel, user)
end
function EventHandler.CHANNEL_RECIPIENT_REMOVE(d, client)
local channel = client._group_channels:get(d.channel_id)
if not channel then return warning(client, 'GroupChannel', d.channel_id, 'CHANNEL_RECIPIENT_REMOVE') end
local user = channel._recipients:_remove(d.user)
return client:emit('recipientRemove', channel, user)
end
function EventHandler.GUILD_CREATE(d, client, shard)
if client._options.syncGuilds and not d.unavailable and not client._user._bot then
shard:syncGuilds({d.id})
end
local guild = client._guilds:get(d.id)
if guild then
if guild._unavailable and not d.unavailable then
guild:_load(d)
guild:_makeAvailable(d)
client:emit('guildAvailable', guild)
end
if shard._loading then
shard._loading.guilds[d.id] = nil
return checkReady(shard)
end
else
guild = client._guilds:_insert(d)
return client:emit('guildCreate', guild)
end
end
function EventHandler.GUILD_UPDATE(d, client)
local guild = client._guilds:_insert(d)
return client:emit('guildUpdate', guild)
end
function EventHandler.GUILD_DELETE(d, client)
if d.unavailable then
local guild = client._guilds:_insert(d)
return client:emit('guildUnavailable', guild)
else
local guild = client._guilds:_remove(d)
return client:emit('guildDelete', guild)
end
end
function EventHandler.GUILD_BAN_ADD(d, client)
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_BAN_ADD') end
local user = client._users:_insert(d.user)
return client:emit('userBan', user, guild)
end
function EventHandler.GUILD_BAN_REMOVE(d, client)
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_BAN_REMOVE') end
local user = client._users:_insert(d.user)
return client:emit('userUnban', user, guild)
end
function EventHandler.GUILD_EMOJIS_UPDATE(d, client)
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_EMOJIS_UPDATE') end
guild._emojis:_load(d.emojis, true)
return client:emit('emojisUpdate', guild)
end
function EventHandler.GUILD_MEMBER_ADD(d, client)
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBER_ADD') end
local member = guild._members:_insert(d)
guild._member_count = guild._member_count + 1
return client:emit('memberJoin', member)
end
function EventHandler.GUILD_MEMBER_UPDATE(d, client)
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBER_UPDATE') end
local member = guild._members:_insert(d)
return client:emit('memberUpdate', member)
end
function EventHandler.GUILD_MEMBER_REMOVE(d, client)
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBER_REMOVE') end
local member = guild._members:_remove(d)
guild._member_count = guild._member_count - 1
return client:emit('memberLeave', member)
end
function EventHandler.GUILD_ROLE_CREATE(d, client)
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_ROLE_CREATE') end
local role = guild._roles:_insert(d.role)
return client:emit('roleCreate', role)
end
function EventHandler.GUILD_ROLE_UPDATE(d, client)
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_ROLE_UPDATE') end
local role = guild._roles:_insert(d.role)
return client:emit('roleUpdate', role)
end
function EventHandler.GUILD_ROLE_DELETE(d, client) -- role object not provided
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_ROLE_DELETE') end
local role = guild._roles:_delete(d.role_id)
if not role then return warning(client, 'Role', d.role_id, 'GUILD_ROLE_DELETE') end
return client:emit('roleDelete', role)
end
function EventHandler.MESSAGE_CREATE(d, client)
local channel = getChannel(client, d.channel_id)
if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_CREATE') end
local message = channel._messages:_insert(d)
return client:emit('messageCreate', message)
end
function EventHandler.MESSAGE_UPDATE(d, client) -- may not contain the whole message
local channel = getChannel(client, d.channel_id)
if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_UPDATE') end
local message = channel._messages:get(d.id)
if message then
message:_setOldContent(d)
message:_load(d)
return client:emit('messageUpdate', message)
else
return client:emit('messageUpdateUncached', channel, d.id)
end
end
function EventHandler.MESSAGE_DELETE(d, client) -- message object not provided
local channel = getChannel(client, d.channel_id)
if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_DELETE') end
local message = channel._messages:_delete(d.id)
if message then
return client:emit('messageDelete', message)
else
return client:emit('messageDeleteUncached', channel, d.id)
end
end
function EventHandler.MESSAGE_DELETE_BULK(d, client)
local channel = getChannel(client, d.channel_id)
if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_DELETE_BULK') end
for _, id in ipairs(d.ids) do
local message = channel._messages:_delete(id)
if message then
client:emit('messageDelete', message)
else
client:emit('messageDeleteUncached', channel, id)
end
end
end
function EventHandler.MESSAGE_REACTION_ADD(d, client)
local channel = getChannel(client, d.channel_id)
if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_REACTION_ADD') end
local message = channel._messages:get(d.message_id)
if message then
local reaction = message:_addReaction(d)
return client:emit('reactionAdd', reaction, d.user_id)
else
local k = d.emoji.id ~= null and d.emoji.id or d.emoji.name
return client:emit('reactionAddUncached', channel, d.message_id, k, d.user_id)
end
end
function EventHandler.MESSAGE_REACTION_REMOVE(d, client)
local channel = getChannel(client, d.channel_id)
if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_REACTION_REMOVE') end
local message = channel._messages:get(d.message_id)
if message then
local reaction = message:_removeReaction(d)
if not reaction then -- uncached reaction?
local k = d.emoji.id ~= null and d.emoji.id or d.emoji.name
return warning(client, 'Reaction', k, 'MESSAGE_REACTION_REMOVE')
end
return client:emit('reactionRemove', reaction, d.user_id)
else
local k = d.emoji.id ~= null and d.emoji.id or d.emoji.name
return client:emit('reactionRemoveUncached', channel, d.message_id, k, d.user_id)
end
end
function EventHandler.MESSAGE_REACTION_REMOVE_ALL(d, client)
local channel = getChannel(client, d.channel_id)
if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_REACTION_REMOVE_ALL') end
local message = channel._messages:get(d.message_id)
if message then
local reactions = message._reactions
if reactions then
for reaction in reactions:iter() do
reaction._count = 0
end
message._reactions = nil
end
return client:emit('reactionRemoveAll', message)
else
return client:emit('reactionRemoveAllUncached', channel, d.message_id)
end
end
function EventHandler.CHANNEL_PINS_UPDATE(d, client)
local channel = getChannel(client, d.channel_id)
if not channel then return warning(client, 'TextChannel', d.channel_id, 'CHANNEL_PINS_UPDATE') end
return client:emit('pinsUpdate', channel)
end
function EventHandler.PRESENCE_UPDATE(d, client) -- may have incomplete data
local user = client._users:get(d.user.id)
if user then
user:_load(d.user)
end
if d.guild_id then
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'PRESENCE_UPDATE') end
local member
if client._options.cacheAllMembers then
member = guild._members:get(d.user.id)
if not member then return end -- still loading or member left
else
if d.status == 'offline' then -- uncache offline members
member = guild._members:_delete(d.user.id)
else
if d.user.username then -- member was offline
member = guild._members:_insert(d)
elseif user then -- member was invisible, user is still cached
member = guild._members:_insert(d)
member._user = user
end
end
end
if member then
member:_loadPresence(d)
return client:emit('presenceUpdate', member)
end
else
local relationship = client._relationships:get(d.user.id)
if relationship then
relationship:_loadPresence(d)
return client:emit('relationshipUpdate', relationship)
end
end
end
function EventHandler.RELATIONSHIP_ADD(d, client)
local relationship = client._relationships:_insert(d)
return client:emit('relationshipAdd', relationship)
end
function EventHandler.RELATIONSHIP_REMOVE(d, client)
local relationship = client._relationships:_remove(d)
return client:emit('relationshipRemove', relationship)
end
function EventHandler.TYPING_START(d, client)
return client:emit('typingStart', d.user_id, d.channel_id, d.timestamp)
end
function EventHandler.USER_UPDATE(d, client)
client._user:_load(d)
return client:emit('userUpdate', client._user)
end
local function load(obj, d)
for k, v in pairs(d) do obj[k] = v end
end
function EventHandler.VOICE_STATE_UPDATE(d, client)
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'VOICE_STATE_UPDATE') end
local member = d.member and guild._members:_insert(d.member) or guild._members:get(d.user_id)
if not member then return warning(client, 'Member', d.user_id, 'VOICE_STATE_UPDATE') end
local states = guild._voice_states
local channels = guild._voice_channels
local new_channel_id = d.channel_id
local state = states[d.user_id]
if state then -- user is already connected
local old_channel_id = state.channel_id
load(state, d)
if new_channel_id ~= null then -- state changed, but user has not disconnected
if new_channel_id == old_channel_id then -- user did not change channels
client:emit('voiceUpdate', member)
else -- user changed channels
local old = channels:get(old_channel_id)
local new = channels:get(new_channel_id)
if d.user_id == client._user._id then -- move connection to new channel
local connection = old._connection
if connection then
new._connection = connection
old._connection = nil
connection._channel = new
connection:_continue(true)
end
end
client:emit('voiceChannelLeave', member, old)
client:emit('voiceChannelJoin', member, new)
end
else -- user has disconnected
states[d.user_id] = nil
local old = channels:get(old_channel_id)
client:emit('voiceChannelLeave', member, old)
client:emit('voiceDisconnect', member)
end
else -- user has connected
states[d.user_id] = d
local new = channels:get(new_channel_id)
client:emit('voiceConnect', member)
client:emit('voiceChannelJoin', member, new)
end
end
function EventHandler.VOICE_SERVER_UPDATE(d, client)
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'VOICE_SERVER_UPDATE') end
local state = guild._voice_states[client._user._id]
if not state then return client:warning('Voice state not initialized before VOICE_SERVER_UPDATE') end
load(state, d)
local channel = guild._voice_channels:get(state.channel_id)
if not channel then return warning(client, 'GuildVoiceChannel', state.channel_id, 'VOICE_SERVER_UPDATE') end
local connection = channel._connection
if not connection then return client:warning('Voice connection not initialized before VOICE_SERVER_UPDATE') end
return client._voice:_prepareConnection(state, connection)
end
function EventHandler.WEBHOOKS_UPDATE(d, client) -- webhook object is not provided
local guild = client._guilds:get(d.guild_id)
if not guild then return warning(client, 'Guild', d.guild_id, 'WEBHOOKS_UDPATE') end
local channel = guild._text_channels:get(d.channel_id)
if not channel then return warning(client, 'TextChannel', d.channel_id, 'WEBHOOKS_UPDATE') end
return client:emit('webhooksUpdate', channel)
end
return EventHandler

View File

@@ -0,0 +1,184 @@
local fs = require('fs')
local ffi = require('ffi')
local ssl = require('openssl')
local class = require('class')
local enums = require('enums')
local permission = enums.permission
local actionType = enums.actionType
local base64 = ssl.base64
local readFileSync = fs.readFileSync
local classes = class.classes
local isInstance = class.isInstance
local isObject = class.isObject
local insert = table.insert
local format = string.format
local Resolver = {}
local istype = ffi.istype
local int64_t = ffi.typeof('int64_t')
local uint64_t = ffi.typeof('uint64_t')
local function int(obj)
local t = type(obj)
if t == 'string' and tonumber(obj) then
return obj
elseif t == 'cdata' then
if istype(int64_t, obj) or istype(uint64_t, obj) then
return tostring(obj):match('%d*')
end
elseif t == 'number' then
return format('%i', obj)
elseif isInstance(obj, classes.Date) then
return obj:toSnowflake()
end
end
function Resolver.userId(obj)
if isObject(obj) then
if isInstance(obj, classes.User) then
return obj.id
elseif isInstance(obj, classes.Member) then
return obj.user.id
elseif isInstance(obj, classes.Message) then
return obj.author.id
elseif isInstance(obj, classes.Guild) then
return obj.ownerId
end
end
return int(obj)
end
function Resolver.messageId(obj)
if isInstance(obj, classes.Message) then
return obj.id
end
return int(obj)
end
function Resolver.channelId(obj)
if isInstance(obj, classes.Channel) then
return obj.id
end
return int(obj)
end
function Resolver.roleId(obj)
if isInstance(obj, classes.Role) then
return obj.id
end
return int(obj)
end
function Resolver.emojiId(obj)
if isInstance(obj, classes.Emoji) then
return obj.id
elseif isInstance(obj, classes.Reaction) then
return obj.emojiId
end
return tostring(obj)
end
function Resolver.guildId(obj)
if isInstance(obj, classes.Guild) then
return obj.id
end
return int(obj)
end
function Resolver.entryId(obj)
if isInstance(obj, classes.AuditLogEntry) then
return obj.id
end
return int(obj)
end
function Resolver.messageIds(objs)
local ret = {}
if isInstance(objs, classes.Iterable) then
for obj in objs:iter() do
insert(ret, Resolver.messageId(obj))
end
elseif type(objs) == 'table' then
for _, obj in pairs(objs) do
insert(ret, Resolver.messageId(obj))
end
end
return ret
end
function Resolver.roleIds(objs)
local ret = {}
if isInstance(objs, classes.Iterable) then
for obj in objs:iter() do
insert(ret, Resolver.roleId(obj))
end
elseif type(objs) == 'table' then
for _, obj in pairs(objs) do
insert(ret, Resolver.roleId(obj))
end
end
return ret
end
function Resolver.emoji(obj)
if isInstance(obj, classes.Emoji) then
return obj.hash
elseif isInstance(obj, classes.Reaction) then
return obj.emojiHash
end
return tostring(obj)
end
function Resolver.color(obj)
if isInstance(obj, classes.Color) then
return obj.value
end
return tonumber(obj)
end
function Resolver.permissions(obj)
if isInstance(obj, classes.Permissions) then
return obj.value
end
return tonumber(obj)
end
function Resolver.permission(obj)
local t = type(obj)
local n = nil
if t == 'string' then
n = permission[obj]
elseif t == 'number' then
n = permission(obj) and obj
end
return n
end
function Resolver.actionType(obj)
local t = type(obj)
local n = nil
if t == 'string' then
n = actionType[obj]
elseif t == 'number' then
n = actionType(obj) and obj
end
return n
end
function Resolver.base64(obj)
if type(obj) == 'string' then
if obj:find('data:.*;base64,') == 1 then
return obj
end
local data, err = readFileSync(obj)
if not data then
return nil, err
end
return 'data:;base64,' .. base64(data)
end
return nil
end
return Resolver

View File

@@ -0,0 +1,254 @@
local json = require('json')
local timer = require('timer')
local EventHandler = require('client/EventHandler')
local WebSocket = require('client/WebSocket')
local constants = require('constants')
local enums = require('enums')
local logLevel = enums.logLevel
local min, max, random = math.min, math.max, math.random
local null = json.null
local format = string.format
local sleep = timer.sleep
local setInterval, clearInterval = timer.setInterval, timer.clearInterval
local concat = table.concat
local wrap = coroutine.wrap
local ID_DELAY = constants.ID_DELAY
local DISPATCH = 0
local HEARTBEAT = 1
local IDENTIFY = 2
local STATUS_UPDATE = 3
local VOICE_STATE_UPDATE = 4
-- local VOICE_SERVER_PING = 5 -- TODO
local RESUME = 6
local RECONNECT = 7
local REQUEST_GUILD_MEMBERS = 8
local INVALID_SESSION = 9
local HELLO = 10
local HEARTBEAT_ACK = 11
local GUILD_SYNC = 12
local ignore = {
['CALL_DELETE'] = true,
['CHANNEL_PINS_ACK'] = true,
['GUILD_INTEGRATIONS_UPDATE'] = true,
['MESSAGE_ACK'] = true,
['PRESENCES_REPLACE'] = true,
['USER_SETTINGS_UPDATE'] = true,
['SESSIONS_REPLACE'] = true,
}
local Shard = require('class')('Shard', WebSocket)
function Shard:__init(id, client)
WebSocket.__init(self, client)
self._id = id
self._client = client
self._backoff = 1000
end
for name in pairs(logLevel) do
Shard[name] = function(self, fmt, ...)
local client = self._client
return client[name](client, format('Shard %i : %s', self._id, fmt), ...)
end
end
function Shard:__tostring()
return format('Shard: %i', self._id)
end
local function getReconnectTime(self, n, m)
return self._backoff * (n + random() * (m - n))
end
local function incrementReconnectTime(self)
self._backoff = min(self._backoff * 2, 60000)
end
local function decrementReconnectTime(self)
self._backoff = max(self._backoff / 2, 1000)
end
function Shard:handleDisconnect(url, path)
self._client:emit('shardDisconnect', self._id)
if self._reconnect then
self:info('Reconnecting...')
return self:connect(url, path)
elseif self._reconnect == nil and self._client._options.autoReconnect then
local backoff = getReconnectTime(self, 0.9, 1.1)
incrementReconnectTime(self)
self:info('Reconnecting after %i ms...', backoff)
sleep(backoff)
return self:connect(url, path)
end
end
function Shard:handlePayload(payload)
local client = self._client
local s = payload.s
local t = payload.t
local d = payload.d
local op = payload.op
if t ~= null then
self:debug('WebSocket OP %s : %s : %s', op, t, s)
else
self:debug('WebSocket OP %s', op)
end
if op == DISPATCH then
self._seq = s
if not ignore[t] then
EventHandler[t](d, client, self)
end
elseif op == HEARTBEAT then
self:heartbeat()
elseif op == RECONNECT then
self:warning('Discord has requested a reconnection')
self:disconnect(true)
elseif op == INVALID_SESSION then
local session_id = self._session_id
self._session_id = nil
if payload.d and session_id then
self:info('Session invalidated, resuming...')
self:resume()
else
self:info('Session invalidated, re-identifying...')
sleep(random(1000, 5000))
self:identify()
end
elseif op == HELLO then
self:info('Received HELLO (%s)', concat(d._trace, ', '))
self:startHeartbeat(d.heartbeat_interval)
if self._session_id then
self:resume()
else
self:identify()
end
elseif op == HEARTBEAT_ACK then
client:emit('heartbeat', self._id, self._sw.milliseconds)
elseif op then
self:warning('Unhandled WebSocket payload OP %i', op)
end
end
local function loop(self)
decrementReconnectTime(self)
return wrap(self.heartbeat)(self)
end
function Shard:startHeartbeat(interval)
if self._heartbeat then
clearInterval(self._heartbeat)
end
self._heartbeat = setInterval(interval, loop, self)
end
function Shard:stopHeartbeat()
if self._heartbeat then
clearInterval(self._heartbeat)
end
self._heartbeat = nil
end
function Shard:identifyWait()
if self:waitFor('READY', 1.5 * ID_DELAY) then
return sleep(ID_DELAY)
end
end
function Shard:heartbeat()
self._sw:reset()
return self:_send(HEARTBEAT, self._seq or json.null)
end
function Shard:identify()
local client = self._client
local mutex = client._mutex
local options = client._options
mutex:lock()
wrap(function()
self:identifyWait()
mutex:unlock()
end)()
self._seq = nil
self._session_id = nil
self._ready = false
self._loading = {guilds = {}, chunks = {}, syncs = {}}
return self:_send(IDENTIFY, {
token = client._token,
properties = {
['$os'] = jit.os,
['$browser'] = 'Discordia',
['$device'] = 'Discordia',
['$referrer'] = '',
['$referring_domain'] = '',
},
compress = options.compress,
large_threshold = options.largeThreshold,
shard = {self._id, client._total_shard_count},
presence = next(client._presence) and client._presence,
}, true)
end
function Shard:resume()
return self:_send(RESUME, {
token = self._client._token,
session_id = self._session_id,
seq = self._seq
})
end
function Shard:requestGuildMembers(id)
return self:_send(REQUEST_GUILD_MEMBERS, {
guild_id = id,
query = '',
limit = 0,
})
end
function Shard:updateStatus(presence)
return self:_send(STATUS_UPDATE, presence)
end
function Shard:updateVoice(guild_id, channel_id, self_mute, self_deaf)
return self:_send(VOICE_STATE_UPDATE, {
guild_id = guild_id,
channel_id = channel_id or null,
self_mute = self_mute or false,
self_deaf = self_deaf or false,
})
end
function Shard:syncGuilds(ids)
return self:_send(GUILD_SYNC, ids)
end
return Shard

View File

@@ -0,0 +1,122 @@
local json = require('json')
local miniz = require('miniz')
local Mutex = require('utils/Mutex')
local Emitter = require('utils/Emitter')
local Stopwatch = require('utils/Stopwatch')
local websocket = require('coro-websocket')
local constants = require('constants')
local inflate = miniz.inflate
local encode, decode, null = json.encode, json.decode, json.null
local ws_parseUrl, ws_connect = websocket.parseUrl, websocket.connect
local GATEWAY_DELAY = constants.GATEWAY_DELAY
local TEXT = 1
local BINARY = 2
local CLOSE = 8
local function connect(url, path)
local options = assert(ws_parseUrl(url))
options.pathname = path
return assert(ws_connect(options))
end
local WebSocket = require('class')('WebSocket', Emitter)
function WebSocket:__init(parent)
Emitter.__init(self)
self._parent = parent
self._mutex = Mutex()
self._sw = Stopwatch()
end
function WebSocket:connect(url, path)
local success, res, read, write = pcall(connect, url, path)
if success then
self._read = read
self._write = write
self._reconnect = nil
self:info('Connected to %s', url)
local parent = self._parent
for message in self._read do
local payload, str = self:parseMessage(message)
if not payload then break end
parent:emit('raw', str)
if self.handlePayload then -- virtual method
self:handlePayload(payload)
end
end
self:info('Disconnected')
else
self:error('Could not connect to %s (%s)', url, res) -- TODO: get new url?
end
self._read = nil
self._write = nil
self._identified = nil
if self.stopHeartbeat then -- virtual method
self:stopHeartbeat()
end
if self.handleDisconnect then -- virtual method
return self:handleDisconnect(url, path)
end
end
function WebSocket:parseMessage(message)
local opcode = message.opcode
local payload = message.payload
if opcode == TEXT then
return decode(payload, 1, null), payload
elseif opcode == BINARY then
payload = inflate(payload, 1)
return decode(payload, 1, null), payload
elseif opcode == CLOSE then
local code, i = ('>H'):unpack(payload)
local msg = #payload > i and payload:sub(i) or 'Connection closed'
self:warning('%i - %s', code, msg)
return nil
end
end
function WebSocket:_send(op, d, identify)
self._mutex:lock()
local success, err
if identify or self._session_id then
if self._write then
success, err = self._write {opcode = TEXT, payload = encode {op = op, d = d}}
else
success, err = false, 'Not connected to gateway'
end
else
success, err = false, 'Invalid session'
end
self._mutex:unlockAfter(GATEWAY_DELAY)
return success, err
end
function WebSocket:disconnect(reconnect)
if not self._write then return end
self._reconnect = not not reconnect
self._write()
self._read = nil
self._write = nil
self._session_id = nil
end
return WebSocket

View File

@@ -0,0 +1,17 @@
return {
CACHE_AGE = 3600, -- seconds
ID_DELAY = 5000, -- milliseconds
GATEWAY_DELAY = 500, -- milliseconds,
DISCORD_EPOCH = 1420070400000, -- milliseconds
GATEWAY_VERSION = 6,
DEFAULT_AVATARS = 5,
ZWSP = '\226\128\139',
NS_PER_US = 1000,
US_PER_MS = 1000,
MS_PER_S = 1000,
S_PER_MIN = 60,
MIN_PER_HOUR = 60,
HOUR_PER_DAY = 24,
DAY_PER_WEEK = 7,
GATEWAY_VERSION_VOICE = 3,
}

View File

@@ -0,0 +1,111 @@
--[=[
@c Activity
@d Represents a Discord user's presence data, either plain game or streaming presence or a rich presence.
]=]
local Container = require('containers/abstract/Container')
local Activity, get = require('class')('Activity', Container)
function Activity:__init(data, parent)
Container.__init(self, data, parent)
return self:_loadMore(data)
end
function Activity:_load(data)
Container._load(self, data)
return self:_loadMore(data)
end
function Activity:_loadMore(data)
local timestamps = data.timestamps
self._start = timestamps and timestamps.start
self._stop = timestamps and timestamps['end'] -- thanks discord
local assets = data.assets
self._small_text = assets and assets.small_text
self._large_text = assets and assets.large_text
self._small_image = assets and assets.small_image
self._large_image = assets and assets.large_image
local party = data.party
self._party_id = party and party.id
self._party_size = party and party.size and party.size[1]
self._party_max = party and party.size and party.size[2]
end
--[=[@p start number/nil The Unix timestamp for when this activity was started.]=]
function get.start(self)
return self._start
end
--[=[@p stop number/nil The Unix timestamp for when this activity was stopped.]=]
function get.stop(self)
return self._stop
end
--[=[@p name string/nil The game that the user is currently playing.]=]
function get.name(self)
return self._name
end
--[=[@p type number/nil The type of user's game status. See the `gameType`
enumeration for a human-readable representation.]=]
function get.type(self)
return self._type
end
--[=[@p url string/nil The URL that is set for a user's streaming game status.]=]
function get.url(self)
return self._url
end
--[=[@p applicationId string The application id controlling this activity.]=]
function get.applicationId(self)
return self._application_id
end
--[=[@p state string/nil string for the Rich Presence state section.]=]
function get.state(self)
return self._state
end
--[=[@p details string/nil string for the Rich Presence details section.]=]
function get.details(self)
return self._details
end
--[=[@p textSmall string/nil string for the Rich Presence small image text.]=]
function get.textSmall(self)
return self._small_text
end
--[=[@p textLarge string/nil string for the Rich Presence large image text.]=]
function get.textLarge(self)
return self._large_text
end
--[=[@p imageSmall string/nil URL for the Rich Presence small image.]=]
function get.imageSmall(self)
return self._small_image
end
--[=[@p imageLarge string/nil URL for the Rich Presence large image.]=]
function get.imageLarge(self)
return self._large_image
end
--[=[@p partyId string/nil Party id for this Rich Presence.]=]
function get.partyId(self)
return self._party_id
end
--[=[@p partySize number/nil Size of the Rich Presence party.]=]
function get.partySize(self)
return self._party_size
end
--[=[@p partyMax number/nil Max size for the Rich Presence party.]=]
function get.partyMax(self)
return self._party_max
end
return Activity

View File

@@ -0,0 +1,223 @@
--[=[
@c AuditLogEntry x Snowflake
@d Represents an entry made into a guild's audit log.
]=]
local Snowflake = require('containers/abstract/Snowflake')
local enums = require('enums')
local actionType = enums.actionType
local AuditLogEntry, get = require('class')('AuditLogEntry', Snowflake)
function AuditLogEntry:__init(data, parent)
Snowflake.__init(self, data, parent)
if data.changes then
for i, change in ipairs(data.changes) do
data.changes[change.key] = change
data.changes[i] = nil
change.key = nil
change.old = change.old_value
change.new = change.new_value
change.old_value = nil
change.new_value = nil
end
self._changes = data.changes
end
self._options = data.options
end
--[=[
@m getBeforeAfter
@r table
@r table
@d Returns two tables of the target's properties before the change, and after the change.
]=]
function AuditLogEntry:getBeforeAfter()
local before, after = {}, {}
for k, change in pairs(self._changes) do
before[k], after[k] = change.old, change.new
end
return before, after
end
local function unknown(self)
return nil, 'unknown audit log action type: ' .. self._action_type
end
local targets = setmetatable({
[actionType.guildUpdate] = function(self)
return self._parent
end,
[actionType.channelCreate] = function(self)
return self._parent:getChannel(self._target_id)
end,
[actionType.channelUpdate] = function(self)
return self._parent:getChannel(self._target_id)
end,
[actionType.channelDelete] = function(self)
return self._parent:getChannel(self._target_id)
end,
[actionType.channelOverwriteCreate] = function(self)
return self._parent:getChannel(self._target_id)
end,
[actionType.channelOverwriteUpdate] = function(self)
return self._parent:getChannel(self._target_id)
end,
[actionType.channelOverwriteDelete] = function(self)
return self._parent:getChannel(self._target_id)
end,
[actionType.memberKick] = function(self)
return self._parent._parent:getUser(self._target_id)
end,
[actionType.memberPrune] = function()
return nil
end,
[actionType.memberBanAdd] = function(self)
return self._parent._parent:getUser(self._target_id)
end,
[actionType.memberBanRemove] = function(self)
return self._parent._parent:getUser(self._target_id)
end,
[actionType.memberUpdate] = function(self)
return self._parent:getMember(self._target_id)
end,
[actionType.memberRoleUpdate] = function(self)
return self._parent:getMember(self._target_id)
end,
[actionType.roleCreate] = function(self)
return self._parent:getRole(self._target_id)
end,
[actionType.roleUpdate] = function(self)
return self._parent:getRole(self._target_id)
end,
[actionType.roleDelete] = function(self)
return self._parent:getRole(self._target_id)
end,
[actionType.inviteCreate] = function()
return nil
end,
[actionType.inviteUpdate] = function()
return nil
end,
[actionType.inviteDelete] = function()
return nil
end,
[actionType.webhookCreate] = function(self)
return self._parent._parent._webhooks:get(self._target_id)
end,
[actionType.webhookUpdate] = function(self)
return self._parent._parent._webhooks:get(self._target_id)
end,
[actionType.webhookDelete] = function(self)
return self._parent._parent._webhooks:get(self._target_id)
end,
[actionType.emojiCreate] = function(self)
return self._parent:getEmoji(self._target_id)
end,
[actionType.emojiUpdate] = function(self)
return self._parent:getEmoji(self._target_id)
end,
[actionType.emojiDelete] = function(self)
return self._parent:getEmoji(self._target_id)
end,
[actionType.messageDelete] = function(self)
return self._parent._parent:getUser(self._target_id)
end,
}, {__index = function() return unknown end})
--[=[
@m getTarget
@r *
@d Gets the target object of the affected entity. The returned object can be:
- [[Guild]]
- [[GuildChannel]]
- [[User]]
- [[Member]]
- [[Role]]
- [[Webhook]]
- [[Emoji]]
- nil
]=]
function AuditLogEntry:getTarget()
return targets[self._action_type](self)
end
--[=[
@m getUser
@r User
@d Gets the user who performed the changes.
]=]
function AuditLogEntry:getUser()
return self._parent._parent:getUser(self._user_id)
end
--[=[
@m getMember
@r Member
@d Gets the member object of the user who performed the changes.
]=]
function AuditLogEntry:getMember()
return self._parent:getMember(self._user_id)
end
--[=[@p changes table/nil A table of audit log change objects. The key represents
the property of the changed target and the value contains a table of `new` and
possibly `old`, representing the property's new and old value.]=]
function get.changes(self)
return self._changes
end
--[=[@p options table/nil A table of optional audit log information.]=]
function get.options(self)
return self._options
end
--[=[@p actionType number The action type. Use the `actionType `enumeration for a human-readable representation.]=]
function get.actionType(self)
return self._action_type
end
--[=[@p targetId string/nil The Snowflake ID of the affected entity. Will be `nil` for certain targets.]=]
function get.targetId(self)
return self._target_id
end
--[=[@p reason string/nil The reason provided by the user for the change.]=]
function get.reason(self)
return self._reason
end
--[=[@p guild Guild The guild in which this audit log entry was found.]=]
function get.guild(self)
return self._parent
end
return AuditLogEntry

View File

@@ -0,0 +1,51 @@
--[=[
@c Ban x Container
@d Represents a Discord guild ban. Essentially a combination of the banned user and
a reason explaining the ban, if one was provided.
]=]
local Container = require('containers/abstract/Container')
local Ban, get = require('class')('Ban', Container)
function Ban:__init(data, parent)
Container.__init(self, data, parent)
self._user = self.client._users:_insert(data.user)
end
--[=[
@m __hash
@r string
@d Returns `Ban.user.id`
]=]
function Ban:__hash()
return self._user._id
end
--[=[
@m delete
@r boolean
@d Deletes the ban object, essentially unbanning the corresponding user.
Equivalent to `Ban.guild:unbanUser(Ban.user)`.
]=]
function Ban:delete()
return self._parent:unbanUser(self._user)
end
--[=[@p reason string/nil The reason for the ban, if one was set. This should be from 1 to 512 characters
in length.]=]
function get.reason(self)
return self._reason
end
--[=[@p guild Guild The guild in which this ban object exists.]=]
function get.guild(self)
return self._parent
end
--[=[@p user User The user that this ban object represents.]=]
function get.user(self)
return self._user
end
return Ban

View File

@@ -0,0 +1,163 @@
--[=[
@c Emoji x Snowflake
@d Represents a custom emoji object usable in message content and reactions.
Standard unicode emojis do not have a class; they are just strings.
]=]
local Snowflake = require('containers/abstract/Snowflake')
local Resolver = require('client/Resolver')
local ArrayIterable = require('iterables/ArrayIterable')
local json = require('json')
local format = string.format
local Emoji, get = require('class')('Emoji', Snowflake)
function Emoji:__init(data, parent)
Snowflake.__init(self, data, parent)
self.client._emoji_map[self._id] = parent
return self:_loadMore(data)
end
function Emoji:_load(data)
Snowflake._load(self, data)
return self:_loadMore(data)
end
function Emoji:_loadMore(data)
if data.roles then
local roles = #data.roles > 0 and data.roles or nil
if self._roles then
self._roles._array = roles
else
self._roles_raw = roles
end
end
end
function Emoji:_modify(payload)
local data, err = self.client._api:modifyGuildEmoji(self._parent._id, self._id, payload)
if data then
self:_load(data)
return true
else
return false, err
end
end
--[=[
@m setName
@p name string
@r boolean
@d Sets the emoji's name. The name must be between 2 and 32 characters in length.
]=]
function Emoji:setName(name)
return self:_modify({name = name or json.null})
end
--[=[
@m setRoles
@p roles Role-ID-Resolvables
@r boolean
@d Sets the roles that can use the emoji.
]=]
function Emoji:setRoles(roles)
roles = Resolver.roleIds(roles)
return self:_modify({roles = roles or json.null})
end
--[=[
@m delete
@r boolean
@d Permanently deletes the emoji. This cannot be undone!
]=]
function Emoji:delete()
local data, err = self.client._api:deleteGuildEmoji(self._parent._id, self._id)
if data then
local cache = self._parent._emojis
if cache then
cache:_delete(self._id)
end
return true
else
return false, err
end
end
--[=[
@m hasRole
@p id Role-ID-Resolvable
@r boolean
@d Returns whether or not the provided role is allowed to use the emoji.
]=]
function Emoji:hasRole(id)
id = Resolver.roleId(id)
local roles = self._roles and self._roles._array or self._roles_raw
if roles then
for _, v in ipairs(roles) do
if v == id then
return true
end
end
end
return false
end
--[=[@p name string The name of the emoji.]=]
function get.name(self)
return self._name
end
--[=[@p guild Guild The guild in which the emoji exists.]=]
function get.guild(self)
return self._parent
end
--[=[@p mentionString string A string that, when included in a message content, may resolve as an emoji image
in the official Discord client.]=]
function get.mentionString(self)
local fmt = self._animated and '<a:%s>' or '<:%s>'
return format(fmt, self.hash)
end
--[=[@p url string The URL that can be used to view a full version of the emoji.]=]
function get.url(self)
local ext = self._animated and 'gif' or 'png'
return format('https://cdn.discordapp.com/emojis/%s.%s', self._id, ext)
end
--[=[@p managed boolean Whether this emoji is managed by an integration such as Twitch or YouTube.]=]
function get.managed(self)
return self._managed
end
--[=[@p requireColons boolean Whether this emoji requires colons to be used in the official Discord client.]=]
function get.requireColons(self)
return self._require_colons
end
--[=[@p hash string An iterable array of roles that may be required to use this emoji, generally
related to integration-managed emojis. Object order is not guaranteed.
]=]
function get.hash(self)
return self._name .. ':' .. self._id
end
--[=[@p animated boolean Whether this emoji is animated. (a .gif)]=]
function get.animated(self)
return self._animated
end
--[=[@p roles ArrayIterable An iterable of roles that have access to the emoji.]=]
function get.roles(self)
if not self._roles then
local roles = self._parent._roles
self._roles = ArrayIterable(self._roles_raw, function(id)
return roles:get(id)
end)
self._roles_raw = nil
end
return self._roles
end
return Emoji

View File

@@ -0,0 +1,117 @@
--[=[
@c GroupChannel x TextChannel
@d Represents a Discord group channel. Essentially a private channel that may have
more than one and up to ten recipients. This class should only be relevant to
user-accounts; bots cannot normally join group channels.
]=]
local json = require('json')
local TextChannel = require('containers/abstract/TextChannel')
local SecondaryCache = require('iterables/SecondaryCache')
local Resolver = require('client/Resolver')
local format = string.format
local GroupChannel, get = require('class')('GroupChannel', TextChannel)
function GroupChannel:__init(data, parent)
TextChannel.__init(self, data, parent)
self._recipients = SecondaryCache(data.recipients, self.client._users)
end
--[=[
@m setName
@p name string
@r boolean
@d Sets the channel's name. This must be between 1 and 100 characters in length.
]=]
function GroupChannel:setName(name)
return self:_modify({name = name or json.null})
end
--[=[
@m setIcon
@p icon Base64-Resolvable
@r boolean
@d Sets the channel's icon. To remove the icon, pass `nil`.
]=]
function GroupChannel:setIcon(icon)
icon = icon and Resolver.base64(icon)
return self:_modify({icon = icon or json.null})
end
--[=[
@m addRecipient
@p id User-ID-Resolvable
@r boolean
@d Adds a user to the channel.
]=]
function GroupChannel:addRecipient(id)
id = Resolver.userId(id)
local data, err = self.client._api:groupDMAddRecipient(self._id, id)
if data then
return true
else
return false, err
end
end
--[=[
@m removeRecipient
@p id User-ID-Resolvable
@r boolean
@d Removes a user from the channel.
]=]
function GroupChannel:removeRecipient(id)
id = Resolver.userId(id)
local data, err = self.client._api:groupDMRemoveRecipient(self._id, id)
if data then
return true
else
return false, err
end
end
--[=[
@m leave
@r boolean
@d Removes the client's user from the channel. If no users remain, the channel
is destroyed.
]=]
function GroupChannel:leave()
return self:_delete()
end
--[=[@p recipients SecondaryCache A secondary cache of users that are present in the channel.]=]
function get.recipients(self)
return self._recipients
end
--[=[@p name string The name of the channel.]=]
function get.name(self)
return self._name
end
--[=[@p ownerId string The Snowflake ID of the user that owns (created) the channel.]=]
function get.ownerId(self)
return self._owner_id
end
--[=[@p owner User/nil Equivalent to `GroupChannel.recipients:get(GroupChannel.ownerId)`.]=]
function get.owner(self)
return self._recipients:get(self._owner_id)
end
--[=[@p icon string/nil The hash for the channel's custom icon, if one is set.]=]
function get.icon(self)
return self._icon
end
--[=[@p iconURL string/nil The URL that can be used to view the channel's icon, if one is set.]=]
function get.iconURL(self)
local icon = self._icon
return icon and format('https://cdn.discordapp.com/channel-icons/%s/%s.png', self._id, icon)
end
return GroupChannel

View File

@@ -0,0 +1,812 @@
--[=[
@c Guild x Snowflake
@d Represents a Discord guild (or server). Guilds are a collection of members,
channels, and roles that represents one community.
]=]
local Cache = require('iterables/Cache')
local Role = require('containers/Role')
local Emoji = require('containers/Emoji')
local Invite = require('containers/Invite')
local Webhook = require('containers/Webhook')
local Ban = require('containers/Ban')
local Member = require('containers/Member')
local Resolver = require('client/Resolver')
local AuditLogEntry = require('containers/AuditLogEntry')
local GuildTextChannel = require('containers/GuildTextChannel')
local GuildVoiceChannel = require('containers/GuildVoiceChannel')
local GuildCategoryChannel = require('containers/GuildCategoryChannel')
local Snowflake = require('containers/abstract/Snowflake')
local json = require('json')
local enums = require('enums')
local channelType = enums.channelType
local floor = math.floor
local format = string.format
local Guild, get = require('class')('Guild', Snowflake)
function Guild:__init(data, parent)
Snowflake.__init(self, data, parent)
self._roles = Cache({}, Role, self)
self._emojis = Cache({}, Emoji, self)
self._members = Cache({}, Member, self)
self._text_channels = Cache({}, GuildTextChannel, self)
self._voice_channels = Cache({}, GuildVoiceChannel, self)
self._categories = Cache({}, GuildCategoryChannel, self)
self._voice_states = {}
if not data.unavailable then
return self:_makeAvailable(data)
end
end
function Guild:_makeAvailable(data)
self._roles:_load(data.roles)
self._emojis:_load(data.emojis)
self._features = data.features
if not data.channels then return end -- incomplete guild
local states = self._voice_states
for _, state in ipairs(data.voice_states) do
states[state.user_id] = state
end
local text_channels = self._text_channels
local voice_channels = self._voice_channels
local categories = self._categories
for _, channel in ipairs(data.channels) do
local t = channel.type
if t == channelType.text then
text_channels:_insert(channel)
elseif t == channelType.voice then
voice_channels:_insert(channel)
elseif t == channelType.category then
categories:_insert(channel)
end
end
return self:_loadMembers(data)
end
function Guild:_loadMembers(data)
local members = self._members
members:_load(data.members)
for _, presence in ipairs(data.presences) do
local member = members:get(presence.user.id)
if member then -- rogue presence check
member:_loadPresence(presence)
end
end
if self._large and self.client._options.cacheAllMembers then
return self:requestMembers()
end
end
function Guild:_modify(payload)
local data, err = self.client._api:modifyGuild(self._id, payload)
if data then
self:_load(data)
return true
else
return false, err
end
end
--[=[
@m requestMembers
@r boolean
@d Asynchronously loads all members for this guild. You do not need to call this
if the `cacheAllMembers` client option (and the `syncGuilds` option for
user-accounts) is enabled on start-up.
]=]
function Guild:requestMembers()
local shard = self.client._shards[self.shardId]
if not shard then
return false, 'Invalid shard'
end
if shard._loading then
shard._loading.chunks[self._id] = true
end
return shard:requestGuildMembers(self._id)
end
--[=[
@m sync
@r boolean
@d Asynchronously loads certain data and enables the receiving of certain events
for this guild. You do not need to call this if the `syncGuilds` client option
is enabled on start-up.
Note: This is only for user accounts. Bot accounts never need to sync guilds!
]=]
function Guild:sync()
local shard = self.client._shards[self.shardId]
if not shard then
return false, 'Invalid shard'
end
if shard._loading then
shard._loading.syncs[self._id] = true
end
return shard:syncGuilds({self._id})
end
--[=[
@m getMember
@p id User-ID-Resolvable
@r Member
@d Gets a member object by ID. If the object is already cached, then the cached
object will be returned; otherwise, an HTTP request is made.
]=]
function Guild:getMember(id)
id = Resolver.userId(id)
local member = self._members:get(id)
if member then
return member
else
local data, err = self.client._api:getGuildMember(self._id, id)
if data then
return self._members:_insert(data)
else
return nil, err
end
end
end
--[=[
@m getRole
@p id Role-ID-Resolvable
@r Role
@d Gets a role object by ID.
]=]
function Guild:getRole(id)
id = Resolver.roleId(id)
return self._roles:get(id)
end
--[=[
@m getEmoji
@p id Emoji-ID-Resolvable
@r Emoji
@d Gets a emoji object by ID.
]=]
function Guild:getEmoji(id)
id = Resolver.emojiId(id)
return self._emojis:get(id)
end
--[=[
@m getChannel
@p id Channel-ID-Resolvable
@r GuildChannel
@d Gets a text, voice, or category channel object by ID.
]=]
function Guild:getChannel(id)
id = Resolver.channelId(id)
return self._text_channels:get(id) or self._voice_channels:get(id) or self._categories:get(id)
end
--[=[
@m createTextChannel
@p name string
@r GuildTextChannel
@d Creates a new text channel in this guild. The name must be between 2 and 100
characters in length.
]=]
function Guild:createTextChannel(name)
local data, err = self.client._api:createGuildChannel(self._id, {name = name, type = channelType.text})
if data then
return self._text_channels:_insert(data)
else
return nil, err
end
end
--[=[
@m createVoiceChannel
@p name string
@r GuildVoiceChannel
@d Creates a new voice channel in this guild. The name must be between 2 and 100
characters in length.
]=]
function Guild:createVoiceChannel(name)
local data, err = self.client._api:createGuildChannel(self._id, {name = name, type = channelType.voice})
if data then
return self._voice_channels:_insert(data)
else
return nil, err
end
end
--[=[
@m createCategory
@p name string
@r GuildCategoryChannel
@d Creates a channel category in this guild. The name must be between 2 and 100
characters in length.
]=]
function Guild:createCategory(name)
local data, err = self.client._api:createGuildChannel(self._id, {name = name, type = channelType.category})
if data then
return self._categories:_insert(data)
else
return nil, err
end
end
--[=[
@m createRole
@p name string
@r Role
@d Creates a new role in this guild. The name must be between 1 and 100 characters
in length.
]=]
function Guild:createRole(name)
local data, err = self.client._api:createGuildRole(self._id, {name = name})
if data then
return self._roles:_insert(data)
else
return nil, err
end
end
--[=[
@m createEmoji
@p name string
@p image Base64-Resolvable
@r Emoji
@d Creates a new emoji in this guild. The name must be between 2 and 32 characters
in length. The image must not be over 256kb, any higher will return a 400 Bad Request
]=]
function Guild:createEmoji(name, image)
image = Resolver.base64(image)
local data, err = self.client._api:createGuildEmoji(self._id, {name = name, image = image})
if data then
return self._emojis:_insert(data)
else
return nil, err
end
end
--[=[
@m setName
@p name string
@r boolean
@d Sets the guilds name. This must be between 2 and 100 characters in length.
]=]
function Guild:setName(name)
return self:_modify({name = name or json.null})
end
--[=[
@m setRegion
@p region string
@r boolean
@d Sets the guild's voice region (eg: `us-east`). See `listVoiceRegions` for a list
of acceptable regions.
]=]
function Guild:setRegion(region)
return self:_modify({region = region or json.null})
end
--[=[
@m setVerificationLevel
@p verification_level number
@r boolean
@d Sets the guild's verification level setting. See the `verificationLevel`
enumeration for acceptable values.
]=]
function Guild:setVerificationLevel(verification_level)
return self:_modify({verification_level = verification_level or json.null})
end
--[=[
@m setNotificationSetting
@p default_message_notifications number
@r boolean
@d Sets the guild's default notification setting. See the `notficationSetting`
enumeration for acceptable values.
]=]
function Guild:setNotificationSetting(default_message_notifications)
return self:_modify({default_message_notifications = default_message_notifications or json.null})
end
--[=[
@m setExplicitContentSetting
@p explicit_content_filter number
@r boolean
@d Sets the guild's explicit content level setting. See the `explicitContentLevel`
enumeration for acceptable values.
]=]
function Guild:setExplicitContentSetting(explicit_content_filter)
return self:_modify({explicit_content_filter = explicit_content_filter or json.null})
end
--[=[
@m setAFKTimeout
@p afk_timeout number
@r number
@d Sets the guild's AFK timeout in seconds.
]=]
function Guild:setAFKTimeout(afk_timeout)
return self:_modify({afk_timeout = afk_timeout or json.null})
end
--[=[
@m setAFKChannel
@p id Channel-ID-Resolvable
@r boolean
@d Sets the guild's AFK channel.
]=]
function Guild:setAFKChannel(id)
id = id and Resolver.channelId(id)
return self:_modify({afk_channel_id = id or json.null})
end
--[=[
@m setSystemChannel
@p id Channel-Id-Resolvable
@r boolean
@d Transfers ownership of the guild to another user. Only the current guild owner
can do this.
]=]
function Guild:setSystemChannel(id)
id = id and Resolver.channelId(id)
return self:_modify({system_channel_id = id or json.null})
end
--[=[
@m setOwner
@p id User-ID-Resolvable
@r boolean
@d Transfers ownership of the guild to another user. Only the current guild owner
can do this.
]=]
function Guild:setOwner(id)
id = id and Resolver.userId(id)
return self:_modify({owner_id = id or json.null})
end
--[=[
@m setIcon
@p icon Base64-Resolvable
@r boolean
@d Sets the guild's icon. To remove the icon, pass `nil`.
]=]
function Guild:setIcon(icon)
icon = icon and Resolver.base64(icon)
return self:_modify({icon = icon or json.null})
end
--[=[
@m setSplash
@p splash Base64-Resolvable
@r boolean
@d Sets the guild's splash. To remove the splash, pass `nil`.
]=]
function Guild:setSplash(splash)
splash = splash and Resolver.base64(splash)
return self:_modify({splash = splash or json.null})
end
--[=[
@m getPruneCount
@op days number
@r number
@d Returns the number of members that would be pruned from the guild if a prune
were to be executed.
]=]
function Guild:getPruneCount(days)
local data, err = self.client._api:getGuildPruneCount(self._id, days and {days = days} or nil)
if data then
return data.pruned
else
return nil, err
end
end
--[=[
@m pruneMembers
@op days number
@r number
@d Prunes (removes) inactive, roleless members from the guild.
]=]
function Guild:pruneMembers(days)
local data, err = self.client._api:beginGuildPrune(self._id, nil, days and {days = days} or nil)
if data then
return data.pruned
else
return nil, err
end
end
--[=[
@m getBans
@r Cache
@d Returns a newly constructed cache of all ban objects for the guild. The
cache is not automatically updated via gateway events, but the internally
referenced user objects may be updated. You must call this method again to
guarantee that the objects are up to date.
]=]
function Guild:getBans()
local data, err = self.client._api:getGuildBans(self._id)
if data then
return Cache(data, Ban, self)
else
return nil, err
end
end
--[=[
@m getBan
@p id User-ID-Resolvable
@r Ban
@d This will return a Ban object for a giver user if that user is banned
from the guild; otherwise, `nil` is returned.
]=]
function Guild:getBan(id)
id = Resolver.userId(id)
local data, err = self.client._api:getGuildBan(self._id, id)
if data then
return Ban(data, self._parent)
else
return nil, err
end
end
--[=[
@m getInvites
@r Cache
@d Returns a newly constructed cache of all invite objects for the guild. The
cache and its objects are not automatically updated via gateway events. You must
call this method again to get the updated objects.
]=]
function Guild:getInvites()
local data, err = self.client._api:getGuildInvites(self._id)
if data then
return Cache(data, Invite, self.client)
else
return nil, err
end
end
--[=[
@m getAuditLogs
@op query table
@r Cache
@d Returns a newly constructed cache of audit log entry objects for the guild. The
cache and its objects are not automatically updated via gateway events. You must
call this method again to get the updated objects.
- query.limit: number
- query.user: UserId Resolvable
- query.before: EntryId Resolvable
- query.type: ActionType Resolvable
]=]
function Guild:getAuditLogs(query)
if type(query) == 'table' then
query = {
limit = query.limit,
user_id = Resolver.userId(query.user),
before = Resolver.entryId(query.before),
action_type = Resolver.actionType(query.type),
}
end
local data, err = self.client._api:getGuildAuditLog(self._id, query)
if data then
self.client._users:_load(data.users)
self.client._webhooks:_load(data.webhooks)
return Cache(data.audit_log_entries, AuditLogEntry, self)
else
return nil, err
end
end
--[=[
@m getWebhooks
@r Cache
@d Returns a newly constructed cache of all webhook objects for the guild. The
cache and its objects are not automatically updated via gateway events. You must
call this method again to get the updated objects.
]=]
function Guild:getWebhooks()
local data, err = self.client._api:getGuildWebhooks(self._id)
if data then
return Cache(data, Webhook, self.client)
else
return nil, err
end
end
--[=[
@m listVoiceRegions
@r table
@d Returns a raw data table that contains a list of available voice regions for
this guild, as provided by Discord, with no additional parsing.
]=]
function Guild:listVoiceRegions()
return self.client._api:getGuildVoiceRegions(self._id)
end
--[=[
@m leave
@r boolean
@d Removes the current user from the guild.
]=]
function Guild:leave()
local data, err = self.client._api:leaveGuild(self._id)
if data then
return true
else
return false, err
end
end
--[=[
@m delete
@r boolean
@d Permanently deletes the guild. This cannot be undone!
]=]
function Guild:delete()
local data, err = self.client._api:deleteGuild(self._id)
if data then
local cache = self._parent._guilds
if cache then
cache:_delete(self._id)
end
return true
else
return false, err
end
end
--[=[
@m kickUser
@p id User-ID-Resolvable
@op reason string
@r boolean
@d Kicks a user/member from the guild with an optional reason.
]=]
function Guild:kickUser(id, reason)
id = Resolver.userId(id)
local query = reason and {reason = reason}
local data, err = self.client._api:removeGuildMember(self._id, id, query)
if data then
return true
else
return false, err
end
end
--[=[
@m banUser
@p id User-ID-Resolvable
@op reason string
@op days number
@r boolean
@d Bans a user/member from the guild with an optional reason. The `days` parameter
is the number of days to consider when purging messages, up to 7.
]=]
function Guild:banUser(id, reason, days)
local query = reason and {reason = reason}
if days then
query = query or {}
query['delete-message-days'] = days
end
id = Resolver.userId(id)
local data, err = self.client._api:createGuildBan(self._id, id, query)
if data then
return true
else
return false, err
end
end
--[=[
@m unbanUser
@p id User-ID-Resolvable
@op reason string
@r boolean
@d Unbans a user/member from the guild with an optional reason.
]=]
function Guild:unbanUser(id, reason)
id = Resolver.userId(id)
local query = reason and {reason = reason}
local data, err = self.client._api:removeGuildBan(self._id, id, query)
if data then
return true
else
return false, err
end
end
--[=[@p shardId number The ID of the shard on which this guild is served. If only one shard is in
operation, then this will always be 0.]=]
function get.shardId(self)
return floor(self._id / 2^22) % self.client._total_shard_count
end
--[=[@p name string The guild's name. This should be between 2 and 100 characters in length.]=]
function get.name(self)
return self._name
end
--[=[@p icon string/nil The hash for the guild's custom icon, if one is set.]=]
function get.icon(self)
return self._icon
end
--[=[@p iconURL string/nil The URL that can be used to view the guild's icon, if one is set.]=]
function get.iconURL(self)
local icon = self._icon
return icon and format('https://cdn.discordapp.com/icons/%s/%s.png', self._id, icon)
end
--[=[@p splash string/nil The hash for the guild's custom splash image, if one is set. Only partnered
guilds may have this.]=]
function get.splash(self)
return self._splash
end
--[=[@p splashURL string/nil The URL that can be used to view the guild's custom splash image, if one is set.
Only partnered guilds may have this.]=]
function get.splashURL(self)
local splash = self._splash
return splash and format('https://cdn.discordapp.com/splashs/%s/%s.png', self._id, splash)
end
--[=[@p large boolean Whether the guild has an arbitrarily large amount of members. Guilds that are
"large" will not initialize with all members.]=]
function get.large(self)
return self._large
end
--[=[@p lazy boolean Whether the guild follows rules for the lazy-loading of client data.]=]
function get.lazy(self)
return self._lazy
end
--[=[@p region string The voice region that is used for all voice connections in the guild.]=]
function get.region(self)
return self._region
end
--[=[@p mfaLevel number The guild's multi-factor (or two-factor) verification level setting. A value of
0 indicates that MFA is not required; a value of 1 indicates that MFA is
required for administrative actions.]=]
function get.mfaLevel(self)
return self._mfa_level
end
--[=[@p joinedAt string The date and time at which the current user joined the guild, represented as
an ISO 8601 string plus microseconds when available.]=]
function get.joinedAt(self)
return self._joined_at
end
--[=[@p afkTimeout number The guild's voice AFK timeout in seconds.]=]
function get.afkTimeout(self)
return self._afk_timeout
end
--[=[@p unavailable boolean Whether the guild is unavailable. If the guild is unavailable, then no property
is guaranteed to exist except for this one and the guild's ID.]=]
function get.unavailable(self)
return self._unavailable or false
end
--[=[@p totalMemberCount number The total number of members that belong to this guild. This should always be
greater than or equal to the total number of cached members.]=]
function get.totalMemberCount(self)
return self._member_count
end
--[=[@p verificationLevel number The guild's verification level setting. See the `verificationLevel`
enumeration for a human-readable representation.]=]
function get.verificationLevel(self)
return self._verification_level
end
--[=[@p notificationSetting number The guild's default notification setting. See the `notficationSetting`
enumeration for a human-readable representation.]=]
function get.notificationSetting(self)
return self._default_message_notifications
end
--[=[@p explicitContentSetting number The guild's explicit content level setting. See the `explicitContentLevel`
enumeration for a human-readable representation.]=]
function get.explicitContentSetting(self)
return self._explicit_content_filter
end
--[=[@p features table Raw table of VIP features that are enabled for the guild.]=]
function get.features(self)
return self._features
end
--[=[@p me Member/nil Equivalent to `Guild.members:get(Guild.client.user.id)`.]=]
function get.me(self)
return self._members:get(self.client._user._id)
end
--[=[@p owner Member/nil Equivalent to `Guild.members:get(Guild.ownerId)`.]=]
function get.owner(self)
return self._members:get(self._owner_id)
end
--[=[@p ownerId string The Snowflake ID of the guild member that owns the guild.]=]
function get.ownerId(self)
return self._owner_id
end
--[=[@p afkChannelId string/nil The Snowflake ID of the channel that is used for AFK members, if one is set.]=]
function get.afkChannelId(self)
return self._afk_channel_id
end
--[=[@p afkChannel GuildVoiceChannel/nil Equivalent to `Guild.voiceChannels:get(Guild.afkChannelId)`.]=]
function get.afkChannel(self)
return self._voice_channels:get(self._afk_channel_id)
end
--[=[@p systemChannelId string/nil The channel id where Discord's join messages will be displayed]=]
function get.systemChannelId(self)
return self._system_channel_id
end
--[=[@p systemChannel GuildTextChannel/nil The channel where Discord's join messages will be displayed]=]
function get.systemChannel(self)
return self._text_channels:get(self._system_channel_id)
end
--[=[@p defaultRole Role Equivalent to `Guild.roles:get(Guild.id)`.]=]
function get.defaultRole(self)
return self._roles:get(self._id)
end
--[=[@p connection VoiceConnection/nil The VoiceConnection for this guild if one exists.]=]
function get.connection(self)
return self._connection
end
--[=[@p roles Cache An iterable cache of all roles that exist in this guild. This includes the
default everyone role.]=]
function get.roles(self)
return self._roles
end
--[=[@p emojis Cache An iterable cache of all emojis that exist in this guild. Note that standard
unicode emojis are not found here; only custom emojis.]=]
function get.emojis(self)
return self._emojis
end
--[=[@p members Cache An iterable cache of all members that exist in this guild and have been
already loaded. If the `cacheAllMembers` client option (and the `syncGuilds`
option for user-accounts) is enabled on start-up, then all members will be
cached. Otherwise, offline members may not be cached. To access a member that
may exist, but is not cached, use `Guild:getMember`.]=]
function get.members(self)
return self._members
end
--[=[@p textChannels Cache An iterable cache of all text channels that exist in this guild.]=]
function get.textChannels(self)
return self._text_channels
end
--[=[@p voiceChannels Cache An iterable cache of all voice channels that exist in this guild.]=]
function get.voiceChannels(self)
return self._voice_channels
end
--[=[@p categories Cache An iterable cache of all channel categories that exist in this guild.]=]
function get.categories(self)
return self._categories
end
return Guild

View File

@@ -0,0 +1,81 @@
--[=[
@c GuildCategoryChannel x GuildChannel
@d Represents a channel category in a Discord guild, used to organize individual
text or voice channels in that guild.
]=]
local GuildChannel = require('containers/abstract/GuildChannel')
local FilteredIterable = require('iterables/FilteredIterable')
local enums = require('enums')
local channelType = enums.channelType
local GuildCategoryChannel, get = require('class')('GuildCategoryChannel', GuildChannel)
function GuildCategoryChannel:__init(data, parent)
GuildChannel.__init(self, data, parent)
end
--[=[
@m createTextChannel
@p name string
@r GuildTextChannel
@d Creates a new GuildTextChannel with this category as it's parent. `Guild:createTextChannel(name)`
]=]
function GuildCategoryChannel:createTextChannel(name)
local guild = self._parent
local data, err = guild.client._api:createGuildChannel(guild._id, {
name = name,
type = channelType.text,
parent_id = self._id
})
if data then
return guild._text_channels:_insert(data)
else
return nil, err
end
end
--[=[
@m createVoiceChannel
@p name string
@r GuildVoiceChannel
@d Creates a new GuildVoiceChannel with this category as it's parent. Similar to `Guild:createVoiceChannel(name)`
]=]
function GuildCategoryChannel:createVoiceChannel(name)
local guild = self._parent
local data, err = guild.client._api:createGuildChannel(guild._id, {
name = name,
type = channelType.voice,
parent_id = self._id
})
if data then
return guild._voice_channels:_insert(data)
else
return nil, err
end
end
--[=[@p textChannels FilteredIterable Returns all textChannels in the Category]=]
function get.textChannels(self)
if not self._text_channels then
local id = self._id
self._text_channels = FilteredIterable(self._parent._text_channels, function(c)
return c._parent_id == id
end)
end
return self._text_channels
end
--[=[@p voiceChannels FilteredIterable Returns all voiceChannels in the Category]=]
function get.voiceChannels(self)
if not self._voice_channels then
local id = self._id
self._voice_channels = FilteredIterable(self._parent._voice_channels, function(c)
return c._parent_id == id
end)
end
return self._voice_channels
end
return GuildCategoryChannel

View File

@@ -0,0 +1,152 @@
--[=[
@c GuildTextChannel x GuildChannel x TextChannel
@d Represents a text channel in a Discord guild, where guild members and webhooks
can send and receive messages.
]=]
local json = require('json')
local GuildChannel = require('containers/abstract/GuildChannel')
local TextChannel = require('containers/abstract/TextChannel')
local FilteredIterable = require('iterables/FilteredIterable')
local Webhook = require('containers/Webhook')
local Cache = require('iterables/Cache')
local Resolver = require('client/Resolver')
local GuildTextChannel, get = require('class')('GuildTextChannel', GuildChannel, TextChannel)
function GuildTextChannel:__init(data, parent)
GuildChannel.__init(self, data, parent)
TextChannel.__init(self, data, parent)
end
function GuildTextChannel:_load(data)
GuildChannel._load(self, data)
TextChannel._load(self, data)
end
--[=[
@m createWebhook
@p name string
@r Webhook
@d Creates a webhook for this channel. The name must be between 2 and 32 characters
in length.
]=]
function GuildTextChannel:createWebhook(name)
local data, err = self.client._api:createWebhook(self._id, {name = name})
if data then
return Webhook(data, self.client)
else
return nil, err
end
end
--[=[
@m getWebhooks
@r Cache
@d Returns a newly constructed cache of all webhook objects for the channel. The
cache and its objects are not automatically updated via gateway events. You must
call this method again to get the updated objects.
]=]
function GuildTextChannel:getWebhooks()
local data, err = self.client._api:getChannelWebhooks(self._id)
if data then
return Cache(data, Webhook, self.client)
else
return nil, err
end
end
--[=[
@m bulkDelete
@p messages Message-ID-Resolvables
@r boolean
@d Bulk deletes multiple messages, from 2 to 100, from the channel.
]=]
function GuildTextChannel:bulkDelete(messages)
messages = Resolver.messageIds(messages)
local data, err
if #messages == 1 then
data, err = self.client._api:deleteMessage(self._id, messages[1])
else
data, err = self.client._api:bulkDeleteMessages(self._id, {messages = messages})
end
if data then
return true
else
return false, err
end
end
--[=[
@m setTopic
@p topic string
@r boolean
@d Sets the channel's topic. This must be between 1 and 1024 characters. Pass `nil`
to remove the topic.
]=]
function GuildTextChannel:setTopic(topic)
return self:_modify({topic = topic or json.null})
end
--[=[
@m setRateLimit
@p limit number
@r boolean
@d Sets the channel's slowmode rate limit in seconds. This must be between 0 and 120.
Passing 0 or `nil` will clear the limit.
]=]
function GuildTextChannel:setRateLimit(limit)
return self:_modify({rate_limit_per_user = limit or json.null})
end
--[=[
@m enableNSFW
@r boolean
@d Enables the NSFW setting for the channel. NSFW channels are hidden from users
until the user explicitly requests to view them.
]=]
function GuildTextChannel:enableNSFW()
return self:_modify({nsfw = true})
end
--[=[
@m disableNSFW
@r boolean
@d Disables the NSFW setting for the channel. NSFW channels are hidden from users
until the user explicitly requests to view them.
]=]
function GuildTextChannel:disableNSFW()
return self:_modify({nsfw = false})
end
--[=[@p topic string/nil The channel's topic. This should be between 1 and 1024 characters.]=]
function get.topic(self)
return self._topic
end
--[=[@p nsfw boolean Whether this channel is marked as NSFW (not safe for work).]=]
function get.nsfw(self)
return self._nsfw or false
end
--[=[@p rateLimit number Slowmode rate limit per guild member.]=]
function get.rateLimit(self)
return self._rate_limit_per_user or 0
end
--[=[@p members FilteredIterable A filtered iterable of guild members that have
permission to read this channel. If you want to check whether a specific member
has permission to read this channel, it would be better to get the member object
elsewhere and use `Member:hasPermission` rather than check whether the member
exists here.]=]
function get.members(self)
if not self._members then
self._members = FilteredIterable(self._parent._members, function(m)
return m:hasPermission(self, 'readMessages')
end)
end
return self._members
end
return GuildTextChannel

View File

@@ -0,0 +1,134 @@
--[=[
@c GuildVoiceChannel x GuildChannel
@d Represents a voice channel in a Discord guild, where guild members can connect
and communicate via voice chat.
]=]
local json = require('json')
local GuildChannel = require('containers/abstract/GuildChannel')
local VoiceConnection = require('voice/VoiceConnection')
local TableIterable = require('iterables/TableIterable')
local GuildVoiceChannel, get = require('class')('GuildVoiceChannel', GuildChannel)
function GuildVoiceChannel:__init(data, parent)
GuildChannel.__init(self, data, parent)
end
--[=[
@m setBitrate
@p bitrate number
@r boolean
@d Sets the channel's audio bitrate in bits per second (bps). This must be between
8000 and 96000 (or 128000 for partnered servers). If `nil` is passed, the
default is set, which is 64000.
]=]
function GuildVoiceChannel:setBitrate(bitrate)
return self:_modify({bitrate = bitrate or json.null})
end
--[=[
@m setUserLimit
@p user_limit number
@r boolean
@d Sets the channel's user limit. This must be between 0 and 99 (where 0 is
unlimited). If `nil` is passed, the default is set, which is 0.
]=]
function GuildVoiceChannel:setUserLimit(user_limit)
return self:_modify({user_limit = user_limit or json.null})
end
--[=[
@m join
@r VoiceConnection
@d Join this channel and form a connection to the Voice Gateway.
]=]
function GuildVoiceChannel:join()
local success, err
local connection = self._connection
if connection then
if connection._ready then
return connection
end
else
local guild = self._parent
local client = guild._parent
success, err = client._shards[guild.shardId]:updateVoice(guild._id, self._id)
if not success then
return nil, err
end
connection = guild._connection
if not connection then
connection = VoiceConnection(self)
guild._connection = connection
end
self._connection = connection
end
success, err = connection:_await()
if success then
return connection
else
return nil, err
end
end
--[=[
@m leave
@r boolean
@d Leave this channel if there is an existing voice connection to it.
Equivalent to GuildVoiceChannel.connection:close()
]=]
function GuildVoiceChannel:leave()
if self._connection then
return self._connection:close()
else
return false, 'No voice connection exists for this channel'
end
end
--[=[@p bitrate number The channel's bitrate in bits per second (bps). This should be between 8000 and
96000 (or 128000 for partnered servers).]=]
function get.bitrate(self)
return self._bitrate
end
--[=[@p userLimit number The amount of users allowed to be in this channel.
Users with `moveMembers` permission ignore this limit.]=]
function get.userLimit(self)
return self._user_limit
end
--[=[@p connectedMembers TableIterable The channel's user limit. This should between 0 and 99 (where 0 is unlimited).]=]
function get.connectedMembers(self)
if not self._connected_members then
local id = self._id
local members = self._parent._members
self._connected_members = TableIterable(self._parent._voice_states, function(state)
return state.channel_id == id and members:get(state.user_id)
end)
end
return self._connected_members
end
--[=[@p connection VoiceConnection/nil The VoiceConnection for this channel if one exists.]=]
function get.connection(self)
return self._connection
end
return GuildVoiceChannel

View File

@@ -0,0 +1,162 @@
--[=[
@c Invite x Container
@d Represents an invitation to a Discord guild channel. Invites can be used to join
a guild, though they are not always permanent.
]=]
local Container = require('containers/abstract/Container')
local json = require('json')
local format = string.format
local null = json.null
local function load(v)
return v ~= null and v or nil
end
local Invite, get = require('class')('Invite', Container)
function Invite:__init(data, parent)
Container.__init(self, data, parent)
self._guild_id = load(data.guild.id)
self._channel_id = load(data.channel.id)
self._guild_name = load(data.guild.name)
self._guild_icon = load(data.guild.icon)
self._guild_splash = load(data.guild.splash)
self._channel_name = load(data.channel.name)
self._channel_type = load(data.channel.type)
if data.inviter then
self._inviter = self.client._users:_insert(data.inviter)
end
end
--[=[
@m __hash
@r string
@d Returns `Invite.code`
]=]
function Invite:__hash()
return self._code
end
--[=[
@m delete
@r boolean
@d Permanently deletes the invite. This cannot be undone!
]=]
function Invite:delete()
local data, err = self.client._api:deleteInvite(self._code)
if data then
return true
else
return false, err
end
end
--[=[@p code string The invite's code which can be used to identify the invite.]=]
function get.code(self)
return self._code
end
--[=[@p guildId string The Snowflake ID of the guild to which this invite belongs.]=]
function get.guildId(self)
return self._guild_id
end
--[=[@p guildName string The name of the guild to which this invite belongs.]=]
function get.guildName(self)
return self._guild_name
end
--[=[@p channelId string The Snowflake ID of the channel to which this belongs.]=]
function get.channelId(self)
return self._channel_id
end
--[=[@p channelName string The name of the channel to which this invite belongs.]=]
function get.channelName(self)
return self._channel_name
end
--[=[@p channelType number The type of the channel to which this invite belongs. Use the `channelType`
enumeration for a human-readable representation.]=]
function get.channelType(self)
return self._channel_type
end
--[=[@p guildIcon string/nil The hash for the guild's custom icon, if one is set.]=]
function get.guildIcon(self)
return self._guild_icon
end
--[=[@p guildSplash string/nil The hash for the guild's custom splash, if one is set.]=]
function get.guildSplash(self)
return self._guild_splash
end
--[=[@p guildIconURL string/nil The URL that can be used to view the guild's icon, if one is set.]=]
function get.guildIconURL(self)
local icon = self._guild_icon
return icon and format('https://cdn.discordapp.com/icons/%s/%s.png', self._guild_id, icon) or nil
end
--[=[@p guildSplashURL string/nil The URL that can be used to view the guild's splash, if one is set.]=]
function get.guildSplashURL(self)
local splash = self._guild_splash
return splash and format('https://cdn.discordapp.com/splashs/%s/%s.png', self._guild_id, splash) or nil
end
--[=[@p inviter User/nil The object of the user that created the invite. This will not exist if the
invite is a guild widget or a vanity invite.]=]
function get.inviter(self)
return self._inviter
end
--[=[@p uses number/nil How many times this invite has been used. This will not exist if the invite is
accessed via `Client:getInvite`.]=]
function get.uses(self)
return self._uses
end
--[=[@p maxUses number/nil The maximum amount of times this invite can be used. This will not exist if the
invite is accessed via `Client:getInvite`.]=]
function get.maxUses(self)
return self._max_uses
end
--[=[@p maxAge number/nil How long, in seconds, this invite lasts before it expires. This will not exist
if the invite is accessed via `Client:getInvite`.]=]
function get.maxAge(self)
return self._max_age
end
--[=[@p temporary boolean/nil Whether the invite grants temporary membership. This will not exist if the
invite is accessed via `Client:getInvite`.]=]
function get.temporary(self)
return self._temporary
end
--[=[@p createdAt string The date and time at which the invite was created, represented as an ISO 8601
string plus microseconds when available. This will not exist if the invite is
accessed via `Client:getInvite`.]=]
function get.createdAt(self)
return self._created_at
end
--[=[@p revoked boolean/nil Whether the invite has been revoked. This will not exist if the invite is
accessed via `Client:getInvite`.]=]
function get.revoked(self)
return self._revoked
end
--[=[@p approximatePresenceCount number/nil The approximate count of online members.]=]
function get.approximatePresenceCount(self)
return self._approximate_presence_count
end
--[=[@p approximateMemberCount number/nil The approximate count of all members.]=]
function get.approximateMemberCount(self)
return self._approximate_member_count
end
return Invite

View File

@@ -0,0 +1,517 @@
--[=[
@c Member x UserPresence
@d Represents a Discord guild member. Though one user may be a member in more than
one guild, each presence is represented by a different member object associated
with that guild.
]=]
local enums = require('enums')
local class = require('class')
local UserPresence = require('containers/abstract/UserPresence')
local ArrayIterable = require('iterables/ArrayIterable')
local Color = require('utils/Color')
local Resolver = require('client/Resolver')
local GuildChannel = require('containers/abstract/GuildChannel')
local Permissions = require('utils/Permissions')
local insert, remove, sort = table.insert, table.remove, table.sort
local band, bor, bnot = bit.band, bit.bor, bit.bnot
local isInstance = class.isInstance
local permission = enums.permission
local Member, get = class('Member', UserPresence)
function Member:__init(data, parent)
UserPresence.__init(self, data, parent)
return self:_loadMore(data)
end
function Member:_load(data)
UserPresence._load(self, data)
return self:_loadMore(data)
end
function Member:_loadMore(data)
if data.roles then
local roles = #data.roles > 0 and data.roles or nil
if self._roles then
self._roles._array = roles
else
self._roles_raw = roles
end
end
end
local function sorter(a, b)
if a._position == b._position then
return tonumber(a._id) < tonumber(b._id)
else
return a._position > b._position
end
end
local function predicate(role)
return role._color > 0
end
--[=[
@m getColor
@r Color
@d Returns a color object that represents the member's color as determined by
its highest colored role. If the member has no colored roles, then the default
color with a value of 0 is returned.
]=]
function Member:getColor()
local roles = {}
for role in self.roles:findAll(predicate) do
insert(roles, role)
end
sort(roles, sorter)
return roles[1] and roles[1]:getColor() or Color()
end
local function has(a, b, admin)
return band(a, b) > 0 or admin and band(a, permission.administrator) > 0
end
--[=[
@m hasPermission
@op channel GuildChannel
@p perm Permissions-Resolvable
@r boolean
@d Checks whether the member has a specific permission. If `channel` is omitted,
then only guild-level permissions are checked. This is a relatively expensive
operation. If you need to check multiple permissions at once, use the
`getPermissions` method and check the resulting object.
]=]
function Member:hasPermission(channel, perm)
if not perm then
perm = channel
channel = nil
end
local guild = self.guild
if channel then
if not isInstance(channel, GuildChannel) or channel.guild ~= guild then
return error('Invalid GuildChannel: ' .. tostring(channel), 2)
end
end
local n = Resolver.permission(perm)
if not n then
return error('Invalid permission: ' .. tostring(perm), 2)
end
if self.id == guild.ownerId then
return true
end
if channel then
local overwrites = channel.permissionOverwrites
local overwrite = overwrites:get(self.id)
if overwrite then
if has(overwrite.allowedPermissions, n) then
return true
end
if has(overwrite.deniedPermissions, n) then
return false
end
end
local allow, deny = 0, 0
for role in self.roles:iter() do
if role.id ~= guild.id then -- just in case
overwrite = overwrites:get(role.id)
if overwrite then
allow = bor(allow, overwrite.allowedPermissions)
deny = bor(deny, overwrite.deniedPermissions)
end
end
end
if has(allow, n) then
return true
end
if has(deny, n) then
return false
end
local everyone = overwrites:get(guild.id)
if everyone then
if has(everyone.allowedPermissions, n) then
return true
end
if has(everyone.deniedPermissions, n) then
return false
end
end
end
for role in self.roles:iter() do
if role.id ~= guild.id then -- just in case
if has(role.permissions, n, true) then
return true
end
end
end
if has(guild.defaultRole.permissions, n, true) then
return true
end
return false
end
--[=[
@m getPermissions
@op channel GuildChannel
@r Permissions
@d Returns a permissions object that represents the member's total permissions for
the guild, or for a specific channel if one is provided. If you just need to
check one permission, use the `hasPermission` method.
]=]
function Member:getPermissions(channel)
local guild = self.guild
if channel then
if not isInstance(channel, GuildChannel) or channel.guild ~= guild then
return error('Invalid GuildChannel: ' .. tostring(channel), 2)
end
end
if self.id == guild.ownerId then
return Permissions.all()
end
local ret = guild.defaultRole.permissions
for role in self.roles:iter() do
if role.id ~= guild.id then -- just in case
ret = bor(ret, role.permissions)
end
end
if band(ret, permission.administrator) > 0 then
return Permissions.all()
end
if channel then
local overwrites = channel.permissionOverwrites
local everyone = overwrites:get(guild.id)
if everyone then
ret = band(ret, bnot(everyone.deniedPermissions))
ret = bor(ret, everyone.allowedPermissions)
end
local allow, deny = 0, 0
for role in self.roles:iter() do
if role.id ~= guild.id then -- just in case
local overwrite = overwrites:get(role.id)
if overwrite then
deny = bor(deny, overwrite.deniedPermissions)
allow = bor(allow, overwrite.allowedPermissions)
end
end
end
ret = band(ret, bnot(deny))
ret = bor(ret, allow)
local overwrite = overwrites:get(self.id)
if overwrite then
ret = band(ret, bnot(overwrite.deniedPermissions))
ret = bor(ret, overwrite.allowedPermissions)
end
end
return Permissions(ret)
end
--[=[
@m addRole
@p id Role-ID-Resolvable
@r boolean
@d Adds a role to the member. If the member already has the role, then no action is
taken. Note that the everyone role cannot be explicitly added.
]=]
function Member:addRole(id)
if self:hasRole(id) then return true end
id = Resolver.roleId(id)
local data, err = self.client._api:addGuildMemberRole(self._parent._id, self.id, id)
if data then
local roles = self._roles and self._roles._array or self._roles_raw
if roles then
insert(roles, id)
else
self._roles_raw = {id}
end
return true
else
return false, err
end
end
--[=[
@m removeRole
@p id Role-ID-Resolvable
@r boolean
@d Removes a role from the member. If the member does not have the role, then no
action is taken. Note that the everyone role cannot be removed.
]=]
function Member:removeRole(id)
if not self:hasRole(id) then return true end
id = Resolver.roleId(id)
local data, err = self.client._api:removeGuildMemberRole(self._parent._id, self.id, id)
if data then
local roles = self._roles and self._roles._array or self._roles_raw
if roles then
for i, v in ipairs(roles) do
if v == id then
remove(roles, i)
break
end
end
if #roles == 0 then
if self._roles then
self._roles._array = nil
else
self._roles_raw = nil
end
end
end
return true
else
return false, err
end
end
--[=[
@m hasRole
@p id Role-ID-Resolvable
@r boolean
@d Checks whether the member has a specific role. This will return true for the
guild's default role in addition to any explicitly assigned roles.
]=]
function Member:hasRole(id)
id = Resolver.roleId(id)
if id == self._parent._id then return true end -- @everyone
local roles = self._roles and self._roles._array or self._roles_raw
if roles then
for _, v in ipairs(roles) do
if v == id then
return true
end
end
end
return false
end
--[=[
@m setNickname
@p nick string
@r boolean
@d Sets the member's nickname. This must be between 1 and 32 characters in length.
Pass `nil` to remove the nickname.
]=]
function Member:setNickname(nick)
nick = nick or ''
local data, err
if self.id == self.client._user._id then
data, err = self.client._api:modifyCurrentUsersNick(self._parent._id, {nick = nick})
else
data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {nick = nick})
end
if data then
self._nick = nick ~= '' and nick or nil
return true
else
return false, err
end
end
--[=[
@m setVoiceChannel
@p id Channel-ID-Resolvable
@r boolean
@d Moves the member to a new voice channel, but only if the member has an active
voice connection in the current guild. Due to complexities in voice state
handling, the member's `voiceChannel` property will update asynchronously via
WebSocket; not as a result of the HTTP request.
]=]
function Member:setVoiceChannel(id)
id = Resolver.channelId(id)
local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {channel_id = id})
if data then
return true
else
return false, err
end
end
--[=[
@m mute
@r boolean
@d Mutes the member in its guild.
]=]
function Member:mute()
local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {mute = true})
if data then
self._mute = true
return true
else
return false, err
end
end
--[=[
@m unmute
@r boolean
@d Unmutes the member in its guild.
]=]
function Member:unmute()
local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {mute = false})
if data then
self._mute = false
return true
else
return false, err
end
end
--[=[
@m deafen
@r boolean
@d Deafens the member in its guild.
]=]
function Member:deafen()
local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {deaf = true})
if data then
self._deaf = true
return true
else
return false, err
end
end
--[=[
@m undeafen
@r boolean
@d Undeafens the member in its guild.
]=]
function Member:undeafen()
local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {deaf = false})
if data then
self._deaf = false
return true
else
return false, err
end
end
--[=[
@m kick
@p reason string
@r boolean
@d Equivalent to `Member.guild:kickUser(Member.user, reason)`
]=]
function Member:kick(reason)
return self._parent:kickUser(self._user, reason)
end
--[=[
@m ban
@p reason string
@p days number
@r boolean
@d Equivalent to `Member.guild:banUser(Member.user, reason, days)`
]=]
function Member:ban(reason, days)
return self._parent:banUser(self._user, reason, days)
end
--[=[
@m unban
@p reason string
@r boolean
@d Equivalent to `Member.guild:unbanUser(Member.user, reason)`
]=]
function Member:unban(reason)
return self._parent:unbanUser(self._user, reason)
end
--[=[@p roles ArrayIterable An iterable array of guild roles that the member has. This does not explicitly
include the default everyone role. Object order is not guaranteed.]=]
function get.roles(self)
if not self._roles then
local roles = self._parent._roles
self._roles = ArrayIterable(self._roles_raw, function(id)
return roles:get(id)
end)
self._roles_raw = nil
end
return self._roles
end
--[=[@p name string If the member has a nickname, then this will be equivalent to that nickname.
Otherwise, this is equivalent to `Member.user.username`.]=]
function get.name(self)
return self._nick or self._user._username
end
--[=[@p nickname string/nil The member's nickname, if one is set.]=]
function get.nickname(self)
return self._nick
end
--[=[@p joinedAt string/nil The date and time at which the current member joined the guild, represented as
an ISO 8601 string plus microseconds when available. Member objects generated
via presence updates lack this property.]=]
function get.joinedAt(self)
return self._joined_at
end
--[=[@p voiceChannel GuildVoiceChannel/nil The voice channel to which this member is connected in the current guild.]=]
function get.voiceChannel(self)
local guild = self._parent
local state = guild._voice_states[self:__hash()]
return state and guild._voice_channels:get(state.channel_id)
end
--[=[@p muted boolean Whether the member is voice muted in its guild.]=]
function get.muted(self)
local state = self._parent._voice_states[self:__hash()]
return state and (state.mute or state.self_mute) or self._mute
end
--[=[@p deafened boolean Whether the member is voice deafened in its guild.]=]
function get.deafened(self)
local state = self._parent._voice_states[self:__hash()]
return state and (state.deaf or state.self_deaf) or self._deaf
end
--[=[@p guild Guild The guild in which this member exists.]=]
function get.guild(self)
return self._parent
end
--[=[@p highestRole Role The highest positioned role that the member has. If the member has no
explicit roles, then this is equivalent to `Member.guild.defaultRole`.]=]
function get.highestRole(self)
local ret
for role in self.roles:iter() do
if not ret or sorter(role, ret) then
ret = role
end
end
return ret or self.guild.defaultRole
end
return Member

View File

@@ -0,0 +1,513 @@
--[=[
@c Message x Snowflake
@d Represents a text message sent in a Discord text channel. Messages can contain
simple content strings, rich embeds, attachments, or reactions.
]=]
local json = require('json')
local constants = require('constants')
local Cache = require('iterables/Cache')
local ArrayIterable = require('iterables/ArrayIterable')
local Snowflake = require('containers/abstract/Snowflake')
local Reaction = require('containers/Reaction')
local Resolver = require('client/Resolver')
local insert = table.insert
local null = json.null
local format = string.format
local Message, get = require('class')('Message', Snowflake)
function Message:__init(data, parent)
Snowflake.__init(self, data, parent)
self._author = self.client._users:_insert(data.author)
if data.member then
data.member.user = data.author
self._parent._parent._members:_insert(data.member)
end
self._timestamp = nil -- waste of space; can be calculated from Snowflake ID
if data.reactions and #data.reactions > 0 then
self._reactions = Cache(data.reactions, Reaction, self)
end
return self:_loadMore(data)
end
function Message:_load(data)
Snowflake._load(self, data)
return self:_loadMore(data)
end
local function parseMentions(content, pattern)
if not content:find('%b<>') then return end
local mentions, seen = {}, {}
for id in content:gmatch(pattern) do
if not seen[id] then
insert(mentions, id)
seen[id] = true
end
end
return mentions
end
function Message:_loadMore(data)
if data.mentions then
for _, user in ipairs(data.mentions) do
if user.member then
user.member.user = user
self._parent._parent._members:_insert(user.member)
else
self.client._users:_insert(user)
end
end
end
local content = data.content
if content then
if self._mentioned_users then
self._mentioned_users._array = parseMentions(content, '<@!?(%d+)>')
end
if self._mentioned_roles then
self._mentioned_roles._array = parseMentions(content, '<@&(%d+)>')
end
if self._mentioned_channels then
self._mentioned_channels._array = parseMentions(content, '<#(%d+)>')
end
if self._mentioned_emojis then
self._mentioned_emojis._array = parseMentions(content, '<a?:[%w_]+:(%d+)>')
end
self._clean_content = nil
end
if data.embeds then
self._embeds = #data.embeds > 0 and data.embeds or nil
end
if data.attachments then
self._attachments = #data.attachments > 0 and data.attachments or nil
end
end
function Message:_addReaction(d)
local reactions = self._reactions
if not reactions then
reactions = Cache({}, Reaction, self)
self._reactions = reactions
end
local emoji = d.emoji
local k = emoji.id ~= null and emoji.id or emoji.name
local reaction = reactions:get(k)
if reaction then
reaction._count = reaction._count + 1
if d.user_id == self.client._user._id then
reaction._me = true
end
else
d.me = d.user_id == self.client._user._id
d.count = 1
reaction = reactions:_insert(d)
end
return reaction
end
function Message:_removeReaction(d)
local reactions = self._reactions
local emoji = d.emoji
local k = emoji.id ~= null and emoji.id or emoji.name
local reaction = reactions:get(k)
if not reaction then return nil end -- uncached reaction?
reaction._count = reaction._count - 1
if d.user_id == self.client._user._id then
reaction._me = false
end
if reaction._count == 0 then
reactions:_delete(k)
end
return reaction
end
function Message:_setOldContent(d)
local ts = d.edited_timestamp
if not ts then return end
local old = self._old
if old then
old[ts] = old[ts] or self._content
else
self._old = {[ts] = self._content}
end
end
function Message:_modify(payload)
local data, err = self.client._api:editMessage(self._parent._id, self._id, payload)
if data then
self:_setOldContent(data)
self:_load(data)
return true
else
return false, err
end
end
--[=[
@m setContent
@p content string
@r boolean
@d Sets the message's content. The message must be authored by the current user
(ie: you cannot change the content of messages sent by other users). The content
must be from 1 to 2000 characters in length.
]=]
function Message:setContent(content)
return self:_modify({content = content or null})
end
--[=[
@m setEmbed
@p embed table
@r boolean
@d Sets the message's embed. The message must be authored by the current user.
(ie: you cannot change the embed of messages sent by other users).
]=]
function Message:setEmbed(embed)
return self:_modify({embed = embed or null})
end
--[=[
@m pin
@r boolean
@d Pins the message in the channel.
]=]
function Message:pin()
local data, err = self.client._api:addPinnedChannelMessage(self._parent._id, self._id)
if data then
self._pinned = true
return true
else
return false, err
end
end
--[=[
@m unpin
@r boolean
@d Unpins the message in the channel.
]=]
function Message:unpin()
local data, err = self.client._api:deletePinnedChannelMessage(self._parent._id, self._id)
if data then
self._pinned = false
return true
else
return false, err
end
end
--[=[
@m addReaction
@p emoji Emoji-Resolvable
@r boolean
@d Adds a reaction to the message. Note that this does not return the new reaction
object; wait for the `reactionAdd` event instead.
]=]
function Message:addReaction(emoji)
emoji = Resolver.emoji(emoji)
local data, err = self.client._api:createReaction(self._parent._id, self._id, emoji)
if data then
return true
else
return false, err
end
end
--[=[
@m removeReaction
@p emoji Emoji-Resolvable
@op id User-ID-Resolvable
@r boolean
@d Removes a reaction from the message. Note that this does not return the old
reaction object; wait for the `reactionAdd` event instead. If no user is
indicated, then this will remove the current user's reaction.
]=]
function Message:removeReaction(emoji, id)
emoji = Resolver.emoji(emoji)
local data, err
if id then
id = Resolver.userId(id)
data, err = self.client._api:deleteUserReaction(self._parent._id, self._id, emoji, id)
else
data, err = self.client._api:deleteOwnReaction(self._parent._id, self._id, emoji)
end
if data then
return true
else
return false, err
end
end
--[=[
@m clearReactions
@r boolean
@d Removes all reactions from the message.
]=]
function Message:clearReactions()
local data, err = self.client._api:deleteAllReactions(self._parent._id, self._id)
if data then
return true
else
return false, err
end
end
--[=[
@m delete
@r boolean
@d Permanently deletes the message. This cannot be undone!
]=]
function Message:delete()
local data, err = self.client._api:deleteMessage(self._parent._id, self._id)
if data then
local cache = self._parent._messages
if cache then
cache:_delete(self._id)
end
return true
else
return false, err
end
end
--[=[
@m reply
@p content string/table
@r Message
@d Equivalent to `Message.channel:send(content)`.
]=]
function Message:reply(content)
return self._parent:send(content)
end
--[=[@p reactions Cache An iterable cache of all reactions that exist for this message.]=]
function get.reactions(self)
if not self._reactions then
self._reactions = Cache({}, Reaction, self)
end
return self._reactions
end
--[=[@p mentionedUsers ArrayIterable An iterable array of all users that are mentioned in this message. Object order
is not guaranteed.]=]
function get.mentionedUsers(self)
if not self._mentioned_users then
local users = self.client._users
local mentions = parseMentions(self._content, '<@!?(%d+)>')
self._mentioned_users = ArrayIterable(mentions, function(id)
return users:get(id)
end)
end
return self._mentioned_users
end
--[=[@p mentionedRoles ArrayIterable An iterable array of known roles that are mentioned in this message, excluding
the default everyone role. The message must be in a guild text channel and the
roles must be cached in that channel's guild for them to appear here. Object
order is not guaranteed.]=]
function get.mentionedRoles(self)
if not self._mentioned_roles then
local client = self.client
local mentions = parseMentions(self._content, '<@&(%d+)>')
self._mentioned_roles = ArrayIterable(mentions, function(id)
local guild = client._role_map[id]
return guild and guild._roles:get(id) or nil
end)
end
return self._mentioned_roles
end
--[=[@p mentionedEmojis ArrayIterable An iterable array of all known emojis that are mentioned in this message. If
the client does not have the emoji cached, then it will not appear here. Object order is not guaranteed.]=]
function get.mentionedEmojis(self)
if not self._mentioned_emojis then
local client = self.client
local mentions = parseMentions(self._content, '<a?:[%w_]+:(%d+)>')
self._mentioned_emojis = ArrayIterable(mentions, function(id)
local guild = client._emoji_map[id]
return guild and guild._emojis:get(id)
end)
end
return self._mentioned_emojis
end
--[=[@p mentionedChannels ArrayIterable An iterable array of all known channels that are mentioned in this message. If
the client does not have the channel cached, then it will not appear here.
Object order is not guaranteed.]=]
function get.mentionedChannels(self)
if not self._mentioned_channels then
local client = self.client
local mentions = parseMentions(self._content, '<#(%d+)>')
self._mentioned_channels = ArrayIterable(mentions, function(id)
local guild = client._channel_map[id]
if guild then
return guild._text_channels:get(id) or guild._voice_channels:get(id) or guild._categories:get(id)
else
return client._private_channels:get(id) or client._group_channels:get(id)
end
end)
end
return self._mentioned_channels
end
local usersMeta = {__index = function(_, k) return '@' .. k end}
local rolesMeta = {__index = function(_, k) return '@' .. k end}
local channelsMeta = {__index = function(_, k) return '#' .. k end}
local everyone = '@' .. constants.ZWSP .. 'everyone'
local here = '@' .. constants.ZWSP .. 'here'
--[=[@p cleanContent string The message content with all recognized mentions replaced by names and with
@everyone and @here mentions escaped by a zero-width space (ZWSP).]=]
function get.cleanContent(self)
if not self._clean_content then
local content = self._content
local guild = self.guild
local users = setmetatable({}, usersMeta)
for user in self.mentionedUsers:iter() do
local member = guild and guild._members:get(user._id)
users[user._id] = '@' .. (member and member._nick or user._username)
end
local roles = setmetatable({}, rolesMeta)
for role in self.mentionedRoles:iter() do
roles[role._id] = '@' .. role._name
end
local channels = setmetatable({}, channelsMeta)
for channel in self.mentionedChannels:iter() do
channels[channel._id] = '#' .. channel._name
end
self._clean_content = content
:gsub('<@!?(%d+)>', users)
:gsub('<@&(%d+)>', roles)
:gsub('<#(%d+)>', channels)
:gsub('<a?(:.+:)%d+>', '%1')
:gsub('@everyone', everyone)
:gsub('@here', here)
end
return self._clean_content
end
--[=[@p mentionsEveryone boolean Whether this message mentions @everyone or @here.]=]
function get.mentionsEveryone(self)
return self._mention_everyone
end
--[=[@p pinned boolean Whether this message belongs to its channel's pinned messages.]=]
function get.pinned(self)
return self._pinned
end
--[=[@p tts boolean Whether this message is a text-to-speech message.]=]
function get.tts(self)
return self._tts
end
--[=[@p nonce string/number/boolean/nil Used by the official Discord client to detect the success of a sent message.]=]
function get.nonce(self)
return self._nonce
end
--[=[@p editedTimestamp string/nil The date and time at which the message was most recently edited, represented as
an ISO 8601 string plus microseconds when available.]=]
function get.editedTimestamp(self)
return self._edited_timestamp
end
--[=[@p oldContent string/table Yields a table containing keys as timestamps and
value as content of the message at that time.]=]
function get.oldContent(self)
return self._old
end
--[=[@p content string The raw message content. This should be between 0 and 2000 characters in length.]=]
function get.content(self)
return self._content
end
--[=[@p author User The object of the user that created the message.]=]
function get.author(self)
return self._author
end
--[=[@p channel TextChannel The channel in which this message was sent.]=]
function get.channel(self)
return self._parent
end
--[=[@p type number The message type. Use the `messageType` enumeration for a human-readable
representation.]=]
function get.type(self)
return self._type
end
--[=[@p embed table/nil A raw data table that represents the first rich embed that exists in this
message. See the Discord documentation for more information.]=]
function get.embed(self)
return self._embeds and self._embeds[1]
end
--[=[@p attachment table/nil A raw data table that represents the first file attachment that exists in this
message. See the Discord documentation for more information.]=]
function get.attachment(self)
return self._attachments and self._attachments[1]
end
--[=[@p embeds table A raw data table that contains all embeds that exist for this message. If
there are none, this table will not be present.]=]
function get.embeds(self)
return self._embeds
end
--[=[@p attachments table A raw data table that contains all attachments that exist for this message. If
there are none, this table will not be present.]=]
function get.attachments(self)
return self._attachments
end
--[=[@p guild Guild/nil The guild in which this message was sent. This will not exist if the message
was not sent in a guild text channel. Equivalent to `Message.channel.guild`.]=]
function get.guild(self)
return self._parent.guild
end
--[=[@p member Member/nil The member object of the message's author. This will not exist if the message
is not sent in a guild text channel or if the member object is not cached.
Equivalent to `Message.guild.members:get(Message.author.id)`.]=]
function get.member(self)
local guild = self.guild
return guild and guild._members:get(self._author._id)
end
--[=[@p link string URL that can be used to jump-to the message in the Discord client.]=]
function get.link(self)
local guild = self.guild
return format('https://discordapp.com/channels/%s/%s/%s', guild and guild._id or '@me', self._parent._id, self._id)
end
return Message

View File

@@ -0,0 +1,221 @@
--[=[
@c PermissionOverwrite x Snowflake
@d Represents an object that is used to allow or deny specific permissions for a
role or member in a Discord guild channel.
]=]
local Snowflake = require('containers/abstract/Snowflake')
local Permissions = require('utils/Permissions')
local Resolver = require('client/Resolver')
local band, bnot = bit.band, bit.bnot
local PermissionOverwrite, get = require('class')('PermissionOverwrite', Snowflake)
function PermissionOverwrite:__init(data, parent)
Snowflake.__init(self, data, parent)
end
--[=[
@m delete
@r boolean
@d Deletes the permission overwrite. This can be undone by created a new version of
the same overwrite.
]=]
function PermissionOverwrite:delete()
local data, err = self.client._api:deleteChannelPermission(self._parent._id, self._id)
if data then
local cache = self._parent._permission_overwrites
if cache then
cache:_delete(self._id)
end
return true
else
return false, err
end
end
--[=[
@m getObject
@r Role/Member
@d Returns the object associated with this overwrite, either a role or member.
This may make an HTTP request if the object is not cached.
]=]
function PermissionOverwrite:getObject()
local guild = self._parent._parent
if self._type == 'role' then
return guild:getRole(self._id)
elseif self._type == 'member' then
return guild:getMember(self._id)
end
end
local function getPermissions(self)
return Permissions(self._allow), Permissions(self._deny)
end
local function setPermissions(self, allow, deny)
local data, err = self.client._api:editChannelPermissions(self._parent._id, self._id, {
allow = allow, deny = deny, type = self._type
})
if data then
self._allow, self._deny = allow, deny
return true
else
return false, err
end
end
--[=[
@m getAllowedPermissions
@r Permissions
@d Returns a permissions object that represents the permissions that this overwrite
explicitly allows.
]=]
function PermissionOverwrite:getAllowedPermissions()
return Permissions(self._allow)
end
--[=[
@m getDeniedPermissions
@r Permissions
@d Returns a permissions object that represents the permissions that this overwrite
explicitly denies.
]=]
function PermissionOverwrite:getDeniedPermissions()
return Permissions(self._deny)
end
--[=[
@m setPermissions
@p allowed Permissions-Resolvables
@p denied Permissions-Resolvables
@r boolean
@d Sets the permissions that this overwrite explicitly allows and denies. This
method does NOT resolve conflicts. Please be sure to use the correct parameters.
]=]
function PermissionOverwrite:setPermissions(allowed, denied)
local allow = Resolver.permissions(allowed)
local deny = Resolver.permissions(denied)
return setPermissions(self, allow, deny)
end
--[=[
@m setAllowedPermissions
@p allowed Permissions-Resolvables
@r boolean
@d Sets the permissions that this overwrite explicitly allows.
]=]
function PermissionOverwrite:setAllowedPermissions(allowed)
local allow = Resolver.permissions(allowed)
local deny = band(bnot(allow), self._deny) -- un-deny the allowed permissions
return setPermissions(self, allow, deny)
end
--[=[
@m setDeniedPermissions
@p denied Permissions-Resolvables
@r boolean
@d Sets the permissions that this overwrite explicitly denies.
]=]
function PermissionOverwrite:setDeniedPermissions(denied)
local deny = Resolver.permissions(denied)
local allow = band(bnot(deny), self._allow) -- un-allow the denied permissions
return setPermissions(self, allow, deny)
end
--[=[
@m allowPermissions
@p ... Permissions-Resolvables
@r boolean
@d Allows individual permissions in this overwrite.
]=]
function PermissionOverwrite:allowPermissions(...)
local allowed, denied = getPermissions(self)
allowed:enable(...); denied:disable(...)
return setPermissions(self, allowed._value, denied._value)
end
--[=[
@m denyPermissions
@p ... Permissions-Resolvables
@r boolean
@d Denies individual permissions in this overwrite.
]=]
function PermissionOverwrite:denyPermissions(...)
local allowed, denied = getPermissions(self)
allowed:disable(...); denied:enable(...)
return setPermissions(self, allowed._value, denied._value)
end
--[=[
@m clearPermissions
@p ... Permissions-Resolvables
@r boolean
@d Clears individual permissions in this overwrite.
]=]
function PermissionOverwrite:clearPermissions(...)
local allowed, denied = getPermissions(self)
allowed:disable(...); denied:disable(...)
return setPermissions(self, allowed._value, denied._value)
end
--[=[
@m allowAllPermissions
@r boolean
@d Allows all permissions in this overwrite.
]=]
function PermissionOverwrite:allowAllPermissions()
local allowed, denied = getPermissions(self)
allowed:enableAll(); denied:disableAll()
return setPermissions(self, allowed._value, denied._value)
end
--[=[
@m denyAllPermissions
@r boolean
@d Denies all permissions in this overwrite.
]=]
function PermissionOverwrite:denyAllPermissions()
local allowed, denied = getPermissions(self)
allowed:disableAll(); denied:enableAll()
return setPermissions(self, allowed._value, denied._value)
end
--[=[
@m clearAllPermissions
@r boolean
@d Clears all permissions in this overwrite.
]=]
function PermissionOverwrite:clearAllPermissions()
local allowed, denied = getPermissions(self)
allowed:disableAll(); denied:disableAll()
return setPermissions(self, allowed._value, denied._value)
end
--[=[@p type string The overwrite type; either "role" or "member".]=]
function get.type(self)
return self._type
end
--[=[@p channel GuildChannel The channel in which this overwrite exists.]=]
function get.channel(self)
return self._parent
end
--[=[@p guild Guild The guild in which this overwrite exists. Equivalent to `PermissionOverwrite.channel.guild`.]=]
function get.guild(self)
return self._parent._parent
end
--[=[@p allowedPermissions number The number representing the total permissions allowed by this overwrite.]=]
function get.allowedPermissions(self)
return self._allow
end
--[=[@p deniedPermissions number The number representing the total permissions denied by this overwrite.]=]
function get.deniedPermissions(self)
return self._deny
end
return PermissionOverwrite

View File

@@ -0,0 +1,36 @@
--[=[
@c PrivateChannel x TextChannel
@d Represents a private Discord text channel used to track correspondences between
the current user and one other recipient.
]=]
local TextChannel = require('containers/abstract/TextChannel')
local PrivateChannel, get = require('class')('PrivateChannel', TextChannel)
function PrivateChannel:__init(data, parent)
TextChannel.__init(self, data, parent)
self._recipient = self.client._users:_insert(data.recipients[1])
end
--[=[
@m close
@r boolean
@d Closes the channel. This does not delete the channel. To re-open the channel,
use `User:getPrivateChannel`.
]=]
function PrivateChannel:close()
return self:_delete()
end
--[=[@p name string Equivalent to `PrivateChannel.recipient.username`.]=]
function get.name(self)
return self._recipient._username
end
--[=[@p recipient User The recipient of this channel's messages, other than the current user.]=]
function get.recipient(self)
return self._recipient
end
return PrivateChannel

View File

@@ -0,0 +1,139 @@
--[=[
@c Reaction x Container
@d Represents an emoji that has been used to react to a Discord text message. Both
standard and custom emojis can be used.
]=]
local json = require('json')
local Container = require('containers/abstract/Container')
local SecondaryCache = require('iterables/SecondaryCache')
local Resolver = require('client/Resolver')
local null = json.null
local format = string.format
local Reaction, get = require('class')('Reaction', Container)
function Reaction:__init(data, parent)
Container.__init(self, data, parent)
self._emoji_id = data.emoji.id ~= null and data.emoji.id or nil
self._emoji_name = data.emoji.name
end
--[=[
@m __hash
@r string
@d Returns `Reaction.emojiId or Reaction.emojiName`
]=]
function Reaction:__hash()
return self._emoji_id or self._emoji_name
end
local function getUsers(self, query)
local emoji = Resolver.emoji(self)
local message = self._parent
local channel = message._parent
local data, err = self.client._api:getReactions(channel._id, message._id, emoji, query)
if data then
return SecondaryCache(data, self.client._users)
else
return nil, err
end
end
--[=[
@m getUsers
@op limit number
@r SecondaryCache
@d Returns a newly constructed cache of all users that have used this reaction in
its parent message. The cache is not automatically updated via gateway events,
but the internally referenced user objects may be updated. You must call this
method again to guarantee that the objects are update to date.
]=]
function Reaction:getUsers(limit)
return getUsers(self, limit and {limit = limit})
end
--[=[
@m getUsersBefore
@p id User-ID-Resolvable
@op limit number
@r SecondaryCache
@d Returns a newly constructed cache of all users that have used this reaction before the specified id in
its parent message. The cache is not automatically updated via gateway events,
but the internally referenced user objects may be updated. You must call this
method again to guarantee that the objects are update to date.
]=]
function Reaction:getUsersBefore(id, limit)
id = Resolver.userId(id)
return getUsers(self, {before = id, limit = limit})
end
--[=[
@m getUsersAfter
@p id User-ID-Resolvable
@op limit number
@r SecondaryCache
@d Returns a newly constructed cache of all users that have used this reaction
after the specified id in its parent message. The cache is not automatically
updated via gateway events, but the internally referenced user objects may be
updated. You must call this method again to guarantee that the objects are update to date.
]=]
function Reaction:getUsersAfter(id, limit)
id = Resolver.userId(id)
return getUsers(self, {after = id, limit = limit})
end
--[=[
@m delete
@op id User-ID-Resolvable
@r boolean
@d Equivalent to `Reaction.message:removeReaction(Reaction)`
]=]
function Reaction:delete(id)
return self._parent:removeReaction(self, id)
end
--[=[@p emojiId string/nil The ID of the emoji used in this reaction if it is a custom emoji.]=]
function get.emojiId(self)
return self._emoji_id
end
--[=[@p emojiName string The name of the emoji used in this reaction if it is a custom emoji. Otherwise,
this will be the raw string for a standard emoji.]=]
function get.emojiName(self)
return self._emoji_name
end
--[=[@p emojiHash The discord hash for the emoji, or Unicode string if it is not custom.]=]
function get.emojiHash(self)
if self._emoji_id then
return self._emoji_name .. ':' .. self._emoji_id
else
return self._emoji_name
end
end
--[=[@p emojiURL string/nil string The URL that can be used to view a full version of the emoji used in this
reaction if it is a custom emoji.]=]
function get.emojiURL(self)
local id = self._emoji_id
return id and format('https://cdn.discordapp.com/emojis/%s.png', id) or nil
end
--[=[@p me boolean Whether the current user has used this reaction.]=]
function get.me(self)
return self._me
end
--[=[@p count number The total number of users that have used this reaction.]=]
function get.count(self)
return self._count
end
--[=[@p message Message The message on which this reaction exists.]=]
function get.message(self)
return self._parent
end
return Reaction

View File

@@ -0,0 +1,27 @@
--[=[
@c Relationship x UserPresence
@d Represents a relationship between the current user and another Discord user.
This is generally either a friend or a blocked user. This class should only be
relevant to user-accounts; bots cannot normally have relationships.
]=]
local UserPresence = require('containers/abstract/UserPresence')
local Relationship, get = require('class')('Relationship', UserPresence)
function Relationship:__init(data, parent)
UserPresence.__init(self, data, parent)
end
--[=[@p name string Equivalent to `Relationship.user.username`.]=]
function get.name(self)
return self._user._username
end
--[=[@p type number The relationship type. See the `relationshipType` enumeration for a
human-readable representation.]=]
function get.type(self)
return self._type
end
return Relationship

View File

@@ -0,0 +1,367 @@
--[=[
@c Role x Snowflake
@d Represents a Discord guild role, which is used to assign priority, permissions,
and a color to guild members.
]=]
local json = require('json')
local Snowflake = require('containers/abstract/Snowflake')
local Color = require('utils/Color')
local Permissions = require('utils/Permissions')
local Resolver = require('client/Resolver')
local FilteredIterable = require('iterables/FilteredIterable')
local format = string.format
local insert, sort = table.insert, table.sort
local min, max, floor = math.min, math.max, math.floor
local huge = math.huge
local Role, get = require('class')('Role', Snowflake)
function Role:__init(data, parent)
Snowflake.__init(self, data, parent)
self.client._role_map[self._id] = parent
end
function Role:_modify(payload)
local data, err = self.client._api:modifyGuildRole(self._parent._id, self._id, payload)
if data then
self:_load(data)
return true
else
return false, err
end
end
--[=[
@m delete
@r boolean
@d Permanently deletes the role. This cannot be undone!
]=]
function Role:delete()
local data, err = self.client._api:deleteGuildRole(self._parent._id, self._id)
if data then
local cache = self._parent._roles
if cache then
cache:_delete(self._id)
end
return true
else
return false, err
end
end
local function sorter(a, b)
if a.position == b.position then
return tonumber(a.id) < tonumber(b.id)
else
return a.position < b.position
end
end
local function getSortedRoles(self)
local guild = self._parent
local id = self._parent._id
local ret = {}
for role in guild.roles:iter() do
if role._id ~= id then
insert(ret, {id = role._id, position = role._position})
end
end
sort(ret, sorter)
return ret
end
local function setSortedRoles(self, roles)
local id = self._parent._id
insert(roles, {id = id, position = 0})
local data, err = self.client._api:modifyGuildRolePositions(id, roles)
if data then
return true
else
return false, err
end
end
--[=[
@m moveDown
@p n number
@r boolean
@d Moves a role down its list. The parameter `n` indicates how many spaces the
role should be moved, clamped to the lowest position, with a default of 1 if
it is omitted. This will also normalize the positions of all roles. Note that
the default everyone role cannot be moved.
]=]
function Role:moveDown(n) -- TODO: fix attempt to move roles that cannot be moved
n = tonumber(n) or 1
if n < 0 then
return self:moveDown(-n)
end
local roles = getSortedRoles(self)
local new = huge
for i = #roles, 1, -1 do
local v = roles[i]
if v.id == self._id then
new = max(1, i - floor(n))
v.position = new
elseif i >= new then
v.position = i + 1
else
v.position = i
end
end
return setSortedRoles(self, roles)
end
--[=[
@m moveUp
@p n number
@r boolean
@d Moves a role up its list. The parameter `n` indicates how many spaces the
role should be moved, clamped to the highest position, with a default of 1 if
it is omitted. This will also normalize the positions of all roles. Note that
the default everyone role cannot be moved.
]=]
function Role:moveUp(n) -- TODO: fix attempt to move roles that cannot be moved
n = tonumber(n) or 1
if n < 0 then
return self:moveUp(-n)
end
local roles = getSortedRoles(self)
local new = -huge
for i = 1, #roles do
local v = roles[i]
if v.id == self._id then
new = min(i + floor(n), #roles)
v.position = new
elseif i <= new then
v.position = i - 1
else
v.position = i
end
end
return setSortedRoles(self, roles)
end
--[=[
@m setName
@p name string
@r boolean
@d Sets the role's name. The name must be between 1 and 100 characters in length.
]=]
function Role:setName(name)
return self:_modify({name = name or json.null})
end
--[=[
@m setColor
@p color Color-Resolvable
@r boolean
@d Sets the role's display color.
]=]
function Role:setColor(color)
color = color and Resolver.color(color)
return self:_modify({color = color or json.null})
end
--[=[
@m setPermissions
@p permissions Permissions-Resolvable
@r boolean
@d Sets the permissions that this role explicitly allows.
]=]
function Role:setPermissions(permissions)
permissions = permissions and Resolver.permissions(permissions)
return self:_modify({permissions = permissions or json.null})
end
--[=[
@m hoist
@r boolean
@d Causes members with this role to display above unhoisted roles in the member
list.
]=]
function Role:hoist()
return self:_modify({hoist = true})
end
--[=[
@m unhoist
@r boolean
@d Causes member with this role to display amongst other unhoisted members.
]=]
function Role:unhoist()
return self:_modify({hoist = false})
end
--[=[
@m enableMentioning
@r boolean
@d Allows anyone to mention this role in text messages.
]=]
function Role:enableMentioning()
return self:_modify({mentionable = true})
end
--[=[
@m disableMentioning
@r boolean
@d Disallows anyone to mention this role in text messages.
]=]
function Role:disableMentioning()
return self:_modify({mentionable = false})
end
--[=[
@m enablePermissions
@p ... Permissions-Resolvables
@r boolean
@d Enables individual permissions for this role. This does not necessarily fully
allow the permissions.
]=]
function Role:enablePermissions(...)
local permissions = self:getPermissions()
permissions:enable(...)
return self:setPermissions(permissions)
end
--[=[
@m disablePermissions
@p ... Permissions-Resolvables
@r boolean
@d Disables individual permissions for this role. This does not necessarily fully
disallow the permissions.
]=]
function Role:disablePermissions(...)
local permissions = self:getPermissions()
permissions:disable(...)
return self:setPermissions(permissions)
end
--[=[
@m enableAllPermissions
@r boolean
@d Enables all permissions for this role. This does not necessarily fully
allow the permissions.
]=]
function Role:enableAllPermissions()
local permissions = self:getPermissions()
permissions:enableAll()
return self:setPermissions(permissions)
end
--[=[
@m disableAllPermissions
@r boolean
@d Disables all permissions for this role. This does not necessarily fully
disallow the permissions.
]=]
function Role:disableAllPermissions()
local permissions = self:getPermissions()
permissions:disableAll()
return self:setPermissions(permissions)
end
--[=[
@m getColor
@r Color
@d Returns a color object that represents the role's display color.
]=]
function Role:getColor()
return Color(self._color)
end
--[=[
@m getPermissions
@r Permissions
@d Returns a permissions object that represents the permissions that this role
has enabled.
]=]
function Role:getPermissions()
return Permissions(self._permissions)
end
--[=[@p hoisted boolean Whether members with this role should be shown separated from other members
in the guild member list.]=]
function get.hoisted(self)
return self._hoist
end
--[=[@p mentionable boolean Whether this role can be mentioned in a text channel message.]=]
function get.mentionable(self)
return self._mentionable
end
--[=[@p managed boolean Whether this role is managed by some integration or bot inclusion.]=]
function get.managed(self)
return self._managed
end
--[=[@p name string The name of the role. This should be between 1 and 100 characters in length.]=]
function get.name(self)
return self._name
end
--[=[@p position number The position of the role, where 0 is the lowest.]=]
function get.position(self)
return self._position
end
--[=[@p color number Represents the display color of the role as a decimal value.]=]
function get.color(self)
return self._color
end
--[=[@p permissions number Represents the total permissions of the role as a decimal value.]=]
function get.permissions(self)
return self._permissions
end
--[=[@p mentionString string A string that, when included in a message content, may resolve as a role
notification in the official Discord client.]=]
function get.mentionString(self)
return format('<@&%s>', self._id)
end
--[=[@p guild Guild The guild in which this role exists.]=]
function get.guild(self)
return self._parent
end
--[=[@p members FilteredIterable A filtered iterable of guild members that have
this role. If you want to check whether a specific member has this role, it would
be better to get the member object elsewhere and use `Member:hasRole` rather
than check whether the member exists here.]=]
function get.members(self)
if not self._members then
self._members = FilteredIterable(self._parent._members, function(m)
return m:hasRole(self)
end)
end
return self._members
end
--[=[@p emojis FilteredIterable A filtered iterable of guild emojis that have
this role. If you want to check whether a specific emoji has this role, it would
be better to get the emoji object elsewhere and use `Emoji:hasRole` rather
than check whether the emoji exists here.]=]
function get.emojis(self)
if not self._emojis then
self._emojis = FilteredIterable(self._parent._emojis, function(e)
return e:hasRole(self)
end)
end
return self._emojis
end
return Role

View File

@@ -0,0 +1,181 @@
--[=[
@c User x Snowflake
@d Represents a single user of Discord, either a human or a bot, outside of any
specific guild's context.
]=]
local Snowflake = require('containers/abstract/Snowflake')
local FilteredIterable = require('iterables/FilteredIterable')
local constants = require('constants')
local format = string.format
local DEFAULT_AVATARS = constants.DEFAULT_AVATARS
local User, get = require('class')('User', Snowflake)
function User:__init(data, parent)
Snowflake.__init(self, data, parent)
end
--[=[
@m getAvatarURL
@op size number
@op ext string
@r string
@d Returns a URL that can be used to view the user's full avatar. If provided, the
size must be a power of 2 while the extension must be a valid image format. If
the user does not have a custom avatar, the default URL is returned.
]=]
function User:getAvatarURL(size, ext)
local avatar = self._avatar
if avatar then
ext = ext or avatar:find('a_') == 1 and 'gif' or 'png'
if size then
return format('https://cdn.discordapp.com/avatars/%s/%s.%s?size=%s', self._id, avatar, ext, size)
else
return format('https://cdn.discordapp.com/avatars/%s/%s.%s', self._id, avatar, ext)
end
else
return self:getDefaultAvatarURL(size)
end
end
--[=[
@m getDefaultAvatarURL
@op size number
@r string
@d Returns a URL that can be used to view the user's default avatar.
]=]
function User:getDefaultAvatarURL(size)
local avatar = self.defaultAvatar
if size then
return format('https://cdn.discordapp.com/embed/avatars/%s.png?size=%s', avatar, size)
else
return format('https://cdn.discordapp.com/embed/avatars/%s.png', avatar)
end
end
--[=[
@m getPrivateChannel
@r PrivateChannel
@d Returns a private channel that can be used to communicate with the user. If the
channel is not cached an HTTP request is made to open one.
]=]
function User:getPrivateChannel()
local id = self._id
local client = self.client
local channel = client._private_channels:find(function(e) return e._recipient._id == id end)
if channel then
return channel
else
local data, err = client._api:createDM({recipient_id = id})
if data then
return client._private_channels:_insert(data)
else
return nil, err
end
end
end
--[=[
@m send
@p content string/table
@r Message
@d Equivalent to `User:getPrivateChannel():send(content)`
]=]
function User:send(content)
local channel, err = self:getPrivateChannel()
if channel then
return channel:send(content)
else
return nil, err
end
end
--[=[
@m sendf
@p content string
@r Message
@d Equivalent to `User:getPrivateChannel():sendf(content)`
]=]
function User:sendf(content, ...)
local channel, err = self:getPrivateChannel()
if channel then
return channel:sendf(content, ...)
else
return nil, err
end
end
--[=[@p bot boolean Whether this user is a bot.]=]
function get.bot(self)
return self._bot or false
end
--[=[@p name string Equivalent to `User.username`.]=]
function get.name(self)
return self._username
end
--[=[@p username string The name of the user. This should be between 2 and 32 characters in length.]=]
function get.username(self)
return self._username
end
--[=[@p discriminator number The discriminator of the user. This is a 4-digit string that is used to
discriminate the user from other users with the same username.]=]
function get.discriminator(self)
return self._discriminator
end
--[=[@p tag string The user's username and discriminator concatenated by an `#`.]=]
function get.tag(self)
return self._username .. '#' .. self._discriminator
end
function get.fullname(self)
self.client:_deprecated(self.__name, 'fullname', 'tag')
return self._username .. '#' .. self._discriminator
end
--[=[@p avatar string/nil The hash for the user's custom avatar, if one is set.]=]
function get.avatar(self)
return self._avatar
end
--[=[@p defaultAvatar number The user's default avatar. See the `defaultAvatar` enumeration for a
human-readable representation.]=]
function get.defaultAvatar(self)
return self._discriminator % DEFAULT_AVATARS
end
--[=[@p avatarURL string Equivalent to the result of calling `User:getAvatarURL()`.]=]
function get.avatarURL(self)
return self:getAvatarURL()
end
--[=[@p defaultAvatarURL string Equivalent to the result of calling `User:getDefaultAvatarURL()`.]=]
function get.defaultAvatarURL(self)
return self:getDefaultAvatarURL()
end
--[=[@p mentionString string A string that, when included in a message content, may resolve as user
notification in the official Discord client.]=]
function get.mentionString(self)
return format('<@%s>', self._id)
end
--[=[@p mutualGuilds FilteredIterable A iterable cache of all guilds where this user shares a membership with the
current user. The guild must be cached on the current client and the user's
member object must be cached in that guild in order for it to appear here.]=]
function get.mutualGuilds(self)
if not self._mutual_guilds then
local id = self._id
self._mutual_guilds = FilteredIterable(self.client._guilds, function(g)
return g._members:get(id)
end)
end
return self._mutual_guilds
end
return User

View File

@@ -0,0 +1,137 @@
--[=[
@c Webhook x Snowflake
@d Represents a handle used to send webhook messages to a guild text channel in a
one-way fashion. This class defines methods and properties for managing the
webhook, not for sending messages.
]=]
local json = require('json')
local enums = require('enums')
local Snowflake = require('containers/abstract/Snowflake')
local User = require('containers/User')
local Resolver = require('client/Resolver')
local defaultAvatar = enums.defaultAvatar
local Webhook, get = require('class')('Webhook', Snowflake)
function Webhook:__init(data, parent)
Snowflake.__init(self, data, parent)
self._user = data.user and self.client._users:_insert(data.user) -- DNE if getting by token
end
function Webhook:_modify(payload)
local data, err = self.client._api:modifyWebhook(self._id, payload)
if data then
self:_load(data)
return true
else
return false, err
end
end
--[=[
@m getAvatarURL
@op size number
@op ext string
@r string
@d Returns a URL that can be used to view the webhooks's full avatar. If provided,
the size must be a power of 2 while the extension must be a valid image format.
If the webhook does not have a custom avatar, the default URL is returned.
]=]
function Webhook:getAvatarURL(size, ext)
return User.getAvatarURL(self, size, ext)
end
--[=[
@m getDefaultAvatarURL
@op size number
@r string
@d Returns a URL that can be used to view the webhooks's default avatar.
]=]
function Webhook:getDefaultAvatarURL(size)
return User.getDefaultAvatarURL(self, size)
end
--[=[
@m setName
@p name string
@r boolean
@d Sets the webhook's name. This must be between 2 and 32 characters in length.
]=]
function Webhook:setName(name)
return self:_modify({name = name or json.null})
end
--[=[
@m setAvatar
@p avatar Base64-Resolvable
@r boolean
@d Sets the webhook's avatar. If `nil` is passed, the avatar is removed.
]=]
function Webhook:setAvatar(avatar)
avatar = avatar and Resolver.base64(avatar)
return self:_modify({avatar = avatar or json.null})
end
--[=[
@m delete
@r boolean
@d Permanently deletes the webhook. This cannot be undone!
]=]
function Webhook:delete()
local data, err = self.client._api:deleteWebhook(self._id)
if data then
return true
else
return false, err
end
end
--[=[@p guildId string The ID of the guild in which this webhook exists.]=]
function get.guildId(self)
return self._guild_id
end
--[=[@p channelId string The ID of the channel in which this webhook exists.]=]
function get.channelId(self)
return self._channel_id
end
--[=[@p user User/nil The user that created this webhook.]=]
function get.user(self)
return self._user
end
--[=[@p token string The token that can be used to access this webhook.]=]
function get.token(self)
return self._token
end
--[=[@p name string The name of the webhook. This should be between 2 and 32 characters in length.]=]
function get.name(self)
return self._name
end
--[=[@p avatar string/nil The hash for the webhook's custom avatar, if one is set.]=]
function get.avatar(self)
return self._avatar
end
--[=[@p avatarURL string Equivalent to the result of calling `Webhook:getAvatarURL()`.]=]
function get.avatarURL(self)
return self:getAvatarURL()
end
--[=[@p defaultAvatar number The default avatar for the webhook. See the `defaultAvatar` enumeration for
a human-readable representation. This should always be `defaultAvatar.blurple`.]=]
function get.defaultAvatar()
return defaultAvatar.blurple
end
--[=[@p defaultAvatarURL string Equivalent to the result of calling `Webhook:getDefaultAvatarURL()`.]=]
function get.defaultAvatarURL(self)
return self:getDefaultAvatarURL()
end
return Webhook

View File

@@ -0,0 +1,66 @@
--[=[
@c Channel x Snowflake
@d Abstract base class that defines the base methods and/or properties for all
Discord channel types.
]=]
local Snowflake = require('containers/abstract/Snowflake')
local enums = require('enums')
local format = string.format
local channelType = enums.channelType
local Channel, get = require('class')('Channel', Snowflake)
function Channel:__init(data, parent)
Snowflake.__init(self, data, parent)
end
function Channel:_modify(payload)
local data, err = self.client._api:modifyChannel(self._id, payload)
if data then
self:_load(data)
return true
else
return false, err
end
end
function Channel:_delete()
local data, err = self.client._api:deleteChannel(self._id)
if data then
local cache
local t = self._type
if t == channelType.text then
cache = self._parent._text_channels
elseif t == channelType.private then
cache = self._parent._private_channels
elseif t == channelType.group then
cache = self._parent._group_channels
elseif t == channelType.voice then
cache = self._parent._voice_channels
elseif t == channelType.category then
cache = self._parent._categories
end
if cache then
cache:_delete(self._id)
end
return true
else
return false, err
end
end
--[=[@p type number The channel type. See the `channelType` enumeration for a
human-readable representation.]=]
function get.type(self)
return self._type
end
--[=[@p mentionString string A string that, when included in a message content,
may resolve as a link to a channel in the official Discord client.]=]
function get.mentionString(self)
return format('<#%s>', self._id)
end
return Channel

View File

@@ -0,0 +1,67 @@
--[=[
@c Container
@d Abstract base class that defines the base methods and/or properties for all
Discord objects and structures. Container classes are constructed internally
with information received from Discord and should never be manually constructed.
]=]
local json = require('json')
local null = json.null
local format = string.format
local Container, get = require('class')('Container')
local types = {['string'] = true, ['number'] = true, ['boolean'] = true}
local function load(self, data)
-- assert(type(data) == 'table') -- debug
for k, v in pairs(data) do
if types[type(v)] then
self['_' .. k] = v
elseif v == null then
self['_' .. k] = nil
end
end
end
function Container:__init(data, parent)
-- assert(type(parent) == 'table') -- debug
self._parent = parent
return load(self, data)
end
--[=[
@m __eq
@r boolean
@d Defines the behavior of the `==` operator. Allows containers to be directly
compared according to their type and `__hash` return values.
]=]
function Container:__eq(other)
return self.__class == other.__class and self:__hash() == other:__hash()
end
--[=[
@m __tostring
@r string
@d Defines the behavior of the `tostring` function. All containers follow the format
`ClassName: hash`.
]=]
function Container:__tostring()
return format('%s: %s', self.__name, self:__hash())
end
Container._load = load
--[=[@p client Client A shortcut to the client object to which this container is visible.]=]
function get.client(self)
return self._parent.client or self._parent
end
--[=[@p parent Container/Client The parent object of to which this container is
a child. For example, the parent of a role is the guild in which the role exists.]=]
function get.parent(self)
return self._parent
end
return Container

View File

@@ -0,0 +1,265 @@
--[=[
@c GuildChannel x Channel
@d Abstract base class that defines the base methods and/or properties for all
Discord guild channels.
]=]
local json = require('json')
local enums = require('enums')
local class = require('class')
local Channel = require('containers/abstract/Channel')
local PermissionOverwrite = require('containers/PermissionOverwrite')
local Invite = require('containers/Invite')
local Cache = require('iterables/Cache')
local Resolver = require('client/Resolver')
local isInstance = class.isInstance
local classes = class.classes
local channelType = enums.channelType
local insert, sort = table.insert, table.sort
local min, max, floor = math.min, math.max, math.floor
local huge = math.huge
local GuildChannel, get = class('GuildChannel', Channel)
function GuildChannel:__init(data, parent)
Channel.__init(self, data, parent)
self.client._channel_map[self._id] = parent
self._permission_overwrites = Cache({}, PermissionOverwrite, self)
return self:_loadMore(data)
end
function GuildChannel:_load(data)
Channel._load(self, data)
return self:_loadMore(data)
end
function GuildChannel:_loadMore(data)
return self._permission_overwrites:_load(data.permission_overwrites, true)
end
--[=[
@m setName
@p name string
@r boolean
@d Sets the channel's name. This must be between 2 and 100 characters in length.
]=]
function GuildChannel:setName(name)
return self:_modify({name = name or json.null})
end
--[=[
@m setCategory
@p id Channel-ID-Resolvable
@r boolean
@d Sets the channel's parent category.
]=]
function GuildChannel:setCategory(id)
id = Resolver.channelId(id)
return self:_modify({parent_id = id or json.null})
end
local function sorter(a, b)
if a.position == b.position then
return tonumber(a.id) < tonumber(b.id)
else
return a.position < b.position
end
end
local function getSortedChannels(self)
local channels
local t = self._type
if t == channelType.text then
channels = self._parent._text_channels
elseif t == channelType.voice then
channels = self._parent._voice_channels
elseif t == channelType.category then
channels = self._parent._categories
end
local ret = {}
for channel in channels:iter() do
insert(ret, {id = channel._id, position = channel._position})
end
sort(ret, sorter)
return ret
end
local function setSortedChannels(self, channels)
local data, err = self.client._api:modifyGuildChannelPositions(self._parent._id, channels)
if data then
return true
else
return false, err
end
end
--[=[
@m moveUp
@p n number
@r boolean
@d Moves a channel up its list. The parameter `n` indicates how many spaces the
channel should be moved, clamped to the highest position, with a default of 1 if
it is omitted. This will also normalize the positions of all channels.
]=]
function GuildChannel:moveUp(n)
n = tonumber(n) or 1
if n < 0 then
return self:moveDown(-n)
end
local channels = getSortedChannels(self)
local new = huge
for i = #channels - 1, 0, -1 do
local v = channels[i + 1]
if v.id == self._id then
new = max(0, i - floor(n))
v.position = new
elseif i >= new then
v.position = i + 1
else
v.position = i
end
end
return setSortedChannels(self, channels)
end
--[=[
@m moveDown
@p n number
@r boolean
@d Moves a channel down its list. The parameter `n` indicates how many spaces the
channel should be moved, clamped to the lowest position, with a default of 1 if
it is omitted. This will also normalize the positions of all channels.
]=]
function GuildChannel:moveDown(n)
n = tonumber(n) or 1
if n < 0 then
return self:moveUp(-n)
end
local channels = getSortedChannels(self)
local new = -huge
for i = 0, #channels - 1 do
local v = channels[i + 1]
if v.id == self._id then
new = min(i + floor(n), #channels - 1)
v.position = new
elseif i <= new then
v.position = i - 1
else
v.position = i
end
end
return setSortedChannels(self, channels)
end
--[=[
@m createInvite
@p payload table
@r Invite
@d Creates an invite to the channel. Optional payload fields are:
- max_age:number time in seconds until expiration, default = 86400 (24 hours)
- max_uses:number total number of uses allowed, default = 0 (unlimited)
- temporary:boolean whether the invite grants temporary membership, default = false
- unique:boolean whether a unique code should be guaranteed, default = false
]=]
function GuildChannel:createInvite(payload)
local data, err = self.client._api:createChannelInvite(self._id, payload)
if data then
return Invite(data, self.client)
else
return nil, err
end
end
--[=[
@m getInvites
@r Cache
@d Returns a newly constructed cache of all invite objects for the channel. The
cache and its objects are not automatically updated via gateway events. You must
call this method again to get the updated objects.
]=]
function GuildChannel:getInvites()
local data, err = self.client._api:getChannelInvites(self._id)
if data then
return Cache(data, Invite, self.client)
else
return nil, err
end
end
--[=[
@m getPermissionOverwriteFor
@p obj Role/Member
@r PermissionOverwrite
@d Returns a permission overwrite object corresponding to the provided member or
role object. If a cached overwrite is not found, an empty overwrite with
zero-permissions is returned instead. Therefore, this can be used to create a
new overwrite when one does not exist. Note that the member or role must exist
in the same guild as the channel does.
]=]
function GuildChannel:getPermissionOverwriteFor(obj)
local id, type
if isInstance(obj, classes.Role) and self._parent == obj._parent then
id, type = obj._id, 'role'
elseif isInstance(obj, classes.Member) and self._parent == obj._parent then
id, type = obj._user._id, 'member'
else
return nil, 'Invalid Role or Member: ' .. tostring(obj)
end
local overwrites = self._permission_overwrites
return overwrites:get(id) or overwrites:_insert(setmetatable({
id = id, type = type, allow = 0, deny = 0
}, {__jsontype = 'object'}))
end
--[=[
@m delete
@r boolean
@d Permanently deletes the channel. This cannot be undone!
]=]
function GuildChannel:delete()
return self:_delete()
end
--[=[@p permissionOverwrites Cache An iterable cache of all overwrites that exist in this channel. To access an
overwrite that may exist, but is not cached, use `GuildChannel:getPermissionOverwriteFor`.]=]
function get.permissionOverwrites(self)
return self._permission_overwrites
end
--[=[@p name string The name of the channel. This should be between 2 and 100 characters in length.]=]
function get.name(self)
return self._name
end
--[=[@p position number The position of the channel, where 0 is the highest.]=]
function get.position(self)
return self._position
end
--[=[@p guild Guild The guild in which this channel exists.]=]
function get.guild(self)
return self._parent
end
--[=[@p category GuildCategoryChannel/nil The parent channel category that may contain this channel.]=]
function get.category(self)
return self._parent._categories:get(self._parent_id)
end
return GuildChannel

View File

@@ -0,0 +1,47 @@
--[=[
@c Snowflake x Container
@d Abstract base class that defines the base methods and/or properties for all
Discord objects that have a Snowflake ID.
]=]
local Date = require('utils/Date')
local Container = require('containers/abstract/Container')
local Snowflake, get = require('class')('Snowflake', Container)
function Snowflake:__init(data, parent)
Container.__init(self, data, parent)
end
--[=[
@m __hash
@r string
@d Returns `Snowflake.id`
]=]
function Snowflake:__hash()
return self._id
end
--[=[@p id string The Snowflake ID that can be used to identify the object. This is guaranteed to
be unique except in cases where an object shares the ID of its parent.]=]
function get.id(self)
return self._id
end
--[=[@p createdAt number The Unix time in seconds at which this object was created by Discord. Additional
decimal points may be present, though only the first 3 (milliseconds) should be
considered accurate.]=]
function get.createdAt(self)
return Date.parseSnowflake(self._id)
end
--[=[@p timestamp string The date and time at which this object was created by Discord, represented as
an ISO 8601 string plus microseconds when available.
Equivalent to `Date.fromSnowflake(Snowflake.id):toISO()`.
]=]
function get.timestamp(self)
return Date.fromSnowflake(self._id):toISO()
end
return Snowflake

View File

@@ -0,0 +1,315 @@
--[=[
@c TextChannel x Channel
@d Abstract base class that defines the base methods and/or properties for all
Discord text channels.
]=]
local pathjoin = require('pathjoin')
local Channel = require('containers/abstract/Channel')
local Message = require('containers/Message')
local WeakCache = require('iterables/WeakCache')
local SecondaryCache = require('iterables/SecondaryCache')
local Resolver = require('client/Resolver')
local fs = require('fs')
local splitPath = pathjoin.splitPath
local insert, remove, concat = table.insert, table.remove, table.concat
local format = string.format
local readFileSync = fs.readFileSync
local TextChannel, get = require('class')('TextChannel', Channel)
function TextChannel:__init(data, parent)
Channel.__init(self, data, parent)
self._messages = WeakCache({}, Message, self)
end
--[=[
@m getMessage
@p id Message-ID-Resolvable
@r Message
@d Gets a message object by ID. If the object is already cached, then the cached
object will be returned; otherwise, an HTTP request is made.
]=]
function TextChannel:getMessage(id)
id = Resolver.messageId(id)
local message = self._messages:get(id)
if message then
return message
else
local data, err = self.client._api:getChannelMessage(self._id, id)
if data then
return self._messages:_insert(data)
else
return nil, err
end
end
end
--[=[
@m getFirstMessage
@r Message
@d Returns the first message found in the channel, if any exist. This is not a
cache shortcut; an HTTP request is made each time this method is called.
]=]
function TextChannel:getFirstMessage()
local data, err = self.client._api:getChannelMessages(self._id, {after = self._id, limit = 1})
if data then
if data[1] then
return self._messages:_insert(data[1])
else
return nil, 'Channel has no messages'
end
else
return nil, err
end
end
--[=[
@m getLastMessage
@r Message
@d Returns the last message found in the channel, if any exist. This is not a
cache shortcut; an HTTP request is made each time this method is called.
]=]
function TextChannel:getLastMessage()
local data, err = self.client._api:getChannelMessages(self._id, {limit = 1})
if data then
if data[1] then
return self._messages:_insert(data[1])
else
return nil, 'Channel has no messages'
end
else
return nil, err
end
end
local function getMessages(self, query)
local data, err = self.client._api:getChannelMessages(self._id, query)
if data then
return SecondaryCache(data, self._messages)
else
return nil, err
end
end
--[=[
@m getMessages
@op limit number
@r SecondaryCache
@d Returns a newly constructed cache of between 1 and 100 (default = 50) message
objects found in the channel. While the cache will never automatically gain or
lose objects, the objects that it contains may be updated by gateway events.
]=]
function TextChannel:getMessages(limit)
return getMessages(self, limit and {limit = limit})
end
--[=[
@m getMessagesAfter
@p id Message-ID-Resolvable
@op limit number
@r SecondaryCache
@d Returns a newly constructed cache of between 1 and 100 (default = 50) message
objects found in the channel after a specific point. While the cache will never
automatically gain or lose objects, the objects that it contains may be updated
by gateway events.
]=]
function TextChannel:getMessagesAfter(id, limit)
id = Resolver.messageId(id)
return getMessages(self, {after = id, limit = limit})
end
--[=[
@m getMessagesBefore
@p id Message-ID-Resolvable
@op limit number
@r SecondaryCache
@d Returns a newly constructed cache of between 1 and 100 (default = 50) message
objects found in the channel before a specific point. While the cache will never
automatically gain or lose objects, the objects that it contains may be updated
by gateway events.
]=]
function TextChannel:getMessagesBefore(id, limit)
id = Resolver.messageId(id)
return getMessages(self, {before = id, limit = limit})
end
--[=[
@m getMessagesAround
@p id Message-ID-Resolvable
@op limit number
@r SecondaryCache
@d Returns a newly constructed cache of between 1 and 100 (default = 50) message
objects found in the channel around a specific point. While the cache will never
automatically gain or lose objects, the objects that it contains may be updated
by gateway events.
]=]
function TextChannel:getMessagesAround(id, limit)
id = Resolver.messageId(id)
return getMessages(self, {around = id, limit = limit})
end
--[=[
@m getPinnedMessages
@r SecondaryCache
@d Returns a newly constructed cache of up to 50 messages that are pinned in the
channel. While the cache will never automatically gain or lose objects, the
objects that it contains may be updated by gateway events.
]=]
function TextChannel:getPinnedMessages()
local data, err = self.client._api:getPinnedMessages(self._id)
if data then
return SecondaryCache(data, self._messages)
else
return nil, err
end
end
--[=[
@m broadcastTyping
@r boolean
@d Indicates in the channel that the client's user "is typing".
]=]
function TextChannel:broadcastTyping()
local data, err = self.client._api:triggerTypingIndicator(self._id)
if data then
return true
else
return false, err
end
end
local function parseFile(obj, files)
if type(obj) == 'string' then
local data, err = readFileSync(obj)
if not data then
return nil, err
end
files = files or {}
insert(files, {remove(splitPath(obj)), data})
elseif type(obj) == 'table' and type(obj[1]) == 'string' and type(obj[2]) == 'string' then
files = files or {}
insert(files, obj)
else
return nil, 'Invalid file object: ' .. tostring(obj)
end
return files
end
local function parseMention(obj, mentions)
if type(obj) == 'table' and obj.mentionString then
mentions = mentions or {}
insert(mentions, obj.mentionString)
else
return nil, 'Unmentionable object: ' .. tostring(obj)
end
return mentions
end
--[=[
@m send
@p content string/table
@r Message
@d Sends a message to the channel. If `content` is a string, then this is simply
sent as the message content. If it is a table, more advanced formatting is
allowed. See [[managing messages]] for more information.
]=]
function TextChannel:send(content)
local data, err
if type(content) == 'table' then
local tbl = content
content = tbl.content
if type(tbl.code) == 'string' then
content = format('```%s\n%s\n```', tbl.code, content)
elseif tbl.code == true then
content = format('```\n%s\n```', content)
end
local mentions
if tbl.mention then
mentions, err = parseMention(tbl.mention)
if err then
return nil, err
end
end
if type(tbl.mentions) == 'table' then
for _, mention in ipairs(tbl.mentions) do
mentions, err = parseMention(mention, mentions)
if err then
return nil, err
end
end
end
if mentions then
insert(mentions, content)
content = concat(mentions, ' ')
end
local files
if tbl.file then
files, err = parseFile(tbl.file)
if err then
return nil, err
end
end
if type(tbl.files) == 'table' then
for _, file in ipairs(tbl.files) do
files, err = parseFile(file, files)
if err then
return nil, err
end
end
end
data, err = self.client._api:createMessage(self._id, {
content = content,
tts = tbl.tts,
nonce = tbl.nonce,
embed = tbl.embed,
}, files)
else
data, err = self.client._api:createMessage(self._id, {content = content})
end
if data then
return self._messages:_insert(data)
else
return nil, err
end
end
--[=[
@m sendf
@p content string
@p ... *
@r Message
@d Sends a message to the channel with content formatted with `...` via `string.format`
]=]
function TextChannel:sendf(content, ...)
local data, err = self.client._api:createMessage(self._id, {content = format(content, ...)})
if data then
return self._messages:_insert(data)
else
return nil, err
end
end
--[=[@p messages WeakCache An iterable weak cache of all messages that are
visible to the client. Messages that are not referenced elsewhere are eventually
garbage collected. To access a message that may exist but is not cached,
use `TextChannel:getMessage`.]=]
function get.messages(self)
return self._messages
end
return TextChannel

View File

@@ -0,0 +1,97 @@
--[=[
@c UserPresence x Container
@d Abstract base class that defines the base methods and/or properties for
classes that represent a user's current presence information. Note that any
method or property that exists for the User class is also available in the
UserPresence class and its subclasses.
]=]
local null = require('json').null
local User = require('containers/User')
local Activity = require('containers/Activity')
local Container = require('containers/abstract/Container')
local UserPresence, get = require('class')('UserPresence', Container)
function UserPresence:__init(data, parent)
Container.__init(self, data, parent)
self._user = self.client._users:_insert(data.user)
end
--[=[
@m __hash
@r string
@d Returns `UserPresence.user.id`
]=]
function UserPresence:__hash()
return self._user._id
end
local activities = setmetatable({}, {__mode = 'v'})
function UserPresence:_loadPresence(presence)
self._status = presence.status
local game = presence.game
if game == null then
self._activity = nil
elseif game then
if self._activity then
self._activity:_load(game)
else
local activity = activities[self:__hash()]
if activity then
activity:_load(game)
else
activity = Activity(game, self)
activities[self:__hash()] = activity
end
self._activity = activity
end
end
end
function get.gameName(self)
self.client:_deprecated(self.__name, 'gameName', 'activity.name')
return self._activity and self._activity._name
end
function get.gameType(self)
self.client:_deprecated(self.__name, 'gameType', 'activity.type')
return self._activity and self._activity._type
end
function get.gameURL(self)
self.client:_deprecated(self.__name, 'gameURL', 'activity.url')
return self._activity and self._activity._url
end
--[=[@p status string The user's online status (online, dnd, idle, offline).]=]
function get.status(self)
return self._status or 'offline'
end
--[=[@p user User The user that this presence represents.]=]
function get.user(self)
return self._user
end
--[=[@p activity Activity The Activity that this presence represents.]=]
function get.activity(self)
return self._activity
end
-- user shortcuts
for k, v in pairs(User) do
UserPresence[k] = UserPresence[k] or function(self, ...)
return v(self._user, ...)
end
end
for k, v in pairs(User.__getters) do
get[k] = get[k] or function(self)
return v(self._user)
end
end
return UserPresence

View File

@@ -0,0 +1,54 @@
return {
CHANNEL = "/channels/%s",
CHANNEL_INVITES = "/channels/%s/invites",
CHANNEL_MESSAGE = "/channels/%s/messages/%s",
CHANNEL_MESSAGES = "/channels/%s/messages",
CHANNEL_MESSAGES_BULK_DELETE = "/channels/%s/messages/bulk-delete",
CHANNEL_MESSAGE_REACTION = "/channels/%s/messages/%s/reactions/%s",
CHANNEL_MESSAGE_REACTIONS = "/channels/%s/messages/%s/reactions",
CHANNEL_MESSAGE_REACTION_ME = "/channels/%s/messages/%s/reactions/%s/@me",
CHANNEL_MESSAGE_REACTION_USER = "/channels/%s/messages/%s/reactions/%s/%s",
CHANNEL_PERMISSION = "/channels/%s/permissions/%s",
CHANNEL_PIN = "/channels/%s/pins/%s",
CHANNEL_PINS = "/channels/%s/pins",
CHANNEL_RECIPIENT = "/channels/%s/recipients/%s",
CHANNEL_TYPING = "/channels/%s/typing",
CHANNEL_WEBHOOKS = "/channels/%s/webhooks",
GATEWAY = "/gateway",
GATEWAY_BOT = "/gateway/bot",
GUILD = "/guilds/%s",
GUILDS = "/guilds",
GUILD_AUDIT_LOGS = "/guilds/%s/audit-logs",
GUILD_BAN = "/guilds/%s/bans/%s",
GUILD_BANS = "/guilds/%s/bans",
GUILD_CHANNELS = "/guilds/%s/channels",
GUILD_EMBED = "/guilds/%s/embed",
GUILD_EMOJI = "/guilds/%s/emojis/%s",
GUILD_EMOJIS = "/guilds/%s/emojis",
GUILD_INTEGRATION = "/guilds/%s/integrations/%s",
GUILD_INTEGRATIONS = "/guilds/%s/integrations",
GUILD_INTEGRATION_SYNC = "/guilds/%s/integrations/%s/sync",
GUILD_INVITES = "/guilds/%s/invites",
GUILD_MEMBER = "/guilds/%s/members/%s",
GUILD_MEMBERS = "/guilds/%s/members",
GUILD_MEMBER_ME_NICK = "/guilds/%s/members/@me/nick",
GUILD_MEMBER_ROLE = "/guilds/%s/members/%s/roles/%s",
GUILD_PRUNE = "/guilds/%s/prune",
GUILD_REGIONS = "/guilds/%s/regions",
GUILD_ROLE = "/guilds/%s/roles/%s",
GUILD_ROLES = "/guilds/%s/roles",
GUILD_WEBHOOKS = "/guilds/%s/webhooks",
INVITE = "/invites/%s",
OAUTH2_APPLICATION_ME = "/oauth2/applications/@me",
USER = "/users/%s",
USER_ME = "/users/@me",
USER_ME_CHANNELS = "/users/@me/channels",
USER_ME_CONNECTIONS = "/users/@me/connections",
USER_ME_GUILD = "/users/@me/guilds/%s",
USER_ME_GUILDS = "/users/@me/guilds",
VOICE_REGIONS = "/voice/regions",
WEBHOOK = "/webhooks/%s",
WEBHOOK_TOKEN = "/webhooks/%s/%s",
WEBHOOK_TOKEN_GITHUB = "/webhooks/%s/%s/github",
WEBHOOK_TOKEN_SLACK = "/webhooks/%s/%s/slack",
}

View File

@@ -0,0 +1,177 @@
local function enum(tbl)
local call = {}
for k, v in pairs(tbl) do
if call[v] then
return error(string.format('enum clash for %q and %q', k, call[v]))
end
call[v] = k
end
return setmetatable({}, {
__call = function(_, k)
if call[k] then
return call[k]
else
return error('invalid enumeration: ' .. tostring(k))
end
end,
__index = function(_, k)
if tbl[k] then
return tbl[k]
else
return error('invalid enumeration: ' .. tostring(k))
end
end,
__pairs = function()
return next, tbl
end,
__newindex = function()
return error('cannot overwrite enumeration')
end,
})
end
local enums = {enum = enum}
enums.defaultAvatar = enum {
blurple = 0,
gray = 1,
green = 2,
orange = 3,
red = 4,
}
enums.notificationSetting = enum {
allMessages = 0,
onlyMentions = 1,
}
enums.channelType = enum {
text = 0,
private = 1,
voice = 2,
group = 3,
category = 4,
}
enums.messageType = enum {
default = 0,
recipientAdd = 1,
recipientRemove = 2,
call = 3,
channelNameChange = 4,
channelIconchange = 5,
pinnedMessage = 6,
memberJoin = 7,
}
enums.relationshipType = enum {
none = 0,
friend = 1,
blocked = 2,
pendingIncoming = 3,
pendingOutgoing = 4,
}
enums.activityType = enum {
default = 0,
streaming = 1,
listening = 2,
}
enums.status = enum {
online = 'online',
idle = 'idle',
doNotDisturb = 'dnd',
invisible = 'invisible',
}
enums.gameType = enum { -- NOTE: deprecated; use activityType
default = 0,
streaming = 1,
listening = 2,
}
enums.verificationLevel = enum {
none = 0,
low = 1,
medium = 2,
high = 3, -- (╯°□°)╯︵ ┻━┻
veryHigh = 4, -- ┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻
}
enums.explicitContentLevel = enum {
none = 0,
medium = 1,
high = 2,
}
enums.permission = enum {
createInstantInvite = 0x00000001,
kickMembers = 0x00000002,
banMembers = 0x00000004,
administrator = 0x00000008,
manageChannels = 0x00000010,
manageGuild = 0x00000020,
addReactions = 0x00000040,
viewAuditLog = 0x00000080,
prioritySpeaker = 0x00000100,
readMessages = 0x00000400,
sendMessages = 0x00000800,
sendTextToSpeech = 0x00001000,
manageMessages = 0x00002000,
embedLinks = 0x00004000,
attachFiles = 0x00008000,
readMessageHistory = 0x00010000,
mentionEveryone = 0x00020000,
useExternalEmojis = 0x00040000,
connect = 0x00100000,
speak = 0x00200000,
muteMembers = 0x00400000,
deafenMembers = 0x00800000,
moveMembers = 0x01000000,
useVoiceActivity = 0x02000000,
changeNickname = 0x04000000,
manageNicknames = 0x08000000,
manageRoles = 0x10000000,
manageWebhooks = 0x20000000,
manageEmojis = 0x40000000,
}
enums.actionType = enum {
guildUpdate = 1,
channelCreate = 10,
channelUpdate = 11,
channelDelete = 12,
channelOverwriteCreate = 13,
channelOverwriteUpdate = 14,
channelOverwriteDelete = 15,
memberKick = 20,
memberPrune = 21,
memberBanAdd = 22,
memberBanRemove = 23,
memberUpdate = 24,
memberRoleUpdate = 25,
roleCreate = 30,
roleUpdate = 31,
roleDelete = 32,
inviteCreate = 40,
inviteUpdate = 41,
inviteDelete = 42,
webhookCreate = 50,
webhookUpdate = 51,
webhookDelete = 52,
emojiCreate = 60,
emojiUpdate = 61,
emojiDelete = 62,
messageDelete = 72,
}
enums.logLevel = enum {
none = 0,
error = 1,
warning = 2,
info = 3,
debug = 4,
}
return enums

View File

@@ -0,0 +1,253 @@
--[[ NOTE:
These standard library extensions are NOT used in Discordia. They are here as a
convenience for those who wish to use them.
There are multiple ways to implement some of these commonly used functions.
Please pay attention to the implementations used here and make sure that they
match your expectations.
You may freely add to, remove, or edit any of the code here without any effect
on the rest of the library. If you do make changes, do be careful when sharing
your expectations with other users.
You can inject these extensions into the standard Lua global tables by
calling either the main module (ex: discordia.extensions()) or each sub-module
(ex: discordia.extensions.string())
]]
local sort, concat = table.sort, table.concat
local insert, remove = table.insert, table.remove
local byte, char = string.byte, string.char
local gmatch, match = string.gmatch, string.match
local rep, find, sub = string.rep, string.find, string.sub
local min, max, random = math.min, math.max, math.random
local ceil, floor = math.ceil, math.floor
local table = {}
function table.count(tbl)
local n = 0
for _ in pairs(tbl) do
n = n + 1
end
return n
end
function table.deepcount(tbl)
local n = 0
for _, v in pairs(tbl) do
n = type(v) == 'table' and n + table.deepcount(v) or n + 1
end
return n
end
function table.copy(tbl)
local ret = {}
for k, v in pairs(tbl) do
ret[k] = v
end
return ret
end
function table.deepcopy(tbl)
local ret = {}
for k, v in pairs(tbl) do
ret[k] = type(v) == 'table' and table.deepcopy(v) or v
end
return ret
end
function table.reverse(tbl)
for i = 1, #tbl do
insert(tbl, i, remove(tbl))
end
end
function table.reversed(tbl)
local ret = {}
for i = #tbl, 1, -1 do
insert(ret, tbl[i])
end
return ret
end
function table.keys(tbl)
local ret = {}
for k in pairs(tbl) do
insert(ret, k)
end
return ret
end
function table.values(tbl)
local ret = {}
for _, v in pairs(tbl) do
insert(ret, v)
end
return ret
end
function table.randomipair(tbl)
local i = random(#tbl)
return i, tbl[i]
end
function table.randompair(tbl)
local rand = random(table.count(tbl))
local n = 0
for k, v in pairs(tbl) do
n = n + 1
if n == rand then
return k, v
end
end
end
function table.sorted(tbl, fn)
local ret = {}
for i, v in ipairs(tbl) do
ret[i] = v
end
sort(ret, fn)
return ret
end
function table.search(tbl, value)
for k, v in pairs(tbl) do
if v == value then
return k
end
end
return nil
end
function table.slice(tbl, start, stop, step)
local ret = {}
for i = start or 1, stop or #tbl, step or 1 do
insert(ret, tbl[i])
end
return ret
end
local string = {}
function string.split(str, delim)
local ret = {}
if not str then
return ret
end
if not delim or delim == '' then
for c in gmatch(str, '.') do
insert(ret, c)
end
return ret
end
local n = 1
while true do
local i, j = find(str, delim, n)
if not i then break end
insert(ret, sub(str, n, i - 1))
n = j + 1
end
insert(ret, sub(str, n))
return ret
end
function string.trim(str)
return match(str, '^%s*(.-)%s*$')
end
function string.pad(str, len, align, pattern)
pattern = pattern or ' '
if align == 'right' then
return rep(pattern, (len - #str) / #pattern) .. str
elseif align == 'center' then
local pad = 0.5 * (len - #str) / #pattern
return rep(pattern, floor(pad)) .. str .. rep(pattern, ceil(pad))
else -- left
return str .. rep(pattern, (len - #str) / #pattern)
end
end
function string.startswith(str, pattern, plain)
local start = 1
return find(str, pattern, start, plain) == start
end
function string.endswith(str, pattern, plain)
local start = #str - #pattern + 1
return find(str, pattern, start, plain) == start
end
function string.levenshtein(str1, str2)
if str1 == str2 then return 0 end
local len1 = #str1
local len2 = #str2
if len1 == 0 then
return len2
elseif len2 == 0 then
return len1
end
local matrix = {}
for i = 0, len1 do
matrix[i] = {[0] = i}
end
for j = 0, len2 do
matrix[0][j] = j
end
for i = 1, len1 do
for j = 1, len2 do
local cost = byte(str1, i) == byte(str2, j) and 0 or 1
matrix[i][j] = min(matrix[i-1][j] + 1, matrix[i][j-1] + 1, matrix[i-1][j-1] + cost)
end
end
return matrix[len1][len2]
end
function string.random(len, mn, mx)
local ret = {}
mn = mn or 0
mx = mx or 255
for _ = 1, len do
insert(ret, char(random(mn, mx)))
end
return concat(ret)
end
local math = {}
function math.clamp(n, minValue, maxValue)
return min(max(n, minValue), maxValue)
end
function math.round(n, i)
local m = 10 ^ (i or 0)
return floor(n * m + 0.5) / m
end
local ext = setmetatable({
table = table,
string = string,
math = math,
}, {__call = function(self)
for _, v in pairs(self) do
v()
end
end})
for n, m in pairs(ext) do
setmetatable(m, {__call = function(self)
for k, v in pairs(self) do
_G[n][k] = v
end
end})
end
return ext

View File

@@ -0,0 +1,103 @@
--[=[
@c ArrayIterable x Iterable
@d Iterable class that contains objects in a constant, ordered fashion, although
the order may change if the internal array is modified. Some versions may use a
map function to shape the objects before they are accessed.
]=]
local wrap, yield = coroutine.wrap, coroutine.yield
local Iterable = require('iterables/Iterable')
local ArrayIterable, get = require('class')('ArrayIterable', Iterable)
function ArrayIterable:__init(array, map)
self._array = array
self._map = map
end
function ArrayIterable:__len()
local array = self._array
if not array or #array == 0 then
return 0
end
local map = self._map
if map then -- map can return nil
return Iterable.__len(self)
else
return #array
end
end
--[=[@p first * The first object in the array]=]
function get.first(self)
local array = self._array
if not array or #array == 0 then
return nil
end
local map = self._map
if map then
for i = 1, #array, 1 do
local v = array[i]
local obj = v and map(v)
if obj then
return obj
end
end
else
return array[1]
end
end
--[=[@p last * The last object in the array]=]
function get.last(self)
local array = self._array
if not array or #array == 0 then
return nil
end
local map = self._map
if map then
for i = #array, 1, -1 do
local v = array[i]
local obj = v and map(v)
if obj then
return obj
end
end
else
return array[#array]
end
end
--[=[
@m iter
@r function
@d Returns an iterator for all contained objects in a consistent order.
]=]
function ArrayIterable:iter()
local array = self._array
if not array or #array == 0 then
return function() -- new closure for consistency
return nil
end
end
local map = self._map
if map then
return wrap(function()
for _, v in ipairs(array) do
local obj = map(v)
if obj then
yield(obj)
end
end
end)
else
local i = 0
return function()
i = i + 1
return array[i]
end
end
end
return ArrayIterable

View File

@@ -0,0 +1,139 @@
--[=[
@c Cache x Iterable
@d description
]=]
local json = require('json')
local Iterable = require('iterables/Iterable')
local null = json.null
local Cache = require('class')('Cache', Iterable)
function Cache:__init(array, constructor, parent)
local objects = {}
for _, data in ipairs(array) do
local obj = constructor(data, parent)
objects[obj:__hash()] = obj
end
self._count = #array
self._objects = objects
self._constructor = constructor
self._parent = parent
end
function Cache:__pairs()
return next, self._objects
end
function Cache:__len()
return self._count
end
local function insert(self, k, obj)
self._objects[k] = obj
self._count = self._count + 1
return obj
end
local function remove(self, k, obj)
self._objects[k] = nil
self._count = self._count - 1
return obj
end
local function hash(data)
-- local meta = getmetatable(data) -- debug
-- assert(meta and meta.__jsontype == 'object') -- debug
if data.id then -- snowflakes
return data.id
elseif data.user then -- members
return data.user.id
elseif data.emoji then -- reactions
return data.emoji.id ~= null and data.emoji.id or data.emoji.name
elseif data.code then -- invites
return data.code
else
return nil, 'json data could not be hashed'
end
end
function Cache:_insert(data)
local k = assert(hash(data))
local old = self._objects[k]
if old then
old:_load(data)
return old
else
local obj = self._constructor(data, self._parent)
return insert(self, k, obj)
end
end
function Cache:_remove(data)
local k = assert(hash(data))
local old = self._objects[k]
if old then
old:_load(data)
return remove(self, k, old)
else
return self._constructor(data, self._parent)
end
end
function Cache:_delete(k)
local old = self._objects[k]
if old then
return remove(self, k, old)
else
return nil
end
end
function Cache:_load(array, update)
if update then
local updated = {}
for _, data in ipairs(array) do
local obj = self:_insert(data)
updated[obj:__hash()] = true
end
for obj in self:iter() do
local k = obj:__hash()
if not updated[k] then
self:_delete(k)
end
end
else
for _, data in ipairs(array) do
self:_insert(data)
end
end
end
--[=[
@m get
@p k *
@r *
@d Returns an individual object by key, where the key should match the result of
calling `__hash` on the contained objects. Unlike Iterable:get, this
method operates with O(1) complexity.
]=]
function Cache:get(k)
return self._objects[k]
end
--[=[
@m iter
@r function
@d Returns an iterator that returns all contained objects. The order of the objects
is not guaranteed.
]=]
function Cache:iter()
local objects, k, obj = self._objects
return function()
k, obj = next(objects, k)
return obj
end
end
return Cache

View File

@@ -0,0 +1,26 @@
--[=[
@c FilteredIterable x Iterable
@d Iterable class that wraps another iterable and serves a subset of the objects
that the original iterable contains.
]=]
local Iterable = require('iterables/Iterable')
local FilteredIterable = require('class')('FilteredIterable', Iterable)
function FilteredIterable:__init(base, predicate)
self._base = base
self._predicate = predicate
end
--[=[
@m iter
@r function
@d Returns an iterator that returns all contained objects. The order of the objects
is not guaranteed.
]=]
function FilteredIterable:iter()
return self._base:findAll(self._predicate)
end
return FilteredIterable

Some files were not shown because too many files have changed in this diff Show More