Compare commits

..

127 commits

Author SHA1 Message Date
LDA
33edd2ceaf [MOD] Do NOT do unnecessary notifications
Some checks failed
Compile Telodendria / Compile Telodendria (aarch64, alpine) (pull_request) Has been cancelled
I should move notifying users into some sort of background task. It is
not particularly necessary to send events, and essentially slows down
the requester thread doing something that's not really sending an event.

It could also scale terribly in rooms where there exists a lot of local
users, with every one of them having possibly at least hundreds of sync
tokens.
2024-12-03 15:57:05 +01:00
LDA
4b427a4c82 [ADD/WIP] Dumb key management
This is just enough to fool FluffyChat and nheko, and still not enough
to actually do proper E2EE. Also, don't use Cinny with it, it seems to
repeteadly upload keys. Terrible.
2024-12-02 11:50:38 +01:00
LDA
8612aae24c [MOD] Mark v1 as stable
Some checks failed
Compile Telodendria / Compile Telodendria (aarch64, alpine) (pull_request) Has been cancelled
Haven't had too many unfixed bugs with it. It just works.
2024-11-23 14:23:41 +01:00
LDA
1d0a7e62f2 [FIX] Make Telodendria play ball with Schildi
The web versions worked fine, but the old Android ones tend to be
strict, which is a great tool for testing.
2024-11-23 14:20:32 +01:00
LDA
276b7fecd7 [ADD] Add /devices
Some checks failed
Compile Telodendria / Compile Telodendria (aarch64, alpine) (pull_request) Has been cancelled
It was easy to add, so I did. Still want to store more information about
devices(and maybe make the Cytoplasm HTTP server be able to actually show
address information)
2024-11-22 11:57:22 +01:00
LDA
68c47e1ae6 [UPD] Push out all changes I did to my test instance
I should really make sure I stop making gigantic commits...
2024-11-22 11:43:11 +01:00
LDA
6b39b3eb74 [FIX] Fix SEGV, notify syncers on exit
Some checks failed
Compile Telodendria / Compile Telodendria (aarch64, alpine) (pull_request) Has been cancelled
2024-11-09 17:56:42 +01:00
LDA
ebbca383cb [ADD/WIP] Float checks, /members, GETting state pieces
Some checks failed
Compile Telodendria / Compile Telodendria (aarch64, alpine) (pull_request) Has been cancelled
2024-10-30 01:55:59 +01:00
LDA
f279ead4d7 [ADD/WIP] Start processing event relations
Some checks failed
Compile Telodendria / Compile Telodendria (aarch64, alpine) (pull_request) Has been cancelled
2024-09-28 12:31:12 +02:00
LDA
0e2d652b8a [FIX] Store redactor outside the PDU itself
Some checks failed
Compile Telodendria / Compile Telodendria (aarch64, alpine) (pull_request) Has been cancelled
2024-09-28 10:47:35 +02:00
LDA
78dbe5ca81 [ADD/WIP] Start thinking AS, fix population
Some checks failed
Compile Telodendria / Compile Telodendria (aarch64, alpine) (pull_request) Has been cancelled
Visibility was all over the place....
2024-09-27 23:45:54 +02:00
LDA
01d170ce39 [FIX/WIP] Encapsulate PDUs
Some checks failed
Compile Telodendria / Compile Telodendria (aarch64, alpine) (pull_request) Has been cancelled
Note that I just broke compatibility. Sorry.
2024-09-27 18:13:54 +02:00
LDA
1369064683 [FIX] Bring back Cytoplasm
Some checks failed
Compile Telodendria / Compile Telodendria (aarch64, alpine) (pull_request) Has been cancelled
2024-09-27 16:03:16 +02:00
LDA
49decbf80f Merge branch 'master' of https://git.telodendria.io/lda/Telodendria into roomwerk
Some checks failed
Compile Telodendria / Compile Telodendria (aarch64, alpine) (pull_request) Has been cancelled
2024-09-27 15:50:40 +02:00
LDA
d5941986ba Merge branch 'master' of https://git.telodendria.io/Telodendria/Telodendria 2024-09-27 15:48:14 +02:00
LDA
29a298efe2 [MOD] Fix out redacts a bit 2024-09-14 20:00:32 +02:00
LDA
0d7f472a24 [FIX] Count in the 'redacts' field 2024-09-14 17:43:23 +02:00
LDA
2fd39bfe25 [ADD] Basic redactwerk
There's still some issues, like the fact that an event is simply gone
like so. Also, I may want to store the redacted event's ID, instead of
the content itself, as that is a storage hog, and if the redacted event
is redacted itself, it simply won't spread.
2024-09-14 15:09:44 +02:00
LDA
01de46b299 [REF] Separate the room functions
User.c, you're next.
2024-08-30 09:42:58 +02:00
LDA
5b51d9159e [MOD] Stateless /message tokins 2024-08-28 19:55:04 +02:00
LDA
dfb950bc7f [MOD] Use keyed schemas for the sync 2024-08-27 18:56:38 +02:00
LDA
0a45cebbfe Merge branch 'master' of https://git.telodendria.io/Telodendria/Telodendria into roomwerk 2024-08-27 18:30:53 +02:00
LDA
1e544a4927 [MOD] Use hashtypes 2024-08-25 21:47:44 +02:00
LDA
4bc7ae92d6 Merge branch 'master' of https://git.telodendria.io/Telodendria/Telodendria 2024-08-25 21:36:20 +02:00
LDA
a77be8b437 [MOD] Modify join/invite lists to be lazy 2024-08-25 18:26:28 +02:00
LDA
620cf91ba0 [MOD/FIX] More descriptive errors, "fix" join
I'll need to be able to "propagate" a user ref as much as possible/be
able to lock a user for read-only operations(as DB intents now allow)
2024-08-25 17:15:08 +02:00
LDA
aa6e375167 Merge branch 'master' of https://git.telodendria.io/Telodendria/Telodendria into roomwerk 2024-08-24 09:39:49 +02:00
LDA
19359045e9 [MOD/FIX] Leaves, fix leaks 2024-08-23 22:19:49 +02:00
LDA
fde6f092b4 [MOD] Cleanup code a bit, more status hjinks!
More work!
2024-08-23 21:03:55 +02:00
LDA
d07880b24b [MOD] Start softfailing/dropped, and fix leaks 2024-08-22 12:10:37 +02:00
LDA
283455b1a0 Merge branch 'master' of https://git.telodendria.io/Telodendria/Telodendria into roomwerk 2024-08-22 00:39:28 +02:00
LDA
b719a16d9f [MOD] Revamping the state a bit 2024-08-21 11:02:28 +02:00
LDA
15f4de2a25 [ADD/WIP] Banning users out 2024-08-18 00:49:45 +02:00
LDA
368690aaeb [ADD/WIP] Start adding kicks
Doing that actually did make me find flaws in my room implementation, so
that's good.
2024-08-18 00:12:55 +02:00
LDA
860dc9637a [MOD/WIP] Propagate nick/displayname changes
I should consider try to shove the previous state in the unsigned if
possible.
2024-08-17 19:10:27 +02:00
LDA
4918932a0e [MOD/WIP] Separate DB entries for the txns 2024-08-17 01:08:09 +02:00
LDA
dc0e463c27 [MOD/WIP] Parallel awaiting of notifications
Passed the very basic test with the tanuki account, over two sessions.
2024-08-16 19:02:57 +02:00
LDA
acd1bf0ec0 [MOD/WIP] Fix broken Cinny UIA and state changes 2024-08-15 11:00:58 +02:00
LDA
cc5c4bde70 [MOD] Spoof some endpoints 2024-08-14 21:36:51 +02:00
LDA
57651df1c7 [MOD/WIP] Try separating joins from the main ref
Still need to do that for transactions, as that will easily bloat up.
2024-08-14 13:21:41 +02:00
LDA
5e15d73c44 [DEL] Remove YCM jank 2024-08-14 03:06:10 +02:00
LDA
2b6805672b [ADD/WIP] Try to parse a filter from raw JSON 2024-08-14 02:59:40 +02:00
LDA
3e5d9dbe62 [ADD/WIP] Filtering some event fields(somewhat) 2024-07-24 13:17:53 +02:00
LDA
123aa0b23a [WIP] Moar filtering, cond variables with timeout
Still isn't _too_ tested, hmph.
2024-07-23 19:49:32 +02:00
LDA
55652eaa15 [MOD] Make a dedicated FilterDecode function
I'll need to parse the JSON from a string, however, which means a stream
abstraction for being able to read from a string(or just a raw
bytestream). Guess I'll have to reimplement that away...
2024-07-23 12:27:52 +02:00
LDA
3d700de17d [FIX] Do not add swap files 2024-07-23 12:00:16 +02:00
LDA
c25db688ba [MOD] Separate IsRoomFiltered 2024-07-23 11:58:46 +02:00
LDA
6f045083a2 [ADD/WIP] Dot-separated fetching, respect 'rooms'
That should get me to filter events, senders, and other things
2024-07-20 17:45:12 +02:00
LDA
d46e418ec1 [FIX/WIP] Fix crash, verify room filtering
Sorry for not contributing that much, currently busy on another
project... I should implement more filtering later on.
2024-07-19 16:56:24 +02:00
LDA
7825559f1b [ADD/WIP/UNTESTED] Start doing filtering
Currently untested until I take some time, as I'm busy with something
else at the moment. I _really_, *really*, need to cleanup that code...
2024-07-08 10:16:54 +02:00
LDA
e2d26c7f33 [ADD/WIP] Start /messages, sync timeouts, cleaning
This code _really_ isn't good. I've spotted some fun racing happen and
leak of some JSON(somehow?) without timeout, and I've been trying to
spend the last several days trying to fix that.

The good news is that, with some endpoint spoofing, you can actually
_try_ this with a Matrix client(dev.cinny.in for example).

I might consider rewriting this pastacode, considering the issues noted
above in favour of a better design, with less odds of this.
2024-06-14 23:30:35 +02:00
LDA
97b1e4d723 [ADD/WIP] Push events into sync
The code is still ugly(still waiting for j2s to support some things
before I can actually get *somewhere* with the schemas, which is my main
complaint), and it's definitely not spec-compliant, but it now means you
can actually see diffs in rooms you joined from the timeline!
2024-06-09 11:46:44 +02:00
LDA
046a04e495 [WIP] Start making (broken) /syncs
This code is *really* ugly. And it also doesn't handle timelines.
2024-06-09 03:20:09 +02:00
LDA
00311f58dc [WIP] Start making bare-bones /sync reply schema. 2024-06-09 02:13:49 +02:00
LDA
a491a740d4 [ADD/WIP] Begin work on storing /sync diffs. 2024-06-09 01:43:16 +02:00
LDA
bb3d15f2c9 [MOD/FIX] Update user list at the lowest level
I'll need to start defining a `diffs' system for sync. I have a decent
idea on how that could go(with 1 level of batching), but I'll need to
consider it some.
2024-06-08 18:37:54 +02:00
LDA
88610a3238 [ADD/WIP] Start basic work on getting room lists.
I think I should actually update by "catching" events on-the-fly instead
of doing this.
2024-06-08 15:49:31 +02:00
LDA
9203ec1268 [MOD] Join rooms via alias 2024-06-08 11:57:52 +02:00
LDA
9600d2ffb5 [FIX/WIP] Fix join rules
I'll still need to notify users that they're *in* the room, instead of
just poking at the state.

But this should allow users to send events to the same room.
2024-06-08 11:03:54 +02:00
lda
250d28b958 [UNTESTED/WIP] Room joins
Still have to test this.
2024-06-08 10:31:21 +02:00
lda
6ce63b01ce [ADD/WIP] Basic means to view a room event. 2024-06-08 09:34:27 +02:00
lda
e268471dba [ADD/WIP] Start work to send messages
This commit *works*  but I don't quite like it.
2024-06-07 18:24:13 +02:00
LDA
39c4af8d4d [MOD/WIP] Start work for event sending 2024-06-07 13:58:34 +02:00
lda
b0c3d6ce31 [MOD/WIP] Move caching in the StateResolve func. 2024-06-06 23:25:46 +02:00
lda
3a2fec8a47 [MOD/TESTING] Begin storing the state at each event
This probably needs some cache evicting to avoid piling up large
states.
2024-06-06 16:02:43 +02:00
lda
604d6cf017 Merge remote-tracking branch 'refs/remotes/origin/roomwerk' into roomwerk 2024-06-04 13:47:15 +02:00
lda
21b015da2c [MOD/WIP] Fix double-free on PL checks 2024-06-04 13:45:12 +02:00
lda
c1630247b1 [FIX/WIP] Fix bug where banned users could be invited.
There seems to be another, much more annoying bug going down with
one of the servers I've tested on, but I don't know how to fix it
as of now.
2024-05-24 16:52:31 +02:00
lda
879e51c169 [ADD/WIP] Set is_direct field in room leaves 2024-05-21 11:17:57 +02:00
lda
c22d628c86 [ADD/WIP] Start implementing invites.
I'll need to add the direct flag somewhere in the room leaves.
Also, it seems like there's a giant bottleneck in my code...
2024-05-20 16:37:41 +02:00
lda
9dcaab0819 [ADD/FIX/WIP] Fix content hashing, respect creation_content.
Oops, I *should* have known sha256 are stored as raw unpadded b64!base64!
2024-05-19 19:11:33 +02:00
lda
cacc72bf84 [ADD/WIP] Basic preset code
I still need to find a solution to a problem related to j2s.
Right now I kind of want to have a way for users to send events
from the endpoints.
2024-05-19 15:17:56 +02:00
lda
a99798a312 [ADD/WIP] Start abstracting aliasing 2024-05-19 00:30:28 +02:00
lda
e3c57d8f05 [ADD/WIP] Basic alias system.
NOTE: I *need* to write an Alias API(and also manage clashes proper.)
2024-05-18 21:24:30 +02:00
lda
7ee35fcc28 [MOD] Clamp depth 2024-05-18 20:26:30 +02:00
lda
327730c2c6 [ADD/WIP] Basic PDU depth system.
NOTE: This is *not* a good way to compute depth.
2024-05-18 20:17:54 +02:00
lda
5ca36396a2 Merge branch 'master' of https://git.telodendria.io/Telodendria/Telodendria into roomwerk 2024-05-18 19:22:27 +02:00
lda
e1d632b135 [ADD/WIP] Remove boundscheck on array sorts 2024-05-18 17:11:29 +02:00
lda
ecb18dc7b2 [ADD/WIP] Added more events at init-time 2024-05-18 16:39:51 +02:00
lda
e36f4357ab [ADD/WIP] Start testing code
I think I'll manage PDU depth later(with an actual good way to handle
it properly(that is not just setting it to the max and calling it a
day.)
2024-05-17 23:57:32 +02:00
lda
1753c2188b [ADD/UNTESTED] Set ServerPart on room creation
Not tested as of now!
2024-05-17 13:52:25 +02:00
lda
9ffa37d7a7 [ADD/UNTESTED] Start implementing event sending
This is NOT perfect. This *will* need changes before it gets pushed.
2024-05-17 13:37:34 +02:00
lda
15b884b04a [ADD/UNTESTED] Barebones State Resolution V1
NOTE: It is probably NOT in an usable state. Even if it is, it should
not be PR'd into Telodendria right now.

Feeling a bit burnt out, so I'll take a break. Feel free to issue PRs to
this branch to clean/weed out some mistakes or bugs, since I only made
sure that it built.
2024-05-15 20:15:49 +02:00
lda
3066a0e8a8 [ADD/UNTESTED] Finalise powerlevels somewhat
Still too hacky for my taste...
2024-05-15 12:51:32 +02:00
lda
878b294030 [ADD/UNTESTED] (Nearly) implement 'invite' in auth
Almost because we don't verify the public key at all.
2024-05-15 10:12:07 +02:00
lda
6a69218479 [UNTESTED/ADD] Add more membership in auth rules
The only TODO left in this bit is `invite'.
2024-05-15 09:37:31 +02:00
lda
aec71d8d32 [ADD/UNTESTED] Finalise step 5.2 of auth rules.
Next up is the invite membership(5.3).
2024-05-15 09:02:09 +02:00
lda
50759a3298 [UNTESTED/ADD] Finish barebones of auth rules 2024-05-15 08:41:59 +02:00
lda
9bc36f324a [ADD/WIP/UNTESTED] Start verifying membership, tested hashing 2024-05-08 12:14:10 +02:00
lda
001b4821fe [ADD/WIP/UNTESTED] Start creating PDU for client events 2024-05-02 20:02:30 +02:00
lda
d0a447a409 [WIP/ADD] Start basic room work 2024-05-01 22:01:56 +02:00
lda
b72f18538d Merge branch 'master' of https://git.telodendria.io/Telodendria/Telodendria 2024-05-01 21:02:08 +02:00
lda
f815f653b0 Merge branch 'master' of https://git.telodendria.io/Telodendria/Telodendria 2024-03-20 15:29:16 +01:00
lda
8231e8a078
[LICENSE] Update licensing for 2024 2024-01-05 01:13:56 +01:00
lda
12061afe62 Merge branch 'master' of https://git.telodendria.io/Telodendria/Telodendria 2024-01-05 01:09:13 +01:00
lda
18488d463e
[FIX] Remove Cytoplasm. 2023-11-30 19:25:04 +01:00
lda
184a0ef1de Merge branch 'master' of https://git.telodendria.io/lda/Telodendria 2023-11-30 19:10:06 +01:00
lda
e8d6f0f857 [FIX] Start fixing merging issue 2023-11-30 19:08:44 +01:00
lda
829b9bd6b4 Merge remote-tracking branch 'upstream/master' 2023-11-30 19:07:18 +01:00
f4cb10804a Add change log entry for #35. 2023-09-11 19:53:31 +02:00
3f69954ca7 Convert configuration documentation. 2023-09-11 19:53:31 +02:00
LoaD Accumulator
afc9e0e5dc Fixes issue #33 related to a memory issue, and format some code. (#35)
Fixes #33.

Co-authored-by: LoaD Accumulator <lda@freetards.xyz>
Reviewed-on: Telodendria/telodendria#35
Co-authored-by: LoaD Accumulator <lda@noreply.git.telodendria.io>
Co-committed-by: LoaD Accumulator <lda@noreply.git.telodendria.io>
2023-09-11 19:53:31 +02:00
5067b5bcf0 Remove send-patch and tp. See #20. 2023-09-11 19:53:31 +02:00
36c07ed17d Put the finishing touches on CONTRIBUTING.md. 2023-09-11 19:53:31 +02:00
d7afd6285d Add contributing documentation. 2023-09-11 19:53:31 +02:00
69e7837fb4 Fix issue config. 2023-09-11 19:53:31 +02:00
6f9ac042b5 Don't allow blank issues. 2023-09-11 19:53:31 +02:00
9b65558a5a Fix a few small bugs in Gitea templates. 2023-09-11 19:53:31 +02:00
16a45e67e6 Add Gitea issue templates. 2023-09-11 19:53:31 +02:00
afde23df70 Fix table rendering in stats.md 2023-09-11 19:53:31 +02:00
b8c99a2b1f Finish moving over current Administrator API documentation. 2023-09-11 19:53:31 +02:00
c781950fff Another typo. 2023-09-11 19:53:30 +02:00
60ebbccf7d Fix typo in privileges.md. 2023-09-11 19:53:30 +02:00
73dd12ef7e Add privileges documentation. 2023-09-11 19:53:30 +02:00
20c4a4d0fb Fix typo in docs/user/admin/README.md 2023-09-11 19:53:30 +02:00
4168557c4b Add admin documentation home page. 2023-09-11 19:53:30 +02:00
5ad199353b Add logo and center title. 2023-09-08 22:54:05 +02:00
b32aababb3 Add technical rationale document. 2023-09-08 22:54:05 +02:00
82d4711f7f Add a nice README which will serve as the basis for the website. 2023-09-08 22:54:05 +02:00
66537d1b0c Add repository structure documentation. 2023-09-08 22:54:05 +02:00
6eb16af9b5 Add setup documentation. 2023-09-08 22:54:05 +02:00
cb6963299c Add porting instructions. 2023-09-08 22:54:05 +02:00
c2294723e6 Add usage and install documentation. 2023-09-08 22:54:05 +02:00
5c7ce30b15 Add documentation home page. 2023-09-08 22:54:05 +02:00
63a6bcce1a Remove old change log. 2023-09-08 22:54:05 +02:00
6f14d0eef3 Add CHANGELOG.md 2023-09-08 22:54:05 +02:00
d9217946f3
[FIX] Fix commit f61009a423's mistake 2023-09-08 16:54:07 +02:00
67 changed files with 9042 additions and 138 deletions

2
.gitignore vendored
View file

@ -16,3 +16,5 @@ contrib/.vagrant
src/Schema
src/include/Schema
man/mandoc.db
*swp
.ccls-cache

View file

@ -45,6 +45,9 @@
"state_key": {
"type": "string"
},
"redacts": {
"type": "string"
},
"type": {
"type": "string",
"required": true

62
Schema/KeyUpload.json Normal file
View file

@ -0,0 +1,62 @@
{
"guard": "TELODENDRIA_KEY_UPLOAD_H",
"header": "Schema/KeyUpload.h",
"types": {
"DeviceKeys": {
"type": "struct",
"fields": {
"device_id": {
"type": "string",
"required": true
},
"user_id": {
"type": "string",
"required": true
},
"algorithms": {
"type": "[string]",
"required": true
},
"keys": {
"type": "{string}",
"required": true
},
"signatures": {
"//": "TODO: More complex j2s types.",
"//": "This is meant to be a map from user ID to ",
"//": "algo+device ID to a signature(string).",
"type": "object",
"required": true
}
}
},
"KeyResponse": {
"type": "struct",
"fields": {
"one_time_key_counts": {
"type": "{integer}",
"required": true
}
}
},
"KeyUploadRequest": {
"type": "struct",
"fields": {
"device_keys": {
"type": "DeviceKeys",
"required": false
},
"fallback_keys": {
"//": "This is a one-time key.",
"type": "object",
"required": false
},
"one_time_keys": {
"//": "This is a one-time key.",
"type": "object",
"required": false
}
}
}
}
}

View file

@ -2,6 +2,16 @@
"guard": "TELODENDRIA_SCHEMA_PDUV1_H",
"header": "Schema/PduV1.h",
"types": {
"PduV1Status": {
"type": "enum",
"fields": {
"dropped": { "name": "PDUV1_STATUS_DROPPED" },
"softfail": { "name": "PDUV1_STATUS_SOFTFAIL" },
"accepted": { "name": "PDUV1_STATUS_ACCEPTED" },
"rejected": { "name": "PDUV1_STATUS_REJECTED" }
}
},
"PduV1EventHash": {
"type": "struct",
"fields": {
@ -16,6 +26,22 @@
"fields": {
"age": {
"type": "integer"
},
"next_events": {
"type": "[string]",
"required": false
},
"pdu_status": {
"type": "PduV1Status",
"required": false
},
"redacted_because": {
"type": "object",
"required": false
},
"transaction_id": {
"type": "string",
"required": false
}
}
},
@ -51,7 +77,8 @@
"required": true
},
"redacts": {
"type": "string"
"type": "string",
"required": false
},
"room_id": {
"type": "string",

19
Schema/Relation.json Normal file
View file

@ -0,0 +1,19 @@
{
"guard": "TELODENDRIA_SCHEMA_RELATION_H",
"header": "Schema/Relation.h",
"types": {
"Relation": {
"type": "struct",
"fields": {
"event_id": {
"type": "string",
"required": true
},
"rel_type": {
"type": "string",
"required": true
}
}
}
}
}

View file

@ -48,6 +48,9 @@
"room_alias_name": {
"type": "string"
},
"room_id": {
"type": "string"
},
"preset": {
"type": "RoomCreatePreset"
}

117
Schema/SyncResponse.json Normal file
View file

@ -0,0 +1,117 @@
{
"guard": "TELODENDRIA_SCHEMA_SYNCRESPONSE_H",
"header": "Schema\/SyncResponse.h",
"types": {
"Event": {
"fields": {
"content": {
"type": "object",
"required": true
},
"type": {
"type": "string",
"required": true
}
},
"type": "struct"
},
"AccountData": {
"fields": {
"events": { "type": "[Event]" }
},
"type": "struct"
},
"InviteState": {
"fields": {
"events": { "type": "[StrippedStateEvent]" }
},
"type": "struct"
},
"States": {
"fields": {
"events": { "type": "array" }
},
"type": "struct"
},
"StrippedStateEvent": {
"fields": {
"content": {
"type": "object",
"required": true
},
"sender": {
"type": "string",
"required": true
},
"state_key": {
"type": "string",
"required": true
},
"type": {
"type": "string",
"required": true
}
},
"type": "struct"
},
"InvitedRooms": {
"fields": {
"invite_state": { "type": "InviteState" }
},
"type": "struct"
},
"ClientEventWithoutRoomID": {
"fields": {
"content": { "type": "object", "required": true },
"event_id": { "type": "string", "required": true },
"origin_server_ts": { "type": "integer", "required": true },
"sender": { "type": "string", "required": true },
"state_key": { "type": "string" },
"redacts": { "type": "string" },
"_unsigned": { "type": "object" },
"type": { "type": "string", "required": true }
},
"type": "struct"
},
"Timeline": {
"fields": {
"events": { "type": "[ClientEventWithoutRoomID]", "required": true },
"limited": { "type": "boolean" },
"prev_batch": { "type": "string" }
},
"type": "struct"
},
"JoinedRooms": {
"fields": {
"timeline": { "type": "Timeline" },
"state": { "type": "States" }
},
"type": "struct"
},
"Rooms": {
"fields": {
"invite": { "type": "{InvitedRooms}" },
"join": { "type": "{JoinedRooms}" }
},
"type": "struct"
},
"SyncResponse": {
"fields": {
"next_batch": {
"type": "string",
"required": true
},
"account_data": {
"type": "AccountData"
},
"rooms": {
"type": "Rooms"
},
"device_one_time_keys_count": {
"type": "{integer}"
}
},
"type": "struct"
}
}
}

4
configure vendored
View file

@ -15,7 +15,7 @@ TOOLS="tools/src"
SCHEMA="Schema"
CYTOPLASM="Cytoplasm"
CFLAGS="-O1 -D_DEFAULT_SOURCE -I${INCLUDE} -I${BUILD}"
CFLAGS="-O1 -D_DEFAULT_SOURCE -I${INCLUDE} -I${SRC} -I${BUILD}"
LIBS="-lm -pthread -lCytoplasm"
@ -155,7 +155,7 @@ print_obj() {
get_deps() {
src="$1"
${CC} -I${INCLUDE} -I${BUILD} $(if [ -n "${CYTOPLASM}" ]; then echo "-I${CYTOPLASM}/include"; fi) -E "$src" \
${CC} -I${SRC} -I${INCLUDE} -I${BUILD} $(if [ -n "${CYTOPLASM}" ]; then echo "-I${CYTOPLASM}/include"; fi) -E "$src" \
| grep '^#' \
| awk '{print $3}' \
| cut -d '"' -f 2 \

View file

@ -1,6 +0,0 @@
all:
sh tools/bin/td
install:
install build/telodendria $(PREFIX)/bin/telodendria
find man -name 'telodendria*\.[1-8]' -exec install {} $(PREFIX)/{} \;

View file

@ -14,6 +14,8 @@ administrator API; the regular Matrix API is unaffected.
registration tokens.
- **CONFIG:** Allows a user to modify the Telodendria server daemon's
configuration.
- **APPSERVICE:** Allows a user to register and manage application
services.
- **GRANT_PRIVILEGES:** Allows a user to modify his or her own
privileges or the privileges of other local users.
- **ALIAS:** Allows a user to modify and see room aliases created by

View file

@ -22,11 +22,16 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "Cytoplasm/Io.h"
#include "Cytoplasm/Stream.h"
#include <CanonicalJson.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Array.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Sha.h>
#include <stdio.h>
#include <string.h>
@ -159,3 +164,55 @@ CanonicalJsonEncode(HashMap * object, Stream * out)
ArrayFree(keys);
return length;
}
static ssize_t
StringIoRead(void *cookie, void *buf, size_t count)
{
(void) cookie;
(void) buf;
(void) count;
return -1; /* TODO: Consider reading properly */
}
static ssize_t
StringIoWrite(void *c, void *buf, size_t count)
{
if (count > 0)
{
char **str = c;
char *stringised = Malloc(count + 1);
char *new;
memcpy(stringised, buf, count);
new = StrConcat(2, *str, stringised);
Free(stringised);
if (*str)
{
Free(*str);
}
*str = new;
}
return count;
}
static const IoFunctions Functions = {
.read = StringIoRead,
.write = StringIoWrite,
.close = NULL,
.seek = NULL
};
unsigned char *
CanonicalJsonHash(HashMap *json)
{
char *string = NULL;
unsigned char *sha;
Io *string_writer = IoCreate(&string, Functions);
Stream *io_stream = StreamIo(string_writer);
CanonicalJsonEncode(json, io_stream);
StreamFlush(io_stream);
sha = Sha256(string);
Free(string);
StreamClose(io_stream);
return sha;
}

View file

@ -230,3 +230,24 @@ ConfigLogLevelToSyslog(ConfigLogLevel level)
}
return LOG_INFO;
}
char *
ConfigGetServerName(Db * db)
{
char *name;
Config config;
ConfigLock(db, &config);
if (!config.ok)
{
return NULL;
}
name = StrDuplicate(config.serverName);
ConfigUnlock(&config);
return name;
}

View file

@ -24,12 +24,256 @@
*/
#include <Filter.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <string.h>
#include <stdio.h>
#include <Schema/Filter.h>
#include <Matrix.h>
/* Verifies whenever an item passes through a set of blacklisted and
* whitelisted groups(e.g "not_rooms"/"rooms"), and makes the calling
* function return true/false if it is explicitely in a filtered list,
* or does nothing otherwise, to allow for chaining multiple filters. */
#define DAFiltered(blacklist, whitelist, item) \
do \
{ \
size_t i, count; \
count = ArraySize(blacklist); \
for (i = 0; i < count; i++) \
{ \
char *notItem = ArrayGet(blacklist, i); \
if (StrEquals(item, notItem)) \
{ \
return true; \
} \
} \
count = ArraySize(whitelist); \
if (count) \
{ \
for (i = 0; i < count; i++) \
{ \
char *yesItem = ArrayGet(whitelist, i); \
if (StrEquals(yesItem, item)) \
{ \
return false; \
} \
} \
return true; \
} \
} while (0)
static bool
IsSenderFiltered(Filter *filter, char *senderID)
{
if (!filter || !senderID)
{
return false;
}
DAFiltered(
filter->room.account_data.not_senders,
filter->room.account_data.senders,
senderID
);
DAFiltered(
filter->room.state.not_senders,
filter->room.state.senders,
senderID
);
DAFiltered(
filter->room.ephemeral.not_senders,
filter->room.ephemeral.senders,
senderID
);
return false;
}
bool
IsRoomFiltered(Filter *filter, char *roomID)
{
if (!filter || !roomID)
{
return false;
}
DAFiltered(
filter->room.not_rooms,
filter->room.rooms,
roomID
);
DAFiltered(
filter->room.account_data.not_rooms,
filter->room.account_data.rooms,
roomID
);
DAFiltered(
filter->room.state.not_rooms,
filter->room.state.rooms,
roomID
);
DAFiltered(
filter->room.ephemeral.not_rooms,
filter->room.ephemeral.rooms,
roomID
);
return false;
}
static HashMap *
IncludeFields(Array * fields, HashMap * event)
{
HashMap *cpy;
size_t i, len;
if (!event)
{
return NULL;
}
if (!fields)
{
return JsonDuplicate(event);
}
cpy = HashMapCreate();
/* NOTE: We intentionally add some fields due to some requirements
* around certain schemas. The specification does find such behavior
* compliant:
* > "[...] A server may include more fields than were requested." */
#define CopyField(field) \
HashMapSet(cpy, field, JsonValueDuplicate(HashMapGet(event, field)))
CopyField("event_id");
CopyField("origin_server_ts");
CopyField("sender");
CopyField("type");
len = ArraySize(fields);
for (i = 0; i < len; i++)
{
char *field = ArrayGet(fields, i);
JsonValue *val = JsonValueDuplicate(MatrixGetJSON(event, field));
JsonValueFree(MatrixSetJSON(cpy, field, val));
}
/* Only copy it if not already present. */
if (!HashMapGet(cpy, "content"))
{
CopyField("content");
}
#undef CopyField
return cpy;
}
/* TODO: MORE FILTERS! */
HashMap *
FilterApply(Filter * filter, HashMap * event)
{
(void) filter;
(void) event;
return NULL;
HashMap *copy;
char *sender, *room;
if (!event)
{
return NULL;
}
if (!filter)
{
/* Do NOT filter anything out */
return JsonDuplicate(event);
}
sender = JsonValueAsString(HashMapGet(event, "sender"));
room = JsonValueAsString(HashMapGet(event, "room_id"));
if (IsRoomFiltered(filter, room) ||
IsSenderFiltered(filter, sender))
{
return NULL;
}
copy = IncludeFields(filter->event_fields, event);
return copy;
}
Filter *
FilterDecode(Db *db, char *user, char *filterStr, char **errp)
{
Filter *ret;
DbRef *filterRef;
HashMap *filterObj;
FILE *fakeFile;
Stream *fakeStream;
char *err;
if (!db || !user || !filterStr)
{
if (errp)
{
*errp =
"Internal error: Field required for FilterDecode is missing.";
}
return NULL;
}
if (*filterStr != '{')
{
filterRef = DbLock(db, 3, "filters", user, filterStr);
filterObj = DbJson(filterRef);
ret = Malloc(sizeof(*ret));
memset(ret, 0, sizeof(*ret));
if (!FilterFromJson(filterObj, ret, &err))
{
if (errp)
{
*errp = err;
}
DbUnlock(db, filterRef);
Free(ret);
return NULL;
}
DbUnlock(db, filterRef);
return ret;
}
/* TODO: This should be it's own JSON function, in my
* opinion. */
fakeFile = fmemopen(filterStr, strlen(filterStr), "r");
fakeStream = StreamFile(fakeFile);
filterObj = JsonDecode(fakeStream);
StreamClose(fakeStream); /* auto-frees fakeFile */
ret = Malloc(sizeof(*ret));
memset(ret, 0, sizeof(*ret));
if (!FilterFromJson(filterObj, ret, &err))
{
if (errp)
{
*errp = err;
}
JsonFree(filterObj);
Free(ret);
return NULL;
}
JsonFree(filterObj);
return ret;
}
void
FilterDestroy(Filter *filter)
{
if (!filter)
{
return;
}
FilterFree(filter);
Free(filter);
}

View file

@ -46,7 +46,7 @@ HtmlBegin(Stream * stream, char *title)
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
"<title>%s | Telodendria</title>"
"<link rel=\"stylesheet\" href=\"/_matrix/static/telodendria.css\">"
"<script src=\"/_matrix/static/telodendria.js\"></script>"
"<script src=\"/_matrix/static/telodendria.js\"></script>"
"</head>"
"<body>"
"<pre class=\"logo\">"

View file

@ -56,6 +56,7 @@
static Array *httpServers;
static volatile int restart;
static MatrixHttpHandlerArgs matrixArgs;
static void
SignalHandler(int signal)
@ -79,13 +80,36 @@ SignalHandler(int signal)
for (i = 0; i < ArraySize(httpServers); i++)
{
HttpServer *server = ArrayGet(httpServers, i);
/* TODO: Notify all users, so that the server doesn't have to
* await for a sync reply. */
UserNotifyAll(matrixArgs.db);
HttpServerStop(server);
}
break;
}
}
/* TODO: Move this! */
static void
UsersClean(MatrixHttpHandlerArgs *args)
{
Db *db = args->db;
Array *arr = DbList(db, 1, "users");
size_t i;
for (i = 0; i < ArraySize(arr); i++)
{
char *id = ArrayGet(arr, i);
User *user = UserLock(db, id);
UserCleanTemporaryData(user);
UserUnlock(user);
}
DbListFree(arr);
}
typedef enum ArgFlag
{
ARG_VERSION = (1 << 0),
@ -121,7 +145,6 @@ Main(Array * args)
/* Signal handling */
struct sigaction sigAction;
MatrixHttpHandlerArgs matrixArgs;
Cron *cron;
char startDir[PATH_MAX];
@ -143,10 +166,12 @@ start:
userInfo = NULL;
groupInfo = NULL;
cron = NULL;
tConfig.ok = false;
token = NULL;
memset(&matrixArgs, 0, sizeof(matrixArgs));
UserInitialisePushTable();
if (!LogConfigGlobal())
{
@ -275,7 +300,8 @@ start:
goto finish;
}
if (!tConfig.log.timestampFormat || !StrEquals(tConfig.log.timestampFormat, "default"))
if (!tConfig.log.timestampFormat ||
!StrEquals(tConfig.log.timestampFormat, "default"))
{
LogConfigTimeStampFormatSet(LogConfigGlobal(), tConfig.log.timestampFormat);
}
@ -513,10 +539,15 @@ start:
Log(LOG_DEBUG, "Registering jobs...");
CronEvery(cron, 30 * 60 * 1000, (JobFunc *) UiaCleanup, &matrixArgs);
CronEvery(cron, 5 * 60 * 1000, (JobFunc *) UsersClean, &matrixArgs);
Log(LOG_NOTICE, "Starting job scheduler...");
CronStart(cron);
/* We still call it anyways to be sure. */
Log(LOG_NOTICE, "Cleaning up user data...");
UsersClean(&matrixArgs);
Log(LOG_NOTICE, "Building routing tree...");
matrixArgs.router = RouterBuild();
if (!matrixArgs.router)
@ -583,6 +614,7 @@ start:
finish:
Log(LOG_NOTICE, "Shutting down...");
UserDestroyPushTable();
if (httpServers)
{
for (i = 0; i < ArraySize(httpServers); i++)
@ -606,9 +638,11 @@ finish:
CronFree(cron);
Log(LOG_DEBUG, "Stopped and freed job scheduler.");
}
ConfigUnlock(&tConfig);
Log(LOG_DEBUG, "Unlocked configuration.");
if (tConfig.ok)
{
ConfigUnlock(&tConfig);
Log(LOG_DEBUG, "Unlocked configuration.");
}
DbClose(matrixArgs.db);
Log(LOG_DEBUG, "Closed database.");

View file

@ -360,3 +360,181 @@ MatrixClientWellKnown(char *base, char *identity)
return response;
}
JsonValue *
MatrixGetJSON(HashMap *json, char *string)
{
JsonValue *retVal;
HashMap *currentObj;
char *field, *fieldTemp;
size_t i, length;
if (!json || !string)
{
return NULL;
}
currentObj = json;
retVal = NULL;
length = strlen(string);
field = NULL;
#define Append(ch) do \
{ \
char chb[2] = { ch, 0 }; \
fieldTemp = field; \
field = StrConcat(2, field, chb); \
Free(fieldTemp); \
} while (0)
/* We include the 0-terminator as a valid separator. */
for (i = 0; i < length + 1; i++)
{
char currentChar = string[i];
char escape;
if (currentChar == '\\' && (escape = string[i+1]))
{
if (escape != '.' && escape != '\\')
{
Append('\\');
}
Append(escape);
i++;
continue;
}
else if (currentChar == '.' || currentChar == '\0')
{
if (!field || !currentObj)
{
Free(field);
return NULL;
}
retVal = HashMapGet(currentObj, field);
currentObj = JsonValueAsObject(retVal);
Free(field);
field = NULL;
continue;
}
Append(currentChar);
}
#undef Append
if (field)
{
/* This is weird. */
Free(field);
}
return retVal;
}
JsonValue *
MatrixSetJSON(HashMap *json, char *string, JsonValue *new)
{
JsonValue *retVal;
HashMap *currentObj;
char *field, *fieldTemp;
size_t i, length;
if (!json || !string || !new)
{
return NULL;
}
currentObj = json;
retVal = NULL;
length = strlen(string);
field = NULL;
#define Append(ch) do \
{ \
char chb[2] = { ch, 0 }; \
fieldTemp = field; \
field = StrConcat(2, field, chb); \
Free(fieldTemp); \
} while (0)
/* We include the 0-terminator as a valid separator. */
for (i = 0; i < length + 1; i++)
{
char currentChar = string[i];
char escape;
if (currentChar == '\\' && (escape = string[i+1]))
{
if (escape != '.' && escape != '\\')
{
Append('\\');
}
Append(escape);
i++;
continue;
}
else if (currentChar == '.' || currentChar == '\0')
{
if (!field || !currentObj)
{
Free(field);
return NULL;
}
if (currentChar == '.')
{
retVal = HashMapGet(currentObj, field);
if (!retVal)
{
retVal = JsonValueObject(HashMapCreate());
HashMapSet(currentObj, field, retVal);
}
currentObj = JsonValueAsObject(retVal);
Free(field);
field = NULL;
continue;
}
retVal = HashMapSet(currentObj, field, new);
Free(field);
field = NULL;
continue;
}
Append(currentChar);
}
#undef Append
if (field)
{
/* This is weird. */
Free(field);
}
return retVal;
}
bool
MatrixCheckFloats(HashMap *obj)
{
size_t i;
char *key;
JsonValue *value;
if (!obj)
{
return false;
}
i = 0;
while (HashMapIterateReentrant(obj, &key, (void **) &value, &i))
{
if (JsonValueType(value) == JSON_FLOAT)
{
return false;
}
else if (JsonValueType(value) == JSON_OBJECT)
{
if (!MatrixCheckFloats(JsonValueAsObject(value)))
{
return false;
}
}
}
return true;
}

View file

@ -487,7 +487,7 @@ ParserRecomposeCommonID(CommonID id)
if (id.server.hostname)
{
char *server = ParserRecomposeServerPart(id.server);
char *tmp = StrConcat(4, "@", ret, ":", server);
char *tmp = StrConcat(3, ret, ":", server);
Free(ret);
Free(server);

View file

@ -23,35 +23,97 @@
* SOFTWARE.
*/
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Array.h>
#include <Cytoplasm/Json.h>
#include <Parser.h>
#include <User.h>
#include <Room.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Base64.h>
#include <Cytoplasm/Util.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Db.h>
#include <Schema/RoomCreateRequest.h>
#include <Schema/PduV1.h>
#include <Schema/PduV3.h>
struct Room
#include <CanonicalJson.h>
#include <Config.h>
#include <Parser.h>
#include <Matrix.h>
#include <State.h>
#include <Event.h>
#include <stdlib.h>
#include <string.h>
#include "Room/internal.h"
static char *
GenerateRoomId(RoomCreateRequest *req, ServerPart s)
{
Db *db;
DbRef *ref;
CommonID cid;
char *string;
char *id;
int version;
};
cid.sigil = '!';
cid.local = (req && req->room_id) ?
StrDuplicate(req->room_id) :
StrRandom(32);
cid.server = s;
string = ParserRecomposeCommonID(cid);
Free(cid.local);
return string;
}
Room *
RoomCreate(Db * db, RoomCreateRequest * req)
RoomCreate(Db * db, User *user, RoomCreateRequest * req, ServerPart s)
{
(void) db;
(void) req;
return NULL;
Room *room;
char *version_string, *full_creator;
int version_num = 1;
HashMap *json;
if (!db || !req || !user)
{
return NULL;
}
version_string = req->room_version;
if (version_string)
{
/* TODO: Eventually use something else than room version 1 by
* default, and maybe add a config parameter. */
version_num = atoi(version_string);
version_num = version_num == 0 ? 1 : version_num;
}
room = Malloc(sizeof(Room));
room->db = db;
room->creator.hostname = s.hostname ? StrDuplicate(s.hostname) : NULL;
room->creator.port = s.port ? StrDuplicate(s.port) : NULL;
room->id = GenerateRoomId(req, s);
room->version = version_num;
room->state_ref = DbCreate(db, 3, "rooms", room->id, "state");
room->leaves_ref = DbCreate(db, 3, "rooms", room->id, "leaves");
json = DbJson(room->leaves_ref);
JsonSet(json, JsonValueArray(ArrayCreate()), 1, "leaves");
full_creator = ParserRecomposeServerPart(room->creator);
JsonSet(json, JsonValueString(full_creator), 1, "creator");
JsonSet(json, JsonValueInteger(version_num), 1, "version");
Free(full_creator);
RoomPopulate(room, user, req, s);
return room;
}
Room *
RoomLock(Db * db, char *id)
{
DbRef *ref;
DbRef *state_ref, *leaves_ref;
HashMap *json;
Room *room;
if (!db || !id)
@ -59,9 +121,10 @@ RoomLock(Db * db, char *id)
return NULL;
}
ref = DbLock(db, 3, "rooms", id, "state");
state_ref = DbLock(db, 3, "rooms", id, "state");
leaves_ref = DbLock(db, 3, "rooms", id, "leaves");
if (!ref)
if (!state_ref || !leaves_ref)
{
return NULL;
}
@ -69,13 +132,21 @@ RoomLock(Db * db, char *id)
room = Malloc(sizeof(Room));
if (!room)
{
DbUnlock(db, ref);
DbUnlock(db, state_ref);
DbUnlock(db, leaves_ref);
return NULL;
}
room->db = db;
room->ref = ref;
room->state_ref = state_ref;
room->leaves_ref = leaves_ref;
room->id = StrDuplicate(id);
room->version = JsonValueAsInteger(HashMapGet(DbJson(leaves_ref), "version"));
json = DbJson(room->leaves_ref);
ParseServerPart(
JsonValueAsString(JsonGet(json, 1, "creator")),
&room->creator);
return room;
}
@ -84,7 +155,8 @@ int
RoomUnlock(Room * room)
{
Db *db;
DbRef *ref;
DbRef *state_ref;
DbRef *leaves_ref;
if (!room)
{
@ -92,33 +164,717 @@ RoomUnlock(Room * room)
}
db = room->db;
ref = room->ref;
state_ref = room->state_ref;
leaves_ref = room->leaves_ref;
Free(room->id);
Free(room);
return DbUnlock(db, ref);
ServerPartFree(room->creator);
return DbUnlock(db, state_ref) &&
DbUnlock(db, leaves_ref);
}
int64_t
ParsePL(JsonValue *v, int64_t def)
{
if (!v)
{
return def;
}
if (JsonValueType(v) == JSON_INTEGER)
{
return JsonValueAsInteger(v);
}
if (JsonValueType(v) == JSON_STRING)
{
char *string = JsonValueAsString(v), *end;
int64_t value= strtoll(string, &end, 10);
if (!((*string != '\0') && (*end == '\0')))
{
/* Invalid string: return the default. */
return def;
}
return value;
}
return def;
}
char *
RoomIdGet(Room * room)
EventContentHash(HashMap * pdu_json)
{
return room ? room->id : NULL;
HashMap * copy = JsonDuplicate(pdu_json);
unsigned char *sha;
char *b64;
JsonValueFree(HashMapDelete(copy, "unsigned"));
JsonValueFree(HashMapDelete(copy, "signatures"));
JsonValueFree(HashMapDelete(copy, "hashes"));
sha = CanonicalJsonHash(copy);
b64 = Base64Encode((const char *) sha, 32);
Base64Unpad(b64, strlen(b64));
Free(sha);
JsonFree(copy);
return b64;
}
int
RoomVersionGet(Room * room)
/* TODO: Separate this when we eventually steamroll */
static HashMap *
RoomEventSendV3(Room * room, HashMap * event)
{
return room ? room->version : 0;
/* TODO */
(void) room;
(void) event;
return NULL;
}
HashMap *
RoomStateGet(Room * room)
RoomEventSend(Room * room, HashMap * event, char **errp)
{
if (!room || !event)
{
return NULL;
}
if (!MatrixCheckFloats(event))
{
if (errp)
{
*errp = "Event contains forbidden float value";
}
return NULL;
}
if (room->version < 3)
{
/* Manage with PDUv1 */
return RoomEventSendV1(room, event, errp);
}
/* Manage with PDUv3 otherwise */
return RoomEventSendV3(room, event);
}
char *
CreateSafeID(char *unsafe_id)
{
size_t length = strlen(unsafe_id);
char *safe_id = Malloc(length + 1);
size_t i;
/* Creates a room ID safe to be put into the database
* for room version 3 and above.
* (with '/'s replaced by '-') */
memcpy(safe_id, unsafe_id, length + 1);
for (i = 0; i < length; i++)
{
if (safe_id[i] == '/')
{
safe_id[i] = '-';
}
}
return safe_id;
}
HashMap *
RoomEventFetchRaw(Room *room, char *id)
{
char *safe_id;
HashMap *ret;
DbRef *event_ref;
if (!room || !id)
{
return NULL;
}
/* Let's try to locally find that event in our junk. */
safe_id = CreateSafeID(id);
event_ref = DbLockIntent(
room->db, DB_HINT_READONLY,
4, "rooms", room->id, "events", safe_id
);
if (!event_ref)
{
/* TODO: Fetch from another homeserver if possible. */
ret = NULL;
goto finish;
}
ret = JsonDuplicate(JsonValueAsObject(HashMapGet(DbJson(event_ref), "pdu")));
DbUnlock(room->db, event_ref);
finish:
Free(safe_id);
return ret;
}
HashMap *
RoomEventFetch(Room *room, char *id, bool prev)
{
char *safe_id, *redactor;
HashMap *ret, *unsign;
DbRef *event_ref;
uint64_t ts;
if (!room || !id)
{
return NULL;
}
/* Let's try to locally find that event in our junk. */
safe_id = CreateSafeID(id);
event_ref = DbLockIntent(
room->db, DB_HINT_READONLY,
4, "rooms", room->id, "events", safe_id
);
if (!event_ref)
{
/* TODO: Fetch from another homeserver if possible. */
ret = NULL;
goto finish;
}
ret = JsonDuplicate(JsonValueAsObject(HashMapGet(DbJson(event_ref), "pdu")));
unsign = JsonValueAsObject(HashMapGet(ret, "unsigned"));
/* Overwrite a few unsigned properties on the fly. */
JsonValueFree(HashMapSet(
unsign,
"next_events",
JsonValueDuplicate(HashMapGet(DbJson(event_ref), "next_events"))
));
JsonValueFree(HashMapSet(
unsign,
"pdu_status",
JsonValueDuplicate(HashMapGet(DbJson(event_ref), "status"))
));
JsonValueFree(HashMapSet(
unsign,
"transaction_id",
JsonValueDuplicate(HashMapGet(DbJson(event_ref), "transaction"))
));
ts = JsonValueAsInteger(HashMapGet(ret, "origin_server_ts"));
JsonValueFree(HashMapSet(
unsign,
"age",
JsonValueInteger(UtilTsMillis() - ts)
));
if (prev)
{
redactor =
JsonValueAsString(HashMapGet(DbJson(event_ref), "redacted_by"));
JsonValueFree(HashMapSet(
unsign,
"redacted_because",
JsonValueObject(RoomEventFetch(room, redactor, false))
));
}
if (prev && HashMapGet(ret, "state_key"))
{
char *prev_ev;
HashMap *prev_obj;
JsonValue *prev_content;
prev_ev =
JsonValueAsString(HashMapGet(DbJson(event_ref), "prev_event"));
prev_obj = RoomEventFetch(room, prev_ev, false);
prev_content = HashMapGet(prev_obj, "content");
JsonValueFree(HashMapSet(
unsign,
"prev_content",
JsonValueDuplicate(prev_content)
));
JsonFree(prev_obj);
}
DbUnlock(room->db, event_ref);
finish:
Free(safe_id);
return ret;
}
HashMap *
RoomEventCreate(char *sender, char *type, char *key, HashMap *c, char *txn)
{
HashMap *event;
if (!sender || !type || !c)
{
return NULL;
}
event = HashMapCreate();
JsonSet(event, JsonValueObject(c), 1, "content");
JsonSet(event, JsonValueString(sender), 1, "sender");
JsonSet(event, JsonValueString(type), 1, "type");
JsonSet(event, JsonValueString(txn), 1, "transaction");
if (key)
{
JsonSet(event, JsonValueString(key), 1, "state_key");
}
return event;
}
void
RoomAddAlias(Db *db, char *roomAlias, char *roomId, char *sender, char *serv)
{
DbRef *aliasRef;
HashMap *json, *alias, *idObject;
Array *servers, *ids;
if (!db || !roomAlias || !roomId || !sender || !serv)
{
return;
}
aliasRef = DbLock(db, 1, "aliases");
if (!aliasRef)
{
aliasRef = DbCreate(db, 1, "aliases");
if (!aliasRef)
{
return;
}
}
json = DbJson(aliasRef);
servers = ArrayCreate();
ArrayAdd(servers, JsonValueString(serv));
/* alias => ID */
alias = HashMapCreate();
JsonSet(alias, JsonValueString(roomId), 1, "id");
JsonSet(alias, JsonValueString(sender), 1, "createdBy");
JsonSet(alias, JsonValueArray(servers), 1, "servers");
JsonValueFree(
JsonSet(json, JsonValueObject(alias), 2, "aliases", roomAlias));
/* ID => alias(es) */
if (!(idObject =
JsonValueAsObject(JsonGet(json, 2, "id", roomId))))
{
ids = ArrayCreate();
idObject = HashMapCreate();
HashMapSet(idObject, "aliases", JsonValueArray(ids));
JsonSet(
json,
JsonValueObject(idObject),
2, "id", roomId);
}
ids = JsonValueAsArray(HashMapGet(idObject, "aliases"));
ArrayAdd(ids, JsonValueString(roomAlias));
DbUnlock(db, aliasRef);
}
char *
RoomResolveAlias(Db *db, char *roomAlias)
{
DbRef *aliasRef;
HashMap *json;
char *ret;
if (!db || !roomAlias)
{
return NULL;
}
aliasRef = DbLock(db, 1, "aliases");
if (!aliasRef)
{
return NULL;
}
json = DbJson(aliasRef);
ret = JsonValueAsString(JsonGet(json, 3, "aliases", roomAlias, "id"));
ret = StrDuplicate(ret);
DbUnlock(db, aliasRef);
return ret;
}
Array *
RoomReverseAlias(Db *db, char *roomId)
{
DbRef *aliasRef;
HashMap *json, *idObject;
Array *ret, *original;
size_t i;
if (!db || !roomId)
{
return NULL;
}
aliasRef = DbLock(db, 1, "aliases");
if (!aliasRef)
{
return NULL;
}
json = DbJson(aliasRef);
if (!(idObject =
JsonValueAsObject(JsonGet(json, 2, "id", roomId))))
{
DbUnlock(db, aliasRef);
return NULL;
}
original = JsonValueAsArray(HashMapGet(idObject, "aliases"));
ret = ArrayCreate();
for (i = 0; i < ArraySize(original); i++)
{
JsonValue *v = ArrayGet(original, i);
ArrayAdd(ret, StrDuplicate(JsonValueAsString(v)));
}
DbUnlock(db, aliasRef);
return ret;
}
void
RoomFreeReverse(Array *arr)
{
size_t i;
if (!arr)
{
return;
}
for (i = 0; i < ArraySize(arr); i++)
{
Free(ArrayGet(arr, i));
}
ArrayFree(arr);
}
void
RoomSendInvite(User *sender, bool direct, char *user, Room *room)
{
HashMap *content, *event;
CommonID *senderID;
char *senderStr;
Config conf;
if (!sender || !user || !room)
{
return;
}
ConfigLock(room->db, &conf);
senderID = UserIdParse(UserGetName(sender), conf.serverName);
senderID->sigil = '@';
senderStr = ParserRecomposeCommonID(*senderID);
UserIdFree(senderID);
content = HashMapCreate();
JsonSet(content, JsonValueBoolean(direct), 1, "is_direct");
JsonSet(content, JsonValueString("invite"), 1, "membership");
event = RoomEventCreate(senderStr, "m.room.member", user, content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
/* TODO: Send to "local" invite list if user is local.
* Otherwise, ping the entire world about it. */
ConfigUnlock(&conf);
Free(senderStr);
}
Db *
RoomGetDB(Room *room)
{
if (!room)
{
return NULL;
}
return NULL;
return room->db;
}
bool
RoomContainsUser(Room *room, char *user)
{
State *state;
bool ret;
if (!room || !user)
{
return false;
}
state = StateCurrent(room);
ret = RoomUserHasMembership(room, state, user, "join");
StateFree(state);
return ret;
}
bool
RoomCanJoin(Room *room, char *user)
{
State *state;
HashMap *joinRule = NULL;
char *joinRuleV;
bool ret;
if (!room || !user)
{
return false;
}
state = StateCurrent(room);
/* No rooms for banned people! */
if (RoomUserHasMembership(room, state, user, "ban"))
{
ret = false;
goto end;
}
/* Check join_rules */
joinRule = RoomEventFetch(
room,
StateGet(state, "m.room.join_rules", ""), false
);
joinRuleV = JsonValueAsString(JsonGet(joinRule, 2, "content", "join_rule"));
if (StrEquals(joinRuleV, "public"))
{
/* Anyone can join the room without any prior action. */
ret = true;
goto end;
}
if (StrEquals(joinRuleV, "invite"))
{
/* A user must first receive an invite from someone already in the
* room in order to join. */
ret = RoomUserHasMembership(room, state, user, "invite");
goto end;
}
if (StrEquals(joinRuleV, "knock"))
{
/* TODO: Knocking and restricted rooms. */
ret = false;
goto end;
}
if (StrEquals(joinRuleV, "restricted"))
{
/* TODO: Knocking and restricted rooms. */
ret = false;
goto end;
}
if (StrEquals(joinRuleV, "knock_restricted"))
{
/* TODO: Knocking and restricted rooms. */
ret = false;
goto end;
}
/* All other rooms are considered private. */
ret = false;
end:
StateFree(state);
JsonFree(joinRule);
return ret;
}
bool
RoomLeave(Room *room, User *user, char **errp)
{
CommonID *userId = NULL;
char *userString = NULL;
char *server = NULL;
HashMap *content = NULL;
HashMap *event = NULL;
HashMap *pdu = NULL;
bool ret = false;
if (!room || !user)
{
return false;
}
server = ConfigGetServerName(room->db);
if (!server)
{
return false;
}
userId = UserIdParse(UserGetName(user), server);
userId->sigil = '@';
userString = ParserRecomposeCommonID(*userId);
Free(server);
server = NULL;
content = HashMapCreate();
JsonSet(content, JsonValueString("leave"), 1, "membership");
event = RoomEventCreate(userString, "m.room.member", userString, content, NULL);
pdu = RoomEventSend(room, event, errp);
/* TODO: One ought to be extremely careful with managing users in those
* scenarios, as the DB flushes do not sync. */
ret = !!pdu;
if (ret)
{
UserRemoveJoin(user, room->id);
}
UserIdFree(userId);
JsonFree(event);
JsonFree(pdu);
if (userString)
{
Free(userString);
}
return ret;
}
char *
RoomRedact(Room *room, User *user, char *eventID, char *reason, char **errp)
{
CommonID *userId = NULL;
char *userString = NULL;
char *server = NULL;
HashMap *event = NULL;
HashMap *content = NULL;
HashMap *pdu = NULL;
char *ret = NULL;
if (!room || !user || !eventID)
{
return NULL;
}
server = ConfigGetServerName(room->db);
if (!server)
{
if (errp)
{
*errp = "Could not retrieve servername.";
}
return NULL;
}
userId = UserIdParse(UserGetName(user), server);
userId->sigil = '@';
userString = ParserRecomposeCommonID(*userId);
Free(server);
server = NULL;
if (!RoomContainsUser(room, userString))
{
ret = NULL;
if (errp)
{
*errp = "User is not already in-room.";
}
goto end;
}
content = HashMapCreate();
HashMapSet(content, "reason", JsonValueString(reason));
event = RoomEventCreate(userString,
"m.room.redaction", NULL,
content, NULL
);
HashMapSet(event, "redacts", JsonValueString(eventID));
pdu = RoomEventSend(room, event, errp);
ret = StrDuplicate(JsonValueAsString(HashMapGet(pdu, "event_id")));
if (errp && !*errp && !ret)
{
*errp = "Unknown error when redacting.";
}
end:
UserIdFree(userId);
JsonFree(event);
JsonFree(pdu);
if (userString)
{
Free(userString);
}
return ret;
}
bool
RoomJoin(Room *room, User *user, char **errp)
{
CommonID *userId = NULL;
char *userString = NULL;
char *server = NULL;
HashMap *content = NULL;
HashMap *event = NULL;
HashMap *pdu = NULL;
bool ret = false;
if (!room || !user)
{
return false;
}
server = ConfigGetServerName(room->db);
if (!server)
{
return false;
}
userId = UserIdParse(UserGetName(user), server);
userId->sigil = '@';
userString = ParserRecomposeCommonID(*userId);
Free(server);
server = NULL;
if (!RoomCanJoin(room, userString))
{
ret = false;
goto end;
}
content = HashMapCreate();
JsonSet(content, JsonValueString("join"), 1, "membership");
event = RoomEventCreate(userString, "m.room.member", userString, content, NULL);
pdu = RoomEventSend(room, event, errp);
/* TODO: One ought to be extremely careful with managing users in those
* scenarios, as the DB flushes do not sync. */
ret = !!pdu;
if (ret)
{
UserAddJoin(user, room->id);
}
end:
UserIdFree(userId);
JsonFree(event);
JsonFree(pdu);
if (userString)
{
Free(userString);
}
return ret;
}
HashMap *
RoomEventClientify(HashMap *pdu)
{
HashMap *event;
if (!pdu)
{
return NULL;
}
event = HashMapCreate();
#define CopyField(field, req) do { \
JsonValue *f = HashMapGet(pdu, field);\
if (!f && req) \
{ \
goto failure; \
} \
HashMapSet( \
event, \
field, JsonValueDuplicate(f) \
); \
} \
while(0)
CopyField("content", true);
CopyField("event_id", true);
CopyField("origin_server_ts", true);
CopyField("room_id", true);
CopyField("sender", true);
CopyField("type", true);
CopyField("state_key",false);
CopyField("redacts", false);
CopyField("unsigned", false);
return event;
failure:
JsonFree(event);
return NULL;
#undef CopyField
}

73
src/Room/Info.c Normal file
View file

@ -0,0 +1,73 @@
#include "Room/internal.h"
char *
RoomIdGet(Room * room)
{
return room ? room->id : NULL;
}
int
RoomVersionGet(Room * room)
{
return room ? room->version : 0;
}
ServerPart
RoomGetCreator(Room *room)
{
ServerPart ret = { 0 };
if (!room)
{
return ret;
}
return room->creator;
}
Array *
RoomPrevEventsGet(Room *room)
{
HashMap *json;
if (!room)
{
return NULL;
}
json = DbJson(room->leaves_ref);
return JsonValueAsArray(JsonGet(json, 1, "leaves"));
}
/* TODO: This has it's fair share of problems, as a malicious
* homeserver could *easily* fake a PDU depth and put this
* function to the depth limit. */
uint64_t
RoomGetDepth(Room *room)
{
Array *leaves;
HashMap *pdu;
size_t i;
uint64_t max;
if (!room)
{
return 0;
}
leaves = RoomPrevEventsGet(room);
if (!leaves)
{
return 0;
}
max = 0;
for (i = 0; i < ArraySize(leaves); i++)
{
uint64_t depth;
pdu = JsonValueAsObject(ArrayGet(leaves, i));
depth = JsonValueAsInteger(HashMapGet(pdu, "depth"));
if (depth > max)
{
max = depth;
}
}
return max;
}

286
src/Room/Populate.c Normal file
View file

@ -0,0 +1,286 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Array.h>
#include <Cytoplasm/Json.h>
#include <Parser.h>
#include <Config.h>
#include <User.h>
#include <Room.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Base64.h>
#include <Cytoplasm/Util.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Log.h>
#include <Cytoplasm/Db.h>
#include <Schema/RoomCreateRequest.h>
#include <Schema/PduV1.h>
#include <Schema/PduV3.h>
#include <CanonicalJson.h>
#include <Config.h>
#include <Parser.h>
#include <State.h>
#include <Event.h>
#include <stdlib.h>
#include <string.h>
#include "Room/internal.h"
void
RoomPopulate(Room *room, User *user, RoomCreateRequest *req, ServerPart s)
{
CommonID sender;
char *sender_str, *key, *version;
HashMap *content, *event, *override, *pl_content;
Array *initial_states;
JsonValue *val;
int64_t pl = 100;
size_t i;
char *join_rules_preset = NULL;
char *history_visibility_preset = NULL;
char *guest_access_preset = NULL;
bool trusted_room = false;
sender.sigil = '@';
sender.local = UserGetName(user);
sender.server = s;
sender_str = ParserRecomposeCommonID(sender);
/* m.room.create */
content = HashMapCreate();
if (room->version <= 10)
{
JsonSet(content, JsonValueString(sender_str), 1, "creator");
}
version = req->room_version ?
StrDuplicate(req->room_version) : StrInt(room->version);
JsonSet(content, JsonValueString(version), 1, "room_version");
Free(version);
while (HashMapIterate(req->creation_content, &key, (void **) &val))
{
JsonValue *content_v = HashMapGet(content, key);
if (content_v &&
!StrEquals(key, "creator") &&
!StrEquals(key, "room_version"))
{
continue;
}
JsonValueFree(HashMapSet(content, key, JsonValueDuplicate(val)));
}
event = RoomEventCreate(sender_str, "m.room.create", "", content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
UserAddJoin(user, room->id);
/* m.room.member */
content = HashMapCreate();
JsonSet(content, JsonValueString("join"), 1, "membership");
event = RoomEventCreate(sender_str, "m.room.member", sender_str, content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
/* m.room.power_levels */
content = HashMapCreate();
JsonSet(
content,
JsonValueInteger(pl),
2, "users", sender_str);
override = req->power_level_content_override;
while (override && HashMapIterate(override, &key, (void **) &val))
{
JsonValue *main = HashMapGet(content, key);
if (main)
{
HashMapDelete(content, key);
JsonValueFree(main);
}
HashMapSet(content, key, JsonValueDuplicate(val));
}
event = RoomEventCreate(sender_str, "m.room.power_levels", "", content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
/* Presets */
switch (req->preset)
{
case ROOM_CREATE_PUBLIC:
join_rules_preset = "public";
history_visibility_preset = "shared";
guest_access_preset = "forbidden";
break;
case ROOM_CREATE_TRUSTED:
trusted_room = true;
/* Fallthrough */
case ROOM_CREATE_PRIVATE:
join_rules_preset = "invite";
history_visibility_preset = "shared";
guest_access_preset = "can_join";
break;
default:
switch (req->visibility)
{
case ROOM_PUBLIC:
join_rules_preset = "public";
history_visibility_preset = "shared";
guest_access_preset = "forbidden";
break;
case ROOM_PRIVATE:
join_rules_preset = "invite";
history_visibility_preset = "shared";
guest_access_preset = "can_join";
break;
}
break;
}
/* Write out presets */
#define SetIfExistent(p,a) do { \
if (p##_preset) \
{ \
content = HashMapCreate(); \
JsonSet( \
content, \
JsonValueString(p##_preset) \
, 1, a); \
event = RoomEventCreate( \
sender_str, \
"m.room." #p, "", content, NULL); \
JsonFree(RoomEventSend(room, event, NULL)); \
JsonFree(event); \
} \
} \
while (0)
SetIfExistent(join_rules, "join_rule");
SetIfExistent(history_visibility, "history_visibility");
SetIfExistent(guest_access, "guest_access");
#undef SetIfExistent
/* User-provided initial states */
initial_states = req->initial_state;
for (i = 0; i < ArraySize(initial_states); i++)
{
RoomStateEvent *rse = ArrayGet(initial_states, i);
HashMap *rseObject = RoomStateEventToJson(rse);
if (!HashMapGet(rseObject, "state_key"))
{
HashMapSet(rseObject, "state_key", JsonValueString(""));
}
HashMapSet(rseObject, "sender", JsonValueString(sender_str));
JsonFree(RoomEventSend(room, rseObject, NULL));
JsonFree(rseObject);
}
/* Name and topic. */
if (req->name)
{
content = HashMapCreate();
JsonSet(content, JsonValueString(req->name), 1, "name");
event = RoomEventCreate(sender_str, "m.room.name", "", content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
}
if (req->topic)
{
content = HashMapCreate();
JsonSet(content, JsonValueString(req->topic), 1, "topic");
event = RoomEventCreate(sender_str, "m.room.topic", "", content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
}
/* Custom alias */
if (req->room_alias_name && !RoomResolveAlias(room->db, room->id))
{
CommonID full;
char *fullStr, *serverStr;
full.sigil = '#';
full.local = req->room_alias_name;
full.server = s;
fullStr = ParserRecomposeCommonID(full);
serverStr = ParserRecomposeServerPart(room->creator);
RoomAddAlias(room->db, fullStr, room->id, sender_str, serverStr);
content = HashMapCreate();
JsonSet(content, JsonValueString(fullStr), 1, "alias");
event = RoomEventCreate(
sender_str,
"m.room.canonical_alias", "", content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
Free(fullStr);
Free(serverStr);
}
/* Invites */
pl_content = HashMapCreate();
JsonSet(pl_content, JsonValueInteger(pl), 2, "users", sender_str);
for (i = 0; i < ArraySize(req->invite); i++)
{
char *user_id = ArrayGet(req->invite, i);
if (!user_id || !ValidCommonID(user_id, '@'))
{
/* TODO: Raise error. */
break;
}
RoomSendInvite(user, req->is_direct, user_id, room);
if (trusted_room)
{
JsonValue *own = JsonValueInteger(pl);
JsonSet(pl_content, own, 2, "users", user_id);
}
}
event = RoomEventCreate(sender_str, "m.room.power_levels", "", pl_content, NULL);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
JsonValueFree(JsonSet(
DbJson(room->leaves_ref),
JsonValueBoolean(req->is_direct), 1, "is_direct"));
/* TODO: The rest of the events mandated by the specification on
* POST /createRoom, and error management. */
Free(sender_str);
/* TODO: Error management (and invite_3pid, later) */
}

266
src/Room/State.c Normal file
View file

@ -0,0 +1,266 @@
#include "Room/internal.h"
#include <Cytoplasm/Memory.h>
#include <Config.h>
#include <State.h>
State *
RoomStateGetID(Room * room, char *event_id)
{
HashMap *event;
State *state;
if (!room || !event_id)
{
return NULL;
}
event = RoomEventFetch(room, event_id, false);
if (!event)
{
return NULL;
}
state = StateResolve(room, event);
JsonFree(event);
return state;
}
static char *
GetCurrentMembership(Room *room, State *s, char *mxid)
{
State *state;
HashMap *event;
char *event_id;
char *ret;
if (!room || !mxid)
{
return NULL;
}
if (s)
{
state = s;
}
else
{
state = StateCurrent(room);
}
event_id = StateGet(state, "m.room.member", mxid);
event = RoomEventFetchRaw(room, event_id);
if (!s)
{
StateFree(state);
}
ret = StrDuplicate(
JsonValueAsString(JsonGet(event, 2, "content", "membership"))
);
JsonFree(event);
return ret;
}
bool
RoomIsEventVisible(Room *room, User *user, char *eventId)
{
State *state;
HashMap *entryV;
char *visibility;
char *membership, *currentMembership;
CommonID cid;
char *server;
char *mxid;
bool ret = false;
if (!room || !user || !eventId)
{
return false;
}
server = ConfigGetServerName(room->db);
cid.sigil = '@';
cid.local = UserGetName(user);
ParseServerPart(server, &cid.server);
mxid = ParserRecomposeCommonID(cid);
Free(server);
state = RoomStateGetID(room, eventId);
entryV = RoomEventFetchRaw(room,
StateGet(state, "m.room.history_visibility", "")
);
if (!(visibility = JsonValueAsString(
JsonGet(entryV, 2, "content", "history_visibility"))))
{
visibility = "shared";
}
visibility = StrDuplicate(visibility);
JsonFree(entryV);
membership = GetCurrentMembership(room, state, mxid);
currentMembership = GetCurrentMembership(room, NULL, mxid);
StateFree(state);
/* TODO: (for shared) "and the user joined the room at _any_
* point after the event was sent, allow."
* Aargh. *Why?* */
if (StrEquals(visibility, "world_readable"))
{
ret = true;
}
else if (StrEquals(membership, "join"))
{
ret = true;
}
else if (StrEquals(visibility, "shared") &&
StrEquals(currentMembership, "join")) /* Check the current
* membership, as a hack */
{
ret = true;
}
else if (StrEquals(visibility, "invited") &&
StrEquals(membership, "invite"))
{
ret = true;
}
Free(mxid);
Free(membership);
Free(visibility);
Free(currentMembership);
ServerPartFree(cid.server);
return ret;
}
#define PrepareState(room, S, type, key, n) \
do \
{ \
id_##n = StateGet(S, type, key); \
if (!id_##n) \
{ \
goto finish; \
} \
n = RoomEventFetchRaw(room, id_##n); \
if (!n) \
{ \
goto finish; \
} \
} \
while (0)
#define FinishState(name) \
if (name) \
{ \
JsonFree(name); \
} \
return ret
bool
RoomIsJoinRule(Room * room, State *state, char *jr)
{
HashMap *joinrule = NULL;
char *id_joinrule = NULL;
bool ret = false;
if (!room || !state || !jr)
{
return false;
}
PrepareState(room, state, "m.room.join_rules", "", joinrule);
ret = StrEquals(
JsonValueAsString(JsonGet(joinrule, 2, "content", "join_rule")),
jr);
finish:
FinishState(joinrule);
}
/* Verifies if user has a specific membership before [e_id] in the room. */
bool
RoomUserHasMembership(Room * room, State *state, char *user, char *mbr)
{
HashMap *membership = NULL;
char *id_membership = NULL;
char *membership_value;
bool ret = false;
if (!room || !state || !user || !mbr)
{
return false;
}
PrepareState(room, state, "m.room.member", user, membership);
membership_value =
JsonValueAsString(JsonGet(membership, 2, "content", "membership"));
ret = StrEquals(membership_value, mbr);
finish:
FinishState(membership);
}
/* Computes the smallest PL needed to do something somewhere */
int64_t
RoomMinPL(Room * room, State *state, char *type, char *act)
{
HashMap *pl = NULL;
JsonValue *val;
char *id_pl;
int64_t ret = 0, def = 0;
if (!room || !state || !act)
{
return 0;
}
PrepareState(room, state, "m.room.power_levels", "", pl);
/* Every other act has a minimum PL of 0 */
def = 0;
if (StrEquals(act, "ban") ||
StrEquals(act, "kick") ||
StrEquals(act, "redact") ||
StrEquals(act, "state_default") ||
(StrEquals(type, "notifications") && StrEquals(act, "room")))
{
def = 50;
}
if (!type)
{
val = JsonGet(pl, 2, "content", act);
}
else
{
val = JsonGet(pl, 3, "content", type, act);
}
ret = ParsePL(val, def);
finish:
FinishState(pl);
}
/* Finds the power level of an user at a state. */
/* TODO: The creator should have PL100 by default. */
int64_t
RoomUserPL(Room * room, State *state, char *user)
{
HashMap *pl = NULL;
char *id_pl;
int64_t ret = 0, def = 0;
if (!room || !state || !user)
{
return 0;
}
PrepareState(room, state, "m.room.power_levels", "", pl);
def = RoomMinPL(room, state, NULL, "users_default");
ret = ParsePL(JsonGet(pl, 3, "content", "users", user), def);
finish:
FinishState(pl);
}

65
src/Room/V1/Auth/Alias.c Normal file
View file

@ -0,0 +1,65 @@
#include "Room/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
static bool
VerifyServers(char *id1, char *id2)
{
CommonID cid1;
CommonID cid2;
char *str1;
char *str2;
bool ret = false;
if (!ParseCommonID(id1, &cid1))
{
return false;
}
if (!ParseCommonID(id2, &cid2))
{
return false;
}
str1 = ParserRecomposeServerPart(cid1.server);
str2 = ParserRecomposeServerPart(cid2.server);
if (StrEquals(str1, str2))
{
ret = true;
goto end;
}
end:
Free(str1);
Free(str2);
CommonIDFree(cid1);
CommonIDFree(cid2);
return ret;
}
bool
AuthoriseAliasV1(PduV1 pdu, char **errp)
{
/* Step 4.1: If event has no state_key, reject. */
if (!pdu.state_key || StrEquals(pdu.state_key, ""))
{
if (errp)
{
*errp = "Step 4.1 fail: no state key in the alias";
}
return false;
}
/* Step 4.2: If sender's domain doesn't matches state_key, reject. */
if (!VerifyServers(pdu.state_key, pdu.sender))
{
if (errp)
{
*errp = "Step 4.2 fail: alias domain doesnt match statekey";
}
return false;
}
/* Step 4.3: Otherwise, allow. */
return true;
}

191
src/Room/V1/Auth/Auth.c Normal file
View file

@ -0,0 +1,191 @@
#include "Room/internal.h"
#include "Room/V1/Auth/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
static bool
VerifyServers(char *id1, char *id2)
{
CommonID cid1;
CommonID cid2;
char *str1;
char *str2;
bool ret = false;
if (!ParseCommonID(id1, &cid1))
{
return false;
}
if (!ParseCommonID(id2, &cid2))
{
return false;
}
str1 = ParserRecomposeServerPart(cid1.server);
str2 = ParserRecomposeServerPart(cid2.server);
if (StrEquals(str1, str2))
{
ret = true;
goto end;
}
end:
Free(str1);
Free(str2);
CommonIDFree(cid1);
CommonIDFree(cid2);
return ret;
}
bool
RoomAuthoriseEventV1(Room * room, PduV1 pdu, State *state, char **errp)
{
HashMap *create_event;
char *create_event_id;
JsonValue *federate;
int64_t pdu_pl = RoomUserPL(room, state, pdu.sender);
int64_t event_pl = RoomMinPL(room,state, "events", pdu.type);
/* Step 1: If m.room.create */
if (StrEquals(pdu.type, "m.room.create"))
{
return AuthoriseCreateV1(pdu, errp);
}
/* Step 2: Considering the event's auth_events.
* TODO: This is slow. */
if (!ConsiderAuthEventsV1(room, pdu, errp))
{
return false;
}
/* Step 3: If the content of the m.room.create event in the room state
* has the property m.federate set to false, and the sender domain of
* the event does not match the sender domain of the create event,
* reject.
* TODO: This is slow.
*/
create_event_id = StateGet(state, "m.room.create", "");
if (!state || !create_event_id)
{
/* At this point, [create_event_id] has to exist */
if (errp)
{
*errp = "No creation event in the state. Needless to say, "
"your room is done for.";
}
return false;
}
create_event = RoomEventFetchRaw(room, create_event_id);
federate = JsonGet(create_event, 2, "content", "m.federate");
if (JsonValueType(federate) == JSON_BOOLEAN)
{
if (!JsonValueAsBoolean(federate))
{
char *c_sender =
JsonValueAsString(JsonGet(create_event, 1, "sender"));
char *p_sender = pdu.sender;
if (!VerifyServers(c_sender, p_sender))
{
if (errp)
{
*errp = "Trying to access a room with m.federate off.";
}
return false;
}
}
}
JsonFree(create_event);
/* Step 4: If type is m.room.aliases */
if (StrEquals(pdu.type, "m.room.aliases"))
{
return AuthoriseAliasV1(pdu, errp);
}
/* Step 5: If type is m.room.member */
if (StrEquals(pdu.type, "m.room.member"))
{
return AuthoriseMemberV1(room, pdu, state, errp);
}
/* Step 6: If the sender's current membership state is not join, reject. */
if (!RoomUserHasMembership(room, state, pdu.sender, "join"))
{
return false;
}
/* Step 7: If type is m.room.third_party_invite */
if (StrEquals(pdu.type, "m.room.third_party_invite"))
{
/* Allow if and only if sender's current power level is greater than
* or equal to the invite level */
int64_t min_pl = RoomMinPL(room, state, NULL, "invite");
return pdu_pl >= min_pl;
}
/* Step 8: If the event type's required power level is greater than the
* sender's power level, reject. */
if (event_pl > pdu_pl)
{
return false;
}
/* Step (9): If the event has a state_key that starts with an @ and does
* not match the sender, reject. */
if (pdu.state_key && *pdu.state_key == '@')
{
if (!StrEquals(pdu.state_key, pdu.sender))
{
return false;
}
}
/* Step 10: If type is m.room.power_levels */
if (StrEquals(pdu.type, "m.room.power_levels"))
{
return AuthorisePowerLevelsV1(room, pdu, state);
}
/* Step 11: If type is m.room.redaction */
if (StrEquals(pdu.type, "m.room.redaction"))
{
int64_t min_pl = RoomMinPL(room, state, NULL, "redact");
/* Step 11.1: If the sender's power level is greater than or equal
* to the redact level, allow. */
if (pdu_pl >= min_pl)
{
return true;
}
if (StrEquals(create_event_id, pdu.redacts))
{
/* This is _not_ spec behaviour. As such, I _need_ to document
* this oddity. Apparently, up until v11 came along, a user could
* redact a creation event. Freely. Without complaining. Of course,
* this breaks the room.
*
* As such, I have made the room to NOT redact creation events.
* Diverges that could be caused by this is, in my opinion low,
* and all other servers that completely listen to the spec will
* be broken. */
return false;
}
/* Step 11.2: If the domain of the event_id of the event being
* redacted is the same as the domain of the event_id of the
* m.room.redaction, allow. */
if (pdu.redacts && VerifyServers(pdu.redacts, pdu.event_id))
{
return true;
}
/* Step 11.3: Otherwise, reject. */
return false;
}
/* Step 12: Otherwise, allow. */
return true;
}

View file

@ -0,0 +1,183 @@
#include "Room/internal.h"
#include "Room/V1/Auth/internal.h"
#include <Schema/PduV1.h>
#include <Cytoplasm/Log.h>
#include <Parser.h>
static bool
VerifyPDUV1(PduV1 *auth_pdu)
{
/* TODO: The PDU could come from an unknown source, which may lack
* the tools to verify softfailing(or we may not trust them)*/
/* TODO:
* https://spec.matrix.org/v1.7/server-server-api/
* #checks-performed-on-receipt-of-a-pdu */
if (RoomIsRejectedV1(*auth_pdu))
{
Log(LOG_ERR, "Auth PDU has been rejected.");
return false;
}
if (RoomIsSoftfailedV1(*auth_pdu))
{
Log(LOG_ERR, "Auth PDU has been softfailed.");
}
return true; /* This only shows whenever an event was rejected, not
* soft-failed */
}
static bool
ValidAuthEventV1(PduV1 *auth_pdu, PduV1 *pdu)
{
if (IsState(auth_pdu, "m.room.create", ""))
{
return true;
}
if (IsState(auth_pdu, "m.room.power_levels", ""))
{
/* TODO: Check if it's the latest in terms of [pdu] */
return true;
}
if (IsState(auth_pdu, "m.room.member", pdu->sender))
{
/* TODO: Check if it's the latest in terms of [pdu] */
return true;
}
if (StrEquals(pdu->type, "m.room.member"))
{
char *membership =
JsonValueAsString(JsonGet(pdu->content, 1, "membership"));
JsonValue *third_pid =
JsonGet(pdu->content, 1, "third_party_invite");
/* The PDU's state_key is the target. So we check if if the
* auth PDU would count as the target's membership. Was very fun to
* find that out when I wanted to kick my 'yukari' alt. */
if (IsState(auth_pdu, "m.room.member", pdu->state_key))
{
/* TODO: Check if it's the latest in terms of [pdu] */
return true;
}
if ((StrEquals(membership, "join") ||
StrEquals(membership, "invite")) &&
IsState(auth_pdu, "m.room.join_rules",""))
{
/* TODO: Check if it's the latest in terms of [pdu] */
return true;
}
if (StrEquals(membership, "invite") && third_pid)
{
HashMap *tpid = JsonValueAsObject(third_pid);
JsonValue *token =
JsonGet(tpid, 2, "signed", "token");
char *token_str = JsonValueAsString(token);
if (IsState(auth_pdu, "m.room.third_party_invite", token_str))
{
/* TODO: Check if it is the latest. */
return true;
}
}
/* V1 simply doesn't have the concept of restricted rooms,
* so we can safely skip this one for this function. */
}
return false;
}
bool
ConsiderAuthEventsV1(Room * room, PduV1 pdu, char **errp)
{
char *ignored;
size_t i;
bool room_create = false;
HashMap *state_keytype;
state_keytype = HashMapCreate();
for (i = 0; i < ArraySize(pdu.auth_events); i++)
{
char *event_id = JsonValueAsString(ArrayGet(pdu.auth_events, i));
HashMap *event = RoomEventFetchRaw(room, event_id);
PduV1 auth_pdu = { 0 };
char *key_type_id;
if (!PduV1FromJson(event, &auth_pdu, &ignored))
{
JsonFree(event);
HashMapFree(state_keytype);
if (errp)
{
*errp = "Couldn't parse an auth event";
}
return false; /* Yeah... we aren't doing that. */
}
/* TODO: Find a better way to do this. Using HashMaps as sets
* *works*, but it's not the best of ideas here. Also, we're using
* strings to compare things, which yeah. */
key_type_id = StrConcat(3, auth_pdu.type, ",", auth_pdu.state_key);
if (HashMapGet(state_keytype, key_type_id))
{
/* Duplicate found! We actually ignore it's actual value. */
if (errp)
{
*errp = "Duplicate auth event was found";
}
JsonFree(event);
PduV1Free(&auth_pdu);
HashMapFree(state_keytype);
Free(key_type_id);
return false;
}
/* Whenever event is valid or not really doesn't matter, as we're
* not using it's value anywhere. */
HashMapSet(state_keytype, key_type_id, event);
Free(key_type_id);
/* Step 2.2: If there are entries whose type and state_key don't
* match those specified by the auth events selection algorithm
* described in the server specification, reject. */
if (!ValidAuthEventV1(&auth_pdu, &pdu))
{
if (errp)
{
*errp = "Invalid authevent given.";
}
JsonFree(event);
PduV1Free(&auth_pdu);
HashMapFree(state_keytype);
return false;
}
/* Step 2.3: If there are entries which were themselves rejected
* under the checks performed on receipt of a PDU, reject.
* TODO */
if (!VerifyPDUV1(&auth_pdu))
{
if (errp)
{
*errp = "Event depends on rejected PDUs";
}
PduV1Free(&auth_pdu);
JsonFree(event);
HashMapFree(state_keytype);
return false;
}
/* Step 2.4: If there is no m.room.create event among the entries,
* reject. */
if (!room_create && IsState(&auth_pdu, "m.room.create", ""))
{
room_create = true; /* Here, we check for the opposite. */
}
JsonFree(event);
PduV1Free(&auth_pdu);
}
HashMapFree(state_keytype);
if (!room_create && errp)
{
*errp = "Room creation event was not in the PDU's auth events";
}
return room_create; /* Step 2.4 is actually done here. */
}

68
src/Room/V1/Auth/Create.c Normal file
View file

@ -0,0 +1,68 @@
#include "Room/internal.h"
#include "Room/V1/Auth/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
bool
AuthoriseCreateV1(PduV1 pdu, char **errp)
{
bool ret = true;
CommonID sender = { 0 }, room_id = { 0 };
char *sender_serv = NULL, *id_serv = NULL;
if (ArraySize(pdu.auth_events) > 0)
{
if (errp)
{
*errp = "Room creation event has auth events";
}
ret = false;
goto finish;
}
if (!ParseCommonID(pdu.room_id, &room_id) ||
!ParseCommonID(pdu.sender, &sender))
{
if (errp)
{
*errp = "Couldn't parse the sender/room ID";
}
ret = false;
goto finish;
}
sender_serv = ParserRecomposeServerPart(sender.server);
id_serv = ParserRecomposeServerPart(room_id.server);
if (!StrEquals(sender_serv, id_serv))
{
if (errp)
{
*errp = "Room is not properly namespaced";
}
ret = false;
goto finish;
}
/* TODO: Check room_version as in step 1.3 */
if (!HashMapGet(pdu.content, "creator"))
{
if (errp)
{
*errp = "Room creation event has auth events";
}
ret = false;
goto finish;
}
finish:
if (sender_serv)
{
Free(sender_serv);
}
if (id_serv)
{
Free(id_serv);
}
CommonIDFree(sender);
CommonIDFree(room_id);
return ret;
}

371
src/Room/V1/Auth/Member.c Normal file
View file

@ -0,0 +1,371 @@
#include "Room/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
static bool
AuthorizeInviteMembershipV1(Room * room, PduV1 pdu, State *state, char **errp)
{
int64_t invite_level;
int64_t pdu_level;
JsonValue *third_pi;
/* Step 5.3.1: If content has a third_party_invite property */
if ((third_pi = JsonGet(pdu.content, 1, "third_party_invite")))
{
JsonValue *signed_val, *mxid, *token;
HashMap *third_pi_obj = JsonValueAsObject(third_pi), *signed_obj;
HashMap *third_pi_event;
char *third_pi_id, *thirdpi_event_sender;
/* Step 5.3.1.1: If target user is banned, reject. */
if (RoomUserHasMembership(room, state, pdu.state_key, "ban"))
{
if (errp)
{
*errp = "Step 5.3.1.1 fail: target is banned";
}
return false;
}
/* Step 5.3.1.2: If content.third_party_invite does not have a signed
* property, reject. */
if (!(signed_val = JsonGet(third_pi_obj, 1, "signed")))
{
if (errp)
{
*errp = "Step 5.3.1.2 fail: unsigned 3PII";
}
return false;
}
signed_obj = JsonValueAsObject(signed_val);
/* Step 5.3.1.3: If signed does not have mxid and token properties,
* reject. */
if (!(mxid = JsonGet(signed_obj, 1, "mxid")))
{
if (errp)
{
*errp = "Step 5.3.1.3 fail: no MXID in 3PII";
}
return false;
}
if (!(token = JsonGet(signed_obj, 1, "token")))
{
if (errp)
{
*errp = "Step 5.3.1.3 fail: no token in 3PII";
}
return false;
}
/* Step 5.3.1.4: If mxid does not match state_key, reject. */
if (!StrEquals(JsonValueAsString(mxid), pdu.state_key))
{
if (errp)
{
*errp = "Step 5.3.1.4 fail: 3PII's MXID != state_key";
}
return false;
}
/* Step 5.3.1.5: If there is no m.room.third_party_invite event
* in the current room state with state_key matching token, reject. */
if (!(third_pi_id = StateGet(
state,
"m.room.third_party_invite", JsonValueAsString(token))))
{
if (errp)
{
*errp = "Step 5.3.1.5 fail: no proper 3PII event";
}
return false;
}
third_pi_event = RoomEventFetchRaw(room, third_pi_id);
/* Step 5.3.1.6: If sender does not match sender of the
* m.room.third_party_invite, reject. */
thirdpi_event_sender = JsonValueAsString(JsonGet(third_pi_event, 1, "sender"));
if (!StrEquals(thirdpi_event_sender, pdu.sender))
{
if (errp)
{
*errp = "Step 5.3.1.6 fail: sender does not match 3PII";
}
JsonFree(third_pi_event);
return false;
}
JsonFree(third_pi_event);
/* TODO:
* Step 5.3.1.7: If any signature in signed matches any public key in
* the m.room.third_party_invite event, allow.
*
* The public keys are in content of m.room.third_party_invite as:
* - A single public key in the public_key property.
* - A list of public keys in the public_keys property. */
/* Step 5.3.1.8: Otherwise, reject. */
if (errp)
{
*errp = "Step 5.3.1.8 fail: signature check do not match";
}
return false;
}
/* Step 5.3.2: If the sender's current membership state is not join,
* reject. */
if (!RoomUserHasMembership(room, state, pdu.sender, "join"))
{
if (errp)
{
*errp = "Step 5.3.2 fail: sender is not 'join'ed";
}
return false;
}
/* Step 5.3.3: If target user's current membership state is join or ban, reject. */
if (RoomUserHasMembership(room, state, pdu.state_key, "join") ||
RoomUserHasMembership(room, state, pdu.state_key, "ban"))
{
if (errp)
{
*errp = "Step 5.3.3 fail: target is 'join'|'ban'd";
}
return false;
}
/* Step 5.3.4: If the sender's power level is greater than or equal to the
* invite level, allow. */
invite_level = RoomMinPL(room, state, NULL, "invite");
pdu_level = RoomUserPL(room, state, pdu.sender);
if (pdu_level >= invite_level)
{
return true;
}
/* Step 5.3.5: Otherwise, reject. */
if (errp)
{
*errp = "Step 5.3.5 fail: sender has no permissions to do so";
}
return false;
}
static bool
AuthorizeLeaveMembershipV1(Room * room, PduV1 pdu, State *state, char **errp)
{
int64_t ban_level = RoomMinPL(room, state, NULL, "ban");
int64_t kick_level = RoomMinPL(room, state, NULL, "kick");
int64_t sender_level = RoomUserPL(room, state, pdu.sender);
int64_t target_level = RoomUserPL(room, state, pdu.state_key);
/* Step 5.4.1: If the sender matches state_key, allow if and only if
* that user's current membership state is invite or join. */
if (StrEquals(pdu.sender, pdu.state_key))
{
bool flag =
RoomUserHasMembership(room, state, pdu.sender, "invite") ||
RoomUserHasMembership(room, state, pdu.sender, "join");
if (!flag && errp)
{
*errp = "Step 5.4.1 fail: user tries to leave but is "
"~'invite' AND ~'join'.";
}
return flag;
}
/* Step 5.4.2: If the sender's current membership state is not join,
* reject. */
if (!RoomUserHasMembership(room, state, pdu.sender, "join"))
{
if (errp)
{
*errp = "Step 5.4.2 fail: sender tries to kick but is "
"~'invite'.";
}
return false;
}
/* Step 5.4.3: If the target user's current membership state is ban,
* and the sender's power level is less than the ban level, reject. */
if (RoomUserHasMembership(room, state, pdu.state_key, "ban") &&
sender_level < ban_level)
{
if (errp)
{
*errp = "Step 5.4.3 fail: sender tries to unban but has no "
"permissions to do so.";
}
return false;
}
/* Step 5.4.4: If the sender's power level is greater than or equal to
* the kick level, and the target user's power level is less than the
* sender's power level, allow. */
if ((sender_level >= kick_level) && target_level < sender_level)
{
return true;
}
/* Step 5.4.5: Otherwise, reject. */
if (errp)
{
*errp = "Step 5.4.5 fail: sender tried to kick but has no "
"permissions to do so.";
}
return false;
}
static bool
AuthorizeBanMembershipV1(Room * room, PduV1 pdu, State *state, char **errp)
{
int64_t ban_pl, pdu_pl, target_pl;
/* Step 5.5.1: If the sender's current membership state is not join, reject. */
if (!RoomUserHasMembership(room, state, pdu.sender, "join"))
{
if (errp)
{
*errp = "Step 5.5.1 fail: sender tries to ban but is not "
"'join'ed";
}
return false;
}
/* Step 5.5.2: If the sender's power level is greater than or equal
* to the ban level, and the target user's power level is less than
* the sender's power level, allow. */
ban_pl = RoomMinPL(room, state, NULL, "ban");
pdu_pl = RoomUserPL(room, state, pdu.sender);
target_pl = RoomUserPL(room, state, pdu.state_key);
if ((pdu_pl >= ban_pl) && (target_pl < pdu_pl))
{
return true;
}
/* Step 5.5.3: Otherwise, reject. */
if (errp)
{
*errp = "Step 5.5.3 fail: sender tries to ban has no permissions to "
"do so";
}
return false;
}
static bool
AuthorizeJoinMembershipV1(Room * room, PduV1 pdu, State *state, char **errp)
{
/* Step 5.2.1: If the only previous event is an m.room.create and the
* state_key is the creator, allow. */
Array *prev = pdu.prev_events;
if (ArraySize(prev) == 1)
{
/* Interperet prev properly, as a list of JsonObjects. */
char *prev_id = JsonValueAsString(ArrayGet(prev, 0));
HashMap *prev_event = RoomEventFetchRaw(room, prev_id);
bool flag = false;
if (prev_event)
{
char *type = JsonValueAsString(HashMapGet(prev_event, "type"));
char *sender = JsonValueAsString(HashMapGet(prev_event, "sender"));
if (StrEquals(type, "m.room.create") &&
StrEquals(sender, pdu.state_key))
{
flag = true;
}
}
JsonFree(prev_event);
if (flag)
{
return true;
}
}
/* Step 5.2.2: If the sender does not match state_key, reject. */
if (!StrEquals(pdu.sender, pdu.state_key))
{
if (errp)
{
*errp = "Step 5.2.2 fail: sender does not match statekey "
"on 'join'";
}
return false;
}
/* Step 5.2.3: If the sender is banned, reject. */
if (RoomUserHasMembership(room, state, pdu.sender, "ban"))
{
if (errp)
{
*errp = "Step 5.2.2 fail: sender is banned on 'join'";
}
return false;
}
/* Step 5.2.4: If the join_rule is invite then allow if membership
* state is invite or join. */
if (RoomIsJoinRule(room, state, "invite") &&
(RoomUserHasMembership(room, state, pdu.sender, "invite") ||
RoomUserHasMembership(room, state, pdu.sender, "join")))
{
return true;
}
/* Step 5.2.5: If the join_rule is public, allow. */
if (RoomIsJoinRule(room, state, "public"))
{
return true;
}
/* Step 5.2.6: Otherwise, reject. */
if (errp)
{
*errp = "Step 5.2.6 fail: join_rule does not allow 'join'";
}
return false;
}
bool
AuthoriseMemberV1(Room * room, PduV1 pdu, State *state, char **errp)
{
JsonValue *membership;
char *membership_str;
/* Step 5.1: If there is no state_key property, or no membership
* property in content, reject. */
if (!pdu.state_key ||
StrEquals(pdu.state_key, "") ||
!(membership = JsonGet(pdu.content, 1, "membership")))
{
if (errp)
{
*errp = "Step 5.1 fail: broken membership's statekey/membership";
}
return false;
}
if (JsonValueType(membership) != JSON_STRING)
{
/* Also check for the type */
if (errp)
{
*errp = "Step 5.1 fail: broken membership's membership";
}
return false;
}
membership_str = JsonValueAsString(membership);
#define JumpIfMembership(mem, func) do { \
if (StrEquals(membership_str, mem)) \
{ \
return func(room, pdu, state, errp); \
} \
} while (0)
/* Step 5.2: If membership is join. */
JumpIfMembership("join", AuthorizeJoinMembershipV1);
/* Step 5.3: If membership is invite. */
JumpIfMembership("invite", AuthorizeInviteMembershipV1);
/* Step 5.4: If membership is leave. */
JumpIfMembership("leave", AuthorizeLeaveMembershipV1);
/* Step 5.5: If membership is ban. */
JumpIfMembership("ban", AuthorizeBanMembershipV1);
/* Step 5.6: Otherwise, the membership is unknown. Reject. */
return false;
#undef JumpIfMembership
}

182
src/Room/V1/Auth/PL.c Normal file
View file

@ -0,0 +1,182 @@
#include "Room/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
bool
AuthorisePowerLevelsV1(Room * room, PduV1 pdu, State *state)
{
/* Step 10.1: If the users property in content is not an object with
* keys that are valid user IDs with values that are integers
* (or a string that is an integer), reject. */
JsonValue *users = JsonGet(pdu.content, 1, "users");
HashMap *users_o, *prev_plevent;
char *user_id, *prev_pl_id, *ev_type;
JsonValue *power_level, *ev_obj;
bool flag = true;
int64_t userpl = RoomUserPL(room, state, pdu.sender);
HashMap *event_obj;
if (JsonValueType(users) != JSON_OBJECT)
{
return false;
}
users_o = JsonValueAsObject(users);
while (HashMapIterate(users_o, &user_id, (void **) &power_level))
{
CommonID as_cid;
if (!flag)
{
continue;
}
if (!ParseCommonID(user_id, &as_cid))
{
flag = false;
}
if (as_cid.sigil != '@')
{
CommonIDFree(as_cid);
flag = false;
continue;
}
/* Verify powerlevels.
* We'll use INT64_MAX as a sentinel value, as this isn't
* a valid powervalue for the specification. */
if (ParsePL(power_level, INT64_MAX) == INT64_MAX)
{
flag = false;
CommonIDFree(as_cid);
continue;
}
CommonIDFree(as_cid);
}
/* HashMapIterate does not support breaking, so we just set a
* flag to be used. */
if (!flag)
{
return false;
}
/* Step 10.2: If there is no previous m.room.power_levels event
* in the room, allow. */
if (!(prev_pl_id = StateGet(state, "m.room.power_levels", "")))
{
return true;
}
/* Step 10.3: For the properties users_default, events_default,
* state_default, ban, redact, kick, invite, check if they were
* added, changed or removed. For each found alteration: */
prev_plevent = RoomEventFetchRaw(room, prev_pl_id);
#define CheckChange(prop) do \
{ \
JsonValue *old = \
JsonGet(prev_plevent, 2, "content", prop);\
JsonValue *new = \
JsonGet(pdu.content, 1, prop); \
int64_t oldv = 0, newv = 0; \
oldv = JsonValueAsInteger(old); \
newv = JsonValueAsInteger(new); \
if ((old && !new) || (!old && new) || \
(oldv != newv)) \
{ \
if (old && (oldv > userpl)) \
{ \
return false; \
} \
if (new && (newv > userpl)) \
{ \
return false; \
} \
} \
} \
while(0)
CheckChange("users_default");
CheckChange("events_default");
CheckChange("state_default");
CheckChange("ban");
CheckChange("redact");
CheckChange("kick");
CheckChange("invite");
#undef CheckChange
#define CheckPLOld(prop) \
event_obj = \
JsonValueAsObject(JsonGet(prev_plevent, 2, "content", prop)); \
flag = true; \
while (HashMapIterate(event_obj, &ev_type, (void **) &ev_obj)) \
{ \
JsonValue *new; \
int64_t new_pl, old_pl; \
\
if (!flag) \
{ \
continue; \
} \
\
new = JsonGet(pdu.content, 2, prop, ev_type); \
old_pl = ParsePL(new, INT64_MAX); \
if (((new_pl = ParsePL(new, INT64_MAX)) != INT64_MAX) && \
((new_pl != old_pl) && old_pl > userpl)) \
{ \
flag = false; \
} \
} \
if (!flag) \
{ \
JsonFree(prev_plevent); \
return false; \
} \
flag = true
#define CheckPLNew(prop) \
event_obj = \
JsonValueAsObject(JsonGet(pdu.content, 1, prop)); \
flag = true; \
while (HashMapIterate(event_obj, &ev_type, (void **) &ev_obj)) \
{ \
JsonValue *old; \
int64_t new_pl, old_pl; \
\
if (!flag) \
{ \
continue; \
} \
\
old = JsonGet(prev_plevent, 3, "content", prop, ev_type); \
new_pl = ParsePL(ev_obj, INT64_MAX); \
if (((old_pl = ParsePL(old, INT64_MAX)) != INT64_MAX && \
new_pl != old_pl) && new_pl > userpl) \
{ \
flag = false; \
} \
} \
if (!flag) \
{ \
JsonFree(prev_plevent); \
return false; \
} \
flag = true
/* Step 10.4: For each entry being changed in, or removed from, the
* events property:
* - If the current value is greater than the sender's current
* power level, reject. */
CheckPLOld("events");
/* Step 10.5: For each entry being added to, or changed in, the events
* property:
* - If the new value is greater than the sender's current power level,
* reject. */
CheckPLNew("events");
/* Steps 10.6 and 10.7 are effectively the same. */
CheckPLOld("users");
CheckPLNew("users");
#undef CheckPLOld
#undef CheckPLNew
/* Step 10.8: Otherwise, allow. */
JsonFree(prev_plevent);
return true;
}

View file

@ -0,0 +1,35 @@
#ifndef TELODENDRIA_IROOM_V1_AUTH_H
#define TELODENDRIA_IROOM_V1_AUTH_H
#include "Room/internal.h"
/**
* Verifies if a room creation PDU would be allowed
* by the auth rules in room version 1 (step 1)
*/
extern bool AuthoriseCreateV1(PduV1, char **);
/**
* Verifies if an auth event PDU would be allowed by the
* auth rules in room version 1 (step 2)
*/
extern bool ConsiderAuthEventsV1(Room *, PduV1, char **);
/**
* Verifies if an alias PDU would be authorised in room
* version 1 (step 4)
*/
extern bool AuthoriseAliasV1(PduV1, char **);
/**
* Verifies if a membership PDUv1 would be allowed by the
* auth rules in room version 1 (step 5)
*/
extern bool AuthoriseMemberV1(Room *, PduV1, State *, char **);
/**
* Verifies if a PDU (power levels) would be allowed by
* the auth rules in room version 1 (step 10)
*/
extern bool AuthorisePowerLevelsV1(Room *, PduV1, State *);
#endif

17
src/Room/V1/Info.c Normal file
View file

@ -0,0 +1,17 @@
#include "Room/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
bool
RoomIsSoftfailedV1(PduV1 pdu)
{
return pdu._unsigned.pdu_status == PDUV1_STATUS_SOFTFAIL;
}
bool
RoomIsRejectedV1(PduV1 pdu)
{
return pdu._unsigned.pdu_status == PDUV1_STATUS_SOFTFAIL;
}

76
src/Room/V1/Populate.c Normal file
View file

@ -0,0 +1,76 @@
#include "Room/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
bool
PopulateEventV1(Room * room, HashMap * event, PduV1 * pdu, ServerPart serv)
{
char *unused;
Array *prev_events;
size_t i;
CommonID cid;
if (PduV1FromJson(event, pdu, &unused))
{
/* TODO: Clean up some fields */
return true;
}
/* Consider the PDU dropped by default */
pdu->_unsigned.pdu_status = PDUV1_STATUS_DROPPED;
/* TODO: Create a PDU of our own, signed and everything.
* https://telodendria.io/blog/matrix-protocol-overview
* has some ideas on how this could be done(up until stage 5). */
pdu->sender =
StrDuplicate(JsonValueAsString(JsonGet(event, 1, "sender")));
pdu->type =
StrDuplicate(JsonValueAsString(JsonGet(event, 1, "type")));
pdu->redacts =
StrDuplicate(JsonValueAsString(JsonGet(event, 1, "redacts")));
pdu->_unsigned.transaction_id =
StrDuplicate(JsonValueAsString(JsonGet(event, 1, "transaction")));
if (JsonGet(event, 1, "state_key"))
{
pdu->state_key =
StrDuplicate(JsonValueAsString(JsonGet(event, 1, "state_key")));
}
pdu->auth_events = ArrayCreate();
pdu->origin_server_ts = UtilTsMillis();
pdu->content =
JsonDuplicate(JsonValueAsObject(JsonGet(event, 1, "content")));
pdu->room_id = StrDuplicate(room->id);
pdu->signatures = HashMapCreate();
pdu->depth = RoomGetDepth(room) + 1;
pdu->depth = pdu->depth >= INT64_MAX ? INT64_MAX : pdu->depth;
/* Create a random event ID for this PDU.
* TODO: Optionally verify whenever it's already used, but that
* would be unlikely considering the lengths of event IDs. */
cid.sigil = '$';
cid.local = StrRandom(32);
cid.server.hostname = StrDuplicate(serv.hostname);
cid.server.port = serv.port ? StrDuplicate(serv.port) : NULL;
pdu->event_id = ParserRecomposeCommonID(cid);
CommonIDFree(cid);
/* Fill prev_events with actual event data.
* Note that we don't actually *clear* out these from our list, as
* that should be done later. */
pdu->prev_events = ArrayCreate();
prev_events = RoomPrevEventsGet(room);
for (i = 0; i < ArraySize(prev_events); i++)
{
HashMap *event = JsonValueAsObject(ArrayGet(prev_events, i));
JsonValue *event_id = JsonGet(event, 1, "event_id");
ArrayAdd(pdu->prev_events, JsonValueDuplicate(event_id));
}
/* TODO: Signatures.
* We currently *don't* have an Ed25519 implementation. */
return false;
}

565
src/Room/V1/Send.c Normal file
View file

@ -0,0 +1,565 @@
#include "Room/internal.h"
#include <Schema/Relation.h>
#include <Schema/PduV1.h>
#include <Parser.h>
static char *
RoomHashEventV1(PduV1 pdu)
{
HashMap *json = PduV1ToJson(&pdu);
char *b64;
b64 = EventContentHash(json);
JsonFree(json);
return b64;
}
static bool
EventFits(HashMap *pdu)
{
int size = CanonicalJsonEncode(pdu, NULL);
JsonValue *key;
/* Main PDU length is 65536 bytes */
if (size > 65536)
{
return false;
}
#define VerifyKey(k,s) do \
{ \
if ((key = JsonGet(pdu, 1, k)) && \
(strlen(JsonValueAsString(key)) > s)) \
{ \
return false; \
} \
} \
while (0)
VerifyKey("sender", 255);
VerifyKey("room_id", 255);
VerifyKey("state_key", 255);
VerifyKey("type", 255);
VerifyKey("event_id", 255);
return true;
#undef VerifyKey
}
static bool
EventFitsV1(PduV1 pdu)
{
HashMap *hm;
bool ret;
hm = PduV1ToJson(&pdu);
ret = EventFits(hm);
JsonFree(hm);
return ret;
}
/* TODO: Rejection */
static PduV1Status
RoomGetEventStatusV1(Room *room, PduV1 *pdu, State *prev, bool client, char **errp)
{
if (!room || !pdu || !prev)
{
if (errp)
{
*errp = "Illegal arguments given to RoomGetEventStatusV1";
}
return PDUV1_STATUS_DROPPED;
}
if (!EventFitsV1(*pdu))
{
/* Reject this event as it is too large. */
if (errp)
{
*errp = "PDU is too large to fit";
}
return PDUV1_STATUS_DROPPED;
}
if (!RoomAuthoriseEventV1(room, *pdu, prev, errp))
{
/* Reject this event as the PDU's own state does not allow it. */
return PDUV1_STATUS_DROPPED;
}
if (!client)
{
/* Checking for soft-failure is not that useful in that case,
* we're only doing pointless computation. */
State *current = StateCurrent(room);
if (!RoomAuthoriseEventV1(room, *pdu, current, errp))
{
StateFree(current);
return PDUV1_STATUS_SOFTFAIL;
}
StateFree(current);
}
return PDUV1_STATUS_ACCEPTED;
}
static void
RedactPDU1(HashMap *obj)
{
Array *keys;
HashMap *content;
char *type = JsonValueAsString(HashMapGet(obj, "type"));
size_t i;
if (!obj)
{
return;
}
keys = HashMapKeys(obj);
for (i = 0; i < ArraySize(keys); i++)
{
char *key = ArrayGet(keys, i);
if (!StrEquals(key, "event_id") && !StrEquals(key, "type") &&
!StrEquals(key, "room_id") && !StrEquals(key, "sender") &&
!StrEquals(key, "state_key")&& !StrEquals(key, "content") &&
!StrEquals(key, "hashes") && !StrEquals(key, "signatures") &&
!StrEquals(key, "depth") && !StrEquals(key, "prev_events") &&
!StrEquals(key, "auth_events")&& !StrEquals(key, "origin") &&
!StrEquals(key, "unsigned") &&
!StrEquals(key, "origin_server_ts")&&!StrEquals(key, "membership"))
{
JsonValueFree(HashMapDelete(obj, key));
continue;
}
}
ArrayFree(keys);
JsonValueFree(HashMapSet(
obj,
"unsigned", JsonValueObject(HashMapCreate())
));
content = JsonValueAsObject(HashMapGet(obj, "content"));
keys = HashMapKeys(content);
for (i = 0; i < ArraySize(keys); i++)
{
char *key = ArrayGet(keys, i);
if (StrEquals(type, "m.room.member"))
{
if (StrEquals(key, "membership"))
{
continue;
}
}
if (StrEquals(type, "m.room.create"))
{
if (StrEquals(key, "creator"))
{
continue;
}
}
if (StrEquals(type, "m.room.join_rules"))
{
if (StrEquals(key, "join_rule"))
{
continue;
}
}
if (StrEquals(type, "m.room.aliases"))
{
if (StrEquals(key, "aliases"))
{
continue;
}
}
if (StrEquals(type, "m.room.history_visibility"))
{
if (StrEquals(key, "history_visibility"))
{
continue;
}
}
if (StrEquals(type, "m.room.power_levels"))
{
if (StrEquals(key, "ban") ||
StrEquals(key, "events") ||
StrEquals(key, "events_default") ||
StrEquals(key, "kick") ||
StrEquals(key, "redact") ||
StrEquals(key, "state_default") ||
StrEquals(key, "users") ||
StrEquals(key, "users_default"))
{
continue;
}
}
JsonValueFree(HashMapDelete(content, key));
}
ArrayFree(keys);
}
bool
RoomAddEventV1(Room *room, PduV1 pdu, PduV1Status status)
{
DbRef *event_ref;
Array *prev_events = NULL, *leaves = NULL;
HashMap *leaves_json = NULL, *pdu_json = NULL;
State *prev_state = NULL;
JsonValue *leaves_val;
char *safe_id, *prev_id;
size_t i;
if (!room || room->version >= 3 ||
status == PDUV1_STATUS_DROPPED)
{
return false;
}
/* Insert our PDU into the event table, regardless of status */
safe_id = CreateSafeID(pdu.event_id);
event_ref = DbCreate(room->db, 4, "rooms", room->id, "events", safe_id);
pdu_json = PduV1ToJson(&pdu);
prev_state = StateResolve(room, pdu_json);
prev_id = StrDuplicate(StateGet(prev_state, pdu.type, pdu.state_key));
StateFree(prev_state);
/* TODO: Use those values concretely. */
pdu._unsigned.pdu_status = status;
JsonSet(DbJson(event_ref), JsonValueObject(pdu_json), 1, "pdu");
pdu_json = NULL;
JsonSet(
DbJson(event_ref),
JsonValueString(PduV1StatusToStr(status)),
1, "status"
);
JsonSet(
DbJson(event_ref),
JsonValueString(prev_id),
1, "prev_event"
);
JsonSet(
DbJson(event_ref),
JsonValueArray(ArrayCreate()),
1, "next_events"
);
JsonSet(
DbJson(event_ref),
JsonValueString(pdu._unsigned.transaction_id),
1, "transaction"
);
DbUnlock(room->db, event_ref);
Free(safe_id);
Free(prev_id);
/* Only accepted PDUs get to do the news */
if (status == PDUV1_STATUS_ACCEPTED)
{
/* Remove managed leaves here. */
leaves_json = DbJson(room->leaves_ref);
leaves_val = JsonValueDuplicate(JsonGet(leaves_json, 1, "leaves"));
leaves = JsonValueAsArray(leaves_val);
Free(leaves_val); /* We do not care about the array's JSON shell. */
prev_events = pdu.prev_events;
for (i = 0; i < ArraySize(prev_events); i++)
{
JsonValue *event_val = ArrayGet(prev_events, i);
char *event_id = JsonValueAsString(event_val);
size_t j;
ssize_t delete_index = -1;
for (j = 0; j < ArraySize(leaves); j++)
{
JsonValue *leaf_val = ArrayGet(leaves, j);
HashMap *leaf_object = JsonValueAsObject(leaf_val);
char *leaf_id =
JsonValueAsString(JsonGet(leaf_object, 1, "event_id"));
if (StrEquals(leaf_id, event_id))
{
delete_index = j;
break;
}
}
if (delete_index == -1)
{
continue;
}
JsonValueFree(ArrayDelete(leaves, delete_index));
}
/* Add our current PDU to the leaves. */
ArrayAdd(leaves, JsonValueObject(PduV1ToJson(&pdu)));
leaves_json = JsonDuplicate(leaves_json);
JsonValueFree(HashMapDelete(leaves_json, "leaves"));
JsonSet(leaves_json, JsonValueArray(leaves), 1, "leaves");
DbJsonSet(room->leaves_ref, leaves_json);
JsonFree(leaves_json);
}
for (i = 0; i < ArraySize(prev_events); i++)
{
JsonValue *event_val = ArrayGet(prev_events, i);
char *id = JsonValueAsString(event_val);
char *error = NULL;
PduV1 prev_pdu = { 0 };
HashMap *prev_object = NULL;
Array *next_events = NULL;
HashMap *refObj = NULL;
HashMap *pduObj = NULL;
/* TODO: This confuses me! */
event_ref = DbLock(room->db, 4, "rooms", room->id, "events", id);
refObj = DbJson(event_ref);
pduObj = JsonValueAsObject(HashMapGet(refObj, "pdu"));
PduV1FromJson(pduObj, &prev_pdu, &error);
next_events = JsonValueAsArray(HashMapGet(refObj, "next_events"));
ArrayAdd(next_events, JsonValueString(pdu.event_id));
prev_object = PduV1ToJson(&prev_pdu);
JsonValueFree(HashMapSet(refObj, "pdu", JsonValueObject(prev_object)));
PduV1Free(&prev_pdu);
DbUnlock(room->db, event_ref);
}
/* Accepted PDUs should be the only one that users should be
* notified about. */
if (status == PDUV1_STATUS_ACCEPTED)
{
HashMap *relates_to =
JsonValueAsObject(HashMapGet(pdu.content, "m.relates_to"));
Relation rel = { 0 };
State *state;
char *type, *state_key, *event_id, *errp = NULL;
pdu_json = PduV1ToJson(&pdu);
/* If we have a membership change, then add it to the
* proper table. */
if (relates_to && RelationFromJson(relates_to, &rel, &errp))
{
DbRef *relate = DbLock(
room->db,
4, "rooms", room->id, "events", rel.event_id
);
HashMap *relObj = DbJson(relate);
if (relObj)
{
Array *relList = JsonValueAsArray(HashMapGet(
relObj, "relations"
));
if (!relList)
{
relList = ArrayCreate();
JsonValueFree(HashMapSet(
relObj, "relations", JsonValueArray(relList)
));
}
/* TODO: We may want to treat the relation as 'special'
* if the specification asks us to bundle it. */
ArrayAdd(relList, JsonValueString(pdu.event_id));
}
RelationFree(&rel);
DbUnlock(room->db, relate);
}
if (StrEquals(pdu.type, "m.room.member"))
{
CommonID *id = UserIdParse(pdu.state_key, NULL);
User *user = UserLockID(room->db, id);
char *membership = JsonValueAsString(
HashMapGet(pdu.content, "membership")
);
if (StrEquals(membership, "join") && user)
{
UserAddJoin(user, room->id);
UserPushJoinSync(user, room->id);
}
else if (StrEquals(membership, "invite") && user)
{
UserAddInvite(user, room->id);
UserPushInviteSync(user, room->id);
}
else if ((StrEquals(membership, "leave") && user) ||
StrEquals(membership, "ban"))
{
UserRemoveInvite(user, room->id);
UserRemoveJoin(user, room->id);
}
UserIdFree(id);
UserUnlock(user);
}
else if (StrEquals(pdu.type, "m.room.redaction") && pdu.redacts)
{
char *redacted = pdu.redacts;
DbRef *eventRef = DbLock(room->db,
4, "rooms", room->id,
"events", redacted
);
RedactPDU1(
JsonValueAsObject(HashMapGet(DbJson(eventRef), "pdu"))
);
JsonValueFree(HashMapSet(
DbJson(eventRef),
"redacted_by", JsonValueString(pdu.event_id)
));
DbUnlock(room->db, eventRef);
}
state = StateCurrent(room);
while (StateIterate(state, &type, &state_key, (void **) &event_id))
{
if (StrEquals(type, "m.room.member"))
{
CommonID *id = UserIdParse(state_key, NULL);
User *user = UserLockID(room->db, id);
UserPushEvent(user, pdu_json);
UserIdFree(id);
UserUnlock(user);
}
Free(type);
Free(state_key);
}
StateFree(state);
JsonFree(pdu_json);
}
return true;
}
HashMap *
RoomEventSendV1(Room * room, HashMap * event, char **errp)
{
PduV1 pdu = { 0 };
HashMap *pdu_object = NULL;
bool client_event, valid = false;
State *state = NULL;
PduV1Status status;
client_event = !PopulateEventV1(room, event, &pdu, RoomGetCreator(room));
pdu_object = PduV1ToJson(&pdu);
state = StateResolve(room, pdu_object);
if (client_event)
{
char *ev_id;
#define AddState(type, key) do \
{ \
ev_id = StateGet(state, type, key); \
if (ev_id) \
{ \
JsonValue *v = JsonValueString(ev_id); \
ArrayAdd(pdu.auth_events, v); \
} \
} \
while (0)
/*
* Implemented from
* https://spec.matrix.org/v1.7/server-server-api/
* #auth-events-selection */
AddState("m.room.create", "");
AddState("m.room.power_levels", "");
AddState("m.room.member", pdu.sender);
if (StrEquals(pdu.type, "m.room.member"))
{
char *target = pdu.state_key;
char *membership =
JsonValueAsString(JsonGet(pdu.content, 1, "membership"));
char *auth_via =
JsonValueAsString(
JsonGet(
pdu.content, 1, "join_authorised_via_users_server")
);
HashMap *inv =
JsonValueAsObject(
JsonGet(pdu.content, 1, "third_party_invite"));
if (target && !StrEquals(target, pdu.sender))
{
AddState("m.room.member", target);
}
if (StrEquals(membership, "join") ||
StrEquals(membership, "invite"))
{
AddState("m.room.join_rules", "");
}
if (StrEquals(membership, "invite") && inv)
{
char *token =
JsonValueAsString(JsonGet(inv, 2, "signed", "token"));
AddState("m.room.third_party_invite", token);
}
if (auth_via && room->version >= 8)
{
AddState("m.room.member", auth_via);
}
}
pdu.hashes.sha256 = RoomHashEventV1(pdu);
#undef AddState
}
/* It seems like we need to behave differently in terms of
* verifying PDUs from the client/federation.
* - In the client, we just do not care about any events that
* are incorrect. We simply drop them, as if they never existed.
* - In the server on the otherhand, the only place where we can
* possibly drop events as such is if it fails signatures. In
* other cases, we *have* to store it(ableit with flags, to
* restrict what we can do).
* - Rejection: We avoid relaying/linking those to anything.
* They must NOT be used for stateres.
* - Softfail: Essentially almost the same as rejects, except
* that they *are* used for stateres.
* I guess a way to do this may be to add a CheckAuthStatus
* function that also verifies if it is a client event, and returns
* an enum:
* - DROPPED: Do NOT process it _at all_
* - REJECT: Process that event as if it was rejected
* - SOFTFAIL: Process the event as if it was softfailed
* The main issue is storing it in the PDU. A naive approach would be to
* add the status to the unsigned field of the PDU, and add functions to
* return the status. I guess that is possible, but then again, can we
* really abuse the unsigned field for this?
*/
/* TODO: For PDU events, we should verify their hashes. */
status = RoomGetEventStatusV1(room, &pdu, state, client_event, errp);
if (status == PDUV1_STATUS_DROPPED)
{
goto finish;
}
StateFree(state);
RoomAddEventV1(room, pdu, status);
state = NULL;
valid = true;
/* If it is a client event, we should make sure that we shout at
* every other homeserver about our new event. */
finish:
if (state)
{
StateFree(state);
}
if (pdu_object)
{
JsonFree(pdu_object);
pdu_object = NULL;
}
if (valid)
{
pdu_object = PduV1ToJson(&pdu);
}
PduV1Free(&pdu);
return pdu_object;
}

90
src/Room/internal.h Normal file
View file

@ -0,0 +1,90 @@
#ifndef TELODENDRIA_IROOM_H
#define TELODENDRIA_IROOM_H
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Util.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Db.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <CanonicalJson.h>
#include <Parser.h>
#include <Config.h>
#include <State.h>
#include <User.h>
#define IsState(p, typ, key) (StrEquals((p)->type, typ) && \
StrEquals((p)->state_key, key))
struct Room
{
Db *db;
DbRef *state_ref;
DbRef *leaves_ref; /* Reference to the leaf list */
ServerPart creator;
char *id;
int version;
};
/**
* Populates a room with the events required for its creation.
*/
extern void RoomPopulate(Room *, User *, RoomCreateRequest *, ServerPart);
/**
* Verifies if the current room state has a joinrule set to a
* specific value.
*/
extern bool RoomIsJoinRule(Room *, State *, char *);
/**
* Tries to parse a PL value from a JsonValue, with a default
* powerlevel, if it cannot parse it.
*/
extern int64_t ParsePL(JsonValue *, int64_t);
/**
* Computes the lowest powerlevel required to execute an action
* in a room.
*/
extern int64_t RoomMinPL(Room *, State *, char *, char *);
/**
* Computes the user's powerlevel at a specific state.
*/
extern int64_t RoomUserPL(Room *, State *, char *);
/**
* Populates an event to a valid PDUv1(and returns true if
* properly created).
*/
extern bool PopulateEventV1(Room *, HashMap *, PduV1 *, ServerPart);
/**
* Sends an event to a room, be it a PDUv1/client event
*/
extern HashMap * RoomEventSendV1(Room *, HashMap *, char **);
/**
* Verifies if the user has a specific membership at a given state.
*/
extern bool RoomUserHasMembership(Room *, State *, char *, char *);
/**
* Computes a PDU's contenthash.
*/
extern char * EventContentHash(HashMap *);
/**
* Creates a new "DB-safe" ID for events.
*/
extern char * CreateSafeID(char *);
#endif

View file

@ -54,6 +54,7 @@ RouterBuild(void)
R("/_matrix/client/v3/auth/(.*)/fallback/web", RouteUiaFallback);
R("/_matrix/client/v3/capabilities", RouteCapabilities);
R("/_matrix/client/v3/devices", RouteDevices);
R("/_matrix/client/v3/login", RouteLogin);
R("/_matrix/client/v3/logout", RouteLogout);
R("/_matrix/client/v3/logout/(all)", RouteLogout);
@ -76,12 +77,35 @@ RouterBuild(void)
R("/_matrix/client/v3/user/(.*)/filter", RouteFilter);
R("/_matrix/client/v3/user/(.*)/filter/(.*)", RouteFilter);
R("/_matrix/client/v3/user/(.*)/account_data/(.*)", RouteLocalData);
R("/_matrix/client/v3/rooms/(.*)/send/(.*)/(.*)", RouteSendEvent);
R("/_matrix/client/v3/rooms/(.*)/redact/(.*)/(.*)", RouteRedact);
R("/_matrix/client/v3/rooms/(.*)/state/(.*)/(.*)", RouteSendState);
R("/_matrix/client/v3/rooms/(.*)/state/(.*)", RouteSendState);
R("/_matrix/client/v3/rooms/(.*)/event/(.*)", RouteFetchEvent);
R("/_matrix/client/v3/rooms/(.*)/(join|leave)", RouteJoinRoom);
R("/_matrix/client/v3/rooms/(.*)/(kick|ban|unban)", RouteKickRoom);
R("/_matrix/client/v3/rooms/(.*)/messages", RouteMessages);
R("/_matrix/client/v3/rooms/(.*)/members", RouteMembers);
R("/_matrix/client/v3/join/(.*)", RouteJoinRoomAlias);
R("/_matrix/client/v3/joined_rooms", RouteJoinedRooms);
R("/_matrix/client/v3/createRoom", RouteCreateRoom);
R("/_matrix/client/v3/sync", RouteSync);
R("/_matrix/client/v3/directory/room/(.*)", RouteAliasDirectory);
R("/_matrix/client/v3/rooms/(.*)/aliases", RouteRoomAliases);
/* Spoofed endpoints, to be TODO'd */
R("/_matrix/client/v3/keys/(query|upload)", RouteKeyQuery);
R("/_matrix/client/v3/pushrules", RoutePushrules);
R("/_matrix/federation/v1/hierarchy/(.*)", RouteHierarchy);
/* Telodendria Admin API Routes */
R("/_telodendria/admin/v1/(restart|shutdown|stats)", RouteProcControl);

338
src/Routes/RouteActRoom.c Normal file
View file

@ -0,0 +1,338 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <string.h>
#include <Config.h>
#include <User.h>
#include <Room.h>
#include <Schema/Filter.h>
ROUTE_IMPL(RouteJoinRoom, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *request = NULL;
HashMap *response = NULL;
User *user = NULL;
char *token = NULL;
CommonID *id = NULL;
char *roomId = ArrayGet(path, 0);
char *action = ArrayGet(path, 1);
char *sender = NULL, *serverName = NULL;
Room *room = NULL;
char *err;
if (!roomId)
{
/* Should be impossible */
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL);
}
if (HttpRequestMethodGet(args->context) != HTTP_POST)
{
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_NOT_JSON, NULL);
goto finish;
}
serverName = ConfigGetServerName(db);
if (!serverName)
{
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
id = UserIdParse(UserGetName(user), serverName);
id->sigil = '@';
sender = ParserRecomposeCommonID(*id);
room = RoomLock(db, roomId);
if (!room)
{
err = "Room ID does not exist.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNKNOWN, err);
goto finish;
}
if (StrEquals(action, "join"))
{
if (RoomContainsUser(room, sender))
{
err = "User is already in the room.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
if (!RoomCanJoin(room, sender))
{
err = "User cannot be in the room.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
/* TODO: Custom reason parameter. */
if (!RoomJoin(room, user, &err))
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_BAD_STATE, err);
goto finish;
}
response = HashMapCreate();
JsonSet(response, JsonValueString(roomId), 1, "room_id");
}
else if (StrEquals(action, "leave"))
{
if (!RoomContainsUser(room, sender))
{
err = "User is not already in the room.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
if (!RoomLeave(room, user, &err))
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_BAD_STATE, err);
goto finish;
}
response = HashMapCreate();
}
finish:
UserIdFree(id);
JsonFree(request);
if (sender)
{
Free(sender);
}
if (serverName)
{
Free(serverName);
}
RoomUnlock(room);
UserUnlock(user);
return response;
}
ROUTE_IMPL(RouteKickRoom, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *request = NULL;
HashMap *response = NULL;
HashMap *content, *membership;
HashMap *pduResponse;
User *user = NULL;
char *token = NULL;
CommonID *id = NULL;
char *roomId = ArrayGet(path, 0);
char *action = ArrayGet(path, 1);
char *kicked = NULL, *reason = NULL;
char *sender = NULL, *serverName = NULL;
char *membershipState;
Room *room = NULL;
char *err;
if (!roomId || !action)
{
/* Should be impossible */
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL);
}
if (StrEquals(action, "kick"))
{
membershipState = "leave";
}
else if (StrEquals(action, "ban"))
{
membershipState = "ban";
}
else if (StrEquals(action, "unban"))
{
membershipState = "leave";
}
else
{
/* Should be impossible */
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL);
}
if (HttpRequestMethodGet(args->context) != HTTP_POST)
{
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_NOT_JSON, NULL);
goto finish;
}
kicked = JsonValueAsString(HashMapGet(request, "reason"));
if (!(kicked = JsonValueAsString(HashMapGet(request, "user_id"))))
{
err = "'user_id' field required(string), but not given";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, err);
goto finish;
}
serverName = ConfigGetServerName(db);
if (!serverName)
{
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
id = UserIdParse(UserGetName(user), serverName);
id->sigil = '@';
sender = ParserRecomposeCommonID(*id);
room = RoomLock(db, roomId);
if (!RoomContainsUser(room, sender))
{
err = "Sender is not already in the room";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
if (RoomContainsUser(room, kicked) == StrEquals(action, "unban"))
{
err = "Victim is not present in the room";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_STATE, err);
goto finish;
}
content = HashMapCreate();
membership = RoomEventCreate(
sender,
"m.room.member", kicked,
content, NULL
);
HashMapSet(content, "membership", JsonValueString(membershipState));
if (reason)
{
HashMapSet(content, "reason", JsonValueString(reason));
}
pduResponse = RoomEventSend(room, membership, &err);
if (!pduResponse)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_BAD_STATE, err);
JsonFree(membership);
goto finish;
}
JsonFree(pduResponse);
JsonFree(membership);
response = HashMapCreate();
finish:
UserIdFree(id);
JsonFree(request);
if (sender)
{
Free(sender);
}
if (serverName)
{
Free(serverName);
}
RoomUnlock(room);
UserUnlock(user);
return response;
}

View file

@ -84,12 +84,12 @@ ROUTE_IMPL(RouteAliasDirectory, path, argp)
switch (HttpRequestMethodGet(args->context))
{
case HTTP_GET:
val = JsonGet(aliases, 2, "alias", alias);
val = JsonGet(aliases, 2, "aliases", alias);
if (val)
{
response = HashMapCreate();
HashMapSet(response, "room_id", JsonValueDuplicate(HashMapGet(JsonValueAsObject(val), "id")));
HashMapSet(response, "servers", JsonValueDuplicate(JsonGet(aliases, 3, "alias", alias, "servers")));
HashMapSet(response, "servers", JsonValueDuplicate(JsonGet(aliases, 3, "aliases", alias, "servers")));
}
else
{

View file

@ -51,7 +51,7 @@ ROUTE_IMPL(RouteCapabilities, path, argp)
/* TODO: When more room versions are implemented, add them to
* roomVersions */
HashMapSet(roomVersions, "1", JsonValueString("unstable"));
HashMapSet(roomVersions, "1", JsonValueString("stable"));
JsonSet(capabilities, JsonValueString("1"), 2, "m.room_versions", "default");
JsonSet(capabilities, JsonValueObject(roomVersions), 2, "m.room_versions", "available");

View file

@ -23,11 +23,14 @@
* SOFTWARE.
*/
#include "Cytoplasm/Http.h"
#include <Routes.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Log.h>
#include <Schema/RoomCreateRequest.h>
#include <Room.h>
#include <User.h>
ROUTE_IMPL(RouteCreateRoom, path, argp)
{
@ -35,11 +38,21 @@ ROUTE_IMPL(RouteCreateRoom, path, argp)
HashMap *request = NULL;
HashMap *response;
RoomCreateRequest parsed;
Room *room = NULL;
Db *db = args->matrixArgs->db;
RoomCreateRequest parsed = { 0 };
User *user = NULL;
Config cfg;
ServerPart server;
char *token;
char *err;
(void) path;
ConfigLock(db, &cfg);
ParseServerPart(cfg.serverName, &server);
if (HttpRequestMethodGet(args->context) != HTTP_POST)
{
err = "Unknown request method.";
@ -48,6 +61,18 @@ ROUTE_IMPL(RouteCreateRoom, path, argp)
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
return response;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
return MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
}
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
@ -56,20 +81,54 @@ ROUTE_IMPL(RouteCreateRoom, path, argp)
goto finish;
}
/* j2s hack: Setting to a default value like that.
* It's definitely NOT good(you can't rely on enums to
* store values outside their limits in the C standard,
* and this line will need to be changed everytime the
* preset list is changed).
*
* I do believe a decent solution to this would be to
* add a 'default' type(maybe initialised to 0 so that
* memsets would work as intended), that *wouldn't* be
* returned by j2s itself.
* TODO. */
parsed.preset = ROOM_CREATE_PRIVATE + 1;
if (!RoomCreateRequestFromJson(request, &parsed, &err))
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, err);
goto finish;
}
if (parsed.room_id && !(UserGetPrivileges(user) & USER_ALIAS))
{
err = "Custom room ID used without ALIAS privileges.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNAUTHORIZED, err);
goto finish;
}
/* No longer need this now that it is parsed */
JsonFree(request);
request = NULL;
if (!(room = RoomCreate(db, user, &parsed, server)))
{
err = "Couldn't create room.";
/* Consider another error status. */
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
response = HashMapCreate();
JsonSet(response, JsonValueString(RoomIdGet(room)), 1, "room_id");
finish:
JsonFree(request);
RoomUnlock(room);
UserUnlock(user);
ConfigUnlock(&cfg);
ServerPartFree(server);
RoomCreateRequestFree(&parsed);
return response;
}

105
src/Routes/RouteDevices.c Normal file
View file

@ -0,0 +1,105 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <string.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Memory.h>
#include <User.h>
ROUTE_IMPL(RouteDevices, path, argp)
{
RouteArgs *args = argp;
HashMap *response = NULL;
HashMap *devices;
char *tokenstr, *deviceID;
JsonValue *deviceVal;
char *msg;
size_t i = 0;
Array *responseDevices;
Db *db = args->matrixArgs->db;
User *user;
if (HttpRequestMethodGet(args->context) != HTTP_GET)
{
msg = "This route only accepts GET.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(M_UNRECOGNIZED, msg);
}
response = MatrixGetAccessToken(args->context, &tokenstr);
if (response)
{
return response;
}
user = UserAuthenticate(db, tokenstr);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
return MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
}
if (!(devices = UserGetDevices(user)))
{
msg = "Couldn't get user's devices. This is not right.";
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL);
goto finish;
}
response = HashMapCreate();
HashMapSet(response,
"devices", JsonValueArray((responseDevices = ArrayCreate()))
);
i = 0;
while (HashMapIterateReentrant(devices, &deviceID, (void **) &deviceVal, &i))
{
HashMap *deviceInfo = HashMapCreate();
HashMap *deviceObj = JsonValueAsObject(deviceVal);
JsonValue *potentialDisplay = HashMapGet(deviceObj, "displayName");
HashMapSet(deviceInfo, "device_id", JsonValueString(deviceID));
HashMapSet(deviceInfo, "display_name", JsonValueDuplicate(potentialDisplay));
/* TODO: As of now, Telodendria does not store enough information to figure out
* last timestamp and IP address. */
ArrayAdd(responseDevices, JsonValueObject(deviceInfo));
}
/* We do not need to free the device object as it lives with the user */
finish:
UserUnlock(user);
(void) path;
return response;
}

View file

@ -0,0 +1,126 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <string.h>
#include <Config.h>
#include <User.h>
#include <Room.h>
#include <Schema/Filter.h>
ROUTE_IMPL(RouteFetchEvent, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *event = NULL;
HashMap *response = NULL;
User *user = NULL;
char *token = NULL;
CommonID *id = NULL;
char *roomId = ArrayGet(path, 0);
char *eventId= ArrayGet(path, 1);
char *sender = NULL, *serverName = NULL;
Room *room = NULL;
char *err;
if (!roomId || !eventId)
{
/* Should be impossible */
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL);
}
if (HttpRequestMethodGet(args->context) != HTTP_GET)
{
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
serverName = ConfigGetServerName(db);
if (!serverName)
{
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
id = UserIdParse(UserGetName(user), serverName);
id->sigil = '@';
sender = ParserRecomposeCommonID(*id);
/* TODO: Better auth(as in check m.room.history_visibility) */
room = RoomLock(db, roomId);
if (!RoomContainsUser(room, sender))
{
err = "User is not in the room.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
event = RoomEventFetch(room, eventId, true);
response = RoomEventClientify(event);
JsonFree(event);
finish:
UserIdFree(id);
if (sender)
{
Free(sender);
}
if (serverName)
{
Free(serverName);
}
RoomUnlock(room);
UserUnlock(user);
return response;
}

View file

@ -30,31 +30,13 @@
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <User.h>
#include <string.h>
#include <Config.h>
#include <User.h>
#include <Schema/Filter.h>
static char *
GetServerName(Db * db)
{
char *name;
Config config;
ConfigLock(db, &config);
if (!config.ok)
{
return NULL;
}
name = StrDuplicate(config.serverName);
ConfigUnlock(&config);
return name;
}
ROUTE_IMPL(RouteFilter, path, argp)
{
RouteArgs *args = argp;
@ -80,7 +62,7 @@ ROUTE_IMPL(RouteFilter, path, argp)
return MatrixErrorCreate(M_UNKNOWN, NULL);
}
serverName = GetServerName(db);
serverName = ConfigGetServerName(db);
if (!serverName)
{
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
@ -189,7 +171,7 @@ ROUTE_IMPL(RouteFilter, path, argp)
DbJsonSet(ref, filterJson);
DbUnlock(db, ref);
Free(filterJson);
JsonFree(filterJson);
response = HashMapCreate();
HashMapSet(response, "filter_id", JsonValueString(filterId));

View file

@ -0,0 +1,84 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Cytoplasm/HttpServer.h>
#include <Routes.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <User.h>
#include <Room.h>
#include <string.h>
#include <Schema/Filter.h>
ROUTE_IMPL(RouteHierarchy, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *request = NULL;
HashMap *response = NULL;
User *user = NULL;
char *token = NULL;
char *err;
if (HttpRequestMethodGet(args->context) != HTTP_GET)
{
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
response = HashMapCreate();
HashMapSet(response, "children", JsonValueArray(ArrayCreate()));
HashMapSet(response, "inaccessible_children", JsonValueArray(ArrayCreate()));
HashMapSet(response, "room", JsonValueObject(HashMapCreate()));
(void) path;
finish:
JsonFree(request);
UserUnlock(user);
return response;
}

View file

@ -0,0 +1,171 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <string.h>
#include <Config.h>
#include <User.h>
#include <Room.h>
#include <Schema/Filter.h>
ROUTE_IMPL(RouteJoinRoomAlias, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *request = NULL;
HashMap *response = NULL;
User *user = NULL;
char *token = NULL;
CommonID *id = NULL;
char *roomId = ArrayGet(path, 0);
char *sender = NULL, *serverName = NULL;
Room *room = NULL;
char *err;
if (!roomId)
{
/* Should be impossible */
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL);
}
if (*roomId != '!')
{
roomId = RoomResolveAlias(db, roomId);
}
else
{
roomId = StrDuplicate(roomId);
}
if (!roomId)
{
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL);
goto finish;
}
if (HttpRequestMethodGet(args->context) != HTTP_POST)
{
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_NOT_JSON, NULL);
goto finish;
}
serverName = ConfigGetServerName(db);
if (!serverName)
{
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
id = UserIdParse(UserGetName(user), serverName);
id->sigil = '@';
sender = ParserRecomposeCommonID(*id);
room = RoomLock(db, roomId);
if (!room)
{
err = "Room ID does not exist.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNKNOWN, err);
goto finish;
}
if (RoomContainsUser(room, sender))
{
err = "User is already in the room.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
if (!RoomCanJoin(room, sender))
{
err = "User cannot be in the room.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
/* TODO: Custom reason parameter. */
if (!RoomJoin(room, user, &err))
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
response = HashMapCreate();
JsonSet(response, JsonValueString(roomId), 1, "room_id");
finish:
Free(roomId);
UserIdFree(id);
JsonFree(request);
if (sender)
{
Free(sender);
}
if (serverName)
{
Free(serverName);
}
RoomUnlock(room);
UserUnlock(user);
return response;
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <User.h>
#include <Room.h>
#include <string.h>
#include <Schema/Filter.h>
ROUTE_IMPL(RouteJoinedRooms, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *response = NULL;
User *user = NULL;
char *token = NULL;
Array *rawRoomList;
Array *roomIds;
char *err;
size_t i;
if (HttpRequestMethodGet(args->context) != HTTP_GET)
{
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
rawRoomList = UserListJoins(user);
roomIds = ArrayCreate();
for (i = 0; i < ArraySize(rawRoomList); i++)
{
ArrayAdd(roomIds, JsonValueString(ArrayGet(rawRoomList, i)));
}
UserFreeList(rawRoomList);
response = HashMapCreate();
HashMapSet(response, "joined_rooms", JsonValueArray(roomIds));
(void) path;
finish:
UserUnlock(user);
return response;
}

View file

@ -0,0 +1,222 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Cytoplasm/HttpServer.h>
#include <Routes.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <User.h>
#include <Room.h>
#include <string.h>
#include <Schema/KeyUpload.h>
HashMap *
UploadKey(RouteArgs *args, User *user, KeyUploadRequest *req, char *sender)
{
char *deviceId = UserGetDeviceId(user);
KeyResponse response = { 0 };
HashMap *json;
char *fbKey;
JsonValue *fbValue;
size_t i;
if (!user || !req || !sender)
{
return NULL;
}
Log(LOG_ERR, "did=%s", deviceId);
if (req->device_keys.user_id)
{
HashMap *publicKeys;
char *pkTag, *pk;
/* We have device key information */
if (!StrEquals(req->device_keys.user_id, sender))
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(
M_UNAUTHORIZED, "Device key update has an invalid user ID"
);
}
if (!StrEquals(req->device_keys.device_id, deviceId))
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(
M_UNAUTHORIZED, "Device key update has an invalid device ID"
);
}
/* Check the public key list */
publicKeys = req->device_keys.keys;
i = 0;
while (HashMapIterateReentrant(publicKeys, &pkTag, (void **) &pk, &i))
{
char *pktDID = strchr(pkTag, ':');
/* Maybe C does need NULL saturation */
pktDID = pktDID ? pktDID + 1 : NULL;
if (!StrEquals(pktDID, deviceId))
{
/* As far as I know, we're not meant to handle other devices'
* public keys */
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
Log(LOG_ERR, "%s!=%s 1", pktDID, deviceId);
return MatrixErrorCreate(
M_UNAUTHORIZED, "Device key update has an invalid device ID"
);
}
}
UserSetDeviceKeys(user, &req->device_keys);
}
UserClearFallbackKeys(user);
i = 0;
while (HashMapIterateReentrant(req->fallback_keys, &fbKey, (void **) &fbValue, &i))
{
char *fbKID = strchr(fbKey, ':');
size_t len = fbKID ? fbKID - fbKey : 0;
char algo[len + 1];
memcpy(algo, fbKey, len);
algo[len] = '\0';
/* Maybe C does need NULL saturation */
fbKID = fbKID ? fbKID + 1 : NULL;
UserAddKey(user, fbKey, fbValue, true);
(void) fbKID;
}
i = 0;
while (HashMapIterateReentrant(req->one_time_keys, &fbKey, (void **) &fbValue, &i))
{
char *fbKID = strchr(fbKey, ':');
size_t len = fbKID ? fbKID - fbKey : 0;
char algo[len + 1];
memcpy(algo, fbKey, len);
algo[len] = '\0';
/* Maybe C does need NULL saturation */
fbKID = fbKID ? fbKID + 1 : NULL;
UserAddKey(user, fbKey, fbValue, false);
(void) fbKID;
}
response.one_time_key_counts = UserGetOnetimeCounts(user);
UserNotifyUser(UserGetName(user));
json = KeyResponseToJson(&response);
KeyResponseFree(&response);
return json;
}
ROUTE_IMPL(RouteKeyQuery, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *request = NULL;
HashMap *response = NULL;
CommonID *id = NULL;
char *token = NULL;
User *user = NULL;
char *serverName = NULL;
char *sender = NULL;
char *method = ArrayGet(path, 0);
char *err;
if (HttpRequestMethodGet(args->context) != HTTP_POST)
{
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
serverName = ConfigGetServerName(db);
id = UserIdParse(UserGetName(user), serverName);
id->sigil = '@';
sender = ParserRecomposeCommonID(*id);
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_NOT_JSON, NULL);
goto finish;
}
if (StrEquals(method, "upload"))
{
KeyUploadRequest upload = { 0 };
if (!KeyUploadRequestFromJson(request, &upload, &err))
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, err);
goto finish;
}
if ((response = UploadKey(args, user, &upload, sender)))
{
KeyUploadRequestFree(&upload);
goto finish;
}
KeyUploadRequestFree(&upload);
}
else if (StrEquals(method, "query"))
{
/* TODO: Fetch a user's key information */
}
response = HashMapCreate();
(void) path;
finish:
JsonFree(request);
UserUnlock(user);
Free(serverName);
UserIdFree(id);
Free(sender);
return response;
}

158
src/Routes/RouteLocalData.c Normal file
View file

@ -0,0 +1,158 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <string.h>
#include <Config.h>
#include <User.h>
#include <Schema/Filter.h>
ROUTE_IMPL(RouteLocalData, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *request = NULL;
HashMap *response = NULL;
User *user = NULL;
CommonID *id = NULL;
char *token = NULL;
char *serverName = NULL;
char *userParam = ArrayGet(path, 0);
char *dataKey = ArrayGet(path, 1);
char *msg;
if (!userParam || !dataKey)
{
/* Should be impossible */
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL);
}
serverName = ConfigGetServerName(db);
if (!serverName)
{
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL);
goto finish;
}
id = UserIdParse(userParam, serverName);
if (!id)
{
msg = "Invalid user ID.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_INVALID_PARAM, msg);
goto finish;
}
if (!ParserServerNameEquals(id->server, serverName))
{
msg = "Cannot use /filter for non-local users.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNAUTHORIZED, msg);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
if (!StrEquals(id->local, UserGetName(user)))
{
msg = "Unauthorized to use /account_data.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_INVALID_PARAM, msg);
goto finish;
}
switch (HttpRequestMethodGet(args->context))
{
case HTTP_GET:
{
HashMap *accountData = UserGetAccountData(user, dataKey);
if (!accountData)
{
msg = "Couldn't find account data.";
HttpResponseStatus(args->context, HTTP_NOT_FOUND);
response = MatrixErrorCreate(M_NOT_FOUND, msg);
goto finish;
}
response = accountData;
}; break;
case HTTP_PUT:
{
if (StrEquals(dataKey, "m.fully_read"))
{
msg = "Cannot use protected data key.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish;
}
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_NOT_JSON, NULL);
goto finish;
}
UserSetAccountData(user, dataKey, request);
response = HashMapCreate();
}; break;
default:
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, NULL);
break;
}
finish:
Free(serverName);
UserIdFree(id);
UserUnlock(user);
JsonFree(request);
return response;
}

175
src/Routes/RouteMembers.c Normal file
View file

@ -0,0 +1,175 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <Config.h>
#include <User.h>
#include <Room.h>
#include <string.h>
#include <stdlib.h>
#include <Schema/Filter.h>
static bool
IsAllowed(HashMap *e, HashMap *params)
{
char *not_membership = HashMapGet(params, "not_membership");
char *membership = HashMapGet(params, "membership");
char *e_membership = JsonValueAsString(JsonGet(e, 2, "content", "membership"));
return StrEquals(e_membership, membership) || !StrEquals(e_membership, not_membership);
}
ROUTE_IMPL(RouteMembers, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *params = HttpRequestParams(args->context);
HashMap *response = NULL;
User *user = NULL;
char *token = NULL;
char *entryType, *entryKey, *entry;
char *serverName = NULL, *sender = NULL;
char *roomId = ArrayGet(path, 0);
char *at = HashMapGet(params, "at");
char *atId = NULL;
CommonID *id = NULL;
Array *messages = NULL, *batch;
Room *room = NULL;
State *state = NULL;
char *err;
if (!roomId)
{
/* Should be impossible */
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL);
}
if (HttpRequestMethodGet(args->context) != HTTP_GET)
{
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
serverName = ConfigGetServerName(db);
if (!serverName)
{
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
id = UserIdParse(UserGetName(user), serverName);
id->sigil = '@';
sender = ParserRecomposeCommonID(*id);
messages = UserGetEvents(user, at, roomId);
atId = ArrayGet(messages, 0);
room = RoomLock(db, roomId);
if (!RoomContainsUser(room, sender))
{
err = "User is not in the room.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
if (!atId)
{
state = StateCurrent(room);
}
else
{
state = RoomStateGetID(room, atId);
}
response = HashMapCreate();
batch = ArrayCreate();
HashMapSet(response, "batch", JsonValueArray(batch));
while (StateIterate(state, &entryType, &entryKey, (void **) &entry))
{
HashMap *event;
if (!StrEquals(entryType, "m.room.member"))
{
Free(entryType);
Free(entryKey);
continue;
}
if (IsAllowed((event = RoomEventFetch(room, entry, true)), params))
{
ArrayAdd(batch, JsonValueObject(event));
event = NULL;
}
Free(entryType);
Free(entryKey);
JsonFree(event);
}
StateFree(state);
RoomUnlock(room);
/* TODO: Filters, to, and friends */
finish:
UserUnlock(user);
UserIdFree(id);
UserFreeList(messages);
if (sender)
{
Free(sender);
}
if (serverName)
{
Free(serverName);
}
return response;
}

172
src/Routes/RouteMessages.c Normal file
View file

@ -0,0 +1,172 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <Config.h>
#include <User.h>
#include <Room.h>
#include <string.h>
#include <stdlib.h>
#include <Schema/Filter.h>
ROUTE_IMPL(RouteMessages, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *params = HttpRequestParams(args->context);
HashMap *response = NULL;
User *user = NULL;
char *token = NULL;
char *roomId = ArrayGet(path, 0);
char *from = HashMapGet(params, "from");
char *limitStr = HashMapGet(params, "limit");
char *end = NULL;
char *sender = NULL;
char *serverName = NULL;
CommonID *id = NULL;
int limit = strtol(limitStr, NULL, 10);
Array *messages = NULL;
Array *state = NULL;
size_t i;
Room *room = NULL;
char *err;
if (!limit || limit < 0)
{
limit = 10;
}
if (!roomId)
{
/* Should be impossible */
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL);
}
if (HttpRequestMethodGet(args->context) != HTTP_GET)
{
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
serverName = ConfigGetServerName(db);
if (!serverName)
{
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
id = UserIdParse(UserGetName(user), serverName);
id->sigil = '@';
sender = ParserRecomposeCommonID(*id);
room = RoomLock(db, roomId);
if (!RoomContainsUser(room, sender))
{
err = "User is not in the room.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
RoomUnlock(room);
/* TODO: Filters, to, and friends */
messages = UserFetchMessages(user, limit, from, &end);
if (!messages)
{
Room *r = RoomLock(db, roomId);
Array *leaf = RoomPrevEventsGet(r);
HashMap *start = JsonValueAsObject(ArrayGet(leaf, 0));
char *startId = StrDuplicate(
JsonValueAsString(HashMapGet(start, "event_id"))
);
char *token = UserNewMessageToken(user, roomId, startId);
RoomUnlock(r);
messages = UserFetchMessages(user, limit, token, &end);
Free(token);
Free(startId);
}
response = HashMapCreate();
state = ArrayCreate();
JsonSet(response, JsonValueArray(messages), 1, "chunk");
for (i = 0; i < ArraySize(messages); i++)
{
HashMap *e = JsonValueAsObject(ArrayGet(messages, i));
if (HashMapGet(e, "state_key"))
{
ArrayAdd(state, JsonValueDuplicate(ArrayGet(messages, i)));
}
}
JsonSet(response, JsonValueArray(state), 1, "state");
JsonSet(response, JsonValueString(from), 1, "start");
if (end)
{
JsonSet(response, JsonValueString(end), 1, "end");
Free(end);
}
UserFreeMessageToken(user, from);
finish:
UserUnlock(user);
UserIdFree(id);
if (sender)
{
Free(sender);
}
if (serverName)
{
Free(serverName);
}
return response;
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Cytoplasm/HttpServer.h>
#include <Routes.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <User.h>
#include <Room.h>
#include <string.h>
#include <Schema/Filter.h>
ROUTE_IMPL(RoutePushrules, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *response = NULL;
User *user = NULL;
char *token = NULL;
char *err;
if (HttpRequestMethodGet(args->context) != HTTP_GET)
{
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
response = HashMapCreate();
JsonSet(response, JsonValueObject(HashMapCreate()), 1, "global");
(void) path;
finish:
UserUnlock(user);
return response;
}

150
src/Routes/RouteRedact.c Normal file
View file

@ -0,0 +1,150 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <string.h>
#include <Config.h>
#include <User.h>
#include <Room.h>
#include <Schema/Filter.h>
ROUTE_IMPL(RouteRedact, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *request = NULL;
HashMap *response = NULL;
User *user = NULL;
CommonID *id = NULL;
char *token = NULL;
char *serverName = NULL;
char *roomId = ArrayGet(path, 0);
char *eventId = ArrayGet(path, 1);
char *transId = ArrayGet(path, 2);
char *redactId = NULL;
char *sender = NULL;
char *reason = NULL;
Room *room = NULL;
char *err = NULL;
if (!roomId || !eventId || !transId)
{
/* Should be impossible */
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL);
}
if (HttpRequestMethodGet(args->context) != HTTP_PUT)
{
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
serverName = ConfigGetServerName(db);
if (!serverName)
{
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_NOT_JSON, NULL);
goto finish;
}
reason = JsonValueAsString(HashMapGet(request, "reason"));
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
id = UserIdParse(UserGetName(user), serverName);
id->sigil = '@';
sender = ParserRecomposeCommonID(*id);
if ((response = UserGetTransaction(user, transId, "redact")))
{
goto finish;
}
room = RoomLock(db, roomId);
if (!RoomContainsUser(room, sender))
{
err = "User is not in the room.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
if (!(redactId = RoomRedact(room, user, eventId, reason, &err)))
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
response = HashMapCreate();
HashMapSet(response, "event_id", JsonValueString(redactId));
UserSetTransaction(user, transId, "redact", response);
Free(redactId);
finish:
RoomUnlock(room);
Free(serverName);
if (sender)
{
Free(sender);
}
UserIdFree(id);
UserUnlock(user);
JsonFree(request);
return response;
}

View file

@ -31,6 +31,7 @@
#include <Matrix.h>
#include <User.h>
#include <Room.h>
ROUTE_IMPL(RouteRoomAliases, path, argp)
{
@ -40,16 +41,15 @@ ROUTE_IMPL(RouteRoomAliases, path, argp)
char *msg;
HashMap *response = NULL;
HashMap *aliases = NULL;
HashMap *reversealias = NULL;
JsonValue *val;
Array *alias = NULL, *arr;
Db *db = args->matrixArgs->db;
DbRef *ref = NULL;
User *user = NULL;
size_t i;
if (HttpRequestMethodGet(args->context) != HTTP_GET)
{
msg = "Route only accepts GET.";
@ -82,10 +82,9 @@ ROUTE_IMPL(RouteRoomAliases, path, argp)
goto finish;
}
ref = DbLock(db, 1, "aliases");
aliases = DbJson(ref);
reversealias = JsonValueAsObject(JsonGet(aliases, 2, "id", roomId));
if (!reversealias)
alias = RoomReverseAlias(db, roomId);
if (!alias)
{
/* We do not know about the room ID. */
msg = "Unknown room ID.";
@ -93,12 +92,17 @@ ROUTE_IMPL(RouteRoomAliases, path, argp)
response = MatrixErrorCreate(M_INVALID_PARAM, msg);
goto finish;
}
arr = ArrayCreate();
for (i = 0; i < ArraySize(alias); i++)
{
char *str = ArrayGet(alias, i);
ArrayAdd(arr, JsonValueString(str));
}
response = HashMapCreate();
val = JsonGet(reversealias, 1, "aliases");
HashMapSet(response, "aliases", JsonValueDuplicate(val));
HashMapSet(response, "aliases", JsonValueArray(arr));
finish:
DbUnlock(db, ref);
RoomFreeReverse(alias);
UserUnlock(user);
return response;
}

306
src/Routes/RouteSendEvent.c Normal file
View file

@ -0,0 +1,306 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <string.h>
#include <Config.h>
#include <User.h>
#include <Room.h>
#include <Schema/Filter.h>
ROUTE_IMPL(RouteSendEvent, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *request = NULL, *event = NULL;
HashMap *response = NULL, *filled = NULL;
User *user = NULL;
CommonID *id = NULL;
char *token = NULL;
char *serverName = NULL;
char *roomId = ArrayGet(path, 0);
char *eventType = ArrayGet(path, 1);
char *transId = ArrayGet(path, 2);
char *sender = NULL;
Room *room = NULL;
char *err = NULL;
if (!roomId || !eventType || !transId)
{
/* Should be impossible */
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL);
}
if (HttpRequestMethodGet(args->context) != HTTP_PUT)
{
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
serverName = ConfigGetServerName(db);
if (!serverName)
{
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_NOT_JSON, NULL);
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
id = UserIdParse(UserGetName(user), serverName);
id->sigil = '@';
sender = ParserRecomposeCommonID(*id);
if ((response = UserGetTransaction(user, transId, "send")))
{
goto finish;
}
room = RoomLock(db, roomId);
if (!RoomContainsUser(room, sender))
{
err = "User is not in the room.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
event = RoomEventCreate(sender, eventType, NULL, JsonDuplicate(request), transId);
filled = RoomEventSend(room, event, &err);
JsonFree(event);
if (!filled)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
response = HashMapCreate();
HashMapSet(
response, "event_id",
JsonValueDuplicate(HashMapGet(filled, "event_id"))
);
JsonFree(filled);
UserSetTransaction(user, transId, "send", response);
finish:
RoomUnlock(room);
Free(serverName);
if (sender)
{
Free(sender);
}
UserIdFree(id);
UserUnlock(user);
JsonFree(request);
return response;
}
ROUTE_IMPL(RouteSendState, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *request = NULL, *event = NULL;
HashMap *response = NULL, *filled = NULL;
User *user = NULL;
CommonID *id = NULL;
char *token = NULL;
char *serverName = NULL;
char *roomId = ArrayGet(path, 0);
char *eventType = ArrayGet(path, 1);
char *stateKey = ArrayGet(path, 2);
char *sender = NULL;
Room *room = NULL;
State *state = NULL;
char *err;
if (!roomId || !eventType)
{
/* Should be impossible */
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL);
}
if (!stateKey)
{
stateKey = "";
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
serverName = ConfigGetServerName(db);
if (!serverName)
{
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL);
goto finish;
}
id = UserIdParse(UserGetName(user), serverName);
id->sigil = '@';
sender = ParserRecomposeCommonID(*id);
switch (HttpRequestMethodGet(args->context))
{
case HTTP_GET:
room = RoomLock(db, roomId);
if (!RoomContainsUser(room, sender))
{
err = "User is not in the room.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
state = StateCurrent(room);
event = RoomEventFetch(
room, StateGet(state, eventType, stateKey), true
);
if (!event)
{
err = "Event could not be found.";
HttpResponseStatus(args->context, HTTP_NOT_FOUND);
response = MatrixErrorCreate(M_UNKNOWN, err);
StateFree(state);
goto finish;
}
response = JsonDuplicate(JsonValueAsObject(
HashMapGet(event, "content")
));
StateFree(state);
JsonFree(event);
break;
case HTTP_PUT:
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_NOT_JSON, NULL);
goto finish;
}
room = RoomLock(db, roomId);
if (!RoomContainsUser(room, sender))
{
err = "User is not in the room.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
event = RoomEventCreate(
sender,
eventType, stateKey ? stateKey : "",
JsonDuplicate(request), NULL
);
filled = RoomEventSend(room, event, &err);
JsonFree(event);
if (!filled)
{
Log(LOG_ERR, "%s", err);
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_FORBIDDEN, err);
goto finish;
}
response = HashMapCreate();
HashMapSet(
response, "event_id",
JsonValueDuplicate(HashMapGet(filled, "event_id"))
);
JsonFree(filled);
break;
default:
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
finish:
RoomUnlock(room);
Free(serverName);
if (sender)
{
Free(sender);
}
UserIdFree(id);
UserUnlock(user);
JsonFree(request);
return response;
}

View file

@ -52,8 +52,8 @@ ROUTE_IMPL(RouteStaticLogin, path, argp)
StreamPuts(stream,
"function buildRequest(user, pass) {"
" var d = findGetParameter('device_id');"
" var i = findGetParameter('initial_device_display_name');"
" var r = findGetParameter('refresh_token') === 'true';"
" var i = findGetParameter('initial_device_display_name');"
" var r = findGetParameter('refresh_token') === 'true';"
" var request = {};"
" request['type'] = 'm.login.password';"
" request['identifier'] = {"
@ -88,9 +88,9 @@ ROUTE_IMPL(RouteStaticLogin, path, argp)
StreamPuts(stream,
"onFormSubmit('login-form', (frm) => {"
" var user = document.getElementById('user').value;"
" var pass = document.getElementById('password').value;"
" var pass = document.getElementById('password').value;"
" if (!user || !pass) {"
" setFormError('Please provide a username and password.');"
" setFormError('Please provide a username and password.');"
" return;"
" }"
" setFormError(null);"

View file

@ -69,7 +69,7 @@ ROUTE_IMPL(RouteStaticResources, path, argp)
"function jsonRequest(meth, url, json, cb) {"
" var xhr = new XMLHttpRequest();"
" xhr.open(meth, url);"
" xhr.setRequestHeader('Content-Type', 'application/json');"
" xhr.setRequestHeader('Content-Type', 'application/json');"
" xhr.onreadystatechange = () => {"
" if (xhr.readyState == 4) {"
" cb(xhr);"

280
src/Routes/RouteSync.c Normal file
View file

@ -0,0 +1,280 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Cytoplasm/HttpServer.h>
#include <Routes.h>
#include <Schema/SyncResponse.h>
#include <Schema/Filter.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Util.h>
#include <Cytoplasm/Str.h>
#include <Filter.h>
#include <State.h>
#include <User.h>
#include <Room.h>
#include <string.h>
#include <stdlib.h>
static ClientEventWithoutRoomID
ClientfyEventSync(HashMap *pdu)
{
ClientEventWithoutRoomID ret = { 0 };
char *ignored;
ClientEventWithoutRoomIDFromJson(pdu, &ret, &ignored);
return ret;
}
ROUTE_IMPL(RouteSync, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
HashMap *params = NULL;
HashMap *response = NULL;
SyncResponse sync = { 0 };
Filter *filterData = NULL;
Array *accountData;
Array *invites;
Array *joins;
size_t i;
User *user = NULL;
char *token = NULL;
char *prevBatch = NULL;
char *nextBatch = NULL;
char *currBatch = NULL;
char *timeout = NULL;
char *filter = NULL;
char *err;
int timeoutDuration;
/* TODO: Respect `timeout', (and stop when something is
* pushed, maybe by 'polling' the database? sounds like
* a bad idea) */
if (HttpRequestMethodGet(args->context) != HTTP_GET)
{
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
params = HttpRequestParams(args->context);
prevBatch = HashMapGet(params, "since");
timeout = HashMapGet(params, "timeout");
filter = HashMapGet(params, "filter");
timeoutDuration = timeout ? atoi(timeout) : 0;
if (filter)
{
char *userName = UserGetName(user);
if (!(filterData = FilterDecode(db, userName, filter, &err)))
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, err);
}
}
if (!prevBatch)
{
prevBatch = NULL;
nextBatch = UserInitSyncDiff(user);
UserFillSyncDiff(user, nextBatch);
}
else if (timeout && timeoutDuration)
{
char *name = StrDuplicate(UserGetName(user));
UserUnlock(user);
UserAwaitNotification(name, timeoutDuration);
Free(name);
user = UserAuthenticate(db, token);
}
currBatch = prevBatch ? prevBatch : nextBatch;
/* TODO: I only am manually parsing this because j2s does not support
* a hashmap of unknown keys pointing to a known type. */
sync.rooms.invite = NULL;
sync.rooms.join = NULL;
sync.account_data.events = NULL;
sync.device_one_time_keys_count = UserGetOnetimeCounts(user);
/* account data */
accountData = UserGetAccountDataSync(user, currBatch);
if (ArraySize(accountData) > 0)
{
sync.account_data.events = ArrayCreate();
}
for (i = 0; i < ArraySize(accountData); i++)
{
char *key = ArrayGet(accountData, i);
Event *event = Malloc(sizeof(*event));
event->type = StrDuplicate(key);
event->content = UserGetAccountData(user, key);
ArrayAdd(sync.account_data.events, event);
}
UserFreeList(accountData);
/* invites */
invites = UserGetInvites(user, currBatch);
if (ArraySize(invites) > 0)
{
sync.rooms.invite = HashMapCreate();
}
for (i = 0; i < ArraySize(invites); i++)
{
char *roomId = ArrayGet(invites, i);
InvitedRooms *invited;
if (IsRoomFiltered(filterData, roomId))
{
continue;
}
invited = Malloc(sizeof(*invited));
memset(invited, 0, sizeof(*invited));
// TODO: Populate the invitestate
invited->invite_state.events = ArrayCreate();
HashMapSet(sync.rooms.invite, roomId, invited);
}
UserFreeList(invites);
/* Joins */
joins = UserGetJoins(user, currBatch);
if (ArraySize(joins) > 0)
{
sync.rooms.join = HashMapCreate();
}
for (i = 0; i < ArraySize(joins); i++)
{
char *roomId = ArrayGet(joins, i);
JoinedRooms *joined;
char *firstEvent = NULL;
char *type, *key, *id;
State *state;
Array *el;
size_t j;
Room *r;
if (IsRoomFiltered(filterData, roomId))
{
continue;
}
joined = Malloc(sizeof(*joined));
memset(joined, 0, sizeof(*joined));
el = UserGetEvents(user, currBatch, roomId);
r = RoomLock(db, roomId);
state = StateCurrent(r);
joined->timeline.events = ArrayCreate();
for (j = 0; j < ArraySize(el); j++)
{
char *event = ArrayGet(el, j);
HashMap *eventObj = RoomEventFetch(r, event, true);
HashMap *filteredObj = FilterApply(filterData, eventObj);
if (filteredObj)
{
ClientEventWithoutRoomID rc = ClientfyEventSync(filteredObj);
ClientEventWithoutRoomID *c = Malloc(sizeof(*c));
memcpy(c, &rc, sizeof(*c));
if (!firstEvent)
{
firstEvent = c->event_id;
}
ArrayAdd(joined->timeline.events, c);
}
JsonFree(eventObj);
JsonFree(filteredObj);
}
joined->timeline.prev_batch = UserNewMessageToken(
user, roomId, firstEvent
);
// TODO: Don't shove the entire state.
// That's a recipe for disaster, especially on large rooms.
joined->state.events = ArrayCreate();
while (StateIterate(state, &type, &key, (void **) &id))
{
HashMap *e = RoomEventFetch(r, id, true);
ArrayAdd(joined->state.events, JsonValueObject(e));
Free(type);
Free(key);
}
StateFree(state);
RoomUnlock(r);
UserFreeList(el);
HashMapSet(sync.rooms.join, roomId, joined);
}
UserFreeList(joins);
if (prevBatch)
{
/* TODO: Should we be dropping syncs? */
//UserDropSync(user, prevBatch);
nextBatch = UserInitSyncDiff(user);
}
sync.next_batch = nextBatch;
response = SyncResponseToJson(&sync);
SyncResponseFree(&sync);
(void) i;
finish:
FilterDestroy(filterData);
UserUnlock(user);
(void) path;
return response;
}

View file

@ -28,10 +28,52 @@
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <User.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <Config.h>
#include <User.h>
#include <Room.h>
static void
SendMembership(Db *db, User *user)
{
char *displayname = UserGetProfile(user, "displayname");
char *avatar_url = UserGetProfile(user, "avatar_url");
Array *joins = UserListJoins(user);
size_t i;
char *server_name = ConfigGetServerName(db);
CommonID *user_id = UserIdParse(UserGetName(user), server_name);
char *sender = ParserRecomposeCommonID(*user_id);
Free(server_name);
for (i = 0; i < ArraySize(joins); i++)
{
char *room_id = ArrayGet(joins, i);
Room *room = RoomLock(db, room_id);
HashMap *content = HashMapCreate();
HashMap *membership = RoomEventCreate(
sender, "m.room.member", sender,
content, NULL
);
HashMapSet(content, "membership", JsonValueString("join"));
HashMapSet(content, "displayname", JsonValueString(displayname));
HashMapSet(content, "avatar_url", JsonValueString(avatar_url));
JsonFree(RoomEventSend(room, membership, NULL));
JsonFree(membership);
RoomUnlock(room);
}
UserFreeList(joins);
UserIdFree(user_id);
Free(sender);
}
ROUTE_IMPL(RouteUserProfile, path, argp)
{
RouteArgs *args = argp;
@ -142,6 +184,8 @@ ROUTE_IMPL(RouteUserProfile, path, argp)
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
/* TODO: This may change in the future. */
entry = ArrayGet(path, 1);
if (StrEquals(entry, "displayname") ||
StrEquals(entry, "avatar_url"))
@ -153,6 +197,7 @@ ROUTE_IMPL(RouteUserProfile, path, argp)
/* TODO: Make UserSetProfile notify other parties of
* the change */
UserSetProfile(user, entry, value);
SendMembership(db, user);
response = HashMapCreate();
goto finish;
}

View file

@ -37,6 +37,9 @@ ROUTE_IMPL(RouteVersions, path, argp)
(void) argp;
#define DECLARE_SPEC_VERSION(x) ArrayAdd(versions, JsonValueString(x))
DECLARE_SPEC_VERSION("v1.0");
DECLARE_SPEC_VERSION("v1.1");
DECLARE_SPEC_VERSION("v1.2");
DECLARE_SPEC_VERSION("v1.3");
DECLARE_SPEC_VERSION("v1.4");

View file

@ -26,26 +26,326 @@
#include <State.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Base64.h>
#include <Cytoplasm/Array.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Sha.h>
#include <string.h>
#include <Room.h>
#include <Event.h>
#include <Room.h>
static HashMap *
StateResolveV1(Array * states)
struct State {
/* A hashmap of event IDs for a state */
HashMap *table;
};
static State *
InitialiseState(void)
{
(void) states;
return NULL;
State *ret = Malloc(sizeof(*ret));
ret->table = HashMapCreate();
return ret;
}
static HashMap *
int
V1Cmp(void *a, void *b)
{
HashMap *e1 = a, *e2 = b;
int64_t depth1, depth2;
depth1 =
JsonValueAsInteger(JsonGet(e1, 1, "depth"));
depth2 =
JsonValueAsInteger(JsonGet(e2, 1, "depth"));
if (depth1 > depth2)
{
return 1;
}
else if (depth1 < depth2)
{
return -1;
}
else
{
char *e1id =
JsonValueAsString(JsonGet(e1, 1, "event_id"));
char *e2id =
JsonValueAsString(JsonGet(e2, 1, "event_id"));
/* Matrix, *seriously*? */
unsigned char *sha1 = Sha1(e1id);
unsigned char *sha2 = Sha1(e2id);
char *str1 = ShaToHex(sha1, HASH_SHA1);
char *str2 = ShaToHex(sha2, HASH_SHA1);
int ret = strcmp(str1, str2) * -1;
Free(str1);
Free(str2);
Free(sha1);
Free(sha2);
/* Descending */
return ret;
}
}
static char *
EncodeStateTuple(char *type, char *stateKey)
{
size_t typeLen = type ? strlen(type) : 0;
size_t stateKeyLen = stateKey ? strlen(stateKey) : 0;
size_t length = typeLen + 1 + stateKeyLen + 1;
char *tuple = Malloc(length);
char *base64;
memset(tuple, '\0', length);
memcpy(tuple, type, typeLen);
memcpy(tuple + typeLen + 1, stateKey, stateKeyLen);
base64 = Base64Encode((const char *) tuple, length);
Free(tuple);
return base64;
}
static void
DecodeStateTuple(char *base64, char **key, char **val)
{
size_t base64Size = base64 ? strlen(base64) : 0;
char *decodeBuffer = NULL;
if (!base64 || !key || !val)
{
return;
}
decodeBuffer = Base64Decode((const char *) base64, base64Size);
*key = StrDuplicate(decodeBuffer);
*val = StrDuplicate(decodeBuffer + strlen(decodeBuffer) + 1);
Free(decodeBuffer);
}
static void
BuildBaseAndConflictV1(Room *room, Array *states, State *R, HashMap *conflicts)
{
size_t i;
char *type = NULL, *key = NULL;
for (i = 0; i < ArraySize(states); i++)
{
char *event_id;
State *state = ArrayGet(states, i);
while (StateIterate(state, &type, &key, (void **) &event_id))
{
char *tuple = EncodeStateTuple(type, key);
if (StateGet(R, type, key) || HashMapGet(conflicts, tuple))
{
Array *arr;
HashMap *hm;
/* Conflicts! */
StateSet(R, type, key, NULL);
arr = HashMapGet(conflicts, tuple);
if (!arr)
{
arr = ArrayCreate();
}
hm = RoomEventFetch(room, event_id, false);
ArrayAdd(arr, hm);
HashMapSet(conflicts, tuple, arr);
}
else
{
/* Add to R */
StateSet(R, type, key, event_id);
}
Free(tuple);
Free(type);
Free(key);
}
}
}
static void
FixoutConflictV1(char *t, Room *room, State *R, HashMap *conflicts)
{
HashMap *first;
Array *state_keys, *conflicting, *events;
char *tuple;
size_t i;
events = ArrayCreate();
while (HashMapIterate(conflicts, &tuple, (void **) &conflicting))
{
char *type, *key;
DecodeStateTuple(tuple, &type, &key);
if (StrEquals(type, t))
{
for (i = 0; i < ArraySize(conflicting); i++)
{
HashMap *event = ArrayGet(conflicting, i);
ArrayAdd(events, event);
}
}
Free(type);
Free(key);
}
ArraySort(events, V1Cmp);
/* Add first event. */
first = ArrayDelete(events, 0);
StateSet(
R,
JsonValueAsString(JsonGet(first, 1, "type")),
JsonValueAsString(JsonGet(first, 1, "state_key")),
JsonValueAsString(JsonGet(first, 1, "event_id"))
);
JsonFree(first);
for (i = 0; i < ArraySize(events); i++)
{
HashMap *event = ArrayGet(events, i);
PduV1 pdu;
char *msg;
PduV1FromJson(event, &pdu, &msg);
if (RoomAuthoriseEventV1(room, pdu, R, NULL))
{
StateSet(R, pdu.type, pdu.state_key, pdu.event_id);
}
else
{
PduV1Free(&pdu);
JsonFree(event);
break;
}
(void) msg;
PduV1Free(&pdu);
JsonFree(event);
}
ArrayFree(events);
state_keys = ArrayCreate();
while (HashMapIterate(conflicts, &tuple, (void **) &conflicting))
{
char *type, *key;
DecodeStateTuple(tuple, &type, &key);
if (StrEquals(type, t))
{
ArrayAdd(state_keys, key);
}
else
{
Free(key);
}
Free(type);
}
for (i = 0; i < ArraySize(state_keys); i++)
{
char *state_key = ArrayGet(state_keys, i);
char *tuple = EncodeStateTuple(t, state_key);
Array *conflict = HashMapDelete(conflicts, tuple);
/* All of the other values are already freed */
ArrayFree(conflict);
Free(state_key);
Free(tuple);
}
ArrayFree(state_keys);
}
static State *
StateResolveV1(Room * room, Array * states)
{
State *R = InitialiseState();
HashMap *conflicts = HashMapCreate();
Array *events = NULL, *conflicting = NULL;
char *type, *key, *tuple;
BuildBaseAndConflictV1(room, states, R, conflicts);
/* R and conflicts are now configured */
FixoutConflictV1("m.room.power_levels", room, R, conflicts);
FixoutConflictV1("m.room.join_rules", room, R, conflicts);
FixoutConflictV1("m.room.member", room, R, conflicts);
while (HashMapIterate(conflicts, &tuple, (void **) &conflicting))
{
ssize_t i;
DecodeStateTuple(tuple, &type, &key);
ArraySort(conflicting, V1Cmp);
for (i = ArraySize(conflicting) - 1; i >= 0; i--)
{
HashMap *event = ArrayGet(events, i);
PduV1 pdu = { 0 };
char *msg;
if (!PduV1FromJson(event, &pdu, &msg))
{
continue;
}
if (RoomAuthoriseEventV1(room, pdu, R, NULL))
{
StateSet(R, pdu.type, pdu.state_key, pdu.event_id);
PduV1Free(&pdu);
break;
}
(void) msg;
PduV1Free(&pdu);
}
Free(type);
Free(key);
}
while (HashMapIterate(conflicts, &tuple, (void **) &conflicting))
{
size_t i;
for (i = 0; i < ArraySize(conflicting); i++)
{
JsonFree(ArrayGet(conflicting, i));
}
ArrayFree(conflicting);
}
HashMapFree(conflicts);
return R;
}
static State *
StateResolveV2(Array * states)
{
(void) states;
return NULL;
}
HashMap *
static State *
StateFromPrevs(Room *room, Array *states)
{
switch (RoomVersionGet(room))
{
case 1:
return StateResolveV1(room, states);
default:
return StateResolveV2(states);
}
}
static bool
IsRejected(HashMap *pdu)
{
JsonValue *val = JsonGet(pdu, 2, "unsigned", "status");
return StrEquals(JsonValueAsString(val), "rejected");
}
State *
StateResolve(Room * room, HashMap * event)
{
Array *states;
@ -53,36 +353,252 @@ StateResolve(Room * room, HashMap * event)
Array *prevEvents;
State *ret_state;
char *room_id, *event_id;
Db *db;
if (!room || !event)
{
return NULL;
}
/* TODO: Return cached state if it exists */
db = RoomGetDB(room);
room_id = JsonValueAsString(HashMapGet(event, "room_id"));
event_id = JsonValueAsString(HashMapGet(event, "event_id"));
if (!room_id || !event_id)
{
return NULL;
}
if (DbExists(db, 4, "rooms", room_id, "state", event_id))
{
DbRef *ref = DbLock(db, 4,
"rooms", room_id, "state", event_id
);
ret_state = StateDeserialise(DbJson(ref));
DbUnlock(db, ref);
if (ret_state)
{
return ret_state;
}
/* If a DB error stops us from getting an existing state,
* recompute it. */
}
states = ArrayCreate();
if (!states)
{
return NULL;
}
prevEvents = HashMapGet(event, "prev_events");
prevEvents = JsonValueAsArray(HashMapGet(event, "prev_events"));
for (i = 0; i < ArraySize(prevEvents); i++)
{
HashMap *prevEvent = ArrayGet(prevEvents, i);
HashMap *state = StateResolve(room, prevEvent);
HashMap *prevEvent = RoomEventFetch(
room, JsonValueAsString(ArrayGet(prevEvents, i)),
false
);
State *state = StateResolve(room, prevEvent);
/* TODO: Apply prevEvent to state if it is a state event */
if (HashMapGet(prevEvent, "state_key") && !IsRejected(prevEvent))
{
StateSet(
state,
JsonValueAsString(HashMapGet(prevEvent, "type")),
JsonValueAsString(HashMapGet(prevEvent, "state_key")),
JsonValueAsString(HashMapGet(prevEvent, "event_id"))
);
}
ArrayAdd(states, state);
JsonFree(prevEvent);
}
ret_state = StateFromPrevs(room, states);
for (i = 0; i < ArraySize(states); i++)
{
State *state = ArrayGet(states, i);
StateFree(state);
}
ArrayFree(states);
if (ret_state)
{
HashMap *json = StateSerialise(ret_state);
DbRef *ref = DbCreate(db, 4, "rooms", room_id, "state", event_id);
DbJsonSet(ref, json);
JsonFree(json);
DbUnlock(db, ref);
}
return ret_state;
}
State *
StateCurrent(Room *room)
{
Array *prevEvents;
Array *states;
size_t i;
State *ret;
if (!room)
{
return NULL;
}
prevEvents = RoomPrevEventsGet(room);
states = ArrayCreate();
for (i = 0; i < ArraySize(prevEvents); i++)
{
HashMap *event =
JsonValueAsObject(ArrayGet(prevEvents, i));
State *state = StateResolve(room, event);
if (HashMapGet(event, "state_key") && !IsRejected(event))
{
StateSet(
state,
JsonValueAsString(HashMapGet(event, "type")),
JsonValueAsString(HashMapGet(event, "state_key")),
JsonValueAsString(HashMapGet(event, "event_id"))
);
}
ArrayAdd(states, state);
}
switch (RoomVersionGet(room))
ret = StateFromPrevs(room, states);
for (i = 0; i < ArraySize(states); i++)
{
case 1:
return StateResolveV1(states);
default:
return StateResolveV2(states);
State *state = ArrayGet(states, i);
StateFree(state);
}
ArrayFree(states);
return ret;
}
bool
StateIterate(State *state, char **type, char **key, void **event)
{
bool ret;
char *tuple;
if (!state || !type || !key || !event)
{
return false;
}
ret = HashMapIterate(state->table, &tuple, event);
if (ret)
{
DecodeStateTuple(tuple, type, key);
}
return ret;
}
char *
StateGet(State *state, char *type, char *key)
{
char *tableKey;
char *ret;
if (!state || !type || !key)
{
return NULL;
}
tableKey = EncodeStateTuple(type, key);
ret = HashMapGet(state->table, tableKey);
Free(tableKey);
return ret;
}
void
StateSet(State *state, char *type, char *key, char *event)
{
char *tableKey;
if (!state || !type || !key)
{
return;
}
tableKey = EncodeStateTuple(type, key);
Free(HashMapDelete(state->table, tableKey));
if (event)
{
HashMapSet(state->table, tableKey, StrDuplicate(event));
}
Free(tableKey);
}
void
StateFree(State *state)
{
char *tuple;
char *eventID;
if (!state)
{
return;
}
while (HashMapIterate(state->table, &tuple, (void **) &eventID))
{
Free(eventID);
}
HashMapFree(state->table);
Free(state);
}
State *
StateDeserialise(HashMap *json_state)
{
State *raw_state;
char *state_type;
JsonValue *state_keys;
if (!json_state)
{
return NULL;
}
raw_state = InitialiseState();
while (HashMapIterate(json_state, &state_type, (void **) &state_keys))
{
HashMap *state_keys_obj = JsonValueAsObject(state_keys);
char *state_key;
JsonValue *event_id;
while (HashMapIterate(state_keys_obj, &state_key, (void **) &event_id))
{
char *eid_string = JsonValueAsString(event_id);
StateSet(raw_state, state_type, state_key, eid_string);
}
}
return raw_state;
}
HashMap *
StateSerialise(State *rawState)
{
HashMap *returned;
char *type, *key, *event;
if (!rawState)
{
return NULL;
}
returned = HashMapCreate();
while (StateIterate(rawState, &type, &key, (void **) &event))
{
JsonSet(returned, JsonValueString(event), 2, type, key);
Free(type);
Free(key);
}
return returned;
}

View file

@ -312,14 +312,16 @@ UiaComplete(Array * flows, HttpServerContext * context, Db * db,
}
}
/* TODO: The type may sometimes be omitted. This means it is implied
* from context. */
val = HashMapGet(auth, "type");
if (!val || JsonValueType(val) != JSON_STRING)
{
msg = "'auth->type' is unset or not a string.";
HttpResponseStatus(context, HTTP_BAD_REQUEST);
*response = MatrixErrorCreate(M_BAD_JSON, msg);
ret = 0;
/* TODO: Clients may want to retrieve the flowlist.
* Still stupid, but eh. */
HttpResponseStatus(context, HTTP_UNAUTHORIZED);
ret = BuildResponse(flows, db, response, session, dbRef);
goto finish;
}

1355
src/User.c

File diff suppressed because it is too large Load diff

View file

@ -79,6 +79,16 @@
*/
extern int CanonicalJsonEncode(HashMap *, Stream *);
/**
* Computes a JSON object encoded as Canonical JSON's SHA-256
* hash.
*
* This function returns a SHA-256 hexstream stored on the heap,
* which will need to be freed with
* .Fn Free .
*/
extern unsigned char * CanonicalJsonHash(HashMap *);
/**
* Encode a JSON value following the rules of Canonical JSON.
* See the documentation for

View file

@ -98,6 +98,16 @@ extern void ConfigLock(Db *, Config *);
*/
extern int ConfigUnlock(Config *);
/**
* Returns the configuration's server name from a database.
* This is an utility function that masks behind
* .Fn ConfigLock ,
* and the value returned lives on the heap, and must be freed
* with
* .Fn Free .
*/
extern char * ConfigGetServerName(Db *);
/**
* Converts a ConfigLogLevel into a valid syslog level.
*/

View file

@ -28,6 +28,7 @@
#include <Schema/Filter.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Db.h>
/***
* @Nm Filter
@ -47,4 +48,22 @@
extern HashMap *
FilterApply(Filter *, HashMap *);
/**
* Verifies if a room would be filtered out by a filter given in.
*/
extern bool IsRoomFiltered(Filter *, char *);
/**
* Decodes a filter from a JSON stream to decode or an ID that was
* registered using the filter API, and may return an optional error
* string, if not set to NULL.
*/
extern Filter * FilterDecode(Db *, char *, char *, char **);
/**
* Frees all memory created by
* .Fn FilterDecode .
*/
extern void FilterDestroy(Filter *);
#endif /* TELODENDRIA_FILTER_H */

View file

@ -39,8 +39,9 @@
#include <Cytoplasm/HttpServer.h>
#include <Cytoplasm/HttpRouter.h>
#include <Cytoplasm/Log.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Log.h>
#include <Config.h>
#include <Cytoplasm/Db.h>
@ -154,4 +155,25 @@ extern HashMap * MatrixRateLimit(HttpServerContext *, Db *);
*/
extern HashMap * MatrixClientWellKnown(char *, char *);
/**
* Decodes a dot-separated path (as defined by the Specification) and
* tries to retrieve it from a JSON object hashmap, failing with NULL
* if it cannot be found, like Cytoplasm's
* .Fn JsonGet .
*/
extern JsonValue * MatrixGetJSON(HashMap *, char *);
/**
* Sets a value given a dot-separated path (see
* .Fn MatrixGetJSON
* for more information).
*/
extern JsonValue * MatrixSetJSON(HashMap *, char *, JsonValue *);
/**
* Traverses the entire JSON object and verifies if no floating-point
* values are present within it.
*/
extern bool MatrixCheckFloats(HashMap *);
#endif

View file

@ -41,17 +41,23 @@
#include <Cytoplasm/Db.h>
#include <Schema/RoomCreateRequest.h>
#include <Schema/PduV1.h>
#include <Parser.h>
#include <User.h>
/**
* The functions in this API operate on an opaque structure.
*/
typedef struct Room Room;
#include <State.h>
/**
* Create a new room in the given database using the given
* RoomCreateRequest.
*/
extern Room * RoomCreate(Db *, RoomCreateRequest *);
extern Room * RoomCreate(Db *, User *, RoomCreateRequest *, ServerPart);
/**
* Lock the existing room in the specified database,
@ -63,6 +69,11 @@ extern Room * RoomCreate(Db *, RoomCreateRequest *);
*/
extern Room * RoomLock(Db *, char *);
/**
* Returns the database structure a room is tied to.
*/
extern Db * RoomGetDB(Room *);
/**
* Unlock a room handle, returning it to the database.
* This function returns the result of calling
@ -89,13 +100,12 @@ extern char * RoomIdGet(Room *);
extern int RoomVersionGet(Room *);
/**
* Resolve the state for the latest events in the
* room. This function uses the appropriate state
* resolution algorithm to compute the latest state,
* which is used to select auth events on incoming
* client events.
* Resolves the room's state before a specific point,
* (with the event hashmap taking priority),
* like
* .Fn RoomStateGet .
*/
extern HashMap * RoomStateGet(Room *);
extern State * RoomStateGetID(Room *, char *);
/**
* Get a list of the most recent events in the
@ -126,6 +136,132 @@ extern int RoomPrevEventsSet(Room *, Array *);
* the room version, which includes setting the
* prev_events and auth_events fields correctly.
*/
extern HashMap * RoomEventSend(Room *, HashMap *);
extern HashMap * RoomEventSend(Room *, HashMap *, char **);
/**
* Sends an invite to a user in a room, and tries
* to notify such user of it.
*/
extern void RoomSendInvite(User *, bool, char *, Room *);
/**
* See if a user is allowed to see an event in a room,
* based on its visibility.
*/
extern bool RoomIsEventVisible(Room *, User *, char *);
/**
* Fetch a single event's PDU in a room into an
* hashmap, given an event ID, from the database
* if possible, or otherwise fetched from a remote
* homeserver participating in the room. If the boolean
* value is set, then the prev_content is set.
*/
extern HashMap * RoomEventFetch(Room *, char *, bool);
/**
* Behaves like
* .Fn RoomEventFetch ,
* but avoids doing any postprocessing.
*/
extern HashMap * RoomEventFetchRaw(Room *, char *);
/**
* Strips all the fields not required in a
* Matrix ClientEvent from a PDU object.
*/
extern HashMap * RoomEventClientify(HashMap *);
/**
* Verifies whenever an event(as a PDUv1) is
* authorised by a room.
*/
extern bool RoomAuthoriseEventV1(Room *, PduV1, State *, char **);
/**
* Gets the room's creator as a ServerPart. This value should
* not be freed, as it lives alongside the room itself
*/
extern ServerPart RoomGetCreator(Room *);
/**
* Puts a PDUv1 into the event list, while updating the leaf
* list.
*/
extern bool RoomAddEventV1(Room *, PduV1, PduV1Status);
/**
* Creates a barebones JSON object to be sent to
* .Fn RoomEventFetch .
*/
extern HashMap * RoomEventCreate(char *, char *, char *, HashMap *, char *);
/**
* Computes an approximation of the PDU depth by looking at
* the leaves stored in the room data.
*/
extern uint64_t RoomGetDepth(Room *);
/**
* Tries to find an alias from room alias to an ID stored
* on the heap, or NULL if it does not exist.
*/
extern char * RoomResolveAlias(Db *, char *);
/**
* Tries to resolve a list of aliases from a room ID into
* an array of strings stored on the heap.
*/
extern Array * RoomReverseAlias(Db *, char *);
/**
* Frees the array returned by
* .Fn RoomReverseAlias .
*/
extern void RoomFreeReverse(Array *);
/**
* Checks whenever a room contains a specific user.
*/
extern bool RoomContainsUser(Room *, char *);
/**
* Checks whenever an user can join a specific room,
* given it's permissions.
*/
extern bool RoomCanJoin(Room *, char *);
/**
* Makes a local user redact an event(from it's ID).
*/
extern char * RoomRedact(Room *, User *, char *, char *, char **);
/**
* Makes a local user join a room, and returns true if
* the room was joined.
*/
extern bool RoomJoin(Room *, User *, char **);
/**
* Makes a local user leave a room, and returns true if
* the room was left.
*/
extern bool RoomLeave(Room *, User *, char **);
/**
* Adds or overwrites a room alias.
*/
extern void RoomAddAlias(Db *, char *, char *, char *, char *);
/**
* Checks if a PDU has been qualified as 'soft-failed'.
*/
extern bool RoomIsSoftfailedV1(PduV1);
/**
* Checks if a PDU has been qualified as 'rejected'.
*/
extern bool RoomIsRejectedV1(PduV1);
#endif /* TELODENDRIA_ROOM_H */

View file

@ -77,6 +77,7 @@ ROUTE(RouteVersions);
ROUTE(RouteWellKnown);
ROUTE(RouteCapabilities);
ROUTE(RouteDevices);
ROUTE(RouteLogin);
ROUTE(RouteLogout);
ROUTE(RouteRegister);
@ -95,12 +96,24 @@ ROUTE(RouteStaticLogin);
ROUTE(RouteStaticResources);
ROUTE(RouteFilter);
ROUTE(RouteLocalData);
ROUTE(RouteProcControl);
ROUTE(RouteConfig);
ROUTE(RoutePrivileges);
ROUTE(RouteCreateRoom);
ROUTE(RouteSendEvent);
ROUTE(RouteRedact);
ROUTE(RouteSendState);
ROUTE(RouteJoinRoom);
ROUTE(RouteKickRoom);
ROUTE(RouteJoinRoomAlias);
ROUTE(RouteFetchEvent);
ROUTE(RouteJoinedRooms);
ROUTE(RouteSync);
ROUTE(RouteMessages);
ROUTE(RouteMembers);
ROUTE(RouteAliasDirectory);
ROUTE(RouteRoomAliases);
@ -109,6 +122,9 @@ ROUTE(RouteAdminDeactivate);
ROUTE(RouteAdminTokens);
ROUTE(RouteKeyQuery);
ROUTE(RoutePushrules);
ROUTE(RouteHierarchy);
#undef ROUTE
#endif

View file

@ -37,21 +37,60 @@
#include <Cytoplasm/HashMap.h>
/**
* An opaque structure to hold information about the state.
*/
typedef struct State State;
#include <Room.h>
/**
* Retrieve the value of a state tuple.
*/
extern char *StateGet(HashMap *, char *, char *);
extern char *StateGet(State *, char *, char *);
/**
* Set a state tuple to a value.
*/
extern char *StateSet(HashMap *, char *, char *, char *);
extern void StateSet(State *, char *, char *, char *);
/**
* Iterates through a statemap, with (type, key) -> event.
* The type and keys are stored on the heap, and will need
* to be freed.
* This function behaves like
* .Fn HashMapIterate .
*/
extern bool StateIterate(State *, char **, char **, void **);
/**
* Compute the room state before the specified event was sent.
*/
extern HashMap * StateResolve(Room *, HashMap *);
extern State * StateResolve(Room *, HashMap *);
/**
* Computes the current state from the room's leaves.
*/
extern State * StateCurrent(Room *);
/**
* Frees an entire state table from the heap.
*/
extern void StateFree(State *);
/**
* Deserialises a state map from JSON to the internal format
* used by this API.
*
* The returned value is independent from the JSON format,
* and should be freed with
* .Fn StateFree .
*/
extern State * StateDeserialise(HashMap *);
/**
* Serialises a state map from the internal format to JSON
* used for the database, for example
*/
extern HashMap * StateSerialise(State *);
#endif /* TELODENDRIA_STATE_H */

View file

@ -44,6 +44,8 @@
#include <Parser.h>
#include <Schema/KeyUpload.h>
#include <stdbool.h>
/**
@ -66,7 +68,8 @@ typedef enum UserPrivileges
USER_GRANT_PRIVILEGES = (1 << 3),
USER_PROC_CONTROL = (1 << 4),
USER_ALIAS = (1 << 5),
USER_ALL = ((1 << 6) - 1)
USER_APPSERVICE = (1 << 6),
USER_ALL = ((1 << 7) - 1),
} UserPrivileges;
/**
@ -91,6 +94,7 @@ typedef struct UserLoginInfo
char *refreshToken;
} UserLoginInfo;
/**
* Take a localpart and domain as separate parameters and validate them
* against the rules of the Matrix specification. The reasion the
@ -134,6 +138,13 @@ extern User * UserCreate(Db *, char *, char *);
*/
extern User * UserLock(Db *, char *);
/**
* Behaves like
* .Fn UserLock ,
* but tries to check from a CommonID, and verifies
* for the serverpart. */
extern User * UserLockID(Db *, CommonID *);
/**
* Take an access token, figure out what user it belongs to, and then
* returns a reference to that user. This function should be used by
@ -303,9 +314,236 @@ extern int UserDecodePrivilege(const char *);
*/
extern CommonID * UserIdParse(char *, char *);
/**
* Gets the reply sent to the user from a transaction ID, and an opaque
* ""path"" ID, which is unique per route.
*/
extern HashMap * UserGetTransaction(User *, char *, char *);
/**
* Stores a transaction for a path with a given response JSON.
*/
extern void UserSetTransaction(User *, char *, char *, HashMap *);
/**
* Puts a room (indexed by room ID) in a user's invite list.
*/
extern void UserAddInvite(User *, char *);
/**
* Removes a room from the user's invite list.
*/
extern void UserRemoveInvite(User *, char *);
/**
* Lists all the rooms a user is invited to into an array of
* strings(room ID) to be freed by
* .Fn UserFreeInvites .
*/
extern Array * UserListInvites(User *);
/**
* Puts a room (indexed by room ID) in a user's join list.
*/
extern void UserAddJoin(User *, char *);
/**
* Removes a room from the user's join list.
*/
extern void UserRemoveJoin(User *, char *);
/**
* Lists all the rooms a user has joined into an array of
* strings(room ID) to be freed by
* .Fn UserFreeJoins .
*/
extern Array * UserListJoins(User *);
/**
* Frees a join/invite list as created by
* .Fn UserListJoins
* or
* .Fn UserListInvites .
*/
extern void UserFreeList(Array *);
/**
* Frees the user's common ID and the memory allocated for it.
*/
extern void UserIdFree(CommonID *);
/**
* Initialises a sync diff table, and returns a next_batch parameter,
* which is stored on the heap.
*/
extern char * UserInitSyncDiff(User *);
/**
* Fills a sync table with initial sync information
*/
extern void UserFillSyncDiff(User *, char *);
/**
* Pushes an invite onto all sync diff tables.
*/
extern void UserPushInviteSync(User *, char *);
/**
* Pushes a join onto all sync diff tables.
*/
extern void UserPushJoinSync(User *, char *);
/**
* Pushes an event (ID) into every diff table of a user.
*/
extern void UserPushEvent(User *, HashMap *);
/**
* Pushes a global account data key into every diff table
* of a user.
*/
extern void UserPushAccountData(User *, char *);
/**
* Shows the invite list(as a room ID table) for an user
* and a specified diff table, to be freed by
* .Fn UserFreeList .
*/
extern Array * UserGetInvites(User *, char *);
/**
* Shows the global account data list for an user
* and a specified diff table, to be freed by
* .Fn UserFreeList .
*/
extern Array * UserGetAccountDataSync(User *, char *);
/**
* Shows a list of rooms for an user and a specified diff
* table, to be freed by
* .Fn UserFreeList .
*/
extern Array * UserGetJoins(User *, char *);
/**
* Get a list of event IDs for a diff table(and room ID),
* to be freed by
* .Fn UserFreeList .
*/
extern Array * UserGetEvents(User *, char *, char *);
/**
* Drops a sync diff, denoted by it's previous batch ID, if it
* exists.
*/
extern void UserDropSync(User *, char *);
/**
* Verifies if the 'sync' data is considered old, and can be
* reasonably removed from the database.
*/
extern bool UserIsSyncOld(User *, char *);
/**
* Verifies if a sync diff exists.
*/
extern bool UserSyncExists(User *, char *);
/**
* Creates a new "message token" start from a room and event ID,
* given a specified direction(with false<=>'b' and true<=>'f')
* to be used by /messages.
*/
extern char * UserNewMessageToken(User *, char *, char *);
/**
* Grabs events from a message token(with the correct direction)
* into an array of HashMaps(which store PDU objects).
* If the string pointer is not NULL, this function sets its
* value to a new message token(which is stored on the heap),
* if and only if, there are any new events to retrieve.
*
* If the counter is negative, then it is treated as a 'f',
* otherwise, it is treated as 'b'.
*/
extern Array * UserFetchMessages(User *, int, char *, char **);
/**
* Deletes a "message token".
*/
extern void UserFreeMessageToken(User *, char *);
/**
* Deletes temporary data about a specific user.
*/
extern void UserCleanTemporaryData(User *);
/**
* Initialise a temporary sync table to signal any /sync changes.
* This should be paired with a call to
* .Fn UserDestroyPushTable .
*/
extern void UserInitialisePushTable(void);
/**
* Notifies the user of a sync update.
*/
extern void UserNotifyUser(char *);
/**
* Notifies *all* users of a sync update. This is meant to be used
* in cases where Telodendria needs to gracefully close any longrunning
* /sync requests.
*/
extern void UserNotifyAll(Db *);
/**
* Waits for a notification for a specific user for a specific
* duration in milliseconds to wait (with 30000ms being the default,
* if the value is negative), and returns true if anything came up.
*/
extern bool UserAwaitNotification(char *, int);
/**
* Destroys the temporary push table created by
* .Fn UserInitialisePushTable .
*/
extern void UserDestroyPushTable(void);
/**
* Returns a user's account data by key into a JSON object living
* on the heap.
*/
extern HashMap * UserGetAccountData(User *, char *);
/**
* Replaces an account data entry.
*/
extern void UserSetAccountData(User *, char *, HashMap *);
/**
* Sets the device key list.
*/
extern void UserSetDeviceKeys(User *, DeviceKeys *);
/**
* Clears the fallback/one-time key list.
*/
extern void UserClearFallbackKeys(User *);
/**
* Adds a one-time/fallback key.
*/
extern void UserAddKey(User *, char *, JsonValue *, bool);
/**
* Generates a hashmap from algorithm to one-time key count as
* a pointer to the uint64_t.
* This is intended for /keys/upload. Please do not use this
* elsewhere */
extern HashMap * UserGetOnetimeCounts(User *);
#endif /* TELODENDRIA_USER_H */