├── .editorconfig
├── .github
├── dependabot.yml
└── workflows
│ ├── build.yml
│ ├── codeql-analysis.yml
│ ├── lint.yml
│ └── web.yml
├── .gitignore
├── .gitmodules
├── .make
├── README.md
└── test.mk
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTORS.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── cmd
├── bot
│ ├── .gitignore
│ ├── Makefile
│ ├── README.md
│ ├── application.go
│ ├── botconfig.go
│ ├── example.config.json
│ ├── main.go
│ ├── main_test.go
│ ├── minit.go
│ ├── minit_csharp.go
│ ├── mkswag.sh
│ ├── start.sh
│ ├── twitchstreamstore.go
│ ├── twitchusercontext.go
│ └── twitchuserstore.go
└── swagger-website
│ ├── .gitignore
│ └── main.go
├── docs
├── ENV.md
├── PLEBLIST.md
├── docs.go
└── swagger
│ ├── swagger.json
│ └── swagger.yaml
├── go.mod
├── go.sum
├── internal
├── commands
│ ├── base
│ │ └── base.go
│ ├── disconnect
│ │ └── disconnect.go
│ └── getuserid
│ │ └── getuserid.go
├── config
│ └── config.go
├── twitchuser
│ └── twitchuser.go
└── utils
│ └── db.go
├── migrations
├── .gitignore
├── 000001_create_channel_table.up.sql
├── 000002_create_command_table.up.sql
├── 000003_change_column_name.up.sql
├── 000004_add_channel_enabled_flag.up.sql
├── 000005_create_user_table.up.sql
├── 000006_create_bot_table.up.sql
├── 000007_add_bot_id_to_channel.up.sql
├── 000008_increase_refreshtoken_length.up.sql
├── 000009_modify_user_refresh_token.up.sql
├── 000010_drop_channel_access_token.up.sql
├── 000011_alter_user_table.up.sql
├── 000012_channel_drop_channel_id.up.sql
├── 000013_alter_user_table2.up.sql
├── 000014_add_permissions_table.up.sql
├── 000015_create_twitch_users_table.up.sql
├── 000016_create_twitch_user_permissions_table.up.sql
├── 000017_change_permissions.up.sql
├── 000018_twitch_user_channel_permissions.up.sql
├── 000019_make_reports_table.up.sql
├── 000020_create_user_table.up.sql
├── 000021_remove_user_permission_primary_key.up.sql
├── 000022_report_add_timestamp.up.sql
├── 000023_rename_tables.up.sql
├── 000024_remove_unused_table.up.sql
├── 000025_create_bot_channel_table.up.sql
├── 000026_create_bot_channel_module_table.up.sql
├── 000027_make_moderation_action_table.up.sql
├── 000028_create_user_sessions_table.up.sql
├── 000029_remove_user_twitch_nonce_column.up.sql
├── 000030_create_report_history_table.up.sql
├── 000031_create_warning_scale_table.up.sql
├── 000032_create_banphrase_group_table.up.sql
├── 000033_create_banphrase_table.up.sql
├── 20190118000448_add_expiry_column_to_bot_table.up.sql
├── 20190118204509_add_unique_index_to_bot_table.up.sql
├── 20190118221100_rename_name_column_in_bot_table.up.sql
├── 20190414182945_make_report_history_id_autoincrement.up.sql
└── psql
│ ├── 1-initial-tables.sql
│ ├── 1567332318-command-tables.sql
│ └── newmig.sh
├── pkg
├── account.go
├── action.go
├── apirequest
│ ├── apirequest.go
│ ├── bttv.go
│ ├── common.go
│ ├── custom.go
│ ├── ffz.go
│ ├── twitch.go
│ ├── twitchratelimit.go
│ └── twitchwrapper.go
├── application.go
├── auth.go
├── auth
│ └── twitch.go
├── banphrase.go
├── bot.go
├── botchannel.go
├── botstore.go
├── botstore
│ ├── botstore.go
│ └── botstoreiterator.go
├── channel.go
├── channels
│ ├── store.go
│ └── twitchchannel.go
├── commandlist
│ └── commandlist.go
├── commands.go
├── commands
│ ├── base.go
│ ├── commands.go
│ ├── commands_test.go
│ ├── custom.go
│ ├── join.go
│ ├── leave.go
│ ├── module.go
│ ├── pajbot1_command.go
│ ├── ping.go
│ ├── quit.go
│ ├── raffle.go
│ ├── rank.go
│ ├── subcommands.go
│ └── user.go
├── commandsubstitution
│ ├── Makefile
│ ├── commandsubstitution.go
│ ├── commandsubstitution_test.go
│ └── helpers_test.go
├── common
│ ├── common.go
│ ├── common_test.go
│ ├── config
│ │ ├── config.go
│ │ └── config_test.go
│ ├── dbuser.go
│ ├── emoji.go
│ ├── emote.go
│ ├── sql.go
│ └── twitchcredentials.go
├── const.go
├── emote.go
├── emotes.go
├── emotes
│ └── emotes.go
├── eventemitter
│ ├── Makefile
│ ├── eventemitter.go
│ ├── eventemitter_test.go
│ └── helpers_test.go
├── events.go
├── eventsub.go
├── filters
│ ├── pajbot1_banphrase.go
│ └── pajbot1_banphrase_test.go
├── message.go
├── mimo.go
├── mimo
│ └── mimo.go
├── module.go
├── modules
│ ├── bad_character_filter
│ │ └── m.go
│ ├── banned_names
│ │ └── m.go
│ ├── base
│ │ └── m.go
│ ├── basic_commands.go
│ ├── bttv_emote_parser.go
│ ├── commands
│ │ ├── add.go
│ │ ├── add_trigger.go
│ │ ├── m.go
│ │ ├── remove_trigger.go
│ │ └── text.go
│ ├── custom_commands.go
│ ├── datastructures
│ │ └── transparentlist.go
│ ├── debug.go
│ ├── emote_limit.go
│ ├── giveaway
│ │ ├── giveaway_config.go
│ │ ├── giveaway_draw.go
│ │ ├── giveaway_start.go
│ │ ├── giveaway_stop.go
│ │ └── m.go
│ ├── goodbye.go
│ ├── latin_filter.go
│ ├── link_filter
│ │ ├── link_filter_test.go
│ │ └── m.go
│ ├── message_height_limit
│ │ └── m.go
│ ├── message_length_limit.go
│ ├── nuke
│ │ ├── m.go
│ │ ├── parse.go
│ │ └── parse_test.go
│ ├── other_commands.go
│ ├── pajbot1_banphrase.go
│ ├── pajbot1_commands.go
│ ├── param.go
│ ├── param_bool.go
│ ├── param_float.go
│ ├── param_string.go
│ ├── punisher
│ │ ├── m.go
│ │ └── timeout.go
│ ├── registry.go
│ ├── report.go
│ ├── server.go
│ ├── spec.go
│ ├── system
│ │ └── m.go
│ ├── test_module.go
│ ├── tusecommands
│ │ └── m.go
│ ├── value.go
│ └── welcome
│ │ └── m.go
├── pajbot
│ └── pajbot.go
├── permissions.go
├── pubsub.go
├── pubsub
│ ├── listener.go
│ └── pubsub.go
├── report
│ └── report.go
├── reportaction.go
├── stream.go
├── streamstore.go
├── twitch
│ ├── account.go
│ ├── action.go
│ ├── bot.go
│ ├── botchannel.go
│ ├── stream.go
│ └── user.go
├── twitchaccount.go
├── twitchactions
│ ├── ban.go
│ ├── base.go
│ ├── delete.go
│ ├── message.go
│ ├── mute.go
│ ├── timeout.go
│ ├── twitchactions.go
│ ├── unmute.go
│ └── whisper.go
├── user.go
├── usercontext.go
├── users
│ ├── server.go
│ └── twitchuser.go
├── userstore.go
├── web
│ ├── controller
│ │ ├── admin
│ │ │ └── admin.go
│ │ ├── api
│ │ │ ├── README.md
│ │ │ ├── api.go
│ │ │ ├── auth
│ │ │ │ ├── routes.go
│ │ │ │ └── twitch
│ │ │ │ │ ├── bot.go
│ │ │ │ │ ├── routes.go
│ │ │ │ │ ├── shared.go
│ │ │ │ │ ├── streamer.go
│ │ │ │ │ └── user.go
│ │ │ ├── channel
│ │ │ │ ├── banphrases
│ │ │ │ │ ├── list.go
│ │ │ │ │ └── routes.go
│ │ │ │ ├── moderation
│ │ │ │ │ ├── checkmessage.go
│ │ │ │ │ ├── latest.go
│ │ │ │ │ ├── routes.go
│ │ │ │ │ └── user.go
│ │ │ │ └── routes.go
│ │ │ ├── report
│ │ │ │ ├── history.go
│ │ │ │ └── routes.go
│ │ │ └── webhook
│ │ │ │ ├── eventsub.go
│ │ │ │ ├── github.go
│ │ │ │ ├── github_test.go
│ │ │ │ └── webhook.go
│ │ ├── banphrases
│ │ │ └── banphrases.go
│ │ ├── channel
│ │ │ └── channel.go
│ │ ├── commands.go
│ │ ├── controller.go
│ │ ├── dashboard
│ │ │ └── dashboard.go
│ │ ├── home
│ │ │ └── home.go
│ │ ├── logout
│ │ │ └── logout.go
│ │ ├── profile.go
│ │ ├── static
│ │ │ └── static.go
│ │ └── ws
│ │ │ ├── hub.go
│ │ │ ├── ws.go
│ │ │ ├── wsconn.go
│ │ │ ├── wsmessage.go
│ │ │ └── wspayload.go
│ ├── errors.go
│ ├── errors_test.go
│ ├── hook.go
│ ├── hooktypes.go
│ ├── payloads.go
│ ├── router
│ │ └── router.go
│ ├── state
│ │ ├── sessionstore.go
│ │ └── state.go
│ ├── views
│ │ └── views.go
│ └── web.go
└── webutils
│ └── webutils.go
├── resources
└── testfiles
│ ├── config1.json
│ └── config2_invalidjson.json
├── staticcheck.conf
├── tools.go
├── utils
├── build.sh
├── createdb.sh
├── docker
│ ├── build.sh
│ └── run.sh
├── findlibcoreclr.sh
├── install.sh
├── mkmig.sh
├── test-all.sh
└── upmig.sh
└── web
├── .babelrc
├── .gitignore
├── .prettierignore
├── .prettierrc.toml
├── package-lock.json
├── package.json
├── src
├── index.jsx
├── js
│ ├── Admin.jsx
│ ├── Banphrases.jsx
│ ├── Commands.jsx
│ ├── Dashboard.jsx
│ ├── LogInButton.jsx
│ ├── Menu.jsx
│ ├── ThemeContext.jsx
│ ├── ThemeLoader.jsx
│ ├── ThemeProvider.jsx
│ ├── ThemeSwitcher.jsx
│ ├── WebSocketHandler.js
│ ├── auth.js
│ └── cookie.js
└── scss
│ ├── app.scss
│ ├── base
│ ├── body.scss
│ ├── buttons.scss
│ └── links.scss
│ ├── modules
│ └── dashboard.scss
│ ├── tools
│ └── mixins.scss
│ ├── variables
│ ├── colors.scss
│ └── spacing.scss
│ └── vendor
│ └── reset.scss
├── static
└── themes
│ ├── Dark
│ └── bootstrap.min.css
│ └── Light
│ └── bootstrap.min.css
├── views
├── 403.html
├── admin.html
├── banphrases.html
├── base.html
├── commands.html
├── dashboard.html
├── home.html
└── profile.html
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.{js,jsx}]
4 | indent_style = space
5 | indent_size = 2
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "gomod"
9 | directory: "/"
10 | schedule:
11 | interval: "daily"
12 | labels:
13 | - "dependencies"
14 | - package-ecosystem: "npm"
15 | directory: "/web"
16 | schedule:
17 | interval: "daily"
18 | labels:
19 | - "dependencies"
20 | versioning-strategy: increase
21 | - package-ecosystem: "github-actions"
22 | directory: "/"
23 | schedule:
24 | interval: "daily"
25 | labels:
26 | - "dependencies"
27 | - package-ecosystem: "gitsubmodule"
28 | directory: "/"
29 | schedule:
30 | interval: "daily"
31 | labels:
32 | - "dependencies"
33 | - "submodules"
34 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Build
3 |
4 | on:
5 | push:
6 | branches:
7 | - master
8 | pull_request:
9 |
10 | jobs:
11 | build:
12 | name: Build bot
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | go: ["oldstable", "stable"]
18 |
19 | steps:
20 | - name: Set up Go
21 | uses: actions/setup-go@v5
22 | with:
23 | go-version: ${{ matrix.go }}
24 | id: go
25 |
26 | - name: Check out code into the Go module directory
27 | uses: actions/checkout@v4
28 |
29 | - name: Get dependencies
30 | run: go get -v -t -d ./...
31 |
32 | - name: Build bot
33 | working-directory: ./cmd/bot
34 | run: go build -v
35 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | name: "CodeQL"
7 |
8 | on:
9 | push:
10 | branches: [master]
11 | pull_request:
12 | # The branches below must be a subset of the branches above
13 | branches: [master]
14 | schedule:
15 | # 3pm every Sunday
16 | - cron: '0 16 * * 0'
17 |
18 | jobs:
19 | analyze:
20 | name: Analyze
21 | runs-on: ubuntu-latest
22 |
23 | strategy:
24 | fail-fast: false
25 | matrix:
26 | # Override automatic language detection by changing the below list
27 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
28 | language: ['go', 'javascript']
29 | # Learn more...
30 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
31 |
32 | steps:
33 | - name: Checkout repository
34 | uses: actions/checkout@v4
35 |
36 | # Initializes the CodeQL tools for scanning.
37 | - name: Initialize CodeQL
38 | uses: github/codeql-action/init@v3
39 | with:
40 | languages: ${{ matrix.language }}
41 | # If you wish to specify custom queries, you can do so here or in a config file.
42 | # By default, queries listed here will override any specified in a config file.
43 | # Prefix the list here with "+" to use these queries and those in the config file.
44 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
45 |
46 | - name: Perform CodeQL Analysis
47 | uses: github/codeql-action/analyze@v3
48 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Lint
3 |
4 | on:
5 | push:
6 | branches:
7 | - master
8 | pull_request:
9 |
10 | jobs:
11 | lint:
12 | name: Lint
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | go: ["oldstable", "stable"]
18 |
19 | steps:
20 | - name: Set up Go
21 | uses: actions/setup-go@v5
22 | with:
23 | go-version: ${{ matrix.go }}
24 | id: go
25 |
26 | - name: Check out code into the Go module directory
27 | uses: actions/checkout@v4
28 |
29 | - name: Install linter
30 | run: go install honnef.co/go/tools/cmd/staticcheck
31 |
32 | - name: Lint
33 | run: staticcheck ./...
34 |
--------------------------------------------------------------------------------
/.github/workflows/web.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Build web
3 |
4 | on:
5 | push:
6 | branches:
7 | - master
8 | pull_request:
9 |
10 | jobs:
11 | build:
12 | name: Build web
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | node: ['18', '20', '22', '23']
18 |
19 | steps:
20 | - name: Check out code into the Go module directory
21 | uses: actions/checkout@v4
22 |
23 | - name: Set up Go
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: ${{ matrix.node }}
27 |
28 | - working-directory: ./web
29 | run: npm ci
30 |
31 | - working-directory: ./web
32 | run: npm run check_formatting
33 |
34 | - working-directory: ./web
35 | run: npm run build
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore binary file
2 | pajbot2
3 | *.exe
4 | *.exe~
5 | /bot
6 |
7 | # Ignore vendor folder
8 | /vendor/
9 |
10 | # Ignore config files
11 | config.json
12 |
13 | # Ignore web Files
14 | web/node_modules
15 | web/static/build
16 |
17 | # Various
18 | .DS_Store
19 | .vscode/
20 |
21 | # Coverage files
22 | coverage.out
23 | coverage.html
24 |
25 | .gdb_history
26 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "3rdParty/MessageHeightTwitch"]
2 | path = 3rdParty/MessageHeightTwitch
3 | url = https://github.com/TETYYS/MessageHeightTwitch.git
4 |
--------------------------------------------------------------------------------
/.make/README.md:
--------------------------------------------------------------------------------
1 | helper makefile files
2 |
--------------------------------------------------------------------------------
/.make/test.mk:
--------------------------------------------------------------------------------
1 | test:
2 | @go test -v
3 |
4 | cover:
5 | @go test -coverprofile=coverage.out -covermode=count
6 | @go tool cover -html=coverage.out -o coverage.html
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - 1.11.x
4 | - 1.12.x
5 |
6 | env:
7 | - GO111MODULE=on
8 |
9 | script:
10 | - pwd
11 | - go test -v ./pkg/... ./internal/...
12 |
13 | install: true
14 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Unversioned
4 |
5 | ## v2.1.0 - 2024-03-06
6 |
7 | - Fixed a crash when nuking with an "invalid" regex. (#1109)
8 | - Bumped minimum Go version to 1.20. (#1083)
9 | - Allow pb2exec commands `ban`, `unban`, `timeout`, `untimeout` to be used with IDs (e.g. `!pb2exec .timeout id:22484632 5 reason`). (#1056)
10 | - Remove Twitter support. (#1033)
11 | Warning messages will be posted in the console if twitter tokens are configured.
12 | - Add GitHub push event webhook support. (#1042, #1043, #1064)
13 | Webhook format: `https://your-bot-domain.com/api/webhook/github/{channel_id}`
14 | Example config has been updated to show Auth -> Github -> Webhook -> Secret
15 |
16 | ## v2.0.0 - 2023-08-08
17 |
18 | - Bumped minimum Go version to 1.19. (#898)
19 | - The nuke module now has tests for parsing parameters. (#530)
20 | - The `Auth->Twitch->Webhook->Secret` config value is now REQUIRED. It's your own private secret you need to generate yourself, and it must be at least 10 characters and at most 100 characters long.
21 | - The nuke module will now recognize users with global permissions. (#268)
22 | - Message height limit no longer applies to Twitch Moderators (#89, #228)
23 | - The version of MessageHeightTwitch was updated, which requires version 3.0 of .NET Core.
24 | - Changed DB backend from MySQL to PostgreSQL.
25 | Setting up the bot from scratch? You don't need to do anything!
26 | Upgrading your already set up bot to this version? Follow the instructions in [this document](/resources/mysql-to-postgresql-transition/README.md)
27 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | # Contributors
2 | - [nuuls](https://github.com/nuuls)
3 | - [pajlada](https://github.com/pajlada)
4 | - [gempir](https://github.com/gempir)
5 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/sdk:6.0.301-1-alpine3.16 AS build
2 | RUN apk add --no-cache go nodejs npm build-base
3 | ADD . /src
4 | RUN cd /src && ./utils/install.sh
5 | RUN cd /src && ./utils/build.sh -v -tags csharp
6 |
7 | FROM mcr.microsoft.com/dotnet/runtime:6.0.6-alpine3.16
8 | WORKDIR /app/cmd/bot
9 | RUN apk add --no-cache icu
10 | ENV LIBCOREFOLDER /usr/share/dotnet/shared/Microsoft.NETCore.App/6.0.6
11 | COPY --from=build /src/web/static /app/web/static
12 | COPY --from=build /src/web/views /app/web/views
13 | COPY --from=build /src/cmd/bot/bot /app/cmd/bot/bot
14 | COPY --from=build /src/migrations /app/migrations/
15 | COPY --from=build /src/cmd/bot/*.dll /app/cmd/bot/
16 | COPY --from=build /src/cmd/bot/charmap.bin.gz /app/cmd/bot/
17 | RUN chmod 777 /app/cmd/bot/charmap.bin.gz
18 | CMD ["./bot"]
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Rasmus Karlsson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | swag:
2 | @swag init --generalInfo cmd/bot/main.go
3 |
4 | lint:
5 | @staticcheck ./...
6 |
7 | build:
8 | @go build ./cmd/bot/
9 |
10 | build-csharp:
11 | @go build -tags csharp ./cmd/bot/
12 |
13 | build-all: build build-csharp
14 |
15 | check: lint build-all
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pajbot 2 [](https://circleci.com/gh/pajbot/pajbot2)
2 |
3 | A rewrite/restructuring of [pajbot 1](https://github.com/pajbot/pajbot) in Go. As with pajbot1, this is a way for me to familiarize myself with the language.
4 |
5 | ## Web guide
6 |
7 | - `cd web && npm install`
8 | - `npm run watch` to let webpack running and compile in background
9 |
10 | ## FAQ
11 |
12 | ### How do I add a bot?
13 |
14 | After making yourself an admin in the config file, open up the web interface. Log in, go to `/admin`, press the "Log in as bot", then after authenticating whatever user you want as a bot, restart the bot!
15 |
--------------------------------------------------------------------------------
/cmd/bot/.gitignore:
--------------------------------------------------------------------------------
1 | /bot
2 | /migrations
3 | /*.dll
4 | /charmap.bin.gz
5 | /web
6 |
--------------------------------------------------------------------------------
/cmd/bot/Makefile:
--------------------------------------------------------------------------------
1 | default:
2 | @go build
3 |
4 | run: default
5 | ./bot
6 |
--------------------------------------------------------------------------------
/cmd/bot/README.md:
--------------------------------------------------------------------------------
1 | # pajbot2
2 |
3 | This is the cmd to actually run the bot
4 |
5 | Just run `go build` in this folder to make the executable, then run it (./bot)
6 |
--------------------------------------------------------------------------------
/cmd/bot/example.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "Admin": {
3 | "TwitchUserID": "12345678"
4 | },
5 | "Web": {
6 | "Host": "127.0.0.1:45000",
7 | "Domain": "paj.pajbot.com",
8 | "Secure": true
9 | },
10 | "PostgreSQL": {
11 | "__dsn_comment": "Refer to https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters for valid options",
12 | "DSN": "host=/var/run/postgresql dbname=pajbot2 sslmode=disable"
13 | },
14 | "Auth": {
15 | "Twitch": {
16 | "Webhook": {
17 | "Secret": "topsecretstringbetween10and100characters"
18 | },
19 | "Bot": {
20 | "ClientID": "123",
21 | "ClientSecret": "123"
22 | },
23 | "User": {
24 | "ClientID": "123",
25 | "ClientSecret": "123"
26 | },
27 | "Streamer": {
28 | "ClientID": "123",
29 | "ClientSecret": "123"
30 | }
31 | },
32 | "Github": {
33 | "Webhook": {
34 | "Secret": "secret"
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/cmd/bot/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
--------------------------------------------------------------------------------
/cmd/bot/minit.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // Initialize any modules that are in their own packages
4 |
5 | import (
6 | _ "github.com/pajbot/pajbot2/pkg/modules/commands" // xd
7 |
8 | _ "github.com/pajbot/pajbot2/pkg/modules/tusecommands" // xd
9 |
10 | _ "github.com/pajbot/pajbot2/pkg/modules/punisher" // xd
11 |
12 | _ "github.com/pajbot/pajbot2/pkg/modules/system"
13 |
14 | _ "github.com/pajbot/pajbot2/pkg/modules/giveaway"
15 |
16 | _ "github.com/pajbot/pajbot2/pkg/modules/link_filter"
17 |
18 | _ "github.com/pajbot/pajbot2/pkg/modules/nuke"
19 |
20 | _ "github.com/pajbot/pajbot2/pkg/modules/welcome"
21 |
22 | _ "github.com/pajbot/pajbot2/pkg/modules/banned_names"
23 |
24 | _ "github.com/pajbot/pajbot2/pkg/modules/bad_character_filter"
25 | )
26 |
--------------------------------------------------------------------------------
/cmd/bot/minit_csharp.go:
--------------------------------------------------------------------------------
1 | // +build csharp
2 |
3 | package main
4 |
5 | // Initialize any modules that are in their own packages and require the csharp tag
6 |
7 | import (
8 | _ "github.com/pajbot/pajbot2/pkg/modules/message_height_limit"
9 | )
10 |
--------------------------------------------------------------------------------
/cmd/bot/mkswag.sh:
--------------------------------------------------------------------------------
1 | cd ../../; make; cd cmd/swagger-website; go build && ./swagger-website
2 |
--------------------------------------------------------------------------------
/cmd/bot/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . ../../utils/findlibcoreclr.sh
4 |
5 | LIBCOREFOLDER="$(get_libcoreclr_path)" ./bot
6 |
--------------------------------------------------------------------------------
/cmd/bot/twitchusercontext.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 |
7 | "github.com/pajbot/pajbot2/pkg"
8 | )
9 |
10 | var _ pkg.UserContext = &UserContext{}
11 |
12 | type UserContext struct {
13 | mutex *sync.Mutex
14 |
15 | // key = channel ID
16 | context map[string]map[string][]string
17 | }
18 |
19 | func NewUserContext() *UserContext {
20 | c := &UserContext{
21 | mutex: &sync.Mutex{},
22 | context: make(map[string]map[string][]string),
23 | }
24 |
25 | return c
26 | }
27 |
28 | func (c *UserContext) GetContext(channelID, userID string) []string {
29 | c.mutex.Lock()
30 | defer c.mutex.Unlock()
31 |
32 | if users, ok := c.context[channelID]; ok {
33 | if userContext, ok := users[userID]; ok {
34 | return userContext
35 | }
36 | }
37 |
38 | return nil
39 | }
40 |
41 | func (c *UserContext) AddContext(channelID, userID, message string) {
42 | if channelID == "" {
43 | fmt.Println("Channel ID is empty")
44 | return
45 | }
46 | if userID == "" {
47 | fmt.Println("User ID is empty")
48 | return
49 | }
50 |
51 | c.mutex.Lock()
52 | defer c.mutex.Unlock()
53 |
54 | _, ok := c.context[channelID]
55 | if !ok {
56 | c.context[channelID] = make(map[string][]string)
57 | }
58 |
59 | c.context[channelID][userID] = append(c.context[channelID][userID], message)
60 | newLen := len(c.context[channelID][userID]) - 5
61 | if newLen > 5 {
62 | c.context[channelID][userID] = c.context[channelID][userID][newLen:]
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/cmd/swagger-website/.gitignore:
--------------------------------------------------------------------------------
1 | /swagger-website
2 |
--------------------------------------------------------------------------------
/cmd/swagger-website/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 | echoSwagger "github.com/swaggo/echo-swagger"
6 |
7 | _ "github.com/pajbot/pajbot2/docs" // docs is generated by Swag CLI, you have to import it.
8 | )
9 |
10 | func main() {
11 | e := echo.New()
12 |
13 | e.GET("/*", echoSwagger.WrapHandler)
14 |
15 | e.Logger.Fatal(e.Start(":1323"))
16 | }
17 |
--------------------------------------------------------------------------------
/docs/ENV.md:
--------------------------------------------------------------------------------
1 | # Environment variables
2 |
3 | ## PAJBOT2_WEB_PATH
4 | Points to the path where the web static and views folders are located.
5 | Default value: `../../web`
6 |
--------------------------------------------------------------------------------
/docs/swagger/swagger.yaml:
--------------------------------------------------------------------------------
1 | basePath: '{{.BasePath}}'
2 | definitions:
3 | moderation.CheckMessageSuccessResponse:
4 | properties:
5 | banned:
6 | type: boolean
7 | filter_data:
8 | items:
9 | $ref: '#/definitions/moderation.filterData'
10 | type: array
11 | input_message:
12 | type: string
13 | type: object
14 | moderation.filterData:
15 | properties:
16 | mute_type:
17 | type: integer
18 | reason:
19 | type: string
20 | type: object
21 | utils.WebAPIError:
22 | properties:
23 | code:
24 | type: integer
25 | error:
26 | type: string
27 | type: object
28 | host: localhost:2355
29 | info:
30 | contact:
31 | email: rasmus.karlsson@pajlada.com
32 | name: pajlada
33 | url: https://pajlada.se
34 | description: API for pajbot2
35 | license:
36 | name: MIT
37 | url: https://github.com/pajbot/pajbot2/blob/master/LICENSE
38 | title: pajbot2 API
39 | version: "1.0"
40 | paths:
41 | /api/channel/{channelID}/moderation/check_message:
42 | get:
43 | parameters:
44 | - description: ID of channel to run the test in
45 | in: path
46 | name: channelID
47 | required: true
48 | type: string
49 | - description: message to test against the bots filters
50 | in: query
51 | name: message
52 | required: true
53 | type: string
54 | produces:
55 | - application/json
56 | responses:
57 | "200":
58 | description: OK
59 | schema:
60 | $ref: '#/definitions/moderation.CheckMessageSuccessResponse'
61 | type: object
62 | "404":
63 | description: Not Found
64 | schema:
65 | $ref: '#/definitions/utils.WebAPIError'
66 | type: object
67 | summary: Check a message in a bots filter
68 | swagger: "2.0"
69 |
--------------------------------------------------------------------------------
/internal/commands/disconnect/disconnect.go:
--------------------------------------------------------------------------------
1 | package disconnect
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/pkg"
5 | )
6 |
7 | type Command struct {
8 | Bot pkg.BotChannel
9 | }
10 |
11 | func (c *Command) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
12 | if !event.User.HasPermission(event.Channel, pkg.PermissionAdmin) {
13 | return nil
14 | }
15 |
16 | c.Bot.Bot().Disconnect()
17 |
18 | return nil
19 | }
20 |
--------------------------------------------------------------------------------
/internal/commands/getuserid/getuserid.go:
--------------------------------------------------------------------------------
1 | package getuserid
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | "github.com/pajbot/pajbot2/pkg/twitchactions"
8 | "github.com/pajbot/utils"
9 | )
10 |
11 | type Command struct {
12 | }
13 |
14 | func (c Command) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
15 | usernames := utils.FilterUsernames(parts[1:])
16 |
17 | if len(usernames) == 0 {
18 | return twitchactions.Mention(event.User, "usage: !userid USERNAME (i.e. !userid pajlada)")
19 | }
20 |
21 | userIDs := event.UserStore.GetIDs(usernames)
22 | var results []string
23 | for username, userID := range userIDs {
24 | results = append(results, username+"="+userID)
25 | }
26 |
27 | if len(results) == 0 {
28 | return twitchactions.Mention(event.User, "no valid usernames were given")
29 | }
30 |
31 | return twitchactions.Mention(event.User, strings.Join(results, ", "))
32 | }
33 |
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import "os"
4 |
5 | var (
6 | WebStaticPath = stringEnv("PAJBOT2_WEB_PATH", "../../web/")
7 | )
8 |
9 | func stringEnv(key, defaultValue string) string {
10 | value, exists := os.LookupEnv(key)
11 | if !exists {
12 | return defaultValue
13 | }
14 | return value
15 | }
16 |
--------------------------------------------------------------------------------
/internal/twitchuser/twitchuser.go:
--------------------------------------------------------------------------------
1 | package twitchuser
2 |
3 | type TwitchUser struct {
4 | id string
5 | name string
6 | }
7 |
8 | func New(id, name string) *TwitchUser {
9 | return &TwitchUser{
10 | id: id,
11 | name: name,
12 | }
13 | }
14 |
15 | func (u *TwitchUser) ID() string {
16 | return u.id
17 | }
18 |
19 | func (u *TwitchUser) Name() string {
20 | return u.name
21 | }
22 |
--------------------------------------------------------------------------------
/internal/utils/db.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "database/sql"
4 |
5 | // WithTransaction taken from https://stackoverflow.com/questions/16184238/database-sql-tx-detecting-commit-or-rollback
6 | func WithTransaction(db *sql.DB, txFunc func(*sql.Tx) error) error {
7 | tx, err := db.Begin()
8 | if err != nil {
9 | return err
10 | }
11 | defer func() {
12 | if p := recover(); p != nil {
13 | tx.Rollback()
14 | panic(p) // re-throw panic after Rollback
15 | } else if err != nil {
16 | tx.Rollback() // err is non-nil; don't change it
17 | } else {
18 | err = tx.Commit() // err is nil; if Commit returns error update err
19 | }
20 | }()
21 | err = txFunc(tx)
22 | return err
23 | }
24 |
--------------------------------------------------------------------------------
/migrations/.gitignore:
--------------------------------------------------------------------------------
1 | *.down.sql
2 |
--------------------------------------------------------------------------------
/migrations/000001_create_channel_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `pb_channel` (
2 | `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
3 | `channel` VARCHAR(64) NOT NULL COMMENT 'i.e. forsenlol',
4 | `nickname` VARCHAR(64) NULL DEFAULT NULL COMMENT 'i.e. Forsen',
5 | `twitch_channel_id` BIGINT(20) NULL DEFAULT NULL COMMENT 'i.e. 12345678',
6 | `twitch_access_token` VARCHAR(64) NULL DEFAULT NULL,
7 | `twitch_refresh_token` VARCHAR(64) NULL DEFAULT NULL,
8 | PRIMARY KEY (`id`),
9 | UNIQUE INDEX `channel` (`channel`)
10 | )
11 | COLLATE='utf8mb4_general_ci'
12 | ENGINE=InnoDB
13 | ;
14 |
--------------------------------------------------------------------------------
/migrations/000002_create_command_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `pb_command` (
2 | `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
3 | `channel_id` INT(10) UNSIGNED NOT NULL,
4 | `triggers` VARCHAR(512) NOT NULL COMMENT 'Each trigger is divided by a pipe character "|". No !\'s allowed in command names. Example: testman|testman1|anotheralias',
5 | `response` VARCHAR(1024) NOT NULL,
6 | `response_type` ENUM('say','whisper','reply') NOT NULL DEFAULT 'say',
7 | `level` INT(11) NOT NULL DEFAULT '100' COMMENT 'User level required to use the command',
8 | `cooldown_all` INT(11) NOT NULL DEFAULT '4',
9 | `cooldown_user` INT(11) NOT NULL DEFAULT '10',
10 | `enabled` ENUM('yes','no','online_only','offline_only') NOT NULL DEFAULT 'yes',
11 | `cost_points` INT(10) UNSIGNED NOT NULL DEFAULT '0',
12 | `filters` SET('banphrases','linkchecker') NOT NULL DEFAULT '',
13 | PRIMARY KEY (`id`),
14 | INDEX `channel_id` (`channel_id`),
15 | CONSTRAINT `FK_pb_command_pb_channel` FOREIGN KEY (`channel_id`) REFERENCES `pb_channel` (`id`)
16 | )
17 | COLLATE='utf8mb4_general_ci'
18 | ENGINE=InnoDB
19 | ;
20 |
--------------------------------------------------------------------------------
/migrations/000003_change_column_name.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `pb_channel` CHANGE `channel` `name` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'i.e. forsenlol';
2 |
--------------------------------------------------------------------------------
/migrations/000004_add_channel_enabled_flag.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `pb_channel` ADD `enabled` BOOLEAN NOT NULL DEFAULT TRUE AFTER `nickname`;
2 |
--------------------------------------------------------------------------------
/migrations/000005_create_user_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `pb_user` (
2 | `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
3 | `name` VARCHAR(64) NOT NULL COMMENT 'i.e. forsenlol',
4 | `twitch_access_token` VARCHAR(64) NULL DEFAULT NULL COMMENT 'User level access-token',
5 | `twitch_refresh_token` VARCHAR(64) NULL DEFAULT NULL COMMENT 'User level refresh-token',
6 | PRIMARY KEY (`id`)
7 | )
8 | COMMENT='Users that log in via the web interface'
9 | COLLATE='utf8mb4_general_ci'
10 | ENGINE=InnoDB
11 | ;
12 |
--------------------------------------------------------------------------------
/migrations/000006_create_bot_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `pb_bot` (
2 | `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
3 | `name` VARCHAR(64) NOT NULL COMMENT 'i.e. snusbot',
4 | `twitch_access_token` VARCHAR(64) NULL DEFAULT NULL COMMENT 'Bot level access-token',
5 | `twitch_refresh_token` VARCHAR(64) NULL DEFAULT NULL COMMENT 'Bot level refresh-token',
6 | PRIMARY KEY (`id`)
7 | )
8 | COMMENT='Store available bot accouns, requires an access token with chat_login scope'
9 | COLLATE='utf8mb4_general_ci'
10 | ENGINE=InnoDB
11 | ;
12 |
--------------------------------------------------------------------------------
/migrations/000007_add_bot_id_to_channel.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `pb_channel` ADD `bot_id` INT(11) UNSIGNED NOT NULL AFTER `nickname`;
2 |
--------------------------------------------------------------------------------
/migrations/000008_increase_refreshtoken_length.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `pb_bot` MODIFY twitch_refresh_token VARCHAR(256);
2 |
--------------------------------------------------------------------------------
/migrations/000009_modify_user_refresh_token.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `pb_user` MODIFY twitch_refresh_token VARCHAR(256);
2 |
--------------------------------------------------------------------------------
/migrations/000010_drop_channel_access_token.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `pb_channel` DROP COLUMN twitch_access_token, DROP COLUMN twitch_refresh_token;
2 |
--------------------------------------------------------------------------------
/migrations/000011_alter_user_table.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `pb_user`
2 | ALTER `twitch_access_token` DROP DEFAULT,
3 | ALTER `twitch_refresh_token` DROP DEFAULT;
4 |
--------------------------------------------------------------------------------
/migrations/000012_channel_drop_channel_id.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `pb_channel`
2 | DROP COLUMN `twitch_channel_id`;
3 |
--------------------------------------------------------------------------------
/migrations/000013_alter_user_table2.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `pb_user`
2 | ADD COLUMN `type` ENUM('bot','user') NOT NULL DEFAULT 'user' AFTER `name`,
3 | CHANGE COLUMN `twitch_access_token` `twitch_access_token` VARCHAR(64) NOT NULL COMMENT 'User level access-token' AFTER `type`,
4 | CHANGE COLUMN `twitch_refresh_token` `twitch_refresh_token` VARCHAR(256) NOT NULL AFTER `twitch_access_token`,
5 | ADD COLUMN `twitch_room_id` BIGINT NOT NULL AFTER `twitch_refresh_token`,
6 | ADD INDEX `INDEX_BY_USER_TYPE` (`type`);
7 |
--------------------------------------------------------------------------------
/migrations/000014_add_permissions_table.up.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE pb_user;
2 |
--------------------------------------------------------------------------------
/migrations/000015_create_twitch_users_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `twitch_users` (
2 | `twitch_user_id` VARCHAR(64) NOT NULL,
3 | `name` VARCHAR(64) NOT NULL COMMENT 'i.e. testaccount_420',
4 | `display_name` VARCHAR(64) NULL COMMENT 'i.e. TestAccount_420',
5 | PRIMARY KEY (`twitch_user_id`),
6 | INDEX `name` (`name`)
7 | )
8 | COLLATE='utf8mb4_general_ci'
9 | ENGINE=InnoDB
10 | ;
11 |
--------------------------------------------------------------------------------
/migrations/000016_create_twitch_user_permissions_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `twitch_user_permissions` (
2 | `twitch_user_id` VARCHAR(64) NOT NULL,
3 | `permission` VARCHAR(64) NOT NULL,
4 |
5 | PRIMARY KEY (`twitch_user_id`),
6 |
7 | UNIQUE INDEX `user_permission` (`twitch_user_id`, `permission`)
8 | )
9 | COLLATE='utf8mb4_general_ci'
10 | ENGINE=InnoDB
11 | ;
12 |
--------------------------------------------------------------------------------
/migrations/000017_change_permissions.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `twitch_user_permissions` CHANGE COLUMN permission permissions BIT(64) NOT NULL DEFAULT 0b0;
2 |
--------------------------------------------------------------------------------
/migrations/000018_twitch_user_channel_permissions.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `twitch_user_channel_permissions` (
2 | `twitch_user_id` VARCHAR(64) NOT NULL,
3 | `channel_id` VARCHAR(64) NOT NULL,
4 | `permissions` BIT(64) NOT NULL DEFAULT 0b0,
5 |
6 | PRIMARY KEY (`twitch_user_id`),
7 |
8 | UNIQUE INDEX `user_channel_permission` (`twitch_user_id`, `channel_id`)
9 | )
10 | COLLATE='utf8mb4_general_ci'
11 | ENGINE=InnoDB
12 | ;
13 |
--------------------------------------------------------------------------------
/migrations/000019_make_reports_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `Report` (
2 | `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
3 | `channel_id` VARCHAR(64) NOT NULL,
4 | `channel_name` VARCHAR(64) NOT NULL,
5 | `channel_type` VARCHAR(64) NOT NULL,
6 | `reporter_id` VARCHAR(64) NOT NULL,
7 | `reporter_name` VARCHAR(64) NOT NULL,
8 | `target_id` VARCHAR(64) NOT NULL,
9 | `target_name` VARCHAR(64) NOT NULL,
10 | `reason` TEXT,
11 | `logs` TEXT,
12 |
13 | PRIMARY KEY (`id`)
14 | )
15 | COLLATE='utf8mb4_general_ci'
16 | ENGINE=InnoDB
17 | ;
18 |
--------------------------------------------------------------------------------
/migrations/000020_create_user_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `User` (
2 | `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
3 | `twitch_username` VARCHAR(64) NOT NULL,
4 | `twitch_userid` VARCHAR(64) NOT NULL,
5 | `twitch_nonce` VARCHAR(64) NOT NULL,
6 |
7 | PRIMARY KEY (`id`),
8 | UNIQUE INDEX `ui_twitch_userid` (`twitch_userid`)
9 | )
10 | COLLATE='utf8mb4_general_ci'
11 | ENGINE=InnoDB
12 | ;
13 |
--------------------------------------------------------------------------------
/migrations/000021_remove_user_permission_primary_key.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE twitch_user_channel_permissions DROP PRIMARY KEY;
2 |
--------------------------------------------------------------------------------
/migrations/000022_report_add_timestamp.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE Report ADD COLUMN time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
2 |
--------------------------------------------------------------------------------
/migrations/000023_rename_tables.up.sql:
--------------------------------------------------------------------------------
1 | RENAME TABLE pb_bot TO Bot, twitch_user_channel_permissions TO TwitchUserChannelPermission, twitch_user_permissions TO TwitchUserGlobalPermission;
2 |
--------------------------------------------------------------------------------
/migrations/000024_remove_unused_table.up.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE twitch_users, pb_command;
2 |
--------------------------------------------------------------------------------
/migrations/000025_create_bot_channel_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `BotChannel` (
2 | `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
3 | `bot_id` INT(11) UNSIGNED NOT NULL,
4 | `twitch_channel_id` VARCHAR(64) NOT NULL COMMENT 'i.e. 11148817',
5 | PRIMARY KEY (`id`),
6 | FOREIGN KEY (bot_id)
7 | REFERENCES Bot(id)
8 | ON DELETE CASCADE,
9 | UNIQUE INDEX `bot_channel` (bot_id, twitch_channel_id)
10 | )
11 | COLLATE='utf8mb4_general_ci'
12 | ENGINE=InnoDB
13 | ;
14 |
--------------------------------------------------------------------------------
/migrations/000026_create_bot_channel_module_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `BotChannelModule` (
2 | `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
3 | `bot_channel_id` INT(11) UNSIGNED NOT NULL,
4 | `module_id` VARCHAR(128) NOT NULL COMMENT 'i.e. nuke',
5 | `enabled` BOOLEAN NULL COMMENT 'if null, it uses the modules default enabled value',
6 | `settings` BLOB NULL COMMENT 'json blob with settings',
7 |
8 | PRIMARY KEY(`id`),
9 |
10 | FOREIGN KEY (bot_channel_id)
11 | REFERENCES BotChannel(id)
12 | ON DELETE CASCADE,
13 |
14 | UNIQUE INDEX `bot_channel_module` (bot_channel_id, module_id)
15 | )
16 | COLLATE='utf8mb4_general_ci'
17 | ENGINE=InnoDB
18 | ;
19 |
--------------------------------------------------------------------------------
/migrations/000027_make_moderation_action_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS `ModerationAction` (
2 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
3 | `ChannelID` varchar(64) NOT NULL COMMENT 'Twitch Channel owners user ID',
4 | `UserID` varchar(64) NOT NULL COMMENT 'Source user ID',
5 | `TargetID` varchar(64) NOT NULL COMMENT 'Target user ID (the user who has banned/unbanned/timed out)',
6 | `Action` smallint(2) NOT NULL COMMENT 'Action in int format, enums declared outside of SQL',
7 | `Duration` int(11) DEFAULT NULL COMMENT 'Duration of action (only used for timeouts atm)',
8 | `Reason` text COMMENT 'Reason for ban. Auto filled in from twich chat, but can be modified in web gui',
9 | `Timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Timestamp of when the timeout occured',
10 | `Context` text,
11 | PRIMARY KEY (`id`),
12 | KEY `ChannelUserTarget_INDEX` (`ChannelID`,`UserID`,`TargetID`),
13 | KEY `ChannelTargetAction_INDEX` (`ChannelID`,`TargetID`,`Action`)
14 | )
15 | COLLATE='utf8mb4_general_ci'
16 | ENGINE=InnoDB
17 | ;
18 |
--------------------------------------------------------------------------------
/migrations/000028_create_user_sessions_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `UserSession` (
2 | `id` varchar(64) NOT NULL,
3 | `user_id` INT(11) UNSIGNED NOT NULL,
4 | `expiry_date` TIMESTAMP NOT NULL DEFAULT 0,
5 | PRIMARY KEY (`id`),
6 | FOREIGN KEY (user_id)
7 | REFERENCES User(id)
8 | ON DELETE CASCADE
9 | )
10 | COLLATE='utf8mb4_general_ci'
11 | ENGINE=InnoDB
12 | ;
13 |
--------------------------------------------------------------------------------
/migrations/000029_remove_user_twitch_nonce_column.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `User` DROP COLUMN twitch_nonce;
2 |
--------------------------------------------------------------------------------
/migrations/000030_create_report_history_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `ReportHistory` (
2 | `id` INT(11) UNSIGNED NOT NULL COMMENT 'report id, same as in Report',
3 | `channel_id` VARCHAR(64) NOT NULL COMMENT 'twitch ID of channel user was reported in',
4 | `channel_name` VARCHAR(64) NOT NULL COMMENT 'twitch username of channel the user was reported in',
5 | `channel_type` VARCHAR(64) NOT NULL,
6 | `reporter_id` VARCHAR(64) NOT NULL COMMENT 'twitch user ID of reporter',
7 | `reporter_name` VARCHAR(64) NOT NULL COMMENT 'twitch user name of reporter',
8 | `target_id` VARCHAR(64) NOT NULL COMMENT 'twitch user ID of person being reported',
9 | `target_name` VARCHAR(64) NOT NULL COMMENT 'twitch user name of person being reported',
10 | `reason` TEXT,
11 | `logs` TEXT,
12 | `time` timestamp NOT NULL DEFAULT 0 COMMENT 'time report was added',
13 |
14 | `handler_id` VARCHAR(64) NOT NULL COMMENT 'twitch user ID of person who handled the report',
15 | `handler_name` VARCHAR(64) NOT NULL COMMENT 'twitch user name of person who handled the report',
16 |
17 | `action` TINYINT UNSIGNED NOT NULL COMMENT 'number constant for what action was taken for the report. 1 = ban, 2 = timeout, 3 = dismiss',
18 | `action_duration` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'number of seconds for the action. only relevant for timeouts',
19 |
20 | `time_handled` timestamp NOT NULL DEFAULT 0,
21 |
22 | PRIMARY KEY (`id`)
23 | )
24 | COLLATE='utf8mb4_general_ci'
25 | ENGINE=InnoDB
26 | ;
27 |
--------------------------------------------------------------------------------
/migrations/000031_create_warning_scale_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `WarningScale` (
2 | `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
3 |
4 | PRIMARY KEY (`id`)
5 | )
6 | COMMENT='Store data about warning scales'
7 | COLLATE='utf8mb4_general_ci'
8 | ENGINE=InnoDB
9 | ;
10 |
--------------------------------------------------------------------------------
/migrations/000032_create_banphrase_group_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `BanphraseGroup` (
2 | `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
3 | `enabled` TINYINT(1) NOT NULL DEFAULT 1,
4 | `name` VARCHAR(64) NOT NULL,
5 | `description` TEXT NULL COMMENT 'Optional description of the banphrase group, i.e. racism or banned emote',
6 | `length` INT(11) UNSIGNED NOT NULL DEFAULT 60 COMMENT '0 = permaban, >0 = timeout for X seconds',
7 | `warning_id` INT(11) UNSIGNED NULL DEFAULT NULL COMMENT 'ID to a warning "scale"',
8 | `case_sensitive` TINYINT(1) NOT NULL DEFAULT 0,
9 | `type` INT(11) NOT NULL DEFAULT 0 COMMENT '0 = contains, more IDs can be found in the go code lol xd',
10 | `sub_immunity` TINYINT(1) NOT NULL DEFAULT 0,
11 | `remove_accents` TINYINT(1) NOT NULL DEFAULT 0,
12 |
13 | PRIMARY KEY (`id`),
14 | FOREIGN KEY (warning_id)
15 | REFERENCES WarningScale(id)
16 | ON DELETE SET NULL,
17 | UNIQUE INDEX `group_name` (name)
18 | )
19 | COMMENT='Store banphrase groups. this will make it easier to manage multiple banphrases at the same time'
20 | COLLATE='utf8mb4_general_ci'
21 | ENGINE=InnoDB
22 | ;
23 |
--------------------------------------------------------------------------------
/migrations/000033_create_banphrase_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `Banphrase` (
2 | `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
3 | `group_id` INT(11) UNSIGNED NULL DEFAULT NULL,
4 | `enabled` TINYINT(1) NULL DEFAULT 1 COMMENT 'NULL = Inherit from group',
5 | `description` TEXT NULL COMMENT 'Optional description of the banphrase, i.e. racism or banned emote',
6 | `phrase` TEXT NOT NULL COMMENT 'The banned phrase itself. This can be a regular expression, it all depends on the "operator" of the banphrase',
7 | `length` INT(11) UNSIGNED NULL DEFAULT 60 COMMENT 'NULL = Inherit from group, 0 = permaban, >0 = timeout for X seconds',
8 | `warning_id` INT(11) UNSIGNED NULL COMMENT 'NULL = Inherit from group, anything else is an ID to a warning "scale"',
9 | `case_sensitive` TINYINT(1) NULL COMMENT 'NULL = Inherit from group',
10 | `type` INT(11) NULL DEFAULT 0 COMMENT 'NULL = Inherit from group, 0 = contains, more IDs can be found in the go code lol xd',
11 | `sub_immunity` TINYINT(1) NULL DEFAULT 0 COMMENT 'NULL = Inherit from group',
12 | `remove_accents` TINYINT(1) NULL DEFAULT 0 COMMENT 'NULL = Inherit from group',
13 |
14 | PRIMARY KEY (`id`),
15 | FOREIGN KEY (warning_id)
16 | REFERENCES WarningScale(id)
17 | ON DELETE SET NULL,
18 | FOREIGN KEY (group_id)
19 | REFERENCES BanphraseGroup(id)
20 | ON DELETE SET NULL
21 | )
22 | COMMENT='Store banned phrases'
23 | COLLATE='utf8mb4_general_ci'
24 | ENGINE=InnoDB
25 | ;
26 |
--------------------------------------------------------------------------------
/migrations/20190118000448_add_expiry_column_to_bot_table.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `Bot`
2 | ADD COLUMN `twitch_userid` VARCHAR(64) NOT NULL AFTER `id`,
3 | ADD COLUMN `twitch_access_token_expiry` DATETIME NOT NULL;
4 |
--------------------------------------------------------------------------------
/migrations/20190118204509_add_unique_index_to_bot_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE UNIQUE INDEX `itwitchuid` ON Bot(twitch_userid);
2 |
--------------------------------------------------------------------------------
/migrations/20190118221100_rename_name_column_in_bot_table.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `Bot`
2 | CHANGE name twitch_username varchar(64) NOT NULL;
3 |
--------------------------------------------------------------------------------
/migrations/20190414182945_make_report_history_id_autoincrement.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE ReportHistory MODIFY COLUMN id INT UNSIGNED NOT NULL AUTO_INCREMENT;
2 |
--------------------------------------------------------------------------------
/migrations/psql/1567332318-command-tables.sql:
--------------------------------------------------------------------------------
1 | CREATE TYPE command_action_type AS ENUM('text_response', 'module_action');
2 |
3 | CREATE TYPE module_action_id AS ENUM('modules.nuke.nuke', 'modules.message_height_limit.heighttest');
4 | COMMENT ON TYPE module_action_id IS 'Available raw actions that can be bound as the action of module commands';
5 |
6 | CREATE TABLE command (
7 | id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
8 | bot_channel_id INT NOT NULL REFERENCES bot_channel(id),
9 | cost INT NOT NULL DEFAULT 0,
10 | cd_user INTERVAL NOT NULL DEFAULT interval '15 seconds',
11 | cd_all INTERVAL NOT NULL DEFAULT interval '5 seconds',
12 | enabled BOOLEAN NOT NULL DEFAULT TRUE,
13 | action_type command_action_type NOT NULL, -- this is the descriminator for the polymorphism (type = ENUM('text_response', 'module_action'))
14 | response TEXT, -- for text_response
15 | action_id module_action_id -- for module_action
16 | -- could add CONSTRAINT to check response not null when action_type = 'text_response' for example
17 | );
18 | COMMENT ON TABLE command IS 'Available command on a given bot, in a given channel';
19 |
20 | CREATE TABLE command_trigger (
21 | command_id INT REFERENCES command(id),
22 | trigger TEXT NOT NULL UNIQUE, -- notice the extra unique constraint
23 | PRIMARY KEY (command_id, trigger)
24 | );
25 | COMMENT ON TABLE command_trigger IS 'Aliases/Triggers for a given command';
26 |
27 | CREATE TYPE permission AS ENUM(
28 | 'twitch_moderator',
29 | 'create_command',
30 | 'edit_command',
31 | 'delete_command',
32 | 'add_banphrase' -- etc.
33 | );
34 | COMMENT ON TYPE permission IS 'Available permissions that can be granted, revoked and required';
35 |
36 | CREATE TABLE command_execute_permission_requirement (
37 | command_id INT REFERENCES command(id),
38 | permission_id permission NOT NULL,
39 | PRIMARY KEY (command_id, permission_id)
40 | );
41 | COMMENT ON TABLE command_execute_permission_requirement IS 'Permissions required to execute a command';
42 |
43 |
--------------------------------------------------------------------------------
/migrations/psql/newmig.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | script_path="$(dirname "$0")"
6 | description="$1"
7 |
8 | if [ -z "$description" ]; then
9 | >&2 echo "usage: $0 description-of-migration"
10 | exit 1
11 | fi
12 |
13 | touch "$script_path/$(date --utc '+%s')-$description.sql"
14 |
--------------------------------------------------------------------------------
/pkg/account.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | type Account interface {
4 | // full id of user (i.e. 11148817)
5 | ID() string
6 |
7 | // full lowercase name (i.e. pajlada)
8 | Name() string
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/action.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type MuteType uint
8 |
9 | const (
10 | MuteTypeTemporary MuteType = iota
11 | MuteTypePermanent
12 | )
13 |
14 | // MuteAction defines an action that will mute/timeout/ban or otherwise stop a user from participating in chat, either temporarily or permanently
15 | type MuteAction interface {
16 | User() User
17 | SetReason(reason string)
18 | Reason() string
19 |
20 | Type() MuteType
21 |
22 | Duration() time.Duration
23 | }
24 |
25 | // UnmuteAction defines an action that will unmute/untimeout or unban a user
26 | type UnmuteAction interface {
27 | User() User
28 |
29 | Type() MuteType
30 | }
31 |
32 | // DeleteAction defines an action that will delete a message
33 | type DeleteAction interface {
34 | Message() string
35 | }
36 |
37 | // MessageAction defines a message that will be publicly displayed
38 | type MessageAction interface {
39 | // TODO: Add reply message action
40 | SetAction(v bool)
41 | Evaluate() string
42 | }
43 |
44 | // WhisperAction defines a message that will be privately sent to a user
45 | type WhisperAction interface {
46 | User() User
47 | Content() string
48 | }
49 |
50 | // Actions is a list of actions that wants to be run
51 | // An implementation of this can decide to filter out all mutes except for the "most grave one"
52 | type Actions interface {
53 | Timeout(user User, duration time.Duration) MuteAction
54 |
55 | Ban(user User) MuteAction
56 |
57 | Unban(user User) UnmuteAction
58 |
59 | Say(content string) MessageAction
60 |
61 | Delete(message string) DeleteAction
62 |
63 | Mention(user User, content string) MessageAction
64 |
65 | Whisper(user User, content string) WhisperAction
66 |
67 | Mutes() []MuteAction
68 | Unmutes() []UnmuteAction
69 | Deletes() []DeleteAction
70 | Messages() []MessageAction
71 | Whispers() []WhisperAction
72 |
73 | StopPropagation() bool
74 |
75 | // DoOnSuccess(func())
76 |
77 | // Do(func())
78 | }
79 |
--------------------------------------------------------------------------------
/pkg/apirequest/apirequest.go:
--------------------------------------------------------------------------------
1 | package apirequest
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | )
7 |
8 | // HTTPRequest requests the given url
9 | func HTTPRequest(url string) ([]byte, error) {
10 | req, err := http.Get(url)
11 | if err != nil {
12 | return nil, err
13 | }
14 | bs, err := io.ReadAll(req.Body)
15 | if err != nil {
16 | return nil, err
17 | }
18 | return bs, nil
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/apirequest/bttv.go:
--------------------------------------------------------------------------------
1 | package apirequest
2 |
3 | import "github.com/pajlada/gobttv"
4 |
5 | // BTTV is the BTTV api endpoint master
6 | var BTTV = gobttv.New()
7 |
--------------------------------------------------------------------------------
/pkg/apirequest/common.go:
--------------------------------------------------------------------------------
1 | package apirequest
2 |
--------------------------------------------------------------------------------
/pkg/apirequest/custom.go:
--------------------------------------------------------------------------------
1 | package apirequest
2 |
--------------------------------------------------------------------------------
/pkg/apirequest/ffz.go:
--------------------------------------------------------------------------------
1 | package apirequest
2 |
3 | import "github.com/pajlada/goffz"
4 |
5 | // FFZ is the FFZ api endpoint master
6 | var FFZ = goffz.New()
7 |
--------------------------------------------------------------------------------
/pkg/apirequest/twitch.go:
--------------------------------------------------------------------------------
1 | package apirequest
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/pkg/common/config"
5 | )
6 |
7 | func InitTwitch(cfg *config.Config) (err error) {
8 | err = initWrapper(cfg)
9 |
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/apirequest/twitchratelimit.go:
--------------------------------------------------------------------------------
1 | package apirequest
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strconv"
7 | "sync"
8 | "time"
9 | )
10 |
11 | type TwitchRateLimit struct {
12 | mutex *sync.RWMutex
13 |
14 | // The rate at which points are added to your bucket. This is the average number of requests per minute you can make over an extended period of time.
15 | Limit int
16 |
17 | // The number of points you have left to use.
18 | Remaining int
19 |
20 | // A timestamp of when your bucket is reset to full.
21 | Reset time.Time
22 | }
23 |
24 | func NewTwitchRateLimit() TwitchRateLimit {
25 | return TwitchRateLimit{
26 | mutex: &sync.RWMutex{},
27 | }
28 | }
29 |
30 | func (l *TwitchRateLimit) Update(r *http.Response) {
31 | limit := r.Header.Get("Ratelimit-Limit")
32 | remaining := r.Header.Get("Ratelimit-Remaining")
33 | reset := r.Header.Get("Ratelimit-Reset")
34 |
35 | if limit == "" || remaining == "" || reset == "" {
36 | return
37 | }
38 |
39 | nLimit, err := strconv.Atoi(limit)
40 | if err != nil {
41 | fmt.Println("Error parsing limit from", limit)
42 | }
43 | nRemaining, err := strconv.Atoi(remaining)
44 | if err != nil {
45 | fmt.Println("Error parsing remaining from", remaining)
46 | }
47 |
48 | l.mutex.Lock()
49 | defer l.mutex.Unlock()
50 |
51 | l.Limit = nLimit
52 | l.Remaining = nRemaining
53 | }
54 |
55 | func (l *TwitchRateLimit) String() string {
56 | return fmt.Sprintf("[RateLimit Limit=%d Remaining=%d Reset=%s]", l.Limit, l.Remaining, l.Reset)
57 | }
58 |
--------------------------------------------------------------------------------
/pkg/application.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | import (
4 | "database/sql"
5 | )
6 |
7 | // Application is an instance of pajbot2
8 | // It's responsible for initializing all bot accounts (`Bot` class)
9 | type Application interface {
10 | UserStore() UserStore
11 | ChannelStore() ChannelStore
12 | UserContext() UserContext
13 | StreamStore() StreamStore
14 | SQL() *sql.DB
15 | PubSub() PubSub
16 | TwitchBots() BotStore
17 | QuitChannel() chan string
18 | TwitchAuths() TwitchAuths
19 | MIMO() MIMO
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/auth.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | import "golang.org/x/oauth2"
4 |
5 | type TwitchAuths interface {
6 | Bot() *oauth2.Config
7 | Streamer() *oauth2.Config
8 | User() *oauth2.Config
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/banphrase.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | import "time"
4 |
5 | type Banphrase interface {
6 | Triggers(text string) bool
7 | IsCaseSensitive() bool
8 |
9 | // IsAdvanced decides whether or not the banphrase should be run on all variations or only the first one
10 | IsAdvanced() bool
11 |
12 | GetName() string
13 | GetID() int
14 | GetDuration() time.Duration
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/bot.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | import "golang.org/x/oauth2"
4 |
5 | type Sender interface {
6 | TwitchAccount() TwitchAccount
7 | GetTokenSource() oauth2.TokenSource
8 |
9 | Connected() bool
10 |
11 | Say(Channel, string)
12 | Mention(Channel, User, string)
13 | Whisper(User, string)
14 | Whisperf(User, string, ...interface{})
15 |
16 | // Timeout times the user out a single time immediately
17 | Timeout(Channel, User, int, string)
18 |
19 | Ban(Channel, User, string)
20 |
21 | GetPoints(Channel, string) uint64
22 |
23 | // give or remove points from user in channel
24 | BulkEdit(string, []string, int32)
25 |
26 | AddPoints(Channel, string, uint64) (bool, uint64)
27 | RemovePoints(Channel, string, uint64) (bool, uint64)
28 | ForceRemovePoints(Channel, string, uint64) uint64
29 |
30 | PointRank(Channel, string) uint64
31 |
32 | // ChannelIDs returns a slice of the channels this bot is connected to
33 | ChannelIDs() []string
34 |
35 | InChannel(string) bool
36 | InChannelName(string) bool
37 | GetUserStore() UserStore
38 | GetUserContext() UserContext
39 |
40 | GetBotChannel(channelName string) BotChannel
41 | GetBotChannelByID(channelID string) BotChannel
42 |
43 | MakeUser(string) User
44 | MakeChannel(string) Channel
45 |
46 | // Permanently join channel with the given channel ID
47 | JoinChannel(channelID string) error
48 |
49 | // Permanently leave channel with the given channel ID
50 | LeaveChannel(channelID string) error
51 |
52 | // Connect to the OnNewChannelJoined callback
53 | OnNewChannelJoined(cb func(channel Channel))
54 |
55 | Quit(message string)
56 |
57 | Application() Application
58 |
59 | // DEV
60 | Disconnect()
61 | }
62 |
--------------------------------------------------------------------------------
/pkg/botchannel.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | import "github.com/pajbot/pajbot2/pkg/eventemitter"
4 |
5 | type MessageSender interface {
6 | Say(string)
7 | Mention(User, string)
8 |
9 | // Moderation
10 | Timeout(User, int, string)
11 | Ban(User, string)
12 | }
13 |
14 | type BotChannel interface {
15 | // Implement Channel interface
16 | GetName() string
17 | GetID() string
18 |
19 | MessageSender
20 |
21 | DatabaseID() int64
22 | Channel() Channel
23 | ChannelID() string
24 | ChannelName() string
25 |
26 | EnableModule(string) error
27 | DisableModule(string) error
28 | GetModule(string) (Module, error)
29 |
30 | // Implement ChannelWithStream interface
31 | Stream() Stream
32 |
33 | Events() *eventemitter.EventEmitter
34 |
35 | HandleMessage(user User, message Message) error
36 | HandleEventSubNotification(notification TwitchEventSubNotification) error
37 | OnModules(cb func(module Module) Actions, stop bool) []Actions
38 |
39 | SetSubscribers(state bool) error
40 | SetUniqueChat(state bool) error
41 | SetEmoteOnly(state bool) error
42 | SetSlowMode(state bool, durationS int) error
43 | SetFollowerMode(state bool, durationM int) error
44 | SetNonModChatDelay(state bool, durationS int) error
45 |
46 | Bot() Sender
47 | }
48 |
--------------------------------------------------------------------------------
/pkg/botstore.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | type BotStore interface {
4 | Add(Sender)
5 |
6 | GetBotFromName(string) Sender
7 | GetBotFromID(string) Sender
8 | GetBotFromChannel(string) Sender
9 |
10 | Iterate() BotStoreIterator
11 | }
12 |
13 | type BotStoreIterator interface {
14 | Next() bool
15 | Value() Sender
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/botstore/botstore.go:
--------------------------------------------------------------------------------
1 | package botstore
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | )
8 |
9 | var _ pkg.BotStore = &BotStore{}
10 |
11 | type BotStore struct {
12 | store []pkg.Sender
13 | }
14 |
15 | func New() *BotStore {
16 | return &BotStore{}
17 | }
18 |
19 | func (s *BotStore) Add(bot pkg.Sender) {
20 | s.store = append(s.store, bot)
21 | }
22 |
23 | func (s *BotStore) GetBotFromName(name string) pkg.Sender {
24 | for _, b := range s.store {
25 | if b.TwitchAccount().Name() == strings.ToLower(name) {
26 | return b
27 | }
28 | }
29 |
30 | return nil
31 | }
32 |
33 | func (s *BotStore) GetBotFromID(id string) pkg.Sender {
34 | for _, b := range s.store {
35 | if b.TwitchAccount().ID() == id {
36 | return b
37 | }
38 | }
39 |
40 | return nil
41 | }
42 |
43 | func (s *BotStore) GetBotFromChannel(channelID string) pkg.Sender {
44 | for _, b := range s.store {
45 | botExists := b.InChannel(channelID)
46 | if botExists {
47 | return b
48 | }
49 | }
50 |
51 | return nil
52 | }
53 |
54 | func (s *BotStore) Iterate() pkg.BotStoreIterator {
55 | return &BotStoreIterator{
56 | data: s.store,
57 | index: -1,
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/pkg/botstore/botstoreiterator.go:
--------------------------------------------------------------------------------
1 | package botstore
2 |
3 | import "github.com/pajbot/pajbot2/pkg"
4 |
5 | var _ pkg.BotStoreIterator = &BotStoreIterator{}
6 |
7 | type BotStoreIterator struct {
8 | data []pkg.Sender
9 |
10 | index int
11 | }
12 |
13 | func (i *BotStoreIterator) Next() bool {
14 | i.index++
15 |
16 | return i.index < len(i.data)
17 | }
18 |
19 | func (i *BotStoreIterator) Value() pkg.Sender {
20 | return i.data[i.index]
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/channel.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | // Channel is the most barebones way of accessing a Twitch channel
4 | // For a Channel to 'live' we must be able to access its Name (Twitch User Login) and ID (Twitch User ID)
5 | type Channel interface {
6 | GetName() string
7 | GetID() string
8 | }
9 |
10 | type ChannelWithStream interface {
11 | Channel
12 |
13 | Stream() Stream
14 | }
15 |
16 | type ChannelStore interface {
17 | TwitchChannel(channelID string) Channel
18 | RegisterTwitchChannel(channel Channel)
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/channels/store.go:
--------------------------------------------------------------------------------
1 | package channels
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | )
8 |
9 | var _ pkg.ChannelStore = &Store{}
10 |
11 | type Store struct {
12 | data map[string]pkg.Channel
13 | dataMutex sync.Mutex
14 | }
15 |
16 | func (s *Store) TwitchChannel(channelID string) (channel pkg.Channel) {
17 | s.dataMutex.Lock()
18 | defer s.dataMutex.Unlock()
19 |
20 | channel = s.data[channelID]
21 |
22 | return
23 | }
24 |
25 | func (s *Store) RegisterTwitchChannel(channel pkg.Channel) {
26 | s.dataMutex.Lock()
27 | defer s.dataMutex.Unlock()
28 |
29 | s.data[channel.GetID()] = channel
30 | }
31 |
32 | func NewStore() *Store {
33 | return &Store{
34 | data: make(map[string]pkg.Channel),
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/channels/twitchchannel.go:
--------------------------------------------------------------------------------
1 | package channels
2 |
3 | type TwitchChannel struct {
4 | Channel string
5 | ID string
6 | }
7 |
8 | func (c TwitchChannel) GetName() string {
9 | return c.Channel
10 | }
11 |
12 | func (c TwitchChannel) GetID() string {
13 | return c.ID
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/commandlist/commandlist.go:
--------------------------------------------------------------------------------
1 | package commandlist
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | )
8 |
9 | var (
10 | commandsMutex sync.Mutex
11 | commands []pkg.CommandInfo
12 | )
13 |
14 | func Register(commandInfo pkg.CommandInfo) {
15 | commandsMutex.Lock()
16 | commands = append(commands, commandInfo)
17 | commandsMutex.Unlock()
18 | }
19 |
20 | func Commands() []pkg.CommandInfo {
21 | commandsMutex.Lock()
22 | defer commandsMutex.Unlock()
23 | return commands
24 | }
25 |
26 | func init() {
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/commands.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | type CommandInfo struct {
4 | Name string
5 | Description string
6 |
7 | Maker func() CustomCommand2 `json:"-"`
8 | }
9 |
10 | type SimpleCommand interface {
11 | Trigger([]string, MessageEvent) Actions
12 | }
13 |
14 | type CustomCommand2 interface {
15 | SimpleCommand
16 |
17 | HasCooldown(User) bool
18 | AddCooldown(User)
19 | }
20 |
21 | type CommandMatcher interface {
22 | Register(aliases []string, command interface{}) interface{}
23 | Deregister(command interface{})
24 | DeregisterAliases(aliases []string)
25 | Match(text string) (interface{}, []string)
26 | }
27 |
28 | type CommandsManager interface {
29 | CommandMatcher
30 | OnMessage(event MessageEvent) Actions
31 |
32 | FindByCommandID(id int64) interface{}
33 |
34 | Register2(id int64, aliases []string, command interface{})
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/commands/base.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
--------------------------------------------------------------------------------
/pkg/commands/commands.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "github.com/pajbot/commandmatcher"
5 | "github.com/pajbot/pajbot2/pkg"
6 | )
7 |
8 | type Commands struct {
9 | *commandmatcher.CommandMatcher
10 |
11 | internalCommands map[int64]interface{}
12 | }
13 |
14 | func NewCommands() *Commands {
15 | c := &Commands{
16 | CommandMatcher: commandmatcher.New(),
17 |
18 | internalCommands: map[int64]interface{}{},
19 | }
20 |
21 | return c
22 | }
23 |
24 | func (c *Commands) Register2(id int64, triggers []string, cmd interface{}) {
25 | c.CommandMatcher.Register(triggers, cmd)
26 | c.internalCommands[id] = cmd
27 | }
28 |
29 | func (c *Commands) FindByCommandID(id int64) interface{} {
30 | return c.internalCommands[id]
31 | }
32 |
33 | func (c *Commands) OnMessage(event pkg.MessageEvent) pkg.Actions {
34 | message := event.Message
35 | user := event.User
36 |
37 | match, parts := c.Match(message.GetText())
38 | if match != nil {
39 | switch command := match.(type) {
40 | case pkg.CustomCommand2:
41 | if command.HasCooldown(user) {
42 | return nil
43 | }
44 | command.AddCooldown(user)
45 | return command.Trigger(parts, event)
46 |
47 | case pkg.SimpleCommand:
48 | return command.Trigger(parts, event)
49 | }
50 | }
51 |
52 | return nil
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/commands/join.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/pajbot/pajbot2/internal/commands/base"
8 | "github.com/pajbot/pajbot2/pkg"
9 | "github.com/pajbot/pajbot2/pkg/commandlist"
10 | "github.com/pajbot/pajbot2/pkg/twitchactions"
11 | )
12 |
13 | func init() {
14 | commandlist.Register(pkg.CommandInfo{
15 | Name: "Join",
16 | Description: "xd join lol",
17 | // FIXME
18 | // Maker: NewJoin,
19 | })
20 | }
21 |
22 | type Join struct {
23 | base.Command
24 |
25 | bot pkg.BotChannel
26 | }
27 |
28 | func NewJoin(bot pkg.BotChannel) pkg.CustomCommand2 {
29 | return &Join{
30 | Command: base.New(),
31 |
32 | bot: bot,
33 | }
34 | }
35 |
36 | func (c Join) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
37 | user := event.User
38 | if !user.HasGlobalPermission(pkg.PermissionAdmin) {
39 | const errorMessage = "you do not have permission to use this command. Admin permission is required"
40 | return twitchactions.Mention(user, errorMessage)
41 | }
42 |
43 | if len(parts) < 2 {
44 | return nil
45 | }
46 |
47 | channelName := parts[1]
48 |
49 | if strings.EqualFold(channelName, c.bot.Bot().TwitchAccount().Name()) {
50 | const errorMessage = "I cannot join my own channel"
51 | return twitchactions.Mention(user, errorMessage)
52 | }
53 |
54 | channelID := c.bot.Bot().GetUserStore().GetID(channelName)
55 | if channelID == "" {
56 | const errorMessage = "no channel with that name exists"
57 | return twitchactions.Mention(user, errorMessage)
58 | }
59 |
60 | err := c.bot.Bot().JoinChannel(channelID)
61 | if err != nil {
62 | return twitchactions.Mention(user, err.Error())
63 | }
64 |
65 | return twitchactions.Mention(user, fmt.Sprintf("joined channel %s(%s)", channelName, channelID))
66 | }
67 |
--------------------------------------------------------------------------------
/pkg/commands/leave.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/pajbot/pajbot2/internal/commands/base"
8 | "github.com/pajbot/pajbot2/pkg"
9 | "github.com/pajbot/pajbot2/pkg/commandlist"
10 | "github.com/pajbot/pajbot2/pkg/twitchactions"
11 | )
12 |
13 | func init() {
14 | commandlist.Register(pkg.CommandInfo{
15 | Name: "Leave",
16 | Description: "xd leave lol",
17 | // Maker: NewLeave,
18 | })
19 | }
20 |
21 | type Leave struct {
22 | base.Command
23 |
24 | bot pkg.BotChannel
25 | }
26 |
27 | func NewLeave(bot pkg.BotChannel) pkg.CustomCommand2 {
28 | return &Leave{
29 | Command: base.New(),
30 |
31 | bot: bot,
32 | }
33 | }
34 |
35 | func (c Leave) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
36 | user := event.User
37 |
38 | if !user.HasGlobalPermission(pkg.PermissionAdmin) {
39 | return twitchactions.Mention(user, "you do not have permission to use this command. Admin permission is required")
40 | }
41 |
42 | if len(parts) < 2 {
43 | return nil
44 | }
45 |
46 | channelName := parts[1]
47 |
48 | if strings.EqualFold(channelName, c.bot.Bot().TwitchAccount().Name()) {
49 | return twitchactions.Mention(user, "I cannot leave my own channel")
50 | }
51 |
52 | channelID := c.bot.Bot().GetUserStore().GetID(channelName)
53 | if channelID == "" {
54 | return twitchactions.Mention(user, "no channel with that name exists")
55 | }
56 |
57 | err := c.bot.Bot().LeaveChannel(channelID)
58 | if err != nil {
59 | return twitchactions.Mention(user, "Error leaving channel: "+err.Error())
60 | }
61 |
62 | return twitchactions.Mention(user, fmt.Sprintf("left channel %s(%s)", channelName, channelID))
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/commands/module.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/pajbot/pajbot2/internal/commands/base"
7 | "github.com/pajbot/pajbot2/pkg"
8 | "github.com/pajbot/pajbot2/pkg/twitchactions"
9 | )
10 |
11 | type moduleCommand struct {
12 | base.Command
13 |
14 | subCommands *subCommands
15 | defaultSubCommand string
16 | }
17 |
18 | func NewModule(bot pkg.BotChannel) pkg.CustomCommand2 {
19 | u := &moduleCommand{
20 | Command: base.New(),
21 | subCommands: newSubCommands(),
22 | defaultSubCommand: "list",
23 | }
24 |
25 | u.UserCooldown = 0
26 | u.GlobalCooldown = 0
27 |
28 | u.subCommands.add("list", &subCommand{
29 | permission: pkg.PermissionAdmin,
30 | cb: func(parts []string, event pkg.MessageEvent) pkg.Actions {
31 | return twitchactions.Mention(event.User, "TODO: list modules")
32 | },
33 | })
34 |
35 | u.subCommands.add("enable", &subCommand{
36 | permission: pkg.PermissionAdmin,
37 | cb: func(parts []string, event pkg.MessageEvent) pkg.Actions {
38 | if len(parts) < 3 {
39 | return twitchactions.Mention(event.User, "usage: !module enable MODULE_ID")
40 | }
41 |
42 | moduleID := parts[2]
43 |
44 | err := bot.EnableModule(moduleID)
45 | if err != nil {
46 | return twitchactions.Mention(event.User, err.Error())
47 | }
48 |
49 | return twitchactions.Mentionf(event.User, "Enabled module %s", moduleID)
50 | },
51 | })
52 |
53 | u.subCommands.addSC("disable", &subCommand{
54 | permission: pkg.PermissionAdmin,
55 | cb: func(parts []string, event pkg.MessageEvent) pkg.Actions {
56 | if len(parts) < 3 {
57 | return twitchactions.Mention(event.User, "usage: !module disable MODULE_ID")
58 | }
59 |
60 | moduleID := parts[2]
61 |
62 | err := bot.DisableModule(moduleID)
63 | if err != nil {
64 | return twitchactions.Mention(event.User, err.Error())
65 | }
66 |
67 | return twitchactions.Mentionf(event.User, "Disabled module %s", moduleID)
68 | },
69 | })
70 |
71 | return u
72 | }
73 |
74 | func (c *moduleCommand) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
75 | subCommandName := c.defaultSubCommand
76 | if len(parts) >= 2 {
77 | subCommandName = strings.ToLower(parts[1])
78 | }
79 |
80 | if subCommand, ok := c.subCommands.find(subCommandName); ok {
81 | return subCommand.run(parts, event)
82 | }
83 |
84 | return nil
85 | }
86 |
--------------------------------------------------------------------------------
/pkg/commands/pajbot1_command.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | "strings"
7 |
8 | "github.com/pajbot/pajbot2/pkg"
9 | )
10 |
11 | const commandPrefix = "!"
12 |
13 | // Pajbot1Command is a command loaded from the old pajbot1 database
14 | type Pajbot1Command struct {
15 | Level int
16 | Action string
17 | Triggers []string
18 | Enabled bool
19 | PointCost int
20 |
21 | GlobalCooldown int
22 | UserCooldown int
23 |
24 | CanExecuteWithWhisper bool
25 | SubOnly bool
26 | ModOnly bool
27 |
28 | TokenCost int
29 |
30 | // If a command has possible userdata, it means we should run it through our banphrase filter before printing it
31 | HasUserdata bool
32 | }
33 |
34 | type pajbot1CommandAction struct {
35 | Message string `json:"message"`
36 | Type string `json:"type"`
37 | }
38 |
39 | func (c *Pajbot1Command) LoadScan(rows *sql.Rows) error {
40 | var actionString []byte
41 | var commandString string
42 | const queryF = `SELECT level, action, command, delay_all, delay_user, enabled, cost, can_execute_with_whisper, sub_only, mod_only, tokens_cost FROM tb_command`
43 | err := rows.Scan(&c.Level, &actionString, &commandString, &c.GlobalCooldown, &c.UserCooldown, &c.Enabled, &c.PointCost, &c.CanExecuteWithWhisper, &c.SubOnly, &c.ModOnly, &c.TokenCost)
44 | if err != nil {
45 | return err
46 | }
47 |
48 | var action pajbot1CommandAction
49 |
50 | err = json.Unmarshal(actionString, &action)
51 | if err != nil {
52 | return err
53 | }
54 |
55 | c.Triggers = strings.Split(commandString, "|")
56 |
57 | for key, t := range c.Triggers {
58 | c.Triggers[key] = commandPrefix + t
59 | }
60 |
61 | c.Action = action.Message
62 |
63 | return nil
64 | }
65 |
66 | func (c *Pajbot1Command) IsTriggered(parts []string) bool {
67 | for _, trigger := range c.Triggers {
68 | if strings.ToLower(parts[0]) == trigger {
69 | return true
70 | }
71 | }
72 |
73 | return false
74 | }
75 |
76 | func (c *Pajbot1Command) Trigger(bot pkg.BotChannel, user pkg.User, parts []string) error {
77 | bot.Say(c.Action)
78 |
79 | return nil
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/commands/ping.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 |
7 | "github.com/pajbot/pajbot2/internal/commands/base"
8 | "github.com/pajbot/pajbot2/pkg"
9 | "github.com/pajbot/pajbot2/pkg/commandlist"
10 | "github.com/pajbot/pajbot2/pkg/common"
11 | "github.com/pajbot/pajbot2/pkg/twitchactions"
12 | "github.com/pajbot/utils"
13 | )
14 |
15 | func init() {
16 | commandlist.Register(pkg.CommandInfo{
17 | Name: "Ping",
18 | Description: "xd ping lol",
19 | Maker: NewPing,
20 | })
21 | }
22 |
23 | type Ping struct {
24 | base.Command
25 | }
26 |
27 | func NewPing() pkg.CustomCommand2 {
28 | return &Ping{
29 | Command: base.New(),
30 | }
31 | }
32 |
33 | func (c Ping) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
34 | msg := fmt.Sprintf("pb2 has been running for %s (%s %s", utils.TimeSince(startTime), common.Version(), runtime.Version())
35 | if common.BuildTime != "" {
36 | msg += " built " + common.BuildTime
37 | }
38 | msg += ")"
39 |
40 | return twitchactions.Mention(event.User, msg)
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/commands/quit.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/internal/commands/base"
5 | "github.com/pajbot/pajbot2/pkg"
6 | "github.com/pajbot/pajbot2/pkg/commandlist"
7 | )
8 |
9 | func init() {
10 | commandlist.Register(pkg.CommandInfo{
11 | Name: "Quit",
12 | Description: "quit the bot",
13 | // FIXME
14 | // Maker: NewQuit,
15 | })
16 | }
17 |
18 | type Quit struct {
19 | base.Command
20 |
21 | bot pkg.BotChannel
22 | }
23 |
24 | func NewQuit(bot pkg.BotChannel) pkg.CustomCommand2 {
25 | c := &Quit{
26 | Command: base.New(),
27 |
28 | bot: bot,
29 | }
30 |
31 | return c
32 | }
33 |
34 | func (c *Quit) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
35 | user := event.User
36 |
37 | // FIXME: Channel should be part of the message event
38 | if !user.HasPermission(c.bot.Channel(), pkg.PermissionAdmin) {
39 | return nil
40 | }
41 |
42 | // FIXME: this should be an "on done action"
43 | c.bot.Bot().Quit("hehe")
44 |
45 | return nil
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/commands/rank.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/pkg"
5 | )
6 |
7 | type Rank struct {
8 | }
9 |
10 | func (c Rank) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
11 | // FIXME: Re-implement (POINTS SYSTEM)
12 | // var potentialTarget string
13 | // targetID := event.User.GetID()
14 |
15 | // if len(parts) >= 2 {
16 | // potentialTarget = utils.FilterUsername(parts[1])
17 | // if potentialTarget != "" {
18 | // potentialTargetID := event.UserStore.GetID(potentialTarget)
19 | // if potentialTargetID != "" {
20 | // targetID = potentialTargetID
21 | // } else {
22 | // potentialTarget = ""
23 | // }
24 | // }
25 | // }
26 |
27 | // rank := botChannel.Bot().PointRank(event.Channel, targetID)
28 | // if potentialTarget == "" {
29 | // return twitchactions.Mention(event.User, "you are rank "+strconv.FormatUint(rank, 10)+" in points")
30 | // }
31 |
32 | // return twitchactions.Mention(event.User, potentialTarget+" is rank "+strconv.FormatUint(rank, 10)+" in points")
33 |
34 | return nil
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/commands/subcommands.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/pkg"
5 | "github.com/pajbot/pajbot2/pkg/twitchactions"
6 | )
7 |
8 | type subCommands struct {
9 | m map[string]*subCommand
10 | }
11 |
12 | func newSubCommands() *subCommands {
13 | return &subCommands{
14 | m: make(map[string]*subCommand),
15 | }
16 | }
17 |
18 | func (c *subCommands) add(name string, sc *subCommand) {
19 | c.m[name] = sc
20 | }
21 |
22 | func (c *subCommands) find(name string) (*subCommand, bool) {
23 | a, b := c.m[name]
24 | return a, b
25 | }
26 |
27 | func (c *subCommands) addSC(name string, sc *subCommand) {
28 | c.add(name, sc)
29 | c.add(name+"s", sc)
30 | }
31 |
32 | type subCommandFunc func(parts []string, event pkg.MessageEvent) pkg.Actions
33 |
34 | type subCommand struct {
35 | permission pkg.Permission
36 | cb subCommandFunc
37 | }
38 |
39 | func (c *subCommand) run(parts []string, event pkg.MessageEvent) pkg.Actions {
40 | if c.permission != pkg.PermissionNone {
41 | if !event.User.HasPermission(event.Channel, c.permission) {
42 | return twitchactions.Mention(event.User, "you do not have permission to use this command")
43 | }
44 | }
45 |
46 | return c.cb(parts, event)
47 | }
48 |
--------------------------------------------------------------------------------
/pkg/commandsubstitution/Makefile:
--------------------------------------------------------------------------------
1 | include ../../.make/test.mk
2 |
--------------------------------------------------------------------------------
/pkg/commandsubstitution/helpers_test.go:
--------------------------------------------------------------------------------
1 | package commandsubstitution
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func assertStringsEqual(t *testing.T, expected, actual string) {
8 | if expected != actual {
9 | t.Errorf("failed asserting that \"%s\" is expected \"%s\"", actual, expected)
10 | }
11 | }
12 |
13 | func assertIntsEqual(t *testing.T, expected, actual int) {
14 | if expected != actual {
15 | t.Errorf("failed asserting that \"%d\" is expected \"%d\"", actual, expected)
16 | }
17 | }
18 |
19 | func assertInt32sEqual(t *testing.T, expected, actual int32) {
20 | if expected != actual {
21 | t.Errorf("failed asserting that \"%d\" is expected \"%d\"", actual, expected)
22 | }
23 | }
24 |
25 | func assertTrue(t *testing.T, actual bool, errorMessage string) {
26 | if !actual {
27 | t.Error(errorMessage)
28 | }
29 | }
30 |
31 | func assertFalse(t *testing.T, actual bool, errorMessage string) {
32 | if actual {
33 | t.Error(errorMessage)
34 | }
35 | }
36 |
37 | func assertStringSlicesEqual(t *testing.T, expected, actual []string) {
38 | if actual == nil {
39 | t.Errorf("actual slice was nil")
40 | return
41 | }
42 |
43 | if len(actual) != len(expected) {
44 | t.Errorf("actual slice was not the same length as expected slice")
45 | }
46 |
47 | for i, v := range actual {
48 | if v != expected[i] {
49 | t.Errorf("actual slice value \"%s\" was not equal to expected value \"%s\" at index \"%d\"", v, expected[i], i)
50 | }
51 | }
52 | }
53 |
54 | func assertErrorsEqual(t *testing.T, expected, actual error) {
55 | if expected != actual {
56 | t.Errorf("failed asserting that error \"%s\" is expected \"%s\"", actual, expected)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/common/common.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | // this should make things easier with redis
4 |
5 | import (
6 | "fmt"
7 | "time"
8 | )
9 |
10 | var (
11 | // BuildTime is the time when the binary was built
12 | // filled in with ./build.sh (ldflags)
13 | BuildTime string
14 |
15 | BuildRelease string
16 |
17 | BuildHash string
18 |
19 | BuildBranch string
20 | )
21 |
22 | func Version() string {
23 | if BuildRelease == "git" {
24 | return fmt.Sprintf("%s@%s", BuildHash, BuildBranch)
25 | }
26 |
27 | return BuildRelease
28 | }
29 |
30 | // GlobalUser will only be used by boss to check if user is admin
31 | // and to decide what channel to send the message to if its a whisper
32 | type GlobalUser struct {
33 | LastActive time.Time
34 | Channel string
35 | Level int
36 | }
37 |
38 | // MsgType specifies the message's type, for example PRIVMSG or WHISPER
39 | type MsgType uint32
40 |
41 | // Various message types which describe what sort of message they are
42 | const (
43 | MsgPrivmsg MsgType = iota + 1
44 | MsgWhisper
45 | MsgSub
46 | MsgThrowAway
47 | MsgUnknown
48 | MsgUserNotice
49 | MsgReSub
50 | MsgNotice
51 | MsgRoomState
52 | MsgSubsOn
53 | MsgSubsOff
54 | MsgSlowOn
55 | MsgSlowOff
56 | MsgR9kOn
57 | MsgR9kOff
58 | MsgHostOn
59 | MsgHostOff
60 | MsgTimeoutSuccess
61 | MsgReconnect
62 | )
63 |
--------------------------------------------------------------------------------
/pkg/common/common_test.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | // Not sure what we can test with this really
4 |
--------------------------------------------------------------------------------
/pkg/common/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestLoadConfig(t *testing.T) {
8 | /*
9 | var configTests = []struct {
10 | inputPath string
11 | expectedC *Config
12 | expectedE bool
13 | }{
14 | {
15 | inputPath: "../../resources/testfiles/config1.json",
16 | expectedC: &Config{
17 | BrokerHost: helper.NewStringPtr("localhost:7353"),
18 | BrokerPass: helper.NewStringPtr("test"),
19 | RedisHost: "localhost:6379",
20 | SQLDSN: "pajbot2:password@tcp(localhost:3306)/pajbot2_test",
21 | RedisPassword: "",
22 | RedisDatabase: -1,
23 | TLSKey: "",
24 | TLSCert: "",
25 | ToWeb: (chan map[string]interface{})(nil),
26 | FromWeb: (chan map[string]interface{})(nil)},
27 | expectedE: false,
28 | },
29 | {
30 | inputPath: "../../resources/testfiles/nonexistingconfigfile.json",
31 | expectedC: nil,
32 | expectedE: true,
33 | },
34 | {
35 | inputPath: "../../resources/testfiles/config2_invalidjson.json",
36 | expectedC: nil,
37 | expectedE: true,
38 | },
39 | }
40 |
41 | for _, tt := range configTests {
42 | res, err := LoadConfig(tt.inputPath)
43 |
44 | if tt.expectedE {
45 | assert.NotNil(t, err)
46 | } else {
47 | assert.Nil(t, err)
48 | }
49 |
50 | assert.Equal(t, tt.expectedC, res)
51 | }
52 | */
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/common/dbuser.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | // DBUser xD
4 | type DBUser struct {
5 | // ID in the database
6 | ID int
7 |
8 | // Name of the user, i.e. snusbot
9 | Name string
10 |
11 | // Type of the user
12 | Type string
13 |
14 | TwitchCredentials TwitchClientCredentials
15 | }
16 |
17 | const userQ = "SELECT id, name, twitch_access_token, twitch_refresh_token FROM pb_user"
18 |
--------------------------------------------------------------------------------
/pkg/common/emoji.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "github.com/urakozz/go-emoji"
5 | )
6 |
7 | var parser = emoji.NewEmojiParser()
8 |
9 | // ParseEmojis prases emojis from the message Text
10 | /*
11 | func ParseEmojis(msg *Msg) {
12 | emoteCount := make(map[string]*Emote)
13 | _ = parser.ReplaceAllStringFunc(msg.Text, func(s string) string {
14 | byteArray := []byte(s)
15 | if emote, ok := emoteCount[s]; ok {
16 | emote.Count++
17 | } else {
18 | emoteCount[s] = &Emote{
19 | ID: fmt.Sprintf("%x", bytes.Runes(byteArray)[0]),
20 | Type: "emoji",
21 | SizeX: 28,
22 | SizeY: 28,
23 | IsGif: false,
24 | Count: 1,
25 | }
26 | }
27 | return ""
28 | })
29 |
30 | for _, emote := range emoteCount {
31 | msg.Emotes = append(msg.Emotes, *emote)
32 | }
33 | }
34 | */
35 |
--------------------------------------------------------------------------------
/pkg/common/emote.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import "time"
4 |
5 | // Emote xD
6 | type Emote struct {
7 | Name string `json:"name"`
8 | ID string `json:"id"`
9 | // Possible types: bttv, twitch, ffz
10 | Type string `json:"type"`
11 |
12 | // Size in pixels
13 | SizeX int `json:"size_x"`
14 | SizeY int `json:"size_y"`
15 |
16 | IsGif bool `json:"is_gif"`
17 | Count int `json:"count"`
18 |
19 | MaxScale int `json:"max_scale"`
20 | }
21 |
22 | // ExtensionEmotes is an object which contains emotes that are shared between all channels
23 | type ExtensionEmotes struct {
24 | // Global BTTV Emotes
25 | Bttv map[string]Emote
26 | BttvLastUpdate time.Time
27 |
28 | // Global FrankerFaceZ Emotes
29 | FrankerFaceZ map[string]Emote
30 | FrankerFaceZLastUpdate time.Time
31 | }
32 |
33 | // EmoteByName implements sort.Interface by emote name
34 | type EmoteByName []Emote
35 |
36 | // Len implements sort.Interface
37 | func (a EmoteByName) Len() int {
38 | return len(a)
39 | }
40 |
41 | // Swap implements sort.Interface
42 | func (a EmoteByName) Swap(i, j int) {
43 | a[i], a[j] = a[j], a[i]
44 | }
45 |
46 | // Less implements sort.Interface
47 | func (a EmoteByName) Less(i, j int) bool {
48 | return a[i].Name < a[j].Name
49 | }
50 |
51 | func (e Emote) GetID() string {
52 | return e.ID
53 | }
54 |
55 | func (e Emote) GetName() string {
56 | return e.Name
57 | }
58 |
59 | func (e Emote) GetType() string {
60 | return e.Type
61 | }
62 |
63 | func (e Emote) GetCount() int {
64 | return e.Count
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/common/sql.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import "github.com/go-sql-driver/mysql"
4 |
5 | func IsDuplicateKey(err error) bool {
6 | me, ok := err.(*mysql.MySQLError)
7 | if !ok {
8 | return false
9 | }
10 |
11 | return me.Number == 1062
12 | }
13 |
--------------------------------------------------------------------------------
/pkg/common/twitchcredentials.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | // TwitchClientCredentials xD
4 | type TwitchClientCredentials struct {
5 | AccessToken string
6 |
7 | RefreshToken string
8 | }
9 |
--------------------------------------------------------------------------------
/pkg/const.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | const VerboseBenchmark = false
4 | const VerboseMessages = false
5 |
--------------------------------------------------------------------------------
/pkg/emote.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | type Emote interface {
4 | // i.e. "8vn23893nvcakuj23" for a bttv emote, or "85489481" for a twitch emote
5 | GetID() string
6 |
7 | // i.e. "NaM" or "forsenE"
8 | GetName() string
9 |
10 | // "twitch" or "bttv"
11 | GetType() string
12 |
13 | GetCount() int
14 | }
15 |
16 | type EmoteReader interface {
17 | Next() bool
18 | Get() Emote
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/emotes.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
--------------------------------------------------------------------------------
/pkg/eventemitter/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | @go test -v
3 |
4 | cover:
5 | @go test -coverprofile=coverage.out -covermode=count
6 | @go tool cover -html=coverage.out -o coverage.html
7 |
--------------------------------------------------------------------------------
/pkg/eventemitter/eventemitter.go:
--------------------------------------------------------------------------------
1 | package eventemitter
2 |
3 | import (
4 | "errors"
5 | "sync"
6 | )
7 |
8 | // type EventEmitter interface {
9 | // Listen(event string, cb interface{}, priority int) (*Listener, error)
10 | // Emit(event string, arguments map[string]interface{})
11 | // }
12 |
13 | var (
14 | ErrBadCallback = errors.New("bad callback passed through to Listen")
15 | )
16 |
17 | type Listener struct {
18 | Disconnected bool
19 |
20 | cb interface{}
21 | priority int
22 | }
23 |
24 | type EventEmitter struct {
25 | listenersMutex sync.Mutex
26 | listeners map[string][]*Listener
27 | }
28 |
29 | func New() *EventEmitter {
30 | return &EventEmitter{
31 | listenersMutex: sync.Mutex{},
32 | listeners: make(map[string][]*Listener),
33 | }
34 | }
35 |
36 | func (e *EventEmitter) Listen(event string, cb interface{}, priority int) (*Listener, error) {
37 | switch cb.(type) {
38 | case func(map[string]interface{}) error:
39 | case func() error:
40 | default:
41 | return nil, ErrBadCallback
42 | }
43 | e.listenersMutex.Lock()
44 | defer e.listenersMutex.Unlock()
45 |
46 | l := &Listener{
47 | cb: cb,
48 | priority: priority,
49 | }
50 |
51 | // TODO: sort by priority
52 | e.listeners[event] = append(e.listeners[event], l)
53 |
54 | return l, nil
55 | }
56 |
57 | func (e *EventEmitter) Emit(event string, arguments map[string]interface{}) (n int, err error) {
58 | e.listenersMutex.Lock()
59 | defer e.listenersMutex.Unlock()
60 |
61 | listeners, ok := e.listeners[event]
62 | if !ok {
63 | return
64 | }
65 |
66 | for _, listener := range listeners {
67 | if listener.Disconnected {
68 | // TODO: Remove from listeners
69 | continue
70 | }
71 |
72 | switch cb := listener.cb.(type) {
73 | case func(map[string]interface{}) error:
74 | err = cb(arguments)
75 | if err != nil {
76 | return
77 | }
78 | n++
79 | case func() error:
80 | err = cb()
81 | if err != nil {
82 | return
83 | }
84 | n++
85 | }
86 | }
87 |
88 | return
89 | }
90 |
--------------------------------------------------------------------------------
/pkg/eventemitter/helpers_test.go:
--------------------------------------------------------------------------------
1 | package eventemitter
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func assertStringsEqual(t *testing.T, expected, actual string) {
8 | if expected != actual {
9 | t.Errorf("failed asserting that \"%s\" is expected \"%s\"", actual, expected)
10 | }
11 | }
12 |
13 | func assertIntsEqual(t *testing.T, expected, actual int) {
14 | if expected != actual {
15 | t.Errorf("failed asserting that \"%d\" is expected \"%d\"", actual, expected)
16 | }
17 | }
18 |
19 | func assertInt32sEqual(t *testing.T, expected, actual int32) {
20 | if expected != actual {
21 | t.Errorf("failed asserting that \"%d\" is expected \"%d\"", actual, expected)
22 | }
23 | }
24 |
25 | func assertTrue(t *testing.T, actual bool, errorMessage string) {
26 | if !actual {
27 | t.Error(errorMessage)
28 | }
29 | }
30 |
31 | func assertFalse(t *testing.T, actual bool, errorMessage string) {
32 | if actual {
33 | t.Error(errorMessage)
34 | }
35 | }
36 |
37 | func assertStringSlicesEqual(t *testing.T, expected, actual []string) {
38 | if actual == nil {
39 | t.Errorf("actual slice was nil")
40 | return
41 | }
42 |
43 | if len(actual) != len(expected) {
44 | t.Errorf("actual slice was not the same length as expected slice")
45 | }
46 |
47 | for i, v := range actual {
48 | if v != expected[i] {
49 | t.Errorf("actual slice value \"%s\" was not equal to expected value \"%s\" at index \"%d\"", v, expected[i], i)
50 | }
51 | }
52 | }
53 |
54 | func assertErrorsEqual(t *testing.T, expected, actual error) {
55 | if expected != actual {
56 | t.Errorf("failed asserting that error \"%s\" is expected \"%s\"", actual, expected)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/events.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | type BaseEvent struct {
4 | UserStore UserStore
5 | }
6 |
7 | type MessageEvent struct {
8 | BaseEvent
9 |
10 | User User
11 | Message Message
12 | Channel ChannelWithStream
13 | }
14 |
15 | type EventSubNotificationEvent struct {
16 | BaseEvent
17 |
18 | Notification TwitchEventSubNotification
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/eventsub.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/nicklaw5/helix/v2"
7 | )
8 |
9 | type TwitchEventSubNotification struct {
10 | Subscription helix.EventSubSubscription `json:"subscription"`
11 | Challenge string `json:"challenge"`
12 | Event json.RawMessage `json:"event"`
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/message.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | type Message interface {
4 | GetText() string
5 | SetText(string)
6 |
7 | GetTwitchReader() EmoteReader
8 |
9 | GetBTTVReader() EmoteReader
10 | AddBTTVEmote(Emote)
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/mimo.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | // MIMO is a Many In Many Out interface
4 | // Implementation for this exists in pkg/mimo/
5 | type MIMO interface {
6 | Subscriber(channelNames ...string) chan interface{}
7 | Publisher(channelName string) chan interface{}
8 | }
9 |
--------------------------------------------------------------------------------
/pkg/mimo/mimo.go:
--------------------------------------------------------------------------------
1 | package mimo
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | type MIMO struct {
9 | subscribersMutex sync.RWMutex
10 | subscribers map[string][]chan interface{}
11 | }
12 |
13 | func New() *MIMO {
14 | return &MIMO{
15 | subscribers: make(map[string][]chan interface{}),
16 | }
17 | }
18 |
19 | func (m *MIMO) Subscriber(channelNames ...string) (out chan interface{}) {
20 | out = make(chan interface{}, 1)
21 | m.subscribersMutex.Lock()
22 | for _, channelName := range channelNames {
23 | m.subscribers[channelName] = append(m.subscribers[channelName], out)
24 | }
25 | m.subscribersMutex.Unlock()
26 | return
27 | }
28 |
29 | func (m *MIMO) Publisher(channelName string) (in chan interface{}) {
30 | in = make(chan interface{})
31 |
32 | go func() {
33 | for msg := range in {
34 | m.subscribersMutex.RLock()
35 | subscribers, ok := m.subscribers[channelName]
36 | m.subscribersMutex.RUnlock()
37 |
38 | if ok {
39 | for _, subscriber := range subscribers {
40 | select {
41 | case subscriber <- msg:
42 | default:
43 | fmt.Println("ERROR SENDING DATA TO SUBSCRIBE. MARK FOR DELETION")
44 | }
45 | }
46 | }
47 | }
48 | }()
49 |
50 | return in
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/module.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | type ModuleFactory func() ModuleSpec
4 |
5 | // A BaseModule is local to a bots channel
6 | // i.e. bot "pajbot" joins channels "pajlada" and "forsen"
7 | // Module list looks like this:
8 | // "pajbot":
9 | // - "pajlada":
10 | // - "MyTestModule"
11 | // - "MyTestModule2"
12 | // - "forsen":
13 | // - "MyTestModule"
14 | type BaseModule interface {
15 | LoadSettings([]byte) error
16 | Parameters() map[string]ModuleParameter
17 | ID() string
18 | Type() ModuleType
19 | Priority() int
20 | }
21 |
22 | type Module interface {
23 | BaseModule
24 |
25 | // Called when the module is disabled. The module can do any cleanup it needs to do here
26 | Disable() error
27 |
28 | // Returns the bot channel that the module has saved
29 | BotChannel() BotChannel
30 |
31 | OnWhisper(event MessageEvent) Actions
32 | OnMessage(event MessageEvent) Actions
33 | OnEventSubNotification(event EventSubNotificationEvent) Actions
34 | }
35 |
36 | type ModuleType uint
37 |
38 | // The order of these values matter. Higher value means higher priority in the "OnModules" function
39 | const (
40 | ModuleTypeUnsorted ModuleType = iota
41 | ModuleTypeFilter
42 | )
43 |
44 | type ModuleSpec interface {
45 | ID() string
46 | Name() string
47 | Type() ModuleType
48 | EnabledByDefault() bool
49 | Parameters() map[string]ModuleParameterSpec
50 |
51 | Create(bot BotChannel) Module
52 |
53 | Priority() int
54 | }
55 |
56 | type ModuleParameterSpec func() ModuleParameter
57 |
58 | type ModuleParameter interface {
59 | Description() string
60 | DefaultValue() interface{}
61 | Parse(string) error
62 | SetInterface(interface{})
63 | Get() interface{}
64 | Link(interface{})
65 | HasValue() bool
66 | HasBeenSet() bool
67 | }
68 |
--------------------------------------------------------------------------------
/pkg/modules/bad_character_filter/m.go:
--------------------------------------------------------------------------------
1 | package bad_character_filter
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | "github.com/pajbot/pajbot2/pkg/modules"
8 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
9 | "github.com/pajbot/pajbot2/pkg/twitchactions"
10 | )
11 |
12 | func init() {
13 | modules.Register("bad_character_filter", func() pkg.ModuleSpec {
14 | return modules.NewSpec("bad_character_filter", "Bad character filter", false, newBadCharacterFilter)
15 | })
16 | }
17 |
18 | type badCharacterFilter struct {
19 | mbase.Base
20 |
21 | badCharacters []rune
22 | }
23 |
24 | func newBadCharacterFilter(b *mbase.Base) pkg.Module {
25 | return &badCharacterFilter{
26 | Base: *b,
27 |
28 | badCharacters: []rune{'\x01'},
29 | }
30 | }
31 |
32 | func (m *badCharacterFilter) OnMessage(event pkg.MessageEvent) pkg.Actions {
33 | message := event.Message
34 |
35 | for _, r := range message.GetText() {
36 | for _, badCharacter := range m.badCharacters {
37 | if r == badCharacter {
38 | actions := &twitchactions.Actions{}
39 | actions.Timeout(event.User, 300*time.Second).SetReason("Your message contains a banned character")
40 | return actions
41 | }
42 | }
43 | }
44 |
45 | return nil
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/modules/banned_names/m.go:
--------------------------------------------------------------------------------
1 | package banned_names
2 |
3 | import (
4 | "regexp"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | "github.com/pajbot/pajbot2/pkg/modules"
8 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
9 | "github.com/pajbot/pajbot2/pkg/twitchactions"
10 | )
11 |
12 | func init() {
13 | modules.Register("banned_names", func() pkg.ModuleSpec {
14 | // TODO: Make configurable
15 | badUsernames := []*regexp.Regexp{
16 | regexp.MustCompile(`tos_is_trash\d+`),
17 | regexp.MustCompile(`trash_is_the_tos\d+`),
18 | regexp.MustCompile(`terms_of_service_uncool\d+`),
19 | regexp.MustCompile(`tos_i_love_mods_no_toxic\d+`),
20 | regexp.MustCompile(`^kemper.+`),
21 | regexp.MustCompile(`^pudele\d+`),
22 | regexp.MustCompile(`^ninjal0ver\d+`),
23 | regexp.MustCompile(`^trihard_account_\d+`),
24 | regexp.MustCompile(`^h[il1]erot[il1]tan.+`),
25 | }
26 |
27 | return modules.NewSpec("banned_names", "Banned names", false, func(b *mbase.Base) pkg.Module {
28 | return newBannedNames(b, badUsernames)
29 | })
30 | })
31 | }
32 |
33 | type bannedNames struct {
34 | mbase.Base
35 |
36 | badUsernames []*regexp.Regexp
37 | }
38 |
39 | func newBannedNames(b *mbase.Base, badUsernames []*regexp.Regexp) pkg.Module {
40 | return &bannedNames{
41 | Base: *b,
42 |
43 | badUsernames: badUsernames,
44 | }
45 | }
46 |
47 | func (m bannedNames) OnMessage(event pkg.MessageEvent) pkg.Actions {
48 | user := event.User
49 |
50 | usernameBytes := []byte(user.GetName())
51 | for _, badUsername := range m.badUsernames {
52 | if badUsername.Match(usernameBytes) {
53 | actions := &twitchactions.Actions{}
54 | actions.Ban(user).SetReason("Ban evasion")
55 | return actions
56 | }
57 | }
58 |
59 | return nil
60 | }
61 |
--------------------------------------------------------------------------------
/pkg/modules/basic_commands.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/pkg"
5 | "github.com/pajbot/pajbot2/pkg/commands"
6 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
7 | )
8 |
9 | func init() {
10 | Register("basic_commands", func() pkg.ModuleSpec {
11 | return &Spec{
12 | id: "basic_commands",
13 | name: "Basic commands",
14 | maker: newBasicCommandsModule,
15 | enabledByDefault: true,
16 | }
17 | })
18 | }
19 |
20 | type basicCommandsModule struct {
21 | mbase.Base
22 |
23 | commands pkg.CommandsManager
24 | }
25 |
26 | func newBasicCommandsModule(b *mbase.Base) pkg.Module {
27 | m := &basicCommandsModule{
28 | Base: *b,
29 |
30 | commands: commands.NewCommands(),
31 | }
32 |
33 | // FIXME
34 | m.Initialize()
35 |
36 | return m
37 | }
38 |
39 | func (m *basicCommandsModule) Initialize() {
40 | m.commands.Register([]string{"!pb2ping"}, commands.NewPing())
41 | m.commands.Register([]string{"!pb2join"}, commands.NewJoin(m.BotChannel()))
42 | m.commands.Register([]string{"!pb2leave"}, commands.NewLeave(m.BotChannel()))
43 | m.commands.Register([]string{"!pb2module"}, commands.NewModule(m.BotChannel()))
44 | m.commands.Register([]string{"!pb2quit"}, commands.NewQuit(m.BotChannel()))
45 |
46 | m.commands.Register([]string{"!user"}, commands.NewUser(m.BotChannel()))
47 | }
48 |
49 | func (m *basicCommandsModule) OnMessage(event pkg.MessageEvent) pkg.Actions {
50 | return m.commands.OnMessage(event)
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/modules/bttv_emote_parser.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "strings"
5 | "unicode"
6 |
7 | "github.com/pajbot/pajbot2/pkg"
8 | "github.com/pajbot/pajbot2/pkg/common"
9 | "github.com/pajbot/pajbot2/pkg/emotes"
10 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
11 | )
12 |
13 | func init() {
14 | Register("bttv_emote_parser", func() pkg.ModuleSpec {
15 | return &Spec{
16 | id: "bttv_emote_parser",
17 | name: "BTTV emote parser",
18 |
19 | enabledByDefault: true,
20 |
21 | priority: -50000,
22 |
23 | maker: newbttvEmoteParser,
24 | }
25 | })
26 | }
27 |
28 | type bttvEmoteParser struct {
29 | mbase.Base
30 |
31 | globalEmotes *map[string]common.Emote
32 | }
33 |
34 | func newbttvEmoteParser(b *mbase.Base) pkg.Module {
35 | return &bttvEmoteParser{
36 | Base: *b,
37 |
38 | globalEmotes: &emotes.GlobalEmotes.Bttv,
39 | }
40 | }
41 |
42 | func (m *bttvEmoteParser) OnMessage(event pkg.MessageEvent) pkg.Actions {
43 | message := event.Message
44 |
45 | parts := strings.FieldsFunc(message.GetText(), func(r rune) bool {
46 | // TODO(pajlada): This needs better testing
47 | return r > 0xFF || unicode.IsSpace(r) || r == '!' || r == '.' || r == '$' || r == '^' || r == '#' || r == '*' || r == '@' || r == ')' || r == '%' || r == '&' || r > 0x7a || r < 0x30 || (r > 0x39 && r < 0x41) || (r > 0x5a && r < 0x61)
48 | })
49 | emoteCount := make(map[string]*common.Emote)
50 | for _, word := range parts {
51 | if emote, ok := emoteCount[word]; ok {
52 | emote.Count++
53 | } else if emote, ok := (*m.globalEmotes)[word]; ok {
54 | emoteCount[word] = &emote
55 | }
56 | }
57 |
58 | for _, emote := range emoteCount {
59 | message.AddBTTVEmote(emote)
60 | }
61 |
62 | return nil
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/modules/commands/add.go:
--------------------------------------------------------------------------------
1 | package mcommands
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | "github.com/pajbot/pajbot2/pkg/twitchactions"
8 | )
9 |
10 | type addCmd struct {
11 | m *CommandsModule
12 | }
13 |
14 | func newAddCmd(m *CommandsModule) *addCmd {
15 | return &addCmd{
16 | m: m,
17 | }
18 | }
19 |
20 | func (c *addCmd) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
21 | if !event.User.IsModerator() {
22 | return twitchactions.Mention(event.User, "no permission")
23 | }
24 |
25 | parts = parts[1:]
26 |
27 | if len(parts) < 2 {
28 | return twitchactions.Mention(event.User, "bad usage")
29 | }
30 |
31 | trigger := parts[0]
32 | response := strings.Join(parts[1:], " ")
33 |
34 | err := c.m.addToDB(trigger, response)
35 | if err != nil {
36 | return twitchactions.Mentionf(event.User, "add error: %s", err)
37 | }
38 |
39 | return twitchactions.Mentionf(event.User, "added command: %s", trigger)
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/modules/commands/add_trigger.go:
--------------------------------------------------------------------------------
1 | package mcommands
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | "github.com/pajbot/pajbot2/pkg/twitchactions"
8 | )
9 |
10 | type cmdAddTrigger struct {
11 | m *CommandsModule
12 | }
13 |
14 | func newCmdAddTrigger(m *CommandsModule) *cmdAddTrigger {
15 | return &cmdAddTrigger{
16 | m: m,
17 | }
18 | }
19 |
20 | func (c *cmdAddTrigger) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
21 | if !event.User.IsModerator() {
22 | return twitchactions.Mention(event.User, "no permission")
23 | }
24 |
25 | parts = parts[1:]
26 |
27 | if len(parts) < 2 {
28 | return twitchactions.Mention(event.User, "bad usage")
29 | }
30 |
31 | commandID, err := strconv.ParseInt(parts[0], 10, 64)
32 | if err != nil {
33 | return twitchactions.Mentionf(event.User, "add trigger error: %s", err)
34 | }
35 | newTrigger := parts[1]
36 |
37 | err = c.m.addTrigger(commandID, newTrigger)
38 | if err != nil {
39 | return twitchactions.Mentionf(event.User, "add error: %s", err)
40 | }
41 |
42 | return twitchactions.Mentionf(event.User, "added command trigger: %s", newTrigger)
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/modules/commands/remove_trigger.go:
--------------------------------------------------------------------------------
1 | package mcommands
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | "github.com/pajbot/pajbot2/pkg/twitchactions"
8 | )
9 |
10 | type cmdRemoveTrigger struct {
11 | m *CommandsModule
12 | }
13 |
14 | func newCmdRemoveTrigger(m *CommandsModule) *cmdRemoveTrigger {
15 | return &cmdRemoveTrigger{
16 | m: m,
17 | }
18 | }
19 |
20 | func (c *cmdRemoveTrigger) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
21 | if !event.User.IsModerator() {
22 | return twitchactions.Mention(event.User, "no permission")
23 | }
24 |
25 | parts = parts[1:]
26 |
27 | if len(parts) < 2 {
28 | return twitchactions.Mention(event.User, "bad usage")
29 | }
30 |
31 | commandID, err := strconv.ParseInt(parts[0], 10, 64)
32 | if err != nil {
33 | return twitchactions.Mentionf(event.User, "remove trigger error: %s", err)
34 | }
35 | newTrigger := parts[1]
36 |
37 | err = c.m.removeTrigger(commandID, newTrigger)
38 | if err != nil {
39 | return twitchactions.Mentionf(event.User, "remove error: %s", err)
40 | }
41 |
42 | return twitchactions.Mentionf(event.User, "remove command trigger: %s", newTrigger)
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/modules/commands/text.go:
--------------------------------------------------------------------------------
1 | package mcommands
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/pkg"
5 | "github.com/pajbot/pajbot2/pkg/twitchactions"
6 | )
7 |
8 | type textResponseCmd struct {
9 | response string
10 | }
11 |
12 | func newTextResponseCommand(response string) *textResponseCmd {
13 | return &textResponseCmd{
14 | response: response,
15 | }
16 | }
17 |
18 | func (c *textResponseCmd) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
19 | return twitchactions.Say(c.response)
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/modules/custom_commands.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/pkg"
5 | "github.com/pajbot/pajbot2/pkg/commands"
6 | )
7 |
8 | type CustomCommands struct {
9 | botChannel pkg.BotChannel
10 |
11 | server *server
12 |
13 | commands pkg.CommandsManager
14 | }
15 |
16 | func NewCustomCommands() *CustomCommands {
17 | return &CustomCommands{
18 | server: &_server,
19 |
20 | commands: commands.NewCommands(),
21 | }
22 | }
23 |
24 | // func (m *CustomCommands) RegisterCommand(aliases []string, command pkg.CustomCommand) {
25 | // for _, alias := range aliases {
26 | // m.commands[alias] = command
27 | // }
28 | // }
29 |
30 | // FIXME
31 | // func (m *CustomCommands) OnWhisper(bot pkg.Sender, source pkg.User, message pkg.Message) error {
32 | // return nil
33 | // }
34 | //
35 | // func (m *CustomCommands) OnMessage(bot pkg.Sender, source pkg.Channel, user pkg.User, message pkg.Message, action pkg.Action) error {
36 | // parts := strings.Split(message.GetText(), " ")
37 | // if len(parts) == 0 {
38 | // return nil
39 | // }
40 | //
41 | // if command, ok := m.commands[strings.ToLower(parts[0])]; ok {
42 | // command.Trigger(m.botChannel, parts, source, user, message, action)
43 | // }
44 | //
45 | // return nil
46 | // }
47 |
--------------------------------------------------------------------------------
/pkg/modules/datastructures/transparentlist.go:
--------------------------------------------------------------------------------
1 | package datastructures
2 |
3 | import (
4 | "errors"
5 |
6 | goahocorasick "github.com/anknown/ahocorasick"
7 | )
8 |
9 | type transparentListRange struct {
10 | Pos int
11 | SkipLength int
12 | }
13 |
14 | type TransparentListSkipRange struct {
15 | skips []transparentListRange
16 | }
17 |
18 | func (t *TransparentListSkipRange) addSkip(r transparentListRange) {
19 | t.skips = append(t.skips, r)
20 | }
21 |
22 | func (t *TransparentListSkipRange) ShouldSkip(index int) int {
23 | if len(t.skips) == 0 {
24 | return 0
25 | }
26 |
27 | skip := t.skips[0]
28 |
29 | if skip.Pos == index {
30 | ret := skip.SkipLength
31 |
32 | t.skips = t.skips[1:]
33 |
34 | return ret
35 | }
36 |
37 | return 0
38 | }
39 |
40 | type TransparentList struct {
41 | m *goahocorasick.Machine
42 |
43 | dict [][]rune
44 | }
45 |
46 | func NewTransparentList() *TransparentList {
47 | t := &TransparentList{}
48 |
49 | t.m = new(goahocorasick.Machine)
50 |
51 | return t
52 | }
53 |
54 | func (t *TransparentList) Add(s string) {
55 | t.dict = append(t.dict, []rune(s))
56 | }
57 |
58 | func (t *TransparentList) Build() error {
59 | if t.m == nil {
60 | return errors.New("transparent list not initialized properly")
61 | }
62 |
63 | return t.m.Build(t.dict)
64 | }
65 |
66 | func (t *TransparentList) Find(text []rune) (ret TransparentListSkipRange) {
67 | terms := t.m.MultiPatternSearch(text, false)
68 |
69 | for _, t := range terms {
70 | ret.addSkip(transparentListRange{
71 | Pos: t.Pos,
72 | SkipLength: len(t.Word),
73 | })
74 | }
75 |
76 | return
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/modules/giveaway/giveaway_config.go:
--------------------------------------------------------------------------------
1 | package giveaway
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | "github.com/pajbot/pajbot2/pkg/twitchactions"
8 | )
9 |
10 | type giveawayCmdConfig struct {
11 | m *giveaway
12 | }
13 |
14 | var (
15 | giveawayValidKeys = map[string]string{
16 | "emoteid": "EmoteID",
17 | "emotename": "EmoteName",
18 | }
19 | )
20 |
21 | func (c *giveawayCmdConfig) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
22 | if !event.User.IsModerator() {
23 | return nil
24 | }
25 |
26 | if len(parts) <= 1 {
27 | return nil
28 | }
29 |
30 | key := strings.ToLower(parts[1])
31 | var ok bool
32 | key, ok = giveawayValidKeys[key]
33 | if !ok {
34 | return twitchactions.Mentionf(event.User, "%s is not a valid giveaway parameter key", key)
35 | }
36 |
37 | if len(parts) >= 3 {
38 | value := parts[2]
39 | return c.m.SetParameterResponse(key, value, event)
40 | }
41 |
42 | switch key {
43 | case "EmoteID":
44 | return twitchactions.Mentionf(event.User, "emote ID is %s", c.m.emoteID)
45 | case "EmoteName":
46 | return twitchactions.Mentionf(event.User, "emote name is %s", c.m.emoteName)
47 | }
48 |
49 | return twitchactions.Mentionf(event.User, "error: unhandled key '%s' in giveaway_config.go", key)
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/modules/giveaway/giveaway_draw.go:
--------------------------------------------------------------------------------
1 | package giveaway
2 |
3 | import (
4 | "math/rand"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | "github.com/pajbot/pajbot2/pkg/twitchactions"
8 | )
9 |
10 | type giveawayCmdDraw struct {
11 | m *giveaway
12 | }
13 |
14 | func (c *giveawayCmdDraw) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
15 | if !event.User.IsModerator() {
16 | // User does not have permission to draw a winner
17 | return nil
18 | }
19 |
20 | if len(c.m.entrants) == 0 {
21 | return twitchactions.Say("No one has joined the giveaway")
22 | }
23 |
24 | winnerIndex := rand.Intn(len(c.m.entrants))
25 | winnerUsername := c.m.entrants[winnerIndex]
26 |
27 | c.m.entrants = append(c.m.entrants[:winnerIndex], c.m.entrants[winnerIndex+1:]...)
28 |
29 | return twitchactions.Say(winnerUsername + " just won the sub emote giveaway PogChamp")
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/modules/giveaway/giveaway_start.go:
--------------------------------------------------------------------------------
1 | package giveaway
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/pkg"
5 | "github.com/pajbot/pajbot2/pkg/twitchactions"
6 | )
7 |
8 | type giveawayCmdStart struct {
9 | m *giveaway
10 | }
11 |
12 | func (c *giveawayCmdStart) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
13 | if !event.User.IsModerator() {
14 | // User does not have permission to start a giveaway
15 | return nil
16 | }
17 |
18 | if c.m.emoteID == "" {
19 | return twitchactions.Mention(event.User, "No emote ID set. Use '!25config emoteid 98374583' to configure this module")
20 | }
21 |
22 | if c.m.emoteName == "" {
23 | return twitchactions.Mention(event.User, "No emote name set. Use '!25config emotename NaM' to configure this module")
24 | }
25 |
26 | if c.m.stopped() {
27 | c.m.start()
28 | return twitchactions.Sayf("Started giveaway, type %s to join the giveaway", c.m.emoteName)
29 | }
30 |
31 | return twitchactions.Say("Giveaway already started")
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/modules/giveaway/giveaway_stop.go:
--------------------------------------------------------------------------------
1 | package giveaway
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/pkg"
5 | "github.com/pajbot/pajbot2/pkg/twitchactions"
6 | )
7 |
8 | type giveawayCmdStop struct {
9 | m *giveaway
10 | }
11 |
12 | func (c *giveawayCmdStop) Trigger(parts []string, event pkg.MessageEvent) pkg.Actions {
13 | if !event.User.IsModerator() {
14 | // User does not have permission to stop a giveaway
15 | return nil
16 | }
17 |
18 | if c.m.started() {
19 | c.m.stop()
20 | return twitchactions.Say("Stopped accepting people into the giveaway")
21 | }
22 |
23 | return twitchactions.Say("Giveaway already stopped")
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/modules/goodbye.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
8 | )
9 |
10 | func init() {
11 | Register("goodbye", func() pkg.ModuleSpec {
12 | return &Spec{
13 | id: "goodbye",
14 | name: "Goodbye",
15 | maker: newGoodbye,
16 |
17 | enabledByDefault: false,
18 | }
19 | })
20 | }
21 |
22 | type goodbye struct {
23 | mbase.Base
24 | }
25 |
26 | func newGoodbye(b *mbase.Base) pkg.Module {
27 | m := &goodbye{
28 | Base: *b,
29 | }
30 |
31 | err := m.Listen("on_quit", func() error {
32 | go m.BotChannel().Say("cya lol")
33 | return nil
34 | }, 100)
35 | if err != nil {
36 | log.Println("Error listening to on_quit event:", err)
37 | // FIXME
38 | // return err
39 | }
40 |
41 | return m
42 | }
43 |
--------------------------------------------------------------------------------
/pkg/modules/link_filter/link_filter_test.go:
--------------------------------------------------------------------------------
1 | package link_filter
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/pajbot/pajbot2/pkg/modules"
7 | )
8 |
9 | func TestLinkFilterUnmatches(t *testing.T) {
10 | spec, ok := modules.GetModuleSpec("link_filter")
11 | if !ok {
12 | t.Fatal("AAAAAA")
13 | }
14 |
15 | m := spec.Create(nil).(*LinkFilter)
16 |
17 | // these strings should not match as links
18 | tests := []string{
19 | "test.Bb5",
20 | }
21 |
22 | for _, link := range tests {
23 | triggered := m.checkMessage(link)
24 | if triggered {
25 | t.Errorf("%s is seen as a link while it should not be", link)
26 | }
27 | }
28 | }
29 |
30 | func TestLinkFilterMatches(t *testing.T) {
31 | spec, ok := modules.GetModuleSpec("link_filter")
32 | if !ok {
33 | t.Fatal("AAAAAA")
34 | }
35 |
36 | m := spec.Create(nil).(*LinkFilter)
37 |
38 | // these strings should match as links
39 | tests := []string{
40 | "google.com",
41 | "mylovely.horse",
42 | "clips.twitch.tv",
43 | "google.nl",
44 | "google.co.uk",
45 | "google.fi",
46 | "twitch.tv",
47 | "pajlada.se",
48 | "test.bb",
49 | "https://google.com",
50 | "https://twitter.com",
51 | }
52 |
53 | for _, link := range tests {
54 | triggered := m.checkMessage(link)
55 | if !triggered {
56 | t.Errorf("%s is not seen as a link while it should be", link)
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/pkg/modules/link_filter/m.go:
--------------------------------------------------------------------------------
1 | package link_filter
2 |
3 | import (
4 | "regexp"
5 | "time"
6 |
7 | "github.com/pajbot/pajbot2/pkg"
8 | "github.com/pajbot/pajbot2/pkg/modules"
9 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
10 | "github.com/pajbot/pajbot2/pkg/twitchactions"
11 | xurls "mvdan.cc/xurls/v2"
12 | )
13 |
14 | func init() {
15 | modules.Register("link_filter", func() pkg.ModuleSpec {
16 | const regexpModifier = `(\b|$)`
17 | relaxedRegexpStr := xurls.Relaxed().String()
18 | strictRegexpStr := xurls.Strict().String()
19 |
20 | relaxedRegexp := regexp.MustCompile(relaxedRegexpStr + regexpModifier)
21 | relaxedRegexp.Longest()
22 | strictRegexp := regexp.MustCompile(strictRegexpStr + regexpModifier)
23 | strictRegexp.Longest()
24 |
25 | return modules.NewSpec("link_filter", "Link filter", false, func(b *mbase.Base) pkg.Module {
26 | return newLinkFilter(b, relaxedRegexp, strictRegexp)
27 | })
28 | })
29 | }
30 |
31 | type LinkFilter struct {
32 | mbase.Base
33 |
34 | relaxedRegexp *regexp.Regexp
35 | strictRegexp *regexp.Regexp
36 | }
37 |
38 | func newLinkFilter(b *mbase.Base, relaxedRegexp, strictRegexp *regexp.Regexp) pkg.Module {
39 | return &LinkFilter{
40 | Base: *b,
41 |
42 | relaxedRegexp: relaxedRegexp,
43 | strictRegexp: strictRegexp,
44 | }
45 | }
46 |
47 | func (m *LinkFilter) checkMessage(text string) bool {
48 | links := m.relaxedRegexp.FindAllString(text, -1)
49 | return len(links) > 0
50 | }
51 |
52 | func (m LinkFilter) OnMessage(event pkg.MessageEvent) pkg.Actions {
53 | if event.User.IsModerator() {
54 | return nil
55 | }
56 |
57 | if event.User.IsVIP() {
58 | return nil
59 | }
60 |
61 | if m.checkMessage(event.Message.GetText()) {
62 | actions := &twitchactions.Actions{}
63 | actions.Timeout(event.User, 180*time.Second).SetReason("No links allowed")
64 | return actions
65 | }
66 |
67 | return nil
68 | }
69 |
--------------------------------------------------------------------------------
/pkg/modules/message_length_limit.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "time"
5 | "unicode/utf8"
6 |
7 | "github.com/pajbot/pajbot2/pkg"
8 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
9 | "github.com/pajbot/pajbot2/pkg/twitchactions"
10 | )
11 |
12 | func init() {
13 | Register("message_length_limit", func() pkg.ModuleSpec {
14 | return &Spec{
15 | id: "message_length_limit",
16 | name: "Message length limit",
17 | maker: newMessageLengthLimit,
18 | }
19 | })
20 | }
21 |
22 | var _ pkg.Module = &MessageLengthLimit{}
23 |
24 | type MessageLengthLimit struct {
25 | mbase.Base
26 | }
27 |
28 | func newMessageLengthLimit(b *mbase.Base) pkg.Module {
29 | return &MessageLengthLimit{
30 | Base: *b,
31 | }
32 | }
33 |
34 | func (m MessageLengthLimit) OnMessage(event pkg.MessageEvent) pkg.Actions {
35 | message := event.Message
36 |
37 | messageLength := utf8.RuneCountInString(message.GetText())
38 | if messageLength > 140 {
39 | if messageLength > 420 {
40 | return twitchactions.DoTimeout(event.User, 600*time.Second, "Your message is way too long")
41 | }
42 |
43 | return twitchactions.DoTimeout(event.User, 300*time.Second, "Your message is too long, shorten it")
44 | }
45 |
46 | return nil
47 | }
48 |
--------------------------------------------------------------------------------
/pkg/modules/nuke/parse.go:
--------------------------------------------------------------------------------
1 | package nuke
2 |
3 | import (
4 | "regexp"
5 | "strings"
6 | "time"
7 | )
8 |
9 | type NukeParameterParser struct {
10 | }
11 |
12 | func (p *NukeParameterParser) ParseNukeParameters(parts []string) (*NukeParameters, error) {
13 | if len(parts) < 3 {
14 | return nil, ErrUsage
15 | }
16 |
17 | phrase := strings.Join(parts[1:len(parts)-2], " ")
18 | var regexPhrase *regexp.Regexp
19 | if strings.HasPrefix(phrase, "/") && strings.HasSuffix(phrase, "/") && len(phrase) >= 2 {
20 | // parse as regex
21 | asd := phrase[1 : len(phrase)-1]
22 | regex, err := regexp.Compile(asd)
23 | if err == nil {
24 | regexPhrase = regex
25 | }
26 | }
27 |
28 | scrollbackLength, err := time.ParseDuration(parts[len(parts)-2])
29 | if err != nil {
30 | return nil, ErrUsage
31 | }
32 | if scrollbackLength < 0 {
33 | return nil, ErrUsage
34 | }
35 | timeoutDuration, err := time.ParseDuration(parts[len(parts)-1])
36 | if err != nil {
37 | return nil, ErrUsage
38 | }
39 | if timeoutDuration < 0 {
40 | return nil, ErrUsage
41 | }
42 |
43 | return &NukeParameters{
44 | Phrase: phrase,
45 | RegexPhrase: regexPhrase,
46 | ScrollbackLength: scrollbackLength,
47 | TimeoutDuration: timeoutDuration,
48 | }, nil
49 | }
50 |
--------------------------------------------------------------------------------
/pkg/modules/other_commands.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/internal/commands/disconnect"
5 | "github.com/pajbot/pajbot2/internal/commands/getuserid"
6 | "github.com/pajbot/pajbot2/pkg"
7 | "github.com/pajbot/pajbot2/pkg/commands"
8 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
9 | )
10 |
11 | func init() {
12 | Register("other_commands", func() pkg.ModuleSpec {
13 | return &Spec{
14 | id: "other_commands",
15 | name: "Other commands",
16 | maker: newOtherCommandsModule,
17 | enabledByDefault: false,
18 | }
19 | })
20 | }
21 |
22 | type otherCommandsModule struct {
23 | mbase.Base
24 |
25 | commands pkg.CommandsManager
26 | }
27 |
28 | func newOtherCommandsModule(b *mbase.Base) pkg.Module {
29 | m := &otherCommandsModule{
30 | Base: *b,
31 |
32 | commands: commands.NewCommands(),
33 | }
34 |
35 | // FIXME
36 | m.Initialize()
37 |
38 | return m
39 | }
40 |
41 | func (m *otherCommandsModule) Initialize() {
42 | m.commands.Register([]string{"!userid"}, &getuserid.Command{})
43 | m.commands.Register([]string{"!username"}, &commands.GetUserName{})
44 | m.commands.Register([]string{"!pb2points"}, &commands.GetPoints{})
45 | m.commands.Register([]string{"!pb2roulette"}, &commands.Roulette{})
46 | m.commands.Register([]string{"!pb2givepoints"}, &commands.GivePoints{})
47 | m.commands.Register([]string{"!pb2disconnect"}, &disconnect.Command{Bot: m.BotChannel()})
48 | // m.commands.Register([]string{"!pb2addpoints"}, &commands.AddPoints{})
49 | // m.commands.Register([]string{"!pb2removepoints"}, &commands.RemovePoints{})
50 | m.commands.Register([]string{"!roffle", "!join"}, commands.NewRaffle())
51 | m.commands.Register([]string{"!pb2rank"}, &commands.Rank{})
52 | m.commands.Register([]string{"!pb2simplify"}, &commands.Simplify{})
53 | // m.commands.Register([]string{"!timemeout"}, &commands.TimeMeOut{})
54 | m.commands.Register([]string{"!pb2test"}, &commands.Test{})
55 | m.commands.Register([]string{"!pb2islive"}, commands.IsLive{})
56 | }
57 |
58 | func (m *otherCommandsModule) OnMessage(event pkg.MessageEvent) pkg.Actions {
59 | return m.commands.OnMessage(event)
60 | }
61 |
--------------------------------------------------------------------------------
/pkg/modules/pajbot1_commands.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "log"
5 | "strings"
6 |
7 | "github.com/pajbot/pajbot2/pkg"
8 | "github.com/pajbot/pajbot2/pkg/commands"
9 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
10 | )
11 |
12 | func init() {
13 | Register("pajbot1_commands", func() pkg.ModuleSpec {
14 | return &Spec{
15 | id: "pajbot1_commands",
16 | name: "pajbot1 commands",
17 | maker: newPajbot1Commands,
18 | }
19 | })
20 | }
21 |
22 | type Pajbot1Commands struct {
23 | mbase.Base
24 |
25 | commands []*commands.Pajbot1Command
26 | }
27 |
28 | func newPajbot1Commands(b *mbase.Base) pkg.Module {
29 | m := &Pajbot1Commands{
30 | Base: *b,
31 | }
32 |
33 | m.loadPajbot1Commands()
34 |
35 | return m
36 | }
37 |
38 | func (m *Pajbot1Commands) loadPajbot1Commands() error {
39 | const queryF = `SELECT level, action, command, delay_all, delay_user, enabled, cost, can_execute_with_whisper, sub_only, mod_only, tokens_cost FROM tb_command`
40 |
41 | session := m.OldSession
42 |
43 | rows, err := session.Query(queryF) // GOOD
44 | if err != nil {
45 | return err
46 | }
47 |
48 | defer rows.Close()
49 |
50 | for rows.Next() {
51 | var command commands.Pajbot1Command
52 | err = command.LoadScan(rows)
53 | if err != nil {
54 | return err
55 | }
56 |
57 | if !command.Enabled {
58 | continue
59 | }
60 |
61 | if command.PointCost > 0 || command.TokenCost > 0 {
62 | continue
63 | }
64 |
65 | m.commands = append(m.commands, &command)
66 | }
67 |
68 | return nil
69 | }
70 |
71 | func (m Pajbot1Commands) OnMessage(event pkg.MessageEvent) pkg.Actions {
72 | user := event.User
73 | message := event.Message
74 |
75 | parts := strings.Split(message.GetText(), " ")
76 | if len(parts) == 0 {
77 | return nil
78 | }
79 |
80 | for _, command := range m.commands {
81 | if command.IsTriggered(parts) {
82 | err := command.Trigger(m.BotChannel(), user, parts)
83 | if err != nil {
84 | return nil
85 | }
86 | log.Println("Triggered command!")
87 | log.Println(command.Action)
88 | }
89 | }
90 |
91 | return nil
92 | }
93 |
--------------------------------------------------------------------------------
/pkg/modules/param.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | var nullBuffer = []byte("null")
4 |
5 | type baseParameter struct {
6 | description string
7 |
8 | hasBeenSet bool
9 | }
10 |
11 | func (b baseParameter) HasBeenSet() bool {
12 | return b.hasBeenSet
13 | }
14 |
15 | func (b baseParameter) Description() string {
16 | return b.description
17 | }
18 |
19 | type ParameterSpec struct {
20 | Description string
21 | DefaultValue interface{}
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/modules/punisher/timeout.go:
--------------------------------------------------------------------------------
1 | package punisher
2 |
3 | import "time"
4 |
5 | type timeout struct {
6 | received time.Time
7 | end time.Time
8 |
9 | duration int
10 | }
11 |
12 | func (t *timeout) IsSmaller(o *timeout, leniency int) bool {
13 | if o.duration == t.duration {
14 | // We are equal, so we are not smaller XD
15 | return false
16 | }
17 |
18 | if o.duration == 0 {
19 | // Other timeout is a permaban, so we are smaller
20 | return true
21 | }
22 |
23 | return o.duration-t.duration > leniency
24 | }
25 |
26 | func (t *timeout) Seconds() float64 {
27 | return time.Until(t.end).Seconds()
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/modules/server.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "database/sql"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | "github.com/pajbot/pajbot2/pkg/common/config"
8 | "github.com/pajbot/pajbot2/pkg/report"
9 | )
10 |
11 | type server struct {
12 | sql *sql.DB
13 | oldSession *sql.DB
14 | pubSub pkg.PubSub
15 | reportHolder *report.Holder
16 | }
17 |
18 | var _server server
19 |
20 | func InitServer(app pkg.Application, pajbot1Config *config.Pajbot1Config, reportHolder *report.Holder) error {
21 | var err error
22 |
23 | _server.sql = app.SQL()
24 | _server.oldSession, err = sql.Open("mysql", pajbot1Config.SQL.DSN)
25 | if err != nil {
26 | return err
27 | }
28 | _server.pubSub = app.PubSub()
29 | _server.reportHolder = reportHolder
30 |
31 | return nil
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/modules/spec.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/pkg"
5 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
6 | )
7 |
8 | type moduleParameterSpec struct {
9 | description string
10 | parameterType string
11 | defaultValue interface{}
12 | }
13 |
14 | type moduleMaker func(b *mbase.Base) pkg.Module
15 |
16 | type Spec struct {
17 | // i.e. "report". This is used in external calls enabling or disabling the module
18 | // the ID is also what's used when storing settings in the database
19 | id string
20 |
21 | // i.e. "Report"
22 | name string
23 |
24 | moduleType pkg.ModuleType
25 |
26 | enabledByDefault bool
27 |
28 | priority int
29 |
30 | parameters map[string]pkg.ModuleParameterSpec
31 |
32 | maker moduleMaker
33 | }
34 |
35 | func NewSpec(id, name string, enabledByDefault bool, maker moduleMaker) *Spec {
36 | return &Spec{
37 | id: id,
38 | name: name,
39 | enabledByDefault: enabledByDefault,
40 | maker: maker,
41 | }
42 | }
43 |
44 | func (s *Spec) ID() string {
45 | return s.id
46 | }
47 |
48 | func (s *Spec) Name() string {
49 | return s.name
50 | }
51 |
52 | func (s *Spec) Type() pkg.ModuleType {
53 | return s.moduleType
54 | }
55 |
56 | func (s *Spec) SetType(moduleType pkg.ModuleType) {
57 | s.moduleType = moduleType
58 | }
59 |
60 | func (s *Spec) EnabledByDefault() bool {
61 | return s.enabledByDefault
62 | }
63 |
64 | func (s *Spec) Create(bot pkg.BotChannel) pkg.Module {
65 | b := mbase.New(s, bot, _server.sql, _server.oldSession, _server.pubSub, _server.reportHolder)
66 | m := s.maker(&b)
67 |
68 | return m
69 | }
70 |
71 | func (s *Spec) Priority() int {
72 | return s.priority
73 | }
74 |
75 | func (s *Spec) Parameters() map[string]pkg.ModuleParameterSpec {
76 | return s.parameters
77 | }
78 |
79 | func (s *Spec) SetParameters(parameters map[string]pkg.ModuleParameterSpec) {
80 | s.parameters = parameters
81 | }
82 |
83 | var _ pkg.ModuleSpec = &Spec{}
84 |
--------------------------------------------------------------------------------
/pkg/modules/system/m.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 |
8 | "github.com/nicklaw5/helix/v2"
9 | "github.com/pajbot/pajbot2/pkg"
10 | "github.com/pajbot/pajbot2/pkg/modules"
11 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
12 | "github.com/pajbot/pajbot2/pkg/twitchactions"
13 | )
14 |
15 | const id = "system"
16 | const name = "System"
17 |
18 | func init() {
19 | modules.Register(id, func() pkg.ModuleSpec {
20 | return modules.NewSpec(id, name, true, newModule)
21 | })
22 | }
23 |
24 | type module struct {
25 | mbase.Base
26 | }
27 |
28 | func newModule(b *mbase.Base) pkg.Module {
29 | m := &module{
30 | Base: *b,
31 | }
32 |
33 | return m
34 | }
35 |
36 | func (m *module) OnEventSubNotification(event pkg.EventSubNotificationEvent) pkg.Actions {
37 | switch event.Notification.Subscription.Type {
38 | case helix.EventSubTypeChannelFollow:
39 | var followEvent helix.EventSubChannelFollowEvent
40 | err := json.NewDecoder(bytes.NewReader(event.Notification.Event)).Decode(&followEvent)
41 | if err != nil {
42 | fmt.Println(err)
43 | return nil
44 | }
45 | return twitchactions.Sayf("%s just followed the stream xD", followEvent.UserLogin)
46 | default:
47 | fmt.Println("Got unhandled eventsub notification:", event)
48 | }
49 | return nil
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/modules/test_module.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/pkg"
5 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
6 | )
7 |
8 | func init() {
9 | Register("test", func() pkg.ModuleSpec {
10 | return &Spec{
11 | id: "test",
12 | name: "Test",
13 | enabledByDefault: false,
14 |
15 | maker: newTest,
16 | }
17 | })
18 | }
19 |
20 | type test struct {
21 | mbase.Base
22 | }
23 |
24 | func newTest(b *mbase.Base) pkg.Module {
25 | return &test{
26 | Base: *b,
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/modules/tusecommands/m.go:
--------------------------------------------------------------------------------
1 | package tusecommands
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/pajbot/pajbot2/pkg"
8 | "github.com/pajbot/pajbot2/pkg/modules"
9 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
10 | mcommands "github.com/pajbot/pajbot2/pkg/modules/commands"
11 | )
12 |
13 | const id = "tusecommands"
14 | const name = "Test module - Use commands module"
15 |
16 | func init() {
17 | modules.Register(id, func() pkg.ModuleSpec {
18 | return modules.NewSpec(id, name, false, newModule)
19 | })
20 | }
21 |
22 | type module struct {
23 | mbase.Base
24 | }
25 |
26 | func (m *module) registerCommand() error {
27 | iCommandsModule, err := m.BotChannel().GetModule("commands")
28 | if err != nil {
29 | return err
30 | }
31 |
32 | commandsModule, ok := iCommandsModule.(*mcommands.CommandsModule)
33 | if !ok {
34 | return errors.New("this module is not a commands module wtf")
35 | }
36 |
37 | fmt.Println("got commands module:", commandsModule)
38 |
39 | return nil
40 | }
41 |
42 | func newModule(b *mbase.Base) pkg.Module {
43 | m := &module{
44 | Base: *b,
45 | }
46 |
47 | return m
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/modules/welcome/m.go:
--------------------------------------------------------------------------------
1 | package welcome
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | "github.com/pajbot/pajbot2/pkg/modules"
8 | mbase "github.com/pajbot/pajbot2/pkg/modules/base"
9 | )
10 |
11 | func init() {
12 | modules.Register("welcome", func() pkg.ModuleSpec {
13 | return modules.NewSpec("welcome", "Welcome", false, newWelcome)
14 | })
15 | }
16 |
17 | type welcome struct {
18 | mbase.Base
19 | }
20 |
21 | func newWelcome(b *mbase.Base) pkg.Module {
22 | m := &welcome{
23 | Base: *b,
24 | }
25 |
26 | // FIXME
27 | m.Initialize()
28 |
29 | return m
30 | }
31 |
32 | func (m *welcome) Initialize() {
33 | err := m.Listen("on_join", func() error {
34 | go m.BotChannel().Say("pb2 joined")
35 | return nil
36 | }, 100)
37 | if err != nil {
38 | // FIXME
39 | log.Println("ERROR LISTENING TO ON JOIN XD")
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/pajbot/pajbot.go:
--------------------------------------------------------------------------------
1 | package pajbot
2 |
3 | // Bot is one instance of the pajbot bot.
4 | // One instance of the pajbot bot can run multiple bots and join multiple chats.
5 | type Bot struct {
6 | }
7 |
8 | func New() Bot {
9 | return Bot{}
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/permissions.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | type Permission uint64
4 |
5 | const (
6 | PermissionNone Permission = 0
7 | PermissionReport Permission = 1 << 0
8 | PermissionRaffle Permission = 1 << 1
9 | PermissionAdmin Permission = 1 << 2
10 | PermissionModeration Permission = 1 << 3
11 | PermissionReportAPI Permission = 1 << 4
12 |
13 | PermissionImmuneToMessageLimits = 1 << 5
14 | )
15 |
16 | // GetPermissionBit converts a string (i.e. "admin") to the binary value it represents.
17 | // 0b100 in this example
18 | func GetPermissionBit(s string) Permission {
19 | if s == "report" {
20 | return PermissionReport
21 | }
22 | if s == "raffle" {
23 | return PermissionRaffle
24 | }
25 | if s == "admin" {
26 | return PermissionAdmin
27 | }
28 | if s == "moderation" {
29 | return PermissionModeration
30 | }
31 | if s == "reportapi" {
32 | return PermissionReportAPI
33 | }
34 | if s == "immunetomessagelimits" {
35 | return PermissionImmuneToMessageLimits
36 | }
37 |
38 | return PermissionNone
39 | }
40 |
41 | // GetPermissionBits converts a list of strings (i.e. ["admin", "raffle"]) to the binary value they represent.
42 | // 0b110 in this example
43 | func GetPermissionBits(permissionNames []string) (permissions Permission) {
44 | for _, permissionName := range permissionNames {
45 | permission := GetPermissionBit(permissionName)
46 | permissions |= permission
47 | }
48 |
49 | return
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/pubsub.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | import "encoding/json"
4 |
5 | type PubSub interface {
6 | Subscribe(source PubSubSource, topic string, parameters json.RawMessage)
7 | Publish(source PubSubSource, topic string, data interface{})
8 |
9 | HandleSubscribe(connection PubSubSubscriptionHandler, topic string)
10 | HandleJSON(source PubSubSource, bytes []byte) error
11 | }
12 |
13 | // PubSubConnection is an interface where a JSON message can be written to
14 | type PubSubConnection interface {
15 | MessageReceived(source PubSubSource, topic string, bytes []byte) error
16 | }
17 |
18 | type PubSubSubscriptionHandler interface {
19 | ConnectionSubscribed(source PubSubSource, topic string, parameters json.RawMessage) (bool, error)
20 | }
21 |
22 | // PubSubSource is an interface that is responsible for a message being written into pubsub
23 | // This will be responsible for checking authorization
24 | type PubSubSource interface {
25 | IsApplication() bool
26 | Connection() PubSubConnection
27 | AuthenticatedUser() User
28 | }
29 |
30 | type PubSubBan struct {
31 | Channel string
32 | Target string
33 | Reason string
34 | }
35 |
36 | type PubSubTimeout struct {
37 | Channel string
38 | Target string
39 | Reason string
40 | Duration uint32
41 | }
42 |
43 | type PubSubUntimeout struct {
44 | Channel string
45 | Target string
46 | }
47 |
48 | type PubSubUser struct {
49 | ID string
50 | Name string
51 | }
52 |
53 | type PubSubBanEvent struct {
54 | Channel PubSubUser
55 | Target PubSubUser
56 | Source PubSubUser
57 | Reason string
58 | }
59 |
60 | type PubSubTimeoutEvent struct {
61 | Channel PubSubUser
62 | Target PubSubUser
63 | Source PubSubUser
64 | Duration int
65 | Reason string
66 | }
67 |
--------------------------------------------------------------------------------
/pkg/pubsub/listener.go:
--------------------------------------------------------------------------------
1 | package pubsub
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | "github.com/pajbot/pajbot2/pkg"
8 | )
9 |
10 | type Listener struct {
11 | connection pkg.PubSubConnection
12 | subscriptionType SubscriptionType
13 | }
14 |
15 | func (l *Listener) Publish(source pkg.PubSubSource, topic string, data interface{}) error {
16 | bytes, err := json.Marshal(data)
17 | if err != nil {
18 | fmt.Printf("Unable to unmarshal %#v\n", data)
19 | // we don't treat this as an actual error
20 | return nil
21 | }
22 |
23 | err = l.connection.MessageReceived(source, topic, bytes)
24 | if err != nil {
25 | return err
26 | }
27 |
28 | return nil
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/reportaction.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | const (
4 | ReportActionUnknown = 0
5 | ReportActionBan = 1
6 | ReportActionTimeout = 2
7 | ReportActionDismiss = 3
8 | ReportActionUndo = 4
9 | )
10 |
11 | func GetReportActionByName(actionName string) uint8 {
12 | switch actionName {
13 | case "ban":
14 | return ReportActionBan
15 | case "timeout":
16 | return ReportActionTimeout
17 | case "dismiss":
18 | return ReportActionDismiss
19 | case "undo":
20 | return ReportActionUndo
21 | default:
22 | return ReportActionUnknown
23 | }
24 | }
25 |
26 | func GetReportActionName(action uint8) string {
27 | switch action {
28 | case ReportActionBan:
29 | return "ban"
30 | case ReportActionTimeout:
31 | return "timeout"
32 | case ReportActionDismiss:
33 | return "dismiss"
34 | case ReportActionUndo:
35 | return "undo"
36 | default:
37 | return "unknown"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/pkg/stream.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/nicklaw5/helix/v2"
7 | )
8 |
9 | type StreamStatus interface {
10 | Live() bool
11 | StartedAt() time.Time
12 | }
13 |
14 | type Stream interface {
15 | Status() StreamStatus
16 |
17 | // Update forwards the given helix data to its internal status
18 | Update(*helix.Stream)
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/streamstore.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | type StreamStore interface {
4 | // GetStream ensures that the given Account is being followed & polled for stream status updates
5 | // If the account is already being followed, the Stream that was already stored is returned.
6 | GetStream(Account) Stream
7 | }
8 |
--------------------------------------------------------------------------------
/pkg/twitch/account.go:
--------------------------------------------------------------------------------
1 | package twitch
2 |
3 | type SimpleAccount struct {
4 | id string
5 | name string
6 | }
7 |
8 | func (a *SimpleAccount) ID() string {
9 | return a.id
10 | }
11 |
12 | func (a *SimpleAccount) Name() string {
13 | return a.name
14 | }
15 |
16 | type TwitchAccount struct {
17 | UserID string
18 | UserName string
19 | }
20 |
21 | func (a *TwitchAccount) ID() string {
22 | return a.UserID
23 | }
24 |
25 | func (a *TwitchAccount) Name() string {
26 | return a.UserName
27 | }
28 |
--------------------------------------------------------------------------------
/pkg/twitch/action.go:
--------------------------------------------------------------------------------
1 | package twitch
2 |
3 | type Action struct {
4 | }
5 |
--------------------------------------------------------------------------------
/pkg/twitch/user.go:
--------------------------------------------------------------------------------
1 | package twitch
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | )
8 |
9 | func NewUser(id, name string) *User {
10 | return &User{
11 | id: id,
12 | name: name,
13 | }
14 | }
15 |
16 | type User struct {
17 | id string
18 | name string
19 | }
20 |
21 | func (u *User) ID() string {
22 | return u.id
23 | }
24 |
25 | func (u *User) Name() string {
26 | return u.name
27 | }
28 |
29 | func (u *User) GetID() string {
30 | return u.id
31 | }
32 |
33 | func (u *User) GetName() string {
34 | return u.name
35 | }
36 |
37 | func (u *User) SetName(v string) {
38 | u.name = v
39 | }
40 |
41 | func (u *User) fillIn(userStore pkg.UserStore) error {
42 | if u.id == "" && u.name != "" {
43 | // Has name but not ID
44 | u.id = userStore.GetID(u.name)
45 | if u.id == "" {
46 | return errors.New("unable to get ID")
47 | }
48 |
49 | return nil
50 | }
51 |
52 | if u.name == "" && u.id != "" {
53 | // Has ID but not name
54 | u.name = userStore.GetName(u.id)
55 | if u.name == "" {
56 | return errors.New("unable to get Name")
57 | }
58 |
59 | return nil
60 | }
61 |
62 | if u.name != "" && u.id != "" {
63 | // User already has name and ID, no need to fetch anything
64 | return nil
65 | }
66 |
67 | return errors.New("User missing both ID and Name")
68 | }
69 |
70 | func (u *User) Valid() bool {
71 | return u.name != "" && u.id != ""
72 | }
73 |
--------------------------------------------------------------------------------
/pkg/twitchaccount.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | type TwitchAccount interface {
4 | ID() string
5 | Name() string
6 | }
7 |
--------------------------------------------------------------------------------
/pkg/twitchactions/ban.go:
--------------------------------------------------------------------------------
1 | package twitchactions
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | )
8 |
9 | var _ pkg.MuteAction = &Ban{}
10 |
11 | type Ban struct {
12 | mute
13 |
14 | duration time.Duration
15 | }
16 |
17 | func (b Ban) Type() pkg.MuteType {
18 | return pkg.MuteTypePermanent
19 | }
20 |
21 | func (b Ban) Duration() time.Duration {
22 | return time.Duration(0)
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/twitchactions/delete.go:
--------------------------------------------------------------------------------
1 | package twitchactions
2 |
3 | type deleteAction struct {
4 | message string
5 | }
6 |
7 | func (m *deleteAction) Message() string {
8 | return m.message
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/twitchactions/message.go:
--------------------------------------------------------------------------------
1 | package twitchactions
2 |
3 | import "github.com/pajbot/pajbot2/pkg"
4 |
5 | var _ pkg.MessageAction = &Message{}
6 |
7 | type Message struct {
8 | content string
9 |
10 | action bool
11 | }
12 |
13 | func (m *Message) SetAction(v bool) {
14 | m.action = v
15 | }
16 |
17 | func (m Message) Evaluate() string {
18 | if m.action {
19 | return ".me " + m.content
20 | }
21 |
22 | return m.content
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/twitchactions/mute.go:
--------------------------------------------------------------------------------
1 | package twitchactions
2 |
3 | import "github.com/pajbot/pajbot2/pkg"
4 |
5 | type mute struct {
6 | user pkg.User
7 | reason string
8 | }
9 |
10 | func (m *mute) User() pkg.User {
11 | return m.user
12 | }
13 |
14 | func (m *mute) SetReason(reason string) {
15 | m.reason = reason
16 | }
17 |
18 | func (m mute) Reason() string {
19 | return m.reason
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/twitchactions/timeout.go:
--------------------------------------------------------------------------------
1 | package twitchactions
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | )
8 |
9 | var _ pkg.MuteAction = &Timeout{}
10 |
11 | type Timeout struct {
12 | mute
13 |
14 | duration time.Duration
15 | }
16 |
17 | func (t Timeout) Type() pkg.MuteType {
18 | return pkg.MuteTypeTemporary
19 | }
20 |
21 | func (t Timeout) Duration() time.Duration {
22 | return t.duration
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/twitchactions/twitchactions.go:
--------------------------------------------------------------------------------
1 | package twitchactions
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/pajbot/pajbot2/pkg"
8 | )
9 |
10 | var _ pkg.Actions = &Actions{}
11 |
12 | // Actions lists
13 | type Actions struct {
14 | baseActions
15 | }
16 |
17 | /// HELPER FUNCTIONS FOR SINGLE ACTIONS
18 |
19 | func Mention(user pkg.User, content string) *Actions {
20 | actions := &Actions{}
21 | actions.Mention(user, content)
22 | return actions
23 | }
24 |
25 | func Mentionf(user pkg.User, format string, a ...interface{}) *Actions {
26 | actions := &Actions{}
27 | actions.Mention(user, fmt.Sprintf(format, a...))
28 | return actions
29 | }
30 |
31 | func Say(content string) *Actions {
32 | actions := &Actions{}
33 | actions.Say(content)
34 | return actions
35 | }
36 |
37 | func Sayf(format string, a ...interface{}) *Actions {
38 | actions := &Actions{}
39 | actions.Say(fmt.Sprintf(format, a...))
40 | return actions
41 | }
42 |
43 | func DoWhisper(user pkg.User, content string) *Actions {
44 | actions := &Actions{}
45 | actions.Whisper(user, content)
46 | return actions
47 | }
48 |
49 | func DoWhisperf(user pkg.User, format string, a ...interface{}) *Actions {
50 | actions := &Actions{}
51 | actions.Whisper(user, fmt.Sprintf(format, a...))
52 | return actions
53 | }
54 |
55 | func DoTimeout(user pkg.User, duration time.Duration, reason string) *Actions {
56 | actions := &Actions{}
57 | actions.Timeout(user, duration).SetReason(reason)
58 | return actions
59 | }
60 |
--------------------------------------------------------------------------------
/pkg/twitchactions/unmute.go:
--------------------------------------------------------------------------------
1 | package twitchactions
2 |
3 | import "github.com/pajbot/pajbot2/pkg"
4 |
5 | type unmute struct {
6 | user pkg.User
7 | muteType pkg.MuteType
8 | }
9 |
10 | func (m *unmute) User() pkg.User {
11 | return m.user
12 | }
13 |
14 | func (m *unmute) Type() pkg.MuteType {
15 | return m.muteType
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/twitchactions/whisper.go:
--------------------------------------------------------------------------------
1 | package twitchactions
2 |
3 | import "github.com/pajbot/pajbot2/pkg"
4 |
5 | type Whisper struct {
6 | user pkg.User
7 | content string
8 | }
9 |
10 | func (w Whisper) User() pkg.User {
11 | return w.user
12 | }
13 |
14 | func (w Whisper) Content() string {
15 | return w.content
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/user.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | type User interface {
4 | // Has channel or global permission
5 | HasPermission(Channel, Permission) bool
6 |
7 | // Has global permission
8 | HasGlobalPermission(Permission) bool
9 |
10 | // Has channel permission
11 | HasChannelPermission(Channel, Permission) bool
12 |
13 | GetName() string
14 | GetDisplayName() string
15 | GetID() string
16 | IsModerator() bool
17 | IsBroadcaster() bool
18 | IsVIP() bool
19 | IsSubscriber() bool
20 | GetBadges() map[string]int
21 |
22 | // Set the ID of this user
23 | // Will return an error if the ID of this user was already set
24 | SetID(string) error
25 |
26 | // Set the Name of this user
27 | // Will return an error if the Name of this user was already set
28 | SetName(string) error
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/usercontext.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | type UserContext interface {
4 | GetContext(channelID, userID string) []string
5 |
6 | // The message must be preformatted. i.e. [2018-09-13 502:501:201] username: message
7 | AddContext(channelID, userID, message string)
8 | }
9 |
--------------------------------------------------------------------------------
/pkg/users/server.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import (
4 | "database/sql"
5 | )
6 |
7 | type server struct {
8 | sql *sql.DB
9 | }
10 |
11 | var _server server
12 |
13 | func InitServer(_sql *sql.DB) {
14 | _server.sql = _sql
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/userstore.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/pajbot/utils"
7 | )
8 |
9 | type UserStoreRequest struct {
10 | ids map[string]bool
11 | names map[string]bool
12 | }
13 |
14 | func NewUserStoreRequest() *UserStoreRequest {
15 | return &UserStoreRequest{
16 | ids: make(map[string]bool),
17 | names: make(map[string]bool),
18 | }
19 | }
20 |
21 | func (r *UserStoreRequest) AddID(id string) {
22 | r.ids[id] = true
23 | }
24 |
25 | func (r *UserStoreRequest) AddName(name string) {
26 | r.names[name] = true
27 | }
28 |
29 | // Execute returns two values:
30 | // First value: map where key is a user ID pointing at a user name
31 | // Second value: map where key is a user name pointing at a user ID
32 | func (r *UserStoreRequest) Execute(userStore UserStore) (names map[string]string, ids map[string]string) {
33 | var wg sync.WaitGroup
34 | if len(r.ids) > 0 {
35 | wg.Add(1)
36 | go func() {
37 | defer wg.Done()
38 | names = userStore.GetNames(utils.SBKey(r.ids))
39 | }()
40 | }
41 |
42 | if len(r.names) > 0 {
43 | wg.Add(1)
44 | go func() {
45 | defer wg.Done()
46 | ids = userStore.GetIDs(utils.SBKey(r.names))
47 | }()
48 | }
49 |
50 | wg.Wait()
51 |
52 | return
53 | }
54 |
55 | type UserStore interface {
56 | // Input: Lowercased twitch usernames
57 | // Returns: user IDs as strings in no specific order, and a bool indicating whether the user needs to exhaust the list first and wait
58 | GetIDs([]string) map[string]string
59 |
60 | GetID(string) string
61 |
62 | GetUserByLogin(string) (User, error)
63 | GetUserByID(string) (User, error)
64 |
65 | GetName(string) string
66 |
67 | // Input: list of twitch IDs
68 | // Returns: map of twitch IDs pointing at twitch usernames
69 | GetNames([]string) map[string]string
70 |
71 | Hydrate([]User) error
72 | }
73 |
--------------------------------------------------------------------------------
/pkg/web/controller/admin/admin.go:
--------------------------------------------------------------------------------
1 | package admin
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "net/http"
7 |
8 | "github.com/pajbot/pajbot2/pkg"
9 | "github.com/pajbot/pajbot2/pkg/users"
10 | "github.com/pajbot/pajbot2/pkg/web/router"
11 | "github.com/pajbot/pajbot2/pkg/web/state"
12 | "github.com/pajbot/pajbot2/pkg/web/views"
13 | )
14 |
15 | func Load(a pkg.Application) {
16 | router.Get("/admin", handleRoot(a))
17 | }
18 |
19 | func handleRoot(a pkg.Application) func(w http.ResponseWriter, r *http.Request) {
20 | return func(w http.ResponseWriter, r *http.Request) {
21 | // Step 1: Ensure user is logged in
22 | c := state.Context(w, r)
23 | if c.Session == nil {
24 | views.Render403(w, r)
25 | return
26 | }
27 |
28 | // Step 2: Ensure logged in user has global admin permissions
29 | user := users.NewSimpleTwitchUser(c.Session.TwitchUserID, c.Session.TwitchUserName)
30 |
31 | if !user.HasGlobalPermission(pkg.PermissionAdmin) {
32 | views.Render403(w, r)
33 | return
34 | }
35 |
36 | type BotInfo struct {
37 | Name string
38 | Connected bool
39 | }
40 |
41 | type Extra struct {
42 | Bots []BotInfo
43 | }
44 |
45 | extra := &Extra{}
46 |
47 | for it := a.TwitchBots().Iterate(); it.Next(); {
48 | bot := it.Value()
49 | botInfo := BotInfo{
50 | Name: bot.TwitchAccount().Name(),
51 | Connected: bot.Connected(),
52 | }
53 | extra.Bots = append(extra.Bots, botInfo)
54 | }
55 |
56 | // TODO: Also fetch all channel admin permissions, and send that along to.
57 | // That would tell the API what channels it can administrate
58 |
59 | extraBytes, _ := json.Marshal(extra)
60 |
61 | err := views.RenderExtra("admin", w, r, extraBytes)
62 | if err != nil {
63 | log.Println("Error rendering admin view:", err)
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/README.md:
--------------------------------------------------------------------------------
1 | # pajbot2 api
2 | First-party API endpoints are registered under this module. These are API modules that are deemed vital for pajbot2.
3 | A short description of each endpoint can be found in each submodules `routes.go` file.
4 |
5 | ## I want to register my own API endpoints
6 | See instructions that I have not yet written but I would like to write.
7 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/api.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/pajbot/pajbot2/pkg"
8 | "github.com/pajbot/pajbot2/pkg/common/config"
9 | "github.com/pajbot/pajbot2/pkg/web/controller/api/auth"
10 | "github.com/pajbot/pajbot2/pkg/web/controller/api/channel"
11 | "github.com/pajbot/pajbot2/pkg/web/controller/api/report"
12 | "github.com/pajbot/pajbot2/pkg/web/controller/api/webhook"
13 | "github.com/pajbot/pajbot2/pkg/web/router"
14 | )
15 |
16 | func apiRoot(w http.ResponseWriter, r *http.Request) {
17 | fmt.Fprintf(w, "XD API ROOT")
18 | fmt.Fprintf(w, "\nTODO: Link to api docs")
19 | }
20 |
21 | func Load(a pkg.Application, cfg *config.Config) {
22 | m := router.Subrouter("/api")
23 |
24 | router.RGet(m, "/", apiRoot)
25 | m.HandleFunc("", apiRoot)
26 |
27 | auth.Load(m, a, cfg)
28 |
29 | channel.Load(m)
30 |
31 | report.Load(m)
32 |
33 | webhook.Load(m, cfg)
34 | }
35 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/auth/routes.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/gorilla/mux"
7 | "github.com/pajbot/pajbot2/pkg"
8 | "github.com/pajbot/pajbot2/pkg/common/config"
9 | "github.com/pajbot/pajbot2/pkg/web/controller/api/auth/twitch"
10 | )
11 |
12 | // Load routes for /api/auth/
13 | func Load(parent *mux.Router, a pkg.Application, cfg *config.Config) {
14 | m := parent.PathPrefix("/auth").Subrouter()
15 |
16 | // Subroute /api/auth/twitch/
17 | err := twitch.Load(m, a)
18 | if err != nil {
19 | fmt.Println("Error loading /api/auth/twitch:", err)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/auth/twitch/bot.go:
--------------------------------------------------------------------------------
1 | package twitch
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/nicklaw5/helix/v2"
7 | "github.com/pajbot/pajbot2/pkg/web/state"
8 | "github.com/pajbot/utils"
9 | "golang.org/x/oauth2"
10 | )
11 |
12 | func onBotAuthenticated(
13 | w http.ResponseWriter, r *http.Request,
14 | self *helix.ValidateTokenResponse, oauth2Token *oauth2.Token, stateData *stateData) {
15 | const queryF = `
16 | INSERT INTO bot (twitch_userid, twitch_username, twitch_access_token, twitch_refresh_token, twitch_access_token_expiry)
17 | VALUES ($1, $2, $3, $4, $5) ON CONFLICT (twitch_userid)
18 | DO
19 | UPDATE
20 | SET
21 | twitch_username = $2,
22 | twitch_access_token = $3,
23 | twitch_refresh_token = $4,
24 | twitch_access_token_expiry = $5`
25 | c := state.Context(w, r)
26 | _, err := c.SQL.Exec(queryF, self.Data.UserID, self.Data.Login, oauth2Token.AccessToken, oauth2Token.RefreshToken, oauth2Token.Expiry) // GOOD
27 | if err != nil {
28 | _ = utils.WebWriteError(w, 500, "Error inserting bot, admin should check console logs NaM")
29 | return
30 | }
31 |
32 | _, _ = w.Write([]byte("Bot added/updated! Restart the bot for the changes to take effect"))
33 | }
34 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/auth/twitch/routes.go:
--------------------------------------------------------------------------------
1 | package twitch
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/gorilla/mux"
7 | "github.com/pajbot/pajbot2/pkg"
8 | )
9 |
10 | // Load routes for /api/auth/twitch/
11 | func Load(parent *mux.Router, a pkg.Application) error {
12 | m := parent.PathPrefix("/twitch").Subrouter()
13 | auths := a.TwitchAuths()
14 | ctx := context.Background()
15 |
16 | // Initialize /api/auth/twitch/bot and /api/auth/twitch/bot/callback routes
17 | initializeOauthRoutes(ctx, m, auths.Bot(), "bot", onBotAuthenticated)
18 |
19 | // Initialize /api/auth/twitch/user and /api/auth/twitch/user/callback routes
20 | initializeOauthRoutes(ctx, m, auths.User(), "user", onUserAuthenticated)
21 |
22 | // Initialize /api/auth/twitch/streamer and /api/auth/twitch/streamer/callback routes
23 | initializeOauthRoutes(ctx, m, auths.Streamer(), "streamer", onStreamerAuthenticated)
24 |
25 | return nil
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/auth/twitch/streamer.go:
--------------------------------------------------------------------------------
1 | package twitch
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/nicklaw5/helix/v2"
7 | "golang.org/x/oauth2"
8 | )
9 |
10 | func onStreamerAuthenticated(
11 | w http.ResponseWriter, r *http.Request,
12 | self *helix.ValidateTokenResponse, oauth2Token *oauth2.Token, stateData *stateData) {
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/auth/twitch/user.go:
--------------------------------------------------------------------------------
1 | package twitch
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/nicklaw5/helix/v2"
8 | "github.com/pajbot/pajbot2/pkg/web/state"
9 | "golang.org/x/oauth2"
10 | )
11 |
12 | func onUserAuthenticated(
13 | w http.ResponseWriter, r *http.Request,
14 | self *helix.ValidateTokenResponse, oauth2Token *oauth2.Token, stateData *stateData) {
15 | c := state.Context(w, r)
16 | twitchUserName := self.Data.Login
17 |
18 | twitchUserID := c.TwitchUserStore.GetID(twitchUserName)
19 |
20 | if twitchUserID == "" {
21 | // TODO: Implement proper error handling
22 | fmt.Println("Unable to get user id for user", twitchUserName)
23 | return
24 | }
25 |
26 | const queryF = `
27 | INSERT INTO "user"
28 | (twitch_username, twitch_userid)
29 | VALUES ($1, $2)
30 | ON CONFLICT (twitch_userid) DO UPDATE SET twitch_username=$1
31 | RETURNING id
32 | `
33 |
34 | var lastInsertID int64
35 | row := c.SQL.QueryRow(queryF, twitchUserName, twitchUserID) // GOOD
36 | err := row.Scan(&lastInsertID)
37 | if err != nil {
38 | panic(err)
39 | }
40 |
41 | fmt.Println("Last insert ID:", lastInsertID)
42 |
43 | sessionID, err := c.CreateSession(lastInsertID)
44 | if err != nil {
45 | panic(err)
46 | }
47 | state.SetSessionCookies(w, sessionID, twitchUserName)
48 |
49 | // TODO: Secure the redirect
50 | if stateData.redirect != "" {
51 | http.Redirect(w, r, stateData.redirect, http.StatusFound)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/channel/banphrases/list.go:
--------------------------------------------------------------------------------
1 | package banphrases
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | "github.com/pajbot/pajbot2/pkg/web/state"
8 | "github.com/pajbot/pajbot2/pkg/webutils"
9 | "github.com/pajbot/utils"
10 | )
11 |
12 | type banphrase struct {
13 | ID string
14 | Enabled bool
15 | Description string
16 | Phrase string
17 | }
18 |
19 | type listResponse struct {
20 | Banphrases []banphrase
21 | ChannelID string
22 | }
23 |
24 | func handleList(w http.ResponseWriter, r *http.Request) {
25 | c := state.Context(w, r)
26 |
27 | if c.Channel == nil {
28 | utils.WebWriteError(w, 400, "Missing channel argument")
29 | return
30 | }
31 |
32 | if !webutils.RequirePermission(w, c, c.Channel, pkg.PermissionModeration) {
33 | return
34 | }
35 |
36 | var response listResponse
37 |
38 | response.ChannelID = c.Channel.GetID()
39 |
40 | const queryF = "SELECT id, enabled, description, phrase FROM banphrase"
41 |
42 | rows, err := c.SQL.Query(queryF) // GOOD
43 | if err != nil {
44 | panic(err)
45 | }
46 |
47 | defer rows.Close()
48 |
49 | for rows.Next() {
50 | var bp banphrase
51 | if err := rows.Scan(&bp.ID, &bp.Enabled, &bp.Description, &bp.Phrase); err != nil {
52 | panic(err)
53 | }
54 |
55 | response.Banphrases = append(response.Banphrases, bp)
56 | }
57 |
58 | utils.WebWrite(w, response)
59 | }
60 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/channel/banphrases/routes.go:
--------------------------------------------------------------------------------
1 | package banphrases
2 |
3 | import (
4 | "github.com/gorilla/mux"
5 | "github.com/pajbot/pajbot2/pkg/web/router"
6 | )
7 |
8 | // Load routes for /api/channel/:channel_id/banphrases/
9 | func Load(parent *mux.Router) {
10 | m := parent.PathPrefix("/banphrases").Subrouter()
11 |
12 | router.RGet(m, `/list`, handleList)
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/channel/moderation/routes.go:
--------------------------------------------------------------------------------
1 | package moderation
2 |
3 | import (
4 | "github.com/gorilla/mux"
5 | "github.com/pajbot/pajbot2/pkg/web/router"
6 | )
7 |
8 | // Load routes for /api/channel/:channel_id/moderation/
9 | func Load(parent *mux.Router) {
10 | m := parent.PathPrefix("/moderation").Subrouter()
11 |
12 | router.RGet(m, `/latest`, apiChannelModerationLatest)
13 |
14 | router.RGet(m, `/user`, apiUser).Queries("user_id", `{user_id:[0-9]+}`)
15 | router.RGet(m, `/user`, apiUser).Queries("user_name", `{user_name:\w+}`)
16 | router.RGet(m, `/user`, apiUserMissingVariables)
17 | router.RGet(m, `/check_message`, apiCheckMessage).Queries("message", `{message:.+}`)
18 | router.RGet(m, `/check_message`, apiCheckMessageMissingVariables)
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/channel/routes.go:
--------------------------------------------------------------------------------
1 | package channel
2 |
3 | import (
4 | "github.com/gorilla/mux"
5 | "github.com/pajbot/pajbot2/pkg/web/controller/api/channel/banphrases"
6 | "github.com/pajbot/pajbot2/pkg/web/controller/api/channel/moderation"
7 | )
8 |
9 | // Load routes for /api/channel/:channel_id
10 | func Load(parent *mux.Router) {
11 | m := parent.PathPrefix(`/channel/{channel_id:\w+}`).Subrouter()
12 |
13 | // Load subroutes for /api/channel/:channel_id/moderation/
14 | moderation.Load(m)
15 |
16 | // Load subroutes for /api/channel/:channel_id/banphrases/
17 | banphrases.Load(m)
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/report/history.go:
--------------------------------------------------------------------------------
1 | package report
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strings"
7 |
8 | "github.com/pajbot/pajbot2/pkg"
9 | "github.com/pajbot/pajbot2/pkg/report"
10 | "github.com/pajbot/pajbot2/pkg/users"
11 | "github.com/pajbot/pajbot2/pkg/web/state"
12 | "github.com/pajbot/utils"
13 | )
14 |
15 | func apiHistory(w http.ResponseWriter, r *http.Request) {
16 | const queryF = `
17 | SELECT
18 | id,
19 | channel_id, channel_name, channel_type,
20 | reporter_id, reporter_name,
21 | target_id, target_name,
22 | reason, logs,
23 | time,
24 | handler_id, handler_name,
25 | action, action_duration,
26 | time_handled
27 | FROM
28 | report_history
29 | ORDER BY time_handled DESC
30 | LIMIT 50;
31 | `
32 |
33 | c := state.Context(w, r)
34 |
35 | if c.Session == nil {
36 | utils.WebWriteError(w, 400, "Not authorized to view this endpoint")
37 | return
38 | }
39 |
40 | user := users.NewSimpleTwitchUser(c.Session.TwitchUserID, c.Session.TwitchUserName)
41 | if user == nil {
42 | utils.WebWriteError(w, 400, "Not authorized to view this endpoint")
43 | return
44 | }
45 |
46 | if !user.HasGlobalPermission(pkg.PermissionReportAPI) {
47 | utils.WebWriteError(w, 400, "Not authorized to view this endpoint!!!")
48 | return
49 | }
50 |
51 | rows, err := c.SQL.Query(queryF) // GOOD
52 | if err != nil {
53 | fmt.Println("error in mysql query apiUser:", err)
54 | utils.WebWriteError(w, 500, "Internal error")
55 | return
56 | }
57 |
58 | defer rows.Close()
59 |
60 | type xd struct {
61 | Reports []report.HistoricReport
62 | }
63 |
64 | var response xd
65 |
66 | for rows.Next() {
67 | var r report.HistoricReport
68 | var logsString string
69 | if err := rows.Scan(&r.ID, &r.Channel.ID, &r.Channel.Name, &r.Channel.Type, &r.Reporter.ID, &r.Reporter.Name, &r.Target.ID, &r.Target.Name, &r.Reason, &logsString, &r.Time, &r.Handler.ID, &r.Handler.Name, &r.Action, &r.ActionDuration, &r.TimeHandled); err != nil {
70 | fmt.Println("error when scanning row:", err)
71 | utils.WebWriteError(w, 500, "Internal error")
72 | return
73 | }
74 | r.Logs = strings.Split(logsString, "\n")
75 |
76 | response.Reports = append(response.Reports, r)
77 | }
78 |
79 | utils.WebWrite(w, response)
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/report/routes.go:
--------------------------------------------------------------------------------
1 | package report
2 |
3 | import (
4 | "github.com/gorilla/mux"
5 | "github.com/pajbot/pajbot2/pkg/web/router"
6 | )
7 |
8 | // Load routes for /api/report/
9 | func Load(parent *mux.Router) {
10 | m := parent.PathPrefix("/report").Subrouter()
11 |
12 | router.RGet(m, `/history`, apiHistory)
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/webhook/eventsub.go:
--------------------------------------------------------------------------------
1 | package webhook
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "log"
9 | "net/http"
10 |
11 | "github.com/nicklaw5/helix/v2"
12 | "github.com/pajbot/pajbot2/pkg"
13 | "github.com/pajbot/pajbot2/pkg/common/config"
14 | "github.com/pajbot/pajbot2/pkg/web/state"
15 | )
16 |
17 | func apiEventsub(cfg *config.TwitchWebhookConfig) func(w http.ResponseWriter, r *http.Request) {
18 | return func(w http.ResponseWriter, r *http.Request) {
19 | c := state.Context(w, r)
20 |
21 | body, err := io.ReadAll(r.Body)
22 | if err != nil {
23 | log.Println(err)
24 | return
25 | }
26 | defer r.Body.Close()
27 |
28 | if !helix.VerifyEventSubNotification(cfg.Secret, r.Header, string(body)) {
29 | log.Println("No valid signature in subscription message")
30 | return
31 | }
32 |
33 | var notification pkg.TwitchEventSubNotification
34 | err = json.NewDecoder(bytes.NewReader(body)).Decode(¬ification)
35 | if err != nil {
36 | fmt.Println(err)
37 | return
38 | }
39 |
40 | if notification.Challenge != "" {
41 | w.Write([]byte(notification.Challenge))
42 | return
43 | }
44 |
45 | var botChannel pkg.BotChannel
46 | var channelID string
47 |
48 | switch notification.Subscription.Type {
49 | case helix.EventSubTypeChannelFollow:
50 | fallthrough
51 | case helix.EventSubTypeStreamOnline:
52 | fallthrough
53 | case helix.EventSubTypeStreamOffline:
54 | channelID = notification.Subscription.Condition.BroadcasterUserID
55 | }
56 |
57 | if channelID != "" {
58 | for it := c.Application.TwitchBots().Iterate(); it.Next(); {
59 | bot := it.Value()
60 | if bot == nil {
61 | continue
62 | }
63 |
64 | botChannel = bot.GetBotChannelByID(channelID)
65 | if botChannel == nil {
66 | continue
67 | }
68 |
69 | break
70 | }
71 | }
72 |
73 | if botChannel == nil {
74 | fmt.Println("No bot channel active to handle this request", channelID, notification.Subscription.Type)
75 | fmt.Println(string(notification.Event))
76 | // No bot channel active to handle this request
77 | return
78 | }
79 |
80 | err = botChannel.HandleEventSubNotification(notification)
81 | if err != nil {
82 | fmt.Println("Error handling eventsub notification:", err)
83 | }
84 |
85 | w.WriteHeader(http.StatusOK)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/pkg/web/controller/api/webhook/webhook.go:
--------------------------------------------------------------------------------
1 | package webhook
2 |
3 | import (
4 | "github.com/gorilla/mux"
5 | "github.com/pajbot/pajbot2/pkg/common/config"
6 | "github.com/pajbot/pajbot2/pkg/web/router"
7 | )
8 |
9 | func Load(parent *mux.Router, cfg *config.Config) {
10 | m := parent.PathPrefix("/webhook").Subrouter()
11 |
12 | // NEW AND FRESH AND COOL
13 | router.RPost(m, `/eventsub`, apiEventsub(&cfg.Auth.Twitch.Webhook))
14 |
15 | router.RPost(m, `/github/{channelID:\w+}`, apiGithub(&cfg.Auth.Github.Webhook))
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/web/controller/banphrases/banphrases.go:
--------------------------------------------------------------------------------
1 | package banphrases
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "net/http"
7 |
8 | "github.com/pajbot/pajbot2/pkg/web/router"
9 | "github.com/pajbot/pajbot2/pkg/web/views"
10 | )
11 |
12 | func Load() {
13 | router.Get("/banphrases", handleBanphrases)
14 | }
15 |
16 | func handleBanphrases(w http.ResponseWriter, r *http.Request) {
17 | banphrases := []string{
18 | "a", "b", "c",
19 | }
20 | extra, _ := json.Marshal(banphrases)
21 |
22 | err := views.RenderExtra("banphrases", w, r, extra)
23 | if err != nil {
24 | log.Println("Error rendering banphrases view:", err)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/web/controller/channel/channel.go:
--------------------------------------------------------------------------------
1 | package channel
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "net/http"
7 |
8 | "github.com/gorilla/mux"
9 | "github.com/pajbot/pajbot2/pkg"
10 | "github.com/pajbot/pajbot2/pkg/common/config"
11 | "github.com/pajbot/pajbot2/pkg/web/router"
12 | "github.com/pajbot/pajbot2/pkg/web/views"
13 | )
14 |
15 | func Load(a pkg.Application, cfg *config.Config) {
16 | m := router.Subrouter("/c/{channel:[a-zA-Z0-9_]+}")
17 |
18 | router.RGet(m, "/dashboard", handleDashboard(a))
19 | }
20 |
21 | func handleDashboard(a pkg.Application) func(w http.ResponseWriter, r *http.Request) {
22 | type BotInfo struct {
23 | Name string
24 | Connected bool
25 | }
26 |
27 | type ChannelInfo struct {
28 | ID string
29 | Name string
30 | }
31 |
32 | type Extra struct {
33 | Bots []BotInfo
34 | Channel ChannelInfo
35 | }
36 |
37 | return func(w http.ResponseWriter, r *http.Request) {
38 | extra := &Extra{}
39 | vars := mux.Vars(r)
40 | channel, ok := vars["channel"]
41 | if !ok {
42 | return
43 | }
44 |
45 | for it := a.TwitchBots().Iterate(); it.Next(); {
46 | bot := it.Value()
47 |
48 | bc := bot.GetBotChannel(channel)
49 | if bc == nil {
50 | continue
51 | }
52 |
53 | extra.Channel.ID = bc.Channel().GetID()
54 | extra.Channel.Name = bc.Channel().GetName()
55 |
56 | // fmt.Fprintf(w, "Bot: %s\n", bot.TwitchAccount().ID())
57 | bi := BotInfo{
58 | Name: bot.TwitchAccount().Name(),
59 | Connected: bot.Connected(),
60 | }
61 | extra.Bots = append(extra.Bots, bi)
62 | }
63 |
64 | extraBytes, _ := json.Marshal(extra)
65 |
66 | err := views.RenderExtra("dashboard", w, r, extraBytes)
67 | if err != nil {
68 | log.Println("Error rendering dashboard view:", err)
69 | return
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/pkg/web/controller/commands.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net/http"
8 |
9 | "github.com/pajbot/pajbot2/pkg/commandlist"
10 | "github.com/pajbot/pajbot2/pkg/web/views"
11 | )
12 |
13 | func handleCommands(w http.ResponseWriter, r *http.Request) {
14 | commands := commandlist.Commands()
15 | bytes, err := json.Marshal(commands)
16 | if err != nil {
17 | fmt.Println("Error:", err)
18 | return
19 | }
20 | err = views.RenderExtra("commands", w, r, bytes)
21 | if err != nil {
22 | log.Println("Error rendering dashboard view:", err)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/web/controller/controller.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/pajbot/pajbot2/pkg"
5 | "github.com/pajbot/pajbot2/pkg/common/config"
6 | "github.com/pajbot/pajbot2/pkg/web/controller/admin"
7 | "github.com/pajbot/pajbot2/pkg/web/controller/api"
8 | "github.com/pajbot/pajbot2/pkg/web/controller/banphrases"
9 | "github.com/pajbot/pajbot2/pkg/web/controller/channel"
10 | "github.com/pajbot/pajbot2/pkg/web/controller/dashboard"
11 | "github.com/pajbot/pajbot2/pkg/web/controller/home"
12 | "github.com/pajbot/pajbot2/pkg/web/controller/logout"
13 | "github.com/pajbot/pajbot2/pkg/web/controller/static"
14 | "github.com/pajbot/pajbot2/pkg/web/controller/ws"
15 | "github.com/pajbot/pajbot2/pkg/web/router"
16 | )
17 |
18 | func LoadRoutes(a pkg.Application, cfg *config.Config) {
19 | channel.Load(a, cfg)
20 |
21 | dashboard.Load()
22 | home.Load()
23 | api.Load(a, cfg)
24 | static.Load()
25 | ws.Load()
26 |
27 | // /logout
28 | logout.Load()
29 |
30 | // /profile
31 | router.Get("/profile", handleProfile)
32 |
33 | // /banphrases
34 | banphrases.Load()
35 |
36 | // /admin
37 | admin.Load(a)
38 |
39 | // /commands
40 | router.Get("/commands", handleCommands)
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/web/controller/dashboard/dashboard.go:
--------------------------------------------------------------------------------
1 | package dashboard
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net/http"
8 |
9 | "github.com/pajbot/pajbot2/pkg"
10 | "github.com/pajbot/pajbot2/pkg/web/router"
11 | "github.com/pajbot/pajbot2/pkg/web/state"
12 | "github.com/pajbot/pajbot2/pkg/web/views"
13 | )
14 |
15 | func Load() {
16 | router.Get("/dashboard", Dashboard)
17 | }
18 |
19 | const dashboardPermissions = pkg.PermissionAdmin | pkg.PermissionReport | pkg.PermissionModeration | pkg.PermissionReportAPI
20 |
21 | func Dashboard(w http.ResponseWriter, r *http.Request) {
22 | c := state.Context(w, r)
23 |
24 | const queryF = `
25 | SELECT
26 | twitch_user_channel_permission.channel_id,
27 | permissions
28 | FROM
29 | "user"
30 | LEFT JOIN
31 | twitch_user_channel_permission ON "user".twitch_userid="twitch_user_channel_permission".twitch_user_id
32 | WHERE twitch_userid=$1`
33 |
34 | type ChannelInfo struct {
35 | ID string
36 | Name string
37 | }
38 |
39 | type Extra struct {
40 | Channels []ChannelInfo
41 | }
42 |
43 | var extraBytes []byte
44 |
45 | if c.Session != nil {
46 | rows, err := c.SQL.Query(queryF, c.Session.TwitchUserID)
47 | if err != nil {
48 | // TODO: render error page somehow?
49 | fmt.Println("ERROR 1:", err)
50 | return
51 | }
52 | defer rows.Close()
53 |
54 | extra := &Extra{}
55 |
56 | for rows.Next() {
57 | var twitchChannelID string
58 | var permission pkg.Permission
59 |
60 | err := rows.Scan(&twitchChannelID, &permission)
61 | if err != nil {
62 | // TODO: render error page somehow?
63 | fmt.Println("ERROR 2:", err)
64 | return
65 | }
66 |
67 | if (permission & dashboardPermissions) != 0 {
68 | extra.Channels = append(extra.Channels, ChannelInfo{
69 | ID: twitchChannelID,
70 | Name: c.TwitchUserStore.GetName(twitchChannelID),
71 | })
72 | }
73 | extraBytes, _ = json.Marshal(extra)
74 | }
75 | }
76 |
77 | err := views.RenderExtra("dashboard", w, r, extraBytes)
78 | if err != nil {
79 | log.Println("Error rendering dashboard view:", err)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/pkg/web/controller/home/home.go:
--------------------------------------------------------------------------------
1 | package home
2 |
3 | import (
4 | "log"
5 | "net/http"
6 |
7 | "github.com/pajbot/pajbot2/pkg/web/router"
8 | "github.com/pajbot/pajbot2/pkg/web/views"
9 | )
10 |
11 | func Load() {
12 | router.Get("/", Home)
13 | router.Get("/home", Home)
14 | }
15 |
16 | func Home(w http.ResponseWriter, r *http.Request) {
17 | err := views.Render("home", w, r)
18 | if err != nil {
19 | log.Println("Error rendering dashboard view:", err)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/web/controller/logout/logout.go:
--------------------------------------------------------------------------------
1 | package logout
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/pajbot/pajbot2/pkg/web/router"
8 | "github.com/pajbot/pajbot2/pkg/web/state"
9 | )
10 |
11 | func Load() {
12 | router.Get("/logout", handleLogout)
13 | }
14 |
15 | func handleLogout(w http.ResponseWriter, r *http.Request) {
16 | c := state.Context(w, r)
17 | if c.SessionID != nil {
18 | sessionID := *c.SessionID
19 |
20 | if state.IsValidSessionID(sessionID) {
21 | // Remove session from database
22 | const queryF = `DELETE FROM user_session WHERE id=$1`
23 |
24 | _, err := c.SQL.Exec(queryF, sessionID) // GOOD
25 | if err != nil {
26 | fmt.Println("Error deleting session ID")
27 | }
28 | }
29 |
30 | state.ClearSessionCookies(w)
31 | }
32 |
33 | // Redirect user
34 | redirectURL := r.FormValue("redirect")
35 |
36 | if redirectURL == "" {
37 | redirectURL = "/"
38 | }
39 |
40 | http.Redirect(w, r, redirectURL, http.StatusFound)
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/web/controller/profile.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "log"
5 | "net/http"
6 |
7 | "github.com/pajbot/pajbot2/pkg/web/views"
8 | )
9 |
10 | func handleProfile(w http.ResponseWriter, r *http.Request) {
11 | err := views.Render("profile", w, r)
12 | if err != nil {
13 | log.Println("Error rendering dashboard view:", err)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/web/controller/static/static.go:
--------------------------------------------------------------------------------
1 | package static
2 |
3 | import (
4 | "net/http"
5 | "path/filepath"
6 |
7 | "github.com/pajbot/pajbot2/internal/config"
8 | "github.com/pajbot/pajbot2/pkg/web/router"
9 | )
10 |
11 | func Load() {
12 | staticPath := filepath.Join(config.WebStaticPath, "static")
13 |
14 | // Serve files statically from ./web/static in /static
15 | router.PathPrefix("/static", http.StripPrefix("/static", http.FileServer(http.Dir(staticPath))))
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/web/controller/ws/hub.go:
--------------------------------------------------------------------------------
1 | package ws
2 |
3 | import "fmt"
4 |
5 | // ConnectionHub xD
6 | type ConnectionHub struct {
7 | connections map[*WSConn]bool
8 | broadcast chan *WSMessage
9 | unregister chan *WSConn
10 | register chan *WSConn
11 | }
12 |
13 | // Hub xD
14 | var Hub = ConnectionHub{
15 | connections: make(map[*WSConn]bool),
16 | broadcast: make(chan *WSMessage),
17 | unregister: make(chan *WSConn),
18 | register: make(chan *WSConn),
19 | }
20 |
21 | func (h *ConnectionHub) run() {
22 | for {
23 | select {
24 | case conn := <-h.register:
25 | fmt.Println("new connection")
26 | h.connections[conn] = true
27 | case conn := <-h.unregister:
28 | if _, ok := h.connections[conn]; ok {
29 | fmt.Println("disconnect")
30 | delete(h.connections, conn)
31 | conn.disconnect()
32 | }
33 | case wsMessage := <-h.broadcast:
34 | fmt.Println("broadcast")
35 | message := wsMessage.Payload.ToJSON()
36 | for conn := range h.connections {
37 | // Figure out if this connection should even be sent this message
38 | if wsMessage.LevelRequired > 0 && (conn.user == nil /* || conn.user.Level < wsMessage.LevelRequired*/) {
39 | // The user did not fulfill the message Level Requirement
40 | fmt.Printf("Not sending %#v to %#v\n", wsMessage, conn)
41 | continue
42 | }
43 |
44 | if wsMessage.MessageType != MessageTypeAll && conn.messageType != MessageTypeAll && wsMessage.MessageType != conn.messageType {
45 | // Invalid message type
46 | fmt.Printf("Not sending %#v to %#v cuz message types differ\n", wsMessage, conn)
47 | continue
48 | }
49 | select {
50 | case conn.send <- message:
51 | default:
52 | // Not sure what this is for
53 | close(conn.send)
54 | delete(h.connections, conn)
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
61 | // Broadcast some data to all connections
62 | func (h *ConnectionHub) Broadcast(data *WSMessage) {
63 | h.broadcast <- data
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/web/controller/ws/ws.go:
--------------------------------------------------------------------------------
1 | package ws
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/gorilla/mux"
8 | "github.com/gorilla/websocket"
9 | "github.com/pajbot/pajbot2/pkg/web/router"
10 | "github.com/pajbot/pajbot2/pkg/web/state"
11 | )
12 |
13 | var upgrader = websocket.Upgrader{
14 | ReadBufferSize: 1024,
15 | WriteBufferSize: 1024,
16 | CheckOrigin: func(r *http.Request) bool {
17 | return true
18 | },
19 | }
20 |
21 | func Load() {
22 | go Hub.run()
23 |
24 | m := router.Subrouter("/ws")
25 |
26 | router.RHandleFunc(m, `/{type}`, wsHandler)
27 | }
28 |
29 | func wsHandler(w http.ResponseWriter, r *http.Request) {
30 | fmt.Println("ws handler")
31 | vars := mux.Vars(r)
32 | messageTypeString := vars["type"]
33 | messageType := MessageTypeNone
34 | switch messageTypeString {
35 | case "clr":
36 | messageType = MessageTypeCLR
37 | case "dashboard":
38 | messageType = MessageTypeDashboard
39 | }
40 |
41 | if messageType == MessageTypeNone {
42 | fmt.Println("ws handler error")
43 | http.Error(w, "Invalid url. Valid urls: /ws/clr and /ws/dashboard", http.StatusBadRequest)
44 | return
45 | }
46 |
47 | ws, err := upgrader.Upgrade(w, r, nil)
48 | if err != nil {
49 | http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
50 | fmt.Printf("Upgrader error: %v\n", err)
51 | return
52 | }
53 |
54 | c := state.Context(w, r)
55 |
56 | conn := NewWSConn(ws, messageType, c)
57 |
58 | fmt.Println("aaaaaaaaa")
59 |
60 | // Create a custom connection
61 | Hub.register <- conn
62 | conn.onConnected()
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/web/controller/ws/wsmessage.go:
--------------------------------------------------------------------------------
1 | package ws
2 |
3 | // MessageType is used to help redirect a message to the proper connections
4 | type MessageType uint8
5 |
6 | // All available MessageTypes
7 | const (
8 | MessageTypeAll MessageType = iota
9 | MessageTypeNone
10 | MessageTypeCLR
11 | MessageTypeDashboard
12 | )
13 |
14 | // WSMessage xD
15 | type WSMessage struct {
16 | Channel string
17 |
18 | MessageType MessageType
19 |
20 | // LevelRequired <=0 means the message does not require authentication, otherwise
21 | // authentication is required and the users level must be equal to or above
22 | // the LevelRequired value
23 | LevelRequired int
24 |
25 | Payload *Payload
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/web/controller/ws/wspayload.go:
--------------------------------------------------------------------------------
1 | package ws
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | )
7 |
8 | // Payload xD
9 | type Payload struct {
10 | Event string `json:"event"`
11 | Data map[string]interface{} `json:"data"`
12 | }
13 |
14 | // ToJSON creates a json string from the payload
15 | func (p *Payload) ToJSON() (ret []byte) {
16 | ret, err := json.Marshal(p)
17 | if err != nil {
18 | log.Println("Error marshaling payload:", err)
19 | }
20 | return
21 | }
22 |
23 | func createPayload(data []byte) (*Payload, error) {
24 | p := &Payload{}
25 | err := json.Unmarshal(data, p)
26 | if err != nil {
27 | return nil, err
28 | }
29 | return p, nil
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/web/errors.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import "regexp"
4 |
5 | const (
6 | // ErrInvalidUserName is returned if an invalid username was posted to a router that expected a valid username
7 | ErrInvalidUserName = "invalid username"
8 | )
9 |
10 | var (
11 | singleUserName = regexp.MustCompile(`\w+`)
12 | userNameList = regexp.MustCompile(`[\w\,]+`)
13 | )
14 |
15 | func isValidUserName(input string) bool {
16 | return singleUserName.FindString(input) == input
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/web/errors_test.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import "testing"
4 |
5 | func TestIsValidUserName(t *testing.T) {
6 | good := []string{
7 | "asd",
8 | "pajlada",
9 | "randers",
10 | "karl_kons",
11 | "testaccount_420",
12 | "bajlada",
13 | }
14 | bad := []string{
15 | "lol hi",
16 | "!@$%!@$!@$!@$",
17 | " pajlada",
18 | }
19 | for _, name := range good {
20 | if !isValidUserName(name) {
21 | t.Fatalf("%s is not considered valid, while it should be", name)
22 | }
23 | }
24 |
25 | for _, name := range bad {
26 | if isValidUserName(name) {
27 | t.Fatalf("%s is considered valid, while it shouldn't be", name)
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/web/hooktypes.go:
--------------------------------------------------------------------------------
1 | package web
2 |
--------------------------------------------------------------------------------
/pkg/web/payloads.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | type user struct {
4 | Name string `json:"name"`
5 | DisplayName string `json:"display_name"`
6 | Points int64 `json:"points"`
7 | Level int64 `json:"level"`
8 | TotalMessageCount int64 `json:"total_message_count"`
9 | OnlineMessageCount int64 `json:"online_message_count"`
10 | OfflineMessageCount int64 `json:"offline_message_count"`
11 | LastSeen string `json:"last_seen"`
12 | LastActive string `json:"last_active"`
13 | }
14 |
15 | type customPayload struct {
16 | data map[string]interface{}
17 | }
18 |
19 | func (p *customPayload) Add(key string, value interface{}) {
20 | if p.data == nil {
21 | p.data = make(map[string]interface{})
22 | }
23 | p.data[key] = value
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/web/router/router.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/gorilla/mux"
8 | )
9 |
10 | // r will most likely need to be mutex locked
11 |
12 | var (
13 | r *mux.Router
14 | )
15 |
16 | func init() {
17 | r = mux.NewRouter()
18 | }
19 |
20 | func Subrouter(path string) *mux.Router {
21 | return r.PathPrefix(path).Subrouter()
22 | }
23 |
24 | func RGet(R *mux.Router, path string, handler http.HandlerFunc) *mux.Route {
25 | return R.HandleFunc(path, handler).Methods("GET")
26 | }
27 |
28 | func RPost(R *mux.Router, path string, handler http.HandlerFunc) *mux.Route {
29 | return R.HandleFunc(path, handler).Methods("POST")
30 | }
31 |
32 | func Get(path string, handler http.HandlerFunc) {
33 | RGet(r, path, handler)
34 | }
35 |
36 | func PathPrefix(path string, handler http.Handler) {
37 | r.PathPrefix(path).Handler(handler)
38 | }
39 |
40 | func RHandleFunc(R *mux.Router, path string, handler http.HandlerFunc) {
41 | R.HandleFunc(path, handler)
42 | }
43 |
44 | func HandleFunc(path string, handler http.HandlerFunc) {
45 | RHandleFunc(r, path, handler)
46 | }
47 |
48 | func Instance() *mux.Router {
49 | return r
50 | }
51 |
52 | func Debug() {
53 | r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
54 | t, err := route.GetPathTemplate()
55 | if err != nil {
56 | return err
57 | }
58 | fmt.Println(t)
59 | return nil
60 | })
61 | }
62 |
--------------------------------------------------------------------------------
/pkg/web/state/sessionstore.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | "net/http"
7 | "time"
8 | )
9 |
10 | const SessionIDCookie = "pb2sessionid"
11 |
12 | type SessionStore struct {
13 | }
14 |
15 | func getCookie(r *http.Request, cookieName string) *string {
16 | c, _ := r.Cookie(cookieName)
17 | if c == nil {
18 | return nil
19 | }
20 |
21 | return &c.Value
22 | }
23 |
24 | func IsValidSessionID(sessionID string) bool {
25 | // TODO: Make this more sophisticated
26 | return sessionID != ""
27 | }
28 |
29 | func SetSessionCookies(w http.ResponseWriter, sessionID string, twitchUserName string) {
30 | sessionIDCookie := &http.Cookie{
31 | Name: SessionIDCookie,
32 | Value: sessionID,
33 | Path: "/",
34 | }
35 | http.SetCookie(w, sessionIDCookie)
36 |
37 | userNameCookie := &http.Cookie{
38 | Name: "pb2username",
39 | Value: twitchUserName,
40 | Path: "/",
41 | }
42 | http.SetCookie(w, userNameCookie)
43 | }
44 |
45 | func ClearSessionCookies(w http.ResponseWriter) {
46 | sessionIDCookie := &http.Cookie{
47 | Name: SessionIDCookie,
48 | Value: "",
49 | Path: "/",
50 |
51 | Expires: time.Unix(0, 0),
52 | }
53 | http.SetCookie(w, sessionIDCookie)
54 |
55 | userNameCookie := &http.Cookie{
56 | Name: "pb2username",
57 | Value: "",
58 | Path: "/",
59 |
60 | Expires: time.Unix(0, 0),
61 | }
62 | http.SetCookie(w, userNameCookie)
63 | }
64 |
65 | func (s *SessionStore) Get(db *sql.DB, sessionID string) *Session {
66 | const queryF = `SELECT user_session.user_id, "user".twitch_userid, "user".twitch_username FROM user_session INNER JOIN "user" ON "user".id=user_session.user_id WHERE user_session.id=$1`
67 |
68 | var userID uint64
69 | var twitchUserID string
70 | var twitchUserName string
71 |
72 | err := db.QueryRow(queryF, sessionID).Scan(&userID, &twitchUserID, &twitchUserName) // GOOD
73 | switch {
74 | case err == sql.ErrNoRows:
75 | // invalid session ID
76 | return nil
77 | case err != nil:
78 | fmt.Println("SQL Error:", err)
79 | // some query or mysql error occurred
80 | return nil
81 | default:
82 | return &Session{
83 | UserID: userID,
84 | TwitchUserID: twitchUserID,
85 | TwitchUserName: twitchUserName,
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/pkg/web/web.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 |
8 | "github.com/gorilla/handlers"
9 | "github.com/pajbot/pajbot2/pkg/common/config"
10 | "github.com/pajbot/pajbot2/pkg/web/router"
11 | )
12 |
13 | func Run(cfg *config.WebConfig) {
14 | fmt.Printf("Starting web on host %s\n", cfg.Host)
15 | corsObj := handlers.AllowedOrigins([]string{"*"})
16 | err := http.ListenAndServe(cfg.Host, handlers.CORS(corsObj)(router.Instance()))
17 | if err != nil {
18 | log.Fatal(err)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/webutils/webutils.go:
--------------------------------------------------------------------------------
1 | package webutils
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/pajbot/pajbot2/pkg"
7 | "github.com/pajbot/pajbot2/pkg/users"
8 | "github.com/pajbot/pajbot2/pkg/web/state"
9 | "github.com/pajbot/utils"
10 | )
11 |
12 | func RequirePermission(w http.ResponseWriter, c state.State, channel pkg.Channel, permission pkg.Permission) bool {
13 | if c.Session == nil {
14 | utils.WebWriteError(w, 400, "Not authorized to view this endpoint")
15 | return false
16 | }
17 |
18 | user := users.NewSimpleTwitchUser(c.Session.TwitchUserID, c.Session.TwitchUserName)
19 | if user == nil {
20 | utils.WebWriteError(w, 400, "Not authorized to view this endpoint")
21 | return false
22 | }
23 |
24 | if channel != nil {
25 | if user.HasPermission(channel, permission) {
26 | return true
27 | }
28 | } else {
29 | if user.HasGlobalPermission(permission) {
30 | return true
31 | }
32 | }
33 |
34 | utils.WebWriteError(w, 400, "Not authorized to view this endpoint!!!")
35 | return false
36 | }
37 |
--------------------------------------------------------------------------------
/resources/testfiles/config1.json:
--------------------------------------------------------------------------------
1 | {
2 | "pass": "oauth:xD",
3 | "nick": "twitch_username",
4 | "channels": [
5 | "pajlada",
6 | "nuuls",
7 | "forsenlol"
8 | ],
9 | "redis_host": "localhost:6379",
10 | "sql_dsn": "pajbot2:password@tcp(localhost:3306)/pajbot2_test",
11 | "broker_host": "localhost:7353",
12 | "broker_pass": "test"
13 | }
14 |
--------------------------------------------------------------------------------
/resources/testfiles/config2_invalidjson.json:
--------------------------------------------------------------------------------
1 | {
2 | "pass": "oauth:xD",
3 | "nick": "twitch_username",
4 | "channels": [
5 | "pajlada",
6 | "nuuls",
7 | "forsenlol",
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/staticcheck.conf:
--------------------------------------------------------------------------------
1 | checks = ["all", "-ST1000", "-ST1003", "-U1000"]
2 |
--------------------------------------------------------------------------------
/tools.go:
--------------------------------------------------------------------------------
1 | // +build tools
2 |
3 | package tools
4 |
5 | import (
6 | _ "honnef.co/go/tools/cmd/staticcheck"
7 | )
8 |
--------------------------------------------------------------------------------
/utils/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | basedir="$(cd "$(dirname "$0")" >/dev/null 2>&1 && pwd)"
6 |
7 | if [ -z "$git_release" ]; then
8 | git_release=$(git describe --exact 2>/dev/null || echo "")
9 | fi
10 | if [ -z "$git_release" ]; then
11 | git_release="git"
12 | fi
13 | if [ -z "$git_hash" ]; then
14 | git_hash=$(git rev-parse --short HEAD)
15 | fi
16 | if [ -z "$git_branch" ]; then
17 | git_branch=$(git rev-parse --abbrev-ref HEAD)
18 | fi
19 |
20 | >&2 echo " * Building pajbot2 with the following flags: git_release=$git_release, git_hash=$git_hash, git_branch=$git_branch"
21 |
22 | cd "$basedir/../web" && npm i && npm run build
23 |
24 | go build -ldflags "\
25 | -X \"main.buildTime=$(date +%Y-%m-%dT%H:%M:%S%:z)\" \
26 | -X \"main.buildRelease=$git_release\" \
27 | -X \"main.buildHash=$git_hash\" \
28 | -X \"main.buildBranch=$git_branch\" \
29 | " \
30 | "$@" \
31 | -o "$basedir/../cmd/bot/" \
32 | "$basedir/../cmd/bot/"
33 |
--------------------------------------------------------------------------------
/utils/createdb.sh:
--------------------------------------------------------------------------------
1 | echo "enter root password"
2 | echo "CREATE DATABASE pajbot2 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;GRANT ALL PRIVILEGES ON pajbot2.* TO 'pajbot2'@'localhost' IDENTIFIED BY 'password';USE pajbot2;CREATE TABLE pb_command (triggers VARCHAR(512));" | mysql -uroot -p
3 |
--------------------------------------------------------------------------------
/utils/docker/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [ ! -d utils/docker ]; then
4 | echo "This script needs to be called from the root folder, i.e. ./utils/docker/build.sh"
5 | exit 1
6 | fi
7 |
8 | IMAGE_NAME=pajbot2:latest
9 |
10 | echo docker build --pull -t "$IMAGE_NAME" .
11 |
--------------------------------------------------------------------------------
/utils/docker/run.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | PBUID=$(id -u pajbot)
3 | PBGID=$(id -g pajbot)
4 |
5 | if [[ -z "${PBUID}${PBGID}" ]]; then
6 | echo 'pajbot user not detected.'
7 | exit 1
8 | fi
9 |
10 | if [ ! -f /opt/pajbot/configs/pajbot2.json ]; then
11 | echo "No config file /opt/pajbot/configs/pajbot2.json found."
12 | exit 1
13 | fi
14 |
15 | echo docker run \
16 | --name pajbot2 \
17 | --network host \
18 | --restart unless-stopped \
19 | -d \
20 | -v /opt/pajbot/configs/pajbot2.json:/app/cmd/bot/config.json \
21 | -v /var/run/postgresql:/var/run/postgresql:ro \
22 | -v /etc/localtime:/etc/localtime:ro \
23 | -u "$PBUID":"$PBGID" \
24 | pajbot2:latest
25 |
--------------------------------------------------------------------------------
/utils/findlibcoreclr.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | get_libcoreclr_path() {
4 | LIBCORECLR_PATH="$(dotnet --list-runtimes | grep Microsoft.NETCore.App | tail -1 | awk '{gsub(/\[|\]/, "", $3); print $3 "/" $2}')"
5 | if [ -z "$LIBCORECLR_PATH" ]; then
6 | echo ""
7 | else
8 | echo $LIBCORECLR_PATH
9 | fi
10 | }
11 |
12 | if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
13 | LIBCORECLR_PATH="$(get_libcoreclr_path)"
14 | if [ -z "$LIBCORECLR_PATH" ]; then
15 | echo "Unable to find path to libcoreclr. Ensure dotnet is installed"
16 | exit 1
17 | fi
18 |
19 | echo "Found libcoreclr.so at $LIBCORECLR_PATH"
20 | echo "To run the bot with csharp modules enabled, build the bot with '-tags csharp' and set the path with LIBCOREFOLDER=$LIBCORECLR_PATH"
21 | fi
22 |
--------------------------------------------------------------------------------
/utils/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | # Compile C++ "coreruncommon" lib
6 | cd 3rdParty/MessageHeightTwitch/c-interop
7 | g++ -c coreruncommon.cpp --std=c++14
8 | ar rvs libcoreruncommon.a coreruncommon.o
9 |
10 | # Compile C# library
11 | cd ../
12 | dotnet publish --configuration Release -o build/
13 |
14 | cp charmap.bin.gz build/*.dll ../../cmd/bot/
15 |
--------------------------------------------------------------------------------
/utils/mkmig.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | migrate -database mysql://pajbot2:password@/pajbot2_test -verbose create -ext sql -seq -dir migrations $1
4 |
--------------------------------------------------------------------------------
/utils/test-all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | go test ./...
3 |
--------------------------------------------------------------------------------
/utils/upmig.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | COMMAND=${1:-up}
4 |
5 | echo $COMMAND
6 |
7 | $GOPATH/bin/migrate -verbose -database mysql://root:penis123@/pajbot2_test -path ./migrations ${COMMAND}
8 |
--------------------------------------------------------------------------------
/web/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/env", "@babel/react"],
3 | "plugins": ["@babel/plugin-proposal-class-properties"]
4 | }
5 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | # The index.html file is generated by webpack
2 | /views/index.html
3 |
--------------------------------------------------------------------------------
/web/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | static
3 |
--------------------------------------------------------------------------------
/web/.prettierrc.toml:
--------------------------------------------------------------------------------
1 | trailingComma = "es5"
2 | singleQuote = true
3 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pajbot2-web",
3 | "scripts": {
4 | "build": "node_modules/.bin/webpack --mode production",
5 | "watch": "node_modules/.bin/webpack --watch --mode development",
6 | "check_formatting": "prettier --check .",
7 | "reformat": "prettier --write ."
8 | },
9 | "dependencies": {
10 | "@babel/plugin-proposal-class-properties": "^7.18.6",
11 | "react": "^19.0.0",
12 | "react-dom": "^19.1.0"
13 | },
14 | "devDependencies": {
15 | "@babel/core": "^7.27.1",
16 | "@babel/preset-env": "^7.27.2",
17 | "@babel/preset-react": "^7.27.1",
18 | "babel-loader": "^10.0.0",
19 | "clean-webpack-plugin": "^4.0.0",
20 | "css-loader": "^7.1.2",
21 | "html-webpack-plugin": "^5.6.3",
22 | "mini-css-extract-plugin": "^2.9.2",
23 | "prettier": "^3.5.3",
24 | "sass": "^1.89.0",
25 | "sass-loader": "^16.0.5",
26 | "style-loader": "^4.0.0",
27 | "webpack": "^5.99.8",
28 | "webpack-cli": "^6.0.1"
29 | },
30 | "bugs": {
31 | "url": "https://github.com/pajbot/pajbot2/issues"
32 | },
33 | "homepage": "https://github.com/pajbot/pajbot2",
34 | "license": "MIT",
35 | "engines": {
36 | "node": ">=14"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/web/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import Dashboard from './js/Dashboard';
4 | import Banphrases from './js/Banphrases';
5 | import Admin from './js/Admin';
6 | import Commands from './js/Commands';
7 | import Menu from './js/Menu';
8 | import ThemeLoader from './js/ThemeLoader';
9 | import ThemeProvider from './js/ThemeProvider';
10 | import './scss/app.scss';
11 |
12 | const menuEl = document.getElementById('menu');
13 | const dashboard = document.getElementById('dashboard');
14 | const banphrases = document.getElementById('banphrases');
15 | const admin = document.getElementById('admin');
16 | const commands = document.getElementById('commands');
17 |
18 | function App() {
19 | return (
20 |
21 |
22 |
23 | {dashboard && }
24 | {banphrases && }
25 | {admin && }
26 | {commands && }
27 |
28 | );
29 | }
30 |
31 | const root = ReactDOM.createRoot(document.getElementById('app'));
32 | root.render( );
33 |
--------------------------------------------------------------------------------
/web/src/js/Admin.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import WebSocketHandler from './WebSocketHandler';
3 |
4 | export default class Admin extends Component {
5 | constructor(props) {
6 | super(props);
7 |
8 | let extra = JSON.parse(props.element.getAttribute('data-extra'));
9 |
10 | this.state = {
11 | bots: extra.Bots,
12 | };
13 | }
14 |
15 | render() {
16 | return (
17 |
18 | Admin
19 |
20 |
Authenticate as bot -{' '}
21 |
22 | Use if you need to reauthenticate a bot below, or add a new one. You
23 | should probably copy-paste the link into incognito mode so you can
24 | log in as your bot account
25 |
26 |
27 |
28 |
29 |
30 | Name
31 | Connected
32 |
33 |
34 |
35 | {this.state.bots.map((bot, index) => (
36 |
37 | {bot.Name}
38 | {bot.Connected ? 'Yes' : 'No'}
39 |
40 | ))}
41 |
42 |
43 |
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/web/src/js/Commands.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import WebSocketHandler from './WebSocketHandler';
3 |
4 | export default class Commands extends Component {
5 | constructor(props) {
6 | super(props);
7 |
8 | let extra = JSON.parse(props.element.getAttribute('data-extra'));
9 |
10 | this.state = {
11 | commands: extra,
12 | };
13 |
14 | console.log(this.state);
15 | }
16 |
17 | render() {
18 | return (
19 |
20 | Commands
21 |
22 |
23 |
24 | Name
25 | Description
26 |
27 |
28 |
29 | {this.state.commands.map((command, index) => (
30 |
31 | {command.Name}
32 | {command.Description}
33 |
34 | ))}
35 |
36 |
37 |
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/web/src/js/LogInButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { isLoggedIn, logIn, logOut, loggedInUsername } from './auth';
4 | import { readCookie } from './cookie';
5 |
6 | export default class LogInButton extends Component {
7 | constructor(props) {
8 | super(props);
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 | Logged in as {loggedInUsername()}
16 |
17 |
18 |
23 | Connect with Twitch
24 |
25 |
30 | Log out
31 |
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/web/src/js/Menu.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import LogInButton from './LogInButton';
3 | import ThemeSwitcher from './ThemeSwitcher';
4 | import { isLoggedIn } from './auth';
5 |
6 | export default class Menu extends Component {
7 | constructor(props) {
8 | super(props);
9 |
10 | this.menuItems = [
11 | {
12 | link: '/',
13 | name: 'Home',
14 | requireLogin: false,
15 | },
16 | {
17 | link: '/admin',
18 | name: 'Admin',
19 | requireLogin: true,
20 | },
21 | {
22 | link: '/dashboard',
23 | name: 'Dashboard',
24 | requireLogin: true,
25 | },
26 | ];
27 | }
28 |
29 | render() {
30 | return (
31 |
32 |
33 | pajbot2
34 |
35 |
44 |
45 |
46 |
47 |
48 | {this.menuItems
49 | .filter(
50 | (item) =>
51 | !item.requireLogin || (item.requireLogin && isLoggedIn())
52 | )
53 | .map((menuItem, index) => (
54 |
61 | {menuItem.name}
62 |
63 | ))}
64 |
65 |
66 |
67 |
68 |
69 | );
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/web/src/js/ThemeContext.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const ThemeContext = React.createContext();
4 |
--------------------------------------------------------------------------------
/web/src/js/ThemeLoader.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { ThemeContext } from './ThemeContext';
4 |
5 | export default class ThemeLoader extends Component {
6 | render() {
7 | return (
8 |
9 | {({ theme }) => }
10 |
11 | );
12 | }
13 |
14 | themePath = (t) => {
15 | return '/static/themes/' + t + '/bootstrap.min.css';
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/web/src/js/ThemeProvider.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { ThemeContext } from './ThemeContext';
4 |
5 | import { createCookie, readCookie } from './cookie';
6 |
7 | export default class ThemeProvider extends Component {
8 | validThemes = ['Light', 'Dark'];
9 |
10 | constructor(props) {
11 | super(props);
12 |
13 | let savedTheme = readCookie('currentTheme');
14 |
15 | if (!savedTheme || this.validThemes.indexOf(savedTheme) === -1) {
16 | savedTheme = 'Dark';
17 | }
18 |
19 | this.state = {
20 | theme: savedTheme,
21 | };
22 | }
23 |
24 | render() {
25 | return (
26 |
33 | {this.props.children}
34 |
35 | );
36 | }
37 |
38 | setTheme = (t) => {
39 | this.setState({
40 | theme: t,
41 | });
42 |
43 | // Save theme
44 | createCookie('currentTheme', t, 3600);
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/web/src/js/ThemeSwitcher.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import ThemeLoader from './ThemeLoader';
4 |
5 | import { ThemeContext } from './ThemeContext';
6 |
7 | export default class ThemeSwitcher extends Component {
8 | render() {
9 | return (
10 |
11 | {({ theme, validThemes, setTheme }) => (
12 |
42 | )}
43 |
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/web/src/js/auth.js:
--------------------------------------------------------------------------------
1 | import { readCookie } from './cookie';
2 |
3 | export function loggedInUsername() {
4 | return readCookie('pb2username') || '???';
5 | }
6 |
7 | export function isLoggedIn() {
8 | return readCookie('pb2sessionid') !== null;
9 | }
10 |
11 | export function logIn() {
12 | window.location.href =
13 | '/api/auth/twitch/user?redirect=' + window.location.pathname;
14 | }
15 |
16 | export function logOut() {
17 | window.location.href = '/logout?redirect=' + window.location.pathname;
18 | }
19 |
--------------------------------------------------------------------------------
/web/src/js/cookie.js:
--------------------------------------------------------------------------------
1 | // taken from https://gist.github.com/thoov/984751
2 | export function createCookie(name, value, days) {
3 | if (days) {
4 | var date = new Date();
5 | date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
6 | var expires = '; expires=' + date.toGMTString();
7 | } else {
8 | var expires = '';
9 | }
10 | document.cookie = name + '=' + value + expires + '; path=/';
11 | }
12 |
13 | export function readCookie(name) {
14 | var nameEQ = name + '=';
15 | var ca = document.cookie.split(';');
16 | for (var i = 0; i < ca.length; i++) {
17 | var c = ca[i];
18 | while (c.charAt(0) == ' ') {
19 | c = c.substring(1, c.length);
20 | }
21 | if (c.indexOf(nameEQ) == 0) {
22 | return c.substring(nameEQ.length, c.length);
23 | }
24 | }
25 |
26 | return null;
27 | }
28 |
29 | export function eraseCookie(name) {
30 | createCookie(name, '', -1);
31 | }
32 |
--------------------------------------------------------------------------------
/web/src/scss/app.scss:
--------------------------------------------------------------------------------
1 | @use 'vendor/reset';
2 |
3 | @use 'variables/colors';
4 | @use 'variables/spacing';
5 |
6 | @use 'tools/mixins';
7 |
8 | @use 'base/body';
9 | @use 'base/buttons';
10 | @use 'base/links';
11 |
12 | @use 'modules/dashboard';
13 |
--------------------------------------------------------------------------------
/web/src/scss/base/body.scss:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: Helvetica, Arial, sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/web/src/scss/base/buttons.scss:
--------------------------------------------------------------------------------
1 | .btn-twitch {
2 | background-color: #6441a4 !important;
3 | color: #fff !important;
4 |
5 | &:hover {
6 | background-color: rgb(80, 52, 131);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/web/src/scss/base/links.scss:
--------------------------------------------------------------------------------
1 | @use '../variables/colors';
2 |
3 | a {
4 | color: colors.$flat-blue;
5 |
6 | &:hover {
7 | cursor: pointer !important;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/web/src/scss/modules/dashboard.scss:
--------------------------------------------------------------------------------
1 | @use '../variables/spacing';
2 |
3 | .dashboard {
4 | padding: spacing.$spacing-XXL;
5 |
6 | .reports {
7 | margin-top: spacing.$spacing-XL;
8 |
9 | .report {
10 | min-height: 100px;
11 | -webkit-transition: all 500ms cubic-bezier(0.25, 0.1, 0.25, 1);
12 | -moz-transition: all 500ms cubic-bezier(0.25, 0.1, 0.25, 1);
13 | -o-transition: all 500ms cubic-bezier(0.25, 0.1, 0.25, 1);
14 | transition: all 500ms cubic-bezier(0.25, 0.1, 0.25, 1); /* ease (default) */
15 |
16 | -webkit-transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
17 | -moz-transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
18 | -o-transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
19 | transition-timing-function: cubic-bezier(
20 | 0.25,
21 | 0.1,
22 | 0.25,
23 | 1
24 | ); /* ease (default) */
25 |
26 | .removing {
27 | height: 0px;
28 | opacity: 0;
29 | }
30 |
31 | .reporter,
32 | .target {
33 | font-weight: 800;
34 | }
35 |
36 | .reason {
37 | font-style: italic;
38 | }
39 |
40 | pre.logs {
41 | font-family: sans-serif;
42 | }
43 | }
44 | }
45 |
46 | .results.loading {
47 | opacity: 0.5;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/web/src/scss/tools/mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin box-shadow($level) {
2 | @if $level == 1 {
3 | box-shadow:
4 | 0 1px 3px rgba(0, 0, 0, 0.12),
5 | 0 1px 2px rgba(0, 0, 0, 0.24);
6 | } @else if $level == 2 {
7 | box-shadow:
8 | 0 3px 6px rgba(0, 0, 0, 0.16),
9 | 0 3px 6px rgba(0, 0, 0, 0.23);
10 | } @else if $level == 3 {
11 | box-shadow:
12 | 0 10px 20px rgba(0, 0, 0, 0.19),
13 | 0 6px 6px rgba(0, 0, 0, 0.23);
14 | } @else if $level == 4 {
15 | box-shadow:
16 | 0 14px 28px rgba(0, 0, 0, 0.25),
17 | 0 10px 10px rgba(0, 0, 0, 0.22);
18 | } @else if $level == 5 {
19 | box-shadow:
20 | 0 19px 38px rgba(0, 0, 0, 0.3),
21 | 0 15px 12px rgba(0, 0, 0, 0.22);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/web/src/scss/variables/colors.scss:
--------------------------------------------------------------------------------
1 | $grey-light: #616161;
2 | $grey-medium: #424242;
3 | $grey-dark: #212121;
4 |
5 | $flat-red: #e74c3c;
6 | $flat-blue: #3498db;
7 |
8 | $twitch-purple: #6441a4;
9 |
--------------------------------------------------------------------------------
/web/src/scss/variables/spacing.scss:
--------------------------------------------------------------------------------
1 | $spacing-S: 2px;
2 | $spacing-M: 5px;
3 | $spacing-L: 8px;
4 | $spacing-XL: 10px;
5 | $spacing-XXL: 15px;
6 | $spacing-3XL: 20px;
7 |
--------------------------------------------------------------------------------
/web/src/scss/vendor/reset.scss:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html,
7 | body,
8 | div,
9 | span,
10 | applet,
11 | object,
12 | iframe,
13 | h1,
14 | h2,
15 | h3,
16 | h4,
17 | h5,
18 | h6,
19 | p,
20 | blockquote,
21 | pre,
22 | a,
23 | abbr,
24 | acronym,
25 | address,
26 | big,
27 | cite,
28 | code,
29 | del,
30 | dfn,
31 | em,
32 | img,
33 | ins,
34 | kbd,
35 | q,
36 | s,
37 | samp,
38 | small,
39 | strike,
40 | strong,
41 | sub,
42 | sup,
43 | tt,
44 | var,
45 | b,
46 | u,
47 | i,
48 | center,
49 | dl,
50 | dt,
51 | dd,
52 | ol,
53 | ul,
54 | li,
55 | fieldset,
56 | form,
57 | label,
58 | legend,
59 | table,
60 | caption,
61 | tbody,
62 | tfoot,
63 | thead,
64 | tr,
65 | th,
66 | td,
67 | article,
68 | aside,
69 | canvas,
70 | details,
71 | embed,
72 | figure,
73 | figcaption,
74 | footer,
75 | header,
76 | hgroup,
77 | menu,
78 | nav,
79 | output,
80 | ruby,
81 | section,
82 | summary,
83 | time,
84 | mark,
85 | audio,
86 | video {
87 | margin: 0;
88 | padding: 0;
89 | border: 0;
90 | font-size: 100%;
91 | font: inherit;
92 | vertical-align: baseline;
93 | }
94 |
95 | /* HTML5 display-role reset for older browsers */
96 |
97 | article,
98 | aside,
99 | details,
100 | figcaption,
101 | figure,
102 | footer,
103 | header,
104 | hgroup,
105 | menu,
106 | nav,
107 | section {
108 | display: block;
109 | }
110 |
111 | body {
112 | line-height: 1;
113 | }
114 |
115 | ol,
116 | ul {
117 | list-style: none;
118 | }
119 |
120 | blockquote,
121 | q {
122 | quotes: none;
123 | }
124 |
125 | blockquote {
126 | &:before,
127 | &:after {
128 | content: '';
129 | content: none;
130 | }
131 | }
132 |
133 | q {
134 | &:before,
135 | &:after {
136 | content: '';
137 | content: none;
138 | }
139 | }
140 |
141 | table {
142 | border-collapse: collapse;
143 | border-spacing: 0;
144 | }
145 |
--------------------------------------------------------------------------------
/web/views/403.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | pajbot2 - 403
4 |
5 |
6 | 403 - Forbidden
7 |
8 | You do not have access to view this page. You might need to log in? Press
9 | back in your browser!
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/web/views/admin.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 | {{end}}
4 |
--------------------------------------------------------------------------------
/web/views/banphrases.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 | {{end}}
4 |
--------------------------------------------------------------------------------
/web/views/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dashboard
5 |
6 |
7 |
12 |
17 |
22 |
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
30 | {{template "content" .}}
31 |
32 |
33 |
--------------------------------------------------------------------------------
/web/views/commands.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 | {{end}}
4 |
--------------------------------------------------------------------------------
/web/views/dashboard.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
7 | {{end}}
8 |
--------------------------------------------------------------------------------
/web/views/home.html:
--------------------------------------------------------------------------------
1 | {{define "content"}} XD {{end}}
2 |
--------------------------------------------------------------------------------
/web/views/profile.html:
--------------------------------------------------------------------------------
1 | {{define "content"}} profile lol {{end}}
2 |
--------------------------------------------------------------------------------
/web/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
5 | const HtmlWebpackPlugin = require('html-webpack-plugin');
6 |
7 | const BUILD_DIR = path.resolve(__dirname, './static/build');
8 |
9 | module.exports = {
10 | entry: './src/index.jsx',
11 | output: {
12 | path: BUILD_DIR,
13 | filename: '[name].[contenthash].js',
14 | publicPath: '/static/build',
15 | },
16 | module: {
17 | rules: [
18 | {
19 | test: /\.jsx$/,
20 | exclude: /node_modules/,
21 | use: {
22 | loader: 'babel-loader',
23 | },
24 | },
25 | {
26 | test: /\.scss$/,
27 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
28 | },
29 | ],
30 | },
31 | resolve: {
32 | extensions: ['.js', '.jsx'],
33 | },
34 | plugins: [
35 | new CleanWebpackPlugin(),
36 | new MiniCssExtractPlugin({
37 | filename: '[name].[contenthash].css',
38 | }),
39 | new HtmlWebpackPlugin({
40 | filename: path.join(__dirname, './views/index.html'),
41 | template: path.join(__dirname, './views/base.html'),
42 | }),
43 | ],
44 | };
45 |
--------------------------------------------------------------------------------