├── .build.yml ├── .clang-format ├── .github └── workflows │ └── gh-pages.yaml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── CHANGELOG ├── LICENSE.txt ├── README.md ├── alpine.sh ├── alpinetest └── Dockerfile ├── auth.sh ├── cbot1.0.md ├── compile_commands.json ├── deploy ├── .gitignore ├── Dockerfile ├── README.md ├── alpine │ └── APKBUILD └── stephen@brennan.io.rsa.pub ├── doc ├── Backends.md ├── Plugins-2.md ├── Plugins.md ├── Tooling.md ├── User.md ├── admin.rst ├── conf.py.in ├── dev.rst ├── doxygen.ini.in ├── index.rst ├── meson.build └── run_doxygen_sphinx.sh.in ├── inc └── cbot │ ├── cbot.h │ ├── curl.h │ ├── db.h │ └── json.h ├── make-release.sh ├── meson.build ├── meson_options.txt ├── plugin ├── Makefile ├── annoy.c ├── aqi.c ├── become.c ├── birthday.c ├── buttcoin.c ├── emote.c ├── greet.c ├── help.c ├── ircctl.c ├── karma.c ├── log.c ├── name.c ├── reactrack.c ├── reply.c ├── sports_schedule.c ├── sqlkarma.c ├── sqlknow.c ├── tok.c ├── trivia.c ├── weather.c └── who.c ├── sample.cfg ├── src ├── cbot.c ├── cbot_cli.c ├── cbot_cli.h ├── cbot_handlers.h ├── cbot_irc.c ├── cbot_irc.h ├── cbot_private.h ├── curl.c ├── db.c ├── fmt.c ├── http.c ├── json.c ├── log.c ├── main.c ├── signal │ ├── backend.c │ ├── internal.h │ ├── jmsg.c │ ├── mention.c │ ├── signalcli_bridge.c │ └── signald_bridge.c ├── tok.c └── utf8.h ├── subprojects ├── Unity.wrap ├── libircclient.wrap ├── nosj.wrap ├── packagecache │ └── libircclient-patch-1.10.tar.gz ├── sc-argparse.wrap ├── sc-collections.wrap ├── sc-lwt.wrap ├── sc-regex.wrap └── sqlite.wrap ├── testing └── test_coinmarketcap.py └── tests ├── mentions.c ├── mentions.json └── meson.build /.build.yml: -------------------------------------------------------------------------------- 1 | image: alpine/3.18 2 | packages: 3 | - meson 4 | - gcc 5 | - git 6 | - alpine-sdk 7 | - libc-dev 8 | - sqlite-dev 9 | - libconfig-dev 10 | - curl-dev 11 | - libmicrohttpd-dev 12 | - openssl-dev 13 | - libucontext-dev 14 | - sqlite 15 | - docker 16 | sources: 17 | - https://github.com/brenns10/cbot 18 | secrets: 19 | - 26940529-4ef2-42b9-af4d-6225d87dd525 # github token (export GITHUB_TOKEN=...) 20 | - 7b922363-3ca0-4080-863f-5404a4e6796f # package key 21 | - 29e80069-2df8-4bd1-9801-313f2ac4a82b # docker hub 22 | tasks: 23 | - ghsetup: | 24 | mkdir ghsetup && cd ghsetup 25 | ghver=2.0.0 26 | wget https://github.com/cli/cli/releases/download/v$ghver/gh_${ghver}_linux_amd64.tar.gz 27 | tar xf gh_${ghver}_linux_amd64.tar.gz 28 | sudo mv gh_${ghver}_linux_amd64/bin/gh /usr/bin/gh 29 | cd .. && rm -r ghsetup 30 | cd cbot 31 | git remote add upstream https://github.com/brenns10/cbot 32 | - setup: | 33 | cd cbot 34 | meson build 35 | - build: | 36 | cd cbot 37 | ninja -C build 38 | - source_release: | 39 | case $GIT_REF in refs/tags/*) true;; *) exit 0;; esac 40 | TAG=${GIT_REF#refs/tags/} 41 | cd cbot 42 | ./make-release.sh $TAG 43 | mkdir -p ~/artifacts 44 | mv cbot-${TAG#v}.tar.gz ~/artifacts/ 45 | - alpine_package: | 46 | case $GIT_REF in refs/tags/*) true;; *) exit 0;; esac 47 | TAG=${GIT_REF#refs/tags/} 48 | cd cbot 49 | export PACKAGER_PRIVKEY=~/packager.key 50 | sudo cp deploy/stephen@brennan.io.rsa.pub /etc/apk/keys/ 51 | export PACKAGER_PUBKEY=~/cbot/deploy/stephen@brennan.io.rsa.pub 52 | cd deploy/alpine 53 | cp ~/artifacts/cbot-${TAG#v}.tar.gz . 54 | sed -i 's?source=.*$?source="'cbot-${TAG#v}.tar.gz'"?' APKBUILD 55 | abuild checksum 56 | abuild 57 | mv ~/packages/deploy/x86_64/cbot-${TAG#v}-r0.apk ~/artifacts/ 58 | - github_release: | 59 | case $GIT_REF in refs/tags/*) true;; *) exit 0;; esac 60 | TAG=${GIT_REF#refs/tags/} 61 | set +x; source ~/.github_token; set -x 62 | cd cbot 63 | # Get the blurb for the current release from CHANGELOG 64 | sed /tmp/changelog 69 | gh release create $TAG ~/artifacts/* -t $TAG -F /tmp/changelog 70 | - docker_build: | 71 | case $GIT_REF in refs/tags/*) true;; *) exit 0;; esac 72 | TAG=${GIT_REF#refs/tags/} 73 | set +x; source ~/.github_token; set -x 74 | cd cbot/deploy 75 | cp ~/artifacts/cbot-${TAG#v}-r0.apk cbot.apk 76 | sudo service docker start 77 | sudo docker login docker.io -u brenns10 --password-stdin < ~/.dockerpw 78 | sudo docker build -t brenns10/cbot:$TAG . 79 | sudo docker tag brenns10/cbot:$TAG brenns10/cbot:latest 80 | sudo docker push -a docker.io/brenns10/cbot 81 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | IndentWidth: 8 3 | UseTab: ForIndentation 4 | BreakBeforeBraces: Linux 5 | AllowShortIfStatementsOnASingleLine: false 6 | AllowShortFunctionsOnASingleLine: false 7 | IndentCaseLabels: false 8 | ContinuationIndentWidth: 8 9 | Cpp11BracedListStyle: false 10 | AlignConsecutiveMacros: true 11 | BraceWrapping: 12 | AfterEnum: true 13 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yaml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-20.04 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Setup Environment 18 | run: | 19 | sudo apt update -y 20 | sudo apt install -y python3-pip doxygen meson gcc libconfig-dev libsqlite3-dev libcurl4-openssl-dev libmicrohttpd-dev 21 | sudo pip install sphinx breathe myst_parser 22 | 23 | - name: Build 24 | run: | 25 | meson build -Ddoc=true 26 | ninja -C build docs 27 | 28 | - name: Deploy 29 | uses: peaceiris/actions-gh-pages@v3 30 | if: ${{ github.ref == 'refs/heads/master' }} 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: ./build/doc/doc/ 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | GPATH 3 | GTAGS 4 | GRTAGS 5 | run.sh 6 | hash.txt 7 | subprojects/* 8 | !subprojects/*.wrap 9 | subprojects/localcache/* 10 | !subprojects/localcache/libircclient-patch-1.10.tar.gz 11 | .clangd 12 | run_local.sh 13 | *.cfg 14 | *.sqlite3 15 | !sample.cfg 16 | cbot-*.tar.gz 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brenns10/cbot/fcd8bf599806ab54c345224e055bfd694d1a51e4/.gitmodules -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: clang-include-cleaner 5 | name: "Run clang-include-cleaner" 6 | language: system 7 | files: "^.*\\.[ch]$" 8 | exclude: "plugin/help.h" 9 | entry: clang-include-cleaner --edit --insert --remove --ignore-headers=unity_internals.h,bits/time.h 10 | - id: clang-format 11 | name: "Run clang-format" 12 | language: system 13 | files: "^.*\\.[ch]$" 14 | entry: clang-format -i 15 | - id: clang-tidy 16 | name: "Run clang-tidy" 17 | language: system 18 | files: "^.*\\.c$" 19 | exclude: "plugin/help.h" 20 | entry: clang-tidy --checks=-*,clang-analyzer-*,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,performance-* --warnings-as-errors=* 21 | - repo: https://github.com/pre-commit/pre-commit-hooks 22 | rev: v2.5.0 23 | hooks: 24 | - id: trailing-whitespace 25 | - id: check-yaml 26 | - id: end-of-file-fixer 27 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Be sure version number is updated in deploy/alpine/APKBUILD. 5 | 6 | Unreleased 7 | ---------- 8 | 9 | 0.15.0 (2024-08-11) 10 | ------------------- 11 | 12 | Fixed NBA eastern timezone. 13 | 14 | 0.14.0 (2024-08-11) 15 | ------------------- 16 | 17 | Added trivia cancellation. 18 | 19 | 0.13.5 (2024-04-18) 20 | ------------------- 21 | 22 | Finally, fixed sports_schedule plugin for musl libc. There were multiple issues 23 | with strptime and strftime formats. Resolved those, and added some more error 24 | checking. Added better logging and some tweaks to make it easy to run cbot in an 25 | Alpine Docker container for doing final testing on my local machine. 26 | 27 | 0.13.4 (2024-04-17) 28 | ------------------- 29 | 30 | Yet another bug fix: 31 | 32 | - Another %Y-%m-%d replaced with %F in strptime()... 33 | - Use %l rather than %I in strftime() and eliminate leading space 34 | 35 | 0.13.3 (2024-04-17) 36 | ------------------- 37 | 38 | Bug fix: 39 | 40 | - Use %Y-%m-%d instead of %F in strptime(), since %F is apparently a glibc 41 | extension. Thanks, musl. 42 | 43 | 0.13.2 (2024-04-16) 44 | ------------------- 45 | 46 | Some tweaks: 47 | 48 | - The sqlknow plugin now uses non-greedy matching, which helps when the value 49 | being stored is a long sentence using the word "is". 50 | - The sqlknow plugin now supports forgetting ("cbot forget $key"), but only 51 | deletion by the admin. There's no good reason for that restriction, I guess 52 | it's just a power trip. 53 | - The CLI backend now has a "/nick" command to test authorization. 54 | 55 | 0.13.1 (2024-04-16) 56 | ------------------- 57 | 58 | As usual, a fix to CI for automatic deployment. 59 | 60 | 0.13.0 (2024-04-16) 61 | ------------------- 62 | 63 | Several major new features! 64 | 65 | Chat backends: 66 | 67 | - The signal backend is improved to support multiple Signal API bridges. The old 68 | code is for Signald and it remains as an option. 69 | - Add support for a new Signal bridge: signal-cli! 70 | - The CLI backend has gained readline/libedit support, so we now have enhanced 71 | line editing when testing. 72 | 73 | Plugins: 74 | 75 | - A new plugin, "sports_schedule" is added. This checks for local SF sports games 76 | and reports them, optionally on a timer schedule. Useful for notifying about 77 | potential traffic disruptions ahead of time. 78 | - The trivia plugin now allows a "?" reaction to mean "maybe". 79 | 80 | 0.12.1 (2023-10-10) 81 | ------------------- 82 | 83 | Fix to CI for automatic deployment. 84 | 85 | 0.12.0 (2023-10-10) 86 | ------------------- 87 | 88 | The trivia plugin has gained new features: 89 | 90 | - Support for non-email notification to the trivia host (still via external command) 91 | - Admin user can now start trivia early, and RSVP early 92 | - Add To/From headers for email in trivia 93 | 94 | The Docker image is now based on alpine 3.18. Perl and curl commands are now added 95 | to enable the external command for non-email notifications. 96 | 97 | 0.11.5 (2023-05-31) 98 | ------------------- 99 | 100 | The birthday plugin has been dramatically simplified using the new callback 101 | system. It should be much less buggy now. 102 | 103 | 0.11.4 (2023-05-31) 104 | ------------------- 105 | 106 | Fixed several bugs in the trivia reservation plugin. 107 | Added reaction capabilities to the CLI backend. 108 | Changed trivia reservation configuration (see commit log). 109 | 110 | 0.11.0-3 (2023-05-31) 111 | --------------------- 112 | 113 | Add trivia reservation plugin. 114 | New ability to respond to Signal reactions. 115 | 116 | 0.10.1,2 (2022-11-15) 117 | --------------------- 118 | 119 | Update buttcoin to include Tether, and better notification behavior. 120 | 121 | 0.10.0 (2022-11-14) 122 | ------------------- 123 | 124 | Added buttcoin plugin. 125 | 126 | 0.9.1 (2022-10-31) 127 | ------------------ 128 | 129 | Updated github deploy token. No code change. 130 | 131 | 0.9.0 (2022-10-31) 132 | ------------------ 133 | 134 | - HTTP server is added. 135 | - Help is delivered as a link to a webpage. 136 | - Plugins may register for HTTP events. 137 | - Birthdays can be viewed as web page 138 | - [deploy] Removed unnecessary kill statement in run.sh 139 | 140 | 0.8.0 (2022-05-30) 141 | ---------------------- 142 | 143 | - deploy: get rid of signald from the docker image 144 | - refresh Github deploy keys 145 | 146 | 0.7.4 (2022-02-28) 147 | ------------------ 148 | 149 | - Signal: Allow using "@cbot" (but not as a real tagged mention) as an alias for 150 | the bot. 151 | - Signal: Fix grotesque logging situation 152 | - Global: Allow to DM the bot without using its name, to trigger commands 153 | - plugin/birthday: code cleanups, use built-in logging framework 154 | - plugin/{sql,}karma: use ratelimited sending 155 | - Docker: set timezone 156 | 157 | 158 | 0.7.3 (2022-02-19) 159 | ------------------ 160 | 161 | Signal: Fix bug in queued message handling which sometimes resulted in 162 | double-free. 163 | 164 | 0.7.{1,2} (2022-02-19) 165 | ---------------------- 166 | 167 | Packaging changes to allow core dumps, and debuginfo. 168 | 169 | 0.7.0 (2022-02-18) 170 | ------------------ 171 | 172 | New Plugin, birthday! 173 | - Tracks birthdays stored in database 174 | - Every day at configurable time, sends happy birthday messages for relevant 175 | people 176 | - Sends a monthly overview message 177 | - Allows management by me 178 | 179 | Signal backend is improved so that waiting for a response from signald does not 180 | drop messages which were sent in the meantime. 181 | 182 | Cbot gained rate limited message support. 183 | 184 | 0.6.2 (2022-02-18) 185 | ------------------ 186 | 187 | Midnight release: 188 | - Alpine/Docker change to support native library 189 | - Fix potential segfault on user name 190 | 191 | 0.6.1 (2022-02-17) 192 | ------------------ 193 | 194 | Fix odd build error from sourcehut. 195 | 196 | 0.6.0 (2022-02-17) 197 | ------------------ 198 | 199 | - Signal backend: 200 | - Add ignore_dm config 201 | - Add the ability to respond to @mentions 202 | - Upgrade signald/java in Docker container 203 | 204 | 0.5.{4..9} (2021-11-23) 205 | ----------------------- 206 | 207 | - Another test release for CI, which hopefully includes the Docker builds. 208 | 209 | 0.5.3 (2021-09-15) 210 | ------------------ 211 | 212 | - Test release for CI. 213 | - This release should include automatic Docker builds. 214 | 215 | 0.5.2 (2021-09-15) 216 | ------------------ 217 | 218 | - Another test release for CI. 219 | 220 | 0.5.1 (2021-09-15) 221 | ------------------ 222 | 223 | - Just a test release for CI. 224 | 225 | 0.5.0 (2021-09-15) 226 | ------------------ 227 | 228 | - Add an AQI plugin 229 | - Add logging system 230 | - Significant cleanup of Signal backend 231 | - Improvements brought in from sc-lwt regarding epoll 232 | 233 | 0.4.1 (2021-09-06) 234 | ------------------ 235 | 236 | - Plugins are now installed at libexecdir, e.g. /usr/libexec/cbot/foo.so. 237 | - Added new aqi plugin 238 | - Fixed shutdown behavior: any plugin/backend threads must gracefully handle the 239 | sc-lwt shutdown signal. 240 | 241 | 0.4.0 (2021-09-06) 242 | ------------------ 243 | 244 | A ton more changes. Up to this point, cbot really hasn't had a real version 245 | scheme or release system. However, this release changes that. I'm preparing cbot 246 | to be deployed via container, and so I want to package it for Alpine. This 247 | requires that I have regular source tarball releases. Each release tarball 248 | contains all necessary subprojects bundled, to make it easy to build offline and 249 | by verifying a single checksum. 250 | 251 | Here's a selection of changes included in the release: 252 | 253 | * Add sc-lwt and begin using lightweight threads. Plugins and other code can 254 | launch a background thread to do some I/O without blocking the main thread. 255 | * Add sqlite database integration, allowing state to be saved. We also add a DB 256 | API to simplify this. 257 | * Add curl integration which allows us to query APIs in plugins. 258 | * Add a super bare-bones Signal backend. This should be considered very alpha 259 | quality. 260 | 261 | 0.3.0 (2020-05-07) 262 | ------------------ 263 | 264 | * Skip 0.2.0 due to inconsistency between meson.build and CHANGELOG 265 | * Added dependency on sc-regex, migrated plugins onto it. 266 | * Added dependency on sc-collections, migrated log plugin onto it. 267 | * Added dependency on sc-argparse, migrated bot runner to use it. 268 | * Removed libstephen dependency from the project! 269 | * Updated plugin API: 270 | - Plugins may directly call functions listed in cbot.h, rather than using a 271 | function pointer. The function pointer struct is removed from the API. 272 | - Plugins no longer need to use the `addressed()` (or `cbot_addressed()`) 273 | function to determine whether a message is addressed to them. They may 274 | simply subscribe to the `CBOT_ADDRESSED` message type. 275 | - Plugins no longer need to compile and execute their own regular expressions. 276 | They may be provided within `cbot_register()` and will be executed by the 277 | bot itself. Handlers will be called only if the regular expression matches. 278 | * Added "become" plugin 279 | 280 | 0.1.0 (2020-02-05) 281 | ------------------ 282 | 283 | * Migrated build system to Meson, began versioning and recording changelog. 284 | * This release was labeled 0.2.0 in the meson.build file 285 | 286 | Prehistory 287 | ---------- 288 | 289 | Before this point, there are no changelogs, only git logs. At this point, the 290 | following was implemented: 291 | 292 | * IRC + CLI backends 293 | * Plugins: greet, emote, help, ircctl, karma, name, sadness, magic8 294 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Stephen Brennan 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cbot 2 | ==== 3 | 4 | [![builds.sr.ht status](https://builds.sr.ht/~brenns10/cbot/commits.svg)](https://builds.sr.ht/~brenns10/cbot/commits?) 5 | 6 | CBot is a chatbot written entirely in C! 7 | 8 | Why would you do that? Because I can. 9 | 10 | 11 | Support 12 | ------- 13 | 14 | CBot can be used with the following chat systems: 15 | 16 | 1. IRC 17 | 2. Signal (thanks to the Signald API bridge) 18 | 3. Your terminal (this one is pretty lonely) 19 | 20 | 21 | Abilities 22 | --------- 23 | 24 | These are some more interesting ones. 25 | 26 | - karma: Tracks the karma of various words. Uses persistent storage so you'll 27 | never forget all those times you said "cbot--" 28 | - know: You can tell cbot to "know" what things are and ask it later. EG "cbot 29 | know that Taylor Swift is awesome", and then "cbot what is Taylor Swift?" 30 | - weather: Request weather at any zip code 31 | - reply: a configurable plugin for triggering responses to messages matching 32 | regex. Think of this as Slackbot responses but much better. 33 | - name: Responds to questions about CBot with a link to the github. 34 | - greet: Say hi back to people, as well as greet when they enter a channel, and 35 | say bad things when they leave. (IRC only) 36 | - sadness: Responds to some forms of insult with odd comebacks. 37 | 38 | 39 | Features 40 | -------- 41 | 42 | * Both the chat backend (e.g. IRC or Signal) and the bot abilities (i.e. 43 | plugins) are modular. So it's easy (ish) to port CBot to Slack (pull requests 44 | welcome) or add a plugin that works on all of the above. 45 | * The bot and plugins can be configured via a 46 | [libconfig](http://hyperrealm.github.io/libconfig/) file, allowing for lots of 47 | flexibility. 48 | * Plugins can store data in a persistent sqlite database. CBot comes with a 49 | straightforward schema migration system, and a set of macros which can help 50 | write query functions. 51 | * The entire bot framework is based on a lightweight threading system and event 52 | loop. This allows asynchronous I/O code, such as HTTP requests, to 53 | cooperatively multitask with the IRC loop without launching true OS threads 54 | and dealing with concurrency. 55 | * Several small utility APIs exist to assist in writing plugins: 56 | - Dynamic arrays, hash table, linked list, string builder via `sc-collections` 57 | - Tokenizing API for turning messages into command arguments via a simple 58 | quoting system 59 | - Argument parsing API via `sc-argparse`, in case you want to go full UNIX 60 | - String templating/formatting API based on callbacks 61 | 62 | 63 | Build & Run 64 | ----------- 65 | 66 | See [doc/Install.md](doc/Install.md) for details on build / install. 67 | 68 | 69 | Plugin API 70 | ---------- 71 | 72 | See [doc/Plugins.md](doc/Plugins.md) for a details on plugin development. 73 | 74 | License 75 | ------- 76 | 77 | This project is under the Revised BSD license. Please see 78 | [`LICENSE.txt`](LICENSE.txt) for details. 79 | -------------------------------------------------------------------------------- /alpine.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | podman build -t cbottest ./alpinetest/ 3 | exec podman run --rm -it \ 4 | -v "$(pwd):$(pwd)" -w "$(pwd)" \ 5 | --detach-keys "ctrl-z,z" \ 6 | -e TZ=America/Los_Angeles \ 7 | cbottest \ 8 | /bin/sh -c "meson setup /tmp/build -Dwith_readline=true && ninja -C /tmp/build && /tmp/build/cbot $* || sh -i" 9 | -------------------------------------------------------------------------------- /alpinetest/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/alpine:3.18 2 | 3 | RUN apk add meson ninja gcompat tzdata msmtp perl curl meson git \ 4 | libc-dev sqlite-dev libconfig-dev curl-dev libmicrohttpd-dev \ 5 | openssl-dev libucontext-dev gcc readline-dev 6 | -------------------------------------------------------------------------------- /auth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | tail -n 1 $1 3 | tmp=$(mktemp) 4 | head -n -1 $1 > $tmp 5 | mv $tmp $1 6 | -------------------------------------------------------------------------------- /cbot1.0.md: -------------------------------------------------------------------------------- 1 | CBOT 1.0 Design 2 | =============== 3 | 4 | This is a rough sketch of what I'd like to achieve for the 1.0 release of CBot. 5 | 6 | Things that stay the same 7 | ------------------------- 8 | 9 | * Plugins are shared objects which get dynamically loaded. 10 | * Several "backends" exist, which actually drive the execution of the bot. 11 | * Main bot interface from the backend is `cbot_handle_event()`. 12 | 13 | Things that are different 14 | ------------------------- 15 | 16 | * No more function pointer API - just create a 'libcbot.so' and link the main 17 | function to it, as well as the plugins. 18 | 19 | * Responsibilities of the bot: 20 | 21 | - Allow plugins to subscribe to several event types: 22 | 23 | (regex event types) 24 | 25 | * channel message matching regex 26 | * channel message, addressed to bot, matching regex 27 | * action message, matching regex 28 | 29 | (passive event types) 30 | 31 | * channel message 32 | * action message 33 | 34 | (informative events) 35 | 36 | * user joins, parts, nick changes 37 | 38 | - Allow plugins to be un-registered! 39 | 40 | - Tracks users in each channel, allowing plugins to iterate over each user in 41 | the channel. 42 | 43 | - Provide plugins with a sqlite connection, and allow them to create tables if 44 | they do not exist during initialization, then use them to implement 45 | functionality. 46 | 47 | Sqlite schemas: 48 | 49 | CREATE TABLE user ( 50 | id INTEGER PRIMARY KEY ASC, 51 | nick TEXT UNIQUE, 52 | realname TEXT, 53 | host TEXT 54 | ); 55 | 56 | CREATE TABLE channel ( 57 | id INTEGER PRIMARY KEY ASC, 58 | name TEXT UNIQUE, 59 | topic TEXT, 60 | ); 61 | 62 | CREATE TABLE membership ( 63 | user_id INT NOT NULL, 64 | channel_id INT NOT NULL, 65 | UNIQUE(user_id, channel_id) 66 | ); 67 | 68 | Concrete changes: 69 | 70 | - [x] rename things to struct style naming 71 | - [x] get rid of unnecessary function pointer API 72 | - [x] draft new set of event types and new event structs 73 | - [x] draft user/channel APIs for plugins 74 | - [ ] make plugins return an int status 75 | - [ ] make plugins register a plugin object 76 | - [ ] make plugins define a cleanup function 77 | - [ ] implement user / channel APIs for plugins 78 | - [x] implement argument parsing library 79 | - [x] make plugin load list API not depend on libstephen 80 | -------------------------------------------------------------------------------- /compile_commands.json: -------------------------------------------------------------------------------- 1 | build/compile_commands.json -------------------------------------------------------------------------------- /deploy/.gitignore: -------------------------------------------------------------------------------- 1 | signald 2 | !run.sh 3 | -------------------------------------------------------------------------------- /deploy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.18 2 | COPY cbot.apk / 3 | COPY stephen@brennan.io.rsa.pub /etc/apk/keys/ 4 | RUN apk add ./cbot.apk gcompat tzdata msmtp perl curl && \ 5 | rm ./cbot.apk && \ 6 | adduser --disabled-password --uid 1002 cbot && \ 7 | mkdir /var/cores && chmod 777 /var/cores 8 | 9 | USER cbot 10 | WORKDIR /home/cbot 11 | ENV TZ=US/Pacific 12 | CMD ["/usr/bin/cbot" "/home/cbot/config/cbot.cfg"] 13 | -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | CBot Docker 2 | =========== 3 | 4 | This Dockerfile is the source of https://hub.docker.com/r/brenns10/cbot. 5 | 6 | In order to build it, you need two things: 7 | 8 | 1. CBot alpine package built and being served on localhost 9 | 2. Signald jars using Java 8, placed into `signald` directory 10 | -------------------------------------------------------------------------------- /deploy/alpine/APKBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Stephen Brennan 2 | pkgname=cbot 3 | pkgver=0.15.0 4 | pkgrel=0 5 | pkgdesc="IRC and Signal chatbot" 6 | url="https://github.com/brenns10/cbot" 7 | arch="all" 8 | license="Revised BSD" 9 | depends="sqlite libconfig libcurl libmicrohttpd openssl libucontext" 10 | makedepends="meson git libc-dev sqlite-dev libconfig-dev curl-dev libmicrohttpd-dev openssl-dev libucontext-dev" 11 | checkdepends="" 12 | install="" 13 | #subpackages="$pkgname-dev $pkgname-doc" 14 | source="$pkgname-$pkgver.tar.gz::https://github.com/brenns10/cbot/releases/download/v$pkgver/$pkgname-$pkgver.tar.gz" 15 | builddir="$srcdir/" 16 | options="!strip" 17 | 18 | build() { 19 | meson \ 20 | --prefix=/usr \ 21 | --sysconfdir=/etc \ 22 | --mandir=/usr/share/man \ 23 | --localstatedir=/var \ 24 | --buildtype=debugoptimized \ 25 | $pkgname-$pkgver output \ 26 | -Dtest=false \ 27 | -Ddefault_library=static 28 | # default_library=static: any dependency we pull in via subproject 29 | # should be build static and compiled into the binary 30 | meson compile ${JOBS:+-j ${JOBS}} -C output 31 | } 32 | 33 | check() { 34 | #meson test --no-rebuild -v -C output 35 | true 36 | } 37 | 38 | package() { 39 | DESTDIR="$pkgdir" meson install --no-rebuild -C output --skip-subproject 40 | } 41 | -------------------------------------------------------------------------------- /deploy/stephen@brennan.io.rsa.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/t0ZEIKAgloou3J3XLi 3 | b/nm6S2Me70dJNIzIC82E/fO5G/cgfCfl+weGR78mP7vmgL27HHPVilFQeahJ9Vu 4 | 0idMNTL8+f8GETiiMg+yONLnYJRlT3KEOYkcjoHep0usY9new30MCLRYlj8qwO5V 5 | I2IT2XQ/84LaYldaL4tu14tTEFCSJxd1iozDSB0OF+0ZjussYhiscCj4swk7T2hQ 6 | JG/xnkKKP8tKbbZgj+qFZK/zSqn8QTsM8oOZ74FC0w5vzEpw4aFBZM1L4wMjMSX+ 7 | ypxjPvQsFebMS68YfVKcbA4HMVU4ZuNp/gxWG0OlbpFYfzb21r3tNAE/eNjO+c9i 8 | TQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /doc/Backends.md: -------------------------------------------------------------------------------- 1 | # CBot Backends 2 | 3 | The backend is the part of CBot that allows the bot to communicate with whatever 4 | chat system it is using. Currently, that is IRC, Signal (via signald), or the 5 | console for testing. This page gives basic information about the backend 6 | responsibilities / APIs, and also information on selected backend 7 | implementations. 8 | 9 | ## APIs 10 | 11 | Backends must populate a `struct cbot_backend_ops` with function pointers to 12 | valid implementations. See the `cbot_private.h` header for details on these 13 | function pointers. 14 | 15 | ## Signald backend 16 | 17 | Communicates with [signald](https://signald.org/) over a JSON API on a Unix 18 | socket. Signald has some pretty decent documentation on the structures and 19 | formats of each request. 20 | 21 | Here are sample JSON requests: 22 | 23 | ### Send message 24 | 25 | ```json 26 | { 27 | "version": "v1", 28 | "type": "send", 29 | "id": "some string", 30 | "username": "+12345678901", 31 | "recipientGroupId": "group recipient ID", 32 | "recipientAddress": {"uuid": "string"}, 33 | "messageBody": "message text here", 34 | "mentions": [ 35 | {"length": 0, "start": 1, "uuid": "user ID"} 36 | ] 37 | } 38 | ``` 39 | 40 | * username - the sender (i.e. cbot) phone number. Maybe could become the signal 41 | username instead? 42 | * either recipientGroupId or recipientAddress is specified 43 | 44 | ### List Groups 45 | 46 | Request 47 | 48 | ```json 49 | { 50 | "version": "v1", 51 | "type": "list_groups", 52 | "account": "+12345678901" 53 | } 54 | ``` 55 | 56 | ### Get user profile 57 | 58 | ```json 59 | { 60 | "version": "v1", 61 | "type": "get_profile", 62 | "account": "+12345678901", 63 | "address": {"uuid": "foo", "number": "foo"} 64 | } 65 | ``` 66 | 67 | * address can specify either uuid or phone number 68 | 69 | ## Signal-CLI backend 70 | 71 | Communicates with [signal-cli](https://github.com/AsamK/signal-cli) over the 72 | jsonRpc API. Compared to signald, signal-cli has much less documentation, but it 73 | does seem that the maintenance has kept up with the signal protocol changes 74 | recently. As a result, signal-cli will be a newly added backend for Signal, in 75 | order to have more options for keeping bots running. 76 | 77 | Minimal API documentation 78 | [here](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-jsonrpc.5.adoc). 79 | 80 | All requests are JSON objects on one line, with the following keys: 81 | 82 | * jsonRpc: version as a string, currently "2.0" 83 | * method: name of the signal-cli command to execute, string 84 | * id: a string that identifies this request for the response 85 | * params: an object containing the request parameters, which are generally the 86 | same as the CLI options, taking each word and turning it into camelCase. 87 | 88 | To aid in development, here are sample JSON requests and responses. 89 | 90 | ### Incoming Message 91 | 92 | From docs: 93 | 94 | ```json 95 | { 96 | "jsonrpc": "2.0", 97 | "method": "receive", 98 | "params": { 99 | "envelope": { 100 | "source": "**REMOVED**", 101 | "sourceNumber": null, 102 | "sourceUuid": "**REMOVED**", 103 | "sourceName": "Stephen Brennan", 104 | "sourceDevice": 3, 105 | "timestamp": 1712876927992, 106 | "dataMessage": { 107 | "timestamp": 1712876927992, 108 | "message": "", 109 | "expiresInSeconds": 0, 110 | "viewOnce": false, 111 | "mentions": [ 112 | { 113 | "name": "**REMOVED**", 114 | "number": "**REMOVED**", 115 | "uuid": "**REMOVED**", 116 | "start": 0, 117 | "length": 1 118 | } 119 | ], 120 | "groupInfo": { 121 | "groupId": "** GROUP ID *", 122 | "type": "DELIVER" 123 | } 124 | } 125 | }, 126 | "account": "** REMOVED*" 127 | } 128 | } 129 | 130 | ``` 131 | 132 | ### Typing Started 133 | 134 | ``` 135 | { 136 | "jsonrpc": "2.0", 137 | "method": "receive", 138 | "params": { 139 | "envelope": { 140 | "source": "**REMVOED (UUID)**", 141 | "sourceNumber": null, 142 | "sourceUuid": "**REMOVED (UUID)**", 143 | "sourceName": "Stephen Brennan", 144 | "sourceDevice": 3, 145 | "timestamp": 1712857247046, 146 | "typingMessage": { 147 | "action": "STARTED", 148 | "timestamp": 1712857247046, 149 | "groupId": "**REMOVED**" 150 | } 151 | }, 152 | "account": "**REMOVED (bot phone number)**" 153 | } 154 | } 155 | ``` 156 | 157 | ### Reaction Add 158 | 159 | ```json 160 | { 161 | "jsonrpc": "2.0", 162 | "method": "receive", 163 | "params": { 164 | "envelope": { 165 | "source": "**REMOVED (UUID of sender)**", 166 | "sourceNumber": null, 167 | "sourceUuid": "**REMOVED (UUID of sender)**", 168 | "sourceName": "Stephen Brennan", 169 | "sourceDevice": 3, 170 | "timestamp": 1712857469462, 171 | "dataMessage": { 172 | "timestamp": 1712857469462, 173 | "message": null, 174 | "expiresInSeconds": 0, 175 | "viewOnce": false, 176 | "reaction": { 177 | "emoji": "❤️ ", 178 | "targetAuthor": "**REMOVED (UUID of author of reacted message)**", 179 | "targetAuthorNumber": null, 180 | "targetAuthorUuid": "**REMOVED**", 181 | "targetSentTimestamp": 1712857247773, 182 | "isRemove": false 183 | }, 184 | "groupInfo": { 185 | "groupId": "**REMOVED**", 186 | "type": "DELIVER" 187 | } 188 | } 189 | }, 190 | "account": "**REMOVED**" 191 | } 192 | } 193 | ``` 194 | 195 | * For removals, set "isRemove" to true! 196 | 197 | ### Group Updates 198 | 199 | It looks like any update to the group will trigger a message like this. 200 | 201 | - User joins/leaves 202 | - Update group name, picture, link 203 | 204 | ```json 205 | { 206 | "jsonrpc": "2.0", 207 | "method": "receive", 208 | "params": { 209 | "envelope": { 210 | "source": "**REMOVED**", 211 | "sourceNumber": null, 212 | "sourceUuid": "**REMOVED**", 213 | "sourceName": "Stephen Brennan", 214 | "sourceDevice": 3, 215 | "timestamp": 1712870390764, 216 | "dataMessage": { 217 | "timestamp": 1712870390764, 218 | "message": null, 219 | "expiresInSeconds": 0, 220 | "viewOnce": false, 221 | "groupInfo": { 222 | "groupId": "**REMOVED**", 223 | "type": "UPDATE" 224 | } 225 | } 226 | }, 227 | "account": "**Removed**" 228 | } 229 | } 230 | ``` 231 | 232 | It would appear this message doesn't actually contain the group info, and you'll 233 | need to query it. 234 | 235 | ### List Groups 236 | 237 | Request: 238 | 239 | ```json 240 | { 241 | "jsonrpc": "2.0", 242 | "method": "listGroups", 243 | "id": "my special mark" 244 | } 245 | ``` 246 | 247 | Response: 248 | 249 | ```json 250 | { 251 | "jsonrpc": "2.0", 252 | "result": [ 253 | { 254 | "id": "* id **", 255 | "name": "* name **", 256 | "description": "* text desciption *", 257 | "isMember": true, 258 | "isBlocked": false, 259 | "messageExpirationTime": 0, 260 | "members": [ 261 | { 262 | "number": null, 263 | "uuid": "* uuid *" 264 | } 265 | ], 266 | "pendingMembers": [], 267 | "requestingMembers": [], 268 | "admins": [ 269 | { 270 | "number": null, 271 | "uuid": "* uuid *" 272 | } 273 | ], 274 | "banned": [], 275 | "permissionAddMember": "EVERY_MEMBER", 276 | "permissionEditDetails": "EVERY_MEMBER", 277 | "permissionSendMessage": "EVERY_MEMBER", 278 | "groupInviteLink": "https://signal.group/#foo" 279 | }, 280 | { 281 | "id": "* id **", 282 | "name": null, 283 | "description": null, 284 | "isMember": false, 285 | "isBlocked": false, 286 | "messageExpirationTime": 0, 287 | "members": [], 288 | "pendingMembers": [], 289 | "requestingMembers": [], 290 | "admins": [], 291 | "banned": [], 292 | "permissionAddMember": "EVERY_MEMBER", 293 | "permissionEditDetails": "EVERY_MEMBER", 294 | "permissionSendMessage": "EVERY_MEMBER", 295 | "groupInviteLink": null 296 | } 297 | ], 298 | "id": "my special mark" 299 | } 300 | ``` 301 | 302 | ### List Contacts 303 | 304 | Request: 305 | 306 | ```json 307 | {"jsonrpc": "2.0", "method": "listContacts", "id": "1"} 308 | ``` 309 | 310 | Response: 311 | 312 | ```json 313 | { 314 | "jsonrpc": "2.0", 315 | "result": [ 316 | { 317 | "number": null, 318 | "uuid": "* UUID **", 319 | "username": null, 320 | "name": "", 321 | "color": null, 322 | "isBlocked": false, 323 | "messageExpirationTime": 0, 324 | "profile": { 325 | "lastUpdateTimestamp": 1712856260058, 326 | "givenName": "NAME HERE", 327 | "familyName": null, 328 | "about": "", 329 | "aboutEmoji": "", 330 | "mobileCoinAddress": null 331 | } 332 | }, 333 | { 334 | "number": null, 335 | "uuid": "* UUID **", 336 | "username": null, 337 | "name": "", 338 | "color": null, 339 | "isBlocked": false, 340 | "messageExpirationTime": 0, 341 | "profile": { 342 | "lastUpdateTimestamp": 1712856468855, 343 | "givenName": "Stephen", 344 | "familyName": "Brennan", 345 | "about": "", 346 | "aboutEmoji": "", 347 | "mobileCoinAddress": null 348 | } 349 | } 350 | ], 351 | "id": "1" 352 | } 353 | ``` 354 | 355 | ### Send Message 356 | 357 | ```json 358 | {"jsonrpc": "2.0", "method": "send", "id": "1", "params": {"message": "hello 💩💩 hello X!", "groupId": "GROUPID", "mentions": ["15:1:UID"]}} 359 | 360 | {"jsonrpc": "2.0", "method": "send", "id": "1", "params": {"message": "hello world", "recipient": "UID"}} 361 | ``` 362 | 363 | ### Success 364 | 365 | ```json 366 | { 367 | "jsonrpc": "2.0", 368 | "result": { 369 | "results": [ 370 | { 371 | "recipientAddress": { 372 | "uuid": "UUID", 373 | "number": null 374 | }, 375 | "type": "SUCCESS" 376 | } 377 | ], 378 | "timestamp": 1713212326970 379 | }, 380 | "id": "1" 381 | } 382 | ``` 383 | 384 | ### Set Nick 385 | 386 | ```json 387 | {"jsonrpc": "2.0", "method": "updateProfile", "id": "1", "params": {"givenName": "cbottest"}} 388 | ``` 389 | -------------------------------------------------------------------------------- /doc/Plugins-2.md: -------------------------------------------------------------------------------- 1 | Plugins - Advanced Topics 2 | ========================= 3 | 4 | This document is not particularly detailed. It's intended to give you an idea 5 | for the tools that CBot contains to help you write plugins. Rather than give 6 | detailed code examples, I'll point to plugins you should read to learn more. 7 | 8 | APIs To Learn 9 | ------------- 10 | 11 | Check out the following libraries (browse the 12 | [source](https://sr.ht/~brenns10/sc-libs) and 13 | [docs](https://brenns10.srht.site/sc-libs/)) for APIs commonly used in CBot: 14 | 15 | * [sc-collections](https://brenns10.srht.site/sc-libs/sc-collections/): we 16 | heavily use the linked list and character buffer. 17 | * [sc-regex](https://brenns10.srht.site/sc-libs/sc-regex/): CBot is built on 18 | plugins that match events against regular expressions. 19 | * [sc-lwt](https://brenns10.srht.site/sc-libs/sc-lwt/): CBot uses a lightweight 20 | thread solution to do lots of I/O heavy "threads". 21 | 22 | Persistent Storage: Sqlite Database 23 | ----------------------------------- 24 | 25 | CBot links to Sqlite and maintains a persistent database file. Plugins are 26 | welcome to use it to provide persistence. CBot provides a couple convenience 27 | tools, but the authoritative source is the [sqlite docs][1]. 28 | 29 | [1]: https://sqlite.org/docs.html 30 | 31 | The following plugins make use of the database and would be a good reference for 32 | the tools documented below: 33 | 34 | - `plugin/sqlkarma.c` 35 | - `plugin/sqlknow.c` 36 | 37 | ### Table registration and migration 38 | 39 | A plugin should "register" a table with a `struct cbot_db_table`. Why? The 40 | struct contains a version field, and CBot maintains a schema version table. If 41 | you decide to change the schema for a table, you just increment the version, and 42 | the bot will know the table in its database is out of date. Then, it will look 43 | into an array of `ALTER TABLE` statements you provide, which will be able to 44 | migrate the old schema to the new one. 45 | 46 | ### Query functions 47 | 48 | Running queries in sqlite is a bit tedious, so CBot contains a macro system to 49 | allow you to generate functions which execute a query and return a result. I 50 | won't bother to go into many details, just read `inc/cbot/db.h`. 51 | 52 | Lightweight Threads 53 | ------------------- 54 | 55 | It may not be obvious, but CBot is actually an "async" program, which makes use 56 | of a pretty awesome lightweight threading model. There's only one "operating 57 | system thread", but CBot itself can switch between several "lightweight threads" 58 | (which I refer to as lwts in code/docs) whenever they have no work to be done. 59 | This is cooperative multitasking. 60 | 61 | The IRC operations and plugins execute all one one lightweight thread. 62 | Typically, that thread is blocked, waiting for I/O from IRC, so there's plenty 63 | of opportunity for other lwts to run. 64 | 65 | Plugins can use `cbot_get_lwt_ctx()` to retrieve a context object for use with 66 | the sc-lwt library, and then they can go ahead and launch lightweight threads. 67 | Note that plugins should behave well and not hog the CPU. This is mainly meant 68 | for other I/O operations. 69 | 70 | Note that if a plugin would like to call ANY function which would block (such as 71 | libcurl) then it should make sure that it is async-safe and integrated with 72 | sc-lwt, and they need to do it off the main lwt. 73 | 74 | A great basic lwt example is `plugin/annoy.c`. 75 | 76 | HTTP Requests: libcurl 77 | ---------------------- 78 | 79 | [Docs](https://curl.se/libcurl/c/) 80 | 81 | libcurl is an excellent HTTP library! It is integrated with CBot now so you can 82 | use it to make plugins which connect to APIs etc. CBot uses the async mode of 83 | libcurl (the Multi API) along with sc-lwt to be able to write code which can run 84 | several HTTP requests in parallel, all on the same OS-thread (different LWTs) 85 | without writing tons of callbacks. 86 | 87 | * In order to use libcurl, you MUST create a separate lwt and do all blocking 88 | actions on that thread. 89 | * Create an "easy handle" (see [cURL easy overview][2]) for your request, but 90 | rather than using `curl_easy_perform()` use `cbot_curl_perform()` (see 91 | `inc/cbot/curl.h`). 92 | 93 | [2]: https://curl.se/libcurl/c/libcurl-easy.html 94 | 95 | Check out `plugin/weather.c` for a curl example. 96 | 97 | Tokenizing 98 | ---------- 99 | 100 | Sometimes you just want to write a command that takes, you know, an array of 101 | arguments. Tokenizing in C can be a drag. CBot has a tokenizer that takes a 102 | string, and gives you an array of tokens which were delimited by whitespace. The 103 | tokenizer supports basic quoting to allow whitespace in your arguments, and 104 | that's about it. 105 | 106 | Check out `plugin/tok.c` to see the tokenizer in action. 107 | It's implemented in `src/tok.c`, which is worth reading. 108 | 109 | Dynamic Formatting 110 | ------------------ 111 | 112 | Love Python format() and f-strings? Well, C won't get you anything nearly as 113 | convenient, but this is a good 70%. It's based on the charbuf struct in 114 | sc-collections. It allows you to give a string like this: 115 | 116 | "Hello {target}, my name is {myname}! Check out {url}" 117 | 118 | And fill in the curly braces with the correct values. This isn't generally all 119 | that useful in C, because you usually can stick with the printf family. But 120 | sometimes you don't know the format string or the order of the items, or even 121 | what you want to include in your string output. 122 | 123 | The `cbot_format()` API takes a formatter function, which gets called for each 124 | curly brace expansion and can append whatever it wants into the string builder / 125 | charbuf. 126 | 127 | See it in action in `plugin/reply.c`. 128 | -------------------------------------------------------------------------------- /doc/Plugins.md: -------------------------------------------------------------------------------- 1 | How to write a CBot Plugin 2 | ========================== 3 | 4 | CBot has a quite pleasant plugin system. If you're familiar with C, you're able 5 | to extend the bot quite a bit. Follow this guide to learn the basics of CBot 6 | plugin development. 7 | 8 | To drive this tutorial, we will try to build a "hello world" plugin. This plugin 9 | will simply reply to any message which says "hello" and respond "world". While 10 | the functionality itself is pretty silly, once you can do this, you'll be able 11 | to start making much more interesting stuff. 12 | 13 | As a prerequisite, this guide assumes you are able to use the build system to 14 | compile CBot. 15 | 16 | Step 1: Plugin structure 17 | ------------------------ 18 | 19 | The best way to write a CBot plugin is "in-tree", that is by writing it within 20 | this repository. Just create a file "plugin/hello.c". 21 | 22 | **Aside:** whatever name you choose for your plugin (not including the `.c` file 23 | extension) should be a valid C identifier, more or less. This will allow it to 24 | be included in the configuration file. 25 | 26 | Here is a completely blank, minimal plugin, which does nothing: 27 | 28 | #include "cbot/cbot.h" 29 | 30 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 31 | { 32 | return 0; 33 | } 34 | 35 | struct cbot_plugin_ops ops = { 36 | .load=load, 37 | }; 38 | 39 | Save that as your plugin file, and then add a your plugin name to the `plugins` 40 | list in the `meson.build` file at the root of this repository. When you 41 | recompile (`ninja -C build`), the build system will detect a change and 42 | reconfigure itself. Then it should compile everything, along with your new 43 | plugin. You'll find the compiled plugin at `build/hello.so`. 44 | 45 | Step 2: ops Struct and load() 46 | ----------------------------- 47 | 48 | The above plugin file contained an empty function and a struct declaration. 49 | These are the minimal items for a plugin. When CBot loads your plugin, it 50 | searches for a symbol named `ops`, and expects it to be of type `struct 51 | cbot_plugin_ops`. You can see the definition of this type in `inc/cbot/cbot.h`, 52 | but here it is with docstrings removed: 53 | 54 | struct cbot_plugin_ops { 55 | char *description; 56 | int (*load)(struct cbot_plugin *plugin, config_setting_t *config); 57 | void (*unload)(struct cbot_plugin *plugin); 58 | void (*help)(struct cbot_plugin *plugin, struct sc_charbuf *cb); 59 | }; 60 | 61 | All of the fields are optional, except for `load`, which is the one we'll focus 62 | on right now. This function should do any initialization tasks for the plugin. 63 | This is the place where a plugin should tell the bot that it would like to 64 | respond to certain types of messages or events. *If you don't register any 65 | handlers for events here, the plugin will never get called again.* So, our empty 66 | load() function above is a bit silly, but it's perfectly valid. The plugin 67 | simply does nothing. 68 | 69 | There are two important arguments to the load() function: a `struct cbot_plugin` 70 | pointer, and a `config_setting_t` pointer. The plugin struct looks like this: 71 | 72 | struct cbot_plugin { 73 | struct cbot_plugin_ops *ops; 74 | void *data; 75 | struct cbot *bot; 76 | }; 77 | 78 | An instance of this struct is allocated for each instance of the plugin which is 79 | loaded. The `ops` pointer should simply point to the variable defined at the 80 | bottom of the file. `data` will be NULL, but can be used by the plugin to store 81 | data for later. Finally, the `bot` pointer points to the main struct of `cbot`, 82 | which is used for most bot actions like sending messages. 83 | 84 | The plugin may modify any field of the plugin struct as it wants. CBot will use 85 | the `ops` that are currently in the plugin struct, so if the bot would like to 86 | dynamically change behavior, that's ok. CBot does not rely on the `bot` pointer 87 | within the plugin, but it's in a plugin's best interest to leave that alone. If 88 | a plugin overwrites it, calling API functions will become much more difficult. 89 | 90 | The second argument is a `config_setting_t *`, which is an element from 91 | [libconfig](http://hyperrealm.github.io/libconfig/). I won't cover that at all, 92 | except to mention that this points to plugin-specific configuration, and can be 93 | accessed using any libconfig function you want. 94 | 95 | Step 3: Registering handlers 96 | ---------------------------- 97 | 98 | Now that we know what load() is, let's use it to register a "handler". CBot lets 99 | us handle a few different types of events (see `enum cbot_event_type`) but the 100 | most important two are `CBOT_MESSAGE` and `CBOT_ADDRESSED`. `CBOT_MESSAGE` is an 101 | event which triggers on every single message: direct message, channel message, 102 | etc. `CBOT_ADDRESSED` is a special event which only triggers on a subset of 103 | messages: those which are "addressed" to the bot. Here are some examples of 104 | "addressed" messages: 105 | 106 | * Message in a channel: `cbot: hello!` 107 | * Message in a channel: `cbot hello!` 108 | * Direct message to cbot: `hello!` 109 | 110 | All three of the above messages would generate the same `CBOT_ADDRESSED` event, 111 | and the contents of the message would be trimmed to remove the bot's name. This 112 | allows plugin authors to respond to messages directed at the bot, without 113 | knowing about the bot's current username, and without having to filter out 114 | irrelevant messages. 115 | 116 | In our case, we want to respond to any message which says "hello" -- not just 117 | those which are addressed to us. So, we should register a handler for 118 | `CBOT_MESSAGE`. 119 | 120 | For message events, CBot lets you specify a regular expression to filter 121 | messages further by their contents. Your handler will only be called if the full 122 | contents of the message matches this regex. We will simply specify a regex of 123 | `hello` to make our plugin nice and easy. 124 | 125 | A handler function, to CBot, is a function which takes a two arguments: 126 | 127 | - `struct cbot_event *event`: this is a general purpose event struct. Different 128 | types of events have more specialized versions with different fields. The 129 | event struct has a `bot` field, a `plugin` field, and a `type` field which 130 | tells you the event type. For message events, you can cast this to a `struct 131 | cbot_message_event` which further contains strings `channel`, `username`, 132 | `message`, and some other fields we won't discuss. 133 | - `void *user`: this is given to CBOT when you register the handler. It can be 134 | used to store handler-specific data. 135 | 136 | With this information, the handler registration function should be clear: 137 | 138 | struct cbot_handler *cbot_register(struct cbot_plugin *plugin, 139 | enum cbot_event_type type, 140 | cbot_handler_t handler, void *user, 141 | char *regex); 142 | 143 | The first argument is the plugin doing the registration. type is the event type, 144 | handler is the function to be called, user is the user data, and regex is the 145 | (optional) regex to filter messages by. Let's put this together and register a 146 | hypothetical handler: 147 | 148 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 149 | { 150 | cbot_register(plugin, CBOT_MESSAGE, (cbot_handler_t)say_hello, NULL, 151 | "hello"); 152 | return 0; 153 | } 154 | 155 | Step 4: Handler function 156 | ------------------------ 157 | 158 | Finally, let's look at the handler function for our plugin: 159 | 160 | static void say_hello(struct cbot_message_event *event, void *user) 161 | { 162 | cbot_send(event->bot, event->channel, "world"); 163 | } 164 | 165 | First, note that our first argument is not `struct cbot_event`, it is `struct 166 | cbot_message_event`. We can do this because our handler only handles messages, 167 | but the downside is that we had to cast this function to `(cbot_handler_t)` 168 | when it was registered above. 169 | 170 | The remainder of this function seems pretty obvious: send the message "world" to 171 | the channel in which the original message came in from. 172 | 173 | Step 5: Compile and run 174 | ----------------------- 175 | 176 | Now that we have a complete plugin, we should compile it and run it. Use `ninja 177 | -C build` to compile the plugin. Running it is as simple as adding a line in the 178 | `plugins` group of your config file: 179 | 180 | hello: {}; 181 | 182 | Notice the empty `{}` group -- any configuration inside of this would go 183 | directly to the `conf` variable in the `load()` function -- neat, right? 184 | 185 | Anyway, if you don't have a configuration yet, this would be a good baseline to 186 | start testing outside of IRC. Save it as `cli.cfg`. You can add more plugins 187 | too, if you so desire. 188 | 189 | cbot: { 190 | name = "cbot"; 191 | channels = ( 192 | { name = "stdin" }, 193 | ); 194 | backend = "cli"; 195 | plugin_dir = "build"; 196 | db = "cli.sqlite3"; 197 | }; 198 | cli: {}; 199 | plugins: { 200 | hello: {}; 201 | }; 202 | 203 | After compiling, run `build/cbot cli.cfg` and you should have a CLI chat with 204 | cbot in it. If you see the following message (with no errors after it), then you 205 | know your plugin has been loaded: 206 | 207 | attempting to load symbol ops from build/hello.so 208 | 209 | You should be able to type "hello" and get a response from cbot: 210 | 211 | > hello 212 | [stdin]cbot: world 213 | > 214 | 215 | And that's it, you've written your first plugin! 216 | 217 | Next up, take a browse through [Plugins-2.md](Plugins-2.md) to learn some 218 | advanced topics and APIs which help you make great plugins. 219 | -------------------------------------------------------------------------------- /doc/Tooling.md: -------------------------------------------------------------------------------- 1 | Dev Tooling 2 | =========== 3 | 4 | ## Language Server (ccls or clangd work well) 5 | 6 | If you use an editor with language server support, (e.g. vim, VSCode), then the 7 | `compile_commands.json` file generated by meson (and symlinked at the root) will 8 | be very useful to you. It should enable completion, jump to definition, etc. 9 | 10 | It really makes it feel like you're programming in 2020. 11 | 12 | ## Commit Hooks 13 | 14 | This repo uses [pre-commit](https://pre-commit.com/) to automatically run a code 15 | formatter. Be sure to install the pre-commit tool, and then use 16 | `pre-commit install` to add the hooks to your git checkout. 17 | 18 | ## Updating Libraries 19 | 20 | Use `meson subprojects update` to update the version of git dependencies (like 21 | the sc-libs). If you need a new feature, be sure to pin a minimum version in the 22 | `meson.build` file. 23 | -------------------------------------------------------------------------------- /doc/User.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | So you're in a chat with CBot. What can it do? Maybe you've been brought here by 4 | the "cbot help" command. This document explains what you need to know as a user, 5 | including the commands you might be able to use, as well as general guidance. 6 | 7 | ## Talking to CBot 8 | 9 | CBot listens to every message sent in a channel/group, as well as all direct 10 | messages. However, most commands require that you send a message "to" CBot, 11 | either by starting off the message with its name (e.g. "cbot help") or by 12 | tagging the bot using the messaging platform (e.g. "@cbot help"). If you send a 13 | direct message to CBot, then you don't need to prefix it with the bot name to 14 | trigger a command. 15 | 16 | ## Plugins 17 | 18 | There are a lot of plugins implemented. But not every plugin gets enabled when 19 | CBut runs: the exact list of plugins depends on the configuration used. Ask the 20 | person who runs your CBot instance if you're curious about the details of your 21 | configuration. 22 | 23 | This section will be divided into two groups: plugins that are commonly enabled, 24 | and then plugins that are commonly disabled. 25 | 26 | ### Commonly Enabled Plugins 27 | 28 | #### aqi 29 | 30 | ``` 31 | me> cbot aqi 32 | cbot> AQI: 30 33 | ``` 34 | 35 | This plugin looks up the current AQI in San Francisco. 36 | 37 | #### birthday 38 | 39 | ``` 40 | me> cbot birthday add 7/4 America 41 | cbot> Ok, I will wish "America" happy birthday on 7/4 42 | 43 | me> cbot birthday list 44 | cbot> All birthdays 45 | 3/14: Pi 46 | 3/17: Saint Patrick 47 | 7/4: America 48 | 12/25: Jesus 49 | 50 | me> cbot birthday remove Pi 51 | cbot> Deleted 1 birthday records 52 | ``` 53 | 54 | This plugin allows you to add, list, and remove birthdays. At a configured time 55 | (likely 9am Pacific), the bot will wish the person a happy birthday in a 56 | configured channel/group. 57 | 58 | On the last day of the month, the plugin will also send a listing of next 59 | month's birthdays. 60 | 61 | #### greet 62 | 63 | ``` 64 | me> hello cbot 65 | cbot> hello, @me! 66 | ``` 67 | 68 | It just responds to "hello" messages. 69 | 70 | #### help 71 | 72 | ``` 73 | me> cbot help 74 | cbot> Please see CBot's user documentation at http://brenns10.github.io/cbot/User.html 75 | ``` 76 | 77 | Takes you... here. 78 | 79 | #### karma 80 | 81 | ``` 82 | me> Stephen is so good at making plugins 83 | me> Stephen++ 84 | me> cbot karma Stephen 85 | cbot> Stephen has 1 karma 86 | me> Only one karma? Stephen-- 87 | me> cbot karma Stephen 88 | cbot> Stephen has 0 karma 89 | ``` 90 | 91 | Use `++` to add one to a word's karma, and `--` to subtract. Query the karma of 92 | a particular word, or use `cbot karma` to see the highest. 93 | 94 | #### reply 95 | 96 | This is a generic plugin, so it's difficult to give specific commands. 97 | Basically, you can configure CBot to choose a random response to a message which 98 | matches a pattern. If you've used Slackbot responses before, it's like that. 99 | Unfortunately, this means that there's no standard set of responses: it's all up 100 | to the configuration. 101 | 102 | That said, here's a few common ones that I usually configure: 103 | 104 | - good morning/evening/afternoon/night -> good X 105 | - lod X -> X: ಠ_ಠ 106 | - ping -> pong 107 | - magic8 -> Magic 8 ball response 108 | - Alternative syntax is `!8ball` 109 | - cbot sucks (and other variants) -> stupid replies 110 | 111 | #### know 112 | 113 | ``` 114 | me> cbot know that Taylor Swift is awesome 115 | cbot> ok, Taylor Swift is awesome 116 | me> cbot what is Taylor Swift 117 | cbot> Taylor Swift is awesome 118 | ``` 119 | 120 | #### weather 121 | 122 | ``` 123 | me> cbot weather 94105 124 | cbot> 94105: 🌦 🌡️+48°F 🌬️→19mph 125 | ``` 126 | 127 | Given a ZIP code or other location, replies with an emoji-rich weather report on 128 | the current conditions. Straight from [wttr.in](http://wttr.in). 129 | 130 | #### trivia 131 | 132 | If you have a weekly trivia, your CBot admin can configure a reminder message. 133 | You can react to the reminder message to RSVP. At a specified time, the reminder 134 | message gets sent out to the trivia host, so that you can get a table reserved. 135 | 136 | Your reactions can be: 137 | 138 | - 😥, 😢, 😭 - explicitly RSVP no 139 | - 1️⃣ ... 9️⃣ - RSVP yes with the given number of guests 140 | - Any other emoji - RSVP yes with no guest, your emoji choice gets sent to the 141 | host for fun! 142 | 143 | #### sports_schedule 144 | 145 | This plugin can let you know about any professional sports games in town on a 146 | particular day, which may gum up traffic or otherwise cause issues. Use: 147 | 148 | - `cbot games` or `cbot games today` - check for Giants & Warriors games today 149 | - `cbot games tomorrow` - check tomorrow 150 | - `cbot games YYYY-MM-DD` - check a specific day 151 | 152 | ### Plugins Not Frequently Used 153 | 154 | - annoy: `cbot annoy ` sends a message to user every few seconds. It's 155 | actually mostly a cool demonstration of how to use some technical features of 156 | the plugin code -- not a good plugin though! 157 | - become: `cbot become ` tells cbot to change username. This only 158 | works for IRC. It's not really a good power to have sitting around though. 159 | Best to keep disabled. 160 | - emote: `cbot emote `. An uninteresting command that just has 161 | CBot repeat a message using the `/me` syntax of IRC 162 | - ircctl: Allows you to instruct CBot to perform IRC-specific actions. Intended 163 | to allow a user to give CBot higher privileges in the channel, and then 164 | instruct it to do commands an their behalf. 165 | - log: Supposedly logs channel messages to a file. I haven't used this one in 166 | years (promise!). Not sure whether it works. 167 | - who: Lists every user in a channel. Not generally good because it sends 168 | notifications to everyone. It's a useful demonstration of CBot's features to 169 | list users in a channel though. 170 | 171 | There are a few other plugins with abilities even more mundane than the above. 172 | They're just not that interesting. 173 | -------------------------------------------------------------------------------- /doc/admin.rst: -------------------------------------------------------------------------------- 1 | Administering CBot 2 | ================== 3 | 4 | Installing (Distribution Packages) 5 | ---------------------------------- 6 | 7 | CBot has source releases on `Github `_ as well 8 | as binary package releases for Alpine Linux. You can install the Alpine packages 9 | with ``apk add --allow-untrusted ``. For other Linux distributions, you'll 10 | need to install from source. 11 | 12 | Installing (Docker) 13 | ------------------- 14 | 15 | CBot also has Docker images published on `Docker Hub 16 | `_. Since version v0.8.0, these Docker 17 | images no longer contain Signald bundled. If you want to run CBot with Signal, 18 | you'll need to setup Signald yourself, likely via their Docker image. 19 | Docker-Compose is great for this. 20 | 21 | Installing (From Source) 22 | ------------------------ 23 | 24 | Dependencies 25 | ^^^^^^^^^^^^ 26 | 27 | First, you'll need to unpack your source release (or checkout a git revision of 28 | interest). Next, setup your dependencies. The following are dependencies of 29 | CBot. 30 | 31 | - meson: this is the build system for CBot, it is required (usually a package 32 | named ``meson``) 33 | - A C compiler (gcc or clang) 34 | - libconfig: This is used to parse the configuration file. It is required. 35 | - libcurl: For HTTP queries to APIs. It is required. 36 | - libmicrohttpd: For HTTP server. It is required 37 | - libircclient: This is for IRC support. It is required, but we can use a 38 | "vendored" version if your OS doesn't package it. 39 | - sqlite3: Used for maintaining state. It is required, but we can use a vendored 40 | version as necessary. 41 | - Several libraries from `sc-libs `_, these 42 | can all be setup by the build system, as they have no OS packaging. 43 | 44 | Here are some commands to get setup for various distributions: 45 | 46 | .. code:: bash 47 | 48 | # Ubuntu 49 | sudo apt install meson gcc libconfig-dev libcurl4-openssl-dev libsqlite3-dev libmicrohttpd-dev 50 | 51 | # Arch 52 | sudo pacman -Sy meson gcc libconfig curl sqlite libmicrohttpd 53 | 54 | Building 55 | ^^^^^^^^ 56 | 57 | Once you have setup the dependencies, you'll need to build the project: 58 | 59 | This project uses the Meson build system. The following commands will build the 60 | project: 61 | 62 | .. code:: bash 63 | 64 | # creates a build directory named "build" 65 | meson build 66 | # change to "build" and compile everything 67 | ninja -C build 68 | 69 | During the ``meson build`` step, the build system will download any dependencies 70 | that it cannot find installed on your system, including all of the sc-libs 71 | dependencies. 72 | 73 | During ``ninja -C build``, the actual program compilation happens. It should be 74 | snappy. 75 | 76 | Initial Run 77 | ^^^^^^^^^^^ 78 | 79 | To test your built program, you can use ``sample.cfg``, which sets up some 80 | plugins in the CLI. Simply run: 81 | 82 | .. code:: bash 83 | 84 | build/cbot sample.cfg 85 | 86 | Configuring 87 | ----------- 88 | 89 | A sample configuration file is bundled as ``sample.cfg``. This should be used as 90 | the basis for your configuration. However, there are some very important things 91 | which ought to be documented. 92 | 93 | Choosing & Configuring Backend 94 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 95 | 96 | In the ``cbot`` configuration object, you should find an option ``backend``. 97 | This is required, and you can choose between: 98 | 99 | - ``"irc"`` 100 | - ``"signal"`` 101 | - ``"cli"`` 102 | 103 | Whichever backend you choose, you must also have a corresponding top-level 104 | configuration object with the same name. This object provides necessary config 105 | for the backend. 106 | 107 | For the irc backend, you need to configure: 108 | 109 | - host: string hostname (use "#" prefix for SSL) 110 | - port: integer port 111 | - password: string password 112 | 113 | For the signal backend, you must run an instance of `Signald 114 | `_. The following configuration is avaialable: 115 | 116 | - phone: phone number of bot account 117 | - signald_socket: filename to connect to Signald 118 | - auth: authorized phone number of privileged account 119 | - ignore_dm: should the bot ignore DMs? 120 | 121 | Configuring CBot 122 | ^^^^^^^^^^^^^^^^ 123 | 124 | Beyond the backend configuration, the ``plugins`` top-level configuration option 125 | specifies each plugin which is loaded. Each plugin named in this mapping will be 126 | loaded. The plugin can be mapped to an empty config dict for plugins with no 127 | options, for example: 128 | 129 | .. code:: 130 | 131 | help: {}; 132 | 133 | If the plugin requires configuration, it can be provided in these objects. 134 | Please see ``sample.cfg`` for examples of each plugin's configuration. 135 | 136 | Plugins 137 | ------- 138 | 139 | This section documents the administration side of some plugins, in case they 140 | need a bit more explanation. 141 | 142 | Trivia 143 | ^^^^^^ 144 | 145 | The trivia plugin is a powerful weekly reminder system for the group chat. 146 | Basically, every week at a given time, a message gets sent announcing the trivia 147 | night and requesting RSVP. Then, at the RSVP deadline, a message gets sent to 148 | the trivia host to RSVP a table. 149 | 150 | In order to RSVP, users react with any emoji, except for a few sad face emoji 151 | choices. If they react with a numeric emoji, that indicates bringing a guest. 152 | 153 | While it's definitely nice to RSVP and reserve a table, the main benefit is 154 | actually for reminding everybody that it's trivia day, and letting everybody see 155 | who else will be joining, without needing to fill the chat with lots of 156 | notifications. 157 | 158 | The RSVP message can be sent to trivia host in one of two ways: 159 | 160 | 1. Email 161 | 2. SMS 162 | 163 | Strangely, signal is not an option here (but it could be easily added, if it 164 | were needed). The email option was the original, but the SMS option can be 165 | configured with "ntfy.sh" and the android app Tasker to automate the SMS 166 | sending. 167 | 168 | Configurations: 169 | 170 | - channel: set the channel where trivia is organized (required) 171 | - sendmail_command: set the command to run to send email (or SMS). This command 172 | is executed via ``popen()``, which means that it is passed to the system 173 | shell, and the message text is written into the command via stdin. 174 | 175 | - A configuration like ``msmtp -t recipient@example.com`` works well for Email 176 | based sending. 177 | - A configuration like ``perl -pe 's/\n/\r\n' - | curl --data-binary @- https://ntfy.sh/TOPIC`` 178 | works for SMS. You'll then need to configure ntfy.sh and the Android Tasker 179 | app as seen `here `_. 180 | 181 | - email_format: set this to false when you're using SMS, true for email (default 182 | true) 183 | - init_hour, init_minute: the start time for trivia 184 | - send_hour, send_miniute: the rsvp time for trivia 185 | - trivia_weekday: day of week (0 - Sunday, 1 - Monday, etc...) 186 | - from_name: string name of sender 187 | - to_name: string name of recipient 188 | - from: sender email address (required when email_format is set) 189 | - to: recipient email address (required when email_format is set) 190 | -------------------------------------------------------------------------------- /doc/conf.py.in: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | import sphinx 18 | 19 | sys.path.append(os.path.abspath('sphinxext')) 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = u'@PROJECT_NAME@' 24 | copyright = u'@COPYRIGHT@' 25 | author = u'@AUTHOR@' 26 | 27 | 28 | # The short X.Y version 29 | version = u'' 30 | # The full version, including alpha/beta/rc tags 31 | release = u'@VERSION@' 32 | 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # If your documentation needs a minimal Sphinx version, state it here. 37 | # 38 | needs_sphinx = '2.1.0' 39 | 40 | # Add any Sphinx extension module names here, as strings. They can be 41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 42 | # ones. 43 | extensions = [ 44 | 'sphinx.ext.autodoc', 45 | 'sphinx.ext.todo', 46 | 'sphinx.ext.coverage', 47 | 'sphinx.ext.viewcode', 48 | 'breathe', 49 | 'myst_parser', 50 | ] 51 | 52 | autosectionlabel_prefix_document = True 53 | 54 | breathe_projects = { "@PROJECT_NAME@": "@DOXYGEN_DIR@/xml/" } 55 | breathe_default_members = ('members', 'undoc-members') 56 | breathe_default_project = "@PROJECT_NAME@" 57 | breathe_domain_by_extension = { 58 | "h" : "c", 59 | "c": "c", 60 | } 61 | 62 | # Add any paths that contain templates here, relative to this directory. 63 | templates_path = ['_templates'] 64 | 65 | # The suffix(es) of source filenames. 66 | # You can specify multiple suffix as a list of string: 67 | source_suffix = { 68 | '.rst': 'restructuredtext', 69 | '.txt': 'markdown', 70 | '.md': 'markdown', 71 | } 72 | 73 | # The master toctree document. 74 | master_doc = 'index' 75 | 76 | # The language for content autogenerated by Sphinx. Refer to documentation 77 | # for a list of supported languages. 78 | # 79 | # This is also used if you do content translation via gettext catalogs. 80 | # Usually you set "language" from the command line for these cases. 81 | language = 'en' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | # This pattern also affects html_static_path and html_extra_path. 86 | exclude_patterns = [] 87 | 88 | # The name of the Pygments (syntax highlighting) style to use. 89 | pygments_style = None 90 | 91 | # default domain 92 | primary_domain = 'c' 93 | 94 | # -- Options for HTML output ------------------------------------------------- 95 | 96 | # The theme to use for HTML and HTML Help pages. See the documentation for 97 | # a list of builtin themes. 98 | # 99 | html_theme = 'alabaster' 100 | 101 | # Theme options are theme-specific and customize the look and feel of a theme 102 | # further. For a list of options available for each theme, see the 103 | # documentation. 104 | # 105 | # html_theme_options = {} 106 | 107 | # Add any paths that contain custom static files (such as style sheets) here, 108 | # relative to this directory. They are copied after the builtin static files, 109 | # so a file named "default.css" will overwrite the builtin "default.css". 110 | # html_static_path = ['_static'] 111 | 112 | # Custom sidebar templates, must be a dictionary that maps document names 113 | # to template names. 114 | # 115 | # The default sidebars (for documents that don't match any pattern) are 116 | # defined by theme itself. Builtin themes are using these templates by 117 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 118 | # 'searchbox.html']``. 119 | # 120 | # html_sidebars = {} 121 | 122 | 123 | # -- Options for manual page output ------------------------------------------ 124 | 125 | # One entry per manual page. List of tuples 126 | # (source start file, name, description, authors, manual section). 127 | man_pages = [ 128 | (master_doc, '@PROJECT_NAME@', u'@PROJECT_NAME@ Documentation', 129 | [author], 1) 130 | ] 131 | -------------------------------------------------------------------------------- /doc/dev.rst: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | .. toctree:: 5 | 6 | Tooling 7 | Plugins 8 | Plugins-2 9 | Backends 10 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to CBot's Documentation! 2 | ================================ 3 | 4 | CBot 5 | ---- 6 | 7 | CBot is an extensible, flexible chatbot that runs on IRC and Signal. It is 8 | implemented in C, thus the name. Please use the table of contents below to 9 | navigate through this documentation. 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | User 15 | admin 16 | dev 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /doc/meson.build: -------------------------------------------------------------------------------- 1 | sphinx = find_program('sphinx-build', required: true) 2 | doxygen = find_program('doxygen', required: true) 3 | breathe = find_program('breathe-apidoc', required: true) 4 | 5 | sphinx_c = run_command(sphinx, '--version', check: true) 6 | breathe_c = run_command(breathe, '--version', check: true) 7 | doxygen_c = run_command(doxygen, '--version', check: true) 8 | 9 | sphinx_v = sphinx_c.stdout().split(' ')[1].strip() 10 | breathe_v = breathe_c.stdout().split(' ')[2].strip() 11 | doxygen_v = doxygen_c.stdout().strip() 12 | 13 | if sphinx_v.version_compare('< 2.1.0') 14 | error('Use at least sphinx version 2.1.0, found ' + sphinx_v) 15 | endif 16 | 17 | if breathe_v.version_compare('< 4.11') 18 | error('Use at least breathe version 4.11, found ' + breathe_v) 19 | endif 20 | 21 | if doxygen_v.version_compare('< 1.8') 22 | error('Use at least doxygen version 1.8, found ' + doxygen_v) 23 | endif 24 | 25 | doxygen_database = meson.current_build_dir() + '/doxygen_doc' 26 | 27 | base_data = configuration_data() 28 | base_data.set('PROJECT_NAME', meson.project_name()) 29 | base_data.set('VERSION', meson.project_version()) 30 | base_data.set('DOXYGEN_DIR', doxygen_database) 31 | base_data.set('SRC_ROOT', meson.source_root()) 32 | base_data.set('AUTHOR', 'Stephen Brennan') 33 | base_data.set('COPYRIGHT', '2021 Stephen Brennan') 34 | 35 | # modify the sphinx configuration and the breathe doxygen XML database 36 | # to point where its being generated by doxygen 37 | sphinx_conf_data = configuration_data() 38 | sphinx_conf_data.merge_from(base_data) 39 | sphinx_conf = configure_file( 40 | input: 'conf.py.in', 41 | output: 'conf.py', 42 | configuration: base_data 43 | ) 44 | 45 | doxy_conf_data = configuration_data() 46 | doxy_conf_data.merge_from(base_data) 47 | doxygen_conf = configure_file( 48 | input: 'doxygen.ini.in', 49 | output: 'doxygen.ini', 50 | configuration: base_data 51 | ) 52 | 53 | script_data = configuration_data() 54 | script_data.set('SRCDIR', meson.current_build_dir()) 55 | script_data.set('OUTDIR', meson.current_build_dir() + '/doc') 56 | script_data.set('DOXYGEN_CONF', meson.current_build_dir() + '/doxygen.ini') 57 | script_data.set('DOXYGEN_CMD', doxygen.path()) 58 | script_data.set('SPHINX_CMD', sphinx.path()) 59 | script_doxy_sphinx = configure_file( 60 | input: 'run_doxygen_sphinx.sh.in', 61 | output: 'run_doxygen_sphinx.sh', 62 | configuration: script_data 63 | ) 64 | 65 | doxygen_target = custom_target( 66 | 'doc-doxygen', 67 | command: [ doxygen, doxygen_conf ], 68 | output: 'doxygen_doc', 69 | build_by_default: false 70 | ) 71 | 72 | # copy everything to build_dir, if you plan on adding other files in the top 73 | # rootdir of sourcedir, please add them here as well, otherwise use 'toc/'s 74 | # meson.build file 75 | sphinx_files = [ 76 | 'index.rst', 77 | 'Tooling.md', 78 | 'Plugins.md', 79 | 'Plugins-2.md', 80 | 'User.md', 81 | 'admin.rst', 82 | 'dev.rst', 83 | 'Backends.md', 84 | ] 85 | foreach file : sphinx_files 86 | configure_file(input: file, output: file, copy: true) 87 | endforeach 88 | 89 | sphinx_doc = custom_target( 90 | 'doc-breathe', 91 | command: script_doxy_sphinx, 92 | output: 'doc', 93 | build_by_default: true, 94 | depends: doxygen_target 95 | ) 96 | 97 | # we need this because we will have a stale 'doc' directory 98 | # and this forces it to be rebuilt 99 | docs = run_target( 100 | 'docs', 101 | command: script_doxy_sphinx, 102 | ) 103 | 104 | #install_subdir( 105 | # sphinx_doc.full_path(), 106 | # install_dir: join_paths(dir_data, 'doc', meson.project_name()), 107 | # strip_directory: true, 108 | #) 109 | -------------------------------------------------------------------------------- /doc/run_doxygen_sphinx.sh.in: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | @DOXYGEN_CMD@ @DOXYGEN_CONF@ 4 | @SPHINX_CMD@ -W -j auto @SRCDIR@ @OUTDIR@ 5 | -------------------------------------------------------------------------------- /inc/cbot/curl.h: -------------------------------------------------------------------------------- 1 | #ifndef CBOT_CURL_H 2 | #define CBOT_CURL_H 3 | 4 | #include "cbot/cbot.h" 5 | #include "sc-collections.h" 6 | #include 7 | 8 | /** 9 | * @brief Use this instead of curl_easy_perform in order to make a request. 10 | * 11 | * Blocks the current lwt until the request is completed. The actual execution, 12 | * of course, will be asynchronous -- other lwts, like the main IRC thread, will 13 | * continue working when they have work. 14 | * 15 | * This function makes use of CURLOPT_PRIVATE, which means that you cannot use 16 | * this yourself. 17 | * 18 | * @param bot Bot which is being used (all requests for the same bot are handled 19 | * on the same curl multi instance) 20 | * @param handle Easy handle for the request. 21 | * @returns The return code which curl_easy_perform would have returned 22 | */ 23 | CURLcode cbot_curl_perform(struct cbot *bot, CURL *handle); 24 | 25 | /** 26 | * @brief Use this to configure your CURL handle to write response to a charbuf 27 | * 28 | * It's just a simple way to get the whole response data as a single string. 29 | * 30 | * @param easy Handle 31 | * @param buf Buffer to write response to 32 | */ 33 | void cbot_curl_charbuf_response(CURL *easy, struct sc_charbuf *buf); 34 | 35 | /** 36 | * @brief Make a HTTP request to a URL and return the result 37 | * 38 | * This is as simple as it gets. On error, NULL is returned and a message is 39 | * logged to cbot's logging. On success, a malloc-allocated buffer is returned 40 | * with the contents of the response. 41 | */ 42 | char *cbot_curl_get(struct cbot *bot, const char *url, ...); 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /inc/cbot/json.h: -------------------------------------------------------------------------------- 1 | /** 2 | * cbot/json.h: JSON helpers built on NOSJ 3 | * 4 | * Quite simply, this allows me to prototype better helper APIs in cbot, where I 5 | * use NOSJ a lot, before merging them into NOSJ itself. So this is a temporary 6 | * header and APIs may change. 7 | */ 8 | #ifndef CBOT_JSON_H 9 | #define CBOT_JSON_H 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | int je_get_object(struct json_easy *je, uint32_t start, const char *key, 16 | uint32_t *out); 17 | int je_get_array(struct json_easy *je, uint32_t start, const char *key, 18 | uint32_t *out); 19 | int je_get_uint(struct json_easy *je, uint32_t start, const char *key, 20 | uint64_t *out); 21 | int je_get_int(struct json_easy *je, uint32_t start, const char *key, 22 | int64_t *out); 23 | int je_get_bool(struct json_easy *je, uint32_t start, const char *key, 24 | bool *out); 25 | int je_get_string(struct json_easy *je, uint32_t start, const char *key, 26 | char **out); 27 | bool je_string_match(struct json_easy *je, uint32_t start, const char *key, 28 | const char *cmp); 29 | 30 | /** Macro to for loop over each entry in an array */ 31 | #define je_arr_for_each(var, jsonp, start) \ 32 | for (var = (jsonp)->tokens[start].length ? ((start) + 1) : 0; \ 33 | var != 0; var = ((jsonp)->tokens)[var].next) 34 | 35 | /** Macro to for loop over key/value in an object */ 36 | #define je_obj_for_each(key, val, jsonp, start) \ 37 | for (key = (jsonp)->tokens[start].length ? ((start) + 1) : 0, \ 38 | val = key + 1; \ 39 | key != 0; key = ((jsonp)->tokens)[key].next, val = key + 1) 40 | 41 | #endif // CBOT_JSON_H 42 | -------------------------------------------------------------------------------- /make-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Create a release tarball. 3 | # 4 | # This is different from git archive in that it includes some untracked files. 5 | # We would like to package all of the subprojects as fallbacks, so that they can 6 | # be used without downloading anything. 7 | # 8 | # usage: 9 | # ./make-release.sh 10 | # make release from HEAD, using "git describe" for the version 11 | # 12 | # ./make-release.sh tag 13 | # make release from tag 14 | 15 | set -euo pipefail 16 | 17 | RELEASE=${1:-$(git describe --tags)} 18 | RELNOV=${RELEASE#v} 19 | WORK=$(pwd) 20 | 21 | git worktree add /tmp/cbot-$RELEASE $RELEASE 22 | pushd /tmp/cbot-$RELEASE 23 | 24 | meson subprojects download 25 | meson subprojects foreach rm -rf .git 26 | 27 | tar --exclude-vcs --transform "s,^\.,cbot-$RELNOV," -czvf $WORK/cbot-$RELNOV.tar.gz . 28 | popd 29 | git worktree remove /tmp/cbot-$RELEASE 30 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'cbot', 'c', 3 | version : '0.15.0', 4 | # be sure to update APKBUILD version too 5 | ) 6 | 7 | sources = [ 8 | 'src/cbot.c', 9 | 'src/cbot_cli.c', 10 | 'src/cbot_irc.c', 11 | 'src/db.c', 12 | 'src/tok.c', 13 | 'src/fmt.c', 14 | 'src/curl.c', 15 | 'src/log.c', 16 | 'src/signal/backend.c', 17 | 'src/signal/signalcli_bridge.c', 18 | 'src/signal/signald_bridge.c', 19 | 'src/signal/jmsg.c', 20 | 'src/signal/mention.c', 21 | 'src/http.c', 22 | 'src/json.c', 23 | ] 24 | 25 | inc = include_directories('inc') 26 | 27 | cc = meson.get_compiler('c') 28 | libircclient_dep = dependency( 29 | 'libircclient', 30 | fallback: ['libircclient', 'libircclient_dep'], 31 | # There are some compiler warnings, silence them 32 | default_options: [ 33 | 'libircclient:warning_level=0', 34 | ], 35 | ) 36 | libsc_regex_dep = dependency( 37 | 'libsc-regex', 38 | fallback: ['sc-regex', 'libsc_regex_dep'], 39 | version: '>=0.4.0', 40 | ) 41 | libsc_collections_dep = dependency( 42 | 'libsc-collections', 43 | fallback: ['sc-collections', 'libsc_collections_dep'], 44 | version: '>=0.11.0', 45 | ) 46 | libsc_argparse_dep = dependency( 47 | 'libsc-argparse', 48 | fallback: ['sc-argparse', 'libsc_argparse_dep'], 49 | version: '>=0.2.0', 50 | ) 51 | libsc_lwt_dep = dependency( 52 | 'libsc-lwt', 53 | fallback: ['sc-lwt', 'libsc_lwt_dep'], 54 | version: '>=0.7.2', 55 | ) 56 | libnosj_dep = dependency( 57 | 'libnosj', 58 | fallback: ['nosj', 'libnosj_dep'], 59 | version: '>=2.2.1', 60 | ) 61 | dl_dep = cc.find_library('dl', required : false) 62 | sqlite_dep = dependency( 63 | 'sqlite3', 64 | fallback: ['sqlite', 'sqlite_dep'], 65 | ) 66 | config_dep = dependency('libconfig') 67 | curl_dep = dependency('libcurl') 68 | uhttp_dep = dependency('libmicrohttpd') 69 | 70 | cbot_deps = [ 71 | libsc_collections_dep, 72 | libsc_regex_dep, 73 | libsc_argparse_dep, 74 | libsc_lwt_dep, 75 | libnosj_dep, 76 | libircclient_dep, 77 | dl_dep, 78 | sqlite_dep, 79 | config_dep, 80 | curl_dep, 81 | uhttp_dep, 82 | ] 83 | 84 | if get_option('with_readline') 85 | cbot_deps += dependency('readline') 86 | add_project_arguments(['-DWITH_READLINE'], language : 'c') 87 | elif get_option('with_libedit') 88 | cbot_deps += dependency('libedit') 89 | add_project_arguments(['-DWITH_LIBEDIT'], language : 'c') 90 | endif 91 | add_project_arguments( 92 | [ 93 | '-D_XOPEN_SOURCE=600', 94 | '-D_POSIX_C_SOURCE=200809L', 95 | '-D_DEFAULT_SOURCE', 96 | '--std=c99', 97 | ], 98 | language : 'c', 99 | ) 100 | 101 | 102 | libcbot = shared_library( 103 | 'cbot', sources, 104 | include_directories : inc, 105 | install : true, 106 | dependencies : cbot_deps, 107 | ) 108 | libcbot_dep = declare_dependency( 109 | include_directories : inc, 110 | link_with : libcbot, 111 | ) 112 | cbot = executable( 113 | 'cbot', ['src/main.c'], 114 | include_directories : inc, 115 | install : true, 116 | dependencies : [libcbot_dep] + cbot_deps, 117 | ) 118 | 119 | plugins = [ 120 | 'emote', 121 | 'greet', 122 | 'help', 123 | 'ircctl', 124 | 'karma', 125 | 'log', 126 | 'name', 127 | 'become', 128 | 'who', 129 | 'annoy', 130 | 'tok', 131 | 'reply', 132 | 'sqlkarma', 133 | 'sqlknow', 134 | 'weather', 135 | 'aqi', 136 | 'birthday', 137 | 'buttcoin', 138 | 'reactrack', 139 | 'trivia', 140 | 'sports_schedule', 141 | ] 142 | plugin_libs = [] 143 | foreach f: plugins 144 | plugin_libs += [ 145 | shared_library( 146 | f, 'plugin/' + f + '.c', 147 | include_directories : inc, install : true, 148 | dependencies : [libcbot_dep] + cbot_deps, 149 | name_prefix : '', 150 | install_dir : get_option('libexecdir') / 'cbot', 151 | ) 152 | ] 153 | endforeach 154 | 155 | install_headers('inc/cbot/cbot.h', subdir : 'cbot') 156 | install_headers('inc/cbot/db.h', subdir : 'cbot') 157 | 158 | if get_option('doc') 159 | subdir('doc') 160 | else 161 | message('Documentation will not be built. Use -Ddoc to build it.') 162 | endif 163 | 164 | if get_option('test') 165 | subdir('tests') 166 | endif 167 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('doc', type : 'boolean', value : false) 2 | option('test', type : 'boolean', value : true) 3 | option('with_readline', type : 'boolean', value : false) 4 | option('with_libedit', type : 'boolean', value : false) 5 | -------------------------------------------------------------------------------- /plugin/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for plugins. Pretty darn simple. 2 | 3 | CC=gcc 4 | INC=-I../inc/ 5 | FLAGS=-fPIC 6 | SOURCES=$(shell find . -type f -name "*.c") 7 | OBJECTS=$(patsubst %.c,%.so,$(SOURCES)) 8 | 9 | CFG=release 10 | ifeq ($(CFG),debug) 11 | FLAGS += -g -DDEBUG 12 | endif 13 | ifneq ($(CFG),debug) 14 | ifneq ($(CFG),release) 15 | $(error Bad build configuration. Choices are debug, release, coverage.) 16 | endif 17 | endif 18 | 19 | all: $(OBJECTS) 20 | 21 | help.so: help.c help.h 22 | 23 | help.h: help.md help_translate.py 24 | ./help_translate.py help.h 25 | 26 | %.so: %.c 27 | $(CC) $(FLAGS) $(INC) -shared $< -o $@ 28 | 29 | clean: 30 | rm -f $(OBJECTS) 31 | rm -f help.h 32 | -------------------------------------------------------------------------------- /plugin/annoy.c: -------------------------------------------------------------------------------- 1 | /** 2 | * annoy.c: CBot plugin which sends annoying messages to a channel 3 | * 4 | * Sample usage: 5 | * 6 | * user> cbot be annoying 7 | * cbot> hello! im an annoying bot 8 | * cbot> hello! im an annoying bot 9 | * cbot> hello! im an annoying bot 10 | * cbot> hello! im an annoying bot 11 | * ... 12 | * user> cbot stop it 13 | */ 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | 22 | #include 23 | #include 24 | 25 | #include "cbot/cbot.h" 26 | 27 | char *channel = NULL; 28 | struct cbot *bot = NULL; 29 | 30 | struct cbot_callback *cb = NULL; 31 | 32 | static void annoy_loop(void *data) 33 | { 34 | struct timespec ts; 35 | ts.tv_sec = 3; 36 | ts.tv_nsec = 0; 37 | while (channel) { 38 | printf("sending annoing message to %s\n", channel); 39 | cbot_send(bot, channel, "hello! im an annoying bot"); 40 | sc_lwt_sleep(&ts); 41 | if (sc_lwt_shutting_down()) 42 | return; 43 | } 44 | } 45 | 46 | static void annoy_cb(struct cbot_plugin *plugin, char *channel) 47 | { 48 | time_t now = time(NULL); 49 | cbot_send(plugin->bot, channel, 50 | "hello! i'm an annoying bot using callbacks"); 51 | cb = cbot_schedule_callback(plugin, (void *)annoy_cb, channel, now + 3); 52 | } 53 | 54 | static void stop(struct cbot_message_event *event, void *user) 55 | { 56 | if (channel) { 57 | /* the loop stops itself by checking channel */ 58 | free(channel); 59 | channel = NULL; 60 | bot = NULL; 61 | } else if (cb) { 62 | /* we need to cancel the callback */ 63 | cbot_cancel_callback(cb); 64 | free(channel); 65 | channel = NULL; 66 | cb = NULL; 67 | } 68 | } 69 | 70 | static void start(struct cbot_message_event *event, void *user) 71 | { 72 | if (channel || cb) 73 | stop(event, user); 74 | 75 | if (user) { 76 | time_t when = time(NULL); 77 | printf("being annoying to %s, via callback\n", event->channel); 78 | cbot_schedule_callback(event->plugin, (void *)annoy_cb, 79 | strdup(event->channel), when); 80 | } else { 81 | printf("being annoying to %s, via loop\n", event->channel); 82 | channel = strdup(event->channel); 83 | bot = event->bot; 84 | sc_lwt_create_task(cbot_get_lwt_ctx(event->bot), annoy_loop, 85 | NULL); 86 | } 87 | } 88 | 89 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 90 | { 91 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)start, NULL, 92 | "be annoying"); 93 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)start, (void *)1, 94 | "be annoying with callbacks"); 95 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)stop, NULL, 96 | "stop it!?"); 97 | return 0; 98 | } 99 | 100 | static void help(struct cbot_plugin *plugin, struct sc_charbuf *cb) 101 | { 102 | sc_cb_concat( 103 | cb, 104 | "- cbot be annoying: send annoying messages to a channel\n"); 105 | sc_cb_concat(cb, "- cbot stop it: stop sending annoying messages\n"); 106 | } 107 | 108 | struct cbot_plugin_ops ops = { 109 | .description = "a plugin that sends annoying messages every 3 seconds", 110 | .load = load, 111 | .help = help, 112 | }; 113 | -------------------------------------------------------------------------------- /plugin/aqi.c: -------------------------------------------------------------------------------- 1 | /* 2 | * aqi.c: Plugin implementing AQI queries 3 | */ 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "cbot/cbot.h" 17 | #include "cbot/curl.h" 18 | 19 | struct aqi { 20 | struct cbot_plugin *plugin; 21 | const char *token; 22 | int query_count; 23 | }; 24 | 25 | struct aqi_query { 26 | struct aqi *aqi; 27 | char *location; 28 | char *channel; 29 | }; 30 | 31 | const char *URLFMT = "https://api.waqi.info/feed/%s/?token=%s"; 32 | 33 | static inline struct aqi *getaqi(struct cbot_plugin *plugin) 34 | { 35 | return (struct aqi *)plugin->data; 36 | } 37 | 38 | static void handle_query(void *data) 39 | { 40 | struct aqi_query *query = data; 41 | struct aqi *aqi = query->aqi; 42 | struct cbot_plugin *plugin = aqi->plugin; 43 | struct sc_charbuf urlbuf, respbuf; 44 | CURL *easy; 45 | struct json_easy *json; 46 | uint32_t idx; 47 | int rv, aqival; 48 | 49 | sc_cb_init(&urlbuf, 256); 50 | sc_cb_init(&respbuf, 512); 51 | sc_cb_printf(&urlbuf, URLFMT, query->location, aqi->token); 52 | 53 | easy = curl_easy_init(); 54 | curl_easy_setopt(easy, CURLOPT_URL, urlbuf.buf); 55 | // curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L); 56 | cbot_curl_charbuf_response(easy, &respbuf); 57 | rv = cbot_curl_perform(plugin->bot, easy); 58 | if (rv != CURLE_OK) { 59 | fprintf(stderr, "aqi: curl error: %s\n", 60 | curl_easy_strerror(rv)); 61 | goto out_curl; 62 | } 63 | // printf("RESULT\n%s\nENDRESULT\n", respbuf.buf); 64 | 65 | json = json_easy_new(respbuf.buf); 66 | rv = json_easy_parse(json); 67 | if (rv != JSON_OK) { 68 | fprintf(stderr, "aqi: nosj error: %s\n", json_strerror(rv)); 69 | goto out_curl; 70 | } 71 | 72 | rv = json_easy_lookup(json, 0, "status", &idx); 73 | bool match; 74 | if (!rv) 75 | json_easy_string_match(json, idx, "ok", &match); 76 | if (rv || !match) { 77 | char *msg = "(not found)"; 78 | bool freemsg = false; 79 | rv = json_easy_lookup(json, 0, "data", &idx); 80 | if (!rv) { 81 | rv = json_easy_string_get(easy, idx, &msg); 82 | if (!rv) 83 | freemsg = true; 84 | } 85 | fprintf(stderr, "aqi: api error: %s\n", msg); 86 | if (freemsg) 87 | free(msg); 88 | goto out_api; 89 | } 90 | 91 | rv = json_easy_lookup(json, 0, "data.aqi", &idx); 92 | if (rv != JSON_OK || json->tokens[idx].type != JSON_NUMBER) { 93 | fprintf(stderr, "aqi: api didn't return data.aqi\n"); 94 | goto out_api; 95 | } 96 | double val; 97 | rv = json_easy_number_get(json, idx, &val); 98 | if (rv != JSON_OK) { 99 | CL_CRIT("aqi: data.aqi is not a number\n"); 100 | goto out_api; 101 | } 102 | aqival = (int)val; 103 | sc_cb_clear(&urlbuf); 104 | sc_cb_printf(&urlbuf, "AQI: %d", aqival); 105 | cbot_send(plugin->bot, query->channel, urlbuf.buf); 106 | 107 | out_api: 108 | json_easy_free(json); 109 | out_curl: 110 | sc_cb_destroy(&urlbuf); 111 | sc_cb_destroy(&respbuf); 112 | free(query->channel); 113 | free(query->location); 114 | free(query); 115 | curl_easy_cleanup(easy); 116 | aqi->query_count--; 117 | } 118 | 119 | static void run(struct cbot_message_event *evt, void *user) 120 | { 121 | struct aqi *aqi = getaqi(evt->plugin); 122 | struct aqi_query *query = calloc(1, sizeof(*query)); 123 | query->aqi = aqi; 124 | query->location = strdup("san-francisco"); 125 | query->channel = strdup(evt->channel); 126 | sc_lwt_create_task(cbot_get_lwt_ctx(evt->bot), handle_query, query); 127 | aqi->query_count++; 128 | } 129 | 130 | static int load(struct cbot_plugin *plugin, config_setting_t *group) 131 | { 132 | int rv; 133 | struct aqi *aqi; 134 | const char *token; 135 | 136 | aqi = calloc(1, sizeof(*aqi)); 137 | 138 | rv = config_setting_lookup_string(group, "token", &token); 139 | if (rv == CONFIG_FALSE) { 140 | fprintf(stderr, "plugin aqi: missing token!\n"); 141 | rv = -1; 142 | goto err; 143 | } 144 | aqi->token = strdup(token); 145 | aqi->plugin = plugin; 146 | plugin->data = aqi; 147 | 148 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)run, NULL, 149 | "aqi *(.*)"); 150 | 151 | return 0; 152 | err: 153 | free(aqi); 154 | return rv; 155 | } 156 | 157 | static void unload(struct cbot_plugin *plugin) 158 | { 159 | struct aqi *aqi = getaqi(plugin); 160 | free((void *)aqi->token); 161 | free(aqi); 162 | plugin->data = NULL; 163 | } 164 | 165 | static void help(struct cbot_plugin *plugin, struct sc_charbuf *cb) 166 | { 167 | sc_cb_concat(cb, 168 | "- cbot aqi: get air quality index of San Francisco\n"); 169 | sc_cb_concat( 170 | cb, 171 | "- cbot aqi LOCATION: get AQI for LOCATION (use a Zip code)\n"); 172 | } 173 | 174 | struct cbot_plugin_ops ops = { 175 | .description = 176 | "show the air quality for a location (default San Francisco)", 177 | .load = load, 178 | .unload = unload, 179 | .help = help, 180 | }; 181 | -------------------------------------------------------------------------------- /plugin/become.c: -------------------------------------------------------------------------------- 1 | /** 2 | * become.c: CBot plugin which lets you tell the bot to change nick 3 | * 4 | * Sample use: 5 | * 6 | * U> cbot become newbot 7 | * newbot> your wish is my command 8 | */ 9 | #include 10 | #include 11 | #include 12 | 13 | #include "cbot/cbot.h" 14 | #include "sc-collections.h" 15 | 16 | static void become(struct cbot_message_event *event, void *user) 17 | { 18 | char *name = sc_regex_get_capture(event->message, event->indices, 0); 19 | cbot_nick(event->bot, name); 20 | free(name); 21 | } 22 | 23 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 24 | { 25 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)become, NULL, 26 | "become (\\w+)"); 27 | return 0; 28 | } 29 | 30 | static void help(struct cbot_plugin *plugin, struct sc_charbuf *cb) 31 | { 32 | sc_cb_concat(cb, "this plugin only works on IRC or CLI!\n"); 33 | sc_cb_concat(cb, "- cbot become : change nick\n"); 34 | } 35 | 36 | struct cbot_plugin_ops ops = { 37 | .description = "cbot plugin which lets you tell the bot to change nick", 38 | .load = load, 39 | .help = help, 40 | }; 41 | -------------------------------------------------------------------------------- /plugin/buttcoin.c: -------------------------------------------------------------------------------- 1 | /* 2 | * buttcoin.c: CBot plugin which notifies if that stupid fake internet money 3 | * crashes, good for some laughs 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "cbot/cbot.h" 17 | #include "cbot/curl.h" 18 | #include "nosj.h" 19 | 20 | struct buttcoin_notify { 21 | struct cbot *bot; 22 | 23 | /* Coin market cap API key, from config */ 24 | char *api_key_header; 25 | /* Channel to shitpost in */ 26 | char *channel; 27 | /* API URL (can sub for test URL) */ 28 | const char *url; 29 | /* Seconds to sleep between fetches */ 30 | int seconds; 31 | /* Seconds to wait before renotifying at the same threshold */ 32 | int renotify_wait; 33 | 34 | /* Most recent price */ 35 | double price_usd; 36 | 37 | bool tether_notified; 38 | 39 | /* The threshold we crossed last time we notified */ 40 | double last_notify_thresh; 41 | }; 42 | 43 | struct prices { 44 | double btc; 45 | double tether; 46 | }; 47 | 48 | static inline double thresh(double price) 49 | { 50 | return (double)((int)(price / 1000) * 1000); 51 | } 52 | 53 | static const char *URL = "https://pro-api.coinmarketcap.com/v2/cryptocurrency/" 54 | "quotes/latest?symbol=BTC,USDT"; 55 | static const char *TESTURL = "http://localhost:4100"; 56 | static const char *LINK = "https://coinmarketcap.com/currencies/bitcoin/"; 57 | static const char *TETHER_LINK = "https://coinmarketcap.com/currencies/tether/"; 58 | 59 | static int lookup_price(struct buttcoin_notify *butt, struct prices *prices) 60 | { 61 | struct cbot *bot = butt->bot; 62 | CURLcode rv; 63 | int ret; 64 | CURL *curl = curl_easy_init(); 65 | struct curl_slist *headers = NULL; 66 | struct sc_charbuf buf; 67 | struct json_easy *json; 68 | uint32_t index; 69 | 70 | sc_cb_init(&buf, 256); 71 | curl_easy_setopt(curl, CURLOPT_URL, butt->url); 72 | /* Should we null test after each one? Yeah. Will we? No. */ 73 | headers = curl_slist_append(headers, "Accept: application/json"); 74 | headers = curl_slist_append(headers, butt->api_key_header); 75 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); 76 | cbot_curl_charbuf_response(curl, &buf); 77 | 78 | rv = cbot_curl_perform(bot, curl); 79 | if (rv != CURLE_OK) { 80 | CL_WARN("buttcoin: curl error: %s\n", curl_easy_strerror(rv)); 81 | ret = -1; 82 | goto out; 83 | } 84 | 85 | json = json_easy_new(buf.buf); 86 | ret = json_easy_parse(json); 87 | if (ret != 0) { 88 | CL_WARN("buttcoin: json parse error: %s\n", json_strerror(ret)); 89 | goto out_free_json; 90 | } 91 | 92 | ret = json_easy_lookup(json, 0, "data.BTC[0].quote.USD.price", &index); 93 | if (!ret) 94 | ret = json_easy_number_get(json, index, &prices->btc); 95 | if (ret != JSON_OK) { 96 | CL_WARN("buttcoin: quote price not found in response: %s\n", 97 | json_strerror(ret)); 98 | ret = -1; 99 | goto out_free_json; 100 | } 101 | 102 | ret = json_easy_lookup(json, 0, "data.USDT[0].quote.USD.price", &index); 103 | if (!ret) 104 | ret = json_easy_number_get(json, index, &prices->tether); 105 | if (ret != JSON_OK) { 106 | CL_WARN("buttcoin: quote price not found in response: %s\n", 107 | json_strerror(ret)); 108 | ret = -1; 109 | goto out_free_json; 110 | } 111 | 112 | out_free_json: 113 | json_easy_free(json); 114 | out: 115 | curl_slist_free_all(headers); 116 | curl_easy_cleanup(curl); 117 | sc_cb_destroy(&buf); 118 | return ret; 119 | } 120 | 121 | static void buttcoin_loop(void *arg) 122 | { 123 | struct buttcoin_notify *butt = arg; 124 | double floor; 125 | struct timespec ts; 126 | struct prices prices; 127 | int ret; 128 | 129 | ret = lookup_price(butt, &prices); 130 | if (ret != 0) { 131 | CL_WARN("buttcoin: initial price lookup failed, bailing\n"); 132 | return; 133 | } 134 | CL_DEBUG("buttcoin: got price: $%.2f\n", prices.btc); 135 | butt->price_usd = prices.btc; 136 | /* 137 | * If we don't already have the last threshold, simply use the current 138 | * price's floor + 1000. So we'll notify when it drops below the current 139 | * $1k threshold. 140 | */ 141 | if (butt->last_notify_thresh == 0) 142 | butt->last_notify_thresh = thresh(butt->price_usd) + 1000; 143 | 144 | for (;;) { 145 | ts.tv_nsec = 0; 146 | ts.tv_sec = butt->seconds; 147 | sc_lwt_sleep(&ts); 148 | if (sc_lwt_shutting_down()) { 149 | CL_DEBUG("buttcoin: got shutdown signal, goodbye\n"); 150 | break; 151 | } 152 | 153 | ret = lookup_price(butt, &prices); 154 | if (ret != 0) { 155 | CL_WARN("buttcoin: lookup got error, skipping this " 156 | "check\n"); 157 | continue; 158 | } 159 | floor = thresh(butt->price_usd); 160 | butt->price_usd = prices.btc; 161 | 162 | if (prices.tether < 0.97 && !butt->tether_notified) { 163 | cbot_send(butt->bot, butt->channel, 164 | "Uh-oh, is Tether losing its peg?\n" 165 | "The price is now $%.4f\n" 166 | "Live graph: %s", 167 | prices.tether, TETHER_LINK); 168 | butt->tether_notified = true; 169 | } 170 | 171 | CL_DEBUG("buttcoin: price: $%.2f floor: $%.2f last floor: " 172 | "$%.2f\n", 173 | prices.btc, floor, butt->last_notify_thresh); 174 | if (prices.btc >= floor || floor >= butt->last_notify_thresh) 175 | continue; 176 | 177 | butt->last_notify_thresh = floor; 178 | cbot_send(butt->bot, butt->channel, 179 | "Lol, BTC is now below $%.0f\n" 180 | "The price is now $%.2f\n" 181 | "Live graph: %s", 182 | floor, prices.btc, LINK); 183 | } 184 | 185 | /* cleanup butt */ 186 | free(butt->channel); 187 | free(butt->api_key_header); 188 | free(butt); 189 | } 190 | 191 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 192 | { 193 | int rv; 194 | const char *channel; 195 | const char *api_key; 196 | int use_test = 0; 197 | int seconds = 300; 198 | double start_thresh = 0; 199 | struct buttcoin_notify *butt; 200 | struct sc_charbuf cb; 201 | 202 | rv = config_setting_lookup_string(conf, "channel", &channel); 203 | if (rv == CONFIG_FALSE) { 204 | CL_CRIT("buttcoin: missing \"channel\" config\n"); 205 | return -1; 206 | } 207 | rv = config_setting_lookup_string(conf, "api_key", &api_key); 208 | if (rv == CONFIG_FALSE) { 209 | CL_CRIT("buttcoin: missing \"channel\" config\n"); 210 | return -1; 211 | } 212 | config_setting_lookup_float(conf, "btc_thresh_start", &start_thresh); 213 | config_setting_lookup_bool(conf, "use_test_endpoint", &use_test); 214 | config_setting_lookup_int(conf, "sleep_interval", &seconds); 215 | 216 | butt = calloc(1, sizeof(*butt)); 217 | butt->bot = plugin->bot; 218 | butt->channel = strdup(channel); 219 | sc_cb_init(&cb, 128); 220 | sc_cb_printf(&cb, "X-CMC_PRO_API_KEY: %s", api_key); 221 | butt->api_key_header = cb.buf; 222 | butt->url = use_test ? TESTURL : URL; 223 | butt->seconds = seconds; 224 | butt->last_notify_thresh = start_thresh; 225 | 226 | sc_lwt_create_task(cbot_get_lwt_ctx(plugin->bot), buttcoin_loop, butt); 227 | return 0; 228 | } 229 | 230 | static void help(struct cbot_plugin *plugin, struct sc_charbuf *cb) 231 | { 232 | sc_cb_concat(cb, "This plugin will send messages when BTC crashes. No " 233 | "commands available.\n"); 234 | } 235 | 236 | struct cbot_plugin_ops ops = { 237 | .description = "a plugin to notify you about bitcoin crashing", 238 | .load = load, 239 | .help = help, 240 | }; 241 | -------------------------------------------------------------------------------- /plugin/emote.c: -------------------------------------------------------------------------------- 1 | /** 2 | * emote.c: CBot plugin which lets a user ask cbot to perform the "me" action. 3 | * 4 | * Sample use: 5 | * 6 | * U> cbot emote is sad 7 | * C> /me is sad 8 | * (above the /me is just used to demonstrate what CBot is doing) 9 | */ 10 | 11 | #include 12 | #include 13 | 14 | #include "cbot/cbot.h" 15 | #include "sc-regex.h" 16 | 17 | static void emote(struct cbot_message_event *event, void *user) 18 | { 19 | char *c = sc_regex_get_capture(event->message, event->indices, 0); 20 | cbot_me(event->bot, event->channel, c); 21 | free(c); 22 | } 23 | 24 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 25 | { 26 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)emote, NULL, 27 | "emote (.*)"); 28 | return 0; 29 | } 30 | 31 | struct cbot_plugin_ops ops = { 32 | .description = "instruct cbot to use the IRC /me command", 33 | .load = load, 34 | }; 35 | -------------------------------------------------------------------------------- /plugin/greet.c: -------------------------------------------------------------------------------- 1 | /** 2 | * greet.c: CBot plugin which replies to hello messages 3 | * 4 | * Sample usage: 5 | * 6 | * user> hi cbot 7 | * cbot> hello, user! 8 | */ 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include "cbot/cbot.h" 15 | 16 | struct cbot_hello_priv { 17 | struct cbot_plugin *plugin; 18 | struct cbot_handler *hello_hdlr; 19 | }; 20 | 21 | static void cbot_hello(struct cbot_message_event *event, void *user) 22 | { 23 | cbot_send(event->bot, event->channel, "hello, %s!", event->username); 24 | } 25 | 26 | static void register_hello(struct cbot_hello_priv *priv) 27 | { 28 | struct sc_charbuf buf; 29 | sc_cb_init(&buf, 256); 30 | sc_cb_printf(&buf, "[Hh](ello|i|ey),? +%s!?", 31 | cbot_get_name(priv->plugin->bot)); 32 | priv->hello_hdlr = 33 | cbot_register(priv->plugin, CBOT_MESSAGE, 34 | (cbot_handler_t)cbot_hello, NULL, buf.buf); 35 | sc_cb_destroy(&buf); 36 | } 37 | 38 | static void cbot_bot_name_change(struct cbot_nick_event *event, void *user) 39 | { 40 | struct cbot_hello_priv *priv = event->plugin->data; 41 | cbot_deregister(event->bot, priv->hello_hdlr); 42 | register_hello(priv); 43 | } 44 | 45 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 46 | { 47 | struct cbot_hello_priv *priv = 48 | calloc(1, sizeof(struct cbot_hello_priv)); 49 | plugin->data = priv; 50 | priv->plugin = plugin; 51 | register_hello(priv); 52 | cbot_register(plugin, CBOT_BOT_NAME, 53 | (cbot_handler_t)cbot_bot_name_change, NULL, NULL); 54 | return 0; 55 | } 56 | 57 | static void unload(struct cbot_plugin *plugin) 58 | { 59 | free(plugin->data); 60 | } 61 | 62 | struct cbot_plugin_ops ops = { 63 | .description = "greets people who say hello to the bot", 64 | .load = load, 65 | .unload = unload, 66 | }; 67 | -------------------------------------------------------------------------------- /plugin/help.c: -------------------------------------------------------------------------------- 1 | /** 2 | * help.c: CBot plugin which sends help text over direct messages 3 | */ 4 | #include 5 | #include 6 | #include 7 | 8 | #include "cbot/cbot.h" 9 | #include "sc-collections.h" 10 | 11 | #include "../src/cbot_private.h" 12 | 13 | static void help(struct cbot_message_event *event, void *user) 14 | { 15 | const char *url = cbot_http_geturl(event->bot); 16 | cbot_send(event->bot, event->channel, 17 | "Please see CBot's user documentation at " 18 | "http://brenns10.github.io/cbot/User.html"); 19 | if (url) 20 | cbot_send(event->bot, event->channel, 21 | "See also this bot's help pages at %s/help", url); 22 | } 23 | 24 | static void http_get(struct cbot_http_event *event, void *user) 25 | { 26 | struct sc_charbuf cb; 27 | struct cbot_plugpriv *plug; 28 | 29 | cbot_http_plainresp_start(&cb, "CBot Help"); 30 | 31 | sc_cb_concat( 32 | &cb, 33 | "CBot Plugin Help\n" 34 | "================\n" 35 | "\n" 36 | "Welcome to the web-based help for CBot. Here you may find\n" 37 | "a list of all the enabled plugins, with a bit of help about\n" 38 | "them. If you want to see full details about how each plugin\n" 39 | "works and other documentation info, please refer to the \n" 40 | "full documentation here.\n" 42 | "\n"); 43 | 44 | sc_list_for_each_entry(plug, &event->bot->plugins, list, 45 | struct cbot_plugpriv) 46 | { 47 | 48 | sc_cb_printf(&cb, "\nPlugin: %s\n", plug->name); 49 | if (plug->p.ops->description) 50 | sc_cb_printf(&cb, " %s\n", plug->p.ops->description); 51 | if (plug->p.ops->help) { 52 | sc_cb_printf(&cb, "Help:\n"); 53 | plug->p.ops->help(&plug->p, &cb); 54 | } 55 | } 56 | 57 | cbot_http_plainresp_send(&cb, event, MHD_HTTP_OK); 58 | } 59 | 60 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 61 | { 62 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)help, NULL, 63 | "[Hh][Ee][Ll][Pp].*"); 64 | 65 | cbot_register(plugin, CBOT_HTTP_GET, (cbot_handler_t)http_get, NULL, 66 | "/help"); 67 | return 0; 68 | } 69 | 70 | struct cbot_plugin_ops ops = { 71 | .description = "print this help message and serve help webpage", 72 | .load = load, 73 | }; 74 | -------------------------------------------------------------------------------- /plugin/ircctl.c: -------------------------------------------------------------------------------- 1 | /** 2 | * ircctl.c: CBot plugin which allows users to request IRC operations like 3 | * giving op privilege, or joining a channel 4 | */ 5 | #include 6 | #include 7 | 8 | #include "cbot/cbot.h" 9 | #include "sc-collections.h" 10 | #include "sc-regex.h" 11 | 12 | static void op_handler(struct cbot_message_event *event, void *user_data) 13 | { 14 | char *user; 15 | user = sc_regex_get_capture(event->message, event->indices, 0); 16 | if (cbot_is_authorized(event->bot, event->username, event->message)) 17 | cbot_op(event->bot, event->channel, user); 18 | else 19 | cbot_send(event->bot, event->channel, 20 | "Sorry, you aren't authorized to do that."); 21 | free(user); 22 | } 23 | 24 | static void join_handler(struct cbot_message_event *event, void *user) 25 | { 26 | char *channel; 27 | channel = sc_regex_get_capture(event->message, event->indices, 0); 28 | if (cbot_is_authorized(event->bot, event->username, event->message)) 29 | cbot_join(event->bot, channel, NULL); 30 | else 31 | cbot_send(event->bot, event->channel, 32 | "Sorry, you aren't authorized to do that."); 33 | free(channel); 34 | } 35 | 36 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 37 | { 38 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)op_handler, NULL, 39 | "op +(.*) ?.*"); 40 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)join_handler, 41 | NULL, "join +(.*) ?.*"); 42 | return 0; 43 | } 44 | 45 | static void help(struct cbot_plugin *plugin, struct sc_charbuf *cb) 46 | { 47 | sc_cb_concat(cb, "- cbot op USER: give operator privileges to USER\n"); 48 | sc_cb_concat(cb, "- cbot join CHANNEL: join CHANNEL\n"); 49 | } 50 | 51 | struct cbot_plugin_ops ops = { 52 | .description = "have cbot perform privileged IRC operator commands", 53 | .load = load, 54 | .help = help, 55 | }; 56 | -------------------------------------------------------------------------------- /plugin/karma.c: -------------------------------------------------------------------------------- 1 | /** 2 | * karma.c: CBot plugin which tracks karma (++ and --) 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "cbot/cbot.h" 12 | #include "sc-collections.h" 13 | #include "sc-regex.h" 14 | 15 | /** 16 | * A type representing a single karma entry, which is a word, number pair. 17 | */ 18 | typedef struct { 19 | int karma; 20 | char *word; 21 | } karma_t; 22 | 23 | /** 24 | * Pointer to the karma array. 25 | */ 26 | static karma_t *karma = NULL; 27 | /** 28 | * Number of karma allocated. 29 | */ 30 | static size_t karma_alloc = 128; 31 | /** 32 | * Number of karma in the array. 33 | */ 34 | static size_t nkarma = 0; 35 | 36 | /** 37 | * @brief Return the index of a word in the karma array, or -1. 38 | * @param word The word to look up. 39 | * @returns The word's index, or -1 if it is not present in the array. 40 | */ 41 | static ssize_t find_karma(const char *word) 42 | { 43 | size_t i; 44 | for (i = 0; i < nkarma; i++) { 45 | if (strcmp(karma[i].word, word) == 0) { 46 | return i; 47 | } 48 | } 49 | return -1; 50 | } 51 | 52 | /** 53 | * @brief Copy a string. 54 | * @param word The word to copy. 55 | * @returns A newly allocated copy of the string. 56 | */ 57 | static char *copy_string(const char *word) 58 | { 59 | char *newword; 60 | size_t length = strlen(word); 61 | newword = malloc(length + 1); 62 | strncpy(newword, word, length + 1); 63 | newword[length] = '\0'; 64 | return newword; 65 | } 66 | 67 | /** 68 | * @brief Return the index of any word in the karma array. 69 | * 70 | * This function will locate an existing word in the karma array and return its 71 | * index. If the word doesn't exist, it will copy it, add it to the karma array, 72 | * set its karma to zero, and return its index. 73 | * 74 | * @param word Word to find karma of. Never modified. 75 | * @returns Index of the word in the karma array. 76 | */ 77 | static size_t find_or_create_karma(const char *word) 78 | { 79 | ssize_t idx; 80 | if (karma == NULL) { 81 | karma = calloc(karma_alloc, sizeof(karma_t)); 82 | nkarma = 0; 83 | } 84 | idx = find_karma(word); 85 | if (idx < 0) { 86 | if (nkarma == karma_alloc) { 87 | karma_alloc *= 2; 88 | karma = realloc(karma, karma_alloc * sizeof(karma_t)); 89 | } 90 | idx = nkarma++; 91 | karma[idx] = (karma_t){ .word = copy_string(word), .karma = 0 }; 92 | } 93 | return idx; 94 | } 95 | 96 | /** 97 | * @brief Removes a word from the karma array if it exists 98 | * 99 | * @param word Word to find and delete 100 | * @returns 1 if deleted, zero if not 101 | */ 102 | static size_t delete_if_exists(const char *word) 103 | { 104 | ssize_t idx; 105 | if (karma == NULL) { 106 | karma = calloc(karma_alloc, sizeof(karma_t)); 107 | nkarma = 0; 108 | } 109 | idx = find_karma(word); 110 | if (idx >= 0) { 111 | free(karma[idx].word); 112 | karma[idx] = karma[--nkarma]; 113 | return 1; 114 | } 115 | return 0; 116 | } 117 | 118 | /* 119 | *These functions are for sorting karma entries using C's built in qsort! 120 | */ 121 | 122 | static int karma_compare(const void *l, const void *r) 123 | { 124 | const karma_t *lhs = l, *rhs = r; 125 | return rhs->karma - lhs->karma; 126 | } 127 | 128 | static void karma_sort() 129 | { 130 | qsort(karma, nkarma, sizeof(karma_t), karma_compare); 131 | } 132 | 133 | /** 134 | * How many words to print karma of? 135 | */ 136 | #define KARMA_TOP 5 137 | 138 | /** 139 | * @brief Print the top KARMA_BEST words. 140 | * @param event The event we're responding to. 141 | */ 142 | static void karma_best(struct cbot_message_event *event) 143 | { 144 | size_t i; 145 | karma_sort(); 146 | for (i = 0; i < (nkarma > KARMA_TOP ? KARMA_TOP : nkarma); i++) { 147 | cbot_send_rl(event->bot, event->channel, "%d. %s (%d karma)", 148 | i + 1, karma[i].word, karma[i].karma); 149 | } 150 | } 151 | 152 | static void karma_check(struct cbot_message_event *event, void *user) 153 | { 154 | ssize_t index; 155 | char *word = sc_regex_get_capture(event->message, event->indices, 1); 156 | 157 | // An empty capture means we should list out the best karma. 158 | if (strcmp(word, "") == 0) { 159 | free(word); 160 | karma_best(event); 161 | return; 162 | } 163 | 164 | index = find_karma(word); 165 | if (index < 0) { 166 | cbot_send(event->bot, event->channel, "%s has no karma yet", 167 | word); 168 | } else { 169 | cbot_send(event->bot, event->channel, "%s has %d karma", word, 170 | karma[index].karma); 171 | } 172 | free(word); 173 | } 174 | 175 | static void karma_change(struct cbot_message_event *event, void *user) 176 | { 177 | char *word = sc_regex_get_capture(event->message, event->indices, 0); 178 | char *op = sc_regex_get_capture(event->message, event->indices, 1); 179 | int index = find_or_create_karma(word); 180 | karma[index].karma += (strcmp(op, "++") == 0 ? 1 : -1); 181 | free(word); 182 | free(op); 183 | } 184 | 185 | static void karma_set(struct cbot_message_event *event, void *user) 186 | { 187 | char *word, *value; 188 | int index; 189 | if (!cbot_is_authorized(event->bot, event->username, event->message)) { 190 | cbot_send(event->bot, event->channel, 191 | "sorry, you're not authorized to do that!"); 192 | return; 193 | } 194 | 195 | word = sc_regex_get_capture(event->message, event->indices, 0); 196 | value = sc_regex_get_capture(event->message, event->indices, 1); 197 | index = find_or_create_karma(word); 198 | karma[index].karma = atoi(value); 199 | free(word); 200 | free(value); 201 | } 202 | 203 | static void karma_forget(struct cbot_message_event *event, void *user) 204 | { 205 | delete_if_exists(event->username); 206 | } 207 | 208 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 209 | { 210 | #define KARMA_WORD "^ \t\n" 211 | #define NOT_KARMA_WORD " \t\n" 212 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)karma_check, NULL, 213 | "karma(\\s+([" KARMA_WORD "]+))?"); 214 | cbot_register(plugin, CBOT_MESSAGE, (cbot_handler_t)karma_change, NULL, 215 | ".*?([" KARMA_WORD "]+)(\\+\\+|--).*?"); 216 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)karma_set, NULL, 217 | "set-karma +([" KARMA_WORD "]+) +(-?\\d+) *.*"); 218 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)karma_forget, 219 | NULL, "forget[ -]me"); 220 | return 0; 221 | } 222 | 223 | static void help(struct cbot_plugin *plugin, struct sc_charbuf *cb) 224 | { 225 | sc_cb_concat( 226 | cb, 227 | "- WORD-- or WORD++: decrement or increment karma for WORD\n"); 228 | sc_cb_concat(cb, "- cbot karma WORD: check karma of WORD\n"); 229 | sc_cb_concat(cb, "- cbot karma: check top karma words\n"); 230 | sc_cb_concat(cb, "- cbot forget me: forget your username\n"); 231 | } 232 | 233 | struct cbot_plugin_ops ops = { 234 | .description = "track karma (++ or --) in a channel", 235 | .load = load, 236 | .help = help, 237 | }; 238 | -------------------------------------------------------------------------------- /plugin/log.c: -------------------------------------------------------------------------------- 1 | /** 2 | * log.c: CBot plugin which logs channel messages 3 | */ 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | 12 | #include "cbot/cbot.h" 13 | 14 | static void write_string(FILE *f, const char *str) 15 | { 16 | size_t i; 17 | fputc('"', f); 18 | for (i = 0; str[i]; i++) { 19 | switch (str[i]) { 20 | case '\n': 21 | fprintf(f, "\\n"); 22 | break; 23 | case '"': 24 | fprintf(f, "\""); 25 | break; 26 | default: 27 | fputc(str[i], f); 28 | } 29 | } 30 | fputc('"', f); 31 | } 32 | 33 | /* 34 | * For every channel message, get the current timestamp, open a file of the 35 | * form: CHANNEL-YYYY-MM-DD.log In append mode, and write out a single-line JSON 36 | * object, containing: 37 | * - timestamp: seconds since the epoch, as a float 38 | * - username: sender of the message 39 | * - message: content of message 40 | */ 41 | static void cbot_log_message(struct cbot_message_event *event, void *user) 42 | { 43 | #define NSEC_PER_SEC 10000000000.0 44 | struct timespec now; 45 | struct tm *tm; 46 | double time_float; 47 | struct sc_charbuf filename; 48 | FILE *f; 49 | 50 | /* 51 | * First get timestamp. 52 | */ 53 | clock_gettime(CLOCK_REALTIME, &now); 54 | tm = localtime(&now.tv_sec); 55 | time_float = now.tv_sec + now.tv_nsec / NSEC_PER_SEC; 56 | 57 | /* 58 | * Create filename and open it. 59 | */ 60 | sc_cb_init(&filename, 40); 61 | sc_cb_printf(&filename, "%s-%04d-%02d-%02d.log", event->channel, 62 | tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); 63 | f = fopen(filename.buf, "a"); 64 | 65 | if (f) { 66 | /* 67 | * Write log line. 68 | */ 69 | fprintf(f, "{\"timestamp\": %f, \"username\": ", time_float); 70 | write_string(f, event->username); 71 | fprintf(f, ", \"message\": "); 72 | write_string(f, event->message); 73 | if (event->is_action) 74 | fprintf(f, ", action: true"); 75 | fprintf(f, "}\n"); 76 | 77 | /* 78 | * Cleanup 79 | */ 80 | fclose(f); 81 | } 82 | sc_cb_destroy(&filename); 83 | } 84 | 85 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 86 | { 87 | cbot_register(plugin, CBOT_MESSAGE, (cbot_handler_t)cbot_log_message, 88 | NULL, NULL); 89 | return 0; 90 | } 91 | 92 | struct cbot_plugin_ops ops = { 93 | .description = "logs messages to a file", 94 | .load = load, 95 | }; 96 | -------------------------------------------------------------------------------- /plugin/name.c: -------------------------------------------------------------------------------- 1 | /** 2 | * name.c: CBot plugin which responds to questions about what CBot is, by 3 | * linking to the CBot repository 4 | */ 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include "cbot/cbot.h" 11 | 12 | struct cbot_karma_priv { 13 | struct cbot_plugin *plugin; 14 | struct cbot_handler *name_hdlr; 15 | }; 16 | 17 | static void name(struct cbot_message_event *event, void *user) 18 | { 19 | const char *botname = cbot_get_name(event->bot); 20 | cbot_send(event->bot, event->channel, 21 | "My name is %s, I am a cbot. My source lives at " 22 | "https://github.com/brenns10/cbot", 23 | botname); 24 | } 25 | 26 | static void register_name(struct cbot_karma_priv *priv) 27 | { 28 | const char *botname = cbot_get_name(priv->plugin->bot); 29 | struct sc_charbuf buf; 30 | sc_cb_init(&buf, 256); 31 | sc_cb_printf(&buf, 32 | "([wW]ho|[wW]hat|[wW][tT][fF])('?s?| +" 33 | "[iI]s| +[aA]re +[yY]ou,?) +(%s|cbot)\\??", 34 | botname); 35 | priv->name_hdlr = cbot_register(priv->plugin, CBOT_MESSAGE, 36 | (cbot_handler_t)name, NULL, buf.buf); 37 | sc_cb_destroy(&buf); 38 | } 39 | 40 | static void cbot_bot_name_change(struct cbot_nick_event *event, void *user) 41 | { 42 | struct cbot_karma_priv *priv = event->plugin->data; 43 | cbot_deregister(event->bot, priv->name_hdlr); 44 | register_name(priv); 45 | } 46 | 47 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 48 | { 49 | struct cbot_karma_priv *priv = 50 | calloc(1, sizeof(struct cbot_karma_priv)); 51 | priv->plugin = plugin; 52 | plugin->data = priv; 53 | register_name(priv); 54 | cbot_register(plugin, CBOT_BOT_NAME, 55 | (cbot_handler_t)cbot_bot_name_change, NULL, NULL); 56 | return 0; 57 | } 58 | 59 | static void unload(struct cbot_plugin *plugin) 60 | { 61 | free(plugin->data); 62 | } 63 | 64 | static void help(struct cbot_plugin *plugin, struct sc_charbuf *cb) 65 | { 66 | sc_cb_concat(cb, "- who/what/wtf is/are [you, ] cbot?\n"); 67 | sc_cb_concat(cb, " replies with bot name and github link\n"); 68 | } 69 | 70 | struct cbot_plugin_ops ops = { 71 | .description = "gives information about the bot", 72 | .load = load, 73 | .unload = unload, 74 | .help = help, 75 | }; 76 | -------------------------------------------------------------------------------- /plugin/reactrack.c: -------------------------------------------------------------------------------- 1 | /** 2 | * reactrack.c: CBot plugin which tests out the ability to receive reactions and 3 | * act upon them 4 | */ 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | static int react(struct cbot_reaction_event *event, void *arg) 12 | { 13 | if (event->remove) { 14 | cbot_send(event->plugin->bot, arg, 15 | "%s removed \"%s\" from my message!\n", event->source, 16 | event->emoji); 17 | } else if (strcmp(event->emoji, "🛑") == 0) { 18 | cbot_send(event->plugin->bot, arg, 19 | "%s has told me to stop watching for reacts", 20 | event->source); 21 | cbot_unregister_reaction(event->bot, event->handle); 22 | free(arg); 23 | } else { 24 | cbot_send(event->plugin->bot, arg, 25 | "%s reacted \"%s\" to my message!\n", event->source, 26 | event->emoji); 27 | } 28 | return 0; 29 | } 30 | 31 | static struct cbot_reaction_ops react_ops = { 32 | .plugin = NULL, 33 | .react_fn = react, 34 | }; 35 | 36 | static void reply(struct cbot_message_event *event, void *user) 37 | { 38 | void *arg = strdup(event->channel); 39 | cbot_sendr(event->bot, event->channel, &react_ops, arg, 40 | "React to this message"); 41 | } 42 | 43 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 44 | { 45 | react_ops.plugin = plugin; 46 | 47 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)reply, NULL, 48 | "react"); 49 | return 0; 50 | } 51 | 52 | struct cbot_plugin_ops ops = { 53 | .description = "notices when you react to its message", 54 | .load = load, 55 | }; 56 | -------------------------------------------------------------------------------- /plugin/reply.c: -------------------------------------------------------------------------------- 1 | /** 2 | * reply.c: CBot plugin which replies to configured triggers with configured 3 | * responses 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "cbot/cbot.h" 14 | 15 | struct rep { 16 | struct sc_list_head list; 17 | struct cbot_handler *hdlr; 18 | char *regex; 19 | int kind; 20 | int count; 21 | char *replies[0]; 22 | }; 23 | 24 | struct priv { 25 | struct cbot_plugin *plugin; 26 | struct sc_list_head replies; 27 | }; 28 | 29 | static int formatter(struct sc_charbuf *buf, char *key, void *user) 30 | { 31 | struct cbot_message_event *event = user; 32 | char *s; 33 | const char *cs; 34 | int cap, rv = 0; 35 | if (strcmp(key, "sender") == 0) { 36 | sc_cb_concat(buf, event->username); 37 | } else if (strcmp(key, "channel") == 0) { 38 | sc_cb_concat(buf, event->channel); 39 | } else if (strcmp(key, "bot") == 0) { 40 | cs = cbot_get_name(event->bot); 41 | sc_cb_concat(buf, cs); 42 | } else if (sscanf(key, "cap:%d", &cap) == 1) { 43 | if (cap < 0 || cap >= event->num_captures) 44 | return -1; 45 | s = sc_regex_get_capture(event->message, event->indices, 0); 46 | sc_cb_concat(buf, s); 47 | free(s); 48 | } else { 49 | rv = -1; 50 | } 51 | return rv; 52 | } 53 | 54 | static void handle_match(struct cbot_message_event *event, void *user) 55 | { 56 | struct rep *rep = user; 57 | int response = rand() % rep->count; 58 | struct sc_charbuf cb; 59 | int rv; 60 | 61 | sc_cb_init(&cb, 256); 62 | rv = cbot_format(&cb, rep->replies[response], formatter, event); 63 | if (rv >= 0) 64 | cbot_send(event->bot, event->channel, "%s", cb.buf); 65 | sc_cb_destroy(&cb); 66 | } 67 | 68 | static void destroy_replies(struct priv *priv) 69 | { 70 | struct rep *rep, *next; 71 | int i; 72 | sc_list_for_each_safe(rep, next, &priv->replies, list, struct rep) 73 | { 74 | cbot_deregister(priv->plugin->bot, rep->hdlr); 75 | for (i = 0; i < rep->count; i++) { 76 | free(rep->replies[i]); 77 | } 78 | free(rep->regex); 79 | free(rep); 80 | } 81 | } 82 | 83 | static struct rep *add_reply(struct priv *priv, config_setting_t *conf, int idx) 84 | { 85 | config_setting_t *replies, *el; 86 | struct rep *rep; 87 | const char *trigger, *resp; 88 | int reply_count, rv, i, addressed = false, kind = CBOT_MESSAGE; 89 | int insensitive = false; 90 | int flags = 0; 91 | 92 | rv = config_setting_lookup_string(conf, "trigger", &trigger); 93 | if (rv == CONFIG_FALSE) { 94 | fprintf(stderr, 95 | "plugin.reply.responses[%d].trigger does not " 96 | "exist or is not a string\n", 97 | idx); 98 | return NULL; 99 | } 100 | rv = config_setting_lookup_bool(conf, "addressed", &addressed); 101 | if (rv == CONFIG_FALSE) 102 | addressed = false; 103 | if (addressed) 104 | kind = CBOT_ADDRESSED; 105 | 106 | rv = config_setting_lookup_bool(conf, "insensitive", &insensitive); 107 | if (rv == CONFIG_FALSE) 108 | insensitive = false; 109 | if (insensitive) 110 | flags |= SC_RE_INSENSITIVE; 111 | 112 | rv = config_setting_lookup_string(conf, "response", &resp); 113 | if (rv == CONFIG_FALSE) { 114 | replies = config_setting_lookup(conf, "responses"); 115 | if (!replies || !config_setting_is_array(replies)) { 116 | fprintf(stderr, 117 | "plugin.reply.responses[%d] has " 118 | "neither string key 'response', or array " 119 | "'responses'\n", 120 | idx); 121 | return NULL; 122 | } 123 | reply_count = config_setting_length(replies); 124 | rep = calloc(1, sizeof(*rep) + reply_count * sizeof(char *)); 125 | rep->regex = strdup(trigger); 126 | rep->kind = kind; 127 | rep->count = reply_count; 128 | rep->hdlr = cbot_register2(priv->plugin, kind, 129 | (cbot_handler_t)handle_match, rep, 130 | (char *)trigger, flags); 131 | for (i = 0; i < reply_count; i++) { 132 | el = config_setting_get_elem(replies, i); 133 | resp = config_setting_get_string(el); 134 | if (!resp) { 135 | fprintf(stderr, 136 | "plugin.reply.responses[%d]" 137 | ".responses[%d] is not a string", 138 | idx, i); 139 | free(rep); 140 | return NULL; 141 | } 142 | rep->replies[i] = strdup(resp); 143 | } 144 | sc_list_insert_end(&priv->replies, &rep->list); 145 | return rep; 146 | } else { 147 | rep = calloc(1, sizeof(*rep) + 1 * sizeof(char *)); 148 | rep->regex = strdup(trigger); 149 | rep->kind = kind; 150 | rep->count = 1; 151 | rep->replies[0] = strdup(resp); 152 | rep->hdlr = cbot_register2(priv->plugin, kind, 153 | (cbot_handler_t)handle_match, rep, 154 | (char *)trigger, flags); 155 | sc_list_insert_end(&priv->replies, &rep->list); 156 | return rep; 157 | } 158 | } 159 | 160 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 161 | { 162 | struct priv *priv = calloc(1, sizeof(struct priv)); 163 | struct config_setting_t *arr, *elem; 164 | struct rep *rep; 165 | int i, len; 166 | priv->plugin = plugin; 167 | plugin->data = priv; 168 | sc_list_init(&priv->replies); 169 | 170 | arr = config_setting_lookup(conf, "responses"); 171 | if (!arr || !config_setting_is_list(arr)) { 172 | fprintf(stderr, "plugins.reply.responses does not exist or is " 173 | "not a list\n"); 174 | goto err; 175 | } 176 | 177 | len = config_setting_length(arr); 178 | for (i = 0; i < len; i++) { 179 | elem = config_setting_get_elem(arr, i); 180 | if (!elem || !config_setting_is_group(elem)) { 181 | fprintf(stderr, 182 | "plugin.reply.responses[%d] is not " 183 | "group", 184 | i); 185 | goto cleanup; 186 | } 187 | rep = add_reply(priv, elem, i); 188 | if (!rep) 189 | goto cleanup; 190 | } 191 | 192 | return 0; 193 | 194 | cleanup: 195 | destroy_replies(priv); 196 | err: 197 | free(priv); 198 | return -1; 199 | } 200 | 201 | static void unload(struct cbot_plugin *plugin) 202 | { 203 | struct priv *priv = plugin->data; 204 | destroy_replies(priv); 205 | free(priv); 206 | } 207 | 208 | static void help(struct cbot_plugin *plugin, struct sc_charbuf *cb) 209 | { 210 | struct priv *priv = plugin->data; 211 | struct rep *r; 212 | 213 | sc_cb_concat(cb, "This plugin will reply to the following triggers:\n"); 214 | sc_list_for_each_entry(r, &priv->replies, list, struct rep) 215 | { 216 | sc_cb_printf(cb, "- %s%s\n", r->regex, 217 | (r->kind == CBOT_ADDRESSED) 218 | ? " (only when @mentioned)" 219 | : ""); 220 | } 221 | } 222 | 223 | struct cbot_plugin_ops ops = { 224 | .description = "replies to configurable prompts", 225 | .load = load, 226 | .unload = unload, 227 | .help = help, 228 | }; 229 | -------------------------------------------------------------------------------- /plugin/sqlkarma.c: -------------------------------------------------------------------------------- 1 | /** 2 | * sqlkarma.c: CBot plugin which can track karma in SQL 3 | */ 4 | #include 5 | #include 6 | #include 7 | 8 | #include "cbot/cbot.h" 9 | #include "cbot/db.h" 10 | #include "sc-collections.h" 11 | #include "sc-regex.h" 12 | 13 | struct cbot; 14 | 15 | const char *tbl_karma_alters[] = {}; 16 | 17 | const struct cbot_db_table tbl_karma = { 18 | .name = "karma", 19 | .version = 0, 20 | .create = "CREATE TABLE karma ( " 21 | " item TEXT NOT NULL UNIQUE, " 22 | " karma INT NOT NULL " 23 | ");", 24 | .alters = tbl_karma_alters, 25 | }; 26 | 27 | struct karma { 28 | char *word; 29 | int karma; 30 | struct sc_list_head list; 31 | }; 32 | 33 | #define KARMA_TOP 5 34 | 35 | static int karma_query_get(struct cbot *bot, char *word, int *karma) 36 | { 37 | CBOTDB_QUERY_FUNC_BEGIN(bot, void, 38 | "SELECT karma FROM karma WHERE item=$word;"); 39 | CBOTDB_BIND_ARG(text, word); 40 | CBOTDB_SINGLE_INTPTR_RESULT(karma); 41 | } 42 | 43 | static int karma_query_del(struct cbot *bot, char *word) 44 | { 45 | CBOTDB_QUERY_FUNC_BEGIN(bot, void, 46 | "DELETE FROM karma WHERE item=$word;"); 47 | CBOTDB_BIND_ARG(text, word); 48 | CBOTDB_NO_RESULT(); 49 | } 50 | 51 | static int karma_query_update_by(struct cbot *bot, char *word, int adjust) 52 | { 53 | CBOTDB_QUERY_FUNC_BEGIN(bot, void, 54 | "INSERT INTO karma(item, karma) " 55 | "VALUES($word, $adjust) " 56 | "ON CONFLICT(item) DO UPDATE " 57 | "SET karma=karma + excluded.karma;"); 58 | CBOTDB_BIND_ARG(text, word); 59 | CBOTDB_BIND_ARG(int, adjust); 60 | CBOTDB_NO_RESULT(); 61 | } 62 | 63 | static int karma_query_set(struct cbot *bot, char *word, int value) 64 | { 65 | CBOTDB_QUERY_FUNC_BEGIN(bot, void, 66 | "INSERT INTO karma(item, karma) " 67 | "VALUES($word, $value) " 68 | "ON CONFLICT(item) DO UPDATE " 69 | "SET karma=excluded.karma;"); 70 | CBOTDB_BIND_ARG(text, word); 71 | CBOTDB_BIND_ARG(int, value); 72 | CBOTDB_NO_RESULT(); 73 | } 74 | 75 | static int karma_query_top(struct cbot *bot, int limit, 76 | struct sc_list_head *res) 77 | { 78 | CBOTDB_QUERY_FUNC_BEGIN(bot, struct karma, 79 | "SELECT item, karma FROM karma " 80 | "ORDER BY karma DESC LIMIT $limit;"); 81 | CBOTDB_BIND_ARG(int, limit); 82 | CBOTDB_LIST_RESULT(bot, res, 83 | /* noformat */ 84 | CBOTDB_OUTPUT(text, 0, word); 85 | CBOTDB_OUTPUT(int, 1, karma);); 86 | } 87 | 88 | static void karma_best(struct cbot_message_event *event) 89 | { 90 | struct sc_list_head res; 91 | struct karma *k, *n; 92 | 93 | sc_list_init(&res); 94 | karma_query_top(event->bot, KARMA_TOP, &res); 95 | sc_list_for_each_safe(k, n, &res, list, struct karma) 96 | { 97 | cbot_send_rl(event->bot, event->channel, "%s: %d", k->word, 98 | k->karma); 99 | free(k->word); 100 | free(k); 101 | } 102 | } 103 | 104 | static void karma_check(struct cbot_message_event *event, void *user) 105 | { 106 | int rv, karma = 0; 107 | char *word = sc_regex_get_capture(event->message, event->indices, 1); 108 | 109 | // An empty capture means we should list out the best karma. 110 | if (strcmp(word, "") == 0) { 111 | free(word); 112 | karma_best(event); 113 | return; 114 | } 115 | 116 | rv = karma_query_get(event->bot, word, &karma); 117 | if (rv < 0) { 118 | cbot_send(event->bot, event->channel, "%s has no karma yet", 119 | word); 120 | } else { 121 | cbot_send(event->bot, event->channel, "%s has %d karma", word, 122 | karma); 123 | } 124 | free(word); 125 | } 126 | 127 | static void karma_change(struct cbot_message_event *event, void *user) 128 | { 129 | char *word = sc_regex_get_capture(event->message, event->indices, 0); 130 | char *op = sc_regex_get_capture(event->message, event->indices, 1); 131 | int adj = (strcmp(op, "++") == 0 ? 1 : -1); 132 | karma_query_update_by(event->bot, word, adj); 133 | free(word); 134 | free(op); 135 | } 136 | 137 | static void karma_set(struct cbot_message_event *event, void *user) 138 | { 139 | char *word, *value; 140 | if (!cbot_is_authorized(event->bot, event->username, event->message)) { 141 | cbot_send(event->bot, event->channel, 142 | "sorry, you're not authorized to do that!"); 143 | return; 144 | } 145 | 146 | word = sc_regex_get_capture(event->message, event->indices, 0); 147 | value = sc_regex_get_capture(event->message, event->indices, 1); 148 | karma_query_set(event->bot, word, atoi(value)); 149 | free(word); 150 | free(value); 151 | } 152 | 153 | static void karma_forget(struct cbot_message_event *event, void *user) 154 | { 155 | karma_query_del(event->bot, (char *)event->username); 156 | } 157 | 158 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 159 | { 160 | int rv; 161 | 162 | (void)conf; 163 | 164 | rv = cbot_db_register(plugin, &tbl_karma); 165 | if (rv < 0) 166 | return rv; 167 | 168 | #define KARMA_WORD "^ \t\n" 169 | #define NOT_KARMA_WORD " \t\n" 170 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)karma_check, NULL, 171 | "karma(\\s+([" KARMA_WORD "]+))?"); 172 | cbot_register(plugin, CBOT_MESSAGE, (cbot_handler_t)karma_change, NULL, 173 | ".*?([" KARMA_WORD "]+)(\\+\\+|--).*?"); 174 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)karma_set, NULL, 175 | "set-karma +([" KARMA_WORD "]+) +(-?\\d+) *.*"); 176 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)karma_forget, 177 | NULL, "forget[ -]me"); 178 | 179 | return 0; 180 | } 181 | 182 | static void help(struct cbot_plugin *plugin, struct sc_charbuf *cb) 183 | { 184 | sc_cb_concat(cb, "This plugin is backed by the sqlite database, so any " 185 | "karma stored\n" 186 | "will be tracked even if CBot restarts!\n"); 187 | sc_cb_concat( 188 | cb, 189 | "- WORD-- or WORD++: decrement or increment karma for WORD\n"); 190 | sc_cb_concat(cb, "- cbot karma WORD: check karma of WORD\n"); 191 | sc_cb_concat(cb, "- cbot karma: check top karma words\n"); 192 | sc_cb_concat(cb, "- cbot forget me: forget your username\n"); 193 | } 194 | 195 | struct cbot_plugin_ops ops = { 196 | .description = "track karma (++ or --) in a channel", 197 | .load = load, 198 | .help = help, 199 | }; 200 | -------------------------------------------------------------------------------- /plugin/sqlknow.c: -------------------------------------------------------------------------------- 1 | /** 2 | * sqlknow.c: CBot plugin which remembers things 3 | */ 4 | #include 5 | #include 6 | 7 | #include "cbot/cbot.h" 8 | #include "cbot/db.h" 9 | #include "sc-collections.h" 10 | #include "sc-regex.h" 11 | 12 | struct cbot; 13 | 14 | const char *tbl_knowledge_alters[] = {}; 15 | 16 | const struct cbot_db_table tbl_knowledge = { 17 | .name = "knowledge", 18 | .version = 0, 19 | .create = "CREATE TABLE knowledge ( " 20 | " key TEXT NOT NULL UNIQUE, " 21 | " value TEXT NOT NULL, " 22 | " nick TEXT NOT NULL, " 23 | " change_count INT NOT NULL " 24 | ");", 25 | .alters = tbl_knowledge_alters, 26 | }; 27 | 28 | struct knowledge { 29 | char *key; 30 | char *value; 31 | char *nick; 32 | int change_count; 33 | }; 34 | 35 | static void knowledge_free(struct knowledge *k) 36 | { 37 | free(k->key); 38 | free(k->value); 39 | free(k->nick); 40 | free(k); 41 | } 42 | 43 | static struct knowledge *knowledge_query_get(struct cbot *bot, char *key) 44 | { 45 | CBOTDB_QUERY_FUNC_BEGIN(bot, struct knowledge, 46 | "SELECT key, value, nick, change_count " 47 | "FROM knowledge " 48 | "WHERE key=$key;"); 49 | CBOTDB_BIND_ARG(text, key); 50 | CBOTDB_SINGLE_STRUCT_RESULT( 51 | /* noformat */ 52 | CBOTDB_OUTPUT(text, 0, key); CBOTDB_OUTPUT(text, 1, value); 53 | CBOTDB_OUTPUT(text, 2, nick); 54 | CBOTDB_OUTPUT(int, 3, change_count);); 55 | } 56 | 57 | static int knowledge_query_update(struct cbot *bot, char *key, char *value, 58 | char *nick) 59 | { 60 | CBOTDB_QUERY_FUNC_BEGIN(bot, struct knowledge, 61 | "INSERT INTO knowledge(" 62 | " key, value, nick, change_count) " 63 | "VALUES($key, $value, $nick, 1) " 64 | "ON CONFLICT(key) DO UPDATE " 65 | "SET value=excluded.value, " 66 | " nick=excluded.nick, " 67 | " change_count=change_count + 1;"); 68 | CBOTDB_BIND_ARG(text, key); 69 | CBOTDB_BIND_ARG(text, value); 70 | CBOTDB_BIND_ARG(text, nick); 71 | CBOTDB_NO_RESULT(); 72 | } 73 | 74 | static int knowledge_query_delete(struct cbot *bot, char *key) 75 | { 76 | CBOTDB_QUERY_FUNC_BEGIN(bot, struct knowledge, 77 | "DELETE FROM knowledge " 78 | "WHERE key=$key;"); 79 | CBOTDB_BIND_ARG(text, key); 80 | CBOTDB_NO_RESULT(); 81 | } 82 | 83 | #define GET_VALUE ((void *)1) 84 | #define GET_WHO ((void *)2) 85 | 86 | static void knowledge_get(struct cbot_message_event *event, void *user) 87 | { 88 | char *key = sc_regex_get_capture(event->message, event->indices, 0); 89 | struct knowledge *k = knowledge_query_get(event->bot, key); 90 | if (!k) { 91 | cbot_send(event->bot, event->channel, 92 | "Sorry, I don't know anything about %s", key); 93 | } else if (user == GET_VALUE) { 94 | cbot_send(event->bot, event->channel, "%s is %s", key, 95 | k->value); 96 | } else { 97 | cbot_send(event->bot, event->channel, 98 | "%s last taught me that %s is %s, but I have been " 99 | "taught about it %d times", 100 | k->nick, k->key, k->value, k->change_count); 101 | } 102 | if (k) 103 | knowledge_free(k); 104 | free(key); 105 | } 106 | 107 | static void knowledge_set(struct cbot_message_event *event, void *user) 108 | { 109 | char *key = sc_regex_get_capture(event->message, event->indices, 0); 110 | char *value = sc_regex_get_capture(event->message, event->indices, 1); 111 | knowledge_query_update(event->bot, key, value, (char *)event->username); 112 | cbot_send(event->bot, event->channel, "ok, %s is %s", key, value); 113 | free(key); 114 | free(value); 115 | } 116 | 117 | static void knowledge_del(struct cbot_message_event *event, void *user) 118 | { 119 | if (!cbot_is_authorized(event->bot, event->username, event->message)) { 120 | cbot_send(event->bot, event->channel, 121 | "I'm sorry %s, I can't do that", event->username); 122 | return; 123 | } 124 | char *key = sc_regex_get_capture(event->message, event->indices, 0); 125 | struct knowledge *k = knowledge_query_get(event->bot, key); 126 | if (!k) { 127 | cbot_send(event->bot, event->channel, 128 | "Forget it? I didn't even know it!"); 129 | return; 130 | } 131 | knowledge_query_delete(event->bot, key); 132 | cbot_send(event->bot, event->channel, "you got it, boss"); 133 | free(key); 134 | knowledge_free(k); 135 | } 136 | 137 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 138 | { 139 | int rv; 140 | 141 | (void)conf; 142 | 143 | rv = cbot_db_register(plugin, &tbl_knowledge); 144 | if (rv < 0) 145 | return rv; 146 | 147 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)knowledge_set, 148 | NULL, "know that +(.+?) +is +(.+)"); 149 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)knowledge_get, 150 | GET_VALUE, "what is +(.+) *"); 151 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)knowledge_get, 152 | GET_WHO, "who taught you about +(.+)\\??"); 153 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)knowledge_del, 154 | GET_WHO, "forget +(.+)"); 155 | 156 | return 0; 157 | } 158 | 159 | static void help(struct cbot_plugin *plugin, struct sc_charbuf *cb) 160 | { 161 | sc_cb_concat( 162 | cb, 163 | "- cbot know that SOMETHING is DEFINITION: store knowledge\n"); 164 | sc_cb_concat(cb, "- cbot what is SOMETHING: return DEFINITION\n"); 165 | sc_cb_concat(cb, "- cbot who taugth you about SOMETHING: cbot is a " 166 | "tattle tale!\n"); 167 | } 168 | 169 | struct cbot_plugin_ops ops = { 170 | .description = "allows the bot to remember things", 171 | .load = load, 172 | .help = help, 173 | }; 174 | -------------------------------------------------------------------------------- /plugin/tok.c: -------------------------------------------------------------------------------- 1 | /** 2 | * tok.c: plugin for testing tokenizing 3 | * 4 | * Sample usage: 5 | * 6 | * user> cbot tok libstephen "hello ""world""" yay 7 | * cbot> [0]: libstephen 8 | * cbot> [1]: hello "world" 9 | * cbot> [2]: yay 10 | */ 11 | 12 | #include 13 | #include 14 | 15 | #include "cbot/cbot.h" 16 | 17 | static void tok(struct cbot_message_event *event, void *user) 18 | { 19 | struct cbot_tok tok; 20 | int rv = cbot_tokenize(event->message + 3, &tok); 21 | if (rv < 0) { 22 | cbot_send(event->bot, event->channel, "error: %d", rv); 23 | return; 24 | } 25 | for (int i = 0; i < tok.ntok; i++) 26 | cbot_send(event->bot, event->channel, "[%d]: %s", i, 27 | tok.tokens[i]); 28 | cbot_tok_destroy(&tok); 29 | } 30 | 31 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 32 | { 33 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)tok, NULL, 34 | "tok .*"); 35 | return 0; 36 | } 37 | 38 | struct cbot_plugin_ops ops = { 39 | .description = "a diagnostic plugin for testing tokenizers", 40 | .load = load, 41 | }; 42 | -------------------------------------------------------------------------------- /plugin/weather.c: -------------------------------------------------------------------------------- 1 | /* 2 | * weather.c: CBot plugin implementing weather queries 3 | */ 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "cbot/cbot.h" 16 | #include "cbot/curl.h" 17 | 18 | char *defloc = "San Francisco"; 19 | 20 | static void sc_cb_rstrip(struct sc_charbuf *buf, char *seq) 21 | { 22 | int i; 23 | bool contained; 24 | while (buf->length > 0) { 25 | contained = false; 26 | for (i = 0; seq[i]; i++) { 27 | if (buf->buf[buf->length - 1] == seq[i]) { 28 | contained = true; 29 | break; 30 | } 31 | } 32 | if (!contained) 33 | return; 34 | buf->buf[--buf->length] = '\0'; 35 | } 36 | } 37 | 38 | struct weather_req { 39 | struct cbot *bot; 40 | char *channel; 41 | char *loc; 42 | char *urlfmt; 43 | }; 44 | 45 | static char *mkurl(CURL *easy, const char *format, char *location) 46 | { 47 | char *encoded = curl_easy_escape(easy, location, 0); 48 | struct sc_charbuf buf; 49 | sc_cb_init(&buf, 256); 50 | sc_cb_printf(&buf, format, encoded); 51 | curl_free(encoded); 52 | return buf.buf; 53 | } 54 | 55 | static void do_weather(void *data) 56 | { 57 | struct sc_charbuf buf; 58 | struct weather_req *req = data; 59 | struct cbot *bot = req->bot; 60 | char *url = NULL; 61 | CURLcode rv; 62 | sc_cb_init(&buf, 256); 63 | CURL *easy = curl_easy_init(); 64 | url = mkurl(easy, req->urlfmt, *req->loc ? req->loc : defloc); 65 | curl_easy_setopt(easy, CURLOPT_URL, url); 66 | // curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L); 67 | cbot_curl_charbuf_response(easy, &buf); 68 | rv = cbot_curl_perform(bot, easy); 69 | if (rv != CURLE_OK) { 70 | fprintf(stderr, "curl: error: %s\n", curl_easy_strerror(rv)); 71 | goto out; 72 | } 73 | sc_cb_rstrip(&buf, " \t\r\n"); 74 | cbot_send(bot, req->channel, "%s", buf.buf); 75 | out: 76 | sc_cb_destroy(&buf); 77 | free(req->channel); 78 | free(req->loc); 79 | free(req); 80 | free(url); 81 | curl_easy_cleanup(easy); 82 | } 83 | 84 | static void weather(struct cbot_message_event *evt, void *user) 85 | { 86 | struct weather_req *req = calloc(1, sizeof(*req)); 87 | req->bot = evt->bot; 88 | req->channel = strdup(evt->channel); 89 | req->loc = sc_regex_get_capture(evt->message, evt->indices, 90 | evt->num_captures - 1); 91 | req->urlfmt = (char *)user; 92 | sc_lwt_create_task(cbot_get_lwt_ctx(evt->bot), do_weather, req); 93 | } 94 | 95 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 96 | { 97 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)weather, 98 | (void *)"https://wttr.in/%s?format=4", "weather *(.*)"); 99 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)weather, 100 | (void *)"https://wttr.in/" 101 | "%s?format=%%l:%%20sunrise:%%S%%20sunset:%%s", 102 | "(sunrise|sunset) *(.*)"); 103 | return 0; 104 | } 105 | 106 | static void help(struct cbot_plugin *plugin, struct sc_charbuf *cb) 107 | { 108 | sc_cb_concat(cb, "- cbot weather: print weather in SF\n"); 109 | sc_cb_concat( 110 | cb, 111 | "- cbot weather LOCATION: weather in LOCATION (zip code?)\n"); 112 | sc_cb_concat( 113 | cb, 114 | "- cbot sunrise/sunset [LOCATION]: give sunrise/sunset info\n"); 115 | } 116 | 117 | struct cbot_plugin_ops ops = { 118 | .description = "lookup the weaither", 119 | .load = load, 120 | .help = help, 121 | }; 122 | -------------------------------------------------------------------------------- /plugin/who.c: -------------------------------------------------------------------------------- 1 | /** 2 | * who.c: CBot plugin which lists members of this channel 3 | * 4 | * Sample usage: 5 | * 6 | * user> hi cbot 7 | * cbot> hello, user! 8 | */ 9 | 10 | #include 11 | 12 | #include 13 | 14 | #include 15 | 16 | #include "cbot/cbot.h" 17 | 18 | static void who(struct cbot_message_event *event, void *user) 19 | { 20 | struct sc_list_head list; 21 | struct cbot_user_info *ui; 22 | struct sc_charbuf buf; 23 | 24 | sc_list_init(&list); 25 | sc_cb_init(&buf, 512); 26 | 27 | cbot_get_members(event->bot, (char *)event->channel, &list); 28 | cbot_send(event->bot, event->channel, 29 | "Members of channel %s (\"censoring\" to avoid ping)", 30 | event->channel); 31 | sc_list_for_each_entry(ui, &list, list, struct cbot_user_info) 32 | { 33 | sc_cb_printf(&buf, "%c*%s ", ui->username[0], &ui->username[1]); 34 | if (buf.length >= 500) { 35 | cbot_send(event->bot, event->channel, buf.buf); 36 | sc_cb_clear(&buf); 37 | } 38 | } 39 | if (buf.length > 0) { 40 | cbot_send(event->bot, event->channel, buf.buf); 41 | } 42 | sc_cb_destroy(&buf); 43 | } 44 | 45 | static int load(struct cbot_plugin *plugin, config_setting_t *conf) 46 | { 47 | cbot_register(plugin, CBOT_ADDRESSED, (cbot_handler_t)who, NULL, 48 | "[wW]ho\\??"); 49 | return 0; 50 | } 51 | 52 | struct cbot_plugin_ops ops = { 53 | .description = "diagnostic: list everyone in the channel", 54 | .load = load, 55 | }; 56 | -------------------------------------------------------------------------------- /sample.cfg: -------------------------------------------------------------------------------- 1 | cbot: { 2 | // Set the chatbot nick. This could change during execution. Unfortunately, 3 | // if the bot name changes at runtime, the configuration does not get updated, 4 | // and the bot name will revert on next run. 5 | name = "cbot"; 6 | 7 | // Channel listing, passwords supported 8 | channels = ( 9 | { name = "#stdin" } 10 | ); 11 | 12 | // Choose backend from the list in cbot.c. Whichever one you choose needs to 13 | // have a configuration group below. 14 | backend = "cli"; 15 | 16 | // Where are the plugin .so objects? 17 | plugin_dir = "build"; 18 | 19 | // Filename for the sqlite database (currently unused). 20 | db = "cli.sqlite3"; 21 | 22 | // Set log filename (:stderr: is special, and default) 23 | log_file = ":stderr:"; 24 | 25 | // Set log level 26 | log_level = "INFO"; 27 | }; 28 | 29 | // Configuration options for the IRC backend 30 | irc: { 31 | // Use a # at the beginning for SSL 32 | host = "#example.com"; 33 | port = 6697; 34 | password = "hunter2"; 35 | }; 36 | 37 | // Configuration options for the CLI backend. There are none, but if you specify 38 | // backend="cli" above, you still need to provide an empty configuration group 39 | // here. 40 | cli: {} 41 | 42 | // Configuration options for the Signald backend. 43 | signal: { 44 | // Phone number of our user. 45 | phone = "+12223334444"; 46 | // Uuid of our user. You should get this from signal-cli or signald 47 | uuid = "00000000-0000-0000-0000-000000000000"; 48 | // Uuid of an "authorized user" 49 | auth = "00000000-0000-0000-0000-000000000000"; 50 | // Which bridge implementation? signald or signal-cli 51 | bridge = "signal-cli"; 52 | // Unix socket to talk to Signald. (if you specify signald above) 53 | signald_socket = "/var/run/signal/signald.sock"; 54 | // Command to run signal-cli (if you specify signal-cli above) 55 | signalcli_cmd = "path/to/signal-cli -a +12223334444 jsonRpc"; 56 | } 57 | 58 | // Finally, the plugin list. Plugin names must be valid C identifiers. Each 59 | // plugin must be mapped to a configuration group, even if it accepts no 60 | // configuration. 61 | plugins: { 62 | aqi: { 63 | // grab a token easily from: https://aqicn.org 64 | token = "blah"; 65 | }; 66 | emote: {}; 67 | greet: {}; 68 | help: {}; 69 | ircctl: {}; 70 | sqlkarma: {}; 71 | name: {}; 72 | become: {}; 73 | who: {}; 74 | annoy: {}; 75 | reply: { 76 | responses: ( 77 | { trigger = "lod (.*)", response = "{cap:0}: ಠ_ಠ", addressed = true }, 78 | { 79 | trigger = "magic8 .*"; 80 | addressed = true; 81 | responses = [ 82 | "It is certain.", 83 | "It is decidedly so.", 84 | "Without a doubt.", 85 | "Yes - definitely.", 86 | "You may rely on it.", 87 | "As I see it, yes.", 88 | "Most likely.", 89 | "Outlook good.", 90 | "Yes.", 91 | "Signs point to yes.", 92 | "Reply hazy, try again.", 93 | "Ask again later.", 94 | "Better not tell you now.", 95 | "Cannot predict now.", 96 | "Concentrate and ask again.", 97 | "Don't count on it.", 98 | "My reply is no.", 99 | "My sources say no.", 100 | "Outlook not so good.", 101 | "Very doubtful." 102 | ]; 103 | }, 104 | { 105 | trigger = "[Yy]ou +[Ss]uck[!.]?|[Ss]ucks[!.]?" 106 | "|[Ii] +[Hh]ate +[Yy]ou[!.]?|[Ss]hut [Uu]p[!.]?"; 107 | addressed = true; 108 | responses = [ 109 | ":(", 110 | "I don't like you, {sender}", 111 | ]; 112 | } 113 | ); 114 | }; 115 | sqlknow: {}; 116 | }; 117 | -------------------------------------------------------------------------------- /src/cbot_cli.h: -------------------------------------------------------------------------------- 1 | /** 2 | * cbot_cli.h 3 | */ 4 | 5 | #ifndef CBOT_CLI_H 6 | #define CBOT_CLI_H 7 | 8 | void run_cbot_cli(int argc, char **argv); 9 | 10 | #endif // CBOT_CLI_H 11 | -------------------------------------------------------------------------------- /src/cbot_handlers.h: -------------------------------------------------------------------------------- 1 | /** 2 | * cbot_handlers.h 3 | */ 4 | 5 | #ifndef CBOT_HANDLERS_H 6 | #define CBOT_HANDLERS_H 7 | 8 | #include "cbot/cbot.h" 9 | 10 | void cbot_handlers_register(struct cbot *bot); 11 | 12 | #endif // CBOT_HANDLERS_H 13 | -------------------------------------------------------------------------------- /src/cbot_irc.h: -------------------------------------------------------------------------------- 1 | /** 2 | * cbot_irc.h 3 | */ 4 | 5 | #ifndef CBOT_IRC_H 6 | #define CBOT_IRC_H 7 | 8 | #include 9 | #include 10 | 11 | #include "cbot/cbot.h" 12 | #include "libircclient.h" 13 | 14 | struct names_rq { 15 | struct sc_list_head list; 16 | char *channel; 17 | struct sc_charbuf names; 18 | }; 19 | 20 | struct cbot_irc_backend { 21 | irc_session_t *session; 22 | irc_callbacks_t callbacks; 23 | struct cbot *bot; 24 | bool connected; 25 | struct sc_list_head join_rqs; 26 | struct sc_list_head topic_rqs; 27 | struct sc_list_head names_rqs; 28 | char *host; 29 | int port; 30 | char *password; 31 | }; 32 | 33 | #endif // CBOT_IRC_H 34 | -------------------------------------------------------------------------------- /src/cbot_private.h: -------------------------------------------------------------------------------- 1 | /** 2 | * cbot_private.h 3 | */ 4 | 5 | #ifndef CBOT_PRIVATE_H 6 | #define CBOT_PRIVATE_H 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include "cbot/cbot.h" 21 | 22 | struct cbot_plugpriv; 23 | 24 | struct cbot_handler { 25 | /* Function called by CBot */ 26 | cbot_handler_t handler; 27 | /* User data for the handler */ 28 | void *user; 29 | /* Plugin owning this */ 30 | struct cbot_plugpriv *plugin; 31 | /* Optionally, a regex which must match in order to be called. */ 32 | struct sc_regex *regex; 33 | /* List containing all handlers for this event. */ 34 | struct sc_list_head handler_list; 35 | /* List containing all handlers for this plugin. */ 36 | struct sc_list_head plugin_list; 37 | }; 38 | 39 | struct cbot_plugpriv { 40 | struct cbot_plugin p; 41 | /* Name of the plugin */ 42 | char *name; 43 | /* Redundant, but the plugin could modify p.bot */ 44 | struct cbot *bot; 45 | /* List of handlers provided */ 46 | struct sc_list_head handlers; 47 | /* List of plugins */ 48 | struct sc_list_head list; 49 | /* dlopen handle */ 50 | void *handle; 51 | }; 52 | 53 | struct cbot_backend_ops { 54 | const char *name; 55 | int (*configure)(struct cbot *cbot, config_setting_t *group); 56 | void (*run)(struct cbot *cbot); 57 | uint64_t (*send)(const struct cbot *cbot, const char *to, 58 | const struct cbot_reaction_ops *ops, void *arg, 59 | const char *msg); 60 | void (*me)(const struct cbot *cbot, const char *to, const char *msg); 61 | void (*op)(const struct cbot *cbot, const char *channel, 62 | const char *username); 63 | void (*join)(const struct cbot *cbot, const char *channel, 64 | const char *password); 65 | void (*nick)(const struct cbot *cbot, const char *newnick); 66 | int (*is_authorized)(const struct cbot *bot, const char *sender, 67 | const char *message); 68 | void (*unregister_reaction)(const struct cbot *bot, uint64_t id); 69 | }; 70 | 71 | extern struct cbot_backend_ops irc_ops; 72 | extern struct cbot_backend_ops cli_ops; 73 | extern struct cbot_backend_ops signald_ops; 74 | 75 | struct cbot_channel_conf { 76 | char *name; 77 | char *pass; 78 | struct sc_list_head list; 79 | }; 80 | 81 | struct cbot_http; 82 | 83 | struct cbot_callback { 84 | struct sc_list_head list; 85 | struct cbot_plugin *plugin; 86 | void *arg; 87 | time_t when; 88 | void (*func)(struct cbot_plugin *plugin, void *arg); 89 | }; 90 | 91 | struct cbot { 92 | /* Loaded from configuration */ 93 | char *name; 94 | struct sc_array aliases; 95 | char *backend_name; 96 | char *plugin_dir; 97 | char *db_file; 98 | struct sc_list_head init_channels; 99 | 100 | struct sc_list_head handlers[_CBOT_NUM_EVENT_TYPES_]; 101 | struct sc_list_head plugins; 102 | uint8_t hash[20]; 103 | struct cbot_backend_ops *backend_ops; 104 | void *backend; 105 | sqlite3 *privDb; 106 | struct sc_lwt_ctx *lwt_ctx; 107 | struct sc_lwt *lwt; 108 | 109 | struct sc_lwt *msgq_thread; 110 | struct sc_list_head msgq; 111 | 112 | CURLM *curlm; 113 | struct sc_lwt *curl_lwt; 114 | 115 | struct MHD_Daemon *http; 116 | struct sc_lwt *http_lwt; 117 | struct cbot_http *httpriv; 118 | 119 | struct sc_list_head callback_list; 120 | struct sc_lwt *callback_lwt; 121 | bool callback_touched; 122 | }; 123 | 124 | struct cbot *cbot_create(void); 125 | int cbot_load_config(struct cbot *bot, const char *conf_file); 126 | void cbot_run(struct cbot *bot); 127 | void cbot_delete(struct cbot *obj); 128 | 129 | void cbot_set_nick(struct cbot *bot, const char *newname); 130 | 131 | struct cbot_handler *cbot_register_priv(struct cbot *bot, 132 | struct cbot_plugpriv *priv, 133 | enum cbot_event_type type, 134 | cbot_handler_t handler, void *user, 135 | char *regex, int re_flags); 136 | 137 | /* Functions which backends can call, to trigger various types of events */ 138 | void cbot_handle_message(struct cbot *bot, const char *channel, 139 | const char *user, const char *message, bool action, 140 | bool is_dm); 141 | void cbot_handle_user_event(struct cbot *bot, const char *channel, 142 | const char *user, enum cbot_event_type type); 143 | void cbot_handle_nick_event(struct cbot *bot, const char *old_username, 144 | const char *new_username); 145 | 146 | void *base64_decode(const char *str, int explen); 147 | 148 | /******* 149 | * Database functions! 150 | *******/ 151 | int cbot_add_membership(struct cbot *bot, char *nick, char *chan); 152 | int cbot_clear_channel_memberships(struct cbot *bot, char *chan); 153 | int cbot_set_channel_topic(struct cbot *bot, char *chan, char *topic); 154 | int cbot_db_init(struct cbot *bot); 155 | int cbot_db_register_internal(struct cbot *bot, 156 | const struct cbot_db_table *tbl); 157 | 158 | /****** 159 | * Curl functions ! 160 | ******/ 161 | int cbot_curl_init(struct cbot *bot); 162 | 163 | #define nelem(arr) (sizeof(arr) / sizeof(arr[0])) 164 | 165 | void run_cbot_irc(int argc, char *argv[]); 166 | void run_cbot_cli(int argc, char **argv); 167 | 168 | #define plugpriv(plug) ((struct cbot_plugpriv *)plug) 169 | 170 | int cbot_http_init(struct cbot *bot, config_setting_t *group); 171 | void cbot_http_destroy(struct cbot *bot); 172 | 173 | #endif // CBOT_PRIVATE_H 174 | -------------------------------------------------------------------------------- /src/curl.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Thin wrapping over the libcurl multi API. 3 | */ 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | 17 | #include "cbot/cbot.h" 18 | #include "cbot/curl.h" 19 | #include "cbot_private.h" 20 | 21 | static ssize_t write_cb(char *data, size_t size, size_t nmemb, void *user) 22 | { 23 | struct sc_charbuf *buf = user; 24 | sc_cb_memcpy(buf, data, size * nmemb); 25 | return size * nmemb; 26 | } 27 | 28 | void cbot_curl_charbuf_response(CURL *easy, struct sc_charbuf *buf) 29 | { 30 | curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_cb); 31 | curl_easy_setopt(easy, CURLOPT_WRITEDATA, buf); 32 | } 33 | 34 | struct sc_list_head waitlist; 35 | 36 | struct curl_waiting { 37 | struct sc_list_head list; 38 | CURL *handle; 39 | struct sc_lwt *thread; 40 | CURLcode result; 41 | bool done; 42 | }; 43 | 44 | CURLcode cbot_curl_perform(struct cbot *bot, CURL *handle) 45 | { 46 | struct curl_waiting wait; 47 | bool first = true; 48 | wait.handle = handle; 49 | wait.done = false; 50 | wait.thread = sc_lwt_current(); 51 | sc_list_init(&wait.list); 52 | curl_easy_setopt(handle, CURLOPT_PRIVATE, (char *)&wait); 53 | curl_multi_add_handle(bot->curlm, handle); 54 | sc_lwt_set_state(bot->curl_lwt, SC_LWT_RUNNABLE); 55 | sc_list_insert_end(&waitlist, &wait.list); 56 | while (!wait.done) { 57 | CL_VERB("curl: %s request, yielding\n", 58 | first ? "enqueued" : "continue"); 59 | first = false; 60 | /* 61 | * The LWT system may wake us up in the case of a shutdown. 62 | * The CURL thread will also get woken up, and it will do 63 | * cleanup, and then re-wake us up. So, if we're woken up 64 | * without the done flag set, we should continue to wait. 65 | */ 66 | sc_lwt_set_state(wait.thread, SC_LWT_BLOCKED); 67 | sc_lwt_yield(); 68 | CL_VERB("curl: wakeup, done? %s\n", wait.done ? "yes" : "no"); 69 | } 70 | return wait.result; 71 | } 72 | 73 | char *cbot_curl_get(struct cbot *bot, const char *url, ...) 74 | { 75 | va_list vl; 76 | struct sc_charbuf url_fmt, resp; 77 | 78 | va_start(vl, url); 79 | sc_cb_init(&url_fmt, 256); 80 | sc_cb_vprintf(&url_fmt, url, vl); 81 | va_end(vl); 82 | CL_DEBUG("cURL: %s\n", url_fmt.buf); 83 | 84 | CURL *easy = curl_easy_init(); 85 | curl_easy_setopt(easy, CURLOPT_URL, url_fmt.buf); 86 | sc_cb_init(&resp, 4096); 87 | cbot_curl_charbuf_response(easy, &resp); 88 | CURLcode rv = cbot_curl_perform(bot, easy); 89 | char *result = resp.buf; 90 | if (rv != CURLE_OK) { 91 | CL_WARN("curl: error: %s\n", curl_easy_strerror(rv)); 92 | result = NULL; 93 | sc_cb_destroy(&resp); 94 | } 95 | curl_easy_cleanup(easy); 96 | sc_cb_destroy(&url_fmt); 97 | return result; 98 | } 99 | 100 | void cbot_curl_run(void *data) 101 | { 102 | struct cbot *bot = data; 103 | struct sc_lwt *cur = sc_lwt_current(); 104 | struct curl_waiting *waiting, *next; 105 | struct timespec ts; 106 | int nhdl, maxfd; 107 | long millis; 108 | bool block; 109 | fd_set in_fd, out_fd, err_fd; 110 | CURLMcode rv; 111 | CURLMsg *msg; 112 | 113 | bot->curl_lwt = cur; 114 | sc_list_init(&waitlist); 115 | 116 | while (true) { 117 | /* 118 | * First, we should mark each file descriptor that curl would 119 | * like us to wait on, in case we end up blocking. 120 | */ 121 | sc_lwt_fdgen_advance(cur); 122 | sc_lwt_clear_fds(&in_fd, &out_fd, &err_fd); 123 | maxfd = 0; 124 | curl_multi_fdset(bot->curlm, &in_fd, &out_fd, &err_fd, &maxfd); 125 | sc_lwt_add_select_fds(cur, &in_fd, &out_fd, &err_fd, maxfd, 126 | NULL); 127 | sc_lwt_fdgen_purge(cur); 128 | 129 | /* 130 | * Sometimes, curl doesn't actually want to block, or wants us 131 | * to block but do a timeout. We need to ask it how long it 132 | * wants to wait. 133 | */ 134 | block = true; 135 | millis = 0; 136 | curl_multi_timeout(bot->curlm, &millis); 137 | /* Clear any previously held timeout for our thread */ 138 | sc_lwt_cleartimeout(cur); 139 | if (millis > 0) { 140 | ts.tv_sec = millis / 1000; 141 | ts.tv_nsec = millis * 1000000; 142 | sc_lwt_settimeout(cur, &ts); 143 | CL_VERB("curlthread: set timeout %d millis\n", millis); 144 | } else if (millis == 0) { 145 | block = false; 146 | } 147 | 148 | /* Only block if curl gave a non-zero timeout */ 149 | if (block) { 150 | CL_VERB("curlthread: yielding\n"); 151 | sc_lwt_set_state(cur, SC_LWT_BLOCKED); 152 | sc_lwt_yield(); 153 | CL_VERB("curlthread: wake up\n"); 154 | if (sc_lwt_shutting_down()) { 155 | CL_DEBUG("curlthread: shutting down\n"); 156 | break; 157 | } 158 | } 159 | 160 | /* Now actually drive curl connections forward. */ 161 | rv = curl_multi_perform(bot->curlm, &nhdl); 162 | if (rv != CURLM_OK) { 163 | CL_CRIT("curlm error %d: %s\n", rv, 164 | curl_multi_strerror(rv)); 165 | break; 166 | } 167 | 168 | /* 169 | * Finally, we must read messages from CURL about which 170 | * connections were completed, etc. 171 | */ 172 | do { 173 | msg = curl_multi_info_read(bot->curlm, &nhdl); 174 | if (msg && msg->msg == CURLMSG_DONE) { 175 | curl_easy_getinfo(msg->easy_handle, 176 | CURLINFO_PRIVATE, &waiting); 177 | curl_multi_remove_handle(bot->curlm, 178 | waiting->handle); 179 | waiting->result = msg->data.result; 180 | waiting->done = true; 181 | sc_list_remove(&waiting->list); 182 | sc_lwt_set_state(waiting->thread, 183 | SC_LWT_RUNNABLE); 184 | } 185 | } while (msg); 186 | 187 | /* 188 | * Clear file descriptors for this thread until next time. 189 | */ 190 | sc_lwt_remove_all(cur); 191 | } 192 | 193 | sc_list_for_each_safe(waiting, next, &waitlist, list, 194 | struct curl_waiting) 195 | { 196 | CL_DEBUG("curlthread: cancel and remove CURL handle+thread\n"); 197 | sc_list_remove(&waiting->list); 198 | curl_multi_remove_handle(bot->curlm, waiting->handle); 199 | waiting->result = CURLE_READ_ERROR; 200 | waiting->done = true; 201 | sc_lwt_set_state(waiting->thread, SC_LWT_RUNNABLE); 202 | } 203 | curl_multi_cleanup(bot->curlm); 204 | bot->curlm = NULL; 205 | } 206 | 207 | int cbot_curl_init(struct cbot *bot) 208 | { 209 | bot->curlm = curl_multi_init(); 210 | bot->curl_lwt = sc_lwt_create_task(bot->lwt_ctx, cbot_curl_run, bot); 211 | return 0; 212 | } 213 | -------------------------------------------------------------------------------- /src/fmt.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "cbot/cbot.h" 5 | 6 | int cbot_format(struct sc_charbuf *buf, const char *fmt, 7 | cbot_formatter_t formatter, void *user) 8 | { 9 | const char *c, *d; 10 | struct sc_charbuf cb; 11 | int rv, count = 0; 12 | sc_cb_init(&cb, 64); 13 | while (*fmt) { 14 | c = strchr(fmt, '{'); 15 | if (!c) { 16 | sc_cb_concat(buf, fmt); 17 | rv = count; 18 | goto out; 19 | } 20 | if (c[1] == '{') { 21 | sc_cb_append(buf, '{'); 22 | fmt = &c[2]; 23 | continue; 24 | } 25 | sc_cb_memcpy(buf, fmt, c - fmt); 26 | d = strchr(c, '}'); 27 | if (!d) { 28 | rv = -1; 29 | goto out; 30 | } 31 | sc_cb_clear(&cb); 32 | sc_cb_memcpy(&cb, c + 1, d - c - 1); 33 | rv = formatter(buf, cb.buf, user); 34 | count += 1; 35 | if (rv < 0) 36 | goto out; 37 | fmt = d + 1; 38 | } 39 | rv = count; 40 | out: 41 | sc_cb_destroy(&cb); 42 | return rv; 43 | } 44 | -------------------------------------------------------------------------------- /src/http.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Integrating libmicrohttpd with cbot 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "cbot/cbot.h" 15 | #include "cbot_private.h" 16 | #include "sc-collections.h" 17 | #include "sc-lwt.h" 18 | #include "sc-regex.h" 19 | 20 | #define PORT 8888 21 | 22 | struct cbot_http { 23 | char *url; 24 | }; 25 | 26 | static int method_to_event(const char *method) 27 | { 28 | if (strcmp(method, "GET") == 0) 29 | return CBOT_HTTP_GET; 30 | return -1; 31 | } 32 | 33 | static struct cbot_handler *lookup_handler(struct sc_list_head *lh, 34 | size_t **indices, const char *method, 35 | const char *url) 36 | { 37 | struct cbot_handler *h; 38 | ssize_t result; 39 | sc_list_for_each_entry(h, lh, handler_list, struct cbot_handler) 40 | { 41 | // No regex matches everything. This probably shouldn't be 42 | // allowed... 43 | if (!h->regex) 44 | return h; 45 | result = sc_regex_exec(h->regex, url, indices); 46 | if (result != -1 && url[result] == '\0') 47 | return h; 48 | } 49 | return NULL; 50 | } 51 | 52 | static int hdlr(void *cls, struct MHD_Connection *connection, const char *url, 53 | const char *method, const char *version, 54 | const char *upload_data, size_t *upload_data_size, 55 | void **con_cls) 56 | { 57 | static int aptr; 58 | const char *notfound = "

Not Found

" 59 | "

CBot ain't got that URL

"; 60 | int evt; 61 | struct cbot *bot = (struct cbot *)cls; 62 | struct cbot_handler *h = NULL; 63 | size_t *indices = NULL; 64 | struct cbot_http_event event; 65 | struct MHD_Response *resp; 66 | enum MHD_Result ret; 67 | 68 | evt = method_to_event(method); 69 | 70 | if (evt != -1) 71 | h = lookup_handler(&bot->handlers[evt], &indices, method, url); 72 | 73 | if (!h) 74 | h = lookup_handler(&bot->handlers[CBOT_HTTP_ANY], &indices, 75 | method, url); 76 | 77 | if (!h) { 78 | /* No registered handler! Return 404. */ 79 | resp = MHD_create_response_from_buffer(strlen(notfound), 80 | (void *)notfound, 81 | MHD_RESPMEM_PERSISTENT); 82 | MHD_add_response_header(resp, "Content-Type", 83 | "text/html; charset=utf-8"); 84 | ret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, resp); 85 | MHD_destroy_response(resp); 86 | return ret; 87 | } else if (*con_cls != &aptr) { 88 | /* We have a registered handler. Continue the connection. */ 89 | *con_cls = &aptr; 90 | return MHD_YES; 91 | } 92 | 93 | /* We have a handler, and the request data has arrived. Dispatch the 94 | * handler. */ 95 | event.bot = bot; 96 | event.plugin = &h->plugin->p; 97 | event.num_captures = 0; 98 | if (h->regex) 99 | event.num_captures = sc_regex_num_captures(h->regex); 100 | event.url = url; 101 | event.indices = indices; 102 | event.connection = connection; 103 | event.method = method; 104 | event.version = version; 105 | event.upload_data = upload_data; 106 | event.upload_data_size = *upload_data_size; 107 | h->handler((struct cbot_event *)&event, h->user); 108 | 109 | free(indices); 110 | *con_cls = NULL; /* reset con_cls when done */ 111 | return MHD_YES; /* TODO: get return value from handler */ 112 | } 113 | 114 | static void cbot_http_run(void *data) 115 | { 116 | struct sc_lwt *cur = sc_lwt_current(); 117 | struct cbot *bot = data; 118 | struct MHD_Daemon *daemon = bot->http; 119 | fd_set in_fd, out_fd, err_fd; 120 | struct timespec ts; 121 | int maxfd, rv; 122 | unsigned long long req_to; 123 | 124 | while (true) { 125 | sc_lwt_fdgen_advance(cur); 126 | maxfd = 0; 127 | sc_lwt_clear_fds(&in_fd, &out_fd, &err_fd); 128 | rv = MHD_get_fdset(daemon, &in_fd, &out_fd, &err_fd, &maxfd); 129 | if (rv == MHD_NO) { 130 | fprintf(stderr, "MHD_get_fdset says no\n"); 131 | } 132 | sc_lwt_add_select_fds(cur, &in_fd, &out_fd, &err_fd, maxfd, 133 | NULL); 134 | sc_lwt_fdgen_purge(cur); 135 | 136 | rv = MHD_get_timeout(daemon, &req_to); 137 | if (rv == MHD_YES) { 138 | ts.tv_sec = req_to / 1000; 139 | ts.tv_nsec = req_to * 1000000; 140 | sc_lwt_settimeout(cur, &ts); 141 | } 142 | 143 | sc_lwt_set_state(cur, SC_LWT_BLOCKED); 144 | sc_lwt_yield(); 145 | if (sc_lwt_shutting_down()) 146 | break; 147 | 148 | rv = MHD_run(daemon); 149 | if (rv == MHD_NO) { 150 | fprintf(stderr, "MHD_run says no\n"); 151 | } 152 | } 153 | MHD_stop_daemon(daemon); 154 | } 155 | 156 | static void cbot_http_root(struct cbot_http_event *evt, void *unused) 157 | { 158 | const char *me = "Hello, browser."; 159 | struct MHD_Response *resp; 160 | resp = MHD_create_response_from_buffer(strlen(me), (void *)me, 161 | MHD_RESPMEM_PERSISTENT); 162 | MHD_add_response_header(resp, "Content-Type", 163 | "text/html; charset=utf-8"); 164 | MHD_queue_response(evt->connection, MHD_HTTP_OK, resp); 165 | MHD_destroy_response(resp); 166 | } 167 | 168 | const char *cbot_http_geturl(struct cbot *bot) 169 | { 170 | if (bot->httpriv) 171 | return bot->httpriv->url; 172 | return NULL; 173 | } 174 | 175 | void cbot_http_destroy(struct cbot *bot) 176 | { 177 | if (bot->httpriv) { 178 | free(bot->httpriv->url); 179 | free(bot->httpriv); 180 | } 181 | } 182 | 183 | void cbot_http_plainresp_start(struct sc_charbuf *cb, const char *title) 184 | { 185 | sc_cb_init(cb, 1024); 186 | sc_cb_printf(cb, "%s
\n",
187 | 	             title);
188 | }
189 | 
190 | void sc_cb_concat_http_esc(struct sc_charbuf *cb, const char *data)
191 | {
192 | 	char *lcaret, *rcaret, *next;
193 | 	do {
194 | 		lcaret = strchr(data, '<');
195 | 		rcaret = strchr(data, '>');
196 | 		if (lcaret && rcaret) {
197 | 			if (lcaret < rcaret)
198 | 				next = lcaret;
199 | 			else
200 | 				next = rcaret;
201 | 		} else if (lcaret) {
202 | 			next = lcaret;
203 | 		} else if (rcaret) {
204 | 			next = rcaret;
205 | 		} else {
206 | 			break;
207 | 		}
208 | 
209 | 		sc_cb_memcpy(cb, data, next - data);
210 | 		if (*data == '<')
211 | 			sc_cb_concat(cb, "<");
212 | 		else
213 | 			sc_cb_concat(cb, ">");
214 | 		data = next + 1;
215 | 	} while (lcaret || rcaret);
216 | 	sc_cb_concat(cb, data);
217 | }
218 | 
219 | int cbot_http_plainresp_send(struct sc_charbuf *cb,
220 |                              struct cbot_http_event *event,
221 |                              unsigned int status_code)
222 | {
223 | 	struct MHD_Response *resp;
224 | 	enum MHD_Result code;
225 | 	int rv = -1;
226 | 
227 | 	sc_cb_concat(cb, "
\n"); 228 | resp = MHD_create_response_from_buffer(cb->length, cb->buf, 229 | MHD_RESPMEM_MUST_FREE); 230 | if (!resp) 231 | return rv; 232 | 233 | code = MHD_add_response_header(resp, "Content-Type", 234 | "text/html; charset=utf-8"); 235 | if (code == MHD_NO) 236 | goto err; 237 | 238 | code = MHD_queue_response(event->connection, status_code, resp); 239 | if (code == MHD_NO) 240 | goto err; 241 | 242 | rv = 0; 243 | err: 244 | MHD_destroy_response(resp); 245 | return rv; 246 | } 247 | 248 | void http_plainresp_abort(struct sc_charbuf *cb) 249 | { 250 | sc_cb_destroy(cb); 251 | } 252 | 253 | int cbot_http_init(struct cbot *bot, config_setting_t *config) 254 | { 255 | int port = PORT; 256 | const char *url = "https://example.com"; 257 | 258 | struct cbot_http *http = calloc(sizeof(*http), 1); 259 | bot->httpriv = http; 260 | 261 | config_setting_lookup_int(config, "port", &port); 262 | config_setting_lookup_string(config, "url", &url); 263 | http->url = strdup(url); 264 | 265 | bot->http = MHD_start_daemon(MHD_USE_EPOLL, port, NULL, NULL, 266 | (MHD_AccessHandlerCallback)hdlr, bot, 267 | MHD_OPTION_END); 268 | if (!bot->http) { 269 | return -1; 270 | } 271 | bot->http_lwt = sc_lwt_create_task(bot->lwt_ctx, cbot_http_run, bot); 272 | 273 | cbot_register_priv(bot, NULL, CBOT_HTTP_ANY, 274 | (cbot_handler_t)cbot_http_root, NULL, "/", 0); 275 | return 0; 276 | } 277 | -------------------------------------------------------------------------------- /src/json.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "cbot/json.h" 5 | #include "nosj.h" 6 | 7 | int je_get_object(struct json_easy *je, uint32_t start, const char *key, 8 | uint32_t *out) 9 | { 10 | int err = json_easy_lookup(je, start, key, out); 11 | if (err != JSON_OK) 12 | return err; 13 | if (je->tokens[*out].type != JSON_OBJECT) 14 | return JSONERR_TYPE; 15 | return JSON_OK; 16 | } 17 | int je_get_array(struct json_easy *je, uint32_t start, const char *key, 18 | uint32_t *out) 19 | { 20 | int err = json_easy_lookup(je, start, key, out); 21 | if (err != JSON_OK) 22 | return err; 23 | if (je->tokens[*out].type != JSON_ARRAY) 24 | return JSONERR_TYPE; 25 | return JSON_OK; 26 | } 27 | int je_get_uint(struct json_easy *je, uint32_t start, const char *key, 28 | uint64_t *out) 29 | { 30 | uint32_t index; 31 | int err = json_easy_lookup(je, start, key, &index); 32 | if (err != JSON_OK) 33 | return err; 34 | return json_easy_number_getuint(je, index, out); 35 | } 36 | int je_get_int(struct json_easy *je, uint32_t start, const char *key, 37 | int64_t *out) 38 | { 39 | uint32_t index; 40 | int err = json_easy_lookup(je, start, key, &index); 41 | if (err != JSON_OK) 42 | return err; 43 | return json_easy_number_getint(je, index, out); 44 | } 45 | int je_get_bool(struct json_easy *je, uint32_t start, const char *key, 46 | bool *out) 47 | { 48 | uint32_t index; 49 | int err = json_easy_lookup(je, start, key, &index); 50 | if (err != JSON_OK) 51 | return err; 52 | *out = je->tokens[index].type == JSON_TRUE; 53 | return JSON_OK; 54 | } 55 | int je_get_string(struct json_easy *je, uint32_t start, const char *key, 56 | char **out) 57 | { 58 | uint32_t index; 59 | int err = json_easy_lookup(je, start, key, &index); 60 | if (err != JSON_OK) 61 | return err; 62 | return json_easy_string_get(je, index, out); 63 | } 64 | 65 | bool je_string_match(struct json_easy *je, uint32_t start, const char *key, 66 | const char *cmp) 67 | { 68 | uint32_t index; 69 | int err = json_easy_lookup(je, start, key, &index); 70 | if (err != JSON_OK) 71 | return false; 72 | bool match; 73 | if (json_easy_string_match(je, index, cmp, &match) != JSON_OK) 74 | return false; 75 | return match; 76 | } 77 | -------------------------------------------------------------------------------- /src/log.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "cbot/cbot.h" 7 | #include "cbot_private.h" 8 | 9 | static int current_log_level; 10 | static FILE *current_log_file; 11 | 12 | void cbot_vlog(int level, const char *format, va_list args) 13 | { 14 | /* clang-tidy decided args is uninitialized? */ 15 | if (current_log_file && level >= current_log_level) 16 | vfprintf(current_log_file, format, args); // NOLINT 17 | } 18 | 19 | void cbot_log(int level, const char *format, ...) 20 | { 21 | va_list args; 22 | va_start(args, format); 23 | cbot_vlog(level, format, args); 24 | va_end(args); 25 | } 26 | 27 | void cbot_set_log_level(int level) 28 | { 29 | current_log_level = level; 30 | } 31 | 32 | int cbot_get_log_level(void) 33 | { 34 | return current_log_level; 35 | } 36 | 37 | void cbot_set_log_file(FILE *f) 38 | { 39 | current_log_file = f; 40 | } 41 | 42 | struct levels { 43 | char *name; 44 | int level; 45 | }; 46 | 47 | /* clang-format off */ 48 | struct levels levels[] = { 49 | { "VERB", VERB }, 50 | { "DEBUG", DEBUG }, 51 | { "INFO", INFO }, 52 | { "WARN", WARN }, 53 | { "CRiT", CRIT }, 54 | }; 55 | /* clang-format on */ 56 | 57 | int cbot_lookup_level(const char *str) 58 | { 59 | for (int i = 0; i < nelem(levels); i++) 60 | if (strcmp(str, levels[i].name) == 0) 61 | return levels[i].level; 62 | return atoi(str); 63 | } 64 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | /** 2 | * main.c: Main CBot entry point 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "cbot_private.h" 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | struct cbot *bot; 15 | int rv; 16 | srand(time(NULL)); 17 | if (argc != 2) { 18 | printf("usage: %s CONFIG_FILE\n", argv[0]); 19 | return EXIT_FAILURE; 20 | } 21 | curl_global_init(0); 22 | 23 | bot = cbot_create(); 24 | if (!bot) 25 | return EXIT_FAILURE; 26 | 27 | rv = cbot_load_config(bot, argv[1]); 28 | if (rv < 0) 29 | return EXIT_FAILURE; 30 | 31 | cbot_run(bot); 32 | cbot_delete(bot); 33 | return EXIT_SUCCESS; 34 | } 35 | -------------------------------------------------------------------------------- /src/signal/backend.c: -------------------------------------------------------------------------------- 1 | /* 2 | * signal/backend.c: defines the common backend code for Signal. Delegates a lot 3 | * of functionality to a signal API bridge, but some code is shared. 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include "../cbot_private.h" 22 | #include "cbot/cbot.h" 23 | #include "internal.h" 24 | 25 | static bool is_sock(const char *path) 26 | { 27 | struct stat st; 28 | int rv = stat(path, &st); 29 | if (rv < 0 && errno == ENOENT) { 30 | return false; 31 | } else if (rv < 0) { 32 | CL_CRIT("stat(%s): %s", path, strerror(errno)); 33 | return false; 34 | } 35 | return (st.st_mode & S_IFMT) == S_IFSOCK; 36 | } 37 | 38 | int cbot_signal_socket(struct cbot_signal_backend *sig, const char *path) 39 | { 40 | struct sockaddr_un addr; 41 | if (strlen(path) >= sizeof(addr.sun_path)) { 42 | CL_CRIT("cbot signal: signald socket path too long\n"); 43 | return -1; 44 | } 45 | 46 | int timeout = 10; 47 | while (!is_sock(path) && timeout--) { 48 | CL_WARN("cbot signal: signald socket doesn't exist, waiting " 49 | "(%d)\n", 50 | timeout); 51 | struct timespec tp = { 0 }; 52 | tp.tv_sec = 1; 53 | nanosleep(&tp, NULL); 54 | } 55 | if (timeout < 0) { 56 | CL_CRIT("cbot signal: timed out waiting for socket: %s\n", 57 | path); 58 | return -1; 59 | } 60 | 61 | sig->fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0); 62 | if (sig->fd < 0) { 63 | perror("create socket"); 64 | return -1; 65 | } 66 | sig->ws = fdopen(sig->fd, "w"); 67 | if (!sig->ws) { 68 | perror("fdopen socket"); 69 | close(sig->fd); 70 | return -1; 71 | } 72 | setvbuf(sig->ws, NULL, _IONBF, 0); 73 | 74 | addr.sun_family = AF_UNIX; 75 | strncpy(addr.sun_path, path, sizeof(addr.sun_path)); 76 | int rv = connect(sig->fd, (struct sockaddr *)&addr, sizeof(addr)); 77 | if (rv) { 78 | perror("connect"); 79 | fclose(sig->ws); 80 | close(sig->fd); 81 | return -1; 82 | } 83 | return 0; 84 | } 85 | 86 | static int cbot_signal_configure(struct cbot *bot, config_setting_t *group) 87 | { 88 | struct cbot_signal_backend *backend; 89 | int rv; 90 | const char *phone; 91 | const char *uuid; 92 | const char *auth = NULL; 93 | const char *bridge; 94 | int ignore_dm = 0; 95 | 96 | rv = config_setting_lookup_string(group, "phone", &phone); 97 | if (rv == CONFIG_FALSE) { 98 | CL_CRIT("cbot signal: key \"phone\" wrong type or not " 99 | "exists\n"); 100 | return -1; 101 | } 102 | 103 | rv = config_setting_lookup_string(group, "uuid", &uuid); 104 | if (rv == CONFIG_FALSE) { 105 | CL_CRIT("cbot signal: key \"uuid\" wrong type or not " 106 | "exists\n"); 107 | return -1; 108 | } 109 | 110 | config_setting_lookup_string(group, "auth", &auth); 111 | 112 | config_setting_lookup_string(group, "bridge", &bridge); 113 | if (rv == CONFIG_FALSE) { 114 | CL_CRIT("cbot signal: key \"bridge\" wrong type or " 115 | "not found\n"); 116 | return -1; 117 | } 118 | 119 | config_setting_lookup_bool(group, "ignore_dm", &ignore_dm); 120 | if (ignore_dm) { 121 | CL_INFO("signal: ignoring DMs\n"); 122 | } 123 | 124 | backend = calloc(1, sizeof(*backend)); 125 | backend->sender = strdup(phone); 126 | backend->uuid = strdup(uuid); 127 | backend->ignore_dm = ignore_dm; 128 | sc_list_init(&backend->messages); 129 | sc_list_init(&backend->msgq); 130 | sc_arr_init(&backend->pending, struct signal_reaction_cb, 16); 131 | if (auth) 132 | backend->auth_uuid = strdup(auth); 133 | 134 | backend->bot = bot; 135 | bot->backend = backend; 136 | 137 | if (strcmp(bridge, "signald") == 0) { 138 | backend->bridge = &signald_bridge; 139 | } else if (strcmp(bridge, "signal-cli") == 0) { 140 | backend->bridge = &signalcli_bridge; 141 | } else { 142 | CL_CRIT("cbot signal: unknown bridge \"%s\"\n", bridge); 143 | goto out; 144 | } 145 | 146 | /* Setup the real @mention (with UUID) as an alias for the bot. */ 147 | char *alias = mention_format_p(uuid, "uuid"); 148 | cbot_add_alias(bot, mention_format_p(uuid, "uuid")); 149 | free(alias); 150 | /* Setup the text "@cbot" and "@@cbot" as aliases for the bot. */ 151 | asprintf(&alias, "@@%s", bot->name); 152 | cbot_add_alias(bot, alias); 153 | cbot_add_alias(bot, &alias[1]); 154 | free(alias); 155 | 156 | rv = backend->bridge->configure(bot, group); 157 | if (rv == 0) 158 | return 0; 159 | out: 160 | free(backend->sender); 161 | free(backend->uuid); 162 | free(backend->auth_uuid); 163 | /* TODO: free all callbacks */ 164 | sc_arr_destroy(&backend->pending); 165 | free(backend); 166 | return -1; 167 | } 168 | 169 | bool signal_is_group_listening(struct cbot_signal_backend *sig, const char *grp) 170 | { 171 | struct cbot *bot = sig->bot; 172 | struct cbot_channel_conf *chan; 173 | 174 | sc_list_for_each_entry(chan, &bot->init_channels, list, 175 | struct cbot_channel_conf) 176 | { 177 | if (strcmp(chan->name, grp) == 0) 178 | return true; 179 | } 180 | return false; 181 | } 182 | 183 | static int reaction_cmp(const struct signal_reaction_cb *lhs, 184 | const struct signal_reaction_cb *rhs) 185 | { 186 | if (lhs->ts < rhs->ts) 187 | return -1; 188 | else if (lhs->ts > rhs->ts) 189 | return 1; 190 | else 191 | return 0; 192 | } 193 | 194 | static void add_reaction_cb(struct cbot_signal_backend *sig, uint64_t ts, 195 | const struct cbot_reaction_ops *ops, void *arg) 196 | { 197 | struct signal_reaction_cb cb = { ts, *ops, arg }; 198 | struct sc_array *a = &sig->pending; 199 | struct signal_reaction_cb *arr = sc_arr(a, struct signal_reaction_cb); 200 | size_t i; 201 | 202 | /* Linear search for insertion, since this is a less common case, and 203 | * bsearch() does not return the correct insertion point. */ 204 | 205 | for (i = 0; i < a->len; i++) 206 | if (ts < arr[i].ts) 207 | break; 208 | CL_DEBUG("signal: registered reaction callback for %lu\n", ts); 209 | sc_arr_insert(a, struct signal_reaction_cb, i, cb); 210 | } 211 | 212 | bool signal_get_reaction_cb(struct cbot_signal_backend *sig, uint64_t ts, 213 | struct signal_reaction_cb *out) 214 | { 215 | struct signal_reaction_cb cb = { ts, { 0 }, 0 }; 216 | struct sc_array *a = &sig->pending; 217 | struct signal_reaction_cb *arr = sc_arr(a, struct signal_reaction_cb); 218 | struct signal_reaction_cb *res = 219 | bsearch(&cb, arr, a->len, sizeof(*res), (void *)reaction_cmp); 220 | if (res) { 221 | *out = *res; 222 | return true; 223 | } else { 224 | return false; 225 | } 226 | } 227 | 228 | static void unregister_reaction(const struct cbot *bot, uint64_t ts) 229 | { 230 | struct signal_reaction_cb cb = { ts, { 0 }, 0 }; 231 | struct cbot_signal_backend *sig = bot->backend; 232 | struct sc_array *a = &sig->pending; 233 | struct signal_reaction_cb *arr = sc_arr(a, struct signal_reaction_cb); 234 | struct signal_reaction_cb *res = 235 | bsearch(&cb, arr, a->len, sizeof(*res), (void *)reaction_cmp); 236 | if (res) { 237 | size_t index = res - arr; 238 | sc_arr_remove(a, struct signal_reaction_cb, index); 239 | } 240 | } 241 | 242 | static uint64_t cbot_signal_send(const struct cbot *bot, const char *to, 243 | const struct cbot_reaction_ops *ops, void *arg, 244 | const char *msg) 245 | { 246 | struct cbot_signal_backend *sig = bot->backend; 247 | char *dest_payload; 248 | int kind; 249 | uint64_t timestamp; 250 | struct signal_mention *mentions; 251 | size_t num_mentions; 252 | 253 | dest_payload = mention_parse(to, &kind, NULL); 254 | char *quoted = json_quote_and_mention(msg, &mentions, &num_mentions); 255 | 256 | switch (kind) { 257 | case MENTION_USER: 258 | timestamp = sig->bridge->send_single(sig, dest_payload, quoted, 259 | mentions, num_mentions); 260 | break; 261 | case MENTION_GROUP: 262 | timestamp = sig->bridge->send_group(sig, dest_payload, quoted, 263 | mentions, num_mentions); 264 | break; 265 | default: 266 | CL_CRIT("error: invalid signal destination \"%s\"\n", to); 267 | timestamp = 0; 268 | } 269 | free(dest_payload); 270 | free(quoted); 271 | for (size_t i = 0; i < num_mentions; i++) 272 | free(mentions[i].uuid); 273 | free(mentions); 274 | if (ops && timestamp) { 275 | add_reaction_cb(sig, timestamp, ops, arg); 276 | return timestamp; 277 | } else { 278 | return 0; 279 | } 280 | } 281 | 282 | static void cbot_signal_nick(const struct cbot *bot, const char *newnick) 283 | { 284 | struct cbot_signal_backend *sig = bot->backend; 285 | sig->bridge->nick(bot, newnick); 286 | cbot_set_nick((struct cbot *)bot, newnick); 287 | } 288 | 289 | static int cbot_signal_is_authorized(const struct cbot *bot, const char *sender, 290 | const char *message) 291 | { 292 | struct cbot_signal_backend *sig = bot->backend; 293 | int kind, rv = 0; 294 | char *uuid; 295 | 296 | if (!sig->auth_uuid) 297 | return 0; 298 | 299 | uuid = mention_parse(sender, &kind, NULL); 300 | if (kind != MENTION_USER) { 301 | free(uuid); 302 | return 0; 303 | } 304 | 305 | if (strcmp(uuid, sig->auth_uuid) == 0) 306 | rv = 1; 307 | free(uuid); 308 | return rv; 309 | } 310 | 311 | static void cbot_signal_run(struct cbot *bot) 312 | { 313 | struct cbot_signal_backend *sig = bot->backend; 314 | sig->bridge->run(bot); 315 | } 316 | 317 | struct cbot_backend_ops signald_ops = { 318 | .name = "signal", 319 | .configure = cbot_signal_configure, 320 | .run = cbot_signal_run, 321 | .send = cbot_signal_send, 322 | .nick = cbot_signal_nick, 323 | .is_authorized = cbot_signal_is_authorized, 324 | .unregister_reaction = unregister_reaction, 325 | }; 326 | -------------------------------------------------------------------------------- /src/signal/internal.h: -------------------------------------------------------------------------------- 1 | /* 2 | * signal/internal.h: shared definitions related to Signal backends 3 | */ 4 | #ifndef CBOT_SIGNALD_INTERNAL_DOT_H 5 | #define CBOT_SIGNALD_INTERNAL_DOT_H 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "cbot/cbot.h" 17 | 18 | struct cbot_signal_backend; 19 | 20 | /** Reaction callback information */ 21 | struct signal_reaction_cb { 22 | /** Timestamp of the message to monitor for reactions */ 23 | uint64_t ts; 24 | /** Operations from the plugin */ 25 | struct cbot_reaction_ops ops; 26 | /** Argument to plugin */ 27 | void *arg; 28 | }; 29 | 30 | /** Signal's representation of a @mention */ 31 | struct signal_mention { 32 | /** UTF-16 index of the start of the text replaced by @mention */ 33 | uint64_t start; 34 | /** UTF-16 length of the text to replace */ 35 | uint64_t length; 36 | /** UUID of the user mentioned */ 37 | char *uuid; 38 | }; 39 | 40 | /** Operations that are specific to a Signal API bridge. */ 41 | struct signal_bridge_ops { 42 | /** Send an already-quoted direct message */ 43 | uint64_t (*send_single)(struct cbot_signal_backend *, const char *to, 44 | const char *quoted_msg, 45 | const struct signal_mention *, size_t); 46 | /** Send an already-quoted message to a group */ 47 | uint64_t (*send_group)(struct cbot_signal_backend *, const char *to, 48 | const char *quoted_msg, 49 | const struct signal_mention *, size_t); 50 | /** Update profile name */ 51 | void (*nick)(const struct cbot *bot, const char *newnick); 52 | /** Run the bot backend thread */ 53 | void (*run)(struct cbot *bot); 54 | /** Bridge-specific configuration routine */ 55 | int (*configure)(struct cbot *bot, config_setting_t *group); 56 | }; 57 | 58 | /* 59 | * We have two current bridges: Signald and Signal-CLI 60 | * https://signald.org/ 61 | * https://github.com/AsamK/signal-cli 62 | */ 63 | extern struct signal_bridge_ops signald_bridge; 64 | extern struct signal_bridge_ops signalcli_bridge; 65 | 66 | struct cbot_signal_backend { 67 | /* Signal bridge operations */ 68 | struct signal_bridge_ops *bridge; 69 | 70 | /* File descriptor for our signal bridge */ 71 | int fd; 72 | 73 | int write_fd; /* Additional descriptor for bridge (ignore if 0) */ 74 | pid_t child; 75 | 76 | /* Queued messages ready to read */ 77 | struct sc_list_head messages; 78 | char *spill; 79 | int spilllen; 80 | 81 | /* Ignore DMs? (Useful for running multiple bots on the same acct) */ 82 | int ignore_dm; 83 | 84 | /* 85 | * A stdio write stream associated with the above socket. It is in 86 | * unbuffered mode, used to write formatted JSON commands. 87 | */ 88 | FILE *ws; 89 | 90 | /* Phone number & uuid of the bot sender */ 91 | char *sender; 92 | char *uuid; 93 | 94 | /* uuid of authorized user */ 95 | char *auth_uuid; 96 | 97 | /* Reference to the bot */ 98 | struct cbot *bot; 99 | 100 | /* Array of message timestamps and information on callbacks */ 101 | struct sc_array pending; 102 | 103 | /* Queued messages to send */ 104 | struct sc_list_head msgq; 105 | 106 | uint64_t id; 107 | }; 108 | 109 | /***** jmsg.c *****/ 110 | 111 | /** Structure representing a line of text which is a JSON message. */ 112 | struct jmsg { 113 | /** Owns the line of text and parsed JSON metadata */ 114 | struct json_easy easy; 115 | /** Links the messages together in the handling queue */ 116 | struct sc_list_head list; 117 | }; 118 | 119 | /** 120 | * Read the next jmsg from the queue of incoming messages. If there are no 121 | * messages in the queue, this will block. 122 | * @param sig Signal backend 123 | * @return NULL on error, otherwise a struct jmsg ready to use 124 | */ 125 | struct jmsg *jmsg_next(struct cbot_signal_backend *sig); 126 | 127 | /** 128 | * Wait for a jmsg where @a field has value @a value 129 | * @param field The field name to wait on 130 | * @param value A value to wait for (only strings are supported) 131 | */ 132 | struct jmsg *jmsg_wait_field(struct cbot_signal_backend *sig, const char *field, 133 | const char *value); 134 | 135 | /** 136 | * Check if a message applies to any waiter. If so, deliver it 137 | * @param sig Signal backend 138 | * @param jm Message to deliver to waiter 139 | * @returns true if the message is delivered to a waiter. When this is the case, 140 | * the waiter takes ownership of @a jm, and it must no longer be accessed by 141 | * the caller. 142 | */ 143 | bool jmsg_deliver(struct cbot_signal_backend *sig, struct jmsg *jm); 144 | 145 | /** 146 | * Free a JSON message object, in whatever lifetime state it may be. 147 | * @param jm Message to free. 148 | */ 149 | void jmsg_free(struct jmsg *jm); 150 | 151 | /***** mention.c *****/ 152 | 153 | #define MENTION_ERR 0 154 | #define MENTION_USER 1 155 | #define MENTION_GROUP 2 156 | 157 | /** 158 | * Formats a mention placeholder, without freeing old string. 159 | * @param string The value of the mention 160 | * @param prefix The prefix to apply (e.g. uuid, group) 161 | * @returns A newly allocated string with the mention text. 162 | */ 163 | char *mention_format_p(const char *string, const char *prefix); 164 | 165 | /** 166 | * Formats a mention placeholder, freeing old string. 167 | * @param string The value of the mention (FREED by this function) 168 | * @param prefix The prefix to apply (e.g. uuid, group) 169 | * @returns A newly allocated string with the mention text. 170 | */ 171 | char *mention_format(char *string, const char *prefix); 172 | 173 | /** 174 | * Parse a mention placeholder text. 175 | * @param string Input text starting at the mention 176 | * @param[out] kind The kind of message: MENTION_ERR for error. 177 | * @param[out] offset If provided, will be filled with the number of characters 178 | * of thismention. 179 | * @return The value of the mention (a new string which must be freed) 180 | */ 181 | char *mention_parse(const char *string, int *kind, int *offset); 182 | 183 | /** 184 | * Return a newly allocated string with mentions "replaced" 185 | * 186 | * Signald gives us messages with mentions in a strange format. The mentions 187 | * come in a JSON array, and their "start" field doesn't seem accurate. However, 188 | * each mention is replaced with MENTION_PLACEHOLDER, so we simply iterate over 189 | * each placeholder, grab a mention from the JSON list, and insert our 190 | * placeholder: 191 | * 192 | * @(uuid:blah) 193 | * 194 | * Our placeholder can be translated back at the end (see below). To preserve 195 | * mentions which may contain @, we also identify the @ sign and double it. 196 | * @param str Message string 197 | * @param je The JSON message buffer 198 | * @param list The index of the array to read menitons from 199 | * @return A newly allocated string with mentions expanded to placeholders. 200 | */ 201 | char *mention_from_json(const char *str, struct json_easy *je, uint32_t list); 202 | 203 | /** 204 | * Return a newly allocated string with necessary escaping for JSON. Return an 205 | * array which contains all mention JSON elements. 206 | * 207 | * This is called with a message text just before sending it. 208 | * 209 | * Beyond obvious JSON escaping, this function detects any mention placeholder 210 | * mention text: 211 | * @(uuid:UUUID) 212 | * That text is removed and a JSON array element is created in "mentions" to 213 | * represent it. 214 | * 215 | * Duplicated "@@" are resolved back to "@" - this is to reverse the escaping 216 | * done by mention_from_json() above. 217 | * 218 | * @param instr Input string 219 | * @param[out] ms Will be set to an array of mention objects parsed 220 | * @param[out] n Will be set to the length of @a ms 221 | */ 222 | char *json_quote_and_mention(const char *instr, struct signal_mention **ms, 223 | size_t *n); 224 | 225 | /** 226 | * Return a newly allocated string with necessary JSON escaping. 227 | * No handling of mentions is done. If there's a "mention" text, it will be 228 | * left as-is. 229 | */ 230 | char *json_quote_nomention(const char *instr); 231 | 232 | /***** backend.c *****/ 233 | 234 | /** 235 | * Fetch the reaction callback for a given message timestamp 236 | * @param sig Signal backend 237 | * @param ts Message timestamp 238 | * @param[out] out Structure filled with details if found 239 | * @returns true if a reaction callback was found for the message 240 | */ 241 | bool signal_get_reaction_cb(struct cbot_signal_backend *sig, uint64_t ts, 242 | struct signal_reaction_cb *out); 243 | 244 | /** 245 | * Return true if the bot is listening to a group and we shoul handle messages 246 | * @param sig Signal backend 247 | * @param group Group ID (not in @(group:foo) format) 248 | * @returns true if we should handle the message 249 | */ 250 | bool signal_is_group_listening(struct cbot_signal_backend *sig, 251 | const char *group); 252 | 253 | /** 254 | * Connect to a Unix socket for the backend 255 | * 256 | * Shared by both signald and signal-cli modes. 257 | * @param sig Signal backend 258 | * @param path Unix socket path 259 | * @returns 0 on success, -1 on failure 260 | */ 261 | int cbot_signal_socket(struct cbot_signal_backend *sig, const char *path); 262 | 263 | #endif // CBOT_SIGNALD_INTERNAL_DOT_H 264 | -------------------------------------------------------------------------------- /src/signal/jmsg.c: -------------------------------------------------------------------------------- 1 | /* 2 | * signal/jmsg.c: utilities for reading and parsing lines of JSON data 3 | */ 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include "../cbot_private.h" 17 | #include "cbot/cbot.h" 18 | #include "cbot/json.h" 19 | #include "internal.h" 20 | 21 | static int async_read(int fd, char *data, size_t nbytes) 22 | { 23 | struct sc_lwt *cur = sc_lwt_current(); 24 | int rv; 25 | 26 | for (;;) { 27 | rv = read(fd, data, nbytes); 28 | if (rv < 0 && (errno == EWOULDBLOCK || errno == EAGAIN)) { 29 | /* would block, we should yield */ 30 | sc_lwt_set_state(cur, SC_LWT_BLOCKED); 31 | sc_lwt_yield(); 32 | } else if (rv < 0) { 33 | perror("cbot_signal pipe read"); 34 | return -1; 35 | } else if (rv == 0) { 36 | CL_CRIT("cbot_signal: pipe is empty, exiting\n"); 37 | return -1; 38 | } else { 39 | return rv; 40 | } 41 | } 42 | } 43 | 44 | static int jmsg_parse(struct jmsg *jm) 45 | { 46 | int res = json_easy_parse(&jm->easy); 47 | 48 | if (res != JSON_OK) { 49 | CL_CRIT("json parse error: %s\n", json_strerror(res)); 50 | return -1; 51 | } 52 | return 0; 53 | } 54 | 55 | /* 56 | * Read at least one jmsg, adding it to the list. All jmsg are parsed. 57 | * 58 | * Return the number of successfully read jmsgs. On error, return -1 (though 59 | * successful messages may still be in the list). 60 | */ 61 | static int jmsg_read(int fd, struct sc_list_head *list) 62 | { 63 | int rv, nextmsgidx; 64 | struct sc_charbuf cb; 65 | char *found; 66 | int count = 0; 67 | 68 | sc_cb_init(&cb, 4096); 69 | 70 | for (;;) { 71 | rv = async_read(fd, cb.buf + cb.length, 72 | cb.capacity - cb.length); 73 | if (rv < 0) { 74 | CL_CRIT("read error: %d\n", rv); 75 | goto err; 76 | } else { 77 | cb.length += rv; 78 | } 79 | 80 | found = memchr(cb.buf, '\n', cb.length); 81 | while (found) { 82 | char *buf = NULL; 83 | struct jmsg *jm = NULL; 84 | 85 | /* 86 | * Find start of next message and replace newline 87 | * with nul terminator 88 | */ 89 | nextmsgidx = found - cb.buf + 1; 90 | *found = '\0'; 91 | 92 | /* 93 | * Copy data into new jmsg and add to output. 94 | */ 95 | buf = malloc(nextmsgidx); 96 | if (!buf) { 97 | CL_CRIT("Allocation error\n"); 98 | goto err; 99 | } 100 | memcpy(buf, cb.buf, nextmsgidx); 101 | jm = calloc(1, sizeof(*jm)); 102 | if (!jm) { 103 | CL_CRIT("Allocation error\n"); 104 | free(buf); 105 | goto err; 106 | } 107 | json_easy_init(&jm->easy, buf); 108 | /* Weirdly, clang-tidy believes that here, buf could be 109 | * leaked. I guess it doesn't pick up on the fact that 110 | * now, jm->easy takes ownership of buf. Suppress the 111 | * false positive.*/ 112 | sc_list_init(&jm->list); // NOLINT 113 | CL_VERB("JM: \"%s\"\n", jm->easy.input); 114 | if (jmsg_parse(jm) < 0) { 115 | jmsg_free(jm); 116 | goto err; 117 | } 118 | sc_list_insert_end(list, &jm->list); 119 | count += 1; 120 | 121 | /* 122 | * Skip past any possible additional newlines. 123 | */ 124 | while (nextmsgidx < cb.length && 125 | cb.buf[nextmsgidx] == '\n') 126 | nextmsgidx++; 127 | 128 | /* 129 | * If there is no more data in the buffer, we're good. 130 | * Return. 131 | */ 132 | if (nextmsgidx == cb.length) { 133 | sc_cb_destroy(&cb); 134 | return count; 135 | } 136 | 137 | /* 138 | * Otherwise, shift data down to 139 | */ 140 | memmove(cb.buf, &cb.buf[nextmsgidx], 141 | cb.length - nextmsgidx); 142 | cb.length -= nextmsgidx; 143 | found = memchr(cb.buf, '\n', cb.length); 144 | } 145 | 146 | /* Ensure there is space to read more data */ 147 | if (cb.length == cb.capacity) { 148 | cb.capacity *= 2; 149 | cb.buf = realloc(cb.buf, cb.capacity); 150 | } 151 | } 152 | err: 153 | sc_cb_destroy(&cb); 154 | return -1; 155 | } 156 | 157 | static struct jmsg *jmsg_first(struct sc_list_head *list) 158 | { 159 | struct jmsg *jm; 160 | 161 | sc_list_for_each_entry(jm, list, list, struct jmsg) 162 | { 163 | sc_list_remove(&jm->list); 164 | return jm; 165 | } 166 | return NULL; 167 | } 168 | 169 | struct jmsg *jmsg_next(struct cbot_signal_backend *sig) 170 | { 171 | struct jmsg *jm; 172 | 173 | if ((jm = jmsg_first(&sig->messages))) 174 | return jm; 175 | if (jmsg_read(sig->fd, &sig->messages) < 0) 176 | return NULL; /* need to propagate error */ 177 | return jmsg_first(&sig->messages); 178 | } 179 | 180 | static struct jmsg *jmsg_find_by_field(struct sc_list_head *list, 181 | const char *field, const char *value) 182 | { 183 | struct jmsg *jm; 184 | uint32_t ix_type; 185 | 186 | sc_list_for_each_entry(jm, list, list, struct jmsg) 187 | { 188 | int ret; 189 | bool match; 190 | 191 | ret = json_easy_object_get(&jm->easy, 0, field, &ix_type); 192 | if (ret != JSON_OK) 193 | continue; 194 | 195 | ret = json_easy_string_match(&jm->easy, ix_type, value, &match); 196 | if (ret != JSON_OK) 197 | continue; 198 | 199 | if (match) { 200 | sc_list_remove(&jm->list); 201 | return jm; 202 | } 203 | } 204 | 205 | return NULL; 206 | } 207 | 208 | struct signal_queued_item { 209 | struct sc_list_head list; 210 | const char *field; 211 | const char *value; 212 | struct sc_lwt *thread; 213 | struct jmsg *result; 214 | }; 215 | 216 | struct jmsg *jmsg_wait_field(struct cbot_signal_backend *sig, const char *field, 217 | const char *value) 218 | { 219 | struct sc_list_head list; 220 | struct sc_lwt *cur; 221 | struct jmsg *jm = jmsg_find_by_field(&sig->messages, field, value); 222 | if (jm) 223 | return jm; 224 | 225 | cur = sc_lwt_current(); 226 | if (cur != sig->bot->lwt) { 227 | struct signal_queued_item item = { 0 }; 228 | item.field = field; 229 | item.value = value; 230 | item.thread = cur; 231 | sc_list_init(&item.list); 232 | sc_list_insert_end(&sig->msgq, &item.list); 233 | while (!item.result && !sc_lwt_shutting_down()) { 234 | sc_lwt_set_state(cur, SC_LWT_BLOCKED); 235 | sc_lwt_set_state(sig->bot->lwt, SC_LWT_RUNNABLE); 236 | sc_lwt_yield(); 237 | } 238 | return item.result; 239 | } 240 | 241 | for (;;) { 242 | sc_list_init(&list); 243 | if (jmsg_read(sig->fd, &list) < 0) { 244 | /* make sure we don't leak them */ 245 | sc_list_move(&list, sig->messages.prev); 246 | return NULL; 247 | } 248 | jm = jmsg_find_by_field(&list, field, value); 249 | sc_list_move(&list, sig->messages.prev); 250 | if (jm) 251 | return jm; 252 | } 253 | } 254 | 255 | bool jmsg_deliver(struct cbot_signal_backend *sig, struct jmsg *jm) 256 | { 257 | struct signal_queued_item *item; 258 | sc_list_for_each_entry(item, &sig->msgq, list, 259 | struct signal_queued_item) 260 | { 261 | if (je_string_match(&jm->easy, 0, item->field, item->value)) { 262 | sc_list_remove(&item->list); 263 | item->result = jm; 264 | sc_lwt_set_state(item->thread, SC_LWT_RUNNABLE); 265 | return true; 266 | } 267 | } 268 | return false; 269 | } 270 | 271 | void jmsg_free(struct jmsg *jm) 272 | { 273 | if (jm) { 274 | /* json_easy does not own input */ 275 | free((void *)jm->easy.input); 276 | json_easy_destroy(&jm->easy); 277 | free(jm); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/signal/mention.c: -------------------------------------------------------------------------------- 1 | /* 2 | * signal/mention.c: Functions which help add and remove @mentions to Signal 3 | * messages. 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include "../utf8.h" 13 | #include "cbot/cbot.h" 14 | #include "cbot/json.h" 15 | #include "internal.h" 16 | #include "nosj.h" 17 | 18 | char *mention_format_p(const char *string, const char *prefix) 19 | { 20 | struct sc_charbuf cb; 21 | sc_cb_init(&cb, 64); 22 | sc_cb_printf(&cb, "@(%s:%s)", prefix, string); 23 | return cb.buf; 24 | } 25 | 26 | char *mention_format(char *string, const char *prefix) 27 | { 28 | char *res = mention_format_p(string, prefix); 29 | free(string); 30 | return res; 31 | } 32 | 33 | /* 34 | * Checks for a prefix, if so, returns a pointer to directly after it. 35 | */ 36 | static const char *startswith(const char *str, const char *pfx) 37 | { 38 | int len = strlen(pfx); 39 | if (strncmp(str, pfx, len) == 0) 40 | return str + len; 41 | return NULL; 42 | } 43 | 44 | char *mention_parse(const char *string, int *kind, int *offset) 45 | { 46 | const char *start, *end; 47 | char *out; 48 | 49 | if ((start = startswith(string, "@(uuid:"))) { 50 | *kind = MENTION_USER; 51 | } else if ((start = startswith(string, "@(group:"))) { 52 | *kind = MENTION_GROUP; 53 | } else { 54 | *kind = MENTION_ERR; 55 | if (offset) 56 | *offset = 1; 57 | return strdup("@???"); 58 | } 59 | end = strchr(start, ')'); 60 | if (!end) { 61 | *kind = MENTION_ERR; 62 | if (offset) 63 | *offset = 1; 64 | return strdup("@???"); 65 | } 66 | out = malloc(end - start + 1); 67 | memcpy(out, start, end - start); 68 | out[end - start] = '\0'; 69 | if (offset) 70 | *offset = end - string + 1; 71 | return out; 72 | } 73 | 74 | /* 75 | * What is this monstrosity of a function? Why does it mention UTF-8? 76 | * 77 | * Each mention is supposed to be a sub-string, but what unit are the substrings 78 | * measured in terms of? Is it Unicode code points? That might make sense to 79 | * _normal_ developers, but not to Signal developers. 80 | * 81 | * Well maybe the substring is measured in terms of UTF-8 encoded bytes? After 82 | * all, UTF-8 is the de-facto standard for almost all text nowadays. 83 | * 84 | * But no! Signal seems to use the unit of "UTF-16 code units". Which is 85 | * *almost* the same as Unicode code points, except that emojis and other 86 | * characters beyond the BMP are represented using TWO UTF-16 code units. 87 | * This function exists to "advance" a byte-index forward to a certan UTF-16 88 | * code unit index, so that we can accurately identify substrings for 89 | * replacement. 90 | */ 91 | int index_of_utf16(const char *str, int index, int u16units, int end_u16, 92 | int len) 93 | { 94 | if (u16units == end_u16) 95 | return index; 96 | while (index < len) { 97 | int nbytes = utf8_nbytes(str[index]); 98 | index += nbytes; 99 | 100 | /* 4-byte UTF-8 representation is beyond the BMP. All code 101 | * points represented by 4 bytes in UTF-8 require surrogate 102 | * pairs in UTF-16. */ 103 | u16units += (nbytes == 4) ? 2 : 1; 104 | 105 | if (u16units == end_u16) { 106 | return index; 107 | } else if (u16units > end_u16) { 108 | /* BUG: can't split a surrogate pair for @mention */ 109 | CL_CRIT("attempted to split surrogate pair for " 110 | "@mention"); 111 | return index; 112 | } 113 | } 114 | CL_CRIT("indexed past end of string for @mention"); 115 | return index; 116 | } 117 | 118 | /* Copy text into the buffer, duplicating (escaping) any @ sign */ 119 | static void copy_in(struct sc_charbuf *cb, const char *str, int start, int end) 120 | { 121 | for (int i = start; i < end; i++) { 122 | sc_cb_append(cb, str[i]); 123 | if (str[i] == '@') 124 | sc_cb_append(cb, '@'); 125 | } 126 | } 127 | 128 | char *mention_from_json(const char *str, struct json_easy *je, uint32_t list) 129 | { 130 | int bytes = 0, u16units = 0; 131 | int len = strlen(str); 132 | uint32_t elem; 133 | struct sc_charbuf cb; 134 | int err; 135 | sc_cb_init(&cb, je->input_len); 136 | 137 | json_easy_for_each(elem, je, list) 138 | { 139 | uint64_t start, length; 140 | char *uuid; 141 | 142 | if ((err = je_get_uint(je, elem, "start", &start)) != JSON_OK) { 143 | CL_CRIT("mention_from_json: failed to load \"start\": " 144 | "%s\n", 145 | json_strerror(err)); 146 | goto err; 147 | } 148 | if ((err = je_get_uint(je, elem, "length", &length)) != 149 | JSON_OK) { 150 | CL_CRIT("mention_from_json: failed to load \"length\": " 151 | "%s\n", 152 | json_strerror(err)); 153 | goto err; 154 | } 155 | if ((err = je_get_string(je, elem, "uuid", &uuid)) != JSON_OK) { 156 | CL_CRIT("mention_from_json: failed to load \"uuid\": " 157 | "%s\n", 158 | json_strerror(err)); 159 | goto err; 160 | } 161 | 162 | /* Copy message up to the mention into the buffer */ 163 | if (start > u16units) { 164 | int new_bytes = index_of_utf16(str, bytes, u16units, 165 | start, len); 166 | copy_in(&cb, str, bytes, new_bytes); 167 | bytes = new_bytes; 168 | u16units = start; 169 | } 170 | 171 | /* Now append a UUID mention */ 172 | sc_cb_printf(&cb, "@(uuid:%s)", uuid); 173 | free(uuid); 174 | 175 | if (length) { 176 | /* Now skip over the part of the string specified */ 177 | int new_bytes = index_of_utf16(str, bytes, u16units, 178 | u16units + length, len); 179 | bytes = new_bytes; 180 | u16units += length; 181 | } 182 | } 183 | 184 | copy_in(&cb, str, bytes, len); 185 | sc_cb_trim(&cb); 186 | return cb.buf; 187 | err: 188 | sc_cb_destroy(&cb); 189 | return NULL; 190 | } 191 | 192 | char *json_quote_and_mention(const char *instr, struct signal_mention **ms, 193 | size_t *n) 194 | { 195 | size_t i = 0, u16extra = 0; 196 | struct sc_charbuf cb; 197 | struct sc_array mb; 198 | struct signal_mention ment; 199 | 200 | sc_cb_init(&cb, strlen(instr)); 201 | sc_arr_init(&mb, struct signal_mention, 1); 202 | 203 | for (i = 0; instr[i]; i++) { 204 | if (instr[i] == '"' || instr[i] == '\\') { 205 | sc_cb_append(&cb, '\\'); 206 | sc_cb_append(&cb, instr[i]); 207 | } else if (instr[i] == '\n') { 208 | sc_cb_append(&cb, '\\'); 209 | sc_cb_append(&cb, 'n'); 210 | } else if (instr[i] == '@' && instr[i + 1] == '@') { 211 | sc_cb_append(&cb, '@'); 212 | i++; 213 | } else if (instr[i] == '@' && instr[i + 1] != '@') { 214 | int kind, offset; 215 | char *uuid = mention_parse(instr + i, &kind, &offset); 216 | if (kind != MENTION_USER) { 217 | sc_cb_append(&cb, '@'); 218 | free(uuid); 219 | } else { 220 | ment.start = cb.length + u16extra; 221 | ment.length = 1; 222 | ment.uuid = uuid; 223 | sc_cb_append(&cb, 'X'); 224 | sc_arr_append(&mb, struct signal_mention, ment); 225 | i += offset - 1; 226 | } 227 | } else if (utf8_nbytes(instr[i]) == 4) { 228 | u16extra++; 229 | sc_cb_append(&cb, instr[i]); 230 | } else { 231 | sc_cb_append(&cb, instr[i]); 232 | } 233 | } 234 | *ms = (struct signal_mention *)mb.arr; 235 | *n = mb.len; 236 | return cb.buf; 237 | } 238 | 239 | char *json_quote_nomention(const char *instr) 240 | { 241 | struct sc_charbuf buf; 242 | sc_cb_init(&buf, strlen(instr) + 1); 243 | for (size_t i = 0; instr[i]; i++) { 244 | switch (instr[i]) { 245 | case '"': 246 | case '\\': 247 | sc_cb_append(&buf, '\\'); 248 | sc_cb_append(&buf, instr[i]); 249 | break; 250 | case '\n': 251 | sc_cb_append(&buf, '\\'); 252 | sc_cb_append(&buf, 'n'); 253 | } 254 | } 255 | return buf.buf; 256 | } 257 | -------------------------------------------------------------------------------- /src/tok.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #include "cbot/cbot.h" 8 | 9 | static ssize_t token_plain(char *s, ssize_t i) 10 | { 11 | while (s[i] && s[i] != ' ') 12 | i++; 13 | if (s[i]) { 14 | s[i] = '\0'; 15 | i++; 16 | } 17 | return i; 18 | } 19 | 20 | static ssize_t token_quote(char *s, ssize_t i) 21 | { 22 | size_t shift = 0; 23 | while (s[i]) { 24 | s[i - shift] = s[i]; 25 | if (s[i] == '"') { 26 | if (s[i + 1] == '"') { 27 | i++; 28 | shift++; 29 | } else if (s[i + 1] == ' ' || s[i + 1] == '\0') { 30 | s[i - shift] = '\0'; 31 | return i + 1; 32 | } else { 33 | return -1; 34 | } 35 | } 36 | i++; 37 | } 38 | return -1; 39 | } 40 | 41 | static ssize_t token(char *s, struct sc_array *a, ssize_t i) 42 | { 43 | while (s[i] == ' ') 44 | i++; 45 | 46 | if (s[i] == '\0') 47 | return 0; 48 | 49 | if (s[i] == '"') { 50 | sc_arr_append(a, char *, &s[i + 1]); 51 | return token_quote(s, i + 1); 52 | } else { 53 | sc_arr_append(a, char *, &s[i]); 54 | return token_plain(s, i); 55 | } 56 | } 57 | 58 | int cbot_tokenize(const char *msg, struct cbot_tok *result) 59 | { 60 | ssize_t idx = 0; 61 | struct sc_array a; 62 | char *str = strdup(msg); 63 | sc_arr_init(&a, char *, 32); 64 | 65 | while (idx >= 0 && str[idx]) { 66 | idx = token(str, &a, idx); 67 | } 68 | 69 | if (idx < 0) { 70 | free(str); 71 | sc_arr_destroy(&a); 72 | return (int)idx; 73 | } else { 74 | result->original = str; 75 | result->tokens = a.arr; 76 | result->ntok = a.len; 77 | return a.len; 78 | } 79 | } 80 | 81 | void cbot_tok_destroy(struct cbot_tok *tokens) 82 | { 83 | free(tokens->original); 84 | tokens->original = NULL; 85 | free(tokens->tokens); 86 | tokens->original = NULL; 87 | } 88 | -------------------------------------------------------------------------------- /src/utf8.h: -------------------------------------------------------------------------------- 1 | #ifndef UTF8_H_ 2 | #define UTF8_H_ 3 | 4 | /* Mask and bit pattern for a 1-byte character */ 5 | #define UTF8_MASK1 0b10000000 6 | #define UTF8_VAL1 0b00000000 7 | 8 | /* Mask and bit pattern for a 2-byte character */ 9 | #define UTF8_MASK2 0b11100000 10 | #define UTF8_VAL2 0b11000000 11 | 12 | /* Mask and bit pattern for a 3-byte character */ 13 | #define UTF8_MASK3 0b11110000 14 | #define UTF8_VAL3 0b11100000 15 | 16 | /* Mask and bit pattern for a 4-byte character */ 17 | #define UTF8_MASK4 0b11111000 18 | #define UTF8_VAL4 0b11110000 19 | 20 | /* Mask and bit pattern for a continuation byte */ 21 | #define UTF8_CMASK 0b11000000 22 | #define UTF8_CVAL 0b10000000 23 | 24 | static inline int utf8_nbytes(char first) 25 | { 26 | if ((first & UTF8_MASK1) == UTF8_VAL1) 27 | return 1; 28 | else if ((first & UTF8_MASK2) == UTF8_VAL2) 29 | return 2; 30 | else if ((first & UTF8_MASK3) == UTF8_VAL3) 31 | return 3; 32 | else if ((first & UTF8_MASK4) == UTF8_VAL4) 33 | return 4; 34 | else 35 | /* Default case: continuation bytes */ 36 | return 0; 37 | } 38 | 39 | #endif // UTF8_H_ 40 | -------------------------------------------------------------------------------- /subprojects/Unity.wrap: -------------------------------------------------------------------------------- 1 | [wrap-file] 2 | directory = Unity-2.5.1 3 | 4 | source_url = https://github.com/ThrowTheSwitch/Unity/archive/v2.5.1.tar.gz 5 | source_filename = Unity-2.5.1.tar.gz 6 | source_hash = 5ce08ef62f5f64d18f8137b3eaa6d29199ee81d1fc952cef0eea96660a2caf47 7 | -------------------------------------------------------------------------------- /subprojects/libircclient.wrap: -------------------------------------------------------------------------------- 1 | [wrap-file] 2 | directory = libircclient-1.10 3 | 4 | source_url = http://downloads.sourceforge.net/project/libircclient/libircclient/1.10/libircclient-1.10.tar.gz 5 | source_fallback_url = http://newcontinuum.dl.sourceforge.net/project/libircclient/libircclient/1.10/libircclient-1.10.tar.gz 6 | source_filename = libircclient-1.10.tar.gz 7 | source_hash = bbb26f3af348b252c5204917a7f91cfdf172f1b6afbf4df1e561b03e20503c2d 8 | 9 | patch_url = http://fake.url.please.dont.download.brennan.io 10 | patch_filename = libircclient-patch-1.10.tar.gz 11 | patch_hash = c4e004e7ababddada8299f4db0c93b0bccf8b453b589934668d00e6e1d1ce25a 12 | -------------------------------------------------------------------------------- /subprojects/nosj.wrap: -------------------------------------------------------------------------------- 1 | [wrap-git] 2 | directory = nosj.git 3 | url = https://github.com/brenns10/nosj 4 | revision = v2.2.1 5 | -------------------------------------------------------------------------------- /subprojects/sc-argparse.wrap: -------------------------------------------------------------------------------- 1 | [wrap-git] 2 | directory = sc-argparse.git 3 | url = https://git.sr.ht/~brenns10/sc-argparse 4 | revision = v0.2.0 5 | -------------------------------------------------------------------------------- /subprojects/sc-collections.wrap: -------------------------------------------------------------------------------- 1 | [wrap-git] 2 | directory = sc-collections.git 3 | url = https://git.sr.ht/~brenns10/sc-collections 4 | revision = v0.11.0 5 | -------------------------------------------------------------------------------- /subprojects/sc-lwt.wrap: -------------------------------------------------------------------------------- 1 | [wrap-git] 2 | directory = sc-lwt.git 3 | url = https://git.sr.ht/~brenns10/sc-lwt 4 | revision = v0.7.2 5 | -------------------------------------------------------------------------------- /subprojects/sc-regex.wrap: -------------------------------------------------------------------------------- 1 | [wrap-git] 2 | directory = sc-regex.git 3 | url = https://git.sr.ht/~brenns10/sc-regex 4 | revision = v0.4.0 5 | -------------------------------------------------------------------------------- /subprojects/sqlite.wrap: -------------------------------------------------------------------------------- 1 | [wrap-file] 2 | directory = sqlite-amalgamation-3320300 3 | source_url = https://www.sqlite.org/2020/sqlite-amalgamation-3320300.zip 4 | source_filename = sqlite-amalgamation-3320300.zip 5 | source_hash = e9cec01d4519e2d49b3810615237325263fe1feaceae390ee12b4a29bd73dbe2 6 | patch_url = https://wrapdb.mesonbuild.com/v1/projects/sqlite/3320300/5/get_zip 7 | patch_filename = sqlite-3320300-5-wrap.zip 8 | patch_hash = 2265bbd1cdec8b60c62d9f3a7025cd9c7362d282f3dcda7065e698454096c242 9 | 10 | [provide] 11 | sqlite3 = sqlite_dep 12 | -------------------------------------------------------------------------------- /testing/test_coinmarketcap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import code 3 | import threading 4 | 5 | from flask import Flask, jsonify 6 | app = Flask(__name__) 7 | 8 | price = 16500 9 | tether = 0.9998 10 | 11 | @app.route('/', defaults={'path': ''}) 12 | @app.route('/') 13 | def hello_world(path): 14 | # lol 15 | return jsonify({ 16 | 'data': { 17 | 'BTC': [ 18 | { 19 | 'quote': { 20 | 'USD': { 21 | 'price': price, 22 | }, 23 | }, 24 | }, 25 | ], 26 | 'USDT': [ 27 | { 28 | 'quote': { 29 | 'USD': { 30 | 'price': tether, 31 | }, 32 | }, 33 | }, 34 | ], 35 | }, 36 | }) 37 | 38 | 39 | if __name__ == '__main__': 40 | threading.Thread(target=app.run, kwargs={"port": 4100}).start() 41 | code.interact(local=locals()) 42 | -------------------------------------------------------------------------------- /tests/mentions.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include "../src/signal/internal.h" 11 | #include "cbot/json.h" 12 | 13 | // NB: meson incremental builds don't pick up on changes to the JSON file. 14 | // This generally isn't a big deal, but just know that you may need to touch 15 | // the test C file after updating the JSON. 16 | __asm("json_buf: .incbin \"mentions.json\""); 17 | __asm("json_buf_end: .byte 0x00"); 18 | extern char json_buf[]; 19 | 20 | struct json_easy *je; 21 | 22 | void setUp(void) 23 | { 24 | je = json_easy_new(json_buf); 25 | json_easy_parse(je); 26 | } 27 | 28 | void tearDown(void) 29 | { 30 | // clean stuff up here 31 | json_easy_destroy(je); 32 | } 33 | 34 | static void do_test_mention_from_json(const char *name) 35 | { 36 | uint32_t item, mentions; 37 | char *message, *result; 38 | 39 | TEST_ASSERT(je_get_object(je, 0, name, &item) == JSON_OK); 40 | TEST_ASSERT(je_get_array(je, item, "mentions", &mentions) == JSON_OK); 41 | TEST_ASSERT(je_get_string(je, item, "message", &message) == JSON_OK); 42 | TEST_ASSERT(je_get_string(je, item, "result", &result) == JSON_OK); 43 | 44 | char *output = mention_from_json(message, je, mentions); 45 | TEST_ASSERT_EQUAL_STRING(result, output); 46 | 47 | free(output); 48 | free(message); 49 | free(result); 50 | } 51 | 52 | static void test_mention_from_json(void) 53 | { 54 | do_test_mention_from_json("test_basic"); 55 | do_test_mention_from_json("test_two"); 56 | do_test_mention_from_json("test_replace_text"); 57 | } 58 | 59 | int main(int argc, char **argv) 60 | { 61 | UNITY_BEGIN(); 62 | RUN_TEST(test_mention_from_json); 63 | return UNITY_END(); 64 | } 65 | -------------------------------------------------------------------------------- /tests/mentions.json: -------------------------------------------------------------------------------- 1 | { 2 | "test_basic": { 3 | "message": "hello 💩 ", 4 | "mentions": [ 5 | {"uuid": "foo", "start": 9, "length": 1} 6 | ], 7 | "result": "hello 💩 @(uuid:foo)" 8 | }, 9 | "test_two": { 10 | "message": "hello 💩  💩  :)", 11 | "mentions": [ 12 | {"uuid": "foo", "start": 9, "length": 1}, 13 | {"uuid": "bar", "start": 14, "length": 1} 14 | ], 15 | "result": "hello 💩 @(uuid:foo) 💩 @(uuid:bar) :)" 16 | }, 17 | "test_replace_text": { 18 | "message": "hello XXX!", 19 | "mentions": [ 20 | {"uuid": "foo", "start": 6, "length": 3} 21 | ], 22 | "result": "hello @(uuid:foo)!" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/meson.build: -------------------------------------------------------------------------------- 1 | fs = import('fs') 2 | 3 | tests = [ 4 | 'mentions.c', 5 | ] 6 | unity_dep = dependency( 7 | 'Unity', 8 | fallback: ['Unity', 'unity_dep'], 9 | ) 10 | foreach t: tests 11 | testname = fs.name(t) 12 | exe = executable( 13 | 'test_' + testname, 14 | t, 15 | dependencies : [libcbot_dep, unity_dep] + cbot_deps, 16 | include_directories : inc, 17 | ) 18 | test('TEST_' + testname, exe) 19 | endforeach 20 | --------------------------------------------------------------------------------