Commit 6981cd35 authored by Grant's avatar Grant
Browse files

Initial commit

parents
Loading
Loading
Loading
Loading

.gitignore

0 → 100644
+3 −0
Original line number Diff line number Diff line
dist
node_modules
 No newline at end of file

package-lock.json

0 → 100644
+151 −0
Original line number Diff line number Diff line
{
  "name": "@sc07/shadow",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "@sc07/shadow",
      "version": "1.0.0",
      "license": "MIT",
      "dependencies": {
        "eventemitter3": "^5.0.1",
        "redis": "^4.6.15"
      },
      "devDependencies": {
        "@tsconfig/recommended": "^1.0.7",
        "@types/node": "^20.14.12",
        "typescript": "^5.5.4"
      }
    },
    "node_modules/@redis/bloom": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
      "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
      "peerDependencies": {
        "@redis/client": "^1.0.0"
      }
    },
    "node_modules/@redis/client": {
      "version": "1.5.17",
      "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.17.tgz",
      "integrity": "sha512-IPvU9A31qRCZ7lds/x+ksuK/UMndd0EASveAvCvEtFFKIZjZ+m/a4a0L7S28KEWoR5ka8526hlSghDo4Hrc2Hg==",
      "dependencies": {
        "cluster-key-slot": "1.1.2",
        "generic-pool": "3.9.0",
        "yallist": "4.0.0"
      },
      "engines": {
        "node": ">=14"
      }
    },
    "node_modules/@redis/graph": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz",
      "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==",
      "peerDependencies": {
        "@redis/client": "^1.0.0"
      }
    },
    "node_modules/@redis/json": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz",
      "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==",
      "peerDependencies": {
        "@redis/client": "^1.0.0"
      }
    },
    "node_modules/@redis/search": {
      "version": "1.1.6",
      "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.6.tgz",
      "integrity": "sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw==",
      "peerDependencies": {
        "@redis/client": "^1.0.0"
      }
    },
    "node_modules/@redis/time-series": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz",
      "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==",
      "peerDependencies": {
        "@redis/client": "^1.0.0"
      }
    },
    "node_modules/@tsconfig/recommended": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/@tsconfig/recommended/-/recommended-1.0.7.tgz",
      "integrity": "sha512-xiNMgCuoy4mCL4JTywk9XFs5xpRUcKxtWEcMR6FNMtsgewYTIgIR+nvlP4A4iRCAzRsHMnPhvTRrzp4AGcRTEA==",
      "dev": true
    },
    "node_modules/@types/node": {
      "version": "20.14.12",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz",
      "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==",
      "dev": true,
      "dependencies": {
        "undici-types": "~5.26.4"
      }
    },
    "node_modules/cluster-key-slot": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
      "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/eventemitter3": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
    },
    "node_modules/generic-pool": {
      "version": "3.9.0",
      "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
      "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
      "engines": {
        "node": ">= 4"
      }
    },
    "node_modules/redis": {
      "version": "4.6.15",
      "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.15.tgz",
      "integrity": "sha512-2NtuOpMW3tnYzBw6S8mbXSX7RPzvVFCA2wFJq9oErushO2UeBkxObk+uvo7gv7n0rhWeOj/IzrHO8TjcFlRSOg==",
      "workspaces": [
        "./packages/*"
      ],
      "dependencies": {
        "@redis/bloom": "1.2.0",
        "@redis/client": "1.5.17",
        "@redis/graph": "1.1.1",
        "@redis/json": "1.0.6",
        "@redis/search": "1.1.6",
        "@redis/time-series": "1.0.5"
      }
    },
    "node_modules/typescript": {
      "version": "5.5.4",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
      "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
      "dev": true,
      "bin": {
        "tsc": "bin/tsc",
        "tsserver": "bin/tsserver"
      },
      "engines": {
        "node": ">=14.17"
      }
    },
    "node_modules/undici-types": {
      "version": "5.26.5",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
      "dev": true
    },
    "node_modules/yallist": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
    }
  }
}

package.json

0 → 100644
+28 −0
Original line number Diff line number Diff line
{
  "name": "@sc07/shadow",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "/dist"
  ],
  "exports": {
    ".": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "description": "",
  "devDependencies": {
    "@tsconfig/recommended": "^1.0.7",
    "@types/node": "^20.14.12",
    "typescript": "^5.5.4"
  },
  "dependencies": {
    "eventemitter3": "^5.0.1",
    "redis": "^4.6.15"
  }
}

src/ShadowAPI.ts

0 → 100644
+81 −0
Original line number Diff line number Diff line
import EventEmitter from "eventemitter3";
import { UnbanEvent } from "./events/UnbanEvent";
import { BanEvent } from "./events/BanEvent";
import { RedisClientType, createClient } from "redis";
import { ShadowEvent } from "./events";

interface Events {
  ban: (info: BanEvent) => void;
  unban: (info: UnbanEvent) => void;
}

export class ShadowAPI extends EventEmitter<Events> {
  private ready: boolean = false;
  private shadow_host: string;
  private redis_sub: RedisClientType;

  private RedisEvents = [BanEvent, UnbanEvent] as const;

  private constructor(shadow_host: string, redis_uri: string) {
    super();

    this.shadow_host = shadow_host;

    this.redis_sub = createClient({ url: redis_uri });
  }

  /**
   * Check shadow_host to make sure it's a shadow endpoint
   * @returns
   */
  async probeHost(): Promise<boolean> {
    // TODO: check shadow host and verify it's running & is a shadow endpoint
    return true;
  }

  async connectToRedis() {
    await this.redis_sub.connect();
    this.setupRedisSubscriptions();
  }

  async connect() {
    if (this.ready) return;

    await this.probeHost();
    await this.connectToRedis();

    this.ready = true;
  }

  setupRedisSubscriptions() {
    // TODO: maybe dynamically create this?
    // or the Events interface?
    const classMapping: [(typeof this.RedisEvents)[number], keyof Events][] = [
      [BanEvent, "ban"],
      [UnbanEvent, "unban"],
    ];

    const prefix = "shadow:";

    for (const [eventcl, emit] of classMapping) {
      this.redis_sub.subscribe(prefix + eventcl.getChannel(), (data) => {
        this.emit(emit, eventcl.fromString(data) as any);
      });
    }
  }

  /**
   * Create shadow instance
   * @param shadow_host
   * @param redis_uri
   * @returns
   */
  static create(shadow_host: string, redis_uri: string): ShadowAPI {
    const shadow = new this(shadow_host, redis_uri);
    return shadow;
  }

  static getAllEvents(): ShadowEvent[] {
    return [BanEvent, UnbanEvent];
  }
}

src/events/BanEvent.ts

0 → 100644
+43 −0
Original line number Diff line number Diff line
import { IBroadcastable } from "..";

export class BanEvent implements IBroadcastable {
  private sub: string;
  private expiresAt: Date;
  private public_reason?: string;

  constructor(sub: string, expiresAt: Date, public_reason?: string) {
    this.sub = sub;
    this.expiresAt = expiresAt;
    this.public_reason = public_reason;
  }

  static getChannel() {
    return "ban";
  }

  static fromString(data: string): BanEvent {
    const [sub, expiresAt, public_reason] = data.split(":");

    return new this(
      sub,
      new Date(Buffer.from(expiresAt, "base64").toString()),
      public_reason === "null"
        ? undefined
        : Buffer.from(public_reason, "base64").toString()
    );
  }

  getChannel() {
    return BanEvent.getChannel();
  }

  getMessage() {
    return [
      this.sub,
      Buffer.from(this.expiresAt.toISOString()).toString("base64"),
      this.public_reason
        ? Buffer.from(this.public_reason).toString("base64")
        : "null",
    ].join(":");
  }
}