diff --git a/.forgejo/workflows/build-release.yaml b/.forgejo/workflows/build-release.yaml new file mode 100644 index 0000000..c649ab1 --- /dev/null +++ b/.forgejo/workflows/build-release.yaml @@ -0,0 +1,85 @@ +name: Builds static Parsee +on: [push] +jobs: + compile: + strategy: + matrix: + distro: + - debian + arch: + - amd64 + - arm64 + runs-on: + - ${{ matrix.distro }} + - ${{ matrix.arch }} + container: + image: ${{ matrix.distro }} + steps: + - name: Install LMDB+OpenSSL tools + run: | + echo $(uname -a) $(env | grep container) + apt update -y + apt install -y build-essential liblmdb-dev nodejs libssl-dev git wget python3 python3-pip bzip2 + - name: Clone everything + run: | + mkdir -p repos + git clone --depth=1 https://git.musl-libc.org/git/musl repos/musl + wget https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-3.6.1/mbedtls-3.6.1.tar.bz2 + tar -xvf mbedtls-3.6.1.tar.bz2 + mv mbedtls-3.6.1/ repos/mbed + git clone --depth=1 https://github.com/LMDB/lmdb repos/lmdb + git clone -b add-mbed --single-branch https://git.telodendria.io/lda/Cytoplasm repos/cyto + echo "PREFIX=$GITHUB_WORKSPACE/usr" >> $GITHUB_ENV + echo "PATH=$GITHUB_WORKSPACE/usr/bin:$PATH" >> $GITHUB_ENV + echo "INCLUDE_PATH=$GITHUB_WORKSPACE/usr/include" >> $GITHUB_ENV + echo "LIBRARY_PATH=$GITHUB_WORKSPACE/usr/lib" >> $GITHUB_ENV + - name: Build musl + run: | + cd repos/musl + ./configure --prefix=${PREFIX} + make -j$(nproc) + make install -j$(nproc) + alias musl-gcc="musl-gcc -Wl,-Bstatic -L ${PREFIX}/lib" + - name: Build MbedTLS + run: | + alias musl-gcc="musl-gcc -Wl,-Bstatic -L ${PREFIX}/lib" + cd repos/mbed + python3 -m pip install -r scripts/basic.requirements.txt --break-system-packages + make CC=musl-gcc -j$(nproc) + make install DESTDIR=${PREFIX} -j$(nproc) + - name: Build LMDB + run: | + alias musl-gcc="musl-gcc -Wl,-Bstatic -L ${PREFIX}/lib" + cd repos/lmdb/libraries/liblmdb + make CC=musl-gcc + make install prefix=${PREFIX} + - name: Build Cytoplasm + run: | + alias musl-gcc="musl-gcc -Wl,-Bstatic -L ${PREFIX}/lib" + cd repos/cyto + git fetch + rm configure + wget https://kappach.at/configure + chmod +x configure + ./configure --cc=musl-gcc --prefix=${PREFIX} --with-lmdb --with-mbed + make -j$(nproc) + make install + - name: Clone/Build Parsee + run: | + git clone https://git.kappach.at/${{ github.repository}} parsee + cd parsee + alias musl-gcc="musl-gcc -Wl,-Bstatic -L ${PREFIX}/lib" + musl-gcc -static configure.c -o configure && ./configure -s -l + make CC=musl-gcc CYTO_INC=${PREFIX}/include CYTO_LIB=${PREFIX}/lib LDFLAGS=-s + make PREFIX=bins install + - name: Create a final archive + run: | + cd parsee + echo "NAM=parsee-$(uname)-$(uname -m)" >> $GITHUB_ENV + echo "DIR=$PWD/bins" >> $GITHUB_ENV + - name: Upload it all(as a ZIP) + uses: https://code.forgejo.org/forgejo/upload-artifact@v4 + with: + name: ${{ env.NAM }} + path: ${{ env.DIR }} + compression-level: 9 diff --git a/.forgejo/workflows/check-gcc.yaml b/.forgejo/workflows/check-gcc.yaml new file mode 100644 index 0000000..5404fa6 --- /dev/null +++ b/.forgejo/workflows/check-gcc.yaml @@ -0,0 +1,41 @@ +name: Checks Parsee's correctness on GCC+Debian +on: [push] +jobs: + compile: + strategy: + matrix: + distro: + - debian + arch: + - amd64 + runs-on: + - ${{ matrix.distro }} + - ${{ matrix.arch }} + container: + image: ${{ matrix.distro }} + steps: + - name: Install LMDB+OpenSSL tools + run: | + echo $(uname -a) $(env | grep container) + apt update -y + apt install -y build-essential liblmdb-dev nodejs libssl-dev git + - name: Clone/Configure Cytoplasm + run: | + git clone https://git.kappach.at/KappaChat/Cytoplasm --depth=1 + cd Cytoplasm + ./configure --with-lmdb --prefix=/usr + - name: Build Cytoplasm + run: 'cd Cytoplasm && make -j$(nproc)' + - name: Install Cytoplasm + run: 'cd Cytoplasm && make install' + - name: Clone Parsee + uses: actions/checkout@v3 + - name: Build Parsee + run: | + cc configure.c -o configure && ./configure + echo 'CFLAGS=-Werror -Wall -Wextra -pedantic -fanalyzer' >> build.conf + cat build.conf + make && make ayadoc + - name: Install Parsee + run: 'make install' + diff --git a/.gitignore b/.gitignore index f2c57e6..09b6fd1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,11 @@ parsee* parsee *.swp .* -data -data/* +data* +data*/* +Makefile +configure +gmon.out tools/out tools/out/* @@ -19,3 +22,11 @@ tags # Whitelists !etc/* !etc/** +!.forgejo +!.forgejo/* +!.forgejo/** + +!.guix +!.guix/* +!.guix/** + diff --git a/.guix/modules/parsee.scm b/.guix/modules/parsee.scm new file mode 100644 index 0000000..75b8b64 --- /dev/null +++ b/.guix/modules/parsee.scm @@ -0,0 +1,82 @@ +(define-module (parsee) + #:use-module (guix packages) + #:use-module (guix git-download) + #:use-module (guix build-system gnu) + #:use-module (guix gexp) + #:use-module (guix utils) + #:use-module (gnu packages tls) + #:use-module (gnu packages databases) + #:use-module ((guix licenses) #:prefix license:)) + +(define vcs-file? + (or (git-predicate + (dirname (dirname (current-source-directory)))) + (const #t))) + +(define-public cytoplasm + (let ((commit "32f31fe6d61583630995d956ed7bd7566c4dc14f") + (revision "0")) + (package + (name "cytoplasm") + (version (git-version "0.4.1" revision commit)) + (source + (origin + (method git-fetch) + (uri (git-reference + (url "https://git.telodendria.io/Telodendria/Cytoplasm") + (commit commit))) + (file-name (git-file-name name version)) + (sha256 + (base32 "09x5xfswryf3wjs1synh972yr2fmpjmffi7pjyjdzb4asqh4whrv")))) + (build-system gnu-build-system) + (arguments + (list + #:tests? #f + #:configure-flags #~'("--with-lmdb") + #:phases #~(modify-phases %standard-phases + (add-before 'configure 'add-ld-flags + (lambda _ + (substitute* "./configure" + (("(LDFLAGS=\"\\$\\{LIBS\\} \\$\\{LDFLAGS\\})\"" all flags) + (string-append flags " -Wl,-rpath=" #$output "/lib\""))) + (mkdir-p (string-append #$output "/lib")))) + (replace 'configure + (lambda* (#:key configure-flags #:allow-other-keys) + (apply invoke `("./configure" + ,(string-append "--prefix=" #$output) + ,@configure-flags))))))) + (inputs (list openssl lmdb)) + (home-page "https://git.telodendria.io/Telodendria/Cytoplasm") + (synopsis "General-purpose high-level networked C library") + (description "Cytoplasm is a general-purpose C library for creating +high-level (particularly networked and multi-threaded) C applications.") + (license license:expat)))) + +(define-public parsee + (package + (name "parsee") + (version "0.0.1-git") + (source + (local-file "../.." "parsee-checkout" + #:recursive? #t + #:select? vcs-file?)) + (build-system gnu-build-system) + (arguments + (list + #:tests? #f + #:make-flags #~(list "CC=gcc" + (string-append "CYTO_INC=" #$cytoplasm "/include") + (string-append "CYTO_LIB=" #$cytoplasm "/lib") + (string-append "PREFIX=" #$output)) + #:phases #~(modify-phases %standard-phases + (replace 'configure + (lambda* (#:key inputs #:allow-other-keys) + (let ((gcc (string-append (assoc-ref inputs "gcc") "/bin/gcc"))) + (invoke gcc "configure.c" "-o" "configure") + (invoke "./configure"))))))) + (home-page "https://git.kappach.at/lda/Parsee") + (synopsis "Jealous Matrix to XMPP bridge") + (description "Parsee is a Matrix-XMPP bridge written in C99 with Cytoplasm.") + (license license:agpl3+))) + +parsee diff --git a/CHANGELOG.md b/CHANGELOG.md index 5762504..f4ffcde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,73 @@ Dates are to be written as DD/MM/YYYY. Please update the changelog as you go, no one wants to keep track of every commit done between releases. +## Release +*There is currently no full releases of Parsee* + +## Beta +*There is currently no beta releases of Parsee* + ## Alpha + +### v0.3.0[lunar-rainbow] +This is the first release of 2025! +TBD +#### New things +- Allow admins to remove users from the nofly list. +- Parsee can now access the homeserver/component locally (rather than over the network) +- The endpoint for media has been changed to '/media/[SERV]/[ID]?hmac=...' +- Add parsee-plumb tool to manage plumbing from outside Parsee if it is not running. +- Add packaging for Guix (see #18) +- Parsee is now dependent on authenticated media to function. + (Please, Matrix, do _not_ touch anything, I do _not_ want to mess with this anymore) +#### Bugfixes +- Fix potential infinite loops when processing some messages. +- Parsee now handles pinging puppets from Matrix more sanely. + +### v0.2.0[star-of-hope] <8/11/2024> +Fixes some media metadata things, replaces the build system, +tries out avatar support some more, MUC contexts, and speeds +up Parsee just a bit. MbedTLS support is still highly unstable. +#### New things +- Start dealing with some basic PEP and vCard-based avatar +support from both sides. +- Banning Parsee from a XMPP MUC effectively unlinks it from +the MUC. +- Start adding basic documentation to Parsee, through the +wiki page. +- Add MUC whitelists for plumbing, alongside a `whitelist` tool +- Add parameter for setting the max stanza size allowed, with +the default being the XMPP minimum of 10000 bytes. +- Allows experimental MbedTLS through a specific Cytoplasm +patch (though still unstable AND slow). +- Does basic work towards NetBSD support(especially with DEC Alpha) +- Start contextualising XMPP commands(all/Parsee admins/MUCs). + +#### Bugfixes +- Adds more information to media events so that clients can +behave. +- Fixes issues where SIGPIPE can actually just kill Parsee. +- "Lone" XMPP messages no longer render weirdly on Element +Android's weird rendering. +- Start fixing bug where Parsee takes several seconds to send +a message coming from XMPP with MbedTLS(it is still slow and +unstable) +- Fix issue where the XMPP server would kill Parsee if avatars +are too large. +- Refactor some of the code to abstract sending stanzas down +the wire to a specific function, thus allowing us to check +for certain conditions more easily(for example, verifying the +size on the fly, or having extensions being able to postprocess +the stanza). +- Parsee now stops when a stream error is received, instead of +being in a limbo state. +- The format for links has been changed to be slighlty *less* +annoying. + +#### Deprecated features +- The old `build.c` and `Makefile`s used for building are removed, +and replaced by the `configure.c` C file(/script via TCC). + ### v0.1.0[tomboyish-bridges-adventure] <9/9/2024> Nothing much to say, but this is the first alpha release of Parsee. May occasionally deadlock. diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..c7f8b75 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,14 @@ +# Lines starting with a '#' are ignored. +# This is a list of people who have contributed to Parsee. +# You *may* add some information about yourself after having made a change +# to the repository(or wiki!), with those fields: +# (N) - A nickname +# (L) - An approximate place of residence +# (X) - An XMPP URI +# (M) - A Matrix MXID +# (D) - A short description about yourself +(N): LDA +(L): France +(X): xmpp:lda@monocles.eu +(M): @fourier:ari.lt +(M): @fourier:ari.lt diff --git a/DATES.TXT b/DATES.TXT index f54dbc0..4da14dc 100644 --- a/DATES.TXT +++ b/DATES.TXT @@ -1,7 +1,3 @@ Some dates for Parsee-related events. They mostly serve as LDA's TODOs with a strict deadline: - - ~September 2024[tomboyish-bridges-adventure]: - Get Parsee into the _Phantasmagoria of Bug View_ stage (essentially - v0.0.1 for a public testing) once I can afford `yama`, and start - bridging the Matrix room alongside a shiny XMPP MUC, bridged by - yours truly. + - Get star-of-hope out by November 8 diff --git a/LICENSE b/LICENSE index 1e1282b..cb9aa73 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -For the files src/Parsee/HMAC.c, src/XML/*, tools/*, src/include/XML.h, etc/*, and Makefile, +For the files src/include/Unistring.h, src/Unistr.h rc/Parsee/HMAC.c, src/XML/*, tools/*, src/include/XML.h, etc/*, and Makefile, see COPYING.CC0. -For any other file in src/, see COPYING.AGPL as the primary license. +For any other file in src/, see COPYING.AGPL as the primary license(AGPL-3.0-or-later) As Parsee depends on Cytoplasm, its license is left here in COPYING.CYTO diff --git a/Makefile b/Makefile deleted file mode 100644 index c1125b3..0000000 --- a/Makefile +++ /dev/null @@ -1,90 +0,0 @@ -# (GNU)Makefile for building Parsee -# ================================ -# TODO: Consider making something akin to a configure script that checks -# for dependencies, or maybe even use *autoconf* (the devil!) - - -# =========================== Parsee Flags ============================= - -include build.conf - -REPOSITORY=$(shell git remote get-url origin) - -# =========================== Compilation Flags ============================ -CYTO_INC ?=/usr/local/include/ # Where Cytoplasm's include path is - # located. -CYTO_LIB ?=/usr/local/lib # Where's Cytoplasm's library is - # located. -PREFIX ?=/usr/local - -AYAS=ayaya -ETC=etc -FCFLAGS=-I$(SOURCE) -I$(INCLUDES) -I$(CYTO_INC) -DNAME="\"$(NAME)\"" -DVERSION="\"$(VERSION)\"" -DREPOSITORY=\"$(REPOSITORY)\" -DCODE=\"$(CODE)\" $(CFLAGS) -FLDFLAGS=-L $(CYTO_LIB) -lCytoplasm $(LDFLAGS) -AFLAGS=-C "$(ETC)/ayadoc/style.css" -p "$(NAME)" -# ============================ Compilation ================================= -SRC_FILES:=$(shell find $(SOURCE) -name '*.c') $(shell find $(ETC)/media -name '*.png') -OBJ_FILES:=${subst $(ETC)/media/,$(OBJECT)/,${subst $(SOURCE)/,$(OBJECT)/,$(patsubst %.png, %.o, $(patsubst %.c, %.o, $(SRC_FILES)))}} - -CPP_FILES:=$(shell find $(INCLUDES) -name '*.h') -AYA_FILES:=${subst $(INCLUDES)/,$(AYAS)/,$(patsubst %.h, %.html, $(CPP_FILES))} - -all: utils binary - -binary: $(OBJ_FILES) - $(CC) $(FLDFLAGS) $(OBJ_FILES) -o $(BINARY) -tags: $(SRC_FILES) - @ctags --recurse $(SOURCE)/ - -clean: - rm -rf $(OBJECT) $(BINARY) $(AYAS) - -$(OBJECT)/%.o: $(ETC)/media/%.png - @mkdir -p $(shell dirname "$@") - @echo "const char media_$(shell basename $< .png)[] =" > $@.c - @base64 $< | \ - sed -e 's/^\(.*\)$$/ "\1"/' | \ - sed -e '$$ s/^\(.*\)$$/\1;/' >> $@.c - $(CC) -c $(FCFLAGS) $@.c -o $@ -$(OBJECT)/%.o: $(SOURCE)/%.c - @mkdir -p $(shell dirname "$@") - $(CC) -c $(FCFLAGS) $< -o $@ - -utils: - (cd tools && make) - -ayadoc: utils $(AYA_FILES) - -$(AYAS)/%.html: $(INCLUDES)/%.h - @mkdir -p $(shell dirname "$@") - tools/out/aya $(AFLAGS) -i $< -o $@ - - -# Installs everything. -install: binary utils ayadoc install_setup install_parsee install_tools install_aya install_man - @echo Installed $(NAME) to $(PREFIX)! - -install_setup: - install -dm755 "$(PREFIX)/bin" - install -dm755 "$(PREFIX)/share/doc" - install -dm755 "$(PREFIX)/man" - -install_parsee: - install -Dm755 "$(BINARY)" "$(PREFIX)/bin/$(BINARY)" - -TOOLS:=$(shell find 'tools/out' -name '*') -ITOOL:=${subst tools/out/,$(PREFIX)/bin/,$(patsubst tools/out/%, tools/out/$(BINARY)-%, $(TOOLS))} -install_tools: $(ITOOL) -$(PREFIX)/bin/$(BINARY)-%: tools/out/% - install -Dm755 "$<" "$@" - -IHTML:=${subst $(AYAS)/,$(PREFIX)/share/doc/$(BINARY)/,$(AYA_FILES)} -install_aya: $(IHTML) -$(PREFIX)/share/doc/$(BINARY)/%: $(AYAS)/% - install -Dm644 "$<" "$@" - -MPAGE:=$(shell find 'etc/man' -name '*.*') -PPAGE:=${subst etc/man/,$(PREFIX)/man/,$(MPAGE)} -install_man: $(PPAGE) -$(PREFIX)/man/%: etc/man/% - install -Dm644 "$<" "$@" diff --git a/README.MD b/README.MD index 51fcf7f..f9522e8 100644 --- a/README.MD +++ b/README.MD @@ -1,45 +1,55 @@ # Parsee - the jealous XMPP<=>Matrix bridge Parsee is a Matrix<=>XMPP bridge written in C99, with Cytoplasm, similar to Bifrost, but it is NOT a drop-in replacment. +Currently, it is *alpha* stage, which means that I wouldn't recommend using this in production, +as I can change anything, at any time, and it may behave strangely at times. ## Why? ### Naming The name 'Parsee' is actually a reference to [Parsee Mizuhashi](https://en.touhouwiki.net/wiki/Parsee_Mizuhashi), -a "*bridge* princess". +a "*bridge* princess". The other name you actually can sometimes see explains itself, so I won't +be talking about it. ### Reasoning (personal to LDA) I hate Bifrost. I also wanted to dip my toes in XMPP, XML, and bridges a bit. Also, as a sister project to KappaChat, this means that I can integrate Parsee with KappaChat however I wish it to be, which allows me to mess around with a codebase I'm already familiar with. -A more "up-to-date" reason may be to have a small, 'Just Werks' bridging solution *that is good*. +A more "up-to-date" reason may be to have a small, 'Just Werks' bridging solution *that is good*, +and maybe as a testing ground for Cytoplasm features I sometimes add. -Well, I'm *trying* to do that, at least. -Please scream at me if that fails(or just doesn't run on a overclocked Raspberry -Pi 4B, which, by the way, is literally where Parsee+XMPP is running for now.) +(Well, I'm *trying* to do that, at least. +Please scream at me if that fails(or just doesn't run on a overclocked Raspberry Pi 4B)) ### "Why not just use Matrix lol" ### "Why not just use XMPP lol" -These two having the same answer should be enough information. Also can I *just* have fun? +These two having the same answer should be enough information. One could also argue that both sides need to migrate(onboard) the other side, so a bridge may be a good way to start. ## BUILDING ```sh -$ make # This generates a 'parsee' executable. -$ cd tools # If you want to build more tools -$ make && cd .. -$ make ayadoc # If you want to build HTML documentation -$ make [PREFIX=(install path)] install # To install Parsee. +$ cc configure.c -o configure # that or use tcc -run to consolidate these two steps. +$ ./configure # use -s if you want static Parsee+MbedTLS, use -s -l if LMDB is needed +$ make +$ make [PREFIX=...] install # run as root if on a protected dir like /usr ``` If there are any Cytoplasm-related build failures, you may want to check the Makefile to change a few variables (you can set `CYTO_INC` and `CYTO_LIB` for Cytoplasm's include and library paths specifically.) +If you build with MbedTLS, please mind setting the `CYTO_TLS_CA` env to Parsee. ### DEPENDENCIES Parsee tries to avoid dependencies aside from [Cytoplasm](https://git.telodendria.io/Telodendria/Cytoplasm). Itself optionally depends on a good POSIX implementation, and optionally OpenSSL/LMDB (highly recommended, but you can get away without those if you're adventurous). +### BUILDING WITH GUIX +If you have [Guix](https://guix.gnu.org/) installed, you can build Parsee using `guix package -f guix.scm`, or test it +using `guix shell -f guix.scm`. You can also generate a Docker/OCI image. +```sh +guix pack -f docker -S /bin=bin -L.guix/modules parsee +``` + ## RUNNING First off, you may want to configure Parsee by running the `config` tool(generally named `parsee-config` in most cases), with the correct flags, like here. @@ -51,7 +61,8 @@ parsee-config \ -H 'blow.hole' \ # Matrix homeserver name -J 'parsee.blow.hole' \ # XMPP component host, must be reachable -s 'A very secure XMPP component secret' \ - -p 5347 + -p 5347 \ + -M 65535 # Maximum stanza size. Stanzas larger than this from Parsee will be dropped to avoid stream closures. Leave this empty if you're unsure. ``` If everything goes well, it should generate a `parsee.json` file. @@ -64,33 +75,44 @@ returns with a landing page, then this side works. You can read it for some more ## DOCS Currently, the main sources of documentation are the Ayadocs(for headers) and the manpages -(see `etc/man`) +(see `etc/man`). ## TODOS before 1.0 rolls around +- Make Parsee actually go *vroooooooooommmmmmm*. +- Make sure Parsee can easily run on just about any reasonable POSIX +system. +- Avoid making 'back-puppets' from Matrix as much as possible +- Extension support. I'd need to design a good system, and maybe do it +with either shared libraries(`dlopen`/`dlclose` on POSIX) or use a +language like Janet or Lua. - Add [libomemo](https://github.com/gkdr/libomemo) or something as an optional dependency. - It depends on more stuff anyways, and I don't want to weigh down the dependency list of Parsee for that. - Matrix's libolm is deprecated. They replaced it with a Rust version that pulls in *way too many* dependencies, and that lacks a C binding. We may - put in the work of either forking off libolm or making a binding to KappaChat. + ~~put in the work of either forking off libolm or~~ be making a binding with + KappaChat(when I get around to remaking UI :p). - Josh did infact tell me that maybe C bindings may happen. I'd be willing to help out, but IDK. In any case, this will at best be an extension packagers may integrate properly. - Get rid of the '?'-syntax and use another invalid Matrix char/valid XMPP char ('$'?) for escaped? -- PROPER FUCKING AVATARS - XMPP->Matrix is decent, Matrix->XMPP is effectiveny not done - Consider making room/MUC admins/owners be able to plumb instead of it being restricted to Parsee admins, with permission from MUC owners, too - Limiting to admins may be a way to "control" consent for both, but this is only if Parsee admins are good-willed, which we must assume such statment to be false by default. + - Currently, MUC owners may kick Parsee out, with the effect of unlinking the + MUC. - Look at XEPS-TBD.TXT for XEPs to be done - Add a MUC server to Parsee, such that it may be able to hook onto it and therefore support XMPP->Matrix bridging. - Manage MUC DMs in a reasonable manner. Thanks `@freeoffers4u:matrix.org` for being a fucking annoyance and DMing an old Parsee semi-anon user for no clear reason. -- Deadlocks. It's always deadlocks. +- Make Parsee cope with stream closures(i.e: XMPP server turning off) better. As of +now, it just kills itself when that happens, instead of trying to negociate a new +connection, which would be a better method that would actually fit Parsee's own +principles. ## DONATING/CONTRIBUTING If you know things about XMPP or Matrix, yet aren't familiar with C99, or just @@ -107,7 +129,7 @@ You may also donate to [the LiberaPay](https://en.liberapay.com/Parsee), alongsi currently maintaining Cytoplasm. ## IM chats -Please avoid asking for help/issues here. If you *really* want, you may just open +Please avoid asking for help/issues there. If you *really* want, you may just open an issue and link it over it. Basic respect for others/not being an asshat is required. (Also, these are temporary room aliases.) - [#parsee:tedomum.net](https://matrix.to/#/%23parsee:tedomum.net) diff --git a/XEPS-TBD.TXT b/XEPS-TBD.TXT index ae74f4f..9ed28cf 100644 --- a/XEPS-TBD.TXT +++ b/XEPS-TBD.TXT @@ -21,10 +21,14 @@ Somewhat implemented XEPs: This allows reactions, which Matrix also has support to. The two systems don't seem *too* restrictive on one-another (unlike some IM platforms I won't mention), so this doesn't sound too bad to do - HALF-IMPLEMENTED: Removing reacts won't work. + TODO: Add support from Matrix. ~ https://xmpp.org/extensions/xep-0184.html Only Matrix->XMPP as of now. Requesting data from Matrix ASes without /sync seems like a non-option as of now, which _sucks_. + ~ https://xmpp.org/extensions/xep-0084.html + Avatar support would be extremely useful, if just a QoL improvment. + Matrix and XMPP both have support for these. + XEP-0084 is a pain in the ass to implement and seems generally just For future XEPs: - https://xmpp.org/extensions/xep-0449.html @@ -34,12 +38,7 @@ For future XEPs: which is used along PEP, it seems, and meanwhile Matrix has ''support'' for packs too, tracking them is between "annoyance" and "yeah, no.". -ON STANDBY BECAUSE THESE HAVE BEEN TERRIBLE TO DEAL WITH AND WHO KEEPS WRITING -THESE I WANT TO SEND THEM A NICE, BRIGHT GIFT: - x https://xmpp.org/extensions/xep-0084.html - Avatar support would be extremely useful, if just a QoL improvment. - Matrix and XMPP both have support for these. - XEP-0084 is a pain in the ass to implement and seems generally just +On Standby: unreliable, however. x https://xmpp.org/extensions/xep-0080.html Can't think of a good analogy to these... @@ -48,7 +47,7 @@ THESE I WANT TO SEND THEM A NICE, BRIGHT GIFT: Not XEPs, but ideas that _needs_ to be added: ~ "GIVE THE PUPPETS APPROPRIATE PLS/ROLES" - Hydro/t4d Happens on Matrix. I'll need to handle that on XMPP as well. - - Standalone/Static Parsee, ideally as small as it can be(if not as APE). + ~ Standalone/Static Parsee, ideally as small as it can be(if not as APE). - Kappa-like extension system(maybe bridging more than just Matrix-XMPP.) - https://www.youtube.com/watch?v=InL414iDZmY diff --git a/build.c b/build.c deleted file mode 100644 index b80a7d9..0000000 --- a/build.c +++ /dev/null @@ -1,863 +0,0 @@ -/* build.c - Simple, POSIX, non-Cytoplasm utility to build out - * the entirety of Parsee from scratch, without any Makefiles. - * - * The main reason why this tool exists is merely because the - * current Make-based building is not POSIX compliant, and I - * am simply not porting it to be. The Makefile shall stay - * supported however, but if possible, use build.c. - * To run it, just build it with: - * cc build.c -o /tmp/build && /tmp/build - * - * TODO: Parallel jobs, CFLAGS and LDFLAGS. - * Note that this bit is formatted differently. - * -------------------------------- - * LICENSE: CC0 - * Written-By: LDA [lda@freetards.xyz] [@fourier:ari.lt] */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define DEFAULT_BUILD_PATH "build.conf" -#define DEFAULT_COMPILER "cc" -#define Compiler(info) (info.basic.cc ? info.basic.cc : DEFAULT_COMPILER) - -static char * -string_rep_ext(char *in, char *ext1, char *ext2) -{ - char *end; - size_t inLen; - if (!in || !ext1 || !ext2) - { - return NULL; - } - - inLen = strlen(in); - end = inLen + in; - - while (end >= in) - { - if (!strcmp(end, ext1)) - { - size_t cpyLen = end - in; - size_t extLen = strlen(ext2); - char *ret = malloc(cpyLen + extLen + 1); - - memcpy(ret, in, cpyLen); - memcpy(ret + cpyLen, ext2, extLen); - ret[cpyLen + extLen] = '\0'; - return ret; - } - end--; - } - return NULL; -} -static char * -string_dup(char *in) -{ - char *out; - size_t len; - if (!in) - { - return NULL; - } - len = strlen(in); - out = malloc(len + 1); - memcpy(out, in, len); - out[len] = '\0'; - - return out; -} -static char * -string_cat(char *in1, char *in2) -{ - char *out; - size_t len1, len2; - if (!in1) - { - return string_dup(in2); - } - if (!in2) - { - return string_dup(in1); - } - - len1 = strlen(in1); - len2 = strlen(in2); - out = malloc(len1 + len2 + 1); - memcpy(out, in1, len1); - memcpy(out + len1, in2, len2); - out[len1 + len2] = '\0'; - - return out; -} -static char * -trim_nl(char *in) -{ - char *tc; - if (!in) - { - return NULL; - } - - while ((tc = strrchr(in, '\n'))) - { - *tc = '\0'; - } - - return in; -} - -typedef struct str_array { - char **values; - size_t quantity; -} str_array_t; -static str_array_t * -str_array_create(void) -{ - str_array_t *ret = malloc(sizeof(*ret)); - - ret->values = NULL; - ret->quantity = 0; - return ret; -} -static void -str_array_free(str_array_t *arr) -{ - size_t i; - if (!arr) - { - return; - } - - for (i = 0; i < arr->quantity; i++) - { - if (arr->values[i]) free(arr->values[i]); - } - if (arr->values) free(arr->values); - free(arr); -} -static void -str_array_set(str_array_t *arr, size_t i, char *str) -{ - if (!arr) - { - return; - } - - if (i >= arr->quantity) - { - size_t size = (i+1) * sizeof(*arr->values); - size_t j; - arr->values = realloc(arr->values, size); - for (j = arr->quantity; j <= i; j++) - { - arr->values[j] = NULL; - } - arr->quantity = i + 1 ; - } - if (arr->values[i]) free(arr->values[i]); - arr->values[i] = string_dup(str); -} -static void -str_array_add(str_array_t *arr, char *str) -{ - if (!arr) - { - return; - } - str_array_set(arr, arr->quantity, str); -} -static char * -str_array_get(str_array_t *arr, size_t i) -{ - if (!arr) - { - return NULL; - } - return i < arr->quantity ? arr->values[i] : NULL; -} -static size_t -str_array_len(str_array_t *arr) -{ - return arr ? arr->quantity : 0; -} - -typedef struct buildinfo { - struct basic { - char *codename; - char *version; - - char *name; - - char *binary; - - char *src; - char *inc; - char *obj; - - char *cflags, *ldflags; - - char *cc; - } basic; - - char *repo; -} buildinfo_t; -static void -destroy_buildinfo(buildinfo_t *info) -{ - if (!info) - { - return; - } - -#define FreeIfExistent(v) do \ - { \ - if (v) \ - { \ - free(v); \ - v = NULL; \ - } \ - } while (0) - - FreeIfExistent(info->basic.codename); - FreeIfExistent(info->basic.version); - FreeIfExistent(info->basic.ldflags); - FreeIfExistent(info->basic.binary); - FreeIfExistent(info->basic.cflags); - FreeIfExistent(info->basic.name); - FreeIfExistent(info->basic.src); - FreeIfExistent(info->basic.inc); - FreeIfExistent(info->basic.obj); - FreeIfExistent(info->basic.cc); - FreeIfExistent(info->repo); -} - -static char * -cmd_stdout(char *cmd) -{ - FILE *f; - char *line = NULL; - size_t size; - if (!cmd) - { - return NULL; - } - if (!(f = popen(cmd, "r"))) - { - return NULL; - } - - getline(&line, &size, f); - pclose(f); - return line; -} -static int -exec_code(char *program, char *argv[]) -{ - pid_t forkRet; - if (!program || !argv) - { - return -1; - } - - forkRet = fork(); - if (forkRet == 0) - { - /* Child */ - execvp(program, argv); - exit(0); - } - else - { - /* Parent */ - int status; - if (waitpid(forkRet, &status, 0) == -1) - { - return -1; - } - return status; - } - - /* We're not meant to ever be there, but TCC is stupid. */ - return -1; -} - -static char * -strchrn(char *s, char c) -{ - s = strchr(s, c); - return s ? s+1 : NULL; -} -static char * -strchrl(char *s, char c) -{ - char *s1 = NULL; - while ((s = strchr(s, c))) - { - s1 = s; - s++; - } - return s1; -} -static void -mkdir_rec(char *dir) -{ - char tmp[PATH_MAX]; - char *start; - if (!dir || strlen(dir) >= PATH_MAX - 1) - { - return; - } - - memset(tmp, '\0', sizeof(tmp)); - for (start = dir; start && *start; start = strchrn(start, '/')) - { - char subtmp[PATH_MAX]; - char *next = strchr(start, '/'); - if (!next) - { - next = start + strlen(start); - } - memcpy(subtmp, start, next - start); - subtmp[next - start] = '\0'; - - { - memcpy(tmp, dir, start - dir); - tmp[strlen(tmp) - 1] = '\0'; - mkdir(tmp, 0770); - } - } -} -static str_array_t * -split(char *dir) -{ - str_array_t *ret; - char *start; - if (!dir || strlen(dir) >= PATH_MAX - 1) - { - return NULL; - } - - ret = str_array_create(); - for (start = dir; start; start = strchrn(start, ' ')) - { - char subtmp[PATH_MAX]; - char *next = strchr(start, ' '); - if (!next) - { - next = start + strlen(start); - } - memcpy(subtmp, start, next - start); - subtmp[next - start] = '\0'; - - str_array_add(ret, subtmp); - } - return ret; -} -static time_t -mod_date(char *file) -{ - struct stat s; - if (stat(file, &s)) - { - return (time_t) 0; - } - return s.st_mtime; -} -static bool -build_file(char *cSource, buildinfo_t info, bool isTool) -{ - str_array_t *args, *cflags; - char *oFileName, *objPath, *oFile; - int ret, i = 0; - int srclen; - - char *code, *name, *vers, *repo; - if (!cSource) - { - return false; - } - - if (!isTool) - { - srclen = strncmp(cSource, "src", 3) ? - strlen(info.basic.obj) + 1 : - strlen(info.basic.src) + 1 ; - objPath = string_cat(info.basic.obj, "/"); - oFile = string_rep_ext(cSource + srclen, ".c", ".o"); - oFileName = string_cat(objPath, oFile); - } - else - { - srclen = 6; - objPath = string_dup("tools/out/"); - oFile = string_rep_ext(cSource + srclen, ".c", ""); - oFileName = string_cat(objPath, oFile); - } - mkdir_rec(oFileName); - - if (!isTool && (mod_date(cSource) < mod_date(oFileName))) - { - free(objPath); - free(oFileName); - free(oFile); - return true; - } - - args = str_array_create(); - if (!isTool) - { - printf("\tCC %s...\n", cSource); - } - - str_array_add(args, Compiler(info)); - if (isTool) - { - str_array_add(args, "-lCytoplasm"); - str_array_add(args, "-I."); - } - else - { - str_array_add(args, "-c"); - } - str_array_add(args, cSource); - - cflags = split(info.basic.cflags); - for (i = 0; i < str_array_len(cflags); i++) - { - str_array_add(args, str_array_get(cflags, i)); - } - str_array_free(cflags); - - str_array_add(args, "-o"); - str_array_add(args, oFileName); - - str_array_add(args, "-I"); - str_array_add(args, info.basic.inc); - str_array_add(args, "-I"); - str_array_add(args, info.basic.src); - - { - char *pre = string_cat("\"", info.basic.version); - char *pos = string_cat(pre, "\""); - vers = string_cat("-DVERSION=", pos); - str_array_add(args, vers); - - free(pos); - free(pre); - } - { - char *pre = string_cat("\"", info.basic.name); - char *pos = string_cat(pre, "\""); - name = string_cat("-DNAME=", pos); - str_array_add(args, name); - - free(pos); - free(pre); - } - { - char *pre = string_cat("\"", info.basic.codename); - char *pos = string_cat(pre, "\""); - code = string_cat("-DCODE=", pos); - str_array_add(args, code); - - free(pos); - free(pre); - } - { - char *pre = string_cat("\"", info.repo); - char *pos = string_cat(pre, "\""); - repo = string_cat("-DREPOSITORY=", pos); - str_array_add(args, repo); - - free(pos); - free(pre); - } - - str_array_add(args, NULL); - - ret = exec_code(Compiler(info), args->values); - - str_array_free(args); - free(objPath); - free(oFileName); - free(oFile); - free(vers); - free(name); - free(code); - free(repo); - return ret == EXIT_SUCCESS; -} -static bool -finalise_file(str_array_t *arr, buildinfo_t info) -{ - str_array_t *flags, *ldflags; - size_t i; - bool ret = true; - if (!arr) - { - return false; - } - - flags = str_array_create(); - str_array_add(flags, Compiler(info)); - - ldflags = split(info.basic.cflags); - for (i = 0; i < str_array_len(ldflags); i++) - { - str_array_add(flags, str_array_get(ldflags, i)); - } - str_array_free(ldflags); - - str_array_add(flags, "-lCytoplasm"); - - for (i = 0; i < str_array_len(arr); i++) - { - char *file = str_array_get(arr, i); - if (file) - { - size_t srclen = strncmp(file, "src", 3) ? - strlen(info.basic.obj) + 1 : - strlen(info.basic.src) + 1 ; - char *objPath = string_cat(info.basic.obj, "/"); - char *oFile = string_rep_ext(file + srclen, ".c", ".o"); - char *oFileName = string_cat(objPath, oFile); - - str_array_add(flags, oFileName); - - free(oFileName); - free(oFile); - free(objPath); - } - } - - str_array_add(flags, "-o"); - str_array_add(flags, info.basic.binary); - str_array_add(flags, NULL); - ret = exec_code(Compiler(info), flags->values); - - str_array_free(flags); - return ret; -} - -static str_array_t * -collect_sources(char *dir, bool head, char *ext) -{ - DIR *handle; - str_array_t *ret; - struct dirent *ent; - if (!dir) - { - return NULL; - } - - ret = str_array_create(); - handle = opendir(dir); - if (!handle) - { - printf("error: cannot open directory '%s'\n", dir); - return ret; - } - while ((ent = readdir(handle))) - { - char *name = ent->d_name; - if (*name == '.') continue; - - if (strlen(name) > strlen(ext) && - !strcmp(name + strlen(name) - strlen(ext), ext)) - { - char *d1 = string_cat(dir, "/"); - char *na = string_cat(d1, name); - str_array_add(ret, na); - free(d1); - free(na); - continue; - } - if (!strchr(name, '.') && - strcmp(name, "out") && - strcmp(name, "Makefile")) - { - str_array_t *sub; - char *d1 = string_cat(dir, "/"); - char *d2 = string_cat(d1, name); - size_t i; - - sub = collect_sources(d2, false, ext); - for (i = 0; i < str_array_len(sub); i++) - { - char *file = str_array_get(sub, i); - str_array_add(ret, file); - } - str_array_free(sub); - free(d2); - free(d1); - } - } - closedir(handle); - return ret; -} - -static char * -process_png(char *png, buildinfo_t info) -{ - size_t i = 0; - char *symbol; - char *cFile, *oFile; - char *pcFile, *poFile; - char *pcFile1, *poFile1; - char *filename, *symbol1; - char *arguments[8] = { 0 }; - if (!png) - { - return NULL; - } - - if (!(filename = strchrl(png, '/'))) - { - return NULL; - } - filename++; - - pcFile1= string_cat(info.basic.obj, "/"); - pcFile = string_cat(pcFile1, filename); - cFile = string_rep_ext(pcFile, ".png", ".c"); - free(pcFile); - free(pcFile1); - - poFile1= string_cat(info.basic.obj, "/"); - poFile = string_cat(poFile1, filename); - oFile = string_rep_ext(poFile, ".png", ".o"); - free(poFile); - free(poFile1); - - symbol1 = string_rep_ext(filename, ".png", ""); - symbol = string_cat("media_", symbol1); - free(symbol1); - - mkdir_rec(oFile); - mkdir_rec(cFile); - - /* Build the image into Base64 */ - arguments[i++] = "tools/out/b64"; - arguments[i++] = png; - arguments[i++] = symbol; - arguments[i++] = cFile; - arguments[i++] = NULL; - - if (exec_code(arguments[0], arguments)) - { - free(symbol); - free(cFile); - free(oFile); - return NULL; - } - - /* Compile it out */ - i = 0; - arguments[i++] = Compiler(info); - arguments[i++] = "-c"; - arguments[i++] = cFile; - arguments[i++] = "-o"; - arguments[i++] = oFile; - arguments[i++] = NULL; - - if (exec_code(arguments[0], arguments)) - { - free(symbol); - free(cFile); - free(oFile); - return NULL; - } - - free(symbol); - free(oFile); - return cFile; -} -/* Builds the entirety of Parsee. */ -static int -main_build(int argc, char *argv[]) -{ - buildinfo_t info = { 0 }; - FILE *buildinfo = NULL; - char *line = NULL; - size_t size, i; - ssize_t nread; - str_array_t *sources, *images; - - /* Step 1: Get all basic information from build.conf */ - if (!(buildinfo = fopen(DEFAULT_BUILD_PATH, "r"))) - { - printf("error: cannot open '%s'\n", DEFAULT_BUILD_PATH); - goto fail; - } - while ((nread = getline(&line, &size, buildinfo)) != -1) - { - char *eq = strchr(line, '='); - char *end = strchr(line, '\n'); - - char *key, *val; - if (!eq) - { - free(line); - printf( - "error: line in '%s' does not contain '='\n", - DEFAULT_BUILD_PATH - ); - goto fail; - } - - /* Set delimiters */ - *eq = '\0'; - if (end) *end = '\0'; - - key = line; - val = eq + 1; - - /* Now, we have KV mappings. */ -#define If(k, v, d) do \ - { \ - if (!strcmp(key, k) && !info.v) \ - { \ - info.v = string_dup(val); \ - printf("%s: %s\n", d, val); \ - } \ - } while (0) - - If("CODE", basic.codename, "Codename"); - If("NAME", basic.name, "Name"); - If("VERSION", basic.version, "Version"); - If("BINARY", basic.binary, "Binary name"); - - If("CFLAGS", basic.cflags, "C compiler arguments"); - If("LDFLAGS", basic.ldflags, "Linker arguments"); - - If("INCLUDES", basic.inc, "Include path"); - If("SOURCE", basic.src, "Source path"); - If("OBJECT", basic.obj, "Object path"); - If("CC", basic.cc, "Compiler"); - } - if (line) - { - free(line); - line = NULL; - } - - fclose(buildinfo); - buildinfo = NULL; - - /* Step 2: Get all information from commands. */ - if (!(info.repo = cmd_stdout("git remote get-url origin"))) - { - printf("error: cannot find origins url\n"); - goto fail; - } - info.repo = trim_nl(info.repo); - - if (argc >= 2 && !strcmp(argv[1], "clean")) - { - char *args[8]; - size_t i; - unlink(info.basic.binary); - - args[i++] = "rm"; - args[i++] = "-r"; - args[i++] = info.basic.obj; - args[i++] = NULL; - exec_code(args[0], args); - goto end; - } - - - /* Step 3: Build all utilities. */ - sources = collect_sources("tools", true, ".c"); - for (i = 0; i < str_array_len(sources); i++) - { - char *file = str_array_get(sources, i); - printf("\tTOOL %s...\n", file); - if (!build_file(file, info, true)) - { - str_array_free(sources); - goto fail; - } - } - str_array_free(sources); - - /* Step 4: Build all media files. */ - sources = collect_sources(info.basic.src, true, ".c"); - images = collect_sources("etc/media", true, ".png"); - for (i = 0; i < str_array_len(images); i++) - { - char *file = str_array_get(images, i); - char *out; - - out = process_png(file, info); - if (!out) - { - str_array_free(images); - str_array_free(sources); - goto fail; - } - printf("\tPNG %s\n", file); - str_array_add(sources, out); - free(out); - } - str_array_free(images); - - /* Step 5: Build all of Parsee itself */ - for (i = 0; i < str_array_len(sources); i++) - { - char *file = str_array_get(sources, i); - if (!build_file(file, info, false)) - { - str_array_free(sources); - goto fail; - } - } - printf("\tLINK\n"); - if (!finalise_file(sources, info)) - { - str_array_free(sources); - goto fail; - } - str_array_free(sources); - /* TODO: Step 6: Build every Ayadoc */ -end: - destroy_buildinfo(&info); - return EXIT_SUCCESS; -fail: - if (buildinfo) - { - fclose(buildinfo); - buildinfo = NULL; - } - destroy_buildinfo(&info); - return EXIT_FAILURE; -} - -int -main(int argc, char *argv[]) -{ - /* TODO: Multiple flags(build/install/ayadoc/...) */ - if ((argc - 1) < 1) - { - /* No arguments, let's just build. */ - return main_build(argc, argv); - } - - if (!strcmp(argv[1], "build") || - !strcmp(argv[1], "clean")) - { - return main_build(argc, argv); - } - - printf("%s: unknown verb: %s\n", argv[0], argv[1]); - return EXIT_FAILURE; -} diff --git a/build.conf b/build.conf index b3ce98b..a2e17c1 100644 --- a/build.conf +++ b/build.conf @@ -1,8 +1,10 @@ -CODE=tomboyish-bridges-adventure +CODE=lunar-rainbow NAME=Parsee -VERSION=0.1.0 +VERSION=0.3.0 BINARY=parsee SOURCE=src INCLUDES=src/include OBJECT=build CC=cc +CFLAGS=-O3 +PREFIX=/usr diff --git a/configure.c b/configure.c new file mode 100644 index 0000000..d3a01d0 --- /dev/null +++ b/configure.c @@ -0,0 +1,727 @@ +/* configure.c - Simple, POSIX, non-Cytoplasm utility to build out + * a Parsee Makefile. + * To run it, just build it with: + * cc configure.c -o configure && configure + * -------------------------------- + * LICENSE: CC0 + * Written-By: LDA [lda@freetards.xyz] [@fourier:ari.lt] */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static char * +string_rep_ext(char *in, char *ext1, char *ext2) +{ + char *end; + size_t inLen; + if (!in || !ext1 || !ext2) + { + return NULL; + } + + inLen = strlen(in); + end = inLen + in; + + while (end >= in) + { + if (!strcmp(end, ext1)) + { + size_t cpyLen = end - in; + size_t extLen = strlen(ext2); + char *ret = malloc(cpyLen + extLen + 1); + + memcpy(ret, in, cpyLen); + memcpy(ret + cpyLen, ext2, extLen); + ret[cpyLen + extLen] = '\0'; + return ret; + } + end--; + } + return NULL; +} +static char * +string_dup(char *in) +{ + char *out; + size_t len; + if (!in) + { + return NULL; + } + len = strlen(in); + out = malloc(len + 1); + memcpy(out, in, len); + out[len] = '\0'; + + return out; +} +static char * +string_cat(char *in1, char *in2) +{ + char *out; + size_t len1, len2; + if (!in1) + { + return string_dup(in2); + } + if (!in2) + { + return string_dup(in1); + } + + len1 = strlen(in1); + len2 = strlen(in2); + out = malloc(len1 + len2 + 1); + memcpy(out, in1, len1); + memcpy(out + len1, in2, len2); + out[len1 + len2] = '\0'; + + return out; +} + +typedef struct str_array { + char **values; + size_t quantity; +} str_array_t; +static str_array_t * +str_array_create(void) +{ + str_array_t *ret = malloc(sizeof(*ret)); + if (!ret) + { + return NULL; + } + + ret->values = NULL; + ret->quantity = 0; + return ret; +} +static void +str_array_free(str_array_t *arr) +{ + size_t i; + if (!arr) + { + return; + } + + for (i = 0; i < arr->quantity; i++) + { + if (arr->values[i]) free(arr->values[i]); + } + if (arr->values) free(arr->values); + free(arr); +} +static void +str_array_set(str_array_t *arr, size_t i, char *str) +{ + if (!arr) + { + return; + } + + if (i >= arr->quantity) + { + size_t size = (i+1) * sizeof(*arr->values); + size_t j; + arr->values = realloc(arr->values, size); + for (j = arr->quantity; j <= i; j++) + { + arr->values[j] = NULL; + } + arr->quantity = i + 1 ; + } + if (arr->values[i]) free(arr->values[i]); + arr->values[i] = string_dup(str); +} +static void +str_array_add(str_array_t *arr, char *str) +{ + if (!arr) + { + return; + } + str_array_set(arr, arr->quantity, str); +} +static char * +str_array_get(str_array_t *arr, size_t i) +{ + if (!arr) + { + return NULL; + } + return i < arr->quantity ? arr->values[i] : NULL; +} +static size_t +str_array_len(str_array_t *arr) +{ + return arr ? arr->quantity : 0; +} +static char * +cmd_stdout(char *cmd) +{ + FILE *f; + char *line = NULL; + size_t size; + int result; + if (!cmd) + { + goto failure; + } + if (!(f = popen(cmd, "r"))) + { + goto failure; + } + + getline(&line, &size, f); + result = pclose(f); + + if (result < 0) + { + perror("pclose(3)"); + goto failure; + } + else if (result) + { + fprintf(stderr, "command exited with status %d: %s\n", result, cmd); + goto failure; + } + return line; + +failure: + free(line); + return NULL; +} + +static char * +strchrn(char *s, char c) +{ + s = strchr(s, c); + return s ? s+1 : NULL; +} +static str_array_t * +split(char *dir) +{ + str_array_t *ret; + char *start; + if (!dir || strlen(dir) >= PATH_MAX - 1) + { + return NULL; + } + + ret = str_array_create(); + for (start = dir; start; start = strchrn(start, '/')) + { + char subtmp[PATH_MAX]; + char *next = strchr(start, '/'); + if (!next) + { + next = start + strlen(start); + } + memcpy(subtmp, start, next - start); + subtmp[next - start] = '\0'; + + str_array_add(ret, subtmp); + } + return ret; +} +static str_array_t * +collect_sources(char *dir, bool head, char *ext) +{ + DIR *handle; + str_array_t *ret; + struct dirent *ent; + if (!dir) + { + return NULL; + } + + ret = str_array_create(); + handle = opendir(dir); + if (!handle) + { + printf("error: cannot open directory '%s'\n", dir); + return ret; + } + while ((ent = readdir(handle))) + { + char *name = ent->d_name; + + if (!strcmp(name, ".") || !strcmp(name, "..")) continue; + + if (strlen(name) > strlen(ext) && + !strcmp(name + strlen(name) - strlen(ext), ext)) + { + char *d1 = string_cat(dir, "/"); + char *na = string_cat(d1, name); + str_array_add(ret, na); + free(d1); + free(na); + continue; + } + { + char *d1 = string_cat(dir, "/"); + char *d2 = string_cat(d1, name); + size_t i; + struct stat sb; + + if (stat(d2, &sb)) + { + fprintf(stderr, "stat(2) %s: %s\n", d2, strerror(errno)); + free(d2); + free(d1); + } + else if (S_ISDIR(sb.st_mode)) + { + str_array_t *sub = collect_sources(d2, false, ext); + for (i = 0; i < str_array_len(sub); i++) + { + char *file = str_array_get(sub, i); + str_array_add(ret, file); + } + str_array_free(sub); + } + + free(d2); + free(d1); + } + } + closedir(handle); + return ret; +} + +/* Builds the entirety of Parsee. */ +static void +write_objects(FILE *makefile, str_array_t *sources) +{ + size_t i; + if (!makefile || !sources) + { + return; + } + for (i = 0; i < str_array_len(sources); i++) + { + char *src = str_array_get(sources, i); + char *ofl = string_rep_ext(src, ".c", ".o"); + char *obj = string_cat("build", ofl + 3); + + fprintf(makefile, " %s", obj); + free(ofl); + free(obj); + } +} +static void +write_images(FILE *makefile, str_array_t *sources) +{ + size_t i; + if (!makefile || !sources) + { + return; + } + for (i = 0; i < str_array_len(sources); i++) + { + char *src = str_array_get(sources, i); + char *ofl = string_rep_ext(src, ".png", ".o"); + char *obj = string_cat("build", ofl + 3 + 1 + 5); + + fprintf(makefile, " %s", obj); + free(ofl); + free(obj); + } +} +static void +write_executable(FILE *makefile, str_array_t *sources) +{ + size_t i; + if (!makefile || !sources) + { + return; + } + for (i = 0; i < str_array_len(sources); i++) + { + char *src = str_array_get(sources, i); + char *ofl = string_rep_ext(src, ".c", ""); + char *obj = string_cat("tools/out", ofl + 5); + + fprintf(makefile, " %s", obj); + free(ofl); + free(obj); + } +} +static void +write_ayas(FILE *makefile, str_array_t *sources) +{ + size_t i; + if (!makefile || !sources) + { + return; + } + for (i = 0; i < str_array_len(sources); i++) + { + char *src = str_array_get(sources, i); + char *ofl = string_rep_ext(src, ".h", ".html"); + char *obj = string_cat("$(AYAYAS)", ofl + 3 + 1 + 7); + + fprintf(makefile, " %s", obj); + free(ofl); + free(obj); + } +} + +static void +analyse_dependencies(FILE *makefile, char *src) +{ + FILE *source = fopen(src, "r"); + char *lineptr = NULL; + char *dn = strdup(src), *dirn; + size_t len = 0; + + dirn = dirname(dn); + + while (getline(&lineptr, &len, source) != -1) + { + char *corrptr = lineptr, *nl, *chevron, *quote, *cutoff; + char *basedir, *srcdir, *incdir, *tmp; + struct stat statinfo; + + /* try to parse the line */ + if ((nl = strchr(corrptr, '\n'))) + { + *nl = '\0'; + } + + while (*corrptr && isblank(*corrptr)) + { + corrptr++; + } + if (strncmp(corrptr, "#include \"", 10) && + strncmp(corrptr, "#include <", 10)) + { + continue; + } + corrptr += 10; + chevron = strchr(corrptr, '>'); + chevron = chevron ? chevron : corrptr + strlen(corrptr); + quote = strchr(corrptr, '"'); + quote = quote ? quote : corrptr + strlen(corrptr); + cutoff = chevron < quote ? chevron : quote; + + *cutoff = '\0'; + + /* if we found something, try to resolve it */ + tmp = string_cat(dirn, "/"); + basedir = string_cat(tmp, corrptr); + free(tmp); + if (!stat(basedir, &statinfo)) + { + fprintf(makefile, " %s", basedir); + free(basedir); + continue; + } + free(basedir); + + srcdir = string_cat("src/", corrptr); + if (!stat(srcdir, &statinfo)) + { + fprintf(makefile, " %s", srcdir); + free(srcdir); + continue; + } + free(srcdir); + + incdir = string_cat("src/include/", corrptr); + if (!stat(incdir, &statinfo)) + { + fprintf(makefile, " %s", incdir); + free(incdir); + continue; + } + free(incdir); + } + free(lineptr); + free(dn); + fclose(source); +} +static int +main_build(int argc, char *argv[]) +{ + FILE *makefile; + char *repo = cmd_stdout("git remote get-url origin"); + size_t i; + bool with_static = false, with_lmdb = false; + int opt; + str_array_t *sources, *images, *utils, *aya; + + if (repo) + { + char *lf = strchr(repo, '\n'); + if (lf) + { + *lf = '\0'; + } + } + else + { + repo = strdup("N/A"); + } + + while ((opt = getopt(argc, argv, "sl")) != -1) + { + switch (opt) + { + case 's': + with_static = true; + break; + case 'l': + with_lmdb = with_static; + with_static = true; + break; + } + } + + makefile = fopen("Makefile", "w"); + if (!makefile) + { + fprintf(stderr, "Couldn't create Makefile.\n"); + fprintf(stderr, "This isn't good, actually.\n"); + + return EXIT_FAILURE; + } + fprintf(makefile, "# Autogenerated POSIX Makefile from Parsee\n"); + fprintf(makefile, "# Ideally do not touch, unless you have a very "); + fprintf(makefile, "good reason to do it. \n\n"); + fprintf(makefile, ".POSIX: \n"); + fprintf(makefile, "include build.conf\n\n"); + fprintf(makefile, "AYAYAS=ayaya\n"); + fprintf(makefile, "CYTO_LIB=/usr/local/lib\n"); + fprintf(makefile, "CYTO_INC=/usr/local/include\n"); + fprintf(makefile, "ETC=etc\n\n"); + + fprintf(makefile, "all: utils binary ayadoc\n\n"); + + /* Create all objects */ + sources = collect_sources("src", true, ".c"); + images = collect_sources("etc/media", true, ".png"); + fprintf(makefile, "binary:"); + write_objects(makefile, sources); + write_images(makefile, images); + fprintf(makefile, "\n\t"); + { + fprintf(makefile, "$(CC) -o $(BINARY)"); + if (with_static) + { + fprintf(makefile, " -static"); + } + fprintf(makefile, " -L $(CYTO_LIB)"); + write_objects(makefile, sources); + write_images(makefile, images); + if (with_static) + { + fprintf(makefile, " -lm -lpthread -lmbedtls -lmbedx509 -lmbedcrypto -lCytoplasm -lmbedtls -lmbedx509 -lmbedcrypto"); + if (with_lmdb) fprintf(makefile, " -llmdb"); + } + fprintf(makefile, " -lCytoplasm $(LDFLAGS)\n"); + } + + /* Write rules for every source */ + for (i = 0; i < str_array_len(sources); i++) + { + char *src = str_array_get(sources, i); + char *ofl = string_rep_ext(src, ".c", ".o"); + char *obj = string_cat("build", ofl + 3); + + fprintf(makefile, "%s: %s", obj, src); + analyse_dependencies(makefile, src); + fprintf(makefile, "\n"); + { + str_array_t *s = split(obj); + ssize_t j; + + fprintf(makefile, "\t@mkdir -p "); + for (j = 0; j < str_array_len(s) - 1; j++) + { + fprintf(makefile, "%s/", str_array_get(s, j)); + } + fprintf(makefile, "\n"); + str_array_free(s); + + fprintf(makefile, "\t$(CC) -c -Isrc -Isrc/include -I$(CYTO_INC) "); + fprintf(makefile, "-DVERSION=\"\\\"$(VERSION)\\\"\" "); + fprintf(makefile, "-DNAME=\"\\\"$(NAME)\\\"\" "); + fprintf(makefile, "-DCODE=\"\\\"$(CODE)\\\"\" "); + fprintf(makefile, "-DREPOSITORY=\"\\\"%s\\\"\" ", repo); + fprintf(makefile, "$(CFLAGS) %s -o %s\n", src, obj); + } + free(ofl); + free(obj); + } + for (i = 0; i < str_array_len(images); i++) + { + char *src = str_array_get(images, i); + char *ofl = string_rep_ext(src, ".png", ".o"); + char *obj = string_cat("build", ofl + 3 + 1 + 5); + char *cfl = string_cat("build", src + 3 + 1 + 5); + + fprintf(makefile, "%s: %s\n", obj, src); + + { + str_array_t *s = split(obj); + char *sym; + ssize_t j; + + fprintf(makefile, "\t@mkdir -p "); + for (j = 0; j < str_array_len(s) - 1; j++) + { + fprintf(makefile, "%s/", str_array_get(s, j)); + } + fprintf(makefile, "\n"); + sym = j != -1 ? str_array_get(s, j): NULL; + sym = string_rep_ext(sym, ".o", ""); + str_array_free(s); + + fprintf(makefile, "\ttools/out/b64 %s 'media_%s' '%s.c'\n", src, sym, obj); + fprintf(makefile, "\t$(CC) -c %s.c -o %s\n", obj, obj); + free(sym); + } + free(obj); + free(ofl); + free(cfl); + } + + /* Build utilities */ + utils = collect_sources("tools", true, ".c"); + fprintf(makefile, "utils:"); + write_executable(makefile, utils); + fprintf(makefile, "\n"); + for (i = 0; i < str_array_len(utils); i++) + { + char *src = str_array_get(utils, i); + char *ofl = string_rep_ext(src, ".c", ""); + char *obj = string_cat("tools/out", ofl + 5); + + fprintf(makefile, "%s: %s\n", obj, src); + { + fprintf(makefile, "\t@mkdir -p tools/out\n"); + fprintf(makefile, "\t$(CC) -o %s", obj); + if (with_static) + { + fprintf(makefile, " -static"); + } + fprintf(makefile, " %s", src); + fprintf(makefile, " -I $(CYTO_INC)"); + fprintf(makefile, " -L $(CYTO_LIB)"); + if (with_static) + { + fprintf(makefile, " -lm -lpthread -lmbedtls -lmbedx509 -lmbedcrypto"); + fprintf(makefile, " -lCytoplasm -lmbedtls -lmbedx509 -lmbedcrypto"); + if (with_lmdb) fprintf(makefile, " -llmdb"); + } + fprintf(makefile, " -lCytoplasm $(CFLAGS)\n"); + } + + free(ofl); + free(obj); + } + + /* Build Aya */ + aya = collect_sources("src/include", true, ".h"); + fprintf(makefile, "ayadoc:"); + write_ayas(makefile, aya); + fprintf(makefile, "\n"); + for (i = 0; i < str_array_len(aya); i++) + { + char *src = str_array_get(aya, i); + char *ofl = string_rep_ext(src, ".h", ".html"); + char *obj = string_cat("$(AYAYAS)", ofl + 3 + 1 + 7); + + fprintf(makefile, "%s: %s\n", obj, src); + { + str_array_t *s = split(obj); + ssize_t j; + + fprintf(makefile, "\t@mkdir -p "); + for (j = 0; j < str_array_len(s) - 1; j++) + { + fprintf(makefile, "%s/", str_array_get(s, j)); + } + fprintf(makefile, "\n"); + str_array_free(s); + + fprintf(makefile, + "\ttools/out/aya " + "-C etc/ayadoc/style.css " + "-p $(NAME) " + "-i %s -o %s\n", src, obj + ); + } + + free(ofl); + free(obj); + } + + fprintf(makefile, "install-parsee: binary\n"); + { + fprintf(makefile, "\tmkdir -m 755 -p $(PREFIX)/bin\n"); + fprintf(makefile, "\tcp $(BINARY) $(PREFIX)/bin\n"); + fprintf(makefile, "\tchmod 755 $(PREFIX)/bin/$(BINARY)\n"); + } + + fprintf(makefile, "install-tools: utils\n"); + fprintf(makefile, "\tmkdir -m 755 -p $(PREFIX)/bin\n"); + for (i = 0; i < str_array_len(utils); i++) + { + char *tool = str_array_get(utils, i); + char *ofl = string_rep_ext(tool, ".c", ""); + char *obj = string_cat("tools/out", ofl + 5); + char *bna = basename(obj); + + fprintf(makefile, "\tcp %s $(PREFIX)/bin/parsee-%s\n", obj, bna); + fprintf(makefile, "\tchmod 755 $(PREFIX)/bin/parsee-%s\n", bna); + + free(ofl); + free(obj); + } + fprintf(makefile, "install-aya: ayadoc\n"); + fprintf(makefile, "\tmkdir -m 755 -p $(PREFIX)/aya/$(BINARY)\n"); + fprintf(makefile, "\tcp -R $(AYAYAS)/* $(PREFIX)/aya/$(BINARY)\n"); + fprintf(makefile, "install-man:\n"); + fprintf(makefile, "\tmkdir -m 755 -p $(PREFIX)/share/man\n"); + fprintf(makefile, "\tcp -R etc/man/* $(PREFIX)/share/man\n"); + + fprintf(makefile, + "install: " + "install-parsee " + "install-tools " + "install-aya " + "install-man\n" + ); + + fprintf(makefile, "clean:\n\trm -rf ayaya build $(BINARY) tools/out\n"); + + str_array_free(sources); + str_array_free(images); + str_array_free(utils); + str_array_free(aya); + fflush(makefile); + fclose(makefile); + free(repo); + return EXIT_SUCCESS; +} + +int +main(int argc, char *argv[]) +{ + return main_build(argc, argv); +} diff --git a/etc/man/man1/parsee-adminify.1 b/etc/man/man1/parsee-adminify.1 index fd4a582..9015d20 100644 --- a/etc/man/man1/parsee-adminify.1 +++ b/etc/man/man1/parsee-adminify.1 @@ -1,6 +1,6 @@ ." The last field is the codename, by the way. ." ALL MANPAGES FOR PARSEE ARE UNDER PUBLIC DOMAIN -.TH parsee-adminify 1 "Parsee Utility" "tomboyish-bridges-adventure" +.TH parsee-adminify 1 "Parsee Utility" "star-of-hope" .SH NAME parsee-adminify - bootstrap an admin to a new Parsee server diff --git a/etc/man/man1/parsee-aya.1 b/etc/man/man1/parsee-aya.1 index bb5caac..b51afa0 100644 --- a/etc/man/man1/parsee-aya.1 +++ b/etc/man/man1/parsee-aya.1 @@ -1,6 +1,6 @@ ." The last field is the codename, by the way. ." ALL MANPAGES FOR PARSEE ARE UNDER PUBLIC DOMAIN -.TH parsee-aya 1 "Parsee Utility" "tomboyish-bridges-adventure" +.TH parsee-aya 1 "Parsee Utility" "star-of-hope" .SH NAME parsee-aya - generate some nice Ayaya! documentation diff --git a/etc/man/man1/parsee-config.1 b/etc/man/man1/parsee-config.1 index cb6e999..5dc258b 100644 --- a/etc/man/man1/parsee-config.1 +++ b/etc/man/man1/parsee-config.1 @@ -1,6 +1,6 @@ ." The last field is the codename, by the way. ." ALL MANPAGES FOR PARSEE ARE UNDER PUBLIC DOMAIN -.TH parsee-config 1 "Parsee Utility" "tomboyish-bridges-adventure" +.TH parsee-config 1 "Parsee Utility" "lunar-rainbow" .SH NAME parsee-config - generate a basic configuration file @@ -11,8 +11,10 @@ parsee-config .B [-s SHARED_SECRET] .B [-m MEDIA_URL] .B [-J JABBER_HOST] +.B [-j JABBER_ADDR] .B [-p JABBER_PORT] .B [-d DATABASE] +.B [-M MAX_STANZA] .B [-S DATABASE size] .SH DESCRIPTION @@ -33,6 +35,7 @@ $ parsee-config \\ -H 'blow.hole' \\ -s 'The Dark Shared Secret' \\ -J 'xmpp.blow.hole' \\ + -j 'localhost' \\ -S 128 .fi .if n \{\ @@ -54,6 +57,10 @@ For example, if you except Parsee users to be on .I SHARED_SECRET is a shared secret known by Parsee and the XMPP component to authenticate. .TP +.BR -M MAX_STANZA +.I MAX_STANZA +is the maximum stanza size accepted by the XMPP host. If it is less than 10000 bytes, then it shall be set to that limit(the standardised value). +.TP .BR -m MEDIA_URL .I MEDIA_URL is an optional field used by Parsee as an address that points to Matrix @@ -63,6 +70,12 @@ media. It must be publicly accessible (behind a reverse proxy to HTTP:7642) .I JABBER_HOST is used as the component host for Parsee. .TP +.BR -j JABBER_ADDR +.I JABBER_ADDR +can optionally be used to change the hostname Parsee will try to contact +for XMPP. Users should ideally use localhost (or a hostname pointing to +the server itself), as XMPP component streams are not encrypted. +.TP .BR -p JABBER_PORT .I JABBER_PORT is used as the component post for Parsee. Parsee uses it alongside diff --git a/etc/man/man1/parsee.1 b/etc/man/man1/parsee.1 index 093af7b..d7eeedf 100644 --- a/etc/man/man1/parsee.1 +++ b/etc/man/man1/parsee.1 @@ -1,6 +1,6 @@ ." The last field is the codename, by the way. ." ALL MANPAGES FOR PARSEE ARE UNDER PUBLIC DOMAIN -.TH parsee 1 "Parsee Utility" "tomboyish-bridges-adventure" +.TH parsee 1 "Parsee Utility" "star-of-hope" .SH NAME parsee - the jealous XMPP-Matrix bridge diff --git a/etc/man/man7/.parsee-bridge-guidebook.7.swp b/etc/man/man7/.parsee-bridge-guidebook.7.swp new file mode 100644 index 0000000..50976ea Binary files /dev/null and b/etc/man/man7/.parsee-bridge-guidebook.7.swp differ diff --git a/etc/man/man7/.parsee-cmd-syntax.7.swp b/etc/man/man7/.parsee-cmd-syntax.7.swp new file mode 100644 index 0000000..64990b7 Binary files /dev/null and b/etc/man/man7/.parsee-cmd-syntax.7.swp differ diff --git a/etc/man/man7/parsee-bridge-guidebook.7 b/etc/man/man7/parsee-bridge-guidebook.7 new file mode 100644 index 0000000..b5932f2 --- /dev/null +++ b/etc/man/man7/parsee-bridge-guidebook.7 @@ -0,0 +1,70 @@ +." The last field is the codename, by the way. +." ALL MANPAGES FOR PARSEE ARE UNDER PUBLIC DOMAIN +.TH parsee-bridge-guidebook 7 "Parsee Utility" "star-of-hope" + +.SH NAME +parsee-bridge-guidebook - A short guidebook on running a Parsee bridge + +.SH INTRODUCTION +.P +This manpage is intended to be a guidebook for Parsee administrators. It +is meant to show how to create an instance with an XMPP-Matrix server +(though it cannot be specific, due to their ecosystem diversity), how to +plumb rooms, and moderate them through. +.P +It also assumes Parsee is properly built and installed, in which case you +are seeing this from +.I man +itself. + +.SH CONVENTIONS +This page shall assume a few things that +.B must +be changed to fit your configuration. Please read those carefully, or it +will come and bite at you! + +.P +First off, it assumes that you have a domain at +.I blow.hole +and that any other domains related to Parsee are it's subdomains, as +you'll see. + +.P +We also assume you're planning on making the XMPP component available at +.I j.blow.hole , +and that Parsee can reach it by using +.I localhost:1234 . +It is highly recommended that Parsee can reach the component locally, as +the stream cannot be encrypted! + +.P +The Parsee HTTP server (which is used for media and the appservice) shall +be reached through +.I https://p.blow.hole/ +via a reverse proxy. This manual shall only show you what port to make +available, as there are many reverse proxy options available. + +.P +Finally, the Matrix server will be publicly known as +.I m.blow.hole +. + +That is, if +.B bob +is in it, then they shall be known as +.I @bob:m.blow.hole . + +.SH SETTING UP +Setting up Parsee mainly involves creating a valid configuration file +and the database. Most of this however is dealt with by +.I parsee-config(1) +TODO + +.P + +.SH LICENSE +This document is under public domain, or CC0 if not allowed by local law. + +.SH SEE ALSO +.B parsee(1), parsee-cmd-syntax(7) + diff --git a/etc/man/man7/parsee-cmd-syntax.7 b/etc/man/man7/parsee-cmd-syntax.7 new file mode 100644 index 0000000..ba11e67 --- /dev/null +++ b/etc/man/man7/parsee-cmd-syntax.7 @@ -0,0 +1,50 @@ +." The last field is the codename, by the way. +." ALL MANPAGES FOR PARSEE ARE UNDER PUBLIC DOMAIN +.TH parsee-cmd-syntax 7 "Parsee Utility" "star-of-hope" + +.SH NAME +parsee-cmd-syntax - Basic syntax information with Parsee Matrix commands + +.SH DESCRIPTION +Parsee uses a specific syntax for commands, which is generally different +from regular bots, but closer to +.B dd(1) 's +syntax. +.PP +A command is formatted as so. +.sp +.if n \{\ +.RS 4 +.\} +.nf +.B ![NAME] arg1=val1 arg2='val2' arg3="val\(rs\(dq3" +.fi +.if n \{\ +.RE +.\} +.sp + +.PP +The +.B arg1=val1 +syntax is to be used for simple values, with values containing no spaces. + +.PP +The +.B NAME +attribute defines the command to be called, and the +.B arg1='val1' , +and +.B arg2="val2" +syntax is to be used for simple values, with values containing spaces. If +the value needs to contain quotes, they may be escaped with +.B \(rs' +and +.B \(rs" +respectively. + +.SH LICENSE +This document is under public domain, or CC0 if not allowed by local law. + +.SH SEE ALSO +.B parsee(1) diff --git a/etc/media/unknown.png b/etc/media/unknown.png new file mode 100644 index 0000000..00ccf91 Binary files /dev/null and b/etc/media/unknown.png differ diff --git a/guix.scm b/guix.scm new file mode 120000 index 0000000..021acc5 --- /dev/null +++ b/guix.scm @@ -0,0 +1 @@ +.guix/modules/parsee.scm \ No newline at end of file diff --git a/src/AS/Indicators.c b/src/AS/Indicators.c index afe860a..9797649 100644 --- a/src/AS/Indicators.c +++ b/src/AS/Indicators.c @@ -30,8 +30,9 @@ ASType(const ParseeConfig *c, char *user, char *room, bool status) json = HashMapCreate(); HashMapSet(json, "typing", JsonValueBoolean(status)); - /* If someone types for 10 minutes straight, they got something weird man. */ - HashMapSet(json, "timeout", JsonValueBoolean(10 MINUTES)); + /* If someone types for 5 minutes straight, they got something + * weird man. */ + HashMapSet(json, "timeout", JsonValueInteger(5 MINUTES)); ctx = ParseeCreateRequest(c, HTTP_PUT, path); Free(path); ASAuthenticateRequest(c, ctx); diff --git a/src/AS/Media.c b/src/AS/Media.c index 4e642e9..5fbc659 100644 --- a/src/AS/Media.c +++ b/src/AS/Media.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -14,7 +15,7 @@ char * ASUpload(const ParseeConfig *c, Stream *from, unsigned int size, char *mime) { char *size_str, *path, *ret, *user; - int i; + unsigned int i; HttpClientContext *ctx; HashMap *reply; if (!c || !from) @@ -127,3 +128,93 @@ ASReupload(const ParseeConfig *c, char *from, char **mime) return ret; } +bool +ASGetMIMESHA(const ParseeConfig *c, char *mxc, char **mime, char **sha) +{ + HttpClientContext *cctx; + Stream *stream; + Stream *fake; + Uri *uri; + char *path, *buf = NULL; + unsigned char *sha1; + size_t len; + if (!c || !mxc || !mime || !sha) + { + return false; + } + *mime = NULL; + *sha = NULL; + + if (!(uri = UriParse(mxc)) || !StrEquals(uri->proto, "mxc")) + { + return false; + } + + path = StrConcat(3, "/_matrix/client/v1/media/download/", uri->host, uri->path); + cctx = ParseeCreateRequest(c, HTTP_GET, path); + ASAuthenticateRequest(c, cctx); + HttpRequestSendHeaders(cctx); + HttpRequestSend(cctx); + + *mime = StrDuplicate( + HashMapGet(HttpResponseHeaders(cctx), "content-type") + ); + stream = HttpClientStream(cctx); + fake = StreamFile(open_memstream(&buf, &len)); + StreamCopy(stream, fake); + StreamClose(fake); + + sha1 = Sha1Raw((unsigned char *) buf, len); + free(buf); + *sha = ShaToHex(sha1, HASH_SHA1); + Free(sha1); + + HttpClientContextFree(cctx); + UriFree(uri); + Free(path); + return true; +} +bool +ASGrab(const ParseeConfig *c, char *mxc, char **mime, char **out, size_t *len) +{ + HttpClientContext *cctx; + Stream *stream; + Stream *fake; + Uri *uri; + char *path, *buf = NULL; + if (!c || !mxc || !mime || !out || !len) + { + return false; + } + *mime = NULL; + *out = NULL; + *len = 0; + + if (!(uri = UriParse(mxc)) || !StrEquals(uri->proto, "mxc")) + { + return false; + } + + path = StrConcat(3, "/_matrix/client/v1/media/download/", uri->host, uri->path); + cctx = ParseeCreateRequest(c, HTTP_GET, path); + ASAuthenticateRequest(c, cctx); + HttpRequestSendHeaders(cctx); + HttpRequestSend(cctx); + + *mime = StrDuplicate( + HashMapGet(HttpResponseHeaders(cctx), "content-type") + ); + stream = HttpClientStream(cctx); + fake = StreamFile(open_memstream(&buf, len)); + StreamCopy(stream, fake); + StreamClose(fake); + + *out = Malloc(*len); + memcpy(*out, buf, *len); + free(buf); + + HttpClientContextFree(cctx); + UriFree(uri); + Free(path); + return true; +} diff --git a/src/AS/Ping.c b/src/AS/Ping.c index 88ac7cb..3a8ffb7 100644 --- a/src/AS/Ping.c +++ b/src/AS/Ping.c @@ -10,15 +10,16 @@ #include -void +bool ASPing(const ParseeConfig *conf) { HttpClientContext *ctx = NULL; HashMap *json = NULL; char *path; + bool ret; if (!conf) { - return; + return false; } path = StrConcat(3, @@ -33,7 +34,9 @@ ASPing(const ParseeConfig *conf) Free(path); json = HashMapCreate(); ASAuthenticateRequest(conf, ctx); - ParseeSetRequestJSON(ctx, json); + ret = ParseeSetRequestJSON(ctx, json) == HTTP_OK; HttpClientContextFree(ctx); JsonFree(json); + + return ret; } diff --git a/src/AS/Profile.c b/src/AS/Profile.c index 73e9b4b..1f1a0da 100644 --- a/src/AS/Profile.c +++ b/src/AS/Profile.c @@ -136,3 +136,71 @@ ASGetName(const ParseeConfig *c, char *room, char *user) } return ret; } +char * +ASGetAvatar(const ParseeConfig *c, char *room, char *user) +{ + HttpClientContext *ctx; + HashMap *reply; + char *path = NULL, *ret = NULL; + char *u2 = user; + if (!c || !user) + { + return NULL; + } + + if (room) + { + user = HttpUrlEncode(user); + room = HttpUrlEncode(room); + path = StrConcat(4, + "/_matrix/client/v3/rooms/", room, + "/state/m.room.member/", user + ); + ctx = ParseeCreateRequest(c, HTTP_GET, path); + Free(user); + Free(room); + ASAuthenticateRequest(c, ctx); + HttpRequestSendHeaders(ctx); + HttpRequestSend(ctx); + + reply = JsonDecode(HttpClientStream(ctx)); + + ret = StrDuplicate( + JsonValueAsString(HashMapGet(reply, "avatar_url")) + ); + HttpClientContextFree(ctx); + JsonFree(reply); + Free(path); + + user = u2; + + Log(LOG_DEBUG, "ASGetAvatar: trying to grab avatar from room, got %s", ret); + } + + if (!ret) + { + user = HttpUrlEncode(user); + path = StrConcat(3, + "/_matrix/client/v3/profile/", user, "/avatar_url" + ); + ctx = ParseeCreateRequest(c, HTTP_GET, path); + Free(user); + user = u2; + ASAuthenticateRequest(c, ctx); + HttpRequestSendHeaders(ctx); + HttpRequestSend(ctx); + + reply = JsonDecode(HttpClientStream(ctx)); + + ret = StrDuplicate( + JsonValueAsString(HashMapGet(reply, "avatar_url")) + ); + HttpClientContextFree(ctx); + JsonFree(reply); + Free(path); + + Log(LOG_DEBUG, "ASGetAvatar: trying to grab avatar from profile, got %s", ret); + } + + return ret; +} diff --git a/src/AS/Relations.c b/src/AS/Relations.c index 4809413..e9baa5a 100644 --- a/src/AS/Relations.c +++ b/src/AS/Relations.c @@ -25,19 +25,20 @@ ASGetRelations(const ParseeConfig *c, size_t n, char *room, char *event, char *t } user = StrConcat(4, "@", c->sender_localpart, ":", c->server_base); - if (event) + if (type) { - path = StrConcat(6, + path = StrConcat(8, "/_matrix/client/v1/rooms/", room, - "/relations/", event, + "/relations/", event, "/", type, "?user_id=", user ); } else { - path = StrConcat(4, + path = StrConcat(6, "/_matrix/client/v1/rooms/", room, - "/relations?user_id=", user + "/relations/", event, + "?user_id=", user ); } Free(user); diff --git a/src/AS/Room.c b/src/AS/Room.c index 3bc7cbf..e9691e1 100644 --- a/src/AS/Room.c +++ b/src/AS/Room.c @@ -25,11 +25,13 @@ ASInvite(const ParseeConfig *conf, char *id, char *invited) "@", conf->sender_localpart, ":", conf->server_base ); + id = HttpUrlEncode(id); path = StrConcat(5, "/_matrix/client/v3/rooms/", id, "/invite", "?user_id=", bridge ); Free(bridge); + Free(id); ctx = ParseeCreateRequest( conf, @@ -60,11 +62,13 @@ ASBan(const ParseeConfig *conf, char *id, char *banned) "@", conf->sender_localpart, ":", conf->server_base ); + id = HttpUrlEncode(id); path = StrConcat(5, "/_matrix/client/v3/rooms/", id, "/ban", "?user_id=", bridge ); Free(bridge); + Free(id); ctx = ParseeCreateRequest( conf, @@ -73,7 +77,7 @@ ASBan(const ParseeConfig *conf, char *id, char *banned) Free(path); json = HashMapCreate(); HashMapSet(json, "user_id", JsonValueString(banned)); - HashMapSet(json, "reason", JsonValueString("Parsee felt jealous.")); + HashMapSet(json, "reason", JsonValueString(NAME " felt jealous.")); ASAuthenticateRequest(conf, ctx); ParseeSetRequestJSON(ctx, json); @@ -95,11 +99,13 @@ ASKick(const ParseeConfig *conf, char *id, char *banned) "@", conf->sender_localpart, ":", conf->server_base ); + id = HttpUrlEncode(id); path = StrConcat(5, "/_matrix/client/v3/rooms/", id, "/kick", "?user_id=", bridge ); Free(bridge); + Free(id); ctx = ParseeCreateRequest( conf, @@ -108,7 +114,7 @@ ASKick(const ParseeConfig *conf, char *id, char *banned) Free(path); json = HashMapCreate(); HashMapSet(json, "user_id", JsonValueString(banned)); - HashMapSet(json, "reason", JsonValueString("Parsee felt jealous.")); + HashMapSet(json, "reason", JsonValueString(NAME " felt jealous.")); ASAuthenticateRequest(conf, ctx); ParseeSetRequestJSON(ctx, json); @@ -120,7 +126,8 @@ ASJoin(const ParseeConfig *conf, char *id, char *masquerade) { HttpClientContext *ctx = NULL; HashMap *json = NULL; - char *path, *ret; + char *path, *ret, *serv; + int status; if (!conf || !id) { return NULL; @@ -139,6 +146,11 @@ ASJoin(const ParseeConfig *conf, char *id, char *masquerade) { masquerade = HttpUrlEncode(masquerade); } + serv = strchr(id, ':'); + if (serv) + { + serv = serv + 1; + } id = HttpUrlEncode(id); path = StrConcat(5, "/_matrix/client/v3/join/", id, "?", @@ -152,7 +164,7 @@ ASJoin(const ParseeConfig *conf, char *id, char *masquerade) Free(path); json = HashMapCreate(); ASAuthenticateRequest(conf, ctx); - ParseeSetRequestJSON(ctx, json); + status = ParseeSetRequestJSON(ctx, json); JsonFree(json); json = JsonDecode(HttpClientStream(ctx)); @@ -163,6 +175,8 @@ ASJoin(const ParseeConfig *conf, char *id, char *masquerade) Free(masquerade); Free(id); + (void) serv; // TODO + return ret; } void diff --git a/src/AS/Send.c b/src/AS/Send.c index 4032423..6811548 100644 --- a/src/AS/Send.c +++ b/src/AS/Send.c @@ -1,21 +1,43 @@ #include #include +#include #include #include #include +#include #include #include +#include #include +static char * +TSToStr(uint64_t ts) +{ + size_t len; + char *str; + + len = snprintf(NULL, 0, "%"PRIu64, ts); + str = Malloc(len+1); + snprintf(str, len+1, "%"PRIu64, ts); + + return str; +} + char * -ASSend(const ParseeConfig *conf, char *id, char *user, char *type, HashMap *c) +ASSend(const ParseeConfig *conf, char *id, char *user, char *type, HashMap *c, uint64_t ts) { HttpClientContext *ctx = NULL; char *path; char *txn, *ret; + char *ts_str; + HttpStatus status; + if (!ret) + { + Log(LOG_ERR, "%", ret); + } HashMap *reply; if (!conf || !id || !type || !user || !c) { @@ -23,18 +45,27 @@ ASSend(const ParseeConfig *conf, char *id, char *user, char *type, HashMap *c) return NULL; } + if (!ts) + { + ts = UtilTsMillis(); + } + ts_str = TSToStr(ts); + txn = StrRandom(16); - path = StrConcat(9, + id = HttpUrlEncode(id); + path = StrConcat(11, "/_matrix/client/v3/rooms/", id, "/send/", type, "/", txn, "?", - "user_id=", user + "user_id=", user, "&ts=", ts_str ); + Free(id); Free(txn); + Free(ts_str); ctx = ParseeCreateRequest(conf, HTTP_PUT, path); Free(path); ASAuthenticateRequest(conf, ctx); - ParseeSetRequestJSON(ctx, c); + status = ParseeSetRequestJSON(ctx, c); reply = JsonDecode(HttpClientStream(ctx)); ret = StrDuplicate(JsonValueAsString(HashMapGet(reply, "event_id"))); diff --git a/src/Command/Parser.c b/src/Command/Parser.c index 2101254..65f295b 100644 --- a/src/Command/Parser.c +++ b/src/Command/Parser.c @@ -25,7 +25,7 @@ CommandParse(char *cmd) } end_data = strchr(cmd, ' '); - if (!end_data) + if (!end_data || (cmd > end_data)) { ret = Malloc(sizeof(*ret)); ret->command = StrDuplicate(cmd); @@ -53,7 +53,7 @@ CommandParse(char *cmd) switch (state) { case STATE_WHITE: - if (!isblank(c)) + if (!isblank((int) c)) { state = STATE_NAME; namestart = cur; @@ -84,7 +84,7 @@ CommandParse(char *cmd) { char c = *cur; char cb[2] = { c, '\0' }; - if ((type && c == char_type) || (!type && isblank(c))) + if ((type && c == char_type) || (!type && isblank((int) c))) { break; } diff --git a/src/Command/Router.c b/src/Command/Router.c index d188640..0a09d28 100644 --- a/src/Command/Router.c +++ b/src/Command/Router.c @@ -15,35 +15,47 @@ CommandCreateRouter(void) void CommandAddCommand(CommandRouter *rter, char *c, CommandRoute rte) { + CommandRoute *indirect; if (!rter || !c || !rte) { return; } - HashMapSet(rter->routes, c, rte); + + /* Little dirty trick to force C99 into submission, and since + * some architectures may separate data/code. Still don't like it... */ + indirect = Malloc(sizeof(rte)); + *indirect = rte; + HashMapSet(rter->routes, c, (void *) indirect); } void RouteCommand(CommandRouter *rter, Command *cmd, void *d) { - CommandRoute route; + CommandRoute *route; if (!rter || !cmd) { return; } route = HashMapGet(rter->routes, cmd->command); - if (route) + if (route && *route) { - route(cmd, d); + (*route)(cmd, d); } } void CommandFreeRouter(CommandRouter *rter) { + char *key; + CommandRoute *val; if (!rter) { return; } + while (HashMapIterate(rter->routes, &key, (void **) &val)) + { + Free(val); + } HashMapFree(rter->routes); Free(rter); } diff --git a/src/Commands/BanUser.c b/src/Commands/BanUser.c index 6e42356..dd380ed 100644 --- a/src/Commands/BanUser.c +++ b/src/Commands/BanUser.c @@ -22,11 +22,28 @@ CommandHead(CmdBanUser, cmd, argp) BotDestroy(); return; } - ASBan(data->config, room, user); - ReplySprintf("Banning %s from '%s'...", - user, room - ); + ASBan(data->config, room, user); + ReplySprintf("Banning %s from '%s'...", user, room); + + BotDestroy(); +} +CommandHead(CmdNoFlyListDel, cmd, argp) +{ + ParseeCmdArg *args = argp; + ParseeData *data = args->data; + HashMap *event = args->event; + char *user = HashMapGet(cmd->arguments, "user"); + BotInitialise(); + + if (!user) + { + BotDestroy(); + return; + } + + ReplySprintf("Unbanning %s", user); + ParseeGlobalUnban(data, user); BotDestroy(); } diff --git a/src/Commands/Help.c b/src/Commands/Help.c index f60980c..0e25009 100644 --- a/src/Commands/Help.c +++ b/src/Commands/Help.c @@ -25,4 +25,6 @@ CommandHead(CmdHelp, cmd, argp) ); ReplyBasic("*Written with a shoelace and UHU glue by LDA <3 !*"); BotDestroy(); + + (void) cmd; } diff --git a/src/Commands/ListBans.c b/src/Commands/ListBans.c index 109c5d4..50d9d31 100644 --- a/src/Commands/ListBans.c +++ b/src/Commands/ListBans.c @@ -32,4 +32,6 @@ CommandHead(CmdListBans, cmd, argp) DbUnlock(data->db, listed); BotDestroy(); + + (void) cmd; } diff --git a/src/Commands/Plumb.c b/src/Commands/Plumb.c index e3c4ff3..f51e4e6 100644 --- a/src/Commands/Plumb.c +++ b/src/Commands/Plumb.c @@ -24,7 +24,8 @@ CommandHead(CmdPlumb, cmd, argp) BotRequired(room); /* Check MUC viability */ - if (ParseeManageBan(args->data, muc, NULL)) + if (ParseeManageBan(args->data, muc, NULL) || + !ParseeIsMUCWhitelisted(args->data, muc)) { ReplySprintf("MUC '%s' is not allowed on this bridge.", muc); goto end; @@ -62,7 +63,7 @@ CommandHead(CmdPlumb, cmd, argp) if (chat_id) { char *rev = StrConcat(2, muc, "/parsee"); - XMPPJoinMUC(args->data->jabber, "parsee", rev, false); + XMPPJoinMUC(args->data->jabber, "parsee", rev, NULL, -1, false); Free(rev); } diff --git a/src/Commands/Stats.c b/src/Commands/Stats.c index 219204a..0df0b44 100644 --- a/src/Commands/Stats.c +++ b/src/Commands/Stats.c @@ -40,4 +40,6 @@ CommandHead(CmdStats, cmd, argp) ReplyBasic("*Written with a shoelace and UHU glue by LDA <3 !*"); BotDestroy(); + + (void) cmd; } diff --git a/src/Commands/UnlinkMUC.c b/src/Commands/UnlinkMUC.c index 3e2ae81..7f7cc50 100644 --- a/src/Commands/UnlinkMUC.c +++ b/src/Commands/UnlinkMUC.c @@ -9,43 +9,60 @@ #include +static bool +Grab(ParseeData *data, Command *cmd, char **muc, char **chat_id, char **room) +{ + if (HashMapGet(cmd->arguments, "muc")) + { + *muc = HashMapGet(cmd->arguments, "muc"); + + *chat_id = ParseeGetFromMUCID(data, *muc); + *room = ParseeGetRoomID(data, *chat_id); + if (!chat_id || !room) + { + return false; + } + return true; + } + else if (HashMapGet(cmd->arguments, "room")) + { + *room = HashMapGet(cmd->arguments, "room"); + + *chat_id = ParseeGetFromRoomID(data, *room); + *muc = ParseeGetMUCID(data, *chat_id); + if (!chat_id || !muc) + { + return false; + } + return true; + } + return false; +} + CommandHead(CmdUnlinkMUC, cmd, argp) { ParseeCmdArg *args = argp; ParseeData *data = args->data; - HashMap *json, *event = args->event, *mucs; - DbRef *ref; + HashMap *event = args->event; char *muc = NULL, *chat_id = NULL, *room = NULL; BotInitialise(); - muc = HashMapGet(cmd->arguments, "muc"); - if (!muc) + if (!Grab(data, cmd, &muc, &chat_id, &room)) { - ReplyBasic("`muc` field REQUIRED."); + ReplyBasic("`muc`|`room` REQUIRED"); goto end; } - ref = DbLock(data->db, 1, "chats"); - json = DbJson(ref); - chat_id = StrDuplicate(GrabString(json, 2, "mucs", muc)); + chat_id = ParseeGetFromMUCID(data, muc); + room = ParseeGetRoomID(data, chat_id); if (!chat_id) { ReplySprintf("No internal mapping to '%s'.", muc); goto end; } - mucs = GrabObject(json, 1, "mucs"); - JsonValueFree(HashMapDelete(mucs, muc)); - DbUnlock(data->db, ref); - room = ParseeGetRoomID(data, chat_id); - ref = DbLock(data->db, 1, "chats"); - json = DbJson(ref); - mucs = GrabObject(json, 1, "rooms"); - JsonValueFree(HashMapDelete(mucs, room)); - DbUnlock(data->db, ref); - - DbDelete(data->db, 2, "chats", chat_id); + ParseeUnlinkRoom(data, chat_id); /* TODO: Do it automatically, if *not plumbed* */ ReplySprintf("The MUC %s is now *unlinked*.", muc); diff --git a/src/Events.c b/src/Events.c index e625d07..e9edf12 100644 --- a/src/Events.c +++ b/src/Events.c @@ -79,7 +79,7 @@ MatrixCreateNickChange(char *nick) return map; } HashMap * -MatrixCreateMedia(char *mxc, char *body, char *mime) +MatrixCreateMedia(char *mxc, char *body, char *mime, FileInfo *info) { HashMap *map; char *mime_type = NULL, *matrix_type = NULL; @@ -91,9 +91,10 @@ MatrixCreateMedia(char *mxc, char *body, char *mime) matrix_type = "m.file"; if (mime) { - size_t i; + size_t i, len; mime_type = StrDuplicate(mime); - for (i = 0; i < strlen(mime); i++) + len = strlen(mime); + for (i = 0; i < len; i++) { if (mime_type[i] == '/') { @@ -119,6 +120,12 @@ MatrixCreateMedia(char *mxc, char *body, char *mime) } map = HashMapCreate(); + JsonSet(map, JsonValueString(mime), 2, "info", "mimetype"); + if (info && info->width && info->height) + { + JsonSet(map, JsonValueInteger(info->width), 2, "info", "w"); + JsonSet(map, JsonValueInteger(info->height), 2, "info", "h"); + } HashMapSet(map, "msgtype", JsonValueString(matrix_type)); HashMapSet(map, "mimetype", JsonValueString(mime)); HashMapSet(map, "body", JsonValueString(body)); diff --git a/src/FileInfo.c b/src/FileInfo.c new file mode 100644 index 0000000..17974d0 --- /dev/null +++ b/src/FileInfo.c @@ -0,0 +1,68 @@ +#include + +#include +#include + +#include +#include + +static int +GetField(XMLElement *elem) +{ + XMLElement *child; + if (!elem || ArraySize(elem->children) != 1) + { + return 0; + } + + child = ArrayGet(elem->children, 0); + + return strtol(child->data, NULL, 10); +} + +FileInfo * +FileInfoFromXMPP(XMLElement *stanza) +{ + FileInfo *info; + XMLElement *reference, *sims, *file; + + if (!stanza) + { + return NULL; + } + + reference = XMLookForTKV(stanza, + "reference", "xmlns", "urn:xmpp:reference:0" + ); + sims = XMLookForTKV(reference, + "media-sharing", "xmlns", "urn:xmpp:sims:1" + ); + file = XMLookForTKV(sims, + "file", "xmlns", "urn:xmpp:jingle:apps:file-transfer:5" + ); + /* TODO: We'll definitely need MIME types to do things like + * WebXDC */ + if (!file) + { + return NULL; + } + + info = Malloc(sizeof(*info)); + + + info->width = GetField(XMLookForUnique(file, "width")); + info->height = GetField(XMLookForUnique(file, "height")); + info->size = GetField(XMLookForUnique(file, "size")); + return info; +} + +void +FileInfoFree(FileInfo *info) +{ + if (!info) + { + return; + } + + Free(info); +} diff --git a/src/HttParsee.c b/src/HttParsee.c index 724f9ff..572487c 100644 --- a/src/HttParsee.c +++ b/src/HttParsee.c @@ -73,7 +73,7 @@ ParseeCreateRequest(const ParseeConfig *conf, HttpRequestMethod meth, char *path ctx = HttpRequest( meth, - HTTP_FLAG_TLS, + conf->homeserver_tls ? HTTP_FLAG_TLS : HTTP_FLAG_NONE, conf->homeserver_port, conf->homeserver_host, path ); diff --git a/src/Main.c b/src/Main.c index 652512c..10c809f 100644 --- a/src/Main.c +++ b/src/Main.c @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include +#include #include #include #include @@ -46,7 +48,7 @@ static const Argument arguments[] = "Generates a parsee.yaml AS file before exiting") Arg('v', false, NULL, "Forces Parsee to print in a more verbose fashion " - "(-vv prints stanzas to stderr)") + "(-vvv prints stanzas to stderr)") Arg('h', false, NULL, "Generates an help screen(this one!)") @@ -56,6 +58,50 @@ static const Argument arguments[] = #undef Argument }; +void +ParseeCheckMatrix(void *datp) +{ + static volatile uint64_t streak = 0; + ParseeData *data = datp; + if (data->config->accept_pings && !ASPing(data->config)) + { + Log(LOG_ERR, "Cannot reach '%s' properly...", data->config->homeserver_host); + if (++streak == 10) + { + DbRef *ref = DbLockIntent(data->db, DB_HINT_READONLY, 1, "chats"); + HashMap *json = DbJson(ref); + HashMap *mucs = GrabObject(json, 1, "mucs"); + char *muc; + void *ignored; + + + /* Notify any potential MUCs about this */ + while (HashMapIterate(mucs, &muc, &ignored)) + { + char *id = StrRandom(32); + char *sender = StrConcat(3, "parsee@", data->jabber->host, "/parsee"); + StanzaBuilder *b = CreateStanzaBuilder(sender, muc, id); + SetStanzaType(b, "groupchat"); + SetStanzaBody(b, + "This bridge hasn't been able to reach the Matrix host, and " + "as such, some messages may not have been sent over." + ); + + WriteoutStanza(b, data->jabber, 0); + DestroyStanzaBuilder(b); + + Free(sender); + Free(id); + } + (void) ignored; + + DbUnlock(data->db, ref); + } + return; + } + streak = 0; +} + int Main(Array *args, HashMap *env) { @@ -78,6 +124,15 @@ Main(Array *args, HashMap *env) ); ParseePrintASCII(); Log(LOG_INFO, "======================="); + Log(LOG_INFO, "(C)opyright 2024-2025 LDA and other contributors"); + Log(LOG_INFO, "(This program is free software, see LICENSE.)"); + +#ifdef PLATFORM_IPHONE + Log(LOG_WARNING, "Wait. Are you running this on an iPhone?"); + Log(LOG_WARNING, "You *ought* to have spoofed this, haven't you?"); + Log(LOG_WARNING, "Simply jealous of you for doing this."); +#endif + LogConfigIndent(LogConfigGlobal()); { @@ -103,6 +158,8 @@ Main(Array *args, HashMap *env) /* Write out the config file to a YAML document */ Log(LOG_INFO, "Generating YAML..."); yaml = StreamOpen("parsee.yaml", "w"); + ParseeConfigLoad(configuration); + ParseeConfigInit(); ParseeExportConfigYAML(yaml); StreamClose(yaml); Free(opts); @@ -113,6 +170,9 @@ Main(Array *args, HashMap *env) case PARSEE_VERBOSE_LOG: LogConfigLevelSet(LogConfigGlobal(), LOG_DEBUG); break; + case PARSEE_VERBOSE_TIMINGS: + Log(LOG_DEBUG, "Logging bench information."); + break; case PARSEE_VERBOSE_STANZA: Log(LOG_DEBUG, "Enabling stanza printing."); break; @@ -145,7 +205,6 @@ Main(Array *args, HashMap *env) } } Free(opts); - ParseeSetThreads(xmpp, http); } if (verbose >= PARSEE_VERBOSE_COMICAL) @@ -159,13 +218,16 @@ Main(Array *args, HashMap *env) { goto end; } + ParseeSetThreads(xmpp, http); Log(LOG_NOTICE, "Connecting to XMPP..."); jabber = XMPPInitialiseCompStream( + parsee_conf->component_addr, parsee_conf->component_host, parsee_conf->component_port ); + Log(LOG_NOTICE, "Connecting to XMPP... %p", jabber); if (!XMPPAuthenticateCompStream( jabber, parsee_conf->shared_comp_secret @@ -206,6 +268,12 @@ Main(Array *args, HashMap *env) } ParseeInitialiseNickTable(); + if (verbose >= PARSEE_VERBOSE_COMICAL) + { + Log(LOG_DEBUG, "Initialising affiliation table"); + } + ParseeInitialiseAffiliationTable(); + conf.port = parsee_conf->port; conf.threads = parsee_conf->http_threads; conf.maxConnections = conf.threads << 2; @@ -223,18 +291,21 @@ Main(Array *args, HashMap *env) if (ASRegisterUser(parsee_conf, parsee_conf->sender_localpart)) { char *parsee = ParseeMXID(conf.handlerArgs); + ASSetAvatar(parsee_conf, parsee, "mxc://tedomum.net/" "7e228734ec8e792960bb5633e43f0cb845f709f61825130490034651136" ); ASSetName(parsee_conf, parsee, "Parsee bridge"); + Free(parsee); } Log(LOG_NOTICE, "Starting up local cronjobs..."); cron = CronCreate(10 SECONDS); CronEvery(cron, 5 MINUTES, ParseeCleanup, conf.handlerArgs); + CronEvery(cron, 10 SECONDS, ParseeCheckMatrix, conf.handlerArgs); ParseeCleanup(conf.handlerArgs); CronStart(cron); @@ -247,8 +318,9 @@ Main(Array *args, HashMap *env) } server = HttpServerCreate(&conf); + ((ParseeData *) conf.handlerArgs)->server = server; - if (!ParseeInitialiseSignals(server, xmpp_thr, jabber)) + if (!ParseeInitialiseSignals(conf.handlerArgs, xmpp_thr)) { goto end; } @@ -272,9 +344,12 @@ end: CronStop(cron); CronFree(cron); ParseeFreeData(conf.handlerArgs); + ParseeDestroyAffiliationTable(); ParseeDestroyNickTable(); ParseeDestroyOIDTable(); ParseeDestroyHeadTable(); ParseeDestroyJIDTable(); + + (void) env; return 0; } diff --git a/src/MatrixEventHandler.c b/src/MatrixEventHandler.c index 82d4cf8..6311fac 100644 --- a/src/MatrixEventHandler.c +++ b/src/MatrixEventHandler.c @@ -8,24 +8,29 @@ #include #include +#include #include #include -#include - static const char * GetXMPPInformation(ParseeData *data, HashMap *event, char **from, char **to); -static void -JoinMUC(ParseeData *data, HashMap *event, char *jid, char *muc, char *name) +static char * +JoinMUC(ParseeData *data, HashMap *event, char *jid, char *muc, char *name, char *hash) { char *sender = GrabString(event, 1, "sender"); - char *nick = StrDuplicate(name); + Unistr *uninick = UnistrCreate(name); + Unistr *filtered = UnistrFilter(uninick, UnistrIsASCII); /* I'm not even going to try messing with the BMP anymore. */ + char *nick = UnistrC(filtered); char *rev = StrConcat(3, muc, "/", nick); int nonce = 0; - while (!XMPPJoinMUC(data->jabber, jid, rev, true) && nonce < 32) + UnistrFree(uninick); + UnistrFree(filtered); + + /* TODO: vCards! */ + while (!XMPPJoinMUC(data->jabber, jid, rev, hash, -1, true) && nonce < 32) { char *nonce_str = StrInt(nonce); char *input = StrConcat(3, sender, name, nonce_str); @@ -50,7 +55,7 @@ JoinMUC(ParseeData *data, HashMap *event, char *jid, char *muc, char *name) ParseePushNickTable(muc, sender, nick); Free(nick); - Free(rev); + return (rev); } static void @@ -93,20 +98,95 @@ ParseeMemberHandler(ParseeData *data, HashMap *event) else if (StrEquals(membership, "join") && !ParseeIsPuppet(conf, state_key)) { char *jid = ParseeEncodeMXID(state_key); + char *sha = NULL, *mime = NULL; + char *avatar = ASGetAvatar(data->config, NULL, state_key); + char *url = ParseeToUnauth(data, avatar, NULL); chat_id = ParseeGetFromRoomID(data, room_id); + + ASGetMIMESHA(data->config, avatar, &mime, &sha); + Free(avatar); + avatar = NULL; if (chat_id) { char *muc = ParseeGetMUCID(data, chat_id); char *name = ASGetName(data->config, room_id, state_key); + char *jabber = JoinMUC(data, event, jid, muc, name, sha); + avatar = ASGetAvatar(data->config, NULL, state_key); + Log(LOG_DEBUG, "MATRIX: Joining as '%s' (avatar=%s)", jabber, avatar); - JoinMUC(data, event, jid, muc, name); + Free(jabber); + Free(avatar); Free(name); Free(muc); + avatar = NULL; /* TODO: XEP-0084 magic to advertise a new avatar if possible. */ } - Free(jid); + else + { + char *full_jid = StrConcat(3, + jid, "@", data->config->component_host + ); + XMLElement *elem, *pevent, *items, *item, *meta, *info; + + Log(LOG_DEBUG, "MATRIX: Got local user '%s'(mxid=%s)", jid, state_key); + + elem = XMLCreateTag("message"); + { +#define PUBSUB "http://jabber.org/protocol/pubsub" +#define AVATAR "urn:xmpp:avatar:metadata" + pevent = XMLCreateTag("event"); + XMLAddAttr(pevent, "xmlns", PUBSUB "#event"); + { + items = XMLCreateTag("items"); + item = XMLCreateTag("item"); + XMLAddAttr(items, "node", AVATAR); + XMLAddAttr(item, "id", sha); + { + meta = XMLCreateTag("metadata"); + info = XMLCreateTag("info"); + XMLAddAttr(meta, "xmlns", AVATAR); + + XMLAddAttr(info, "id", sha); + XMLAddAttr(info, "url", url); + XMLAddAttr(info, "type", mime); + + XMLAddChild(meta, info); + XMLAddChild(item, meta); + } + XMLAddChild(items, item); + XMLAddChild(pevent, items); + } + XMLAddChild(elem, pevent); +#undef PUBSUB + } + + /* TODO: Broadcast PEP avatar change */ + ParseeBroadcastStanza(data, full_jid, elem); + XMLFreeElement(elem); + + elem = XMLCreateTag("presence"); + { + XMLElement *x = XMLCreateTag("x"); + XMLElement *photo; + + XMLAddAttr(x, "xmlns", "vcard-temp:x:update"); + photo = XMLCreateTag("photo"); + XMLAddChild(photo, XMLCreateText(sha)); + XMLAddChild(x, photo); + XMLAddChild(elem, x); + } + ParseeBroadcastStanza(data, full_jid, elem); + XMLFreeElement(elem); + + Free(full_jid); + } Free(chat_id); + Free(avatar); + Free(mime); + Free(sha); + Free(jid); + Free(url); } else if ((StrEquals(membership, "leave") || StrEquals(membership, "ban")) @@ -175,14 +255,9 @@ ParseeBotHandler(ParseeData *data, HashMap *event) return; } - if (*body != '!') + if (!body || *body != '!') { /* All commands are to be marked with a ! */ - Free(ASSend( - data->config, id, profile, - "m.room.message", - MatrixCreateNotice("Please enter a valid command") - )); Free(profile); return; } @@ -192,7 +267,7 @@ ParseeBotHandler(ParseeData *data, HashMap *event) Free(ASSend( data->config, id, profile, "m.room.message", - MatrixCreateNotice("You are not authorised to do this.") + MatrixCreateNotice("You are not authorised to do this."), 0 )); Free(profile); return; @@ -214,10 +289,10 @@ GetXMPPInformation(ParseeData *data, HashMap *event, char **from, char **to) char *room_id = GrabString(event, 1, "room_id"); char *matrix_sender = GrabString(event, 1, "sender"); char *chat_id = NULL, *muc_id = NULL; - char *user; + char *user = NULL; - DbRef *room_data; - HashMap *data_json; + DbRef *room_data = NULL; + HashMap *data_json = NULL; bool direct = false; if (!data || !event || !from || !to) @@ -249,7 +324,8 @@ GetXMPPInformation(ParseeData *data, HashMap *event, char **from, char **to) } else { - char *matrix_name; + char *matrix_name = NULL, *matrix_avatar = NULL; + char *mime = NULL, *sha = NULL; muc_id = ParseeGetMUCID(data, chat_id); if (!chat_id) @@ -263,10 +339,16 @@ GetXMPPInformation(ParseeData *data, HashMap *event, char **from, char **to) } matrix_name = ASGetName(data->config, room_id, matrix_sender); - JoinMUC(data, event, *from, muc_id, matrix_name); + matrix_avatar = ASGetAvatar(data->config, NULL, matrix_sender); + + ASGetMIMESHA(data->config, matrix_avatar, &mime, &sha); + Free(JoinMUC(data, event, *from, muc_id, matrix_name, sha)); *to = muc_id; + Free(matrix_avatar); Free(matrix_name); + Free(mime); + Free(sha); } Free(chat_id); @@ -276,28 +358,47 @@ GetXMPPInformation(ParseeData *data, HashMap *event, char **from, char **to) static void ParseeMessageHandler(ParseeData *data, HashMap *event) { + if (!data || !event) + { + return; + } XMPPComponent *jabber = data->jabber; - StanzaBuilder *builder; + StanzaBuilder *builder = NULL; DbRef *ref = NULL; - HashMap *json; + HashMap *json = NULL; + char *msgtype = GrabString(event, 2, "content", "msgtype"); + char *m_sender = GrabString(event, 1, "sender"); + char *unedited_id = NULL; char *body = GrabString(event, 2, "content", "body"); char *id = GrabString(event, 1, "room_id"); char *ev_id = GrabString(event, 1, "event_id"); - char *m_sender = GrabString(event, 1, "sender"); - char *chat_id, *muc_id; + char *chat_id = NULL, *muc_id = NULL; char *reply_id = MatrixGetReply(event); - char *xepd = ParseeXMPPify(event); + char *xepd = ParseeXMPPify(data, event); char *type, *user, *xmppified_user = NULL, *to = NULL; char *unauth = NULL; char *origin_id = NULL, *stanza = NULL; char *sender = NULL; - char *unedited_id = MatrixGetEdit(event); char *url = GrabString(event, 2, "content", "url"); char *encoded_from = NULL; bool direct = false; + unedited_id = MatrixGetEdit(event); + if (unedited_id) + { + char *new_content = GrabString(event, 3, "content", "m.new_content", "body"); + if (new_content) body = new_content; + } + + if (data->config->ignore_bots && StrEquals(msgtype, "m.notice")) + { + Free(reply_id); + Free(xepd); + Free(unedited_id); + return; + } if (ParseeIsPuppet(data->config, m_sender) || ParseeManageBan(data, m_sender, id)) { @@ -328,7 +429,7 @@ ParseeMessageHandler(ParseeData *data, HashMap *event) type = direct ? "chat" : "groupchat"; user = GrabString(json, 1, "xmpp_user"); - unauth = ParseeToUnauth(data, url); + unauth = ParseeToUnauth(data, url, GrabString(event, 2, "content", "filename")); encoded_from = ParseeEncodeMXID(m_sender); xmppified_user = StrConcat(3, @@ -342,7 +443,8 @@ ParseeMessageHandler(ParseeData *data, HashMap *event) } else { - char *name; + char *name, *mime = NULL, *sha = NULL; + char *avatar; /* Try to find the chat ID */ muc_id = ParseeGetMUCID(data, chat_id); if (!chat_id) @@ -350,17 +452,21 @@ ParseeMessageHandler(ParseeData *data, HashMap *event) goto end; } - /* TODO: Check the name's validity. - * Is there a good way to check for that that isn't - * just "await on join and try again?" */ + /* TODO: Avoid using the AS endpoints */ name = ASGetName(data->config, id, m_sender); + avatar = ASGetAvatar(data->config, NULL, m_sender); + ASGetMIMESHA(data->config, avatar, &mime, &sha); - JoinMUC(data, event, encoded_from, muc_id, name); + Free(JoinMUC(data, event, encoded_from, muc_id, name, sha)); to = muc_id; + Free(sha); + Free(mime); Free(name); + Free(avatar); } + if (reply_id) { /* TODO: Monocles chat DM users HATE this trick! @@ -394,7 +500,7 @@ ParseeMessageHandler(ParseeData *data, HashMap *event) SetStanzaEdit(builder, origin_id); SetStanzaXParsee(builder, event); - WriteoutStanza(builder, jabber); + WriteoutStanza(builder, jabber, data->config->max_stanza_size); DestroyStanzaBuilder(builder); if (direct) @@ -417,8 +523,8 @@ end: Free(stanza); Free(sender); Free(unauth); - Free(unedited_id); Free(encoded_from); + Free(unedited_id); DbUnlock(data->db, ref); ref = NULL; @@ -449,8 +555,7 @@ ParseeEventHandler(ParseeData *data, HashMap *event) return; } else if (StrEquals(event_type, "m.room.message") || - StrEquals(event_type, "m.sticker")) /* TODO: Actual sticker - * support here... */ + StrEquals(event_type, "m.sticker")) { ParseeMessageHandler(data, event); Free(parsee); diff --git a/src/MatrixID.c b/src/MatrixID.c index 4488fd4..bb7c584 100644 --- a/src/MatrixID.c +++ b/src/MatrixID.c @@ -1,6 +1,9 @@ #include #include +#include +#include +#include #include @@ -32,3 +35,34 @@ MatrixParseID(char *user) return ret; } +UserID * +MatrixParseIDFromMTO(Uri *uri) +{ + UserID *id = NULL; + char *path, *params, *decoded; + if (!uri) + { + return NULL; + } + + if (!StrEquals(uri->proto, "https") || !StrEquals(uri->host, "matrix.to")) + { + return NULL; + } + if (strncmp(uri->path, "/#/", 3)) + { + return NULL; + } + path = StrDuplicate(uri->path + 3); + params = path ? strchr(path, '?') : NULL; + if (params) + { + *params = '\0'; + } + decoded = HttpUrlDecode(path); + id = MatrixParseID(decoded); + Free(decoded); + Free(path); + + return id; +} diff --git a/src/Parsee/Config.c b/src/Parsee/Config.c index 72eff53..b95e5e1 100644 --- a/src/Parsee/Config.c +++ b/src/Parsee/Config.c @@ -30,12 +30,18 @@ ParseeConfigLoad(char *conf) { return; } - stream = StreamOpen("parsee.json", "r"); + stream = StreamOpen(conf ? conf : "parsee.json", "r"); if (!stream) { return; } json = JsonDecode(stream); + if (!json) + { + Log(LOG_ERR, "Could not parse config JSON"); + StreamClose(stream); + return; + } config = Malloc(sizeof(*config)); #define CopyToStr(to, str) config->to = StrDuplicate( \ @@ -44,6 +50,9 @@ ParseeConfigLoad(char *conf) #define CopyToInt(to, str) config->to = (int) ( \ JsonValueAsInteger(HashMapGet(json, str)) \ ) +#define CopyToBool(to, str) config->to = (int) ( \ + JsonValueAsBoolean(HashMapGet(json, str)) \ + ) config->http_threads = 8; config->xmpp_threads = 8; @@ -58,10 +67,25 @@ ParseeConfigLoad(char *conf) CopyToStr(server_base, "hs_base"); CopyToStr(homeserver_host, "hs_host"); CopyToInt(homeserver_port, "hs_port"); + CopyToBool(homeserver_tls, "hs_tls"); + if (!HashMapGet(json, "hs_tls")) + { + config->homeserver_tls = true; + } + CopyToBool(accept_pings, "accept_pings"); CopyToInt(component_port, "component_port"); + CopyToStr(component_addr, "component_addr"); CopyToStr(component_host, "component_host"); CopyToStr(shared_comp_secret, "shared_secret"); + CopyToInt(max_stanza_size, "max_stanza_size"); + if (!config->max_stanza_size) + { + /* Standard XMPP "minimum" maximum */ + config->max_stanza_size = 10000; + } + + CopyToBool(ignore_bots, "ignore_bots"); CopyToStr(media_base, "media_base"); @@ -77,6 +101,7 @@ ParseeSetThreads(int xmpp, int http) { if (!config) { + Achievement("THREAD COUNT REQUEST WITHOUT CONFIG", true); return; } config->http_threads = http; @@ -88,6 +113,7 @@ ParseeExportConfigYAML(Stream *stream) { if (!stream || !config) { + Achievement("YAML EXPORT REQUEST WITHOUT CONFIG", true); return; } StreamPrintf(stream, "# Autogenerated YAML AS entry for %s\n", NAME); @@ -101,6 +127,7 @@ ParseeExportConfigYAML(Stream *stream) StreamPrintf(stream, "hs_token: \"%s\"\n", config->hs_token); StreamPrintf(stream, "sender_localpart: \"%s\"\n", config->sender_localpart); StreamPrintf(stream, "protocols: [\"xmpp\", \"jabber\"]\n"); + StreamPrintf(stream, "receive_ephemeral: true\n"); /* TODO: Actually use that field */ StreamPrintf(stream, "\n"); StreamPrintf(stream, "namespaces: \n"); StreamPrintf(stream, " users:\n"); @@ -109,6 +136,7 @@ ParseeExportConfigYAML(Stream *stream) StreamPrintf(stream, " aliases:\n"); StreamPrintf(stream, " - exclusive: true\n"); StreamPrintf(stream, " regex: \"#%s_.*\"\n", config->namespace_base); + StreamFlush(stream); } void @@ -119,6 +147,7 @@ ParseeConfigFree(void) return; } Free(config->component_host); + Free(config->component_addr); Free(config->shared_comp_secret); Free(config->db_path); Free(config->homeserver_host); diff --git a/src/Parsee/Data.c b/src/Parsee/Data.c index f5c20fe..46d1433 100644 --- a/src/Parsee/Data.c +++ b/src/Parsee/Data.c @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -26,11 +27,15 @@ ParseeInitData(XMPPComponent *comp) data->config = ParseeConfigGet(); data->router = HttpRouterCreate(); data->jabber = comp; + data->muc = CreateMUCServer(data); data->handler = CommandCreateRouter(); data->oid_servers = HashMapCreate(); pthread_mutex_init(&data->oidl, NULL); + data->halted = false; + pthread_mutex_init(&data->halt_lock, NULL); + if (data->config->db_size) { data->db = DbOpenLMDB(data->config->db_path, data->config->db_size); @@ -50,14 +55,16 @@ ParseeInitData(XMPPComponent *comp) char *id = StrRandom(64); ref = DbCreate(data->db, 1, "info"); HashMapSet(DbJson(ref), "identifier", JsonValueString(id)); + HashMapSet(DbJson(ref), "version", JsonValueString(VERSION)); Free(id); } version = GrabString(DbJson(ref), 1, "version"); - if (!ParseeIsCompatible(VERSION, version)) + if (version && !ParseeIsCompatible(VERSION, version)) { Log(LOG_WARNING, "Version mismatch(curr=%s db=%s).", VERSION, version); Log(LOG_WARNING, "Yeah. You may want to _not_ do that."); + Log(LOG_WARNING, "(Parsee still needs an upgradepath mechanism.)"); DbUnlock(data->db, ref); DbClose(data->db); @@ -107,8 +114,10 @@ ParseeFreeData(ParseeData *data) } HashMapFree(data->oid_servers); pthread_mutex_destroy(&data->oidl); + pthread_mutex_destroy(&data->halt_lock); Free(data->id); XMPPEndCompStream(data->jabber); + FreeMUCServer(data->muc); DbClose(data->db); HttpRouterFree(data->router); CommandFreeRouter(data->handler); @@ -123,9 +132,6 @@ ParseeCleanup(void *datp) char *chat; size_t i; uint64_t ts = UtilTsMillis(); - size_t entries = 0; - - Log(LOG_DEBUG, "Cleaning up..."); chats = DbList(data->db, 1, "chats"); @@ -169,7 +175,6 @@ ParseeCleanup(void *datp) if (cleaned > threshold) \ { \ DbDelete(data->db, 4, "chats", chat, #field"s", field); \ - entries++; \ } \ Free(field); \ } \ @@ -177,9 +182,12 @@ ParseeCleanup(void *datp) } \ while (0) - CleanupField(stanza, 30 MINUTES, 50); - CleanupField(event, 30 MINUTES, 50); - CleanupField(id, 30 MINUTES, 50); + /* TODO: Custom retention period for any 1.0 */ + CleanupField(stanza, 30 MINUTES, 500); + CleanupField(event, 30 MINUTES, 500); + CleanupField(id, 30 MINUTES, 500); + + /* TODO: Also cleanup user cache information */ #undef CleanupField } DbListFree(chats); @@ -223,7 +231,6 @@ ParseeCleanup(void *datp) if (cleaned > threshold) \ { \ JsonValueFree(HashMapDelete(field##s, field)); \ - entries++; \ } \ Free(field); \ } \ @@ -231,14 +238,13 @@ ParseeCleanup(void *datp) } \ while (0) - CleanupField(stanza, 3 HOURS, 50); - CleanupField(event, 3 HOURS, 50); - CleanupField(id, 3 HOURS, 50); + CleanupField(stanza, 3 HOURS, 500); + CleanupField(event, 3 HOURS, 500); + CleanupField(id, 3 HOURS, 500); DbUnlock(data->db, ref); } DbListFree(chats); - Log(LOG_DEBUG, "Cleant up %d entries...", entries); } void @@ -333,6 +339,15 @@ ParseePushStanza(ParseeData *data, char *chat_id, char *stanza_id, char *id, cha } /* TODO */ + { + ref = DbLock(data->db, 2, "chats", chat_id); + j = DbJson(ref); + if (j) + { + JsonValueFree(HashMapSet(j, "ts", JsonValueInteger(age))); + } + DbUnlock(data->db, ref); + } { ref = DbCreate(data->db, 4, "chats", chat_id, "stanzas", stanza_id); j = DbJson(ref); @@ -535,3 +550,178 @@ end: return ret; } + +void +ParseeUnlinkRoom(ParseeData *data, char *chat_id) +{ + char *muc, *room; + DbRef *ref; + if (!data || !chat_id) + { + return; + } + + muc = ParseeGetMUCID(data, chat_id); + room = ParseeGetRoomID(data, chat_id); + if (!muc || !room) + { + Free(muc); + Free(room); + return; + } + + ref = DbLock(data->db, 1, "chats"); + JsonValueFree(HashMapDelete( + GrabObject(DbJson(ref), 1, "rooms"), + room + )); + JsonValueFree(HashMapDelete( + GrabObject(DbJson(ref), 1, "mucs"), + muc + )); + DbUnlock(data->db, ref); + DbDelete(data->db, 2, "chats", chat_id); + + Free(muc); + Free(room); +} +bool +ParseeIsMUCWhitelisted(ParseeData *data, char *muc) +{ + char *server, *serv_start, *postserv; + DbRef *ref; + bool ret; + if (!data || !muc) + { + return false; + } + + if (!DbExists(data->db, 1, "whitelist")) + { + return true; + } + + serv_start = strchr(muc, '@'); + serv_start = serv_start ? serv_start : muc; + server = StrDuplicate(serv_start + 1); + postserv = server ? strchr(server, '/') : NULL; + if (postserv) /* GCC doesn't know strchr is pure. */ + { + *postserv = '\0'; + } + + ref = DbLockIntent(data->db, + DB_HINT_READONLY, + 1, "whitelist" + ); + ret = HashMapGet(DbJson(ref), server); + DbUnlock(data->db, ref); + Free(server); + + return ret; +} + +extern HashMap * +ParseeGetChatSettings(ParseeData *data, char *chat) +{ + HashMap *ret, *json; + DbRef *ref; + + char *key; + JsonValue *value; + if (!data || !chat) + { + return NULL; + } + + ref = DbLockIntent(data->db, DB_HINT_READONLY, + 3, "chats", chat, "settings" + ); + json = DbJson(ref); + if (!ref) + { + return HashMapCreate(); + } + + ret = HashMapCreate(); + while (HashMapIterate(json, &key, (void **) &value)) + { + char *str = JsonValueAsString(value); + HashMapSet(ret, key, StrDuplicate(str)); + } + DbUnlock(data->db, ref); + return ret; +} +void +ParseeFreeChatSettings(HashMap *settings) +{ + char *key; + void *val; + if (!settings) + { + return; + } + while (HashMapIterate(settings, &key, &val)) + { + Free(val); + } + HashMapFree(settings); +} +char * +ParseeGetChatSetting(ParseeData *data, char *chat, char *key) +{ + HashMap *map; + char *ret; + if (!data || !chat || !key) + { + return NULL; + } + + map = ParseeGetChatSettings(data, chat); + ret = StrDuplicate(HashMapGet(map, key)); + ParseeFreeChatSettings(map); + + return ret; +} +void +ParseeSetChatSetting(ParseeData *data, char *chat, char *key, char *val) +{ + DbRef *ref; + HashMap *json; + if (!data || !chat || !key || !val) + { + return; + } + + ref = DbLockIntent(data->db, DB_HINT_WRITE, + 3, "chats", chat, "settings" + ); + if (!ref) + { + ref = DbCreate(data->db, 3, "chats", chat, "settings"); + } + json = DbJson(ref); + + JsonValueFree(HashMapSet(json, key, JsonValueString(val))); + + DbUnlock(data->db, ref); + return; +} +bool +ParseeIsMediaEnabled(ParseeData *data, char *chat_id) +{ + char *value; + bool ret; + if (!data || !chat_id) + { + return false; + } + + ret = !StrEquals( + (value = ParseeGetChatSetting(data, chat_id, "p.media.enabled")), + "false" + ); + Free(value); + + return ret; +} diff --git a/src/Parsee/Logo.c b/src/Parsee/Logo.c index 245a91c..30885d9 100644 --- a/src/Parsee/Logo.c +++ b/src/Parsee/Logo.c @@ -4,11 +4,10 @@ const char *parsee_ascii[PARSEE_ASCII_LINES] = { - "----------------------------", " =+======", " || | _ _/__----", " / || \\ ==+= _/_____\\_", - " | || | -|- L___J", + " | || | -|- L___J ", "_/ || \\_ ||| .______\\", " || | | | |.____.|", " || / | \\ |L____||", diff --git a/src/Parsee/Tables/Affiliation.c b/src/Parsee/Tables/Affiliation.c new file mode 100644 index 0000000..e3b5f55 --- /dev/null +++ b/src/Parsee/Tables/Affiliation.c @@ -0,0 +1,106 @@ +#include + +#include +#include +#include +#include + +static pthread_mutex_t affi_lock; +static HashMap *affi_table = NULL; + +typedef struct XMPPStatus { + char *role; + char *affiliation; +} XMPPStatus; +static XMPPStatus * +CreateStatus(char *role, char *affiliation) +{ + XMPPStatus *ret; + if (!role || !affiliation) + { + return NULL; + } + + ret = Malloc(sizeof(*ret)); + ret->role = StrDuplicate(role); + ret->affiliation = StrDuplicate(affiliation); + + return ret; +} +static void +FreeStatus(XMPPStatus *status) +{ + if (!status) + { + return; + } + + Free(status->affiliation); + Free(status->role); + Free(status); +} + +void +ParseeInitialiseAffiliationTable(void) +{ + if (affi_table) + { + return; + } + pthread_mutex_init(&affi_lock, NULL); + pthread_mutex_lock(&affi_lock); + affi_table = HashMapCreate(); + pthread_mutex_unlock(&affi_lock); +} +void +ParseePushAffiliationTable(char *user, char *affi, char *role) +{ + XMPPStatus *status; + if (!user || !affi || !role) + { + return; + } + pthread_mutex_lock(&affi_lock); + + status = CreateStatus(role, affi); + FreeStatus(HashMapSet(affi_table, user, status)); + + pthread_mutex_unlock(&affi_lock); +} +bool +ParseeLookupAffiliation(char *user, char **affiliation, char **role) +{ + XMPPStatus *status; + if (!user || !affiliation || !role) + { + return false; + } + pthread_mutex_lock(&affi_lock); + + status = HashMapGet(affi_table, user); + *affiliation = StrDuplicate(status ? status->affiliation : NULL); + *role = StrDuplicate(status ? status->role : NULL); + + pthread_mutex_unlock(&affi_lock); + return !!status; +} +void +ParseeDestroyAffiliationTable(void) +{ + char *key; + void *val; + if (!affi_table) + { + return; + } + pthread_mutex_lock(&affi_lock); + while (HashMapIterate(affi_table, &key, &val)) + { + FreeStatus(val); + } + HashMapFree(affi_table); + affi_table = NULL; + pthread_mutex_unlock(&affi_lock); + pthread_mutex_destroy(&affi_lock); +} + diff --git a/src/Parsee/User.c b/src/Parsee/User.c index c751731..6e673bb 100644 --- a/src/Parsee/User.c +++ b/src/Parsee/User.c @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -147,14 +148,15 @@ char * ParseeEncodeJID(const ParseeConfig *c, char *jid, bool trim) { char *ret, *tmp; - size_t i; + size_t i, len; if (!c || !jid) { return NULL; } ret = StrConcat(2, c->namespace_base, "_l_"); - for (i = 0; i < strlen(jid); i++) + len = strlen(jid); + for (i = 0; i < len; i++) { char cpy = jid[i]; char cs[4] = { 0 }; @@ -164,7 +166,7 @@ ParseeEncodeJID(const ParseeConfig *c, char *jid, bool trim) /* RID: Break everything and die. */ break; } - if (islower(*cs) || isalnum(*cs) || *cs == '_' || + if (islower((int) *cs) || isalnum((int) *cs) || *cs == '_' || *cs == '=' || *cs == '-' || *cs == '/' || *cs == '+' || *cs == '.') { @@ -193,7 +195,7 @@ char * ParseeGetLocal(char *mxid) { char *cpy; - size_t i; + size_t i, len; if (!mxid) { return NULL; @@ -203,12 +205,14 @@ ParseeGetLocal(char *mxid) return StrDuplicate(mxid); } - mxid++; - cpy = Malloc(strlen(mxid) + 1); - memset(cpy, '\0', strlen(mxid) + 1); - memcpy(cpy, mxid, strlen(mxid)); + len = strlen(mxid); - for (i = 0; i < strlen(mxid); i++) + mxid++; + cpy = Malloc(len + 1); + memset(cpy, '\0', len + 1); + memcpy(cpy, mxid, len); + + for (i = 0; i < len; i++) { if (cpy[i] == ':') { @@ -224,15 +228,16 @@ char * ParseeEncodeMXID(char *mxid) { char *ret; - size_t i, j; + size_t i, j, len; if (!mxid) { return NULL; } /* Worst case scenario of 3-bytes the char */ - ret = Malloc(strlen(mxid) * 3 + 1); - for (i = 0, j = 0; i < strlen(mxid); i++) + len = strlen(mxid); + ret = Malloc(len * 3 + 1); + for (i = 0, j = 0; i < len; i++) { char src = mxid[i]; @@ -356,8 +361,6 @@ ParseePushDMRoom(ParseeData *d, char *mxid, char *jid, char *r) void ParseeDeleteDM(ParseeData *d, char *mxid, char *jid) { - DbRef *ref; - HashMap *j; char *dmid; if (!d || !mxid || !jid) { @@ -374,14 +377,15 @@ char * ParseeTrimJID(char *jid) { char *ret; - size_t i; + size_t i, len; if (!jid) { return NULL; } ret = StrDuplicate(jid); - for (i = 0; i < strlen(ret); i++) + len = strlen(ret); + for (i = 0; i < len; i++) { if (ret[i] == '/') { @@ -535,7 +539,6 @@ ParseeGetMUCID(ParseeData *data, char *chat_id) return ret; } - void ParseeSendPresence(ParseeData *data) { @@ -554,10 +557,16 @@ ParseeSendPresence(ParseeData *data) while (HashMapIterate(mucs, &muc, (void **) &val)) { char *rev = StrConcat(2, muc, "/parsee"); + char *chat_id = ParseeGetFromMUCID(data, muc); + DbRef *chat = DbLockIntent(data->db, DB_HINT_READONLY, 2, "chats", chat_id); + uint64_t ts = GrabInteger(DbJson(chat), 1, "ts"); + int diff = ts ? (int) ((UtilTsMillis() - ts) / 1000) : -1; /* Make a fake user join the MUC */ Log(LOG_NOTICE, "Sending presence to %s", rev); - XMPPJoinMUC(data->jabber, "parsee", rev, false); + XMPPJoinMUC(data->jabber, "parsee", rev, NULL, diff, false); + DbUnlock(data->db, chat); + Free(chat_id); Free(rev); } DbUnlock(data->db, ref); @@ -679,12 +688,13 @@ end: #include char * -ParseeToUnauth(ParseeData *data, char *mxc) +ParseeToUnauth(ParseeData *data, char *mxc, char *filename) { Uri *url = NULL; char *ret; char *key, *hmac; -#define PAT "%s/_matrix/client/v1/media/download/%s%s?hmac=%s" +#define PAT "%s/media/%s%s?hmac=%s" +#define PATF "%s/media/%s%s/%s?hmac=%s" size_t l; if (!data || !mxc) { @@ -705,19 +715,45 @@ ParseeToUnauth(ParseeData *data, char *mxc) hmac = ParseeHMACS(data->id, key); Free(key); - l = snprintf(NULL, 0, - PAT, - data->config->media_base, - url->host, url->path, - hmac - ); + if (!filename) + { + l = snprintf(NULL, 0, + PAT, + data->config->media_base, + url->host, url->path, + hmac + ); + } + else + { + char *encoded = HttpUrlEncode(filename); + l = snprintf(NULL, 0, + PATF, + data->config->media_base, + url->host, url->path, encoded, + hmac + ); + Free(encoded); + } ret = Malloc(l + 3); - snprintf(ret, l + 1, - PAT, - data->config->media_base, - url->host, url->path, - hmac - ); + if (!filename) + { + snprintf(ret, l + 1, + PAT, + data->config->media_base, + url->host, url->path, + hmac + ); + } + else + { + snprintf(ret, l + 1, + PATF, + data->config->media_base, + url->host, url->path, filename, + hmac + ); + } UriFree(url); Free(hmac); return ret; diff --git a/src/Parsee/Utils/Formatting.c b/src/Parsee/Utils/Formatting.c index 4e89aec..ec95aef 100644 --- a/src/Parsee/Utils/Formatting.c +++ b/src/Parsee/Utils/Formatting.c @@ -5,11 +5,14 @@ #include #include #include +#include +#include #include #include #include +#include #include #include @@ -17,7 +20,7 @@ typedef struct XMPPFlags { bool quote; } XMPPFlags; static char * -XMPPifyElement(HashMap *event, XMLElement *elem, XMPPFlags flags) +XMPPifyElement(const ParseeConfig *conf, HashMap *event, XMLElement *elem, XMPPFlags flags) { char *xepd = NULL, *tmp = NULL; @@ -55,7 +58,7 @@ XMPPifyElement(HashMap *event, XMLElement *elem, XMPPFlags flags) } \ } \ while (0) - switch (elem->type) + switch (elem ? elem->type : -1) { case XML_ELEMENT_DATA: Concat(elem->data); @@ -67,7 +70,7 @@ XMPPifyElement(HashMap *event, XMLElement *elem, XMPPFlags flags) for (i = 0; i < ArraySize(elem->children); i++) { child = ArrayGet(elem->children, i); - subxep = XMPPifyElement(event, child, flags); + subxep = XMPPifyElement(conf, event, child, flags); Concat(subxep); Free(subxep); @@ -80,7 +83,7 @@ XMPPifyElement(HashMap *event, XMLElement *elem, XMPPFlags flags) for (i = 0; i < ArraySize(elem->children); i++) { child = ArrayGet(elem->children, i); - subxep = XMPPifyElement(event, child, flags); + subxep = XMPPifyElement(conf, event, child, flags); Concat(subxep); Free(subxep); @@ -93,7 +96,7 @@ XMPPifyElement(HashMap *event, XMLElement *elem, XMPPFlags flags) for (i = 0; i < ArraySize(elem->children); i++) { child = ArrayGet(elem->children, i); - subxep = XMPPifyElement(event, child, flags); + subxep = XMPPifyElement(conf, event, child, flags); Concat(subxep); Free(subxep); @@ -126,7 +129,7 @@ XMPPifyElement(HashMap *event, XMLElement *elem, XMPPFlags flags) for (i = 0; i < ArraySize(elem->children); i++) { child = ArrayGet(elem->children, i); - subxep = XMPPifyElement(event, child, flags); + subxep = XMPPifyElement(conf, event, child, flags); Concat(subxep); Free(subxep); @@ -141,35 +144,70 @@ XMPPifyElement(HashMap *event, XMLElement *elem, XMPPFlags flags) for (i = 0; i < ArraySize(elem->children); i++) { child = ArrayGet(elem->children, i); - subxep = XMPPifyElement(event, child, flags); + subxep = XMPPifyElement(conf, event, child, flags); Concat(subxep); Free(subxep); } - Concat("\n"); + if (i != 0) + { + Concat("\n"); + } } else if (StrEquals(elem->name, "a")) { char *href = HashMapGet(elem->attrs, "href"); - Concat("("); - for (i = 0; i < ArraySize(elem->children); i++) + Uri *pref = UriParse(href); + if (pref && StrEquals(pref->host, "matrix.to")) { - child = ArrayGet(elem->children, i); - subxep = XMPPifyElement(event, child, flags); - - Concat(subxep); - Free(subxep); + /* TODO: Check if the element here is a Matrix.TO + * pointing to a Parsee user. */ + UserID *id = MatrixParseIDFromMTO(pref); + if (id) + { + char *real_id = StrConcat(4, "@", id->localpart, ":", id->server); + /* TODO: Detect if it already is a Parsee user */ + if (ParseeIsPuppet(conf, real_id)) + { + char *name = ASGetName(conf, NULL, real_id); + Concat((name ? name : real_id)); + Free(name); + } + else + { + Concat(real_id); + } + Free(real_id); + } + else + { + Concat(href); + } + + Free(id); } - Concat(" points to "); - Concat(href); - Concat(" )"); + else + { + for (i = 0; i < ArraySize(elem->children); i++) + { + child = ArrayGet(elem->children, i); + subxep = XMPPifyElement(conf, event, child, flags); + + Concat(subxep); + Free(subxep); + } + Concat(" < "); + Concat(href); + Concat(" >"); + } + UriFree(pref); } else { for (i = 0; i < ArraySize(elem->children); i++) { child = ArrayGet(elem->children, i); - subxep = XMPPifyElement(event, child, flags); + subxep = XMPPifyElement(conf, event, child, flags); Concat(subxep); Free(subxep); @@ -181,8 +219,45 @@ XMPPifyElement(HashMap *event, XMLElement *elem, XMPPFlags flags) } return xepd; } +static char * +GetRawBody(HashMap *event) +{ + void *id; + if ((id = MatrixGetEdit(event))) + { + char *new = GrabString(event, 3, "content", "m.new_content", "body"); + Free(id); + if (new) + { + return new; + } + } + return GrabString(event, 2, "content", "body"); +} +static char * +GetHTMLBody(HashMap *event) +{ + if (MatrixGetEdit(event)) + { + char *new = GrabString(event, 3, "content", "m.new_content", "formatted_body"); + if (new) + { + return new; + } + } + return GrabString(event, 2, "content", "formatted_body"); +} +static char * +GetBodyFormat(HashMap *event) +{ + if (MatrixGetEdit(event)) + { + return GrabString(event, 3, "content", "m.new_content", "format"); + } + return GrabString(event, 2, "content", "format"); +} char * -ParseeXMPPify(HashMap *event) +ParseeXMPPify(ParseeData *data, HashMap *event) { char *type, *format, *html; char *xepd = NULL; @@ -201,20 +276,28 @@ ParseeXMPPify(HashMap *event) return NULL; } - format = JsonValueAsString(JsonGet(event, 2, "content", "format")); - if (!StrEquals(format, "org.matrix.custom.html")) + if (!StrEquals(GetBodyFormat(event), "org.matrix.custom.html")) { /* Settle for the raw body instead. */ - char *body = JsonValueAsString(JsonGet(event, 2, "content", "body")); + char *body = GetRawBody(event); return StrDuplicate(body); } - html = JsonValueAsString(JsonGet(event, 2, "content", "formatted_body")); + html = GetHTMLBody(event); + html = StrConcat(3, "", html, ""); elem = XMLCDecode(StrStreamReader(html), true, true); + if (!elem) + { + /* Settle for the raw body instead. + * TODO: Have the parser be more leinent on errors in HTML mode. */ + char *body = GetRawBody(event); + Free(html); + return StrDuplicate(body); + } flags.quote = false; - xepd = XMPPifyElement(event, elem, flags); + xepd = XMPPifyElement(data ? data->config : NULL, event, elem, flags); XMLFreeElement(elem); Free(html); @@ -231,6 +314,7 @@ ParseeGenerateMTO(char *common_id) return NULL; } + /* TODO: Is HttpUrlEncode okay? */ common_id = HttpUrlEncode(common_id); matrix_to = StrConcat(2, "https://matrix.to/#/", common_id); Free(common_id); diff --git a/src/Parsee/Utils/Nofly.c b/src/Parsee/Utils/Nofly.c index 8a82c0f..439c7d0 100644 --- a/src/Parsee/Utils/Nofly.c +++ b/src/Parsee/Utils/Nofly.c @@ -6,6 +6,28 @@ #include #include +void +ParseeGlobalUnban(ParseeData *data, char *glob) +{ + DbRef *ref; + HashMap *j; + if (!data || !glob) + { + return; + } + + ref = DbLock(data->db, 1, "global_bans"); + if (!ref) + { + ref = DbCreate(data->db, 1, "global_bans"); + } + + j = DbJson(ref); + + JsonValueFree(HashMapDelete(j, glob)); + + DbUnlock(data->db, ref); +} void ParseeGlobalBan(ParseeData *data, char *glob, char *reason) { diff --git a/src/Parsee/Utils/String.c b/src/Parsee/Utils/String.c index 89d6f4a..c054d13 100644 --- a/src/Parsee/Utils/String.c +++ b/src/Parsee/Utils/String.c @@ -3,6 +3,8 @@ #include #include +#include + #include #include @@ -37,6 +39,22 @@ ParseeFindDatastart(char *data) return (int) (startline - data); } +int +ParseeFindDatastartU(char *data) +{ + Unistr *str; + size_t ret; + if (!data) + { + return 0; + } + + str = UnistrCreate(data); + ret = UnistrGetOffset(str, (uint32_t) '>'); + UnistrFree(str); + + return (int) ret; +} char * ParseeStringifyDate(uint64_t millis) diff --git a/src/Routes/Media.c b/src/Routes/Media.c index 7d6bc68..498eba2 100644 --- a/src/Routes/Media.c +++ b/src/Routes/Media.c @@ -10,6 +10,37 @@ #include +static HttpClientContext * +TryDownload(ParseeData *data, char *server, char *identi) +{ + HttpClientContext *cctx; + char *path; + server = HttpUrlEncode(server); + identi = HttpUrlEncode(identi); + + path = StrConcat(4, "/_matrix/media/v3/download/", server, "/", identi); + cctx = ParseeCreateRequest(data->config, HTTP_GET, path); + ASAuthenticateRequest(data->config, cctx); + Free(path); + + HttpRequestSendHeaders(cctx); + if (HttpRequestSend(cctx) != HTTP_OK) + { + Log(LOG_WARNING, "Failing back."); + HttpClientContextFree(cctx); + path = StrConcat(4, "/_matrix/client/v1/media/download/", server, "/", identi); + cctx = ParseeCreateRequest(data->config, HTTP_GET, path); + ASAuthenticateRequest(data->config, cctx); + Free(path); + HttpRequestSendHeaders(cctx); + HttpRequestSend(cctx); + } + + Free(server); + Free(identi); + return cctx; +} + RouteHead(RouteMedia, arr, argp) { ParseeHttpArg *args = argp; @@ -17,7 +48,7 @@ RouteHead(RouteMedia, arr, argp) HashMap *reqh, *params; char *server = ArrayGet(arr, 0); char *identi = ArrayGet(arr, 1); - char *path, *key, *val; + char *key, *val; char *hmac, *chkmak = NULL; params = HttpRequestParams(args->ctx); @@ -44,15 +75,7 @@ RouteHead(RouteMedia, arr, argp) /* Proxy the media through an authenticated endpoint if the HMAC * is valid. */ - server = HttpUrlEncode(server); - identi = HttpUrlEncode(identi); - path = StrConcat(4, "/_matrix/media/v3/download/", server, "/", identi); - cctx = ParseeCreateRequest(args->data->config, HTTP_GET, path); - ASAuthenticateRequest(args->data->config, cctx); - Free(path); - - HttpRequestSendHeaders(cctx); - HttpRequestSend(cctx); + cctx = TryDownload(args->data, server, identi); reqh = HttpResponseHeaders(cctx); while (HashMapIterate(reqh, &key, (void **) &val)) { @@ -65,8 +88,6 @@ RouteHead(RouteMedia, arr, argp) } HttpClientContextFree(cctx); - Free(server); - Free(identi); return NULL; } diff --git a/src/Routes/Ping.c b/src/Routes/Ping.c new file mode 100644 index 0000000..1d4191e --- /dev/null +++ b/src/Routes/Ping.c @@ -0,0 +1,36 @@ +#include + +#include + +#include +#include + +RouteHead(RoutePing, arr, argp) +{ + ParseeHttpArg *args = argp; + HashMap *request = NULL; + HashMap *response = NULL; + + response = ASVerifyRequest(args); + if (response) + { + goto end; + } + if (HttpRequestMethodGet(args->ctx) != HTTP_POST) + { + HttpResponseStatus(args->ctx, HTTP_METHOD_NOT_ALLOWED); + response = MatrixCreateError( + "M_UNRECOGNIZED", + "Path /ping only accepts POST as a valid method." + ); + goto end; + } + + RequestJSON(); + + response = HashMapCreate(); +end: + (void) arr; + JsonFree(request); + return response; +} diff --git a/src/Routes/Root.c b/src/Routes/Root.c index af89909..0ddb731 100644 --- a/src/Routes/Root.c +++ b/src/Routes/Root.c @@ -54,7 +54,13 @@ GetRandomQuote(void) NAME ": the federated world's little little kobashi", "Go take a look at your stanzas!", - "Go take a look at your objects!" + "Go take a look at your objects!", + + "DEC Alpha AXP-Certified!", + + "this is the moment parsee started parsing or smth idk" + " - another wise person", + "Ah, merde, mon TGV est en retard de 53 minutes !" }; const size_t count = sizeof(quotes)/sizeof(*quotes); @@ -84,11 +90,13 @@ RouteHead(RouteRoot, arr, argp) P("color: #eee;"); P("font-family: sans-serif;"); P("}"); - P("#cols {"); - P("column-count: 3;"); - P("min-width: 100%;"); - P("max-width: 100%;"); - P("width: 100%;"); + P("#ascii {"); + P("text-align: center;"); + P("color: #be1337;"); + P("}"); + P("#ascii pre {"); + P("display: inline-block;"); + P("text-align: left;"); P("}"); P("img {"); P("image-rendering: pixelated;"); @@ -115,6 +123,7 @@ RouteHead(RouteRoot, arr, argp) P(""); { + size_t i; P("
"); P("

Your %s is running, all with that %s!

", NAME, CODE); P("
"); @@ -124,6 +133,15 @@ RouteHead(RouteRoot, arr, argp) P("%s", GetRandomQuote()); } P(""); + P("
");
+            for (i = 0; i < PARSEE_ASCII_LINES; i++)
+            {
+                XMLElement *e = XMLCreateText((char *) parsee_ascii[i]);
+                XMLEncode(args->stream, e);
+                XMLFreeElement(e);
+                P("
"); + } + P("
"); P("

"); { @@ -161,7 +179,7 @@ RouteHead(RouteRoot, arr, argp) P("

"); { P("More information available at "); - P("the actual page."); } @@ -236,5 +254,6 @@ RouteHead(RouteRoot, arr, argp) P(""); #undef P + (void) arr; return NULL; } diff --git a/src/Routes/Transactions.c b/src/Routes/Transactions.c index 27e7393..580e43c 100644 --- a/src/Routes/Transactions.c +++ b/src/Routes/Transactions.c @@ -42,6 +42,7 @@ RouteHead(RouteTxns, arr, argp) response = HashMapCreate(); end: + (void) arr; JsonFree(request); return response; } diff --git a/src/Routes/UserAck.c b/src/Routes/UserAck.c index 9bb17e2..50029ba 100644 --- a/src/Routes/UserAck.c +++ b/src/Routes/UserAck.c @@ -67,7 +67,8 @@ RouteHead(RouteRoomAck, arr, argp) } muc = ParseeDecodeLocalMUC(args->data->config, room); - if (ParseeManageBan(args->data, muc, NULL)) + if (ParseeManageBan(args->data, muc, NULL) || + ParseeIsMUCWhitelisted(args->data, muc)) { HttpResponseStatus(args->ctx, HTTP_METHOD_NOT_ALLOWED); response = MatrixCreateError( @@ -130,7 +131,7 @@ RouteHead(RouteRoomAck, arr, argp) { char *rev = StrConcat(2, muc, "/parsee"); Log(LOG_NOTICE, "Sending presence to %s", rev); - XMPPJoinMUC(args->data->jabber, "parsee", rev, false); + XMPPJoinMUC(args->data->jabber, "parsee", rev, NULL, -1, false); Free(rev); } diff --git a/src/Signal.c b/src/Signal.c index 85e771c..3f01dcb 100644 --- a/src/Signal.c +++ b/src/Signal.c @@ -6,32 +6,35 @@ #include -static HttpServer *server = NULL; +static ParseeData *data; static pthread_t xmpp_thr; -static XMPPComponent *jabber = NULL; static void SignalHandler(int signal) { - if (server && (signal == SIGTERM || signal == SIGINT)) + if (data->server && (signal == SIGTERM || signal == SIGINT)) { Log(LOG_INFO, "Killing thread..."); - XMPPFinishCompStream(jabber); + + pthread_mutex_lock(&data->halt_lock); + data->halted = true; + pthread_mutex_unlock(&data->halt_lock); + + XMPPFinishCompStream(data->jabber); pthread_join(xmpp_thr, NULL); Log(LOG_INFO, "Stopping server..."); - HttpServerStop(server); + HttpServerStop(data->server); return; } } bool -ParseeInitialiseSignals(HttpServer *s, pthread_t xmpp, XMPPComponent *j) +ParseeInitialiseSignals(ParseeData *d, pthread_t xmpp) { struct sigaction sa; - server = s; + data = d; xmpp_thr = xmpp; - jabber = j; sigfillset(&sa.sa_mask); sa.sa_handler = SignalHandler; diff --git a/src/StanzaBuilder.c b/src/StanzaBuilder.c index 5426f80..f03c3b6 100644 --- a/src/StanzaBuilder.c +++ b/src/StanzaBuilder.c @@ -40,6 +40,7 @@ CreateStanzaBuilder(char *from, char *to, char *id) builder->replying_to_stanza = NULL; builder->replying_to_sender = NULL; + builder->editing = NULL; builder->type = NULL; builder->body = NULL; builder->oob = NULL; @@ -184,7 +185,7 @@ ExportStanza(StanzaBuilder *builder) builder->replying_to_sender && builder->body) { - int off = ParseeFindDatastart(builder->body); + int off = ParseeFindDatastartU(builder->body); char *ostr = StrInt(off); XMLElement *reply = XMLCreateTag("reply"); XMLElement *fallback = XMLCreateTag("fallback"); @@ -227,22 +228,21 @@ ExportStanza(StanzaBuilder *builder) } void -WriteoutStanza(StanzaBuilder *builder, XMPPComponent *jabber) +WriteoutStanza(StanzaBuilder *builder, XMPPComponent *jabber, size_t max) { XMLElement *elem; if (!builder || !jabber) { return; } + if (!max) + { + max = 10000; /* XMPP recommended limit */ + } elem = ExportStanza(builder); - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, elem); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); - + XMPPSendStanza(jabber, elem, max); XMLFreeElement(elem); - return; } StanzaBuilder * @@ -258,7 +258,6 @@ SetStanzaXParsee(StanzaBuilder *builder, HashMap *e) { XMLElement *parsee_version, *ver_elem; XMLElement *parsee_link, *link_elem; - XMLElement *parsee_text, *text_elem; XMLElement *parsee_event, *event_elem; XMLElement *parsee_json, *json_elem; char *event_id = GrabString(e, 1, "event_id"); @@ -285,16 +284,6 @@ SetStanzaXParsee(StanzaBuilder *builder, HashMap *e) XMLAddChild(parsee_link, link_elem); XMLAddChild(parsee, parsee_link); - parsee_text = XMLCreateTag("zayds-note"); - text_elem = XMLCreateText("\"LDA HANG YOURSELF\" - Zayd"); - XMLAddChild(parsee_text, text_elem); - XMLAddChild(parsee, parsee_text); - - parsee_text = XMLCreateTag("mcnebs-note"); - text_elem = XMLCreateText("LDA will never beat the allegations"); - XMLAddChild(parsee_text, text_elem); - XMLAddChild(parsee, parsee_text); - if (event_id) { parsee_event = XMLCreateTag("event-id"); diff --git a/src/StrSplit.c b/src/StrSplit.c index 91ecec7..eccd97f 100644 --- a/src/StrSplit.c +++ b/src/StrSplit.c @@ -7,7 +7,6 @@ #include #include #include -#include char ** StrSplitLines(char *text) @@ -117,7 +116,7 @@ StrFullRect(char **split) char StrGet(StringRect *rect, int line, int col) { - int actual_line, actual_col; + size_t actual_line, actual_col; char *linep; if (!rect || !rect->source_lines) { @@ -150,7 +149,7 @@ StrGet(StringRect *rect, int line, int col) size_t StrViewChars(StringRect rect, int line) { - int actual_line; + size_t actual_line; char *linep; if (!rect.source_lines) { @@ -174,7 +173,7 @@ StrViewChars(StringRect rect, int line) StringRect StrGetl(StringRect *rect, int line, bool extend) { - int actual_line; + size_t actual_line; StringRect ret; if (!rect->source_lines) { @@ -204,7 +203,7 @@ StrGetl(StringRect *rect, int line, bool extend) StringRect StrShift(StringRect rect, int n) { - int new = rect.start_char + n; + size_t new = rect.start_char + n; if (new > rect.end_char) { new = rect.end_char; diff --git a/src/Streams/Reader.c b/src/Streams/Reader.c index 2c0613a..bca261a 100644 --- a/src/Streams/Reader.c +++ b/src/Streams/Reader.c @@ -10,10 +10,10 @@ Stream * StrStreamReaderN(char *buffer, int n) { - if (!buffer) + if (!buffer || n < 0) { return NULL; } - return StreamFile(fmemopen(buffer, n ? n : strlen(buffer), "rb")); + return StreamFile(fmemopen(buffer, n ? (size_t) n : strlen(buffer), "rb")); } diff --git a/src/Streams/Writer.c b/src/Streams/Writer.c index 88bc7ee..c29b026 100644 --- a/src/Streams/Writer.c +++ b/src/Streams/Writer.c @@ -11,6 +11,9 @@ static ssize_t ReadStreamWriter(void *coop, void *to, size_t n) { /* Reading from a stream writer is silly. */ + (void) coop; + (void) to; + (void) n; return 0; } static ssize_t @@ -33,6 +36,9 @@ static off_t SeekStreamWriter(void *coop, off_t mag, int sgn) { /* TODO: Seeking would be useful, though not supported yet. */ + (void) coop; + (void) mag; + (void) sgn; return 0; } @@ -40,10 +46,11 @@ static int CloseStreamWriter(void *coop) { /* Nothing to free as of now. */ + (void) coop; return 0; } -const static IoFunctions Functions = { +static const IoFunctions Functions = { .read = ReadStreamWriter, .seek = SeekStreamWriter, .write = WriteStreamWriter, diff --git a/src/Unistr.c b/src/Unistr.c new file mode 100644 index 0000000..7eab2e0 --- /dev/null +++ b/src/Unistr.c @@ -0,0 +1,314 @@ +#include + +#include +#include +#include +#include + +#include +#include + +struct Unistr { + size_t length; + uint32_t *codepoints; +}; + +void +UnistrAddch(Unistr *unistr, uint32_t u) +{ + if (!unistr || !u) + { + return; + } + unistr->length++; + unistr->codepoints = Realloc( + unistr->codepoints, + unistr->length * sizeof(*unistr->codepoints) + ); + + unistr->codepoints[unistr->length - 1] = u; +} + +static bool +UTFIsN(char *off, size_t available, size_t n, uint8_t pc) +{ + size_t i; + uint8_t *offu = (uint8_t *) off; + if (((available < n) || ((*offu >> (8-n-1)) != pc)) && (n >= 1)) + { + return false; + } + + for (i = 0; i < n - 1; i++) + { + if ((offu[i+1] >> 6) != 0x2) + { + return false; + } + } + return true; +} + +Unistr * +UnistrCreate(char *src) +{ + size_t len, i; + Unistr *str; + if (!src) + { + return NULL; + } + + len = strlen(src); + str = Malloc(sizeof(*str)); + str->length = 0; + str->codepoints = NULL; + + /* We can't just set the length to {len}. */ + for (i = 0; i < len; i++) + { + char byte = src[i]; + size_t available = len - i; + if ((byte & 0x80) == 0) + { + /* This is a regular codepoint */ + UnistrAddch(str, byte & 0x7F); + continue; + } + else if (UTFIsN(&src[i], available, 2, 0x06)) + { + char a = src[i+0] & 0x1F; + char b = src[i+1] & 0x3F; + uint32_t u = (a << (6 * 1)) | b; + + /* Overlongs are errors. */ + if (u < 0x0080 || u > 0x07FF) + { + UnistrFree(str); + return NULL; + } + + UnistrAddch(str, u); + i += 2 - 1; + continue; + } + else if (UTFIsN(&src[i], available, 3, 0x0E)) + { + char a = src[i+0] & 0x0F; + char b = src[i+1] & 0x3F; + char c = src[i+2] & 0x3F; + uint32_t u = + (a << (6 * 2)) | + (b << (6 * 1)) | + (c << (6 * 0)) ; + + /* Overlongs are errors. */ + if (u < 0x0800 || u > 0xFFFF) + { + UnistrFree(str); + return NULL; + } + + UnistrAddch(str, u); + i += 3 - 1; + continue; + } + else if (UTFIsN(&src[i], available, 4, 0x1E)) + { + char a = src[i+0] & 0x07; + char b = src[i+1] & 0x3F; + char c = src[i+2] & 0x3F; + char d = src[i+3] & 0x3F; + uint32_t u = + (a << (6 * 3)) | + (b << (6 * 2)) | + (c << (6 * 1)) | + (d << (6 * 0)) ; + + /* Overlongs are errors. */ + if (u < 0x10000 || u > 0x10FFFF) + { + UnistrFree(str); + return NULL; + } + + UnistrAddch(str, u); + i += 4 - 1; + continue; + + } + } + + return str; +} +void +UnistrFree(Unistr *unistr) +{ + if (!unistr) + { + return; + } + + Free(unistr->codepoints); + Free(unistr); +} +char * +UnistrC(Unistr *unistr) +{ + char *ret, *tmp, *utf; + size_t i; + if (!unistr) + { + return NULL; + } + + ret = NULL; + for (i = 0; i < unistr->length; i++) + { + uint32_t code = unistr->codepoints[i]; + utf = StrUtf8Encode(code); + + tmp = ret; + ret = StrConcat(2, ret, utf); + Free(tmp); + Free(utf); + } + + return ret; +} +size_t +UnistrSize(Unistr *unistr) +{ + return unistr ? unistr->length : 0; +} +uint32_t +UnistrGetch(Unistr *unistr, size_t i) +{ + if (!unistr) + { + return 0; + } + + return i < unistr->length ? unistr->codepoints[i] : 0; +} +bool +UnistrIsASCII(uint32_t u) +{ + if (u == 0) + { + return NULL; + } + + return u < 0x7F; +} +bool +UnistrIsBMP(uint32_t u) +{ + if (u == 0) + { + return NULL; + } + + return u <= 0xFFFF; +} +Unistr * +UnistrFilter(Unistr *str, UnistrFilterFunc filter) +{ + Unistr *unistr; + size_t i; + if (!str || !filter) + { + return NULL; + } + + unistr = UnistrCreate(""); + for (i = 0; i < UnistrSize(str); i++) + { + uint32_t code = UnistrGetch(str, i); + if (!filter(code)) + { + continue; + } + UnistrAddch(unistr, code); + } + + return unistr; +} + +Unistr * +UnistrConcat(size_t n, ...) +{ + va_list list; + size_t i; + Unistr *ret = UnistrCreate(""); + + va_start(list, n); + for (i = 0; i < n; i++) + { + Unistr *to_concat = va_arg(list, Unistr *); + size_t j; + for (j = 0; j < UnistrSize(to_concat); j++) + { + UnistrAddch(ret, UnistrGetch(to_concat, j)); + } + } + + va_end(list); + return ret; +} +size_t +UnistrGetOffset(Unistr *str, uint32_t sep) +{ + size_t i; + uint32_t prev = 0x0A; + if (!str || !sep) + { + return 0; + } + + for (i = 0; i < str->length; i++) + { + uint32_t curr = str->codepoints[i]; + if (prev == 0x0A && curr != sep) + { + return i; + } + prev = curr; + } + return 0; +} +size_t +UnistrGetUTFOffset(char *cstr, size_t unicode) +{ + Unistr *tmp; + size_t ret = 0; + if (!cstr) + { + return 0; + } + + tmp = UnistrCreate(cstr); + for (size_t i = 0; i < unicode && i < tmp->length; i++) + { + uint32_t codepoint = tmp->codepoints[i]; + if (codepoint >= 0x0000 && codepoint <= 0x007F) + { + ret += 1; + } + else if (codepoint >= 0x0080 && codepoint <= 0x07FF) + { + ret += 2; + } + else if (codepoint >= 0x0800 && codepoint <= 0xFFFF) + { + ret += 3; + } + else if (codepoint >= 0x010000 && codepoint <= 0x10FFFF) + { + ret += 4; + } + } +end: + Free(tmp); + return ret; +} diff --git a/src/XEP-0393.c b/src/XEP-0393.c index a90b4a1..784bacf 100644 --- a/src/XEP-0393.c +++ b/src/XEP-0393.c @@ -87,7 +87,7 @@ DecodeQuote(StringRect rect, size_t *skip) * > four but that's for Nerds * > seasons See, Touhou reference! * concealing!) */ - while ((ch = StrGet(&rect, lines - 1, shift_by)) && isspace(ch)) + while ((ch = StrGet(&rect, lines - 1, shift_by)) && isspace((int) ch)) { shift_by++; } @@ -132,7 +132,7 @@ DecodeSpan(StringRect rect, char del, size_t *skip) { return StrFullRect(NULL); } - if (!ret.source_lines && isspace(c)) + if (!ret.source_lines && isspace((int) c)) { return StrFullRect(NULL); } @@ -329,8 +329,8 @@ ShoveXML(XEP393Element *element, XMLElement *xmlparent) break; case XEP393_MONO: head = XMLCreateTag("code"); + XMLAddChild(xmlparent, XMLCreateText("`")); XMLAddChild(xmlparent, head); - XMLAddChild(head, XMLCreateText("`")); break; case XEP393_SRKE: head = XMLCreateTag("s"); @@ -372,7 +372,7 @@ ShoveXML(XEP393Element *element, XMLElement *xmlparent) XMLAddChild(head, XMLCreateText("_")); break; case XEP393_MONO: - XMLAddChild(head, XMLCreateText("`")); + XMLAddChild(xmlparent, XMLCreateText("`")); break; case XEP393_SRKE: XMLAddChild(head, XMLCreateText("~")); @@ -399,21 +399,37 @@ ShoveXML(XEP393Element *element, XMLElement *xmlparent) char * XEP393ToXMLString(XEP393Element *xepd) { - XMLElement *root; + XMLElement *root, *act_root; + XMLElement *child; Stream *writer; char *ret = NULL; + size_t i, children; if (!xepd) { return NULL; } root = XMLCreateTag("span"); + act_root = root; ShoveXML(xepd, root); writer = StrStreamWriter(&ret); - XMLEncode(writer, root); - XMLFreeElement(root); + children = ArraySize(root->children); + + child = ArrayGet(root->children, 0); + if (children == 1 && StrEquals(child->name, "p")) + { + children = ArraySize(child->children); + root = child; + } + for (i = 0; i < children; i++) + { + child = ArrayGet(root->children, i); + + XMLEncode(writer, child); + } + XMLFreeElement(act_root); StreamFlush(writer); StreamClose(writer); diff --git a/src/XML/Parser.c b/src/XML/Parser.c index 1b8d30a..350f0e4 100644 --- a/src/XML/Parser.c +++ b/src/XML/Parser.c @@ -31,6 +31,12 @@ XMLCDecode(Stream *stream, bool autofree, bool html) bool flag = false; switch (event->type) { + case XML_ERROR: + XMLFreeEvent(event); + XMLFreeElement(ret); + ArrayFree(stack); + XMLFreeLexer(lexer); + return NULL; case XML_LEXER_STARTELEM: /* Create a new element that will populated. */ top = XMLCreateTag(event->element); @@ -114,9 +120,19 @@ XMLCDecode(Stream *stream, bool autofree, bool html) void XMLEncodeString(Stream *stream, char *data) { - size_t i; + size_t i, len; - for (i = 0; i < strlen(data); i++) + if (!stream || !data) + { + return; + } + + /* TODO: I should write a "Parsee Best Practice" guideline and make sure + * people understand to NOT constantly recompute lengths parameter on + * these kinds of loops. ArraySize is fine(since its indirection), but + * operations like strlen take time! */ + len = strlen(data); + for (i = 0; i < len; i++) { char c = data[i]; if (c == '<') @@ -145,6 +161,9 @@ XMLEncodeString(Stream *stream, char *data) continue; } StreamPrintf(stream, "%c", c); + /* TODO: Maybe consider Unistrings and encode arbitrary Unicode + * codepoints * with special XML. Oughta make it printable, you know? + */ } } void diff --git a/src/XML/SAX.c b/src/XML/SAX.c index 07c920a..defea6b 100644 --- a/src/XML/SAX.c +++ b/src/XML/SAX.c @@ -58,6 +58,7 @@ static char * XMLPopElement(XMLexer *lexer); static XMLEvent * XMLCreateEmptyElem(XMLexer *lexer, HashMap *attrs); static XMLEvent * XMLCreateStart(XMLexer *lexer, HashMap *attrs); static XMLEvent * XMLCreateRelax(XMLexer *lexer); +static XMLEvent * XMLCreateError(XMLexer *lexer); static XMLEvent * XMLCreateEnd(XMLexer *lexer, char *end); static XMLEvent * XMLCreateData(XMLexer *lexer); @@ -198,7 +199,9 @@ XMLCrank(XMLexer *lexer) else if (XMLookahead(lexer, "--", false)) { /* Throw error */ - return NULL; + XMLFreeEvent(event); + event = XMLCreateError(lexer); + break; } break; case XML_STATE_PI: @@ -215,6 +218,9 @@ XMLCrank(XMLexer *lexer) if (!attrname) { /* TODO: Throw error */ + XMLFreeEvent(event); + event = XMLCreateError(lexer); + break; } XMLPushElement(lexer, attrname); @@ -241,7 +247,10 @@ XMLCrank(XMLexer *lexer) } else if (XMLookahead(lexer, "'", true)) { - while (true); + //while (true); uh oh + XMLFreeEvent(event); + event = XMLCreateError(lexer); + break; } break; case XML_STATE_ATTRTAIL: @@ -250,6 +259,8 @@ XMLCrank(XMLexer *lexer) if (!XMLookahead(lexer, ">", true)) { /* TODO: Throw error. */ + XMLFreeEvent(event); + event = XMLCreateError(lexer); break; } lexer->state = XML_STATE_NONE; @@ -258,6 +269,8 @@ XMLCrank(XMLexer *lexer) break; default: /* TODO */ + XMLFreeEvent(event); + event = XMLCreateError(lexer); break; } /* TODO: Crank our XML parser. */ @@ -295,7 +308,7 @@ static bool XMLookahead(XMLexer *lexer, const char *str, bool skip) { int *stack; - size_t top, i; + size_t top, i, len; ssize_t ntop; bool ret = false; if (!lexer || !str) @@ -304,9 +317,10 @@ XMLookahead(XMLexer *lexer, const char *str, bool skip) } top = 0; - stack = Malloc(strlen(str) * sizeof(*stack)); + len = strlen(str); + stack = Malloc(len * sizeof(*stack)); - for (i = 0; i < strlen(str); i++) + for (i = 0; i < len; i++) { char c = str[i]; int getc = XMLGetc(lexer); @@ -336,8 +350,8 @@ seekback: return ret; } -#define IsNamestart(c) ((c == ':') || isalpha(c) || (c == '_')) -#define IsNamepart(c) (IsNamestart(c) || (c == '-') || isdigit(c)) +#define IsNamestart(c) ((c == ':') || isalpha((int) c) || (c == '_')) +#define IsNamepart(c) (IsNamestart(c) || (c == '-') || isdigit((int) c)) static char * XMLParseName(XMLexer *lexer) { @@ -582,6 +596,8 @@ XMLCreateEnd(XMLexer *lexer, char *end) event->col = 0; event->offset = 0; + (void) lexer; + return event; } static XMLEvent * @@ -690,6 +706,26 @@ XMLCreateData(XMLexer *lexer) return event; } static XMLEvent * +XMLCreateError(XMLexer *lexer) +{ + XMLEvent *event = Malloc(sizeof(*event)); + size_t elements = ArraySize(lexer->data.elements); + + event->type = XML_ERROR; + event->element = elements ? + StrDuplicate(ArrayGet(lexer->data.elements, elements - 1)) : + NULL; + event->attrs = NULL; + event->data = NULL; + + /* TODO */ + event->line = 0; + event->col = 0; + event->offset = 0; + + return event; +} +static XMLEvent * XMLCreateRelax(XMLexer *lexer) { XMLEvent *event = Malloc(sizeof(*event)); diff --git a/src/XMPP/Component.c b/src/XMPP/Component.c index 71f617a..e3ff4e4 100644 --- a/src/XMPP/Component.c +++ b/src/XMPP/Component.c @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -18,7 +19,7 @@ #define DEFAULT_PROSODY_PORT 5347 XMPPComponent * -XMPPInitialiseCompStream(char *host, int port) +XMPPInitialiseCompStream(char *addr, char *host, int port) { int sd = -1; struct addrinfo hints, *res, *res0; @@ -27,12 +28,17 @@ XMPPInitialiseCompStream(char *host, int port) Stream *stream; XMPPComponent *comp; + if (!addr) + { + addr = host; + } + snprintf(serv, sizeof(serv), "%hu", port ? port : DEFAULT_PROSODY_PORT); memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; - error = getaddrinfo(host, serv, &hints, &res0); + error = getaddrinfo(addr, serv, &hints, &res0); if (error) { const char *error_str = gai_strerror(error); @@ -55,6 +61,10 @@ XMPPInitialiseCompStream(char *host, int port) if (connect(sd, res->ai_addr, res->ai_addrlen) < 0) { + Log(LOG_ERR, + "%s: cannot connect to '%s': %s", __func__, + host, strerror(errno) + ); close(sd); sd = -1; continue; @@ -65,6 +75,10 @@ XMPPInitialiseCompStream(char *host, int port) if (sd < 0) { + Log(LOG_ERR, + "%s: cannot connect to '%s': no socket available", __func__, + host + ); return NULL; } freeaddrinfo(res0); @@ -72,6 +86,10 @@ XMPPInitialiseCompStream(char *host, int port) stream = StreamFd(sd); if (!stream) { + Log(LOG_ERR, + "%s: cannot connect to '%s': %s", __func__, + host, "couldn't create a Cytoplasm stream" + ); close(sd); return NULL; } @@ -134,7 +152,7 @@ XMPPAuthenticateCompStream(XMPPComponent *comp, char *shared) } break; } - if (ev->type != XML_LEXER_STARTELEM || + if (!ev || ev->type != XML_LEXER_STARTELEM || !StrEquals(ev->element, "stream:stream")) { Log(LOG_ERR, "Excepted stream:stream element."); @@ -159,12 +177,54 @@ XMPPAuthenticateCompStream(XMPPComponent *comp, char *shared) } break; } - if (ev->type != XML_LEXER_ELEM || + if (!ev || ev->type != XML_LEXER_ELEM || !StrEquals(ev->element, "handshake")) { - Log(LOG_DEBUG, "type=%d elem='%s'", ev->type, ev->element); Log(LOG_ERR, "Excepted empty handshake reply, got nonsense."); Log(LOG_ERR, "Another service (possibly Parsee) may have taken over."); + while ((ev = XMLCrank(sax))) + { + char *key, *val; + switch (ev->type) + { + case XML_LEXER_STARTELEM: + Log(LOG_DEBUG, "<%s>", ev->element); + + LogConfigIndent(LogConfigGlobal()); + LogConfigIndent(LogConfigGlobal()); + /* TODO: Log out attributes a little better */ + while (HashMapIterate(ev->attrs, &key, (void **) &val)) + { + Log(LOG_DEBUG, "(%s=%s)", key, val); + } + LogConfigUnindent(LogConfigGlobal()); + LogConfigUnindent(LogConfigGlobal()); + + LogConfigIndent(LogConfigGlobal()); + break; + case XML_LEXER_ELEM: + Log(LOG_DEBUG, "<%s/>", ev->element); + LogConfigIndent(LogConfigGlobal()); + LogConfigIndent(LogConfigGlobal()); + /* TODO: Log out attributes a little better */ + while (HashMapIterate(ev->attrs, &key, (void **) &val)) + { + Log(LOG_DEBUG, "(%s=%s)", key, val); + } + LogConfigUnindent(LogConfigGlobal()); + LogConfigUnindent(LogConfigGlobal()); + break; + case XML_LEXER_ENDELEM: + LogConfigUnindent(LogConfigGlobal()); + Log(LOG_DEBUG, "", ev->element); + break; + case XML_LEXER_DATA: + Log(LOG_DEBUG, "%s", ev->data); + break; + } + XMLFreeEvent(ev); + } + LogConfigIndentSet(LogConfigGlobal(), 0); Log(LOG_ERR, ""); Log(LOG_ERR, "Simply jealous of that other service..."); Free(stream_id); @@ -213,3 +273,35 @@ XMPPEndCompStream(XMPPComponent *comp) Free(comp->host); Free(comp); } +void +XMPPSendStanza(XMPPComponent *comp, XMLElement *stanza, size_t max) +{ + size_t len; + char *c = NULL; + Stream *stringWriter; + if (!comp || !stanza) + { + return; + } + + stringWriter = StrStreamWriter(&c); + XMLEncode(stringWriter, stanza); + StreamFlush(stringWriter); + StreamClose(stringWriter); + if (c && max && (len = strlen(c)) > max) + { + Log(LOG_WARNING, + "Unexceptedly large stanza received (len=%d max=%d).", + (int) len, (int) max + ); + Free(c); + return; + } + + + pthread_mutex_lock(&comp->write_lock); + StreamPrintf(comp->stream, "%s", c); + StreamFlush(comp->stream); + pthread_mutex_unlock(&comp->write_lock); + Free(c); +} diff --git a/src/XMPP/MUC.c b/src/XMPP/MUC.c index 832fdfe..9ecfcf2 100644 --- a/src/XMPP/MUC.c +++ b/src/XMPP/MUC.c @@ -8,6 +8,8 @@ #include #include +#include "XMPPThread/internal.h" + bool XMPPQueryMUC(XMPPComponent *jabber, char *muc, MUCInfo *out) { @@ -18,8 +20,6 @@ XMPPQueryMUC(XMPPComponent *jabber, char *muc, MUCInfo *out) return false; } - pthread_mutex_lock(&jabber->write_lock); - iq_query = XMLCreateTag("iq"); query = XMLCreateTag("query"); @@ -35,8 +35,7 @@ XMPPQueryMUC(XMPPComponent *jabber, char *muc, MUCInfo *out) { XMLElement *identity; - XMLEncode(jabber->stream, iq_query); - StreamFlush(jabber->stream); + XMPPSendStanza(jabber, iq_query, 10000); XMLFreeElement(iq_query); /* Except an IQ reply. 10 seconds of timeout is pretty @@ -45,8 +44,8 @@ XMPPQueryMUC(XMPPComponent *jabber, char *muc, MUCInfo *out) Free(uuid); if (!iq_query || !StrEquals(iq_query->name, "iq")) { + Log(LOG_ERR, "Didn't receive an stanza"); XMLFreeElement(iq_query); - pthread_mutex_unlock(&jabber->write_lock); return false; } query = XMLookForUnique(iq_query, "query"); @@ -57,7 +56,11 @@ XMPPQueryMUC(XMPPComponent *jabber, char *muc, MUCInfo *out) "conference")) { XMLFreeElement(iq_query); - pthread_mutex_unlock(&jabber->write_lock); + Log(LOG_DEBUG, "MUC INFO ERROR"); + Log(LOG_DEBUG, + "identityp=%p category=%s", identity, + identity ? HashMapGet(identity->attrs, "category") : NULL + ); return false; } @@ -73,7 +76,6 @@ XMPPQueryMUC(XMPPComponent *jabber, char *muc, MUCInfo *out) XMLFreeElement(iq_query); } } - pthread_mutex_unlock(&jabber->write_lock); return true; } @@ -116,7 +118,6 @@ XMPPRequestVoice(XMPPComponent *jabber, char *from, char *muc) return; } - pthread_mutex_lock(&jabber->write_lock); stanza = XMLCreateTag("message"); XMLAddAttr(stanza, "id", (identifier = StrRandom(32))); XMLAddAttr(stanza, "from", from); @@ -160,44 +161,68 @@ XMPPRequestVoice(XMPPComponent *jabber, char *from, char *muc) } XMLAddChild(stanza, x); } - XMLEncode(jabber->stream, stanza); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); + XMPPSendStanza(jabber, stanza, 10000); XMLFreeElement(stanza); Free(identifier); } bool -XMPPJoinMUC(XMPPComponent *comp, char *fr, char *muc, bool care) +XMPPJoinMUC(XMPPComponent *comp, char *fr, char *muc, char *hash, int time, bool ret) { - XMLElement *presence, *x, *reply; - char *from, *id; + XMLElement *presence, *x, *reply, *history, *photo; + IQFeatures *features; + char *from, *id, *stime = "3600"; if (!comp || !fr || !muc) { return false; } - pthread_mutex_lock(&comp->write_lock); - presence = XMLCreateTag("presence"); x = XMLCreateTag("x"); XMLAddAttr(presence, "from", (from = StrConcat(3, fr, "@", comp->host))); XMLAddAttr(presence, "to", muc); XMLAddAttr(presence, "id", (id = StrRandom(8))); XMLAddAttr(x, "xmlns", "http://jabber.org/protocol/muc"); + history = XMLCreateTag("history"); + + if (time > 0) + { + stime = StrInt(time); + } + XMLAddAttr(history, "seconds", stime); + if (time > 0) + { + Free(stime); + stime = NULL; + } + XMLAddChild(x, history); XMLAddChild(presence, x); - XMPPAnnotatePresence(presence); + features = LookupJIDFeatures(from); +#define IdentitySimple(cat, Type, Name) AddIQIdentity(features, cat, NULL, Type, Name); + IQ_IDENTITY +#undef IdentitySimple +#define AdvertiseSimple(feature) AdvertiseIQFeature(features, feature); + IQ_ADVERT +#undef AdvertiseSimple + XMPPAnnotatePresence(presence, features); + FreeIQFeatures(features); - XMLEncode(comp->stream, presence); - StreamFlush(comp->stream); + if (hash) + { + x = XMLCreateTag("x"); + XMLAddAttr(x, "xmlns", "vcard-temp:x:update"); + photo = XMLCreateTag("photo"); + XMLAddChild(photo, XMLCreateText(hash)); + XMLAddChild(x, photo); + XMLAddChild(presence, x); + } + XMPPSendStanza(comp, presence, 10000); XMLFreeElement(presence); Free(from); - pthread_mutex_unlock(&comp->write_lock); - - if (care && (reply = ParseeAwaitStanza(id, 500))) + if (ret && (reply = ParseeAwaitStanza(id, 500))) { bool exit_code = true; @@ -217,14 +242,13 @@ void XMPPLeaveMUC(XMPPComponent *comp, char *fr, char *muc, char *reason) { XMLElement *presence; + IQFeatures *features; char *from, *id; if (!comp || !fr || !muc) { return; } - pthread_mutex_lock(&comp->write_lock); - presence = XMLCreateTag("presence"); XMLAddAttr(presence, "from", (from = StrConcat(3, fr, "@", comp->host))); XMLAddAttr(presence, "to", muc); @@ -240,14 +264,20 @@ XMPPLeaveMUC(XMPPComponent *comp, char *fr, char *muc, char *reason) XMLAddChild(presence, status); } - XMPPAnnotatePresence(presence); + features = LookupJIDFeatures(from); +#define IdentitySimple(cat, Type, Name) AddIQIdentity(features, cat, NULL, Type, Name); + IQ_IDENTITY +#undef IdentitySimple +#define AdvertiseSimple(feature) AdvertiseIQFeature(features, feature); + IQ_ADVERT +#undef AdvertiseSimple + XMPPAnnotatePresence(presence, features); + FreeIQFeatures(features); - XMLEncode(comp->stream, presence); - StreamFlush(comp->stream); + + XMPPSendStanza(comp, presence, 10000); XMLFreeElement(presence); Free(from); Free(id); - - pthread_mutex_unlock(&comp->write_lock); } diff --git a/src/XMPP/MUCServ.c b/src/XMPP/MUCServ.c new file mode 100644 index 0000000..557c4a2 --- /dev/null +++ b/src/XMPP/MUCServ.c @@ -0,0 +1,154 @@ +#include + +#include +#include +#include + +#include +#include + +#define MUCNS "http://jabber.org/protocol/muc" + +struct MUCServer { + pthread_mutex_t mut; + + ParseeData *data; +}; + +static char * +GetMUCName(char *jid) +{ + char *name, *at; + size_t len; + if (!jid || *jid != '#') + { + return NULL; + } + + jid++; + + at = strchr(jid, '@'); + if (!at) + { + return StrDuplicate(jid); + } + len = at - jid; + name = Malloc(len + 1); + memset(name, '\0', len + 1); + memcpy(name, jid, len); + + return name; +} + +MUCServer * +CreateMUCServer(ParseeData *data) +{ + MUCServer *server; + if (!data) + { + return NULL; + } + + server = Malloc(sizeof(*server)); + pthread_mutex_init(&server->mut, NULL); + server->data = data; + return server; +} + +static bool +MUCManagePresence(MUCServer *serv, XMLElement *stanza, char *from, char *to) +{ + char *name; + XMLElement *x = XMLookForTKV(stanza, "x", "xmlns", MUCNS); + char *type = HashMapGet(stanza->attrs, "type"); + if (x && !type) + { + /* The user is trying to join the MUC */ + name = GetMUCName(to); + Log(LOG_WARNING, "%s is trying to join MUC '%s'", from, name); + + /* TODO: Check if the user should be joining. If so, make them. + * Implementing MUCs is gonna be fun. */ + + /* TODO: Presence broadcast hell. */ + Free(name); + } + return false; +} + +static bool +MUCManageMessage(MUCServer *serv, XMLElement *stanza, char *from, char *to) +{ + Log(LOG_WARNING, "MUCSERV: got a message %s->%s", from, to); + return false; +} + +bool +ManageMUCStanza(MUCServer *serv, XMLElement *stanza) +{ + char *stype, *from, *to; + bool ret; + if (!serv || !stanza) + { + return false; + } + + from = HashMapGet(stanza->attrs, "from"); + to = HashMapGet(stanza->attrs, "to"); + stype = stanza->name; + + if (to && *to != '#') + { + /* We aren't interacting with a MUC at all. Don't do anything. */ + return false; + } + if (StrEquals(stype, "iq")) + { + /* TODO: Worry about IQ later */ + return false; + } + + ret = false; + pthread_mutex_lock(&serv->mut); + if (StrEquals(stype, "presence")) + { + ret = MUCManagePresence(serv, stanza, from, to); + } + else if (StrEquals(stype, "message")) + { + ret = MUCManageMessage(serv, stanza, from, to); + } + /* TODO: Do stuff while locked */ + pthread_mutex_unlock(&serv->mut); + + /* TODO: Verify the destination, and make sure we aren't doing + * anything stupid(especially with discovery) */ + (void) ret; + return false; +} + +bool +MUCServerExists(MUCServer *serv, char *muc) +{ + if (!serv || !muc) + { + return false; + } + + return false; +} + +void +FreeMUCServer(MUCServer *serv) +{ + if (!serv) + { + return; + } + + pthread_mutex_lock(&serv->mut); + /* TODO */ + pthread_mutex_unlock(&serv->mut); + pthread_mutex_destroy(&serv->mut); + Free(serv); +} diff --git a/src/XMPP/Stanza.c b/src/XMPP/Stanza.c index e731792..a2daf5a 100644 --- a/src/XMPP/Stanza.c +++ b/src/XMPP/Stanza.c @@ -7,6 +7,8 @@ #include #include +#include + void XMPPRetract(XMPPComponent *comp, char *fr, char *to, char *type, char *redact) @@ -66,13 +68,9 @@ XMPPRetract(XMPPComponent *comp, char *fr, char *to, char *type, char *redact) } - pthread_mutex_lock(&comp->write_lock); - XMLEncode(comp->stream, message); - StreamFlush(comp->stream); + XMPPSendStanza(comp, message, 10000); XMLFreeElement(message); - pthread_mutex_unlock(&comp->write_lock); - Free(from); Free(ident); } @@ -85,6 +83,8 @@ XMPPIsParseeStanza(XMLElement *stanza) return false; } + /* TODO: Check if the user is a trustworthy Parsee puppet instead of some + * guy sending random stanzas */ return !!XMLookForUnique(stanza, "x-parsee"); } @@ -161,27 +161,31 @@ XMPPGetReply(XMLElement *elem) return HashMapGet(rep->attrs, "id"); } -void -XMPPAnnotatePresence(XMLElement *presence) +ssize_t +XMPPGetReplyOffset(XMLElement *elem) { - XMLElement *c; - char *ver; - if (!presence) + if (!elem) { - return; + return -1; } - - ver = XMPPGenerateVer(); - c = XMLCreateTag("c"); - XMLAddAttr(c, "xmlns", "http://jabber.org/protocol/caps"); - XMLAddAttr(c, "hash", "sha-1"); - XMLAddAttr(c, "node", REPOSITORY); - XMLAddAttr(c, "ver", ver); - - Free(ver); - XMLAddChild(presence, c); + for (size_t i = 0; i < ArraySize(elem->children); i++) + { + XMLElement *child = ArrayGet(elem->children, i); + char *xmlns = HashMapGet(child->attrs, "xmlns"); + char *xfor = HashMapGet(child->attrs, "for"); + if (StrEquals(child->name, "fallback") && + StrEquals(xmlns, "urn:xmpp:feature-fallback:0") && + StrEquals(xfor, "urn:xmpp:reply:0")) + { + XMLElement *body = XMLookForUnique(child, "body"); + if (body && HashMapGet(body->attrs, "end")) + { + return strtol(HashMapGet(body->attrs, "end"), NULL, 10); + } + } + } + return -1; } - char * XMPPGetModeration(XMLElement *stanza) { @@ -266,10 +270,7 @@ XMPPSendDisco(ParseeData *data, char *from, char *to) XMLAddChild(iq, query); } - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, iq); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); + XMPPSendStanza(jabber, iq, 10000); XMLFreeElement(iq); ret = ParseeAwaitStanza(identifier, 1.25 SECONDS); diff --git a/src/XMPPCommand/Manager.c b/src/XMPPCommand/Manager.c index 580773f..00a8c89 100644 --- a/src/XMPPCommand/Manager.c +++ b/src/XMPPCommand/Manager.c @@ -34,7 +34,17 @@ struct XMPPCommandManager { HashMap *sessions; void *cookie; + + XMPPCmdFilter filter; }; +static bool +XMPPDefaultFilter(XMPPCommandManager *manager, char *id, XMLElement *stanza) +{ + (void) manager; + (void) stanza; + (void) id; + return true; +} static void XMPPDestroySession(XMPPSession *session) { @@ -118,6 +128,7 @@ XMPPCreateManager(void *cookie) ret->commands = HashMapCreate(); ret->sessions = HashMapCreate(); ret->cookie = cookie; + ret->filter = XMPPDefaultFilter; return ret; } @@ -161,12 +172,12 @@ XMPPFreeManager(XMPPCommandManager *manager) Free(manager); } void -XMPPShoveCommandList(XMPPCommandManager *m, char *jid, XMLElement *p) +XMPPShoveCommandList(XMPPCommandManager *m, char *jid, XMLElement *p, XMLElement *s) { char *node_name; XMPPCommand *val; XMLElement *item; - if (!m || !p || !jid) + if (!m || !p || !jid || !s) { return; } @@ -174,11 +185,14 @@ XMPPShoveCommandList(XMPPCommandManager *m, char *jid, XMLElement *p) pthread_mutex_lock(&m->lock); while (HashMapIterate(m->commands, &node_name, (void **) &val)) { - item = XMLCreateTag("item"); - XMLAddAttr(item, "jid", jid); - XMLAddAttr(item, "node", node_name); - XMLAddAttr(item, "name", XMPPGetCommandDesc(val)); - XMLAddChild(p, item); + if (m->filter(m, node_name, s)) + { + item = XMLCreateTag("item"); + XMLAddAttr(item, "jid", jid); + XMLAddAttr(item, "node", node_name); + XMLAddAttr(item, "name", XMPPGetCommandDesc(val)); + XMLAddChild(p, item); + } } pthread_mutex_unlock(&m->lock); } @@ -206,6 +220,16 @@ XMPPVerifySession(XMPPCommandManager *mgr, char *s_id, char *from, char *to) return ret; } + +void +XMPPManagerSetFilter(XMPPCommandManager *manager, XMPPCmdFilter filter) +{ + if (!manager || !filter) + { + return; + } + manager->filter = filter; +} bool XMPPManageCommand(XMPPCommandManager *m, XMLElement *stanza, ParseeData *data) { @@ -242,7 +266,7 @@ XMPPManageCommand(XMPPCommandManager *m, XMLElement *stanza, ParseeData *data) /* This is an execution. */ cmd = HashMapGet(m->commands, node); - if (!cmd) + if (!cmd || !m->filter(m, node, stanza)) { /* TODO: Set an error note */ goto end; @@ -274,10 +298,7 @@ XMPPManageCommand(XMPPCommandManager *m, XMLElement *stanza, ParseeData *data) x = XMPPFormifyCommand(m, cmd, from); XMLAddChild(command_xml, x); - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, iq); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); + XMPPSendStanza(jabber, iq, data->config->max_stanza_size); XMLFreeElement(iq); goto end; @@ -305,10 +326,7 @@ XMPPManageCommand(XMPPCommandManager *m, XMLElement *stanza, ParseeData *data) XMPPExecuteCommand(m, cmd, from, command_xml, NULL); XMLAddChild(iq, command_xml); - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, iq); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); + XMPPSendStanza(jabber, iq, data->config->max_stanza_size); XMLFreeElement(iq); InvalidateSession(m, session_id); @@ -348,10 +366,7 @@ XMPPManageCommand(XMPPCommandManager *m, XMLElement *stanza, ParseeData *data) XMPPExecuteCommand(m, cmd, from, command_xml, x_form); XMLAddChild(iq, command_xml); - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, iq); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); + XMPPSendStanza(jabber, iq, data->config->max_stanza_size); XMLFreeElement(iq); InvalidateSession(m, session_given); diff --git a/src/XMPPCommands/Admins.c b/src/XMPPCommands/Admins.c index 7f0b172..5749165 100644 --- a/src/XMPPCommands/Admins.c +++ b/src/XMPPCommands/Admins.c @@ -15,19 +15,11 @@ void AdminsCallback(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement *out) { ParseeData *data = XMPPGetManagerCookie(m); - char *trimmed = ParseeTrimJID(from); size_t i; XMLElement *x; XMLElement *title; XMLElement *reported, *item, *field, *value, *txt; - if (!ParseeIsAdmin(data, trimmed)) - { - SetNote("error", "User is not authorised to execute command."); - Free(trimmed); - return; - } - Free(trimmed); x = XMLCreateTag("x"); title = XMLCreateTag("title"); @@ -58,4 +50,7 @@ AdminsCallback(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement * DbUnlock(data->db, ref); } XMLAddChild(out, x); + + (void) form; + (void) from; } diff --git a/src/XMPPCommands/Cleanup.c b/src/XMPPCommands/Cleanup.c index 4554f53..22780ff 100644 --- a/src/XMPPCommands/Cleanup.c +++ b/src/XMPPCommands/Cleanup.c @@ -29,4 +29,6 @@ CleanCallback(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement *o ParseeCleanup(data); /* TODO: Cleanup old sessions? */ SetNote("info", "Parsee data was sucessfully cleant up."); + + (void) form; } diff --git a/src/XMPPCommands/MUCInformation.c b/src/XMPPCommands/MUCInformation.c new file mode 100644 index 0000000..53dc684 --- /dev/null +++ b/src/XMPPCommands/MUCInformation.c @@ -0,0 +1,49 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +void +MUCInformationID(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement *out) +{ + ParseeData *data = XMPPGetManagerCookie(m); + char *muc = ParseeTrimJID(from); + char *chat_id = ParseeGetFromMUCID(data, muc); + char *room_id = ParseeGetRoomID(data, chat_id); + char *msg = StrConcat(5, + "The MUC ", muc, " is bridged to ", room_id, "." + ); + + SetNote("info", msg); + + Free(muc); + Free(msg); + Free(room_id); + Free(chat_id); + (void) form; +} +void +MUCInformationCID(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement *out) +{ + ParseeData *data = XMPPGetManagerCookie(m); + char *muc = ParseeTrimJID(from); + char *chat_id = ParseeGetFromMUCID(data, muc); + char *msg = StrConcat(5, + "The MUC ", muc, "'s internal ID is ", chat_id, "." + ); + + SetNote("info", msg); + + Free(muc); + Free(msg); + Free(chat_id); + (void) form; +} diff --git a/src/XMPPCommands/MUCKV.c b/src/XMPPCommands/MUCKV.c new file mode 100644 index 0000000..80ac9c4 --- /dev/null +++ b/src/XMPPCommands/MUCKV.c @@ -0,0 +1,117 @@ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +void +MUCSetKey(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement *out) +{ + ParseeData *data = XMPPGetManagerCookie(m); + char *affiliation = NULL, *role = NULL; + char *chat_id = NULL; + char *muc = NULL; + + char *key = NULL, *val = NULL; + + ParseeLookupAffiliation(from, &affiliation, &role); + Free(role); + if (!StrEquals(affiliation, "owner")) + { + SetNote("error", "Setting MUC properties requires the 'owner' affiliation."); + Free(affiliation); + return; + } + + GetFieldValue(key, "key", form); + GetFieldValue(val, "val", form); + if (!key || !val) + { + SetNote("error", "No keys or no value given."); + goto end; + } + + muc = ParseeTrimJID(from); + chat_id = ParseeGetFromMUCID(data, muc); + ParseeSetChatSetting(data, chat_id, key, val); + + SetNote("info", "Set key-value pair!"); +end: + Free(affiliation); + Free(chat_id); + Free(muc); + (void) form; +} +void +MUCGetKeys(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement *out) +{ + ParseeData *data = XMPPGetManagerCookie(m); + char *affiliation = NULL, *role = NULL; + char *chat_id = NULL; + char *muc = NULL; + + XMLElement *x; + XMLElement *title; + XMLElement *reported, *item, *field, *value, *txt; + + HashMap *settings = NULL; + + ParseeLookupAffiliation(from, &affiliation, &role); + Free(role); + if (!StrEquals(affiliation, "owner")) + { + SetNote("error", "Getting MUC roperties requires the 'owner' affiliation."); + goto end; + } + + + muc = ParseeTrimJID(from); + chat_id = ParseeGetFromMUCID(data, muc); + settings = ParseeGetChatSettings(data, chat_id); + + x = XMLCreateTag("x"); + title = XMLCreateTag("title"); + + SetTitle(x, "MUC/room settings"); + + XMLAddChild(x, title); + + XMLAddAttr(x, "xmlns", "jabber:x:data"); + XMLAddAttr(x, "type", "result"); + { + char *key, *val; + reported = XMLCreateTag("reported"); + XMLAddChild(x, reported); + + /* Report */ + Report("key", "Setting's key"); + Report("val", "Setting's value"); + + /* Set */ + while (HashMapIterate(settings, &key, (void **) &val)) + { + BeginItem(); + SetField("key", key); + SetField("val", val); + EndItem(); + } + } + XMLAddChild(out, x); + +end: + ParseeFreeChatSettings(settings); + Free(affiliation); + Free(chat_id); + Free(muc); + (void) form; +} diff --git a/src/XMPPCommands/MUCUnlink.c b/src/XMPPCommands/MUCUnlink.c new file mode 100644 index 0000000..f5560d3 --- /dev/null +++ b/src/XMPPCommands/MUCUnlink.c @@ -0,0 +1,67 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +void +MUCUnlink(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement *out) +{ + ParseeData *data = XMPPGetManagerCookie(m); + char *affiliation, *role; + char *chat_id; + char *parsee; + char *room; + char *muc; + + ParseeLookupAffiliation(from, &affiliation, &role); + Free(role); + if (!StrEquals(affiliation, "owner")) + { + SetNote("error", "Unlinking a MUC requires the 'owner' affiliation."); + Free(affiliation); + return; + } + + muc = ParseeTrimJID(from); + chat_id = ParseeGetFromMUCID(data, muc); + room = ParseeGetRoomID(data, chat_id); + if (!chat_id) + { + SetNote("error", "Couldn't fetch chat ID."); + Free(muc); + return; + } + ParseeUnlinkRoom(data, chat_id); + + parsee = ParseeMXID(data); + Free(ASSend( + data->config, room, parsee, + "m.room.message", + MatrixCreateNotice("This room has been unlinked."), + 0 + )); + ASLeave(data->config, room, parsee); + + XMPPLeaveMUC(data->jabber, "parsee", muc, "Unlinked by MUC admin."); + + /* Setting an error here won't work, as we're communicating through + * the MUC, which we *left*. I guess we can try to defer the leave. */ + SetNote("info", "Unlinked MUC."); + + Free(affiliation); + Free(chat_id); + Free(parsee); + Free(room); + Free(muc); + (void) form; +} diff --git a/src/XMPPCommands/Nofly.c b/src/XMPPCommands/Nofly.c index 86a7bb1..dcbef67 100644 --- a/src/XMPPCommands/Nofly.c +++ b/src/XMPPCommands/Nofly.c @@ -87,4 +87,6 @@ NoflyCallback(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement *o } DbUnlock(data->db, ref); } + + (void) form; } diff --git a/src/XMPPCommands/Status.c b/src/XMPPCommands/Status.c index 41a923a..1b19129 100644 --- a/src/XMPPCommands/Status.c +++ b/src/XMPPCommands/Status.c @@ -14,7 +14,6 @@ void StatusCallback(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement *out) { - ParseeData *data = XMPPGetManagerCookie(m); char *trimmed = ParseeTrimJID(from); size_t alloc = MemoryAllocated(); size_t kb = alloc >> 10; @@ -26,13 +25,6 @@ StatusCallback(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement * XMLElement *x; XMLElement *title, *txt; - if (!ParseeIsAdmin(data, trimmed)) - { - SetNote("error", "User is not authorised to execute command."); - - Free(trimmed); - return; - } Free(trimmed); x = XMLCreateTag("x"); title = XMLCreateTag("title"); @@ -75,4 +67,7 @@ StatusCallback(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement * EndItem(); } XMLAddChild(out, x); + + (void) form; + (void) m; } diff --git a/src/XMPPCommands/Whitelist.c b/src/XMPPCommands/Whitelist.c new file mode 100644 index 0000000..8e097a8 --- /dev/null +++ b/src/XMPPCommands/Whitelist.c @@ -0,0 +1,142 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +void +ClearWhitelistCallback(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement *out) +{ + ParseeData *data = XMPPGetManagerCookie(m); + char *trimmed = ParseeTrimJID(from); + + if (!ParseeIsAdmin(data, trimmed)) + { + SetNote("error", "User is not authorised to execute command."); + + Free(trimmed); + return; + } + Free(trimmed); + + if (!DbDelete(data->db, 1, "whitelist")) + { + SetNote("error", "Parsee whitelist was non-existent or could not be removed."); + return; + } + /* TODO: Cleanup old sessions? */ + SetNote("info", "Parsee whitelist was removed."); + + (void) form; +} +void +AddWhitelistCallback(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement *out) +{ + ParseeData *data = XMPPGetManagerCookie(m); + char *trimmed = ParseeTrimJID(from); + char *entity = NULL; + DbRef *ref; + + GetFieldValue(entity, "entity", form); + + if (!ParseeIsAdmin(data, trimmed)) + { + SetNote("error", "User is not authorised to execute command."); + + Free(trimmed); + return; + } + if (!entity) + { + SetNote("error", "No entity found."); + Free(trimmed); + return; + } + + Free(trimmed); + + ref = DbLock(data->db, 1, "whitelist"); + if (!ref) + { + ref = DbCreate(data->db, 1, "whitelist"); + } + if (!ref) + { + SetNote("error", "Couldn't get a database entry. You're cooked."); + return; + } + JsonValueFree(HashMapSet( + DbJson(ref), + entity, JsonValueObject(HashMapCreate()) + )); + DbUnlock(data->db, ref); + + SetNote("info", "Server successfully whitelisted."); +} +void +WhitelistCallback(XMPPCommandManager *m, char *from, XMLElement *form, XMLElement *out) +{ + ParseeData *data = XMPPGetManagerCookie(m); + char *trimmed = ParseeTrimJID(from); + XMLElement *x; + XMLElement *title; + XMLElement *reported, *item, *field, *value, *txt; + + if (!ParseeIsAdmin(data, trimmed)) + { + SetNote("error", "User is not authorised to execute command."); + + Free(trimmed); + return; + } + + x = XMLCreateTag("x"); + XMLAddAttr(x, "xmlns", "jabber:x:data"); + title = XMLCreateTag("title"); + XMLAddChild(x, title); + XMLAddChild(out, x); + + Free(trimmed); + + SetTitle(x, NAME " chat whitelist"); + + XMLAddAttr(x, "type", "result"); + { + DbRef *ref = DbLock(data->db, 1, "whitelist"); + HashMap *obj; + char *serv; + JsonValue *obj_val; + reported = XMLCreateTag("reported"); + XMLAddChild(x, reported); + + if (!ref) + { + ref = DbCreate(data->db, 1, "global_bans"); + } + + obj = DbJson(ref); + + /* Report */ + Report("server", "Allowed servers"); + + /* Set */ + while (HashMapIterate(obj, &serv, (void **) &obj_val)) + { + BeginItem(); + SetField("server", serv); + EndItem(); + + (void) obj_val; + } + DbUnlock(data->db, ref); + } + + (void) form; +} diff --git a/src/XMPPThread/Bridged.c b/src/XMPPThread/Bridged.c index 58fb571..0ad23ab 100644 --- a/src/XMPPThread/Bridged.c +++ b/src/XMPPThread/Bridged.c @@ -121,7 +121,7 @@ ParseeVerifyAllStanza(ParseeData *args, XMLElement *stanza) bool ServerHasXEP421(ParseeData *data, char *from) { - char *server = NULL, *parsee; + char *server = NULL, *postserv, *parsee; XMLElement *disco; bool ret = false; if (!data || !from) @@ -140,9 +140,10 @@ ServerHasXEP421(ParseeData *data, char *from) } server = StrDuplicate(server); - if (strchr(server, '/')) + postserv = server ? strchr(server, '/') : NULL; + if (postserv) { - *(strchr(server, '/')) = '\0'; + *postserv = '\0'; } parsee = ParseeJID(data); @@ -226,7 +227,6 @@ ParseeGetBridgedUserI(ParseeData *data, XMLElement *stanza, char *force) { ParseePushOIDTable(xmpp_from, occ_id); } - Log(LOG_DEBUG, "Trying Occ ID for %s{%s}", xmpp_from, occ_id); } if (!occ_id) diff --git a/src/XMPPThread/Caps.c b/src/XMPPThread/Caps.c index 867adbd..d79a30c 100644 --- a/src/XMPPThread/Caps.c +++ b/src/XMPPThread/Caps.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -12,33 +13,86 @@ #include "XMPPThread/internal.h" +IQFeatures * +CreateIQFeatures(void) +{ + IQFeatures *ret = Malloc(sizeof(*ret)); + + ret->identity = ArrayCreate(); + ret->adverts = ArrayCreate(); + + return ret; +} +void +FreeIQFeatures(IQFeatures *features) +{ + size_t i; + if (!features) + { + return; + } + + for (i = 0; i < ArraySize(features->adverts); i++) + { + Free(ArrayGet(features->adverts, i)); + } + ArrayFree(features->adverts); + + for (i = 0; i < ArraySize(features->identity); i++) + { + XMPPIdentity *identity = ArrayGet(features->identity, i); + + Free(identity->category); + Free(identity->type); + Free(identity->lang); + Free(identity->name); + + Free(identity); + } + ArrayFree(features->identity); + + Free(features); +} + +void +AdvertiseIQFeature(IQFeatures *f, char *feature) +{ + if (!f || !feature) + { + return; + } + + ArrayAdd(f->adverts, StrDuplicate(feature)); +} +void +AddIQIdentity(IQFeatures *f, char *cat, char *lang, char *type, char *name) +{ + XMPPIdentity *identity; + if (!f) + { + return; + } + + identity = Malloc(sizeof(*identity)); + identity->category = StrDuplicate(cat); + identity->type = StrDuplicate(type); + identity->lang = StrDuplicate(lang); + identity->name = StrDuplicate(name); + ArrayAdd(f->identity, identity); +} /* Generates a SHA-256 hash of the ver field. */ char * -XMPPGenerateVer(void) +XMPPGenerateVer(IQFeatures *features) { char *S = NULL; unsigned char *Sha = NULL; - Array *identities = ArrayCreate(); - Array *features = ArrayCreate(); size_t i; /* Initialise identity table, to be sorted */ -#define IdentitySimple(cat, Type, Name) { \ - XMPPIdentity *id = Malloc(sizeof(*id)); \ - id->category = cat; \ - id->lang = NULL; \ - id->type = Type; \ - id->name = Name; \ - ArrayAdd(identities, id); } - IQ_IDENTITY -#undef IdentitySimple -#define AdvertiseSimple(feature) ArrayAdd(features, feature); - IQ_ADVERT -#undef AdvertiseSimple - ArraySort(identities, IdentitySort); - for (i = 0; i < ArraySize(identities); i++) + ArraySort(features->identity, IdentitySort); + for (i = 0; i < ArraySize(features->identity); i++) { - XMPPIdentity *identity = ArrayGet(identities, i); + XMPPIdentity *identity = ArrayGet(features->identity, i); char *id_chunk = StrConcat(7, identity->category, "/", identity->type, "/", @@ -50,10 +104,10 @@ XMPPGenerateVer(void) Free(id_chunk); } - ArraySort(features, ((int (*) (void *, void *)) ICollate)); - for (i = 0; i < ArraySize(features); i++) + ArraySort(features->adverts, ((int (*) (void *, void *)) ICollate)); + for (i = 0; i < ArraySize(features->adverts); i++) { - char *feature = ArrayGet(features, i); + char *feature = ArrayGet(features->adverts, i); char *tmp = S; S = StrConcat(3, S, feature, "<"); Free(tmp); @@ -64,16 +118,64 @@ XMPPGenerateVer(void) S = Base64Encode((const char *) Sha, 20); Free(Sha); - ArrayFree(features); - for (i = 0; i < ArraySize(identities); i++) - { - XMPPIdentity *identity = ArrayGet(identities, i); - /* We don't have to do anything here. */ - Free(identity); - } - ArrayFree(identities); - return S; } +void +XMPPAnnotatePresence(XMLElement *presence, IQFeatures *features) +{ + XMLElement *c; + char *ver; + if (!presence || !features) + { + return; + } + + ver = XMPPGenerateVer(features); + c = XMLCreateTag("c"); + XMLAddAttr(c, "xmlns", "http://jabber.org/protocol/caps"); + XMLAddAttr(c, "hash", "sha-1"); + XMLAddAttr(c, "node", REPOSITORY); + XMLAddAttr(c, "ver", ver); + + Free(ver); + XMLAddChild(presence, c); +} + + +IQFeatures * +LookupJIDFeatures(char *jid) +{ + IQFeatures *features; + if (!jid) + { + return NULL; + } + + features = CreateIQFeatures(); + + if (*jid == '#') + { + /* This is a MUC. As such, we need to advertise MUCs */ +#define ID(...) AddIQIdentity(features, __VA_ARGS__) +#define AD(var) AdvertiseIQFeature(features, var) + ID("gateway", NULL, "matrix", "Parsee MUC gateway"); + ID("conference", NULL, "text", "Parsee MUC gateway"); + ID("component", NULL, "generic", "Parsee component"); + + AD("http://jabber.org/protocol/muc"); +#undef AD +#undef ID + } + else + { +#define IdentitySimple(cat, Type, Name) AddIQIdentity(features, cat, NULL, Type, Name); + IQ_IDENTITY +#undef IdentitySimple +#define AdvertiseSimple(feature) AdvertiseIQFeature(features, feature); + IQ_ADVERT +#undef AdvertiseSimple + } + return features; +} diff --git a/src/XMPPThread/PEP.c b/src/XMPPThread/PEP.c index be60096..2196ba7 100644 --- a/src/XMPPThread/PEP.c +++ b/src/XMPPThread/PEP.c @@ -10,6 +10,8 @@ #include #include +#define PUBSUB "http://jabber.org/protocol/pubsub" + XMLElement * CreatePubsubRequest(char *from, char *to, char *node) { @@ -22,7 +24,7 @@ CreatePubsubRequest(char *from, char *to, char *node) XMLAddAttr(iq_req, "type", "set"); pubsub = XMLCreateTag("pubsub"); - XMLAddAttr(pubsub, "xmlns", "http://jabber.org/protocol/pubsub"); + XMLAddAttr(pubsub, "xmlns", PUBSUB); XMLAddChild(iq_req, pubsub); sub = XMLCreateTag("subscribe"); @@ -34,12 +36,40 @@ CreatePubsubRequest(char *from, char *to, char *node) return iq_req; } +static bool +IsPubsubRequest(XMLElement *stanza) +{ + char *type = HashMapGet(stanza ? stanza->attrs : NULL, "type"); + XMLElement *pubsub; + if (!stanza) + { + return false; + } + + if (!StrEquals(stanza->name, "iq") || + !StrEquals(type, "set")) + { + return false; + } + + pubsub = XMLookForTKV(stanza, "pubsub", "xmlns", PUBSUB); + if (!pubsub) + { + return false; + } + + return XMLookForUnique(pubsub, "subscribe"); +} + + struct PEPManager { pthread_mutex_t lock; ParseeData *data; HashMap *node_table; + HashMap *followers; + void *cookie; }; @@ -56,6 +86,7 @@ CreatePEPManager(ParseeData *data, void *cookie) ret->cookie = cookie; ret->data = data; ret->node_table = HashMapCreate(); + ret->followers = HashMapCreate(); pthread_mutex_init(&ret->lock, NULL); return ret; @@ -68,29 +99,32 @@ PEPManagerCookie(PEPManager *manager) void PEPManagerAddEvent(PEPManager *manager, char *node, PEPEvent event) { + PEPEvent *indirect; if (!manager || !node || !event) { return; } + indirect = Malloc(sizeof(event)); + *indirect = event; pthread_mutex_lock(&manager->lock); - HashMapSet(manager->node_table, node, event); + HashMapSet(manager->node_table, node, indirect); pthread_mutex_unlock(&manager->lock); } static bool PEPManagerHandleEvent(PEPManager *manager, XMLElement *stanza) { - PEPEvent call = NULL; - XMLElement *event, *ps, *ev; + PEPEvent *call = NULL; + XMLElement *event = NULL, *ps = NULL, *ev = NULL; size_t i; if (!manager || !stanza) { return false; } -#define PEP_NS "http://jabber.org/protocol/pubsub" - if (!(ps = XMLookForTKV(stanza, "pubsub", "xmlns", PEP_NS)) && - !(ev = XMLookForTKV(stanza, "event", "xmlns", PEP_NS "#event"))) + + if (!(ps = XMLookForTKV(stanza, "pubsub", "xmlns", PUBSUB)) && + !(ev = XMLookForTKV(stanza, "event", "xmlns", PUBSUB "#event"))) { return false; } @@ -101,7 +135,7 @@ PEPManagerHandleEvent(PEPManager *manager, XMLElement *stanza) XMLElement *items = ArrayGet(event->children, i); char *node = HashMapGet(items->attrs, "node"); - if ((call = HashMapGet(manager->node_table, node))) + if ((call = HashMapGet(manager->node_table, node)) && *call) { size_t j; /* Use the callback over all items */ @@ -109,13 +143,13 @@ PEPManagerHandleEvent(PEPManager *manager, XMLElement *stanza) { for (j = 0; j < ArraySize(items->children); j++) { - call(manager, stanza, ArrayGet(items->children, j)); + (*call)(manager, stanza, ArrayGet(items->children, j)); } return true; } /* ... or over "items" specifically. */ - call(manager, stanza, items); + (*call)(manager, stanza, items); return true; } } @@ -132,6 +166,11 @@ PEPManagerHandle(PEPManager *manager, XMLElement *stanza) } /* Check if it is a PEP stanza */ + if (IsPubsubRequest(stanza)) + { + Log(LOG_DEBUG, "UNIMPLEMENTED PUBSUB SUBSCRIPTION"); + /* TODO */ + } if (PEPManagerHandleEvent(manager, stanza)) { return true; @@ -142,12 +181,21 @@ PEPManagerHandle(PEPManager *manager, XMLElement *stanza) void DestroyPEPManager(PEPManager *manager) { + char *key; + PEPEvent *val; if (!manager) { return; } pthread_mutex_destroy(&manager->lock); + while (HashMapIterate(manager->node_table, &key, (void **) &val)) + { + Free(val); + } HashMapFree(manager->node_table); + + HashMapFree(manager->followers); + Free(manager); } diff --git a/src/XMPPThread/PEPs/Avatar.c b/src/XMPPThread/PEPs/Avatar.c index 819bcd0..c42d260 100644 --- a/src/XMPPThread/PEPs/Avatar.c +++ b/src/XMPPThread/PEPs/Avatar.c @@ -72,11 +72,7 @@ PEPAvatarEvent(PEPManager *m, XMLElement *stanza, XMLElement *item) char *url = HashMapGet(item->attrs, "url"); XMLElement *request = CreateAvatarRequest(from, to, id); - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, request); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); - + XMPPSendStanza(jabber, request, args->config->max_stanza_size); XMLFreeElement(request); (void) url; /* TODO */ diff --git a/src/XMPPThread/PEPs/VCard.c b/src/XMPPThread/PEPs/VCard.c index a57c948..086e248 100644 --- a/src/XMPPThread/PEPs/VCard.c +++ b/src/XMPPThread/PEPs/VCard.c @@ -113,9 +113,7 @@ PEPVCardEvent(PEPManager *m, XMLElement *stanza, XMLElement *item) } } - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, reply); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); + XMPPSendStanza(jabber, reply, data->config->max_stanza_size); XMLFreeElement(reply); + (void) item; } diff --git a/src/XMPPThread/PresenceSub.c b/src/XMPPThread/PresenceSub.c new file mode 100644 index 0000000..dc30400 --- /dev/null +++ b/src/XMPPThread/PresenceSub.c @@ -0,0 +1,144 @@ +#include "XMPPThread/internal.h" + +#include +#include +#include +#include +#include +#include + +#include + +static char * +SubscriptionHash(char *from, char *to) +{ + uint8_t *sum; + char *hash; + size_t len; + + len = strlen(from) + 1 + strlen(to); + sum = Malloc(len); + memset(sum, 0x00, len); + memcpy(&sum[0], from, strlen(from)); + memcpy(&sum[strlen(from) + 1], to, strlen(to)); + + hash = Base64Encode((const char *) sum, len); + Free(sum); + + return hash; +} +static void +DecodeSubscription(ParseeData *data, char *hash, char **from, char **to) +{ + char *sum; + if (!data || !hash || !from || !to) + { + return; + } + + sum = Base64Decode(hash, strlen(hash)); + *from = StrDuplicate(sum); + *to = StrDuplicate(sum + strlen(sum) + 1); + Free(sum); +} + +void +AddPresenceSubscriber(ParseeData *data, char *from, char *to) +{ + Db *database; + DbRef *ref; + char *hash; + if (!data || !from || !to) + { + return; + } + + database = data->db; + hash = SubscriptionHash(from, to); + ref = DbCreate(database, 2, "subs", hash); + if (!ref) + { + goto end; + } + + HashMapSet(DbJson(ref), "from", JsonValueString(from)); + HashMapSet(DbJson(ref), "to", JsonValueString(to)); + /* I don't think we need more information right now */ + +end: + DbUnlock(database, ref); + Free(hash); +} + +bool +IsSubscribed(ParseeData *data, char *user, char *to) +{ + Db *database; + char *hash; + bool ret; + if (!data || !user || !to) + { + return false; + } + + database = data->db; + hash = SubscriptionHash(user, to); + ret = DbExists(database, 2, "subs", hash); + Free(hash); + + return ret; +} + +void +ParseeBroadcastStanza(ParseeData *data, char *from, XMLElement *stanza) +{ + XMPPComponent *jabber = data ? data->jabber : NULL; + Array *entries; + size_t i; + if (!data || !from || !stanza) + { + return; + } + + /* Copy our stanza so that we can freely modify it */ + stanza = XMLCopy(stanza); + + /* Start doing a storm on Mt. Subs. */ + entries = DbList(data->db, 1, "subs"); + for (i = 0; i < ArraySize(entries); i++) + { + char *entry = ArrayGet(entries, i); + char *entry_from = NULL, *entry_to = NULL; + char *storm_id; /* ooe */ + XMLElement *sub; + + DecodeSubscription(data, entry, &entry_from, &entry_to); + + if (!StrEquals(entry_to, from)) + { + goto end; + } + + Log(LOG_DEBUG, + "PRESENCE SYSTEM: " + "We should be brotkasting straight to %s (from %s)", + entry_from, from + ); + sub = XMLCopy(stanza); + XMLAddAttr(sub, "from", from); + XMLAddAttr(sub, "to", entry_from); + + /* TODO: Should we store IDs somewhere? */ + XMLAddAttr(sub, "id", (storm_id = StrRandom(16))); + + XMPPSendStanza(jabber, sub, data->config->max_stanza_size); + XMLFreeElement(sub); + Free(storm_id); + end: + Free(entry_from); + Free(entry_to); + } + DbListFree(entries); + XMLFreeElement(stanza); + +} diff --git a/src/XMPPThread/ReListener.c b/src/XMPPThread/ReListener.c index 5d410f6..44f22f8 100644 --- a/src/XMPPThread/ReListener.c +++ b/src/XMPPThread/ReListener.c @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -57,6 +56,8 @@ XMPPDispatcher(void *argp) if (!stanza) { + /* TODO: We shouldn't be busywaiting. Even with a sleep call. + */ UtilSleepMillis(10); continue; } @@ -66,9 +67,15 @@ XMPPDispatcher(void *argp) Log(LOG_DEBUG, "Received stanza='%s'.", stanza->name); } + if (ManageMUCStanza(args->muc, stanza)) + { + XMLFreeElement(stanza); + continue; + } + if (StrEquals(stanza->name, "presence")) { - PresenceStanza(args, stanza); + PresenceStanza(args, stanza, thread); XMLFreeElement(stanza); continue; } @@ -127,6 +134,49 @@ ParseeCongestion(void) return congestion; } +bool +XMPPCommandFilter(XMPPCommandManager *m, char *id, XMLElement *stanza) +{ + ParseeData *args = XMPPGetManagerCookie(m); + char *trimmed_from; + char *from; + char *chat_id; + bool is_muc; + if (!m || !id || !stanza) + { + return false; + } + + from = HashMapGet(stanza->attrs, "from"); + trimmed_from = ParseeTrimJID(from); + is_muc = !!(chat_id = ParseeGetFromMUCID(args, trimmed_from)); + Free(trimmed_from); + Free(chat_id); +#define XMPP_COMMAND(f,l,n,t,s) \ + if (StrEquals(n, id)) \ + { \ + if (l == XMPPCMD_ALL) \ + { \ + return true; \ + } \ + else if (l == XMPPCMD_MUC) \ + { \ + return is_muc; \ + } \ + else if (l == XMPPCMD_ADMINS) \ + { \ + bool is_admin; \ + trimmed_from = ParseeTrimJID(from); \ + is_admin = ParseeIsAdmin(args, trimmed_from); \ + Free(trimmed_from); \ + return is_admin; \ + } \ + } + XMPPCOMMANDS +#undef XMPP_COMMAND + + return false; +} void * ParseeXMPPThread(void *argp) @@ -134,16 +184,20 @@ ParseeXMPPThread(void *argp) ParseeData *args = argp; XMPPComponent *jabber = args->jabber; XMLElement *stanza = NULL; + HashMap *await_table2; size_t i; + + bool error = false; /* Initialise the await table */ await_table = HashMapCreate(); /* Initialise the managers, and add all handlers. */ info.m = XMPPCreateManager(args); + XMPPManagerSetFilter(info.m, XMPPCommandFilter); { XMPPCommand *cmd; -#define XMPP_COMMAND(f,n,t,s) \ +#define XMPP_COMMAND(f,l,n,t,s) \ cmd = XMPPBasicCmd( \ n, t, f \ ); \ @@ -190,56 +244,95 @@ ParseeXMPPThread(void *argp) } } - while (true) + while (!args->halted) { - char *id; - - stanza = XMLDecode(jabber->stream, false); - if (!stanza) + while (true) { - if (args->verbosity >= PARSEE_VERBOSE_COMICAL) + char *id; + + stanza = XMLDecode(jabber->stream, false); + if (!stanza) { - Log(LOG_DEBUG, "RECEIVED EOF."); + /* Try to check if an error is abound */ + if (args->verbosity >= PARSEE_VERBOSE_COMICAL) + { + Log(LOG_DEBUG, "RECEIVED EOF."); + } + break; } - break; - } - if (args->verbosity >= PARSEE_VERBOSE_STANZA) - { - Stream *output = StreamStderr(); - StreamPrintf(output, "-------STANZA BEGIN-------" "\n"); - XMLEncode(output, stanza); - StreamPrintf(output, "\n--------STANZA END--------" "\n"); - StreamFlush(output); - } - - id = HashMapGet(stanza->attrs, "id"); - if (id) - { - XMPPAwait *await; - /* Lock out the table to see if we're awaiting. */ - pthread_mutex_lock(&await_lock); - if ((await = HashMapGet(await_table, id))) + if (args->verbosity >= PARSEE_VERBOSE_STANZA) { - pthread_mutex_lock(&await->cond_lock); - await->stanza = stanza; - pthread_cond_signal(&await->condition); - pthread_mutex_unlock(&await->cond_lock); + Stream *output = StreamStderr(); + StreamPrintf(output, "-------STANZA BEGIN-------" "\n"); + XMLEncode(output, stanza); + StreamPrintf(output, "\n--------STANZA END--------" "\n"); + StreamFlush(output); + } - HashMapDelete(await_table, id); + id = HashMapGet(stanza->attrs, "id"); + if (id) + { + XMPPAwait *await; + /* Lock out the table to see if we're awaiting. */ + pthread_mutex_lock(&await_lock); + if ((await = HashMapGet(await_table, id))) + { + pthread_mutex_lock(&await->cond_lock); + await->stanza = stanza; + pthread_cond_signal(&await->condition); + pthread_mutex_unlock(&await->cond_lock); + HashMapDelete(await_table, id); + + pthread_mutex_unlock(&await_lock); + continue; + } pthread_mutex_unlock(&await_lock); - continue; } - pthread_mutex_unlock(&await_lock); - } - /* Push it into the stanza FIFO. A dispatcher thread should then - * be able to freely grab a value(locked by a mutex). We can't make - * dispatchers read stanzas on their own, since that's _not_ how - * streams work, but this should mitigate some issues, and allow a - * few threads to be busy, while the rest of Parsee works. */ - PushStanza(&info, stanza); + /* Push it into the stanza FIFO. A dispatcher thread should then + * be able to freely grab a value(locked by a mutex). We can't make + * dispatchers read stanzas on their own, since that's _not_ how + * streams work, but this should mitigate some issues, and allow a + * few threads to be busy, while the rest of Parsee works. */ + PushStanza(&info, stanza); + } + pthread_mutex_lock(&args->halt_lock); + if (!args->halted) + { + Log(LOG_WARNING, "XMPP server is closing stream..."); + for (size_t i = 0; i < 50; i++) + { + UtilSleepMillis(100); /* Wait a bit so that temporary failures don't fuck everything up */ + + Log(LOG_WARNING, "Restarting XMPP stream."); + /* This is the part where a new connection is being considered */ + XMPPFinishCompStream(jabber); + XMPPEndCompStream(jabber); + + args->jabber = XMPPInitialiseCompStream( + args->config->component_addr, + args->config->component_host, + args->config->component_port + ); + jabber = args->jabber; + if (!jabber || !XMPPAuthenticateCompStream(jabber, args->config->shared_comp_secret)) + { + /* Oops, there is something wrong! */ + Log(LOG_ERR, "Couldn't authenticate to XMPP server"); + XMPPEndCompStream(jabber); + args->jabber = NULL; + jabber = NULL; + if (i == 4) + { + pthread_mutex_unlock(&args->halt_lock); + break; + } + } + } + } + pthread_mutex_unlock(&args->halt_lock); } info.running = false; @@ -268,12 +361,18 @@ ParseeXMPPThread(void *argp) } ArrayFree(info.stanzas); - HashMapFree(await_table); + await_table2 = await_table; + await_table = NULL; + HashMapFree(await_table2); pthread_mutex_destroy(&info.lock); DestroyPEPManager(info.pep_manager); XMPPFreeManager(info.m); + if (error) + { + HttpServerStop(args->server); /* minor trolling */ + } return NULL; } diff --git a/src/XMPPThread/Stanzas/IQ.c b/src/XMPPThread/Stanzas/IQ.c index ebfd84e..1b5087d 100644 --- a/src/XMPPThread/Stanzas/IQ.c +++ b/src/XMPPThread/Stanzas/IQ.c @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -32,7 +33,7 @@ TrimBase64(char *b64) while (*b64) { char ch[2] = { *b64, 0 }; - if (isspace(*b64)) + if (isspace((int) *b64)) { b64++; continue; @@ -46,35 +47,106 @@ TrimBase64(char *b64) return ret; } +static bool +AvatarGrab(ParseeData *data, char *mxc, char **mime, char **b64, size_t *len) +{ + char *mimei = NULL, *outi = NULL, *b64i = NULL; + size_t sizei; + if (!data || !mxc || !mime || !b64 || !len) + { + return false; + } + + if (!ASGrab(data->config, mxc, &mimei, &outi, &sizei)) + { + Free(mimei); + Free(outi); + return false; + } + + b64i = Base64Encode(outi, sizei); + Free(outi); + if (!b64i) + { + Free(mimei); + b64i = StrDuplicate(media_unknown); /* TODO: Different assets! */ + mimei = StrDuplicate("image/png"); + } + + *mime = mimei; + *b64 = b64i; + *len = sizei; + return true; +} + +XMLElement * +GenerateAvatarData(ParseeData *data, char *mxid) +{ + char *mxc = NULL, *mime = NULL, *b64 = NULL; + XMLElement *elem = NULL, *type, *binval; + size_t len = 0; + if (!data || !mxid) + { + return NULL; + } + + /* TODO: Use the right room for the avatar! */ + mxc = ASGetAvatar(data->config, NULL, mxid); + if (!mxc || !AvatarGrab(data, mxc, &mime, &b64, &len)) + { + goto end; + } + + elem = XMLCreateTag("PHOTO"); + type = XMLCreateTag("TYPE"); + binval = XMLCreateTag("BINVAL"); + + XMLAddChild(type, XMLCreateText(mime)); + XMLAddChild(binval, XMLCreateText(b64)); + + XMLAddChild(elem, type); + XMLAddChild(elem, binval); + +end: + Free(mime); + Free(mxc); + Free(b64); + return elem; +} + #define DISCO "http://jabber.org/protocol/disco#info" static XMLElement * -IQGenerateQuery(void) +IQGenerateQuery(IQFeatures *features) { - XMLElement *query = XMLCreateTag("query"); + XMLElement *query; + if (!features) + { + return NULL; + } + query = XMLCreateTag("query"); XMLAddAttr(query, "xmlns", DISCO); { XMLElement *feature; -#define IdentitySimple(c,t,n) do \ - { \ - feature = XMLCreateTag("identity"); \ - XMLAddAttr(feature, "category", c); \ - XMLAddAttr(feature, "type", t); \ - XMLAddAttr(feature, "name", n); \ - XMLAddChild(query, feature); \ - } \ - while (0); - IQ_IDENTITY -#undef IdentitySimple -#define AdvertiseSimple(f) do \ - { \ - feature = XMLCreateTag("feature"); \ - XMLAddAttr(feature, "var", f); \ - XMLAddChild(query, feature); \ - } \ - while (0); + size_t i; + for (i = 0; i < ArraySize(features->identity); i++) + { + XMPPIdentity *identity = ArrayGet(features->identity, i); - IQ_ADVERT -#undef AdvertiseSimple + feature = XMLCreateTag("identity"); + XMLAddAttr(feature, "category", identity->category); + XMLAddAttr(feature, "type", identity->type); + XMLAddAttr(feature, "name", identity->name); + + XMLAddChild(query, feature); + } + for (i = 0; i < ArraySize(features->adverts); i++) + { + char *var = ArrayGet(features->adverts, i); + + feature = XMLCreateTag("feature"); + XMLAddAttr(feature, "var", var); + XMLAddChild(query, feature); + } } return query; @@ -84,6 +156,7 @@ IQDiscoGet(ParseeData *args, XMPPComponent *jabber, XMLElement *stanza) { char *from, *to, *id; XMLElement *iq_reply, *query; + IQFeatures *features; from = HashMapGet(stanza->attrs, "from"); to = HashMapGet(stanza->attrs, "to"); @@ -96,9 +169,10 @@ IQDiscoGet(ParseeData *args, XMPPComponent *jabber, XMLElement *stanza) XMLAddAttr(iq_reply, "type", "result"); XMLAddAttr(iq_reply, "id", id); - query = IQGenerateQuery(); + features = LookupJIDFeatures(to); + query = IQGenerateQuery(features); { - char *ver = XMPPGenerateVer(); + char *ver = XMPPGenerateVer(features); char *node = StrConcat(3, REPOSITORY, "#", ver); XMLAddAttr(query, "node", node); @@ -106,13 +180,12 @@ IQDiscoGet(ParseeData *args, XMPPComponent *jabber, XMLElement *stanza) Free(ver); } XMLAddChild(iq_reply, query); + FreeIQFeatures(features); - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, iq_reply); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); - + XMPPSendStanza(jabber, iq_reply, args->config->max_stanza_size); XMLFreeElement(iq_reply); + + (void) args; } void @@ -301,7 +374,7 @@ IQResult(ParseeData *args, XMLElement *stanza, XMPPThread *thr) bool IQIsCommandList(ParseeData *args, XMLElement *stanza) { - char *parsee = NULL; + char *parsee = NULL, *to; XMLElement *query = XMLookForTKV( stanza, "query", "xmlns", "http://jabber.org/protocol/disco#items" @@ -316,15 +389,27 @@ IQIsCommandList(ParseeData *args, XMLElement *stanza) } parsee = ParseeJID(args); - ret = StrEquals(HashMapGet(stanza->attrs, "to"), parsee); + to = HashMapGet(stanza->attrs, "to"); + ret = StrEquals(to, parsee) || StrEquals(to, args->config->component_host); Free(parsee); return ret; } +static bool +IsInMUC(ParseeData *data, char *jid) +{ + char *trimmed = ParseeTrimJID(jid); + char *chat_id = ParseeGetFromMUCID(data, trimmed); + + Free(trimmed); + Free(chat_id); + return !!chat_id; +} void IQGet(ParseeData *args, XMLElement *stanza, XMPPThread *thr) { XMPPComponent *jabber = args->jabber; + XMLElement *pubsub; char *from = HashMapGet(stanza->attrs, "from"); char *to = HashMapGet(stanza->attrs, "to"); char *id = HashMapGet(stanza->attrs, "id"); @@ -334,33 +419,38 @@ IQGet(ParseeData *args, XMLElement *stanza, XMPPThread *thr) if (IQIsCommandList(args, stanza)) { XMLElement *iq_reply = XMLCreateTag("iq"); + char *trimmed = ParseeTrimJID(from); + XMLAddAttr(iq_reply, "type", "result"); XMLAddAttr(iq_reply, "from", to); XMLAddAttr(iq_reply, "to", from); XMLAddAttr(iq_reply, "id", id); { XMLElement *q = XMLCreateTag("query"); + char *parsee_muc_jid = StrConcat(3, trimmed, "/", "parsee"); XMLAddAttr(q, "xmlns", "http://jabber.org/protocol/disco#items"); XMLAddAttr(q, "node", "http://jabber.org/protocol/commands"); - XMPPShoveCommandList(thr->info->m, to, q); + XMPPShoveCommandList(thr->info->m, + IsInMUC(args, from) ? parsee_muc_jid : to, + q, stanza + ); XMLAddChild(iq_reply, q); + Free(parsee_muc_jid); } - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, iq_reply); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); + XMPPSendStanza(jabber, iq_reply, args->config->max_stanza_size); XMLFreeElement(iq_reply); + Free(trimmed); } else if (XMLookForTKV(stanza, "vCard", "xmlns", "vcard-temp")) { - Log(LOG_INFO, "vCard information GET for %s", to); + char *to_matrix = ParseeGetBridgedUser(args, stanza); + char *name = ASGetName(args->config, NULL, to_matrix); + XMLElement *iqVCard; + Log(LOG_DEBUG, "vCard information GET for %s (%s)", to, to_matrix); - /* TODO: "a compliant server MUST respond on behalf of the - * requestor and not forward the IQ to the requestee's - * connected resource". */ if (!strncmp(to, "parsee@", 7)) { - XMLElement *iqVCard = XMLCreateTag("iq"); + iqVCard = XMLCreateTag("iq"); XMLAddAttr(iqVCard, "from", to); XMLAddAttr(iqVCard, "to", from); XMLAddAttr(iqVCard, "id", id); @@ -386,13 +476,97 @@ IQGet(ParseeData *args, XMLElement *stanza, XMPPThread *thr) XMLAddChild(iqVCard, vCard); } - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, iqVCard); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); + XMPPSendStanza(jabber, iqVCard, args->config->max_stanza_size); XMLFreeElement(iqVCard); + Free(to_matrix); + Free(name); + return; + } + + Free(to_matrix); + to_matrix = ParseeDecodeMXID(to); + + iqVCard = XMLCreateTag("iq"); + XMLAddAttr(iqVCard, "from", to); + XMLAddAttr(iqVCard, "to", from); + XMLAddAttr(iqVCard, "id", id); + XMLAddAttr(iqVCard, "type", "result"); + { + XMLElement *vCard = XMLCreateTag("vCard"); + char *mto_link = ParseeGenerateMTO(to_matrix); + XMLAddAttr(vCard, "xmlns", "vcard-temp"); + { + XMLAddChild(vCard, GenerateAvatarData(args, to_matrix)); + + Free(mto_link); + } + XMLAddChild(iqVCard, vCard); + } + + XMPPSendStanza(jabber, iqVCard, args->config->max_stanza_size); + XMLFreeElement(iqVCard); + Free(to_matrix); + Free(name); + } +#define PS "http://jabber.org/protocol/pubsub" + else if ((pubsub = XMLookForTKV(stanza, "pubsub", "xmlns", PS))) + { + /* TODO: Pass this through the PEP manager */ + XMLElement *a_items = XMLookForTKV(pubsub, + "items", "node", "urn:xmpp:avatar:data" + ); + if (a_items) + { + /* Do, without regret, start shoving an avatar out the bus. + * NOTE: I explicitely choose to not do any manipulation + * because messing with random user images is inherently a + * risk I do *not* want to take. */ + char *to_matrix = ParseeDecodeMXID(to); + char *avatar = ASGetAvatar(args->config, NULL, to_matrix); + char *mime = NULL; + char *b64 = NULL; + size_t len = 0; + XMLElement *reply; + + AvatarGrab(args, avatar, &mime, &b64, &len); + + Log(LOG_DEBUG, "IQ-GET: PUBSUB AVATAR OF=%s", to_matrix); + /* Strike back with a response */ + reply = XMLCreateTag("iq"); + XMLAddAttr(reply, "type", "result"); + XMLAddAttr(reply, "to", from); + XMLAddAttr(reply, "from", to); + XMLAddAttr(reply, "id", HashMapGet(stanza->attrs, "id")); + { + XMLElement *ps = XMLCreateTag("pubsub"); + XMLElement *items = XMLCreateTag("items"); + XMLAddAttr(ps, "xmlns", PS); + XMLAddAttr(items, "node", "urn:xmpp:avatar:data"); + { + XMLElement *item = XMLCreateTag("item"); + XMLElement *data = XMLCreateTag("data"); + XMLAddAttr(item, "id", "TODO"); + XMLAddAttr(data, "xmlns", "urn:xmpp:avatar:data"); + + XMLAddChild(data, XMLCreateText(b64)); + + XMLAddChild(item, data); + XMLAddChild(items, item); + } + XMLAddChild(ps, items); + XMLAddChild(reply, ps); + } + + XMPPSendStanza(jabber, reply, args->config->max_stanza_size); + XMLFreeElement(reply); + + Free(to_matrix); + Free(avatar); + Free(mime); + Free(b64); } } +#undef PS else if (XMLookForTKV(stanza, "query", "xmlns", DISCO)) { IQDiscoGet(args, jabber, stanza); @@ -400,7 +574,7 @@ IQGet(ParseeData *args, XMLElement *stanza, XMPPThread *thr) else if (XMLookForTKV(stanza, "query", "xmlns", "jabber:iq:version")) { XMLElement *iq_reply, *query; - XMLElement *name, *version; + XMLElement *name, *version, *os; iq_reply = XMLCreateTag("iq"); XMLAddAttr(iq_reply, "to", from); @@ -411,28 +585,34 @@ IQGet(ParseeData *args, XMLElement *stanza, XMPPThread *thr) query = XMLCreateTag("query"); XMLAddAttr(query, "xmlns", "jabber:iq:version"); { + struct utsname info; name = XMLCreateTag("name"); version = XMLCreateTag("version"); + os = XMLCreateTag("os"); + uname(&info); XMLAddChild(name, XMLCreateText(NAME)); XMLAddChild(version, XMLCreateText(VERSION "[" CODE "]")); + XMLAddChild(os, XMLCreateText(info.sysname)); } XMLAddChild(query, name); XMLAddChild(query, version); + XMLAddChild(query, os); XMLAddChild(iq_reply, query); - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, iq_reply); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); + XMPPSendStanza(jabber, iq_reply, args->config->max_stanza_size); XMLFreeElement(iq_reply); } else { + char *buf = NULL; + Stream *s = StrStreamWriter(&buf); Log(LOG_WARNING, "Unknown I/Q received:"); - XMLEncode(StreamStdout(), stanza); - StreamPrintf(StreamStdout(),"\n"); - StreamFlush(StreamStdout()); + XMLEncode(s, stanza); + StreamFlush(s); + StreamClose(s); + Log(LOG_WARNING, "%s", buf); + Free(buf); } } @@ -440,6 +620,9 @@ void IQError(ParseeData *args, XMLElement *stanza, XMPPThread *thr) { /* TODO */ + (void) args; + (void) stanza; + (void) thr; } void IQSet(ParseeData *args, XMLElement *stanza, XMPPThread *thr) diff --git a/src/XMPPThread/Stanzas/Message.c b/src/XMPPThread/Stanzas/Message.c index 75722e5..13021df 100644 --- a/src/XMPPThread/Stanzas/Message.c +++ b/src/XMPPThread/Stanzas/Message.c @@ -1,14 +1,82 @@ #include "XMPPThread/internal.h" #include +#include #include #include +#include #include #include #include +static void +LazyRegister(ParseeData *data, char *mxid, char *name) +{ + DbRef *ref; + char *hash = ParseeHMACS(data->id, mxid); + char *dbname; + if (!(ref = DbLock(data->db, 2, "users", hash))) + { + ASRegisterUser(data->config, mxid); + ref = DbCreate(data->db, 2, "users", hash); + if (ref) + { + HashMapSet(DbJson(ref), "mxid", JsonValueString(mxid)); + HashMapSet(DbJson(ref), + "ts", JsonValueInteger(UtilTsMillis()) + ); + } + } + dbname = GrabString(DbJson(ref), 1, "name"); + if (name && !StrEquals(dbname, name)) + { + ASSetName(data->config, mxid, name); + if (ref) + { + JsonValueFree(HashMapSet( + DbJson(ref), "name", JsonValueString(name) + )); + } + } + DbUnlock(data->db, ref); + Free(hash); +} +static char * +LazySend(ParseeData *args, char *mxid, char *mroom_id, char *type, HashMap *ev, uint64_t ts) +{ + HashMap *duplicate; + char *event; + if (!args || !mxid || !mroom_id || !ev) + { + return NULL; + } + if (!type) + { + type = "m.room.message"; + } + + duplicate = JsonDuplicate(ev); + event = ASSend( + args->config, mroom_id, mxid, + type, ev, ts + ); + if (event) + { + JsonFree(duplicate); + return event; + } + + ASInvite(args->config, mroom_id, mxid); + Free(ASJoin(args->config, mroom_id, mxid)); + + return ASSend( + args->config, mroom_id, mxid, + type, duplicate, ts + ); +} + static void ProcessChatstates(ParseeData *args, XMLElement *stanza) { @@ -44,7 +112,24 @@ ProcessChatstates(ParseeData *args, XMLElement *stanza) from_matrix = NULL; } if (XMLookForTKV(stanza, "paused", "xmlns", CHAT_STATES) || - XMLookForTKV(stanza, "received", "xmlns", "urn:xmpp:receipts") || + XMLookForTKV(stanza, "inactive", "xmlns", CHAT_STATES)) + { + char *latest = NULL; + from_matrix = ParseeGetBridgedUser(args, stanza); + mroom_id = ParseeGetBridgedRoom(args, stanza); + + latest = ParseeLookupHead(mroom_id); + + ASType(args->config, from_matrix, mroom_id, false); + ASPresence(args->config, from_matrix, mroom_id, latest); + + Free(from_matrix); + Free(latest); + Free(mroom_id); + mroom_id = NULL; + from_matrix = NULL; + } + if (XMLookForTKV(stanza, "received", "xmlns", "urn:xmpp:receipts") || XMLookForTKV(stanza, "displayed", "xmlns", "urn:xmpp:chat-markers:0")) { /* TODO: Use stanza ID if possible */ @@ -65,6 +150,16 @@ ProcessChatstates(ParseeData *args, XMLElement *stanza) } #undef CHAT_STATES } + +static float +TimeElapsed(uint64_t *rectime, uint64_t v) +{ + uint64_t time = UtilTsMillis(); + float t = ((time-v)/1000.f); + *rectime = time; + + return t; +} bool MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr) { @@ -83,18 +178,29 @@ MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr) char *type = HashMapGet(stanza->attrs, "type"); bool chat = StrEquals(type, "chat"); size_t i; + uint64_t time, rectime; +#define Elapsed(v) (TimeElapsed(&rectime, v)) + to = NULL; from = NULL; decode_from = NULL; from_matrix = NULL; Log(LOG_DEBUG, " usage=%d", MemoryAllocated()); + time = UtilTsMillis(); + rectime = time; + from = HashMapGet(stanza->attrs, "from"); if (ParseeManageBan(args, from, NULL)) { XMLFreeElement(stanza); - Log(LOG_DEBUG, " usage=%d (%s:%d)", MemoryAllocated(), __FILE__, __LINE__); + Log(LOG_DEBUG, + " time=%f " + "usage=%d (%s:%d)", + Elapsed(time), + MemoryAllocated(), __FILE__, __LINE__ + ); return false; } @@ -107,18 +213,25 @@ MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr) char *occ_id = occupant ? HashMapGet(occupant->attrs, "id") : NULL; if (occ_id) { - Log(LOG_DEBUG, - "'%s' has support for XEP-421, fetching OID=%s", - from, occ_id - ); + if (args->verbosity >= PARSEE_VERBOSE_COMICAL) + { + Log(LOG_DEBUG, + "'%s' has support for XEP-421, fetching OID=%s", + from, occ_id + ); + } ParseePushOIDTable(from, occ_id); } } + if (args->verbosity >= PARSEE_VERBOSE_TIMINGS) + { + Log(LOG_DEBUG, "XEP-421: %fs", Elapsed(rectime)); + } if (StrEquals(type, "error")) { - char *type, *text, *user, *parsee; - char *message, *room; + char *type = NULL, *text = NULL, *user = NULL, *parsee = NULL; + char *message = NULL, *room = NULL; from = HashMapGet(stanza->attrs, "from"); to = HashMapGet(stanza->attrs, "to"); @@ -139,10 +252,10 @@ MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr) message = StrConcat(3, type, ": ", text); room = ParseeGetBridgedRoom(args, stanza); - Free(ASSend( - args->config, room, parsee, - "m.room.message", - MatrixCreateNotice(message) + Free(LazySend( + args, parsee, room, NULL, + MatrixCreateNotice(message), + time )); end_error: @@ -151,11 +264,20 @@ end_error: Free(parsee); Free(room); Free(user); - Log(LOG_DEBUG, " usage=%d (%s:%d)", MemoryAllocated(), __FILE__, __LINE__); + Log(LOG_DEBUG, + " time=%f " + "usage=%d (%s:%d)", + Elapsed(time), + MemoryAllocated(), __FILE__, __LINE__ + ); return false; } + if (args->verbosity >= PARSEE_VERBOSE_TIMINGS) + { + Log(LOG_DEBUG, "Error management: %fs", Elapsed(rectime)); + } - if (moderated) + if (moderated && !(!chat && strncmp(HashMapGet(stanza->attrs, "to"), "parsee@", 7))) { /* TODO: Parsee MUST check if it is a valid MUC */ char *resource = ParseeGetResource(from); @@ -181,29 +303,39 @@ end_error: body = XMLookForUnique(stanza, "body"); PEPManagerHandle(thr->info->pep_manager, stanza); - - ProcessChatstates(args, stanza); + if (args->verbosity >= PARSEE_VERBOSE_TIMINGS) + { + Log(LOG_DEBUG, "PEP management: %fs", Elapsed(rectime)); + } to = ParseeDecodeMXID(HashMapGet(stanza->attrs, "to")); decode_from = ParseeLookupJID(from); from_matrix = ParseeEncodeJID(args->config, decode_from, true); room = ParseeFindDMRoom(args, to, from); data = body ? ArrayGet(body->children, 0) : NULL; + if (args->verbosity >= PARSEE_VERBOSE_TIMINGS) + { + Log(LOG_DEBUG, "Fetching user info: %fs", Elapsed(rectime)); + } /* TODO: CLEAN THAT UP INTO A CREATEDM FUNCTION */ mroom_id = ParseeGetBridgedRoom(args, stanza); Log(LOG_DEBUG, "Bridging event to '%s'...", mroom_id); + if (args->verbosity >= PARSEE_VERBOSE_TIMINGS) + { + Log(LOG_DEBUG, "Fetching bridge: %fs", Elapsed(rectime)); + } + char *trim = ParseeTrimJID(from); if (!mroom_id && !room && !XMPPIsParseeStanza(stanza) && - to && *to == '@') + to && *to == '@' && !XMPPQueryMUC(jabber, trim, NULL)) { DbRef *room_ref; HashMap *room_json; char *from = HashMapGet(stanza->attrs, "from"); - ASRegisterUser(args->config, from_matrix); + LazyRegister(args, from_matrix, NULL); room = ASCreateDM(args->config, from_matrix, to); mroom_id = StrDuplicate(room); - Log(LOG_INFO, "Creating a DM to '%s'(%s)...", to, mroom_id); if (room) { room_ref = DbCreate(args->db, 3, "rooms", room, "data"); @@ -214,12 +346,15 @@ end_error: ParseePushDMRoom(args, to, from, room); } } + Free(trim); /* TODO: THIS IS A HACK. THIS CODE IGNORES ALL MUC MESSAGES EVER * SENT TO NON-PARSEE PUPPETS, AS A "FIX" TO THE MULTITHREADED * ISSUE. * - * I HATE THIS. I NEED TO FIND A BETTER WAY. */ + * I HATE THIS. I NEED TO FIND A BETTER WAY. + * + * Actually, is it even _that_ bad? */ if (!chat && strncmp(HashMapGet(stanza->attrs, "to"), "parsee@", 7)) { goto end; @@ -238,20 +373,18 @@ end_error: ps = CreatePubsubRequest( parsee, decode_from, "urn:xmpp:avatar:metadata" ); - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, ps); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); + XMPPSendStanza(jabber, ps, args->config->max_stanza_size); XMLFreeElement(ps); ps = CreatePubsubRequest( parsee, trim, "urn:xmpp:avatar:metadata" ); - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, ps); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); + XMPPSendStanza(jabber, ps, args->config->max_stanza_size); XMLFreeElement(ps); + if (args->verbosity >= PARSEE_VERBOSE_TIMINGS) + { + Log(LOG_DEBUG, "Subscription to XEP-0084: %fs", Elapsed(rectime)); + } Free(parsee); Free(trim); @@ -260,15 +393,26 @@ end_error: if (ParseeVerifyAllStanza(args, stanza) && !replaced) { XMLElement *oob, *oob_data; - + char *chat_id = ParseeGetFromMUCID(args, from); + bool media_enabled = chat_id ? + ParseeIsMediaEnabled(args, chat_id) : + true ; + bool is_parsee; + /* Note that chat_id still has meaningful info as a boolean + * despite being freed */ + Free(chat_id); pthread_mutex_unlock(&thr->info->chk_lock); - ASRegisterUser(args->config, encoded); - if (!chat) + + parsee = ParseeJID(args); + is_parsee = StrEquals(to, parsee); + Free(parsee); + parsee = NULL; + + LazyRegister(args, encoded, !chat ? res : NULL); + if (args->verbosity >= PARSEE_VERBOSE_TIMINGS) { - ASSetName(args->config, encoded, res); + Log(LOG_DEBUG, "Matrix registration: %fs", Elapsed(rectime)); } - ASInvite(args->config, mroom_id, encoded); - Free(ASJoin(args->config, mroom_id, encoded)); reactions = XMLookForTKV(stanza, "reactions", "xmlns", "urn:xmpp:reactions:0" @@ -276,7 +420,12 @@ end_error: /* Check if it is a media link */ oob = XMLookForTKV(stanza, "x", "xmlns", "jabber:x:oob"); - if (oob && data) + if (chat_id && StrEquals(type, "chat") && !is_parsee) + { + /* Very clearly a MUC DM. */ + event_id = NULL; + } + else if (oob && data && media_enabled) { char *mxc, *mime = NULL; HashMap *content = NULL; @@ -286,24 +435,27 @@ end_error: if (oob_data) { + FileInfo *info = FileInfoFromXMPP(stanza); mxc = ASReupload(args->config, oob_data->data, &mime); if (mxc) { - content = MatrixCreateMedia(mxc, data->data, mime); + content = MatrixCreateMedia( + mxc, data->data, mime, info + ); - /* Yeah, no, I'm not modifying the media creation code. */ HashMapSet(content, "at.kappach.at.parsee.external", JsonValueString(oob_data->data) ); ShoveStanza(content, stanza); - event_id = ASSend( - args->config, mroom_id, encoded, - "m.room.message", content + event_id = LazySend( + args, encoded, mroom_id, NULL, + content, time ); Free(mxc); } + FileInfoFree(info); Free(mime); } } @@ -340,13 +492,13 @@ end_error: reaction = ArrayGet(react_child, i); react_data = ArrayGet(reaction->children, 0); - Free(ASSend( - args->config, mroom_id, encoded, - "m.reaction", + Free(LazySend( + args, encoded, mroom_id, "m.reaction", ShoveStanza( MatrixCreateReact(event_id, react_data->data), stanza - ) + ), + time )); } Free(event_id); @@ -369,6 +521,15 @@ end_error: * too. Go figure. */ size_t off = reply_to ? ParseeFindDatastart(data->data) : 0; + size_t stanzaoff = XMPPGetReplyOffset(stanza); + if (reply_to && stanzaoff != -1) + { + off = UnistrGetUTFOffset(data->data, stanzaoff); + } + if (data->data && off >= strlen(data->data)) + { + off = 0; + } HashMap *ev = MatrixCreateMessage(data->data + off); if (reply_to) { @@ -377,9 +538,9 @@ end_error: Free(reply_id); } ShoveStanza(ev, stanza); - event_id = ASSend( - args->config, mroom_id, encoded, - "m.room.message", ev + event_id = LazySend( + args, encoded, mroom_id, NULL, + ev, time ); } pthread_mutex_lock(&thr->info->chk_lock); @@ -400,9 +561,9 @@ end_error: ); Log(LOG_DEBUG, "Replacing events in %s(%s)", mroom_id, event_id); - Free(ASSend( - args->config, mroom_id, encoded, - "m.room.message", ev + Free(LazySend( + args, encoded, mroom_id, NULL, + ev, time )); ParseePushAllStanza(args, stanza, event_id); pthread_mutex_unlock(&thr->info->chk_lock); @@ -428,6 +589,12 @@ end_error: } } + ProcessChatstates(args, stanza); + if (args->verbosity >= PARSEE_VERBOSE_TIMINGS) + { + Log(LOG_DEBUG, "Chatstate management: %fs", Elapsed(rectime)); + } + end: Free(mroom_id); mroom_id = NULL; @@ -435,7 +602,12 @@ end: Free(decode_from); Free(room); Free(to); - Log(LOG_DEBUG, " usage=%d (%s:%d)", MemoryAllocated(), __FILE__, __LINE__); + Log(LOG_DEBUG, + " time=%f " + "usage=%d (%s:%d)", + (UtilTsMillis()-time)/1000.f, + MemoryAllocated(), __FILE__, __LINE__ + ); return true; } diff --git a/src/XMPPThread/Stanzas/Presence.c b/src/XMPPThread/Stanzas/Presence.c index 225a361..947fc4b 100644 --- a/src/XMPPThread/Stanzas/Presence.c +++ b/src/XMPPThread/Stanzas/Presence.c @@ -56,8 +56,62 @@ GuessStatus(XMLElement *stanza) } return USER_STATUS_ONLINE; } + +static Array * +GetStatuses(XMLElement *info) +{ + XMLElement *child; + Array *ret; + size_t i; + if (!info) + { + return NULL; + } + + ret = ArrayCreate(); + for (i = 0; i < ArraySize(info->children); i++) + { + child = ArrayGet(info->children, i); + if (StrEquals(child->name, "status")) + { + ArrayAdd(ret, HashMapGet(child->attrs, "code")); + } + } + + return ret; +} + +static void +FreeStatuses(Array *statuses) +{ + if (!statuses) + { + return; + } + + ArrayFree(statuses); +} +static bool +HasStatus(Array *statuses, const char *code) +{ + size_t i; + if (!statuses || !code) + { + return false; + } + + for (i = 0; i < ArraySize(statuses); i++) + { + if (StrEquals(ArrayGet(statuses, i), code)) + { + return true; + } + } + + return false; +} void -PresenceStanza(ParseeData *args, XMLElement *stanza) +PresenceStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr) { #define MUC_USER_NS "http://jabber.org/protocol/muc#user" XMLElement *user_info; @@ -65,6 +119,20 @@ PresenceStanza(ParseeData *args, XMLElement *stanza) XMLElement *status = XMLookForUnique(stanza, "status"); char *oid = HashMapGet(stanza->attrs, "from"); + char *dst = HashMapGet(stanza->attrs, "to"); + char *type = HashMapGet(stanza->attrs, "type"); + + if (StrEquals(type, "subscribe")) + { + Log(LOG_WARNING, "!PRESENCE SUBSCRIPTION REQUEST! (%s:%s)", oid, dst); + AddPresenceSubscriber(args, oid, dst); /* TODO: Send presence updates + * whenever possible. */ + } + + if (PEPManagerHandle(thr->info->pep_manager, stanza)) + { + return; + } if (ServerHasXEP421(args, oid)) { @@ -75,10 +143,6 @@ PresenceStanza(ParseeData *args, XMLElement *stanza) char *occ_id = occupant ? HashMapGet(occupant->attrs, "id") : NULL; if (occ_id) { - Log(LOG_DEBUG, - "'%s' has support for XEP-421, fetching OID=%s", - oid, occ_id - ); ParseePushOIDTable(oid, occ_id); } } @@ -86,27 +150,33 @@ PresenceStanza(ParseeData *args, XMLElement *stanza) if ((user_info = XMLookForTKV(stanza, "x", "xmlns", MUC_USER_NS))) { XMLElement *item = XMLookForUnique(user_info, "item"); - XMLElement *status = XMLookForUnique(user_info, "status"); -#define IsStatus(code) (status && \ - StrEquals(HashMapGet(status->attrs, "code"), #code)) + Array *statuses = GetStatuses(user_info); +#define IsStatus(code) (HasStatus(statuses, #code)) char *jid = item ? HashMapGet(item->attrs, "jid") : NULL; char *trim = ParseeTrimJID(jid); char *from = NULL; - char *type = HashMapGet(stanza->attrs, "type"); char *room = ParseeGetBridgedRoom(args, stanza); - char *decode_from, *real_matrix; + char *decode_from = NULL, *real_matrix = NULL; char *matrix_user_pl = ParseeEncodeJID(args->config, trim, false); - char *affiliation = HashMapGet(item->attrs, "affiliation"); - char *role = HashMapGet(item->attrs, "role"); + char *affiliation = item ? HashMapGet(item->attrs, "affiliation") : NULL; + char *role = item ? HashMapGet(item->attrs, "role") : NULL; int power_level = 0; char *parsee = ParseeMXID(args); + char *parsee_j = ParseeJID(args); + char *muc = ParseeTrimJID(HashMapGet(stanza->attrs, "from")); + char *parsee_muc = StrConcat(3, muc, "/", "parsee"); Free(trim); + if (!item) + { + goto end_item; + } if (jid) { ParseePushJIDTable(oid, jid); } + ParseePushAffiliationTable(oid, affiliation, role); decode_from = ParseeLookupJID(oid); real_matrix = ParseeDecodeMXID(decode_from); @@ -135,22 +205,16 @@ PresenceStanza(ParseeData *args, XMLElement *stanza) if (StrEquals(role, "visitor")) { - char *parsee = ParseeJID(args); - if (!StrEquals(HashMapGet(stanza->attrs, "to"), parsee) && + if (!StrEquals(HashMapGet(stanza->attrs, "to"), parsee_j) && IsStatus(110)) { - char *muc = ParseeTrimJID(HashMapGet(stanza->attrs, "from")); char *usr = HashMapGet(stanza->attrs, "to"); /* Ask for voice in a visitor self-presence. We do not notify * the user, as an error MUST occur, which is handled and * logged out. */ XMPPRequestVoice(args->jabber, usr, muc); - - Free(muc); } - - Free(parsee); } /* Set the user's PL @@ -190,7 +254,7 @@ PresenceStanza(ParseeData *args, XMLElement *stanza) { ASBan(args->config, room, real_matrix); } - else if (IsStatus(307)) + else if (IsStatus(307) && !IsStatus(333)) { ASKick(args->config, room, real_matrix); } @@ -204,16 +268,37 @@ PresenceStanza(ParseeData *args, XMLElement *stanza) ASLeave(args->config, room, real_matrix); } } + if (StrEquals(type, "unavailable") && + (StrEquals(jid, parsee_muc) || StrEquals(jid, parsee_j)) + && IsStatus(301)) + { + char *chat_id = ParseeGetFromRoomID(args, room); + ParseeUnlinkRoom(args, chat_id); + Free(ASSend( + args->config, room, parsee, + "m.room.message", + MatrixCreateNotice("This room has been unlinked."), + 0 + )); + ASLeave(args->config, room, parsee); + Free(chat_id); + } + +end_item: + Free(muc); Free(from); + Free(parsee_muc); Free(decode_from); Free(real_matrix); Free(matrix_user_pl); + Free(parsee_j); Free(parsee); Free(room); + FreeStatuses(statuses); } - if (status) + if (status && !StrEquals(type, "unavailable")) { XMLElement *status_data = ArrayGet(status->children, 0); char *decode_from = ParseeLookupJID(oid); @@ -224,6 +309,8 @@ PresenceStanza(ParseeData *args, XMLElement *stanza) status_str = status_data->data; } + + /* TODO: "The server will automatically set a user's presence to * unavailable if their last active time was over a threshold value * (e.g. 5 minutes)." @@ -299,11 +386,8 @@ PresenceStanza(ParseeData *args, XMLElement *stanza) vcard_request = CreateVCardRequest( from, HashMapGet(stanza->attrs, "from") ); - pthread_mutex_lock(&jabber->write_lock); - XMLEncode(jabber->stream, vcard_request); - StreamFlush(jabber->stream); - pthread_mutex_unlock(&jabber->write_lock); + XMPPSendStanza(jabber, vcard_request, args->config->max_stanza_size); XMLFreeElement(vcard_request); Free(from); } diff --git a/src/XMPPThread/internal.h b/src/XMPPThread/internal.h index d937b8a..efdd300 100644 --- a/src/XMPPThread/internal.h +++ b/src/XMPPThread/internal.h @@ -32,13 +32,17 @@ IdentitySimple("client", "pc", NAME " v" VERSION " bridge") \ IdentitySimple("component", "generic", "Parsee's component") -typedef struct PEPManager PEPManager; -typedef void (*PEPEvent)(PEPManager *m, XMLElement *stanza, XMLElement *item); - +typedef struct IQFeatures { + Array *identity; + Array *adverts; +} IQFeatures; typedef struct XMPPIdentity { char *category, *type, *lang, *name; } XMPPIdentity; +typedef struct PEPManager PEPManager; +typedef void (*PEPEvent)(PEPManager *m, XMLElement *stanza, XMLElement *item); + typedef struct XMPPThread XMPPThread; typedef struct XMPPThreadInfo { /* A FIFO of stanzas inbound, to be read by dispatcher @@ -65,6 +69,29 @@ struct XMPPThread { int ICollate(unsigned char *cata, unsigned char *catb); int IdentitySort(void *idap, void *idbp); +IQFeatures * CreateIQFeatures(void); +void AddIQIdentity(IQFeatures *, char *category, char *lang, char *type, char *name); +void AdvertiseIQFeature(IQFeatures *, char *feature); +void FreeIQFeatures(IQFeatures *); + +/** + * Looks up the features supported by a JID + * ---------------------- + * Returns: an IQ featureset[LA:HEAP] | NULL + * Thrasher: FreeIQFeatures + * Modifies: NOTHING */ +IQFeatures * LookupJIDFeatures(char *jid); + +/** Generate the B64-encoded SHA-256 hash for the 'ver' field in caps. + * -------- + * Returns: A base64 encoded ver hash[LA:HEAP] + * Modifies: NOTHING + * See-Also: https://xmpp.org/extensions/xep-0115.html */ +char * XMPPGenerateVer(IQFeatures *features); + +/* Annotates a presence with https://xmpp.org/extensions/xep-0115.html */ +void XMPPAnnotatePresence(XMLElement *presence, IQFeatures *features); + char * ParseeGetBridgedRoom(ParseeData *data, XMLElement *stanza); char * ParseeGetEventFromID(ParseeData *data, XMLElement *stanza, char *id); char * ParseeGetReactedEvent(ParseeData *data, XMLElement *stanza); @@ -78,7 +105,7 @@ XMLElement * CreatePubsubRequest(char *from, char *to, char *node); bool MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr); void IQStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr); -void PresenceStanza(ParseeData *args, XMLElement *stanza); +void PresenceStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr); bool ServerHasXEP421(ParseeData *data, char *from); @@ -89,6 +116,7 @@ PEPManager * CreatePEPManager(ParseeData *data, void *cookie); void * PEPManagerCookie(PEPManager *manager); void PEPManagerAddEvent(PEPManager *manager, char *node, PEPEvent event); bool PEPManagerHandle(PEPManager *manager, XMLElement *stanza); +void PEPManagerBroadcast(PEPManager *manager, char *as, XMLElement *item); void DestroyPEPManager(PEPManager *manager); /* PEP callbacks for the handler */ @@ -96,3 +124,7 @@ void PEPAvatarEvent(PEPManager *m, XMLElement *stanza, XMLElement *item); void PEPVCardEvent(PEPManager *m, XMLElement *stanza, XMLElement *item); char * ScrambleOID(ParseeData *data, char *opaque_oid); + +/* Presence management */ +void AddPresenceSubscriber(ParseeData *data, char *from, char *to); +bool IsSubscribed(ParseeData *data, char *user, char *to); diff --git a/src/include/AS.h b/src/include/AS.h index 1d993e1..562d172 100644 --- a/src/include/AS.h +++ b/src/include/AS.h @@ -29,7 +29,7 @@ extern void ASAuthenticateRequest(const ParseeConfig *, HttpClientContext *); extern bool ASRegisterUser(const ParseeConfig *, char *); /* Pings the homeserver to get attention. */ -extern void ASPing(const ParseeConfig *); +extern bool ASPing(const ParseeConfig *); /** Joins a room from an {id} and a given {user} we want to masquerade * as. @@ -67,7 +67,8 @@ extern HashMap * ASFind(const ParseeConfig *, char *, char *); /* Sends a message event with a specific type and body. * Said body is freed during the function's execution. */ -extern char * ASSend(const ParseeConfig *, char *, char *, char *, HashMap *); +extern char * +ASSend(const ParseeConfig *, char *, char *, char *, HashMap *, uint64_t ts); extern void ASType(const ParseeConfig *, char *, char *, bool); extern void ASPresence(const ParseeConfig *, char *, char *, char *); @@ -141,6 +142,14 @@ extern void ASSetStatus(const ParseeConfig *c, char *user, UserStatus status, ch * Modifies: NOTHING */ extern char * ASGetName(const ParseeConfig *c, char *room, char *user); +/** Returns the user's avatar in a room, or a the global user avatar, to be + * Free'd + * ------------- + * Returns: The user's name in the [HEAP] | NULL + * Thrasher: Free + * Modifies: NOTHING */ +extern char * ASGetAvatar(const ParseeConfig *c, char *room, char *user); + /** Uploads data to Matrix to be used later * ---------------- * Returns: A valid MXC URI[HEAP] | NULL @@ -170,4 +179,16 @@ extern Array * ASGetRelations(const ParseeConfig *c, size_t n, char *room, char * Thrashes: {relations} * See-Also: ASGetRelations */ extern void ASFreeRelations(Array *relations); + +/** Returns the MIME and SHA-1 hash of a media entry, in one fell swoop. + * ----------------- + * Returns: whenever the media exists + * Modifies: {mime}[HEAP], {sha}[HEAP] */ +extern bool ASGetMIMESHA(const ParseeConfig *c, char *mxc, char **mime, char **sha); + +/** Retrieves media off an MXC link. + * ------------ + * Returns: whenever the media exists + * Modifies {mime}[HEAP], {out}[HEAP], {len} */ +extern bool ASGrab(const ParseeConfig *c, char *mxc, char **mime, char **out, size_t *len); #endif diff --git a/src/include/Bot.h b/src/include/Bot.h index e2c0340..3154a3a 100644 --- a/src/include/Bot.h +++ b/src/include/Bot.h @@ -26,7 +26,7 @@ Free(ASSend( \ data->config, id, profile, \ "m.room.message", \ - MatrixCreateNotice(rep) \ + MatrixCreateNotice(rep), 0 \ )); \ } \ while(0) @@ -45,7 +45,7 @@ Free(ASSend( \ data->config, id, profile, \ "m.room.message", \ - MatrixCreateNotice(formatted) \ + MatrixCreateNotice(formatted), 0 \ )); \ Free(formatted); \ } \ diff --git a/src/include/FileInfo.h b/src/include/FileInfo.h new file mode 100644 index 0000000..3d98c9b --- /dev/null +++ b/src/include/FileInfo.h @@ -0,0 +1,23 @@ +#ifndef PARSEE_FILEINFO_H +#define PARSEE_FILEINFO_H + +#include "XML.h" + +typedef struct FileInfo { + int width; + int height; + int size; +} FileInfo; + +/** Grab file metadata through SIMS. + * ---------------- + * Returns: File information(SIMS)[HEAP] + * Thrasher: FileInfoFree */ +extern FileInfo * FileInfoFromXMPP(XMLElement *stanza); + +/** Frees all information related with file metadata. + * -------------- + * Thrashes: info */ +extern void FileInfoFree(FileInfo *info); + +#endif diff --git a/src/include/MUCServ.h b/src/include/MUCServ.h new file mode 100644 index 0000000..f9a2e4b --- /dev/null +++ b/src/include/MUCServ.h @@ -0,0 +1,39 @@ +#ifndef PARSEE_MUCSERV_H +#define PARSEE_MUCSERV_H + +/*-* An easy interface for handling MUCs. + */ + +typedef struct MUCServer MUCServer; + +#include + +#include + +/** Creates a MUC server handle given a ParseeData handle. + * This handle shall be used for all MUC-related operations. + * ---------------------------------------------------------- + * Returns: a MUC server handle[HEAP] | NULL + * Thrasher: FreeMUCServer + * See-Also: https://xmpp.org/extensions/xep-0045.html */ +extern MUCServer * CreateMUCServer(ParseeData *data); + +/** Manages a stanza from a MUC, and returns true if the stanza + * was actually processed by the MUC server. + * ------------------------------------------------------------- + * See-Also: CreateMUCServer */ +extern bool ManageMUCStanza(MUCServer *serv, XMLElement *stanza); + +/** Checks if a MUC(from its localpart, so + * '#foobar@bridge.blow.hole' -> 'foobar') exists. + * -------------- + * Returns: whenever the MUC exists */ +extern bool MUCServerExists(MUCServer *serv, char *muc); + +/** Destroys all memory and references from a MUC server handle. + * -------------- + * Thrashes: CreateMUCServer + * See-Also: https://xmpp.org/extensions/xep-0045.html */ +extern void FreeMUCServer(MUCServer *serv); + +#endif diff --git a/src/include/Matrix.h b/src/include/Matrix.h index 0ffb7ec..fb0b5f7 100644 --- a/src/include/Matrix.h +++ b/src/include/Matrix.h @@ -2,6 +2,9 @@ #define PARSEE_MATRIX_H #include +#include + +#include "FileInfo.h" /* A simple representation of everything. It is not as complex as * Telodendria's CommonID parser, simply because Parsee does not @@ -18,6 +21,12 @@ typedef struct UserID { * Thrasher: Free */ extern UserID * MatrixParseID(char *user); +/** Attempts to parse a Matrix ID from a matrix.to URL. + * ------------------------------------------------- + * Returns: a valid user ID[HEAP] | NULL + * Thrasher: Free */ +extern UserID * MatrixParseIDFromMTO(Uri *uri); + /* Creates an error message JSON, with the specified code and message. */ extern HashMap * MatrixCreateError(char *err, char *msg); @@ -33,7 +42,7 @@ extern HashMap * MatrixCreateReact(char *event, char *body); extern HashMap * MatrixCreateReplace(char *event, char *body); /* Creates the content for a media file. */ -extern HashMap * MatrixCreateMedia(char *mxc, char *body, char *mime); +extern HashMap * MatrixCreateMedia(char *mxc, char *body, char *mime, FileInfo *info); /* Creates the content for a m.room.name state event */ extern HashMap * MatrixCreateNameState(char *name); diff --git a/src/include/Parsee.h b/src/include/Parsee.h index e19fd42..4b1f7aa 100644 --- a/src/include/Parsee.h +++ b/src/include/Parsee.h @@ -18,9 +18,12 @@ typedef struct ParseeData ParseeData; #include #include +#include + #define PARSEE_VERBOSE_NONE 0 #define PARSEE_VERBOSE_LOG 1 -#define PARSEE_VERBOSE_STANZA 2 +#define PARSEE_VERBOSE_TIMINGS 2 +#define PARSEE_VERBOSE_STANZA 3 #define PARSEE_VERBOSE_COMICAL 53 /* MINUTES */ typedef struct ParseeConfig { @@ -38,12 +41,18 @@ typedef struct ParseeConfig { /* Homeserver port info */ char *homeserver_host; int homeserver_port; + int homeserver_tls; + + bool accept_pings; + bool ignore_bots; /* ------- JABBER -------- */ + char *component_addr; char *component_host; char *shared_comp_secret; int component_port; + size_t max_stanza_size; /* ------- DB -------- */ char *db_path; @@ -58,7 +67,9 @@ typedef struct ParseeData { HttpRouter *router; CommandRouter *handler; + HttpServer *server; XMPPComponent *jabber; + MUCServer *muc; Db *db; char *id; @@ -67,6 +78,10 @@ typedef struct ParseeData { HashMap *oid_servers; pthread_mutex_t oidl; + + /* If Parsee was intentionally halted */ + bool halted; + pthread_mutex_t halt_lock; } ParseeData; typedef struct Argument { @@ -79,6 +94,7 @@ typedef struct Argument { const char *description; } Argument; +/* A few helpful macros to make JSON less of a PITA */ #define GrabString(obj, ...) JsonValueAsString(JsonGet(obj, __VA_ARGS__)) #define GrabInteger(obj, ...) JsonValueAsInteger(JsonGet(obj, __VA_ARGS__)) #define GrabBoolean(obj, ...) JsonValueAsBoolean(JsonGet(obj, __VA_ARGS__)) @@ -99,9 +115,12 @@ typedef struct Argument { /* A base64-encoded Parsee logo */ extern const char media_parsee_logo[]; +/* "Unknown avatar" */ +extern const char media_unknown[]; -/* An ASCII-art rendition of "小橋" */ -#define PARSEE_ASCII_LINES 9 +/* An ASCII-art rendition of "小橋". + * I'm sorry for its quality. If anyone wants to redraw it, feel free. */ +#define PARSEE_ASCII_LINES 8 extern const char *parsee_ascii[PARSEE_ASCII_LINES]; /** @@ -112,6 +131,9 @@ extern void ParseePrintASCII(void); /** * Checks if two versions of Parsee can be considered "compatible". + * This is mainly used for things like database operations. TODO: + * Make an auto-upgrade system to comply with the (undocumented) + * rule of "Don't Make The Sysadmin Think About Parsee Too Much". * --------------- * Modifies: NOTHING */ extern bool ParseeIsCompatible(char *ver1, char *ver2); @@ -262,6 +284,47 @@ extern char * ParseeGetRoomID(ParseeData *, char *chat_id); /* Finds the MUC JID from a chat ID */ extern char * ParseeGetMUCID(ParseeData *, char *chat_id); +/** Fetches a configuration value from a key in a chat(given a Chat ID), + * as a string or NULL. Keys are to be stored like Java packages(reveres DNS). + * Parsee has the right over any key with the 'p.' prefix. + * ----------------------------------- + * Returns: a valid string[HEAP] | NULL + * Modifies: NOTHING + * See-Also: ParseeGetFromMUCID, ParseeGetFromRoomID, ParseeSetChatSetting */ +extern char * +ParseeGetChatSetting(ParseeData *data, char *chat, char *key); + +/** Fetches the entire configuration in a chat(given a Chat ID), as an hashmap + * of strings. + * ----------------------------------- + * Returns: a valid string[HEAP] | NULL + * Modifies: NOTHING + * Thrasher: ParseeFreeChatSettings + * See-Also: ParseeGetFromMUCID, ParseeGetFromRoomID, ParseeSetChatSetting, ParseeGetChatSetting */ +extern HashMap * +ParseeGetChatSettings(ParseeData *data, char *chat); + +/** Destroys memory allocated from a call to {ParseeGetChatSettings}. + * ----------------------- + * Returns: NOTHING + * Modifies: {settings} + * Thrashes: {settings} + * See-Also: ParseeGetChatSettings */ +extern void +ParseeFreeChatSettings(HashMap *settings); + +/** Replaces a configuration key-value pair within the chat's context, which + * can be read with {ParseeGetChatSetting}. + * ------------------------------------- + * Returns: NOTHING + * Modifies: the chat context + * See-Also: ParseeGetFromMUCID, ParseeGetFromRoomID, ParseeGetChatSetting */ +extern void +ParseeSetChatSetting(ParseeData *data, char *chat, char *key, char *val); + +extern bool +ParseeIsMediaEnabled(ParseeData *data, char *chat_id); + /* Pushes a stanza ID to a chat ID */ extern void ParseePushStanza(ParseeData *, char *chat_id, char *stanza_id, char *origin_id, char *event, char *sender); extern void ParseePushDMStanza(ParseeData *, char *room_id, char *stanza_id, char *origin_id, char *event, char *sender); @@ -278,7 +341,11 @@ extern bool ParseeGetDMOrigin(ParseeData *data, char *chat_id, char *ev, char ** /* Sends presence requests for every MUC around as a fake JID */ extern void ParseeSendPresence(ParseeData *); -extern bool ParseeInitialiseSignals(HttpServer *, pthread_t, XMPPComponent *); +/** Initialises signal-handling code within Parsee. + * -------------------- + * Modifies: the signal handler + * Returns: whenever it has properly been setup */ +extern bool ParseeInitialiseSignals(ParseeData *data, pthread_t xmpp); /* Job used to cleanup Parsee data that isn't necessary anymore. */ extern void ParseeCleanup(void *data); @@ -286,9 +353,13 @@ extern void ParseeCleanup(void *data); /* Finds the offset of the first line without a '>' at its start. */ extern int ParseeFindDatastart(char *data); +/* Finds the offset of the first line without a '>' at its start(as + * Unicode codepoints). */ +extern int ParseeFindDatastartU(char *data); + /* XMPP-ifies a message event to XEP-0393 if possible. */ -extern char * ParseeXMPPify(HashMap *event); +extern char * ParseeXMPPify(ParseeData *data, HashMap *event); /* Finds an event ID from an ID in the stanza's attributes */ extern char * ParseeEventFromID(ParseeData *d, char *c_id, char *ori_id); @@ -298,13 +369,18 @@ extern char * ParseeDMEventFromID(ParseeData *d, char *r_id, char *ori_id); extern char * ParseeDMEventFromSID(ParseeData *d, char *r_id, char *ori_id); /* Gets a Parsee "shim" link to an MXC, usable as unauth for a limited time */ -extern char * ParseeToUnauth(ParseeData *data, char *mxc); +extern char * ParseeToUnauth(ParseeData *data, char *mxc, char *filename); extern void ParseeInitialiseOIDTable(void); extern void ParseePushOIDTable(char *muc, char *occupant); extern char *ParseeLookupOID(char *muc); extern void ParseeDestroyOIDTable(void); +extern void ParseeInitialiseAffiliationTable(void); +extern void ParseePushAffiliationTable(char *user, char *affiliation, char *role); +extern bool ParseeLookupAffiliation(char *muc, char **affiliation, char **role); +extern void ParseeDestroyAffiliationTable(void); + extern void ParseeInitialiseJIDTable(void); extern void ParseePushJIDTable(char *muc, char *bare); extern char *ParseeLookupJID(char *muc); @@ -324,10 +400,17 @@ extern void ParseeDestroyNickTable(void); * to ban them from rooms where Parsee has the ability to do so ("noflying"). * --------------- * Returns: NOTHING - * See-Also: ParseeManageBan + * See-Also: ParseeManageBan, ParseeGlobalUnban * Modifies: the database */ extern void ParseeGlobalBan(ParseeData *, char *user, char *reason); +/** Revokes a user/room/MUC's nofly status + * --------------- + * Returns: NOTHING + * See-Also: ParseeManageBan, ParseeGlobalBan + * Modifies: the database */ +extern void ParseeGlobalUnban(ParseeData *, char *user); + /** Verifies if a user was banned globally. If so (and if {room} is set), * tries to ban the user from it. * --------------- @@ -340,7 +423,8 @@ extern bool ParseeManageBan(ParseeData *, char *user, char *room); extern bool ParseeVerifyDMStanza(ParseeData *data, char *room_id, char *id); -/** Checks if a Matrix/XMPP user is considered as "administrator" by Parsee. +/** Checks if a Matrix/XMPP user is considered as "administrator" by + * Parsee. * ---------------------- * Returns: (whenever the user is an admin) * Modifies: NOTHING */ @@ -401,4 +485,22 @@ extern void ParseeSetThreads(int xmpp, int http); extern char * ParseeHMAC(char *key, uint8_t *msg, size_t msglen); #define ParseeHMACS(key, msg) ParseeHMAC(key, (uint8_t *) msg, strlen(msg)) +/** Broadcasts a stanza from a user to any that may be interested by it + * (PEP or subscription) + * ------------------------------------- + * Returns: NOTHING */ +extern void ParseeBroadcastStanza(ParseeData *data, char *from, XMLElement *s); + +/** Destroys the mapping between a MUC and a room from a chat ID. + * ---------------- + * Modifies: the DB's room mappings + * Returns: NOTHING */ +extern void ParseeUnlinkRoom(ParseeData *data, char *chat_id); + +/** Verifies if there is no whitelists OR that a MUC's server is whitelisted. + * ---------------------- + * Returns: if a MUC is to be allowed + * Modifies: NOTHING */ +extern bool ParseeIsMUCWhitelisted(ParseeData *data, char *muc); + #endif diff --git a/src/include/Routes.h b/src/include/Routes.h index 369a0d7..664032b 100644 --- a/src/include/Routes.h +++ b/src/include/Routes.h @@ -27,6 +27,10 @@ typedef struct ParseeCmdArg { "ban-list", CmdNoFlyList, \ "Globally bans a user from using Parsee" \ ) \ + X_COMMAND( \ + "unban-list", CmdNoFlyListDel, \ + "Globally unbans a user from using Parsee" \ + ) \ X_COMMAND( \ "nuke-muc", CmdUnlinkMUC, \ "Removes a MUC. Users should then run " \ @@ -74,7 +78,9 @@ typedef struct ParseeCmdArg { X_ROUTE("/_matrix/app/v1/transactions/(.*)", RouteTxns) \ X_ROUTE("/_matrix/app/v1/users/(.*)", RouteUserAck) \ X_ROUTE("/_matrix/app/v1/rooms/(.*)", RouteRoomAck) \ - X_ROUTE("/_matrix/client/v1/media/download/(.*)/(.*)", RouteMedia) + X_ROUTE("/_matrix/app/v1/ping", RoutePing) \ + X_ROUTE("/media/(.*)/(.*)", RouteMedia) \ + X_ROUTE("/media/(.*)/(.*)/(.*)", RouteMedia) #define X_ROUTE(path, name) extern void * name(Array *, void *); ROUTES diff --git a/src/include/StanzaBuilder.h b/src/include/StanzaBuilder.h index e434e43..18b56bb 100644 --- a/src/include/StanzaBuilder.h +++ b/src/include/StanzaBuilder.h @@ -16,7 +16,7 @@ extern StanzaBuilder * SetStanzaID(StanzaBuilder *builder, char *id); extern StanzaBuilder * SetStanzaXParsee(StanzaBuilder *builder, HashMap *e); extern StanzaBuilder * AddStanzaElem(StanzaBuilder *builder, XMLElement *item); extern XMLElement * ExportStanza(StanzaBuilder *builder); -extern void WriteoutStanza(StanzaBuilder *builder, XMPPComponent *jabber); +extern void WriteoutStanza(StanzaBuilder *builder, XMPPComponent *jabber, size_t max); extern void DestroyStanzaBuilder(StanzaBuilder *builder); #endif diff --git a/src/include/StringStream.h b/src/include/StringStream.h index 007a66b..03a25ab 100644 --- a/src/include/StringStream.h +++ b/src/include/StringStream.h @@ -4,7 +4,7 @@ #include /* Creates a string stream writer. The referenced buffer must be in the heap, - * or NULL. */ + * or NULL for an empty string. */ extern Stream * StrStreamWriter(char **buffer); /* Creates a string stream reader. The referenced buffer may be everywhere. */ diff --git a/src/include/Unistring.h b/src/include/Unistring.h new file mode 100644 index 0000000..7eff7a1 --- /dev/null +++ b/src/include/Unistring.h @@ -0,0 +1,91 @@ +#ifndef PARSEE_UNISTRING_H +#define PARSEE_UNISTRING_H + +/*-*

A basic datastructure to handle Unicode strings easily.

+ *

Mainly used because dealing with UTF-8 directly may be an + * annoyance, and it may be used as a base for Cytoplasm's own + * string management

+ * -------- + * Written-By: LDA + * License: CC0 */ + +#include +#include +#include + +/* An opaque structure for a Unistring */ +typedef struct Unistr Unistr; + +/** Decodes an UTF-8 string into a separate Unistr. + * ------- + * Returns: a valid Unistr[HEAP] | NULL + * Thrasher: UnistrFree */ +extern Unistr * UnistrCreate(char *src); + +/** Returns the length of an unistring. + * ---------- + * Returns: the unistring's length | NULL */ +extern size_t UnistrSize(Unistr *unistr); + +/** Returns the character of an unistring at a location, + * or 0 if it is inaccessible. + * ---------- + * Returns: The Unicode codepoint of a specific 0-index | 0 */ +extern uint32_t UnistrGetch(Unistr *unistr, size_t i); + +/** Adds a singular codepoint to a unistring(IFF not 0 and valid). + * ------------- + * Returns: NOTHING + * Modifies: unistr */ +extern void UnistrAddch(Unistr *unistr, uint32_t u); + +/** Concats N unistrings into a new, separate unistring. + * --------------------- + * Returns: a new unistring[HEAP] + * Modifies: NOTHING + * Thrasher: UnistrFree */ +extern Unistr * UnistrConcat(size_t n, ...); + +/** Encodes a unistring into a C UTF-8 string + * -------------- + * Returns: a valid NULL-terminated string[HEAP] | NULL + * Thrasher: Free */ +extern char * UnistrC(Unistr *unistr); + +/** Destroys all memory associated with a unistring. + * ---------- + * Returns: NOTHING + * Thrashes: {unistr} */ +extern void UnistrFree(Unistr *unistr); + +/** Returns true IFF the character is within the unicode BMP and + * not 0x0000 + * ------------------------------------------------------------ + * Returns: whenever the character is within the BMP */ +extern bool UnistrIsBMP(uint32_t u); + +/** Returns true IFF the character is within the 7-bit ASCII range + * not 0x0000 + * ------------------------------------------------------------ + * Returns: whenever the character is within ASCII */ +extern bool UnistrIsASCII(uint32_t u); + +typedef bool (*UnistrFilterFunc)(uint32_t u); +/** "Filters" characters in a Unistring by codepoint, removing + * those with callbacks which return false into a new unistring. + * -------------------- + * Returns: a new unistring with filtered characters removed */ +extern Unistr * UnistrFilter(Unistr *str, UnistrFilterFunc filter); + +/** Finds the offset of the first line not starting with a specific + * characters in terms of Unicode codepoints. + * -------- + * Returns: an offset of the first line to not start by {c} | 0 */ +extern size_t UnistrGetOffset(Unistr *str, uint32_t sep); + +/** Locates the new index within a regular UTF-8 sequence from an index + * within all codepoints. + * ----------------------------------- + * Returns: an offset */ +extern size_t UnistrGetUTFOffset(char *cstr, size_t unicode); +#endif diff --git a/src/include/XML.h b/src/include/XML.h index 8fd108d..c7b6610 100644 --- a/src/include/XML.h +++ b/src/include/XML.h @@ -13,6 +13,8 @@ typedef struct XMLEvent { XML_LEXER_DATA, XML_LEXER_ELEM, /* Empty attr */ XML_LEXER_ENDELEM, + + XML_ERROR, XML_RELAX } type; diff --git a/src/include/XMPP.h b/src/include/XMPP.h index 24a23d9..3b53f04 100644 --- a/src/include/XMPP.h +++ b/src/include/XMPP.h @@ -22,15 +22,23 @@ typedef struct XMPPComponent { /* Initialises a raw component stream to host, with an optional port. * If said port is 0, then it is set to the default Prosody port */ -extern XMPPComponent * XMPPInitialiseCompStream(char *host, int port); +extern XMPPComponent * XMPPInitialiseCompStream(char *addr, char *host, int port); /* Authenticates a component stream with a given shared secret, * with a stream ID from the server. This should be called right * after XMPPInitialiseCompStream. */ extern bool XMPPAuthenticateCompStream(XMPPComponent *comp, char *shared); +/** Writes an XML stanza through a component, while making sure any locking + * work is done. If {max} is non-zero, then this function will not send + * stanzas beyond {max} bytes. + * ----------------------- + * Modifies: {comp}, the XMPP stream + * See-Also: XMPPInitialiseCompStream, XMPPAuthenticateCompStream, XMPP-core RFC */ +extern void XMPPSendStanza(XMPPComponent *comp, XMLElement *stanza, size_t max); + /* Makes a user join/leave a MUC */ -extern bool XMPPJoinMUC(XMPPComponent *comp, char *fr, char *muc, bool care); +extern bool XMPPJoinMUC(XMPPComponent *comp, char *fr, char *muc, char *hash, int secs, bool ret); extern void XMPPLeaveMUC(XMPPComponent *comp, char *fr, char *muc, char *r); /* TODO: XMPP stuff, I don't fucking know, I'm not a Jabbernerd. */ @@ -82,6 +90,9 @@ extern char * XMPPGetRetractedID(XMLElement *); /* Get the replied-to stanza ID, if existent. */ extern char * XMPPGetReply(XMLElement *elem); +/* Get the replied-to stanza offset, if existent. */ +extern ssize_t XMPPGetReplyOffset(XMLElement *elem); + /** Get the moderated message ID(as a stanza ID/plain ID) from a moderation * stanza, that lives *alongside* the stanza itself. * ---------------------------------------------------------------------- @@ -90,16 +101,6 @@ extern char * XMPPGetReply(XMLElement *elem); * See-Also: https://xmpp.org/extensions/xep-0425.html */ extern char * XMPPGetModeration(XMLElement *stanza); -/** Generate the B64-encoded SHA-256 hash for the 'ver' field in caps. - * -------- - * Returns: A base64 encoded ver hash[LA:HEAP] - * Modifies: NOTHING - * See-Also: https://xmpp.org/extensions/xep-0115.html */ -extern char * XMPPGenerateVer(void); - -/* Annotates a presence with https://xmpp.org/extensions/xep-0115.html */ -extern void XMPPAnnotatePresence(XMLElement *presence); - extern bool XMPPHasError(XMLElement *stanza, char *type); #include diff --git a/src/include/XMPPCommand.h b/src/include/XMPPCommand.h index 2680a64..33b121f 100644 --- a/src/include/XMPPCommand.h +++ b/src/include/XMPPCommand.h @@ -12,6 +12,7 @@ typedef struct XMPPCommand XMPPCommand; typedef struct XMPPOption XMPPOption; typedef void (*XMPPCmdCallback)(XMPPCommandManager *, char *, XMLElement *, XMLElement *); typedef void (*XMPPOptionWriter)(XMPPCommandManager *, XMPPCommand *, char *); +typedef bool (*XMPPCmdFilter)(XMPPCommandManager *, char *id, XMLElement *stanza); /** Creates a simple XMPP command manager, which routes commands * with a single-stage form system, with a {cookie} @@ -19,16 +20,28 @@ typedef void (*XMPPOptionWriter)(XMPPCommandManager *, XMPPCommand *, char *); * Returns: An opaque command manager[LA:HEAP] * Modifies: NOTHING * See-Also: XMPPFreeManager */ -extern XMPPCommandManager * XMPPCreateManager(void *cookie); +extern XMPPCommandManager * XMPPCreateManager(void *cookie); extern void * XMPPGetManagerCookie(XMPPCommandManager *manager); +/** Changes the filter used for the command manager. This function + * takes in the source stanza and the command "ID" to filter, and + * returns true IFF the command should be shown to the requester. + * ----------- + * Returns: NOTHING + * Modifies: the manager's filter + * See-Also: XMPPCreateManager */ +extern void +XMPPManagerSetFilter(XMPPCommandManager *manager, XMPPCmdFilter filter); + /** Create a basic command with a node and name description * ----------------------------------------------------- * Returns: A command to be used with {XMPPRegisterCommand}[LA:HEAP] * Modifies: NOTHING * See-Also: XMPPRegisterCommand */ -extern XMPPCommand * XMPPBasicCmd(char *node, char *name, XMPPCmdCallback cb); -extern void XMPPCmdOptionsCreator(XMPPCommand *cmd, XMPPOptionWriter writer); +extern XMPPCommand * + XMPPBasicCmd(char *node, char *name, XMPPCmdCallback cb); +extern void +XMPPCmdOptionsCreator(XMPPCommand *cmd, XMPPOptionWriter writer); extern void XMPPAddOption(XMPPCommand *cmd, XMPPOption *opt); extern XMLElement * XMPPFormifyCommand(XMPPCommandManager *m, XMPPCommand *cmd, char *from); extern char * XMPPGetCommandNode(XMPPCommand *cmd); @@ -71,11 +84,12 @@ extern bool XMPPVerifyForm(XMPPCommand *cmd, XMLElement *form); extern void XMPPRegisterCommand(XMPPCommandManager *m, XMPPCommand *cmd); /** Shoves all {m} commands into XML as children of {p}, and a {jid} + * (and with the stanza source) * ----------------------------------------------------- * Returns: NOTHING * Modifies: {p} * See-Also: XMPPCreateManager */ -extern void XMPPShoveCommandList(XMPPCommandManager *m, char *jid, XMLElement *p); +extern void XMPPShoveCommandList(XMPPCommandManager *m, char *jid, XMLElement *p, XMLElement *s); /** Destroys all memory related to the command {manager}. * ----------------------------------------------------- @@ -95,7 +109,7 @@ extern bool XMPPManageCommand(XMPPCommandManager *m, XMLElement *stanza, ParseeD /* --------------------------------- COMMANDS --------------------------------- */ /* Please edit stc/XMPPThread.c (you can just force-save) for these to apply! */ -#define XMPP_COMMAND(f,n,t,s) \ +#define XMPP_COMMAND(f,l,n,t,s) \ extern void \ f(XMPPCommandManager *, char *, XMLElement *, XMLElement *); \ /* This symbol might not exist. We do, however, not care, as diff --git a/src/include/XMPPCommands.x.h b/src/include/XMPPCommands.x.h index d305340..dba6f48 100644 --- a/src/include/XMPPCommands.x.h +++ b/src/include/XMPPCommands.x.h @@ -1,10 +1,15 @@ /* C X-macro file */ +typedef enum XMPPCommandFlags { + XMPPCMD_ALL = 0, + XMPPCMD_MUC , /* Only for MUCs */ + XMPPCMD_ADMINS /* Only for administrators */ +} XMPPCommandFlags; #define XMPPCOMMANDS \ - XMPP_COMMAND(StatusCallback, "stats", "Get Parsee statistics", {}) \ - XMPP_COMMAND(CleanCallback, "clean", "Cleanup temporary Parsee data", {}) \ - XMPP_COMMAND(AdminsCallback, "admin", "Get Parsee admin list", {}) \ - XMPP_COMMAND(NoflyCallback, "nofly", "Get Parsee nofly list", {}) \ - XMPP_COMMAND(AddAdminCallback, "add-admin", "Adds glob as admin", { \ + XMPP_COMMAND(StatusCallback, XMPPCMD_ALL, "stats", "Get Parsee statistics", {}) \ + XMPP_COMMAND(CleanCallback, XMPPCMD_ADMINS, "clean", "Cleanup temporary Parsee data", {}) \ + XMPP_COMMAND(AdminsCallback, XMPPCMD_ALL, "admin", "Get Parsee admin list", {}) \ + XMPP_COMMAND(NoflyCallback, XMPPCMD_ADMINS, "nofly", "Get Parsee nofly list", {}) \ + XMPP_COMMAND(AddAdminCallback, XMPPCMD_ADMINS, "add-admin", "Adds glob as admin", { \ XMPPOption *glob = XMPPCreateText(true, "glob", ""); \ XMPPSetDescription(glob, "Glob pattern to set as admin"); \ XMPPAddOption(cmd, glob); \ @@ -12,12 +17,12 @@ XMPPSetFormTitle(cmd, "Admin addition form"); \ XMPPSetFormInstruction(cmd, "Select a glob pattern to add as an admin"); \ }) \ - XMPP_COMMAND(DelAdminCallback, "del-admin", "Removes glob from being admin", { \ + XMPP_COMMAND(DelAdminCallback, XMPPCMD_ADMINS, "del-admin", "Removes glob from being admin", { \ XMPPCmdOptionsCreator(cmd, FormDelAdminCallback); \ XMPPSetFormTitle(cmd, "Admin removal form"); \ XMPPSetFormInstruction(cmd, "Select a glob pattern to remove as an admin"); \ }) \ - XMPP_COMMAND(AddNoflyCallback, "add-nofly", "Adds user to nofly", { \ + XMPP_COMMAND(AddNoflyCallback, XMPPCMD_ADMINS, "add-nofly", "Adds user to nofly", { \ XMPPOption *entity = XMPPCreateText(true, "entity", ""); \ XMPPOption *reason = XMPPCreateText(false, "reason", "Not behaving"); \ XMPPSetDescription(entity, "Entity(glob) to no-fly"); \ @@ -28,5 +33,30 @@ XMPPSetFormTitle(cmd, "No-fly addition form"); \ XMPPSetFormInstruction(cmd, "Select a glob pattern to add to the nofly"); \ }) \ + XMPP_COMMAND(ClearWhitelistCallback, XMPPCMD_ADMINS, "clear-wl", "Removes the chat whitelist", {}) \ + XMPP_COMMAND(AddWhitelistCallback, XMPPCMD_ADMINS, "add-wl", "Adds server to chat whitelist", { \ + XMPPOption *serv = XMPPCreateText(true, "entity", ""); \ + XMPPSetDescription(serv, "Server to mark as admin"); \ + XMPPAddOption(cmd, serv); \ + \ + XMPPSetFormTitle(cmd, "Chatlist addition form"); \ + XMPPSetFormInstruction(cmd, "Add a server to whitelist"); \ + }) \ + XMPP_COMMAND(WhitelistCallback, XMPPCMD_ADMINS, "wl", "Get Parsee's chat whitelist", {}) \ + XMPP_COMMAND(MUCInformationID, XMPPCMD_MUC, "muc-info-id", "Get bridged Matrix room ID", {}) \ + XMPP_COMMAND(MUCInformationCID, XMPPCMD_MUC, "muc-info-cid", "Get MUC's internal ID", {}) \ + XMPP_COMMAND(MUCUnlink, XMPPCMD_MUC, "muc-unlink", "Unlinks MUC", {}) \ + XMPP_COMMAND(MUCSetKey, XMPPCMD_MUC, "muc-set-key", "Sets a key within the MUC/room's context", { \ + XMPPOption *key = XMPPCreateText(true, "key", ""); \ + XMPPOption *val = XMPPCreateText(true, "val", ""); \ + XMPPSetDescription(key, "Key"); \ + XMPPSetDescription(val, "Value"); \ + XMPPAddOption(cmd, key); \ + XMPPAddOption(cmd, val); \ + \ + XMPPSetFormTitle(cmd, "Set a key-value pair"); \ + XMPPSetFormInstruction(cmd, "Replace a key with a specific value"); \ + }) \ + XMPP_COMMAND(MUCGetKeys, XMPPCMD_MUC, "muc-get-keys", "Get all key-values in the MUC/room.", {}) \ XMPPCOMMANDS diff --git a/tools/Makefile b/tools/Makefile deleted file mode 100644 index 7fa93bc..0000000 --- a/tools/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -# Makefile for building Parsee -# ================================ -# TODO: Consider making something akin to a configure script that checks -# for dependencies, or maybe even use *autoconf* (the devil!) - - -# =========================== Parsee Flags ============================= -NAME=Parsee -VERSION=0.0.0 -REPOSITORY=$(shell git remote get-url origin) - -# =========================== Compilation Flags ============================ -CYTO_INC=/usr/local/include/ # Where Cytoplasm's include path is - # located. -CYTO_LIB=/usr/local/lib # Where's Cytoplasm's library is - # located. - -SOURCE=. -OBJECT=out -CC=cc -CFLAGS=-I$(SOURCE) -I$(CYTO_INC) -DNAME="\"$(NAME)\"" -DVERSION="\"$(VERSION)\"" -DREPOSITORY=\"$(REPOSITORY)\" -g -ggdb -Wall -Werror -LDFLAGS=-L $(CYTO_LIB) -lCytoplasm -g -ggdb -# ============================ Compilation ================================= -SRC_FILES:=$(shell find $(SOURCE) -name '*.c') -OBJ_FILES:=${subst $(SOURCE)/,$(OBJECT)/,$(patsubst %.c, %, $(SRC_FILES))} - -all: $(OBJ_FILES) - -$(OBJECT)/%: $(SOURCE)/%.c - @mkdir -p $(shell dirname "$@") - $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@ - -clean: - rm -rf $(OBJECT) - diff --git a/tools/adminify.c b/tools/adminify.c index 26afb92..1fdd6cf 100644 --- a/tools/adminify.c +++ b/tools/adminify.c @@ -89,30 +89,30 @@ Main(Array *args, HashMap *env) glob = ArrayGet(args, 2); parsee = GetDB(db_path); - if (parsee) + if (!parsee) { + Log(LOG_ERR, "%s: couldn't open config '%s' or couldnt edit DB", exec, db_path); + (void) env; + return EXIT_FAILURE; + } - if (glob) - { - Log(LOG_NOTICE, "Adding glob '%s' to database %s", glob, db_path); - AddAdmin(parsee, glob); - Log(LOG_INFO, "Successfully added glob %s.", glob); - Log(LOG_INFO, "*I'm jealous of all these admins!*"); - - DbClose(parsee); - return EXIT_SUCCESS; - } - - /* List admins */ - Log(LOG_INFO, "Admin list:"); - LogConfigIndent(LogConfigGlobal()); - ListAdmins(parsee); - LogConfigUnindent(LogConfigGlobal()); + if (glob) + { + Log(LOG_NOTICE, "Adding glob '%s' to database %s", glob, db_path); + AddAdmin(parsee, glob); + Log(LOG_INFO, "Successfully added glob %s.", glob); + Log(LOG_INFO, "*I'm jealous of all these admins!*"); DbClose(parsee); return EXIT_SUCCESS; } - Log(LOG_ERR, "%s: couldn't open DB '%s'", exec, db_path); - return EXIT_FAILURE; + /* List admins */ + Log(LOG_INFO, "Admin list:"); + LogConfigIndent(LogConfigGlobal()); + ListAdmins(parsee); + LogConfigUnindent(LogConfigGlobal()); + + DbClose(parsee); + return EXIT_SUCCESS; } diff --git a/tools/aya.c b/tools/aya.c index 04a80f5..dc49421 100644 --- a/tools/aya.c +++ b/tools/aya.c @@ -83,7 +83,7 @@ ParseAyadoc(char *raw) if (!strncmp(start, " *", 2) || !strncmp(start, "*", 1)) { start += 1 + (*start == ' '); - while (start && isspace(*start)) + while (start && isspace((int) *start)) { start++; } @@ -109,10 +109,10 @@ ParseAyadoc(char *raw) if (parsing_notes) { char *del = strchr(line_content, ':'); - char *val = del + 1; + char *val = del ? del + 1 : NULL; if (del && strlen(del) >= 1) { - while (*val && isspace(*val)) + while (*val && isspace((int) *val)) { val++; } @@ -157,6 +157,9 @@ HandleReturnValue(Stream *out, AyadocComment *aya, HeaderDeclaration d, char *fi *tag_del = '\0'; } + (void) aya; + (void) d; + /* Already write the field */ StreamPrintf(out, " %s", field); @@ -200,6 +203,8 @@ HandleSeeValue(Stream *out, AyadocComment *aya, HeaderDeclaration d, char *field "%s", field, field ); + (void) aya; + (void) d; } #define SplitBy(sym, cb, del_str) \ char *start_of_arg = val, *separator = NULL; \ @@ -209,7 +214,7 @@ HandleSeeValue(Stream *out, AyadocComment *aya, HeaderDeclaration d, char *field { \ char *temporary, *field = NULL, *index; \ char *last_char; \ - while (*start_of_arg && isspace(*start_of_arg)) \ + while (*start_of_arg && isspace((int) *start_of_arg)) \ { \ start_of_arg++; \ } \ @@ -225,7 +230,7 @@ HandleSeeValue(Stream *out, AyadocComment *aya, HeaderDeclaration d, char *field \ /* Trim out spaces */ \ last_char = separator - 1; \ - while (*last_char && isspace(*last_char)) \ + while (*last_char && isspace((int) *last_char)) \ { \ last_char--; \ } \ @@ -492,5 +497,6 @@ Main(Array *args, HashMap *env) StreamClose(output); StreamClose(input); + (void) env; return EXIT_SUCCESS; } diff --git a/tools/b64.c b/tools/b64.c index 53c08e8..ba125f2 100644 --- a/tools/b64.c +++ b/tools/b64.c @@ -50,5 +50,6 @@ Main(Array *args, HashMap *env) StreamFlush(outStream); StreamClose(outStream); + (void) env; return EXIT_SUCCESS; } diff --git a/tools/common.h b/tools/common.h index eee4793..9687ea3 100644 --- a/tools/common.h +++ b/tools/common.h @@ -108,7 +108,7 @@ GetDB(char *config) { ret = DbOpenLMDB(db_path, lmdb_size); } - if (!ret) + else { ret = DbOpen(db_path, 0); } diff --git a/tools/config.c b/tools/config.c index c6ab515..d8e0aea 100644 --- a/tools/config.c +++ b/tools/config.c @@ -20,14 +20,16 @@ Main(Array *args, HashMap *env) Uri *api_base; char *homeserver = NULL, *jcp = NULL, *jabber = NULL; char *data = NULL, *media = NULL, *listen = NULL; + char *component_as = NULL; int flag, code = EXIT_FAILURE; int port = 5347; size_t lmdb_size = 0; + size_t max_stanza = 10000; listen = "localhost"; ArgParseStateInit(&state); - while ((flag = ArgParse(&state, args, "H:J:s:d:p:m:l:S:")) != -1) + while ((flag = ArgParse(&state, args, "H:J:j:s:d:p:m:l:S:M:")) != -1) { switch (flag) { @@ -44,6 +46,9 @@ Main(Array *args, HashMap *env) case 'J': jabber = state.optArg; break; + case 'j': + component_as = state.optArg; + break; case 'd': data = state.optArg; break; @@ -56,6 +61,12 @@ Main(Array *args, HashMap *env) case 'S': lmdb_size = strtol(state.optArg, NULL, 10) * 1024 * 1024; break; + case 'M': + max_stanza = strtol(state.optArg, NULL, 10); + if (max_stanza < 10000) + { + max_stanza = 10000; + } } } @@ -71,6 +82,7 @@ Main(Array *args, HashMap *env) "-l [Host/IP to listen as] " "-p [XMPP component port=5347] " "-J [parsee.xmppserver.ex]", + "-M [max stanza size>=10000]", "-S [LMDB size]", ArrayGet(args, 0) ); @@ -109,11 +121,13 @@ Main(Array *args, HashMap *env) JsonSet(json, JsonValueString(jabber), 1, "component_host"); JsonSet(json, JsonValueInteger(port), 1, "component_port"); + JsonSet(json, JsonValueInteger(max_stanza), 1, "max_stanza_size"); JsonSet(json, JsonValueString(jcp), 1, "shared_secret"); JsonSet(json, JsonValueString(media), 1, "media_base"); JsonSet(json, JsonValueString(listen), 1, "listen_as"); + JsonSet(json, JsonValueString(component_as), 1, "component_addr"); JsonEncode(json, file, JSON_PRETTY); StreamFlush(file); @@ -131,5 +145,6 @@ Main(Array *args, HashMap *env) end: UriFree(api_base); Free(media); + (void) env; return code; } diff --git a/tools/noavatars.c b/tools/noavatars.c index 0c5526c..93fba1c 100644 --- a/tools/noavatars.c +++ b/tools/noavatars.c @@ -30,5 +30,6 @@ Main(Array *args, HashMap *env) } Log(LOG_ERR, "%s: couldn't open DB '%s'", exec, db_path); + (void) env; return EXIT_FAILURE; } diff --git a/tools/plumb.c b/tools/plumb.c new file mode 100644 index 0000000..1bc9e14 --- /dev/null +++ b/tools/plumb.c @@ -0,0 +1,152 @@ +/* plumb.c - Small utility to manage plumbings from a shutoff instance. + * ============================================================ + * TODO: write other commands, and move some code to common.h + * + * Under CC0, as its a rather useful example of a Parsee tool. + * See LICENSE for more information about Parsee's licensing. */ + +#include "common.h" + +#include + +static void +DeletePlumbID(Db *parsee, char *chat_id) +{ + DbRef *chat_id_ref = DbLockIntent(parsee, DB_HINT_READONLY, 2, "chats", chat_id); + DbRef *chats = DbLock(parsee, 1, "chats"); + char *matrix_id = GrabString(DbJson(chat_id_ref), 1, "room_id"); + char *jabber_id = GrabString(DbJson(chat_id_ref), 1, "jabber_id"); + + HashMap *rooms = GrabObject(DbJson(chats), 1, "rooms"); + HashMap *mucs = GrabObject(DbJson(chats), 1, "mucs"); + + JsonValueFree(HashMapDelete(rooms, matrix_id)); + JsonValueFree(HashMapDelete(mucs, jabber_id)); + + DbUnlock(parsee, chat_id_ref); + DbUnlock(parsee, chats); + DbDelete(parsee, 2, "chats", chat_id); +} +static void +DeletePlumb(Db *parsee, char *potential_id) +{ + if (!parsee || !potential_id) + { + return; + } + + if (!strncmp(potential_id, "xmpp:", 5)) + { + DbRef *ref; + HashMap *mucs; + char *chat_id; + /* Try to parse it as an XMPP address */ + potential_id += 5; + + ref = DbLockIntent(parsee, DB_HINT_READONLY, 1, "chats"); + mucs = GrabObject(DbJson(ref), 1, "mucs"); + chat_id = StrDuplicate(GrabString(mucs, 1, potential_id)); + DbUnlock(parsee, ref); + + DeletePlumbID(parsee, chat_id); + Free(chat_id); + + return; + } + if (*potential_id == '!') + { + /* Try to parse it as a Matrix room ID */ + DbRef *ref; + HashMap *rooms; + char *chat_id; + + ref = DbLockIntent(parsee, DB_HINT_READONLY, 1, "chats"); + rooms = GrabObject(DbJson(ref), 1, "rooms"); + chat_id = StrDuplicate(GrabString(rooms, 1, potential_id)); + DbUnlock(parsee, ref); + + DeletePlumbID(parsee, chat_id); + Free(chat_id); + + return; + } + + /* Try to parse it as a chat ID */ + DeletePlumbID(parsee, potential_id); +} +static void +ListPlumbs(Db *parsee) +{ + DbRef *ref; + HashMap *mucs; + + char *muc; + JsonValue *value; + if (!parsee) + { + return; + } + + ref = DbLockIntent(parsee, DB_HINT_READONLY, 1, "chats"); + mucs = GrabObject(DbJson(ref), 1, "mucs"); + while (HashMapIterate(mucs, &muc, (void **) &value)) + { + char *chat_id = JsonValueAsString(value); + DbRef *chat_id_ref = DbLockIntent(parsee, DB_HINT_READONLY, 2, "chats", chat_id); + char *matrix_id = GrabString(DbJson(chat_id_ref), 1, "room_id"); + + /* TODO */ + Log(LOG_INFO, "- Plumb xmpp:%s <=> %s", muc, matrix_id); + Log(LOG_INFO, " - ID=%s", chat_id); + + DbUnlock(parsee, chat_id_ref); + } + + DbUnlock(parsee, ref); +} + +int +Main(Array *args, HashMap *env) +{ + char *db_path, *action, *exec; + int ret = EXIT_SUCCESS; + Db *parsee; + + exec = ArrayGet(args, 0); + + if (ArraySize(args) < 3) + { + Log(LOG_ERR, "Usage: %s [config] [action] ", exec); + return EXIT_FAILURE; + } + + db_path = ArrayGet(args, 1); + action = ArrayGet(args, 2); + + parsee = GetDB(db_path); + if (!parsee) + { + Log(LOG_ERR, "%s: couldn't open config '%s' or couldnt edit DB", exec, db_path); + return EXIT_FAILURE; + } + + if (StrEquals(action, "list") || StrEquals(action, "ls")) + { + ListPlumbs(parsee); + } + else if (StrEquals(action, "del")) + { + if (ArraySize(args) != 4) + { + Log(LOG_ERR, "%s: please show a !roomid:matrix.org, xmpp:mucid@jabber.org, or local hash", exec); + ret = EXIT_FAILURE; + goto end; + } + DeletePlumb(parsee, ArrayGet(args, 3)); + } + + +end: + DbClose(parsee); + return ret; +} diff --git a/tools/whitelist.c b/tools/whitelist.c new file mode 100644 index 0000000..e1348d5 --- /dev/null +++ b/tools/whitelist.c @@ -0,0 +1,114 @@ +/* whitelist.c - Manages a Parsee MUC/user whitelist manually + * ============================================================ + * Example of usage: + * parsee-whitelist [CONFIG] [add|del|remove|list] + * Under CC0, as its a rather useful example of a Parsee tool. + * See LICENSE for more information about Parsee's licensing. */ + +#include "common.h" + +#include + +int +Main(Array *args, HashMap *env) +{ + int ret = EXIT_SUCCESS; + char *verb; + Db *db; + if (ArraySize(args) <= 2) + { + Log(LOG_ERR, + "Usage: %s [CONFIG] [add|del|clear|list] ", + ArrayGet(args, 0) + ); + return EXIT_FAILURE; + } + + if (!(db = GetDB(ArrayGet(args, 1)))) + { + Log(LOG_ERR, "Couldn't load database"); + ret = EXIT_FAILURE; + goto end; + } + + verb = ArrayGet(args, 2); + if (StrEquals(verb, "add")) + { + char *user = ArrayGet(args, 3); + DbRef *ref = !user ? NULL : DbLock( + db, 1, "whitelist" + ); + if (!ref && user) + { + ref = DbCreate( + db, 1, "whitelist" + ); + } + + if (!user) + { + ret = EXIT_FAILURE; + goto end; + } + JsonValueFree(HashMapSet( + DbJson(ref), + user, JsonValueObject(HashMapCreate()) + )); + DbUnlock(db, ref); + } + else if (StrEquals(verb, "del")) + { + char *user = ArrayGet(args, 3); + DbRef *ref = !user ? NULL : DbLock( + db, 1, "whitelist" + ); + if (!ref && user) + { + ref = DbCreate( + db, 1, "whitelist" + ); + } + + if (!user) + { + ret = EXIT_FAILURE; + goto end; + } + JsonValueFree(HashMapDelete(DbJson(ref), user)); + DbUnlock(db, ref); + } + else if (StrEquals(verb, "clear")) + { + DbDelete(db, 1, "whitelist"); + } + else if (StrEquals(verb, "list")) + { + DbRef *ref = DbLockIntent( + db, DB_HINT_READONLY, + 1, "whitelist" + ); + Array *keys = HashMapKeys(DbJson(ref)); + size_t i; + + for (i = 0; i < ArraySize(keys); i++) + { + char *key = ArrayGet(keys, i); + + Log(LOG_INFO, "- %s", key); + } + + + ArrayFree(keys); + DbUnlock(db, ref); + } + else + { + ret = EXIT_FAILURE; + goto end; + } + +end: + (void) env; + DbClose(db); + return ret; +}