telodendria/src/User.c
2024-08-14 21:36:51 +02:00

1801 lines
36 KiB
C

/*
* 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/Db.h>
#include <User.h>
#include <Cytoplasm/Util.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Sha.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Log.h>
#include <Parser.h>
#include <Room.h>
#include <pthread.h>
#include <string.h>
#include <errno.h>
struct User
{
Db *db;
DbRef *ref;
DbRef *inviteRef, *joinRef;
char *name;
char *deviceId;
};
static pthread_mutex_t pushLock;
static HashMap *pushTable = NULL;
bool
UserValidate(char *localpart, char *domain)
{
size_t maxLen = 255 - strlen(domain) - 1;
size_t i = 0;
while (localpart[i])
{
char c = localpart[i];
if (i > maxLen)
{
return false;
}
if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
(c == '.') || (c == '_') || (c == '=') || (c == '-') ||
(c == '/')))
{
return false;
}
i++;
}
return true;
}
bool
UserHistoricalValidate(char *localpart, char *domain)
{
size_t maxLen = 255 - strlen(domain) - 1;
size_t i = 0;
while (localpart[i])
{
char c = localpart[i];
if (i > maxLen)
{
return false;
}
if (!((c >= 0x21 && c <= 0x39) || (c >= 0x3B && c <= 0x7E)))
{
return false;
}
i++;
}
return true;
}
bool
UserExists(Db * db, char *name)
{
return DbExists(db, 2, "users", name);
}
User *
UserLock(Db * db, char *name)
{
User *user = NULL;
DbRef *ref = NULL;
if (!name || !UserExists(db, name))
{
return NULL;
}
ref = DbLock(db, 2, "users", name);
user = Malloc(sizeof(User));
user->db = db;
user->ref = ref;
user->name = StrDuplicate(name);
user->deviceId = NULL;
user->inviteRef = DbLock(db, 3, "users", user->name, "invites");
user->joinRef = DbLock(db, 3, "users", user->name, "joins");
return user;
}
User *
UserAuthenticate(Db * db, char *accessToken)
{
User *user;
DbRef *atRef;
char *userName;
char *deviceId;
uint64_t expires;
if (!db || !accessToken)
{
return NULL;
}
atRef = DbLock(db, 3, "tokens", "access", accessToken);
if (!atRef)
{
return NULL;
}
userName = JsonValueAsString(HashMapGet(DbJson(atRef), "user"));
deviceId = JsonValueAsString(HashMapGet(DbJson(atRef), "device"));
expires = JsonValueAsInteger(HashMapGet(DbJson(atRef), "expires"));
user = UserLock(db, userName);
if (!user)
{
DbUnlock(db, atRef);
return NULL;
}
if (expires && UtilTsMillis() >= expires)
{
UserUnlock(user);
DbUnlock(db, atRef);
return NULL;
}
user->deviceId = StrDuplicate(deviceId);
DbUnlock(db, atRef);
return user;
}
bool
UserUnlock(User * user)
{
bool ret;
Db *db;
DbRef *ref;
if (!user)
{
return false;
}
db = user->db;
ref = user->ref;
Free(user->name);
Free(user->deviceId);
ret = DbUnlock(db, ref) &&
DbUnlock(db, user->joinRef) &&
DbUnlock(db, user->inviteRef);
user->db = NULL;
user->ref = NULL;
Free(user);
return ret;
}
User *
UserCreate(Db * db, char *name, char *password)
{
User *user = NULL;
HashMap *json = NULL;
uint64_t ts = UtilTsMillis();
/* TODO: Put some sort of password policy(like for example at least
* 8 chars, or maybe check it's entropy)? */
if (!db ||
(name && UserExists(db, name)) ||
!password ||
!strlen(password))
{
/* User exists or cannot be registered, therefore, do NOT
* bother */
return NULL;
}
user = Malloc(sizeof(User));
user->db = db;
if (!name)
{
user->name = StrRandom(12);
}
else
{
user->name = StrDuplicate(name);
}
user->ref = DbCreate(db, 2, "users", user->name);
if (!user->ref)
{
/* The only scenario where I can see that occur is if for some
* strange reason, Db fails to create a file(e.g fs is full) */
Free(user->name);
Free(user);
return NULL;
}
UserSetPassword(user, password);
json = DbJson(user->ref);
HashMapSet(json, "createdOn", JsonValueInteger(ts));
HashMapSet(json, "deactivated", JsonValueBoolean(false));
user->inviteRef = DbCreate(db, 3, "users", user->name, "invites");
user->joinRef = DbCreate(db, 3, "users", user->name, "joins");
return user;
}
UserLoginInfo *
UserLogin(User * user, char *password, char *deviceId, char *deviceDisplayName,
int withRefresh)
{
DbRef *rtRef = NULL;
HashMap *devices;
HashMap *device;
UserLoginInfo *result;
if (!user || !password)
{
return NULL;
}
if (!UserCheckPassword(user, password) || UserDeactivated(user))
{
return NULL;
}
result = Malloc(sizeof(UserLoginInfo));
if (!result)
{
return NULL;
}
result->refreshToken = NULL;
if (!deviceId)
{
deviceId = StrRandom(10);
}
else
{
deviceId = StrDuplicate(deviceId);
}
/* Generate an access token */
result->accessToken = UserAccessTokenGenerate(user, deviceId, withRefresh);
UserAccessTokenSave(user->db, result->accessToken);
if (withRefresh)
{
result->refreshToken = StrRandom(64);
rtRef = DbCreate(user->db, 3, "tokens", "refresh", result->refreshToken);
HashMapSet(DbJson(rtRef), "refreshes",
JsonValueString(result->accessToken->string));
DbUnlock(user->db, rtRef);
}
devices = JsonValueAsObject(HashMapGet(DbJson(user->ref), "devices"));
if (!devices)
{
devices = HashMapCreate();
HashMapSet(DbJson(user->ref), "devices", JsonValueObject(devices));
}
device = JsonValueAsObject(HashMapGet(devices, deviceId));
if (device)
{
JsonValue *val;
val = HashMapDelete(device, "accessToken");
if (val)
{
DbDelete(user->db, 3, "tokens", "access", JsonValueAsString(val));
JsonValueFree(val);
}
val = HashMapDelete(device, "refreshToken");
if (val)
{
DbDelete(user->db, 3, "tokens", "refresh", JsonValueAsString(val));
JsonValueFree(val);
}
}
else
{
device = HashMapCreate();
HashMapSet(devices, deviceId, JsonValueObject(device));
if (deviceDisplayName)
{
HashMapSet(device, "displayName",
JsonValueString(deviceDisplayName));
}
}
Free(deviceId);
if (result->refreshToken)
{
HashMapSet(device, "refreshToken",
JsonValueString(result->refreshToken));
}
HashMapSet(device, "accessToken",
JsonValueString(result->accessToken->string));
return result;
}
char *
UserGetName(User * user)
{
return user ? user->name : NULL;
}
char *
UserGetDeviceId(User * user)
{
return user ? user->deviceId : NULL;
}
bool
UserCheckPassword(User * user, char *password)
{
HashMap *json;
char *storedHash;
char *salt;
unsigned char *hashBytes;
char *hashedPwd;
char *tmp;
bool result;
if (!user || !password)
{
return false;
}
json = DbJson(user->ref);
storedHash = JsonValueAsString(HashMapGet(json, "password"));
salt = JsonValueAsString(HashMapGet(json, "salt"));
if (!storedHash || !salt)
{
return false;
}
tmp = StrConcat(2, password, salt);
hashBytes = Sha256(tmp);
hashedPwd = ShaToHex(hashBytes);
Free(tmp);
Free(hashBytes);
result = StrEquals(hashedPwd, storedHash);
Free(hashedPwd);
return result;
}
bool
UserSetPassword(User * user, char *password)
{
HashMap *json;
unsigned char *hashBytes;
char *hash = NULL;
char *salt = NULL;
char *tmpstr = NULL;
if (!user || !password)
{
return false;
}
json = DbJson(user->ref);
salt = StrRandom(16);
tmpstr = StrConcat(2, password, salt);
hashBytes = Sha256(tmpstr);
hash = ShaToHex(hashBytes);
JsonValueFree(HashMapSet(json, "salt", JsonValueString(salt)));
JsonValueFree(HashMapSet(json, "password", JsonValueString(hash)));
Free(salt);
Free(hash);
Free(hashBytes);
Free(tmpstr);
return true;
}
bool
UserDeactivate(User * user, char * from, char * reason)
{
HashMap *json;
JsonValue *val;
if (!user)
{
return false;
}
/* By default, it's the target's username */
if (!from)
{
from = UserGetName(user);
}
json = DbJson(user->ref);
JsonValueFree(HashMapSet(json, "deactivated", JsonValueBoolean(true)));
val = JsonValueString(from);
JsonValueFree(JsonSet(json, val, 2, "deactivate", "by"));
if (reason)
{
val = JsonValueString(reason);
JsonValueFree(JsonSet(json, val, 2, "deactivate", "reason"));
}
return true;
}
bool
UserReactivate(User * user)
{
HashMap *json;
if (!user)
{
return false;
}
json = DbJson(user->ref);
JsonValueFree(HashMapSet(json, "deactivated", JsonValueBoolean(false)));
JsonValueFree(HashMapDelete(json, "deactivate"));
return true;
}
bool
UserDeactivated(User * user)
{
HashMap *json;
if (!user)
{
return true;
}
json = DbJson(user->ref);
return JsonValueAsBoolean(HashMapGet(json, "deactivated"));
}
HashMap *
UserGetDevices(User * user)
{
HashMap *json;
if (!user)
{
return NULL;
}
json = DbJson(user->ref);
return JsonValueAsObject(HashMapGet(json, "devices"));
}
UserAccessToken *
UserAccessTokenGenerate(User * user, char *deviceId, int withRefresh)
{
UserAccessToken *token;
if (!user || !deviceId)
{
return NULL;
}
token = Malloc(sizeof(UserAccessToken));
if (!token)
{
return NULL;
}
token->user = StrDuplicate(user->name);
token->deviceId = StrDuplicate(deviceId);
token->string = StrRandom(64);
if (withRefresh)
{
token->lifetime = 1000 * 60 * 60 * 24 * 7; /* 1 Week */
}
else
{
token->lifetime = 0;
}
return token;
}
bool
UserAccessTokenSave(Db * db, UserAccessToken * token)
{
DbRef *ref;
HashMap *json;
if (!token)
{
return false;
}
ref = DbCreate(db, 3, "tokens", "access", token->string);
if (!ref)
{
return false;
}
json = DbJson(ref);
HashMapSet(json, "user", JsonValueString(token->user));
HashMapSet(json, "device", JsonValueString(token->deviceId));
if (token->lifetime)
{
HashMapSet(json, "expires", JsonValueInteger(UtilTsMillis() + token->lifetime));
}
return DbUnlock(db, ref);
}
void
UserAccessTokenFree(UserAccessToken * token)
{
if (!token)
{
return;
}
Free(token->user);
Free(token->string);
Free(token->deviceId);
Free(token);
}
bool
UserDeleteToken(User * user, char *token)
{
char *username;
char *deviceId;
char *refreshToken;
Db *db;
DbRef *tokenRef;
HashMap *tokenJson;
HashMap *userJson;
HashMap *deviceObj;
JsonValue *deletedVal;
if (!user || !token)
{
return false;
}
db = user->db;
/* First check if the token even exists */
if (!DbExists(db, 3, "tokens", "access", token))
{
return false;
}
/* If it does, get it's username. */
tokenRef = DbLock(db, 3, "tokens", "access", token);
if (!tokenRef)
{
return false;
}
tokenJson = DbJson(tokenRef);
username = JsonValueAsString(HashMapGet(tokenJson, "user"));
deviceId = JsonValueAsString(HashMapGet(tokenJson, "device"));
if (!StrEquals(username, UserGetName(user)))
{
/* Token does not match user, do not delete it */
DbUnlock(db, tokenRef);
return false;
}
userJson = DbJson(user->ref);
deviceObj = JsonValueAsObject(HashMapGet(userJson, "devices"));
if (!deviceObj)
{
return false;
}
/* Delete refresh token, if present */
refreshToken = JsonValueAsString(JsonGet(deviceObj, 2, deviceId, "refreshToken"));
if (refreshToken)
{
DbDelete(db, 3, "tokens", "refresh", refreshToken);
}
/* Delete the device object */
deletedVal = HashMapDelete(deviceObj, deviceId);
if (!deletedVal)
{
return false;
}
JsonValueFree(deletedVal);
/* Delete the access token. */
if (!DbUnlock(db, tokenRef) || !DbDelete(db, 3, "tokens", "access", token))
{
return false;
}
return true;
}
char *
UserGetProfile(User * user, char *name)
{
HashMap *json = NULL;
if (!user || !name)
{
return NULL;
}
json = DbJson(user->ref);
return JsonValueAsString(JsonGet(json, 2, "profile", name));
}
void
UserSetProfile(User * user, char *name, char *val)
{
HashMap *json = NULL;
if (!user || !name || !val)
{
return;
}
json = DbJson(user->ref);
JsonValueFree(JsonSet(json, JsonValueString(val), 2, "profile", name));
}
bool
UserDeleteTokens(User * user, char *exempt)
{
HashMap *devices;
char *deviceId;
JsonValue *deviceObj;
if (!user)
{
return false;
}
devices = JsonValueAsObject(HashMapGet(DbJson(user->ref), "devices"));
if (!devices)
{
return false;
}
while (HashMapIterate(devices, &deviceId, (void **) &deviceObj))
{
HashMap *device = JsonValueAsObject(deviceObj);
char *accessToken = JsonValueAsString(HashMapGet(device, "accessToken"));
char *refreshToken = JsonValueAsString(HashMapGet(device, "refreshToken"));
if (exempt && (StrEquals(accessToken, exempt)))
{
continue;
}
if (accessToken)
{
DbDelete(user->db, 3, "tokens", "access", accessToken);
}
if (refreshToken)
{
DbDelete(user->db, 3, "tokens", "refresh", refreshToken);
}
JsonValueFree(HashMapDelete(devices, deviceId));
}
return true;
}
int
UserGetPrivileges(User * user)
{
if (!user)
{
return USER_NONE;
}
return UserDecodePrivileges(JsonValueAsArray(HashMapGet(DbJson(user->ref), "privileges")));
}
bool
UserSetPrivileges(User * user, int privileges)
{
JsonValue *val;
if (!user)
{
return false;
}
if (!privileges)
{
JsonValueFree(HashMapDelete(DbJson(user->ref), "privileges"));
return true;
}
val = JsonValueArray(UserEncodePrivileges(privileges));
if (!val)
{
return false;
}
JsonValueFree(HashMapSet(DbJson(user->ref), "privileges", val));
return true;
}
int
UserDecodePrivileges(Array * arr)
{
int privileges = USER_NONE;
size_t i;
if (!arr)
{
goto finish;
}
for (i = 0; i < ArraySize(arr); i++)
{
JsonValue *val = ArrayGet(arr, i);
if (!val || JsonValueType(val) != JSON_STRING)
{
continue;
}
privileges |= UserDecodePrivilege(JsonValueAsString(val));
}
finish:
return privileges;
}
int
UserDecodePrivilege(const char *p)
{
if (!p)
{
return USER_NONE;
}
else if (StrEquals(p, "ALL"))
{
return USER_ALL;
}
else if (StrEquals(p, "DEACTIVATE"))
{
return USER_DEACTIVATE;
}
else if (StrEquals(p, "ISSUE_TOKENS"))
{
return USER_ISSUE_TOKENS;
}
else if (StrEquals(p, "CONFIG"))
{
return USER_CONFIG;
}
else if (StrEquals(p, "GRANT_PRIVILEGES"))
{
return USER_GRANT_PRIVILEGES;
}
else if (StrEquals(p, "PROC_CONTROL"))
{
return USER_PROC_CONTROL;
}
else if (StrEquals(p, "ALIAS"))
{
return USER_ALIAS;
}
else
{
return USER_NONE;
}
}
Array *
UserEncodePrivileges(int privileges)
{
Array *arr = ArrayCreate();
if (!arr)
{
return NULL;
}
if ((privileges & USER_ALL) == USER_ALL)
{
ArrayAdd(arr, JsonValueString("ALL"));
goto finish;
}
#define A(priv, as) \
if ((privileges & priv) == priv) \
{ \
ArrayAdd(arr, JsonValueString(as)); \
}
A(USER_DEACTIVATE, "DEACTIVATE");
A(USER_ISSUE_TOKENS, "ISSUE_TOKENS");
A(USER_CONFIG, "CONFIG");
A(USER_GRANT_PRIVILEGES, "GRANT_PRIVILEGES");
A(USER_PROC_CONTROL, "PROC_CONTROL");
A(USER_ALIAS, "ALIAS");
#undef A
finish:
return arr;
}
CommonID *
UserIdParse(char *id, char *defaultServer)
{
CommonID *userId;
char *server;
if (!id)
{
return NULL;
}
id = StrDuplicate(id);
if (!id)
{
return NULL;
}
userId = Malloc(sizeof(CommonID));
if (!userId)
{
goto finish;
}
memset(userId, 0, sizeof(CommonID));
/* Fully-qualified user ID */
if (*id == '@')
{
if (!ParseCommonID(id, userId) || !userId->server.hostname)
{
UserIdFree(userId);
userId = NULL;
goto finish;
}
}
else
{
/* Treat it as just a localpart */
userId->local = StrDuplicate(id);
ParseServerPart(defaultServer, &userId->server);
}
server = ParserRecomposeServerPart(userId->server);
if (!UserHistoricalValidate(userId->local, server))
{
UserIdFree(userId);
userId = NULL;
}
Free(server);
finish:
Free(id);
return userId;
}
void
UserIdFree(CommonID * id)
{
if (id)
{
CommonIDFree(*id);
Free(id);
}
}
HashMap *
UserGetTransaction(User *user, char *transaction, char *path)
{
HashMap *devices;
if (!user || !transaction || !path)
{
return NULL;
}
devices = JsonValueAsObject(
HashMapGet(UserGetDevices(user), UserGetDeviceId(user))
);
return JsonDuplicate(JsonValueAsObject(
JsonGet(devices, 3, "transactions", path, transaction)
));
}
void
UserSetTransaction(User *user, char *transaction, char *path, HashMap *resp)
{
HashMap *devices;
if (!user || !transaction || !path || !resp)
{
return;
}
devices = JsonValueAsObject(
HashMapGet(UserGetDevices(user), UserGetDeviceId(user))
);
/* Overwrite the transaction */
JsonValueFree(JsonSet(
devices, JsonValueObject(JsonDuplicate(resp)),
3, "transactions", path, transaction
));
}
void
UserAddInvite(User *user, char *roomId)
{
HashMap *data;
if (!user || !roomId)
{
return;
}
data = DbJson(user->inviteRef);
JsonFree(HashMapSet(data, roomId, JsonValueNull()));
}
void
UserRemoveInvite(User *user, char *roomId)
{
HashMap *data;
if (!user || !roomId)
{
return;
}
data = DbJson(user->inviteRef);
JsonFree(HashMapDelete(data, roomId));
}
Array *
UserListInvites(User *user)
{
Array *arr;
HashMap *data;
size_t i;
if (!user)
{
return NULL;
}
data = DbJson(user->inviteRef);
arr = HashMapKeys(data);
for (i = 0; i < ArraySize(arr); i++)
{
ArraySet(arr, i, StrDuplicate(ArrayGet(arr, i)));
}
return arr;
}
void
UserAddJoin(User *user, char *roomId)
{
HashMap *data;
if (!user || !roomId)
{
return;
}
data = DbJson(user->joinRef);
if (!HashMapGet(data, roomId))
{
JsonFree(HashMapSet(data, roomId, JsonValueNull()));
}
UserNotifyUser(user);
}
void
UserRemoveJoin(User *user, char *roomId)
{
HashMap *data;
if (!user || !roomId)
{
return;
}
data = DbJson(user->joinRef);
JsonFree(HashMapDelete(data, roomId));
UserNotifyUser(user);
}
Array *
UserListJoins(User *user)
{
Array *arr;
HashMap *data;
size_t i;
if (!user)
{
return NULL;
}
data = DbJson(user->joinRef);
arr = HashMapKeys(data);
for (i = 0; i < ArraySize(arr); i++)
{
ArraySet(arr, i, StrDuplicate(ArrayGet(arr, i)));
}
return arr;
}
void
UserFreeList(Array *arr)
{
size_t i;
if (!arr)
{
return;
}
for (i = 0; i < ArraySize(arr); i++)
{
Free(ArrayGet(arr, i));
}
ArrayFree(arr);
}
char *
UserInitSyncDiff(User *user)
{
char *nextBatch;
DbRef *syncRef;
HashMap *data;
if (!user)
{
return NULL;
}
nextBatch = StrRandom(16);
syncRef = DbCreate(user->db, 4, "users", user->name, "sync", nextBatch);
if (!syncRef)
{
Free(nextBatch);
return NULL;
}
data = DbJson(syncRef);
HashMapSet(data, "nextBatch", JsonValueString(nextBatch));
HashMapSet(data, "invites", JsonValueArray(ArrayCreate()));
HashMapSet(data, "leaves", JsonValueArray(ArrayCreate()));
HashMapSet(data, "joins", JsonValueObject(HashMapCreate()));
HashMapSet(data, "creation", JsonValueInteger(UtilTsMillis()));
DbUnlock(user->db, syncRef);
return nextBatch;
}
void
UserPushInviteSync(User *user, char *roomId)
{
DbRef *syncRef;
HashMap *data;
Array *entries;
Array *invites;
size_t i;
if (!user || !roomId)
{
return;
}
entries = DbList(user->db, 3, "users", user->name, "sync");
for (i = 0; i < ArraySize(entries); i++)
{
char *entry = ArrayGet(entries, i);
syncRef = DbLock(user->db, 4, "users", user->name, "sync", entry);
data = DbJson(syncRef);
invites = JsonValueAsArray(HashMapGet(data, "invites"));
ArrayAdd(invites, JsonValueString(roomId));
DbUnlock(user->db, syncRef);
}
DbListFree(entries);
UserNotifyUser(user);
}
void
UserPushJoinSync(User *user, char *roomId)
{
DbRef *syncRef;
HashMap *data;
Array *entries;
HashMap *join;
size_t i;
if (!user || !roomId)
{
return;
}
entries = DbList(user->db, 3, "users", user->name, "sync");
for (i = 0; i < ArraySize(entries); i++)
{
char *entry = ArrayGet(entries, i);
HashMap *joinEntry;
syncRef = DbLock(user->db, 4, "users", user->name, "sync", entry);
data = DbJson(syncRef);
join = JsonValueAsObject(HashMapGet(data, "joins"));
/* TODO */
joinEntry = HashMapCreate();
HashMapSet(joinEntry, "timeline", JsonValueArray(ArrayCreate()));
if (!HashMapGet(join, roomId))
{
JsonFree(HashMapSet(join, roomId, JsonValueObject(joinEntry)));
}
else
{
JsonFree(joinEntry);
}
DbUnlock(user->db, syncRef);
}
DbListFree(entries);
UserNotifyUser(user);
}
void
UserPushEvent(User *user, HashMap *event)
{
DbRef *syncRef;
HashMap *data;
Array *entries;
HashMap *join;
size_t i;
char *roomId, *eventId;
if (!user || !event)
{
return;
}
roomId = JsonValueAsString(HashMapGet(event, "room_id"));
eventId = JsonValueAsString(HashMapGet(event, "event_id"));
UserPushJoinSync(user, roomId);
entries = DbList(user->db, 3, "users", user->name, "sync");
for (i = 0; i < ArraySize(entries); i++)
{
char *entry = ArrayGet(entries, i);
HashMap *joinEntry;
Array *timeline;
syncRef = DbLock(user->db, 4, "users", user->name, "sync", entry);
data = DbJson(syncRef);
join = JsonValueAsObject(HashMapGet(data, "joins"));
joinEntry = JsonValueAsObject(HashMapGet(join, roomId));
timeline = JsonValueAsArray(HashMapGet(joinEntry, "timeline"));
ArrayAdd(timeline, JsonValueString(eventId));
DbUnlock(user->db, syncRef);
}
DbListFree(entries);
UserNotifyUser(user);
}
void
UserDropSync(User *user, char *batch)
{
if (!user || !batch)
{
return;
}
DbDelete(user->db, 4, "users", user->name, "sync", batch);
}
Array *
UserGetInvites(User *user, char *batch)
{
DbRef *syncRef;
HashMap *data;
Array *keys;
size_t i;
if (!user || !batch)
{
return NULL;
}
syncRef = DbLock(user->db, 4, "users", user->name, "sync", batch);
if (!syncRef)
{
return NULL;
}
data = DbJson(syncRef);
keys = ArrayDuplicate(JsonValueAsArray(HashMapGet(data, "invites")));
for (i = 0; i < ArraySize(keys); i++)
{
char *str = JsonValueAsString(ArrayGet(keys, i));
ArraySet(keys, i, StrDuplicate(str));
}
DbUnlock(user->db, syncRef);
return keys;
}
void
UserFillSyncDiff(User *user, char *batch)
{
Array *joins;
size_t i;
DbRef *syncRef;
if (!user || !batch)
{
return;
}
joins = UserListJoins(user);
syncRef = DbLock(
user->db, 4, "users", user->name, "sync", batch
);
for (i = 0; i < ArraySize(joins); i++)
{
char *roomId = ArrayGet(joins, i);
HashMap *joinEntry;
HashMap *data = DbJson(syncRef);
HashMap *join = JsonValueAsObject(HashMapGet(data, "joins"));
Array *timeline = ArrayCreate();
Room *r = RoomLock(user->db, roomId);
Array *prevs = RoomPrevEventsGet(r);
size_t j;
for (j = 0; j < ArraySize(prevs); j++)
{
HashMap *e = JsonValueAsObject(ArrayGet(prevs, j));
/* TODO: Backfill the user a 'lil more. */
ArrayAdd(timeline, JsonValueDuplicate(HashMapGet(e, "event_id")));
}
RoomUnlock(r);
joinEntry = HashMapCreate();
HashMapSet(joinEntry, "timeline", JsonValueArray(timeline));
if (!HashMapGet(join, roomId))
{
JsonFree(HashMapSet(join, roomId, JsonValueObject(joinEntry)));
}
else
{
JsonFree(joinEntry);
}
}
UserFreeList(joins);
DbUnlock(user->db, syncRef);
}
Array *
UserGetJoins(User *user, char *batch)
{
Db *db;
DbRef *syncRef;
HashMap *data;
Array *keys;
size_t i;
if (!user || !batch)
{
return NULL;
}
db = user->db;
syncRef = DbLock(db, 4, "users", user->name, "sync", batch);
if (!syncRef)
{
return NULL;
}
data = DbJson(syncRef);
keys = HashMapKeys(JsonValueAsObject(HashMapGet(data, "joins")));
for (i = 0; i < ArraySize(keys); i++)
{
char *str = ArrayGet(keys, i);
ArraySet(keys, i, StrDuplicate(str));
}
DbUnlock(db, syncRef);
return keys;
}
Array *
UserGetEvents(User *user, char *batch, char *roomId)
{
DbRef *syncRef;
HashMap *data;
HashMap *joins;
Array *keys, *ret;
size_t i;
if (!user || !batch || !roomId)
{
return NULL;
}
syncRef = DbLock(user->db, 4, "users", user->name, "sync", batch);
if (!syncRef)
{
return NULL;
}
data = DbJson(syncRef);
joins = JsonValueAsObject(JsonGet(data, 2, "joins", roomId));
keys = (JsonValueAsArray(HashMapGet(joins, "timeline")));
ret = ArrayCreate();
for (i = 0; i < ArraySize(keys); i++)
{
char *str = JsonValueAsString(ArrayGet(keys, i));
ArrayAdd(ret, StrDuplicate(str));
}
DbUnlock(user->db, syncRef);
return ret;
}
char *
UserNewMessageToken(User *user, char *room, char *event)
{
DbRef *messageRef;
HashMap *json;
char *messageToken;
if (!user || !room || !event)
{
return NULL;
}
messageToken = StrRandom(16);
messageRef = DbCreate(user->db,
4, "users", user->name, "msg", messageToken
);
json = DbJson(messageRef);
HashMapSet(json, "room", JsonValueString(room));
HashMapSet(json, "from", JsonValueString(event));
DbUnlock(user->db, messageRef);
return messageToken;
}
Array *
UserFetchMessages(User *user, int n, char *token, char **next)
{
Array *messages = NULL;
Array *nexts = NULL;
DbRef *messageRef;
HashMap *json;
Room *room;
char *roomId;
size_t i;
bool limited = false;
bool dir = false;
if (!user || !token || n == 0)
{
return NULL;
}
if (n < 0)
{
n = -n;
dir = true;
}
messageRef = DbLock(user->db,
4, "users", user->name, "msg", token
);
json = DbJson(messageRef);
if (!messageRef)
{
/* Regenerate a new one */
return NULL;
}
roomId = JsonValueAsString(HashMapGet(json, "room"));
room = RoomLock(user->db, roomId);
/* TODO (very important): CHECK IF THE USER IS ABLE TO SEE
* HISTORY. THROUGHOUT THE STEPS HERE. */
if (!room)
{
DbUnlock(user->db, messageRef);
return NULL;
}
nexts = ArrayCreate();
messages = ArrayCreate();
/* A stack of elements to deal with the DAG. */
ArrayAdd(nexts,
StrDuplicate(JsonValueAsString(HashMapGet(json, "from")))
);
for (i = 0; i < (size_t) n && ArraySize(nexts); i++)
{
char *curr = ArrayDelete(nexts, ArraySize(nexts) - 1);
HashMap *event = RoomEventFetch(room, curr);
Array *prevEvents;
size_t j;
Free(curr);
/* Push event into our message list. */
ArrayAdd(messages, event);
prevEvents = JsonValueAsArray(HashMapGet(event, "prev_events"));
if (dir)
{
HashMap *unsign = JsonValueAsObject(HashMapGet(event, "unsigned"));
/* prevEvents is now nextEvents */
prevEvents = JsonValueAsArray(HashMapGet(unsign, "next_events"));
}
for (j = 0; j < ArraySize(prevEvents); j++)
{
char *prevEvent = JsonValueAsString(ArrayGet(prevEvents, j));
ArrayAdd(nexts, StrDuplicate(prevEvent));
}
if (ArraySize(prevEvents) == 0)
{
limited = true;
}
}
for (i = 0; i < ArraySize(nexts); i++)
{
Free(ArrayGet(nexts, i));
}
ArrayFree(nexts);
RoomUnlock(room);
if (next && !limited)
{
HashMap *lastMessage = ArrayGet(messages, ArraySize(messages) - 1);
char *eId = JsonValueAsString(HashMapGet(lastMessage, "event_id"));
*next = UserNewMessageToken(user, roomId, eId);
}
DbUnlock(user->db, messageRef);
for (i = 0; i < ArraySize(messages); i++)
{
HashMap *e = ArrayGet(messages, i);
ArraySet(messages, i, JsonValueObject(e));
}
return messages;
}
void
UserFreeMessageToken(User *user, char *token)
{
if (!user || !token)
{
return;
}
DbDelete(user->db,
4, "users", user->name, "msg", token
);
}
void
UserCleanTemporaryData(User *user)
{
Array *list;
size_t i;
if (!user)
{
return;
}
list = DbList(user->db, 3, "users", user->name, "msg");
for (i = 0; i < ArraySize(list); i++)
{
char *token = ArrayGet(list, i);
UserFreeMessageToken(user, token);
}
DbListFree(list);
list = DbList(user->db, 3, "users", user->name, "sync");
for (i = 0; i < ArraySize(list); i++)
{
char *token = ArrayGet(list, i);
if (UserIsSyncOld(user, token))
{
UserDropSync(user, token);
}
}
DbListFree(list);
}
bool
UserIsSyncOld(User *user, char *token)
{
DbRef *ref;
HashMap *map;
int64_t dt;
if (!user || !token)
{
return false;
}
ref = DbLock(user->db, 4, "users", user->name, "sync", token);
map = DbJson(ref);
dt = UtilTsMillis() - JsonValueAsInteger(HashMapGet(map, "creation"));
DbUnlock(user->db, ref);
return dt > (5 * 60 * 1000); /* 5-minutes of timeout. */
}
bool
UserSyncExists(User *user, char *sync)
{
if (!user || !sync)
{
return false;
}
return DbLock(user->db, 4, "users", user->name, "sync", sync);
}
typedef struct NotificationEntry {
enum {
NOTIF_AWAIT,
NOTIF_GOTTEN
} type;
pthread_mutex_t lock;
pthread_cond_t cond;
bool notified;
} NotificationEntry;
extern void
UserInitialisePushTable(void)
{
if (pushTable)
{
return;
}
pthread_mutex_init(&pushLock, NULL);
pthread_mutex_lock(&pushLock);
pushTable = HashMapCreate();
pthread_mutex_unlock(&pushLock);
}
void
UserNotifyUser(User *user)
{
NotificationEntry *entry;
if (!user || !pushTable)
{
return;
}
pthread_mutex_lock(&pushLock);
entry = HashMapGet(pushTable, user->name);
if (entry && entry->type == NOTIF_AWAIT)
{
pthread_mutex_lock(&entry->lock);
entry->notified = true;
pthread_cond_signal(&entry->cond);
pthread_mutex_unlock(&entry->lock);
pthread_mutex_unlock(&pushLock);
return;
}
else if (!entry)
{
entry = Malloc(sizeof(*entry));
entry->type = NOTIF_GOTTEN;
HashMapSet(pushTable, user->name, entry);
pthread_mutex_unlock(&pushLock);
return;
}
/* There was already a notification on the line. */
pthread_mutex_unlock(&pushLock);
}
void
UserDestroyPushTable(void)
{
if (!pushTable)
{
return;
}
pthread_mutex_lock(&pushLock);
HashMapFree(pushTable);
pushTable = NULL;
pthread_mutex_unlock(&pushLock);
pthread_mutex_destroy(&pushLock);
}
bool
UserAwaitNotification(char *user, int await)
{
NotificationEntry *entry, ownEntry;
struct timespec timeout;
int code;
bool timedout = false, notified = false;
if (!user)
{
return false;
}
if (await < 0)
{
/* 30 seconds */
await = 30000;
}
pthread_mutex_lock(&pushLock);
/* Check if we got any notifications yet. */
entry = HashMapGet(pushTable, user);
if (entry && entry->type == NOTIF_GOTTEN)
{
/* Got a notification entry already. */
Free(entry);
HashMapDelete(pushTable, user);
pthread_mutex_unlock(&pushLock);
return true;
}
else if (entry)
{
/* Another thread's awaiting... TODO: Manage these conditions. */
Log(LOG_ERR,
"Unimplemented feature: awaiting for other threads."
);
pthread_mutex_unlock(&pushLock);
return false;
}
/* No one's waiting or notifying; let's create our own entry,
* and await for something(NOTE that we're not allocating from
* the heap, since I've noticed some strange behaviour with
* Cytoplasm on ARM64... This is a hack and deserves to be fixed). */
entry = &ownEntry;
entry->type = NOTIF_AWAIT;
entry->notified = false;
pthread_mutex_init(&entry->lock, NULL);
pthread_cond_init(&entry->cond, NULL);
HashMapSet(pushTable, user, entry);
pthread_mutex_unlock(&pushLock);
/* Now, it's time for us to wait. */
clock_gettime(CLOCK_REALTIME, &timeout);
timeout.tv_sec += await / 1000;
timeout.tv_nsec += (await % 1000) * 1000000;
if (timeout.tv_nsec > 999999999)
{
uint64_t driftSeconds = timeout.tv_nsec / 1000000000;
uint64_t driftMicros = timeout.tv_nsec % 1000000000;
timeout.tv_nsec = driftMicros;
timeout.tv_sec += driftSeconds;
}
pthread_mutex_lock(&entry->lock);
while (!entry->notified)
{
code = pthread_cond_timedwait(&entry->cond, &entry->lock, &timeout);
if (code == ETIMEDOUT)
{
timedout = true;
break;
}
}
pthread_mutex_unlock(&entry->lock);
pthread_mutex_destroy(&entry->lock);
pthread_cond_destroy(&entry->cond);
notified = !timedout && entry->notified;
pthread_mutex_lock(&pushLock);
HashMapDelete(pushTable, user);
pthread_mutex_unlock(&pushLock);
return notified;
}