mirror of
https://github.com/FoxxMD/context-mod.git
synced 2026-01-14 07:57:57 -05:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
296f1c8dee | ||
|
|
77856a6d97 | ||
|
|
e32ac60db5 | ||
|
|
052c1218c6 | ||
|
|
fcf718f1d0 | ||
|
|
95216b3950 | ||
|
|
58a21e8d05 | ||
|
|
49ac8cda19 | ||
|
|
e736379f85 | ||
|
|
c0e1a93fb4 | ||
|
|
bd35b06ebf | ||
|
|
f852e85234 | ||
|
|
661ae11e18 |
@@ -13,7 +13,7 @@ coverage
|
||||
*.json5
|
||||
*.yaml
|
||||
*.yml
|
||||
|
||||
*.env
|
||||
|
||||
# exceptions
|
||||
!heroku.yml
|
||||
|
||||
3
.github/push-hook-sample.json
vendored
Normal file
3
.github/push-hook-sample.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ref": "refs/heads/edge"
|
||||
}
|
||||
@@ -1,4 +1,14 @@
|
||||
name: Publish Docker image to Dockerhub
|
||||
name: Publish Docker image to registries
|
||||
|
||||
# Builds image and tags based on the type of push event:
|
||||
# * branch push -> tag is branch name IE context-mod:edge
|
||||
# * release (tag) -> tag is release name IE context-mod:0.13.0
|
||||
#
|
||||
# Then pushes tagged images to multiple registries
|
||||
#
|
||||
# Based on
|
||||
# https://github.com/docker/build-push-action/blob/master/docs/advanced/push-multi-registries.md
|
||||
# https://github.com/docker/metadata-action
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -13,8 +23,12 @@ on:
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
name: Build and Push Docker image to registries
|
||||
runs-on: ubuntu-latest
|
||||
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
@@ -25,12 +39,22 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: foxxmd/context-mod
|
||||
images: |
|
||||
foxxmd/context-mod
|
||||
ghcr.io/foxxmd/context-mod
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ endsWith(github.ref, 'master') }}
|
||||
@@ -40,7 +64,8 @@ jobs:
|
||||
latest=false
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v2
|
||||
if: ${{ !env.ACT }}
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -336,6 +336,7 @@ web_modules/
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
*.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
@@ -391,6 +392,7 @@ dist
|
||||
*.json5
|
||||
|
||||
!src/Schema/*.json
|
||||
!.github/push-hook-sample.json
|
||||
!docs/**/*.json5
|
||||
!docs/**/*.yaml
|
||||
!docs/**/*.json
|
||||
|
||||
3
act.env.example
Normal file
3
act.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
GITHUB_TOKEN=
|
||||
DOCKERHUB_USERNAME=
|
||||
DOCKER_PASSWORD=
|
||||
@@ -1,5 +1,17 @@
|
||||
TODO add more development sections...
|
||||
|
||||
# Developing/Testing Github Actions
|
||||
|
||||
Use [act](https://github.com/nektos/act) to run Github actions locally.
|
||||
|
||||
An example secrets file can be found in the project working directory at [act.env.example](act.env.example)
|
||||
|
||||
Modify [push-hook-sample.json](.github/push-hook-sample.json) to point to the local branch you want to run a `push` event trigger on, then run this command from the project working directory:
|
||||
|
||||
```bash
|
||||
act -e .github/push-hook-sample.json --secret-file act.env
|
||||
```
|
||||
|
||||
# Mocking Reddit API
|
||||
|
||||
Using [MockServer](https://www.mock-server.com/)
|
||||
|
||||
@@ -8,7 +8,10 @@ ContextMod can be run on almost any operating system but it is recommended to us
|
||||
|
||||
PROTIP: Using a container management tool like [Portainer.io CE](https://www.portainer.io/products/community-edition) will help with setup/configuration tremendously.
|
||||
|
||||
### [Dockerhub](https://hub.docker.com/r/foxxmd/context-mod)
|
||||
Images available from these registeries:
|
||||
|
||||
* [Dockerhub](https://hub.docker.com/r/foxxmd/context-mod) - `docker.io/foxxmd/context-mod`
|
||||
* [GHCR](https://github.com/foxxmd/context-mod/pkgs/container/context-mod) - `ghcr.io/foxxmd/context-mod`
|
||||
|
||||
An example of starting the container using the [minimum configuration](/docs/operator/configuration.md#minimum-config):
|
||||
|
||||
@@ -17,7 +20,7 @@ An example of starting the container using the [minimum configuration](/docs/ope
|
||||
* Expose the web interface using the container port `8085`
|
||||
|
||||
```
|
||||
docker run -d -v /host/path/folder:/config -p 8085:8085 foxxmd/context-mod
|
||||
docker run -d -v /host/path/folder:/config -p 8085:8085 ghcr.io/foxxmd/context-mod:latest
|
||||
```
|
||||
|
||||
The location of `DATA_DIR` in the container can be changed by passing it as an environmental variable EX `-e "DATA_DIR=/home/abc/config`
|
||||
@@ -34,7 +37,7 @@ To get the UID and GID for the current user run these commands from a terminal:
|
||||
* `id -g` -- prints GID
|
||||
|
||||
```
|
||||
docker run -d -v /host/path/folder:/config -p 8085:8085 -e PUID=1000 -e PGID=1000 foxxmd/context-mod
|
||||
docker run -d -v /host/path/folder:/config -p 8085:8085 -e PUID=1000 -e PGID=1000 ghcr.io/foxxmd/context-mod:latest
|
||||
```
|
||||
|
||||
## Locally
|
||||
|
||||
@@ -245,10 +245,11 @@ export const authorCriteriaProperties = ['name', 'flairCssClass', 'flairText', '
|
||||
* */
|
||||
export interface AuthorCriteria {
|
||||
/**
|
||||
* A list of reddit usernames (case-insensitive) to match against. Do not include the "u/" prefix
|
||||
* A list of reddit usernames (case-insensitive) or regular expressions to match against. Do not include the "u/" prefix
|
||||
*
|
||||
*
|
||||
* EX to match against /u/FoxxMD and /u/AnotherUser use ["FoxxMD","AnotherUser"]
|
||||
* @examples ["FoxxMD","AnotherUser"]
|
||||
* @examples ["FoxxMD","AnotherUser", "/.*Foxx.\/*i"]
|
||||
* */
|
||||
name?: string[],
|
||||
/**
|
||||
|
||||
@@ -42,4 +42,4 @@ export const filterCriteriaDefault: FilterCriteriaDefaults = {
|
||||
export const defaultDataDir = path.resolve(__dirname, '../..');
|
||||
export const defaultConfigFilenames = ['config.json', 'config.yaml'];
|
||||
|
||||
export const VERSION = '0.12.1';
|
||||
export const VERSION = '0.12.2';
|
||||
|
||||
@@ -418,6 +418,7 @@ export class ConfigBuilder {
|
||||
}
|
||||
structuredRuns.push({
|
||||
...r,
|
||||
filterCriteriaDefaults: configFilterDefaultsFromRun,
|
||||
checks: structuredChecks,
|
||||
authorIs: derivedRunAuthorIs,
|
||||
itemIs: derivedRunItemIs
|
||||
@@ -642,7 +643,7 @@ const getNamedOrReturn = <T>(namedFilters: Map<string, NamedCriteria<T>>, filter
|
||||
if(!namedFilters.has(x.toLocaleLowerCase())) {
|
||||
throw new Error(`No named ${filterName} criteria with the name "${x}"`);
|
||||
}
|
||||
return namedFilters.get(x) as NamedCriteria<T>;
|
||||
return namedFilters.get(x.toLocaleLowerCase()) as NamedCriteria<T>;
|
||||
}
|
||||
if(asNamedCriteria(x)) {
|
||||
return x;
|
||||
|
||||
@@ -24,7 +24,7 @@ import {RunResultEntity} from "../Common/Entities/RunResultEntity";
|
||||
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {RunnableBase} from "../Common/RunnableBase";
|
||||
import {RunnableBaseJson, RunnableBaseOptions, StructuredRunnableBase} from "../Common/Infrastructure/Runnable";
|
||||
import {FilterCriteriaDefaults} from "../Common/Infrastructure/Filters/FilterShapes";
|
||||
import {FilterCriteriaDefaults, FilterCriteriaDefaultsJson} from "../Common/Infrastructure/Filters/FilterShapes";
|
||||
import {IncludesData} from "../Common/Infrastructure/Includes";
|
||||
|
||||
export class Run extends RunnableBase {
|
||||
@@ -284,7 +284,7 @@ export interface IRun extends PostBehavior, RunnableBaseJson {
|
||||
*
|
||||
* Default behavior is to exclude all mods and automoderator from checks
|
||||
* */
|
||||
filterCriteriaDefaults?: FilterCriteriaDefaults
|
||||
filterCriteriaDefaults?: FilterCriteriaDefaultsJson
|
||||
|
||||
/**
|
||||
* Use this option to override the `dryRun` setting for all Actions of all Checks in this Run
|
||||
@@ -326,4 +326,5 @@ export interface RunConfigHydratedData extends IRun {
|
||||
|
||||
export interface RunConfigObject extends Omit<RunConfigHydratedData, 'authorIs' | 'itemIs'>, StructuredRunnableBase {
|
||||
checks: ActivityCheckObject[]
|
||||
filterCriteriaDefaults?: FilterCriteriaDefaults
|
||||
}
|
||||
|
||||
@@ -289,10 +289,11 @@
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "A list of reddit usernames (case-insensitive) to match against. Do not include the \"u/\" prefix\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"description": "A list of reddit usernames (case-insensitive) or regular expressions to match against. Do not include the \"u/\" prefix\n\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"examples": [
|
||||
"FoxxMD",
|
||||
"AnotherUser"
|
||||
"AnotherUser",
|
||||
"/.*Foxx./*i"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
|
||||
@@ -665,10 +665,11 @@
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "A list of reddit usernames (case-insensitive) to match against. Do not include the \"u/\" prefix\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"description": "A list of reddit usernames (case-insensitive) or regular expressions to match against. Do not include the \"u/\" prefix\n\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"examples": [
|
||||
"FoxxMD",
|
||||
"AnotherUser"
|
||||
"AnotherUser",
|
||||
"/.*Foxx./*i"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -2180,69 +2181,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FilterCriteriaDefaults": {
|
||||
"properties": {
|
||||
"authorIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FilterOptions<AuthorCriteria>"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AuthorCriteria"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"authorIsBehavior": {
|
||||
"enum": [
|
||||
"merge",
|
||||
"replace"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FilterOptions<TypedActivityState>"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"itemIsBehavior": {
|
||||
"description": "Determine how itemIs defaults behave when itemIs is present on the check\n\n* merge => adds defaults to check's itemIs\n* replace => check itemIs will replace defaults (no defaults used)",
|
||||
"enum": [
|
||||
"merge",
|
||||
"replace"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FilterCriteriaDefaultsJson": {
|
||||
"properties": {
|
||||
"authorIs": {
|
||||
@@ -2313,62 +2251,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FilterOptions<AuthorCriteria>": {
|
||||
"properties": {
|
||||
"exclude": {
|
||||
"description": "Only runs if `include` is not present. Each Criteria is comprised of conditions that the filter (Author/Item) being checked must \"not\" pass. See excludeCondition for set behavior\n\nEX: `isMod: true, name: Automoderator` => Will pass if the Author IS NOT a mod and IS NOT named Automoderator",
|
||||
"items": {
|
||||
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"excludeCondition": {
|
||||
"default": "OR",
|
||||
"description": "* OR => if ANY exclude condition \"does not\" pass then the exclude test passes\n* AND => if ALL exclude conditions \"do not\" pass then the exclude test passes\n\nDefaults to OR",
|
||||
"enum": [
|
||||
"AND",
|
||||
"OR"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"include": {
|
||||
"description": "Will \"pass\" if any set of Criteria passes",
|
||||
"items": {
|
||||
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FilterOptions<TypedActivityState>": {
|
||||
"properties": {
|
||||
"exclude": {
|
||||
"description": "Only runs if `include` is not present. Each Criteria is comprised of conditions that the filter (Author/Item) being checked must \"not\" pass. See excludeCondition for set behavior\n\nEX: `isMod: true, name: Automoderator` => Will pass if the Author IS NOT a mod and IS NOT named Automoderator",
|
||||
"items": {
|
||||
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"excludeCondition": {
|
||||
"default": "OR",
|
||||
"description": "* OR => if ANY exclude condition \"does not\" pass then the exclude test passes\n* AND => if ALL exclude conditions \"do not\" pass then the exclude test passes\n\nDefaults to OR",
|
||||
"enum": [
|
||||
"AND",
|
||||
"OR"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"include": {
|
||||
"description": "Will \"pass\" if any set of Criteria passes",
|
||||
"items": {
|
||||
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FilterOptionsConfig<ActivityState>": {
|
||||
"properties": {
|
||||
"exclude": {
|
||||
@@ -5362,7 +5244,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"filterCriteriaDefaults": {
|
||||
"$ref": "#/definitions/FilterCriteriaDefaults",
|
||||
"$ref": "#/definitions/FilterCriteriaDefaultsJson",
|
||||
"description": "Set the default filter criteria for all checks. If this property is specified it will override any defaults passed from the bot's config\n\nDefault behavior is to exclude all mods and automoderator from checks"
|
||||
},
|
||||
"itemIs": {
|
||||
|
||||
@@ -679,10 +679,11 @@
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "A list of reddit usernames (case-insensitive) to match against. Do not include the \"u/\" prefix\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"description": "A list of reddit usernames (case-insensitive) or regular expressions to match against. Do not include the \"u/\" prefix\n\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"examples": [
|
||||
"FoxxMD",
|
||||
"AnotherUser"
|
||||
"AnotherUser",
|
||||
"/.*Foxx./*i"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
|
||||
@@ -133,10 +133,11 @@
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "A list of reddit usernames (case-insensitive) to match against. Do not include the \"u/\" prefix\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"description": "A list of reddit usernames (case-insensitive) or regular expressions to match against. Do not include the \"u/\" prefix\n\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"examples": [
|
||||
"FoxxMD",
|
||||
"AnotherUser"
|
||||
"AnotherUser",
|
||||
"/.*Foxx./*i"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
|
||||
@@ -594,10 +594,11 @@
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "A list of reddit usernames (case-insensitive) to match against. Do not include the \"u/\" prefix\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"description": "A list of reddit usernames (case-insensitive) or regular expressions to match against. Do not include the \"u/\" prefix\n\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"examples": [
|
||||
"FoxxMD",
|
||||
"AnotherUser"
|
||||
"AnotherUser",
|
||||
"/.*Foxx./*i"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
|
||||
@@ -562,10 +562,11 @@
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "A list of reddit usernames (case-insensitive) to match against. Do not include the \"u/\" prefix\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"description": "A list of reddit usernames (case-insensitive) or regular expressions to match against. Do not include the \"u/\" prefix\n\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"examples": [
|
||||
"FoxxMD",
|
||||
"AnotherUser"
|
||||
"AnotherUser",
|
||||
"/.*Foxx./*i"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
|
||||
@@ -676,10 +676,11 @@
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "A list of reddit usernames (case-insensitive) to match against. Do not include the \"u/\" prefix\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"description": "A list of reddit usernames (case-insensitive) or regular expressions to match against. Do not include the \"u/\" prefix\n\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
|
||||
"examples": [
|
||||
"FoxxMD",
|
||||
"AnotherUser"
|
||||
"AnotherUser",
|
||||
"/.*Foxx./*i"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -1961,13 +1962,10 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FilterCriteriaDefaults": {
|
||||
"FilterCriteriaDefaultsJson": {
|
||||
"properties": {
|
||||
"authorIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FilterOptions<AuthorCriteria>"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"anyOf": [
|
||||
@@ -1976,12 +1974,19 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
|
||||
}
|
||||
]
|
||||
],
|
||||
"description": "Determine how authorIs defaults behave when authorIs is present on the check\n\n* merge => merges defaults with check's authorIs\n* replace => check authorIs will replace defaults (no defaults used)"
|
||||
},
|
||||
"authorIsBehavior": {
|
||||
"enum": [
|
||||
@@ -1992,9 +1997,6 @@
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FilterOptions<TypedActivityState>"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"anyOf": [
|
||||
@@ -2006,10 +2008,16 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2024,62 +2032,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FilterOptions<AuthorCriteria>": {
|
||||
"properties": {
|
||||
"exclude": {
|
||||
"description": "Only runs if `include` is not present. Each Criteria is comprised of conditions that the filter (Author/Item) being checked must \"not\" pass. See excludeCondition for set behavior\n\nEX: `isMod: true, name: Automoderator` => Will pass if the Author IS NOT a mod and IS NOT named Automoderator",
|
||||
"items": {
|
||||
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"excludeCondition": {
|
||||
"default": "OR",
|
||||
"description": "* OR => if ANY exclude condition \"does not\" pass then the exclude test passes\n* AND => if ALL exclude conditions \"do not\" pass then the exclude test passes\n\nDefaults to OR",
|
||||
"enum": [
|
||||
"AND",
|
||||
"OR"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"include": {
|
||||
"description": "Will \"pass\" if any set of Criteria passes",
|
||||
"items": {
|
||||
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FilterOptions<TypedActivityState>": {
|
||||
"properties": {
|
||||
"exclude": {
|
||||
"description": "Only runs if `include` is not present. Each Criteria is comprised of conditions that the filter (Author/Item) being checked must \"not\" pass. See excludeCondition for set behavior\n\nEX: `isMod: true, name: Automoderator` => Will pass if the Author IS NOT a mod and IS NOT named Automoderator",
|
||||
"items": {
|
||||
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"excludeCondition": {
|
||||
"default": "OR",
|
||||
"description": "* OR => if ANY exclude condition \"does not\" pass then the exclude test passes\n* AND => if ALL exclude conditions \"do not\" pass then the exclude test passes\n\nDefaults to OR",
|
||||
"enum": [
|
||||
"AND",
|
||||
"OR"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"include": {
|
||||
"description": "Will \"pass\" if any set of Criteria passes",
|
||||
"items": {
|
||||
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FilterOptionsConfig<ActivityState>": {
|
||||
"properties": {
|
||||
"exclude": {
|
||||
@@ -4933,7 +4885,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"filterCriteriaDefaults": {
|
||||
"$ref": "#/definitions/FilterCriteriaDefaults",
|
||||
"$ref": "#/definitions/FilterCriteriaDefaultsJson",
|
||||
"description": "Set the default filter criteria for all checks. If this property is specified it will override any defaults passed from the bot's config\n\nDefault behavior is to exclude all mods and automoderator from checks"
|
||||
},
|
||||
"itemIs": {
|
||||
|
||||
@@ -2780,7 +2780,7 @@ export class SubredditResources {
|
||||
const authPass = () => {
|
||||
|
||||
for (const n of nameVal) {
|
||||
if (n.toLowerCase() === authorName.toLowerCase()) {
|
||||
if (testMaybeStringRegex(n, authorName)[0]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1029,6 +1029,7 @@ const webClient = async (options: OperatorConfigWithFileContext) => {
|
||||
...req.instancesViewData,
|
||||
bots: resp.bots,
|
||||
now: dayjs().add(1, 'minute').format('YYYY-MM-DDTHH:mm'),
|
||||
defaultExpire: dayjs().add(1, 'day').format('YYYY-MM-DDTHH:mm'),
|
||||
botId: (req.instance as CMInstance).getName(),
|
||||
isOperator: isOp,
|
||||
system: isOp ? {
|
||||
@@ -1462,27 +1463,6 @@ const webClient = async (options: OperatorConfigWithFileContext) => {
|
||||
}
|
||||
emitter.on('log', botWebLogListener);
|
||||
socketListeners.set(socket.id, [...(socketListeners.get(socket.id) || []), botWebLogListener]);
|
||||
|
||||
// only setup streams if the user can actually access them (not just a web operator)
|
||||
if(session.authBotId !== undefined) {
|
||||
// streaming stats from client
|
||||
const newStreams: (AbortController | NodeJS.Timeout)[] = [];
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const resp = await got.get(`${bot.normalUrl}/stats`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${createToken(bot, user)}`,
|
||||
}
|
||||
}).json() as object;
|
||||
io.to(session.id).emit('opStats', resp);
|
||||
} catch (err: any) {
|
||||
bot.logger.error(new ErrorWithCause('Could not retrieve stats', {cause: err}));
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 5000);
|
||||
newStreams.push(interval);
|
||||
sockStreams.set(socket.id, newStreams);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,24 @@ const generateDeltaResponse = (data: Record<string, any>, hash: string, response
|
||||
// delta[k] = {new: newGuestItems, removed: removedGuestItems};
|
||||
delta[k] = v;
|
||||
break;
|
||||
case 'subreddits':
|
||||
// only used by opStats!
|
||||
const refSubs = reference[k].map((x: any) => `${x.name}-${x.indicator}`);
|
||||
const lastestSubs = v.map((x: any) => `${x.name}-${x.indicator}`);
|
||||
|
||||
if(symmetricalDifference(refSubs, lastestSubs).length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const changedSubs = v.reduce((acc: any[], curr: any) => {
|
||||
if(!reference[k].some((x: any) => x.name === curr.name && x.indicator === curr.indicator)) {
|
||||
acc.push(curr);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
delta[k] = changedSubs;
|
||||
break
|
||||
default:
|
||||
if(!deepEqual(v, reference[k])) {
|
||||
if(v !== null && typeof v === 'object' && reference[k] !== null && typeof reference[k] === 'object') {
|
||||
@@ -104,6 +122,67 @@ const generateDeltaResponse = (data: Record<string, any>, hash: string, response
|
||||
return resp;
|
||||
}
|
||||
|
||||
export const opStatResponse = () => {
|
||||
const middleware = [
|
||||
authUserCheck(),
|
||||
botRoute(false)
|
||||
];
|
||||
|
||||
const response = async(req: Request, res: Response) =>
|
||||
{
|
||||
const responseType = req.query.type === 'delta' ? 'delta' : 'full';
|
||||
|
||||
let bots: Bot[] = [];
|
||||
if(req.serverBot !== undefined) {
|
||||
bots = [req.serverBot];
|
||||
} else if(req.user !== undefined) {
|
||||
bots = req.user.accessibleBots(req.botApp.bots);
|
||||
}
|
||||
const resp = [];
|
||||
let index = 1;
|
||||
for(const b of bots) {
|
||||
resp.push({name: b.botName ?? `Bot ${index}`, data: {
|
||||
status: b.running ? 'RUNNING' : 'NOT RUNNING',
|
||||
indicator: b.running ? 'green' : 'red',
|
||||
running: b.running,
|
||||
startedAt: b.startedAt.local().format('MMMM D, YYYY h:mm A Z'),
|
||||
error: b.error,
|
||||
subreddits: req.user?.accessibleSubreddits(b).map((manager: Manager) => {
|
||||
let indicator;
|
||||
if (manager.managerState.state === RUNNING && manager.queueState.state === RUNNING && manager.eventsState.state === RUNNING) {
|
||||
indicator = 'green';
|
||||
} else if (manager.managerState.state === STOPPED && manager.queueState.state === STOPPED && manager.eventsState.state === STOPPED) {
|
||||
indicator = 'red';
|
||||
} else {
|
||||
indicator = 'yellow';
|
||||
}
|
||||
return {
|
||||
name: manager.displayLabel,
|
||||
indicator,
|
||||
};
|
||||
}),
|
||||
}});
|
||||
index++;
|
||||
}
|
||||
|
||||
const deltaResp = [];
|
||||
for(const bResp of resp) {
|
||||
const hash = `${req.user?.name}-opstats-${bResp.name}`;
|
||||
const respData = generateDeltaResponse(bResp.data, hash, responseType);
|
||||
if(Object.keys(respData).length !== 0) {
|
||||
deltaResp.push({data: respData, name: bResp.name});
|
||||
}
|
||||
}
|
||||
|
||||
if(deltaResp.length === 0) {
|
||||
return res.status(304).send();
|
||||
}
|
||||
return res.json(deltaResp);
|
||||
}
|
||||
|
||||
return [...middleware, response];
|
||||
}
|
||||
|
||||
const liveStats = () => {
|
||||
const middleware = [
|
||||
authUserCheck(),
|
||||
|
||||
@@ -13,7 +13,7 @@ import http from "http";
|
||||
import {heartbeat} from "./routes/authenticated/applicationRoutes";
|
||||
import logs from "./routes/authenticated/user/logs";
|
||||
import status from './routes/authenticated/user/status';
|
||||
import liveStats from './routes/authenticated/user/liveStats';
|
||||
import liveStats, {opStatResponse} from './routes/authenticated/user/liveStats';
|
||||
import {
|
||||
actionedEventsRoute,
|
||||
actionRoute, addGuestModRoute,
|
||||
@@ -161,41 +161,8 @@ const rcbServer = async function (options: OperatorConfigWithFileContext) {
|
||||
|
||||
server.getAsync('/logs', ...logs());
|
||||
|
||||
server.getAsync('/stats', [authUserCheck(), botRoute(false)], async (req: Request, res: Response) => {
|
||||
let bots: Bot[] = [];
|
||||
if(req.serverBot !== undefined) {
|
||||
bots = [req.serverBot];
|
||||
} else if(req.user !== undefined) {
|
||||
bots = req.user.accessibleBots(req.botApp.bots);
|
||||
}
|
||||
const resp = [];
|
||||
let index = 1;
|
||||
for(const b of bots) {
|
||||
resp.push({name: b.botName ?? `Bot ${index}`, data: {
|
||||
status: b.running ? 'RUNNING' : 'NOT RUNNING',
|
||||
indicator: b.running ? 'green' : 'red',
|
||||
running: b.running,
|
||||
startedAt: b.startedAt.local().format('MMMM D, YYYY h:mm A Z'),
|
||||
error: b.error,
|
||||
subreddits: req.user?.accessibleSubreddits(b).map((manager: Manager) => {
|
||||
let indicator;
|
||||
if (manager.managerState.state === RUNNING && manager.queueState.state === RUNNING && manager.eventsState.state === RUNNING) {
|
||||
indicator = 'green';
|
||||
} else if (manager.managerState.state === STOPPED && manager.queueState.state === STOPPED && manager.eventsState.state === STOPPED) {
|
||||
indicator = 'red';
|
||||
} else {
|
||||
indicator = 'yellow';
|
||||
}
|
||||
return {
|
||||
name: manager.displayLabel,
|
||||
indicator,
|
||||
};
|
||||
}),
|
||||
}});
|
||||
index++;
|
||||
}
|
||||
return res.json(resp);
|
||||
});
|
||||
server.getAsync('/stats', ...opStatResponse());
|
||||
|
||||
const passLogs = async (req: Request, res: Response, next: Function) => {
|
||||
// @ts-ignore
|
||||
req.sysLogs = sysLogs;
|
||||
|
||||
@@ -190,3 +190,8 @@ li > ul {
|
||||
.introjs-tooltip-title,.introjs-tooltiptext {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.guestAdd {
|
||||
border-top: 1px solid white;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
BIN
src/Web/assets/public/logo.png
Normal file
BIN
src/Web/assets/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -21,6 +21,7 @@
|
||||
<div class="container mx-auto">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center flex-grow pr-4">
|
||||
<a href="/"><img src="/public/logo.png" style="max-height:40px; padding-right: 0.75rem;"/></a>
|
||||
<% if(locals.title !== undefined) { %>
|
||||
<a href="/events?instance=<%= instance %>&bot=<%= bot %><%= subreddit !== undefined ? `&subreddit=${subreddit}` : '' %>"><%= title %></a>
|
||||
<% } %>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
statusEl.innerHTML = '<span class="iconify-inline green" data-icon="ep:circle-check-filled"></span>';
|
||||
break;
|
||||
default:
|
||||
dstatusEl.innerHTML = '<span class="iconify-inline red" data-icon="ep:warning-filled"></span>';
|
||||
statusEl.innerHTML = '<span class="iconify-inline red" data-icon="ep:warning-filled"></span>';
|
||||
break;
|
||||
}
|
||||
// data.page.updated_at
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div class="container mx-auto">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center flex-grow pr-4">
|
||||
<a href="/"><img src="/public/logo.png" style="max-height:40px;"/></a>
|
||||
<% if(locals.instances !== undefined) { %>
|
||||
<ul class="inline-flex flex-wrap">
|
||||
<% instances.forEach(function (data) { %>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div class="container mx-auto">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center flex-grow pr-4">
|
||||
<a href="/"><img src="/public/logo.png" style="max-height:40px; padding-right: 0.75rem;"/></a>
|
||||
<% if(locals.title !== undefined) { %>
|
||||
<%= title %>
|
||||
<% } %>
|
||||
|
||||
@@ -288,10 +288,25 @@
|
||||
style="width:200px;"
|
||||
class="guestAddName border-gray-50 placeholder-gray-500 rounded mr-1 p-1 text-black"
|
||||
placeholder="userName"/>
|
||||
<div class="mt-2">
|
||||
<span class="has-tooltip">
|
||||
<span style="margin-top:55px" class='tooltip rounded shadow-lg p-1 bg-gray-100 text-black space-y-3 p-2 text-left'>
|
||||
When should Guest Access expire for this user?
|
||||
</span>
|
||||
<span>
|
||||
Expires At<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 inline-block cursor-help"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<use xlink:href="public/questionsymbol.svg#q" />
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<input type="datetime-local"
|
||||
class="guestAddTime border-gray-50 placeholder-gray-500 mt-2 mr-2 rounded text-black"
|
||||
value="<%= now %>"
|
||||
class="guestAddTime border-gray-50 placeholder-gray-500 mr-2 rounded text-black"
|
||||
value="<%= defaultExpire %>"
|
||||
min="<%= now %>"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<a href="" class="addGuest">Add</a>
|
||||
@@ -1095,6 +1110,44 @@
|
||||
|
||||
const delayedItemsMap = new Map();
|
||||
let lastSeenIdentifier = null;
|
||||
const subIndicators = ['red', 'green', 'yellow'];
|
||||
|
||||
function updateOpStats(resp, responseType) {
|
||||
for (const b of resp) {
|
||||
const {
|
||||
name,
|
||||
data: {
|
||||
running,
|
||||
indicator,
|
||||
subreddits = [],
|
||||
} = {},
|
||||
} = b;
|
||||
const botTab = document.querySelector(`[data-bot="${name}"] .botTabStatus`);
|
||||
if (botTab !== null) {
|
||||
if (running !== undefined) {
|
||||
const currentStatusClass = `bg-${running ? 'green' : 'red'}-400`;
|
||||
const oppositeStatusClass = `bg-${running ? 'red' : 'green'}-400`;
|
||||
if (!botTab.classList.contains(currentStatusClass)) {
|
||||
botTab.classList.remove(oppositeStatusClass);
|
||||
botTab.classList.add(currentStatusClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const subData of subreddits) {
|
||||
const subredditTab = document.querySelector(`[data-bot="${name}"] [data-subreddit="${subData.name}"] .subredditTabStatus`);
|
||||
if (subredditTab !== null) {
|
||||
const currentSubIndicatorClass = `bg-${subData.indicator}-400`;
|
||||
const nonSubIndicatorClasses = subIndicators.filter(x => x !== subData.indicator).map(x => `bg-${x}-400`);
|
||||
if (!subredditTab.classList.contains(currentSubIndicatorClass)) {
|
||||
for (const nonIndicator of nonSubIndicatorClasses) {
|
||||
subredditTab.classList.remove(nonIndicator);
|
||||
}
|
||||
subredditTab.classList.add(currentSubIndicatorClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateLiveStats(resp, sub, bot, responseType) {
|
||||
let el;
|
||||
@@ -1360,30 +1413,32 @@
|
||||
|
||||
const now = dayjs();
|
||||
|
||||
el.innerHTML = '';
|
||||
if(data.length === 0) {
|
||||
const node = document.createElement("LI");
|
||||
node.classList.add('smallLi');
|
||||
node.appendChild(document.createTextNode('None'));
|
||||
el.appendChild(node);
|
||||
} else {
|
||||
for(const g of data) {
|
||||
if(el !== null) {
|
||||
el.innerHTML = '';
|
||||
if(data.length === 0) {
|
||||
const node = document.createElement("LI");
|
||||
node.classList.add('smallLi');
|
||||
let relTime = g.expiresAt === undefined ? 'Never' : dayjs.duration(dayjs(g.expiresAt).diff(now)).humanize();
|
||||
let guestText = g.name;
|
||||
if(isAll) {
|
||||
guestText += ` (${g.subreddits.length} Subs, at least ${relTime})`;
|
||||
} else {
|
||||
guestText += ` (${relTime})`;
|
||||
}
|
||||
node.appendChild(document.createTextNode(guestText));
|
||||
node.insertAdjacentHTML('beforeend', `<a href="" class="remove ml-1" data-name="${g.name}"><span class="cancellable iconify-inline" data-icon="icons8:cancel"></span></a>`);
|
||||
node.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
removeGuestMod(bot, sub, g.name);
|
||||
});
|
||||
node.appendChild(document.createTextNode('None'));
|
||||
el.appendChild(node);
|
||||
} else {
|
||||
for(const g of data) {
|
||||
const node = document.createElement("LI");
|
||||
node.classList.add('smallLi');
|
||||
let relTime = g.expiresAt === undefined ? 'Never' : dayjs.duration(dayjs(g.expiresAt).diff(now)).humanize();
|
||||
let guestText = g.name;
|
||||
if(isAll) {
|
||||
guestText += ` (${g.subreddits.length} Subs, at least ${relTime})`;
|
||||
} else {
|
||||
guestText += ` (${relTime})`;
|
||||
}
|
||||
node.appendChild(document.createTextNode(guestText));
|
||||
node.insertAdjacentHTML('beforeend', `<a href="" class="remove ml-1" data-name="${g.name}"><span class="cancellable iconify-inline" data-icon="icons8:cancel"></span></a>`);
|
||||
node.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
removeGuestMod(bot, sub, g.name);
|
||||
});
|
||||
el.appendChild(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1410,6 +1465,23 @@
|
||||
});
|
||||
});
|
||||
|
||||
function getOpStats(responseType = 'full') {
|
||||
console.debug(`Getting op live stats for <%= instanceId %>`)
|
||||
return fetch(`/api/stats?instance=<%= instanceId %>&type=${responseType}`)
|
||||
.then(response => {
|
||||
if(response.status === 304) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(resp => {
|
||||
if(resp === false) {
|
||||
return;
|
||||
}
|
||||
updateOpStats(resp, responseType);
|
||||
});
|
||||
}
|
||||
|
||||
function getLiveStats(bot, sub, responseType = 'full') {
|
||||
console.debug(`Getting live stats for ${bot} ${sub}`)
|
||||
return fetch(`/api/liveStats?instance=<%= instanceId %>&bot=${bot}&subreddit=${sub}&type=${responseType}`)
|
||||
@@ -1515,6 +1587,19 @@
|
||||
onVisible(el, () => onSubVisible(bot, sub));
|
||||
});
|
||||
|
||||
//window.init = true;
|
||||
let opTimeoutId = null;
|
||||
let opTimeout = () => {
|
||||
getOpStats('full').then(() => {
|
||||
opTimeoutId = setInterval(() => {
|
||||
getOpStats('delta').catch((err) => {
|
||||
console.error(err);
|
||||
clearInterval(opTimeoutId);
|
||||
})
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
let backgroundTimeout = null;
|
||||
|
||||
document.addEventListener("visibilitychange", (e) => {
|
||||
@@ -1531,6 +1616,9 @@
|
||||
controller.abort();
|
||||
}
|
||||
backgroundTimeout = null;
|
||||
clearInterval(opTimeoutId);
|
||||
opTimeoutId = null;
|
||||
window.init = true;
|
||||
}, 15000);
|
||||
} else {
|
||||
// cancel real-time data timeout because page is visible again
|
||||
@@ -1547,10 +1635,15 @@
|
||||
recentlySeen.delete(lastSeenIdentifier);
|
||||
onSubVisible(bot, sub);
|
||||
}
|
||||
if(opTimeoutId === null) {
|
||||
opTimeout();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
opTimeout();
|
||||
|
||||
var searchParams = new URLSearchParams(window.location.search);
|
||||
const shownSub = searchParams.get('sub') || 'All'
|
||||
let shownBot = searchParams.get('bot');
|
||||
@@ -1619,43 +1712,6 @@
|
||||
|
||||
socket.on("connect", () => {
|
||||
document.body.classList.add('connected')
|
||||
|
||||
const shownSub = searchParams.get('sub') || 'All'
|
||||
let shownBot = searchParams.get('bot');
|
||||
window.socket.emit('viewing', {bot: shownBot, subreddit: shownSub});
|
||||
|
||||
// TODO web logging
|
||||
// socket.on('log')
|
||||
|
||||
const subIndicators = ['red', 'green', 'yellow'];
|
||||
socket.on('opStats', (resp) => {
|
||||
for(const b of resp) {
|
||||
const {name, data} = b;
|
||||
const botTab = document.querySelector(`[data-bot="${name}"] .botTabStatus`);
|
||||
if(botTab !== null) {
|
||||
const currentStatusClass = `bg-${data.running ? 'green' : 'red'}-400`;
|
||||
const oppositeStatusClass = `bg-${data.running ? 'red' : 'green'}-400`;
|
||||
if(!botTab.classList.contains(currentStatusClass)) {
|
||||
botTab.classList.remove(oppositeStatusClass);
|
||||
botTab.classList.add(currentStatusClass);
|
||||
}
|
||||
}
|
||||
for (const subData of data.subreddits) {
|
||||
const subredditTab = document.querySelector(`[data-bot="${name}"] [data-subreddit="${subData.name}"] .subredditTabStatus`);
|
||||
if(subredditTab !== null) {
|
||||
const currentSubIndicatorClass = `bg-${subData.indicator}-400`;
|
||||
const nonSubIndicatorClasses = subIndicators.filter(x => x !== subData.indicator).map(x => `bg-${x}-400`);
|
||||
if(!subredditTab.classList.contains(currentSubIndicatorClass)) {
|
||||
for(const nonIndicator of nonSubIndicatorClasses) {
|
||||
subredditTab.classList.remove(nonIndicator);
|
||||
}
|
||||
subredditTab.classList.add(currentSubIndicatorClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
|
||||
Reference in New Issue
Block a user